Compare commits
21 Commits
fac5e5a597
...
master
Author | SHA1 | Date | |
---|---|---|---|
2c5e0459ad | |||
f2c243ecf6 | |||
b424aafeab | |||
09511b37c9 | |||
c72353716f | |||
1cc941d893 | |||
88647b1c41 | |||
7438ba407a | |||
1c6c03cd41 | |||
32f96d488d | |||
630dbf0800 | |||
ac1d8cfab9 | |||
60b6d6f989 | |||
483773f042 | |||
56cd4c2db2 | |||
7720e74a3d | |||
885d2c0075 | |||
8073ed23c0 | |||
862f11d445 | |||
9408651ea8 | |||
75cd807187 |
@@ -4,4 +4,7 @@
|
|||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
<RootNamespace>AceFieldNewHorizon</RootNamespace>
|
<RootNamespace>AceFieldNewHorizon</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SimpleInjector" Version="5.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
@@ -19,8 +19,38 @@
|
|||||||
"buildTime": 3.0,
|
"buildTime": 3.0,
|
||||||
"allowedRotations": [0],
|
"allowedRotations": [0],
|
||||||
"layer": 1,
|
"layer": 1,
|
||||||
|
"size": [1, 1]
|
||||||
|
},
|
||||||
|
"turret": {
|
||||||
|
"scene": "res://Scenes/Tiles/TurretTile.tscn",
|
||||||
|
"cost": {
|
||||||
|
"stone": 10,
|
||||||
|
"ore_iron": 5
|
||||||
|
},
|
||||||
|
"durability": 200,
|
||||||
|
"buildTime": 5.0,
|
||||||
|
"allowedRotations": [0],
|
||||||
|
"layer": 1,
|
||||||
|
"size": [1, 1]
|
||||||
|
},
|
||||||
|
"reactor": {
|
||||||
|
"scene": "res://Scenes/Tiles/ReactorTile.tscn",
|
||||||
|
"cost": {},
|
||||||
|
"durability": 1000,
|
||||||
|
"buildTime": 5.0,
|
||||||
|
"allowedRotations": [0],
|
||||||
|
"layer": 1,
|
||||||
"size": [3, 3]
|
"size": [3, 3]
|
||||||
},
|
},
|
||||||
|
"enemy_portal": {
|
||||||
|
"scene": "res://Scenes/Tiles/EnemyPortalTile.tscn",
|
||||||
|
"cost": {},
|
||||||
|
"durability": 200,
|
||||||
|
"buildTime": 0.0,
|
||||||
|
"allowedRotations": [0],
|
||||||
|
"layer": 1,
|
||||||
|
"size": [1, 2]
|
||||||
|
},
|
||||||
"ground": {
|
"ground": {
|
||||||
"scene": "res://Scenes/Tiles/GroundTile.tscn",
|
"scene": "res://Scenes/Tiles/GroundTile.tscn",
|
||||||
"cost": {},
|
"cost": {},
|
||||||
|
BIN
Scenes/Entities/Bullet.png
Normal file
BIN
Scenes/Entities/Bullet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 659 KiB |
34
Scenes/Entities/Bullet.png.import
Normal file
34
Scenes/Entities/Bullet.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b5mb8tu15rc2p"
|
||||||
|
path="res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Scenes/Entities/Bullet.png"
|
||||||
|
dest_files=["res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
18
Scenes/Entities/Bullet.tscn
Normal file
18
Scenes/Entities/Bullet.tscn
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://erqawdsydh6a"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b5mb8tu15rc2p" path="res://Scenes/Entities/Bullet.png" id="1_fi8au"]
|
||||||
|
[ext_resource type="Script" uid="uid://vgx2a8gm7l8b" path="res://Scripts/Entities/Bullet.cs" id="1_k5b1m"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_fi8au"]
|
||||||
|
size = Vector2(44, 12)
|
||||||
|
|
||||||
|
[node name="Bullet" type="Area2D"]
|
||||||
|
collision_mask = 2
|
||||||
|
script = ExtResource("1_k5b1m")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
scale = Vector2(0.03, 0.03)
|
||||||
|
texture = ExtResource("1_fi8au")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
shape = SubResource("RectangleShape2D_fi8au")
|
BIN
Scenes/Entities/Enemy.png
Normal file
BIN
Scenes/Entities/Enemy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 807 KiB |
34
Scenes/Entities/Enemy.png.import
Normal file
34
Scenes/Entities/Enemy.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://x4u6oatvsm8y"
|
||||||
|
path="res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Scenes/Entities/Enemy.png"
|
||||||
|
dest_files=["res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
21
Scenes/Entities/Enemy.tscn
Normal file
21
Scenes/Entities/Enemy.tscn
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://b3ffcucytwmk"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cvsmy820b8dwl" path="res://Scripts/Entities/Enemy.cs" id="1_jajit"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://x4u6oatvsm8y" path="res://Scenes/Entities/Enemy.png" id="2_jajit"]
|
||||||
|
|
||||||
|
[node name="Enemy" type="CharacterBody2D"]
|
||||||
|
collision_layer = 2
|
||||||
|
collision_mask = 3
|
||||||
|
script = ExtResource("1_jajit")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionPolygon2D" parent="."]
|
||||||
|
polygon = PackedVector2Array(-2, -21, 3, -21, 24, 6, 24, 10, 20, 14, -21, 14, -24, 11, -24, 7)
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
scale = Vector2(0.05, 0.05)
|
||||||
|
texture = ExtResource("2_jajit")
|
||||||
|
|
||||||
|
[node name="AttackArea" type="Area2D" parent="."]
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionPolygon2D" parent="AttackArea"]
|
||||||
|
polygon = PackedVector2Array(-3, -24, 4, -24, 27, 5, 27, 14, 20, 19, -21, 19, -27, 14, -27, 5)
|
@@ -4,7 +4,9 @@
|
|||||||
[ext_resource type="Texture2D" uid="uid://jye6c2ehuxtg" path="res://Scenes/Entities/Player.png" id="1_ucweq"]
|
[ext_resource type="Texture2D" uid="uid://jye6c2ehuxtg" path="res://Scenes/Entities/Player.png" id="1_ucweq"]
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody2D"]
|
[node name="Player" type="CharacterBody2D"]
|
||||||
|
collision_mask = 3
|
||||||
script = ExtResource("1_08t41")
|
script = ExtResource("1_08t41")
|
||||||
|
MinZoom = 0.1
|
||||||
MaxZoom = 5.0
|
MaxZoom = 5.0
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
@@ -1,45 +1,30 @@
|
|||||||
[gd_scene load_steps=8 format=3 uid="uid://c22aprj452aha"]
|
[gd_scene load_steps=6 format=3 uid="uid://c22aprj452aha"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://cudpc3w17mbsw" path="res://Scripts/System/GridManager.cs" id="1_knkkn"]
|
|
||||||
[ext_resource type="Script" uid="uid://dfi2snip78eq6" path="res://Scripts/System/ResourceManager.cs" id="1_pl8e4"]
|
|
||||||
[ext_resource type="Script" uid="uid://cfbj72nm0eovg" path="res://Scripts/System/BuildingRegistry.cs" id="1_sxhdm"]
|
|
||||||
[ext_resource type="Script" uid="uid://cugfbvw70clgd" path="res://Scripts/System/NaturalResourceGenerator.cs" id="2_oss8w"]
|
[ext_resource type="Script" uid="uid://cugfbvw70clgd" path="res://Scripts/System/NaturalResourceGenerator.cs" id="2_oss8w"]
|
||||||
[ext_resource type="Script" uid="uid://bx1wj7gn6vrqe" path="res://Scripts/System/PlacementManager.cs" id="2_sxhdm"]
|
[ext_resource type="Script" uid="uid://bx1wj7gn6vrqe" path="res://Scripts/System/PlacementManager.cs" id="2_sxhdm"]
|
||||||
[ext_resource type="PackedScene" uid="uid://doxy60afddg1m" path="res://Scenes/Entities/Player.tscn" id="3_oss8w"]
|
[ext_resource type="PackedScene" uid="uid://doxy60afddg1m" path="res://Scenes/Entities/Player.tscn" id="3_oss8w"]
|
||||||
[ext_resource type="PackedScene" uid="uid://xwkplaxmye3v" path="res://Scenes/System/ItemPickup.tscn" id="7_is6ib"]
|
[ext_resource type="PackedScene" uid="uid://xwkplaxmye3v" path="res://Scenes/System/ItemPickup.tscn" id="7_is6ib"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://byv2vu0k2drdd" path="res://Scenes/System/HUD.tscn" id="8_hud_scene"]
|
||||||
|
|
||||||
[node name="Root" type="Node2D"]
|
[node name="Root" type="Node2D"]
|
||||||
|
|
||||||
[node name="ResourceSystem" type="Node" parent="."]
|
[node name="NaturalResourceGenerator" type="Node2D" parent="."]
|
||||||
script = ExtResource("1_pl8e4")
|
|
||||||
|
|
||||||
[node name="BuildingRegistry" type="Node" parent="."]
|
|
||||||
script = ExtResource("1_sxhdm")
|
|
||||||
|
|
||||||
[node name="NaturalResourceGenerator" type="Node2D" parent="." node_paths=PackedStringArray("Grid", "Registry")]
|
|
||||||
script = ExtResource("2_oss8w")
|
script = ExtResource("2_oss8w")
|
||||||
Grid = NodePath("../GridSystem")
|
|
||||||
Registry = NodePath("../BuildingRegistry")
|
|
||||||
|
|
||||||
[node name="GridSystem" type="Node2D" parent="."]
|
[node name="PlacementSystem" type="Node2D" parent="."]
|
||||||
script = ExtResource("1_knkkn")
|
|
||||||
|
|
||||||
[node name="PlacementSystem" type="Node2D" parent="." node_paths=PackedStringArray("Grid", "Inventory", "Registry")]
|
|
||||||
script = ExtResource("2_sxhdm")
|
script = ExtResource("2_sxhdm")
|
||||||
Grid = NodePath("../GridSystem")
|
|
||||||
Inventory = NodePath("../ResourceSystem")
|
|
||||||
Registry = NodePath("../BuildingRegistry")
|
|
||||||
|
|
||||||
[node name="Player" parent="." node_paths=PackedStringArray("Inventory") instance=ExtResource("3_oss8w")]
|
[node name="Player" parent="." instance=ExtResource("3_oss8w")]
|
||||||
scale = Vector2(0.35, 0.35)
|
scale = Vector2(0.35, 0.35)
|
||||||
Inventory = NodePath("../ResourceSystem")
|
|
||||||
|
[node name="HUD" parent="." instance=ExtResource("8_hud_scene")]
|
||||||
|
|
||||||
[node name="ItemPickup" parent="." instance=ExtResource("7_is6ib")]
|
[node name="ItemPickup" parent="." instance=ExtResource("7_is6ib")]
|
||||||
position = Vector2(-496, -245)
|
position = Vector2(-496, -245)
|
||||||
ItemId = "stone"
|
ItemId = "stone"
|
||||||
Infinite = true
|
Quantity = 64
|
||||||
|
|
||||||
[node name="ItemPickup2" parent="." instance=ExtResource("7_is6ib")]
|
[node name="ItemPickup2" parent="." instance=ExtResource("7_is6ib")]
|
||||||
position = Vector2(-495, 5)
|
position = Vector2(-495, 5)
|
||||||
ItemId = "ore_iron"
|
ItemId = "ore_iron"
|
||||||
Infinite = true
|
Quantity = 16
|
||||||
|
13
Scenes/System/HUD.tscn
Normal file
13
Scenes/System/HUD.tscn
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://byv2vu0k2drdd"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ddoqqcg77f60v" path="res://Scripts/System/Hud.cs" id="1_yw4ou"]
|
||||||
|
|
||||||
|
[node name="HUD" type="CanvasLayer"]
|
||||||
|
script = ExtResource("1_yw4ou")
|
||||||
|
|
||||||
|
[node name="ResourceDisplay" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 10.0
|
||||||
|
offset_top = 10.0
|
||||||
|
offset_right = 100.0
|
||||||
|
offset_bottom = 100.0
|
@@ -4,16 +4,23 @@
|
|||||||
[ext_resource type="Script" uid="uid://qgcue2doj2lf" path="res://Scripts/System/ItemPickup.cs" id="1_ps3kh"]
|
[ext_resource type="Script" uid="uid://qgcue2doj2lf" path="res://Scripts/System/ItemPickup.cs" id="1_ps3kh"]
|
||||||
|
|
||||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_4weev"]
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_4weev"]
|
||||||
size = Vector2(160, 160)
|
size = Vector2(50, 50)
|
||||||
|
|
||||||
[node name="ItemPickup" type="Node2D"]
|
[node name="ItemPickup" type="Area2D"]
|
||||||
script = ExtResource("1_ps3kh")
|
script = ExtResource("1_ps3kh")
|
||||||
|
MagnetRange = 96.0
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
scale = Vector2(0.05, 0.05)
|
scale = Vector2(0.05, 0.05)
|
||||||
texture = ExtResource("1_4weev")
|
texture = ExtResource("1_4weev")
|
||||||
|
|
||||||
[node name="Area2D" type="Area2D" parent="."]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
|
|
||||||
shape = SubResource("RectangleShape2D_4weev")
|
shape = SubResource("RectangleShape2D_4weev")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="."]
|
||||||
|
offset_left = -20.0
|
||||||
|
offset_right = 20.0
|
||||||
|
offset_bottom = 23.0
|
||||||
|
theme_override_font_sizes/font_size = 0
|
||||||
|
text = "x1"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
BIN
Scenes/Tiles/EnemyPortalTile.png
Normal file
BIN
Scenes/Tiles/EnemyPortalTile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 431 KiB |
34
Scenes/Tiles/EnemyPortalTile.png.import
Normal file
34
Scenes/Tiles/EnemyPortalTile.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dv2xwfyshxdtp"
|
||||||
|
path="res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Scenes/Tiles/EnemyPortalTile.png"
|
||||||
|
dest_files=["res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
23
Scenes/Tiles/EnemyPortalTile.tscn
Normal file
23
Scenes/Tiles/EnemyPortalTile.tscn
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://dup2su0s3ybcy"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyPortalTile.cs" id="1_o543x"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b3ffcucytwmk" path="res://Scenes/Entities/Enemy.tscn" id="2_nh7ff"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dv2xwfyshxdtp" path="res://Scenes/Tiles/EnemyPortalTile.png" id="3_i4us4"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_id484"]
|
||||||
|
size = Vector2(54, 79)
|
||||||
|
|
||||||
|
[node name="EnemyPortal" type="StaticBody2D"]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
script = ExtResource("1_o543x")
|
||||||
|
EnemyScene = ExtResource("2_nh7ff")
|
||||||
|
TileId = "enemy_portal"
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
scale = Vector2(0.1, 0.1)
|
||||||
|
texture = ExtResource("3_i4us4")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
position = Vector2(0, -0.5)
|
||||||
|
shape = SubResource("RectangleShape2D_id484")
|
@@ -8,6 +8,7 @@ size = Vector2(54, 54)
|
|||||||
|
|
||||||
[node name="GroundTile" type="StaticBody2D"]
|
[node name="GroundTile" type="StaticBody2D"]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
script = ExtResource("1_mqsaf")
|
script = ExtResource("1_mqsaf")
|
||||||
TileId = "ground"
|
TileId = "ground"
|
||||||
|
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
[gd_scene load_steps=4 format=3 uid="uid://cbu81slklwq3u"]
|
[gd_scene load_steps=5 format=3 uid="uid://cbu81slklwq3u"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://dyubkyqtpcg3a" path="res://Scripts/Tiles/MinerTile.cs" id="1_mecoy"]
|
[ext_resource type="Script" uid="uid://dyubkyqtpcg3a" path="res://Scripts/Tiles/MinerTile.cs" id="1_mecoy"]
|
||||||
[ext_resource type="Texture2D" uid="uid://bt6xmcgrbb078" path="res://Scenes/Tiles/MinerTile.png" id="2_mecoy"]
|
[ext_resource type="Texture2D" uid="uid://bt6xmcgrbb078" path="res://Scenes/Tiles/MinerTile.png" id="2_mecoy"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://xwkplaxmye3v" path="res://Scenes/System/ItemPickup.tscn" id="2_xhk0k"]
|
||||||
|
|
||||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"]
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"]
|
||||||
size = Vector2(54, 54)
|
size = Vector2(54, 54)
|
||||||
|
|
||||||
[node name="MinerTile" type="StaticBody2D"]
|
[node name="MinerTile" type="StaticBody2D"]
|
||||||
scale = Vector2(3, 3)
|
|
||||||
script = ExtResource("1_mecoy")
|
script = ExtResource("1_mecoy")
|
||||||
|
ItemPickup = ExtResource("2_xhk0k")
|
||||||
TileId = "miner"
|
TileId = "miner"
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
@@ -8,8 +8,9 @@ size = Vector2(54, 54)
|
|||||||
|
|
||||||
[node name="OreIronTile" type="StaticBody2D"]
|
[node name="OreIronTile" type="StaticBody2D"]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
script = ExtResource("1_exnim")
|
script = ExtResource("1_exnim")
|
||||||
TileId = "stone_iron"
|
TileId = "ore_iron"
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
position = Vector2(1.49012e-08, -9.53674e-07)
|
position = Vector2(1.49012e-08, -9.53674e-07)
|
||||||
|
BIN
Scenes/Tiles/ReactorTile.png
Normal file
BIN
Scenes/Tiles/ReactorTile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 KiB |
34
Scenes/Tiles/ReactorTile.png.import
Normal file
34
Scenes/Tiles/ReactorTile.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://fg03qxqphp7n"
|
||||||
|
path="res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Scenes/Tiles/ReactorTile.png"
|
||||||
|
dest_files=["res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
26
Scenes/Tiles/ReactorTile.tscn
Normal file
26
Scenes/Tiles/ReactorTile.tscn
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://w6ni678js7cu"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c4k3ottt7j3b1" path="res://Scripts/Tiles/ReactorTile.cs" id="1_yldg2"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://fg03qxqphp7n" path="res://Scenes/Tiles/ReactorTile.png" id="3_fk1vt"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"]
|
||||||
|
size = Vector2(54, 54)
|
||||||
|
|
||||||
|
[node name="ReactorTile" type="StaticBody2D"]
|
||||||
|
script = ExtResource("1_yldg2")
|
||||||
|
TileId = "reactor"
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
scale = Vector2(0.3, 0.3)
|
||||||
|
texture = ExtResource("3_fk1vt")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
scale = Vector2(3, 3)
|
||||||
|
shape = SubResource("RectangleShape2D_8o613")
|
||||||
|
|
||||||
|
[node name="ProgressOverlay" type="ColorRect" parent="."]
|
||||||
|
offset_left = -81.0
|
||||||
|
offset_top = -81.0
|
||||||
|
offset_right = -27.0
|
||||||
|
offset_bottom = -27.0
|
||||||
|
scale = Vector2(3, 3)
|
@@ -8,6 +8,7 @@ size = Vector2(54, 54)
|
|||||||
|
|
||||||
[node name="StoneTile" type="StaticBody2D"]
|
[node name="StoneTile" type="StaticBody2D"]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
script = ExtResource("1_rndy8")
|
script = ExtResource("1_rndy8")
|
||||||
TileId = "stone"
|
TileId = "stone"
|
||||||
|
|
||||||
|
BIN
Scenes/Tiles/TurretTile.png
Normal file
BIN
Scenes/Tiles/TurretTile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 448 KiB |
34
Scenes/Tiles/TurretTile.png.import
Normal file
34
Scenes/Tiles/TurretTile.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://ckssi7soymu7g"
|
||||||
|
path="res://.godot/imported/TurretTile.png-d55543f854deaa0fedf248ba979d1cb4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Scenes/Tiles/TurretTile.png"
|
||||||
|
dest_files=["res://.godot/imported/TurretTile.png-d55543f854deaa0fedf248ba979d1cb4.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
39
Scenes/Tiles/TurretTile.tscn
Normal file
39
Scenes/Tiles/TurretTile.tscn
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
[gd_scene load_steps=6 format=3 uid="uid://dbup2pvjl8het"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://n5g6i0uovxfk" path="res://Scripts/Tiles/TurretTile.cs" id="1_j3157"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://ckssi7soymu7g" path="res://Scenes/Tiles/TurretTile.png" id="2_7ljeh"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://erqawdsydh6a" path="res://Scenes/Entities/Bullet.tscn" id="2_gfad6"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dmbbkwgff7dej" path="res://Scenes/Tiles/TurretTileBarrel.png" id="3_gfad6"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_pndcb"]
|
||||||
|
size = Vector2(54, 54)
|
||||||
|
|
||||||
|
[node name="Turret" type="StaticBody2D"]
|
||||||
|
script = ExtResource("1_j3157")
|
||||||
|
BulletScene = ExtResource("2_gfad6")
|
||||||
|
BarrelTipPath = NodePath("Barrel/Marker2D")
|
||||||
|
TileId = "turret"
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
scale = Vector2(0.1, 0.1)
|
||||||
|
texture = ExtResource("2_7ljeh")
|
||||||
|
|
||||||
|
[node name="Barrel" type="Sprite2D" parent="."]
|
||||||
|
position = Vector2(0, -3)
|
||||||
|
scale = Vector2(0.08, 0.08)
|
||||||
|
texture = ExtResource("3_gfad6")
|
||||||
|
offset = Vector2(0, -54)
|
||||||
|
|
||||||
|
[node name="Marker2D" type="Marker2D" parent="Barrel"]
|
||||||
|
position = Vector2(0, -225)
|
||||||
|
scale = Vector2(12.5, 12.5)
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
shape = SubResource("RectangleShape2D_pndcb")
|
||||||
|
|
||||||
|
[node name="ProgressOverlay" type="ColorRect" parent="."]
|
||||||
|
visible = false
|
||||||
|
offset_left = -27.0
|
||||||
|
offset_top = -27.0
|
||||||
|
offset_right = 27.0
|
||||||
|
offset_bottom = 27.0
|
BIN
Scenes/Tiles/TurretTileBarrel.png
Normal file
BIN
Scenes/Tiles/TurretTileBarrel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 185 KiB |
34
Scenes/Tiles/TurretTileBarrel.png.import
Normal file
34
Scenes/Tiles/TurretTileBarrel.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dmbbkwgff7dej"
|
||||||
|
path="res://.godot/imported/TurretTileBarrel.png-a9f6a29579c44d5d6ed4cc8f6c09fd72.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Scenes/Tiles/TurretTileBarrel.png"
|
||||||
|
dest_files=["res://.godot/imported/TurretTileBarrel.png-a9f6a29579c44d5d6ed4cc8f6c09fd72.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
14
Scripts/AutoLoad/DIInitializer.cs
Normal file
14
Scripts/AutoLoad/DIInitializer.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Godot;
|
||||||
|
using AceFieldNewHorizon.Scripts.System;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.AutoLoad;
|
||||||
|
|
||||||
|
public partial class DIInitializer : Node
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// Initialize the Simple Injector container as early as possible
|
||||||
|
DependencyInjection.Initialize();
|
||||||
|
GD.Print("[DIInitializer] Dependency Injection container initialized via AutoLoad.");
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/AutoLoad/DIInitializer.cs.uid
Normal file
1
Scripts/AutoLoad/DIInitializer.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cr2a8w6ur4uei
|
97
Scripts/Entities/Bullet.cs
Normal file
97
Scripts/Entities/Bullet.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.Entities;
|
||||||
|
|
||||||
|
public partial class Bullet : Area2D
|
||||||
|
{
|
||||||
|
[Export] public float Speed = 400.0f;
|
||||||
|
[Export] public int Damage = 10;
|
||||||
|
[Export] public float MaxDistance = 1000.0f;
|
||||||
|
|
||||||
|
private Vector2 _direction = Vector2.Right;
|
||||||
|
private Vector2 _startPosition;
|
||||||
|
private float _distanceTraveled = 0f;
|
||||||
|
private bool _hasHit = false;
|
||||||
|
private uint _ignoreCollisionLayer = 0; // Layer to ignore (will be set by turret)
|
||||||
|
|
||||||
|
public void Initialize(Vector2 direction, Vector2 position, float rotation, uint ignoreLayer = 0)
|
||||||
|
{
|
||||||
|
_direction = direction.Normalized();
|
||||||
|
Position = position;
|
||||||
|
Rotation = rotation;
|
||||||
|
_startPosition = position;
|
||||||
|
_ignoreCollisionLayer = ignoreLayer;
|
||||||
|
|
||||||
|
// Connect the area entered signal
|
||||||
|
BodyEntered += OnBodyEntered;
|
||||||
|
AreaEntered += OnAreaEntered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _PhysicsProcess(double delta)
|
||||||
|
{
|
||||||
|
if (_hasHit) return;
|
||||||
|
|
||||||
|
// Move the bullet
|
||||||
|
var movement = _direction * Speed * (float)delta;
|
||||||
|
Position += movement;
|
||||||
|
_distanceTraveled += movement.Length();
|
||||||
|
|
||||||
|
// Check if bullet has traveled max distance
|
||||||
|
if (_distanceTraveled >= MaxDistance)
|
||||||
|
{
|
||||||
|
QueueFree();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBodyEntered(Node2D body)
|
||||||
|
{
|
||||||
|
HandleCollision(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAreaEntered(Area2D area)
|
||||||
|
{
|
||||||
|
HandleCollision(area);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCollision(Node2D node)
|
||||||
|
{
|
||||||
|
if (_hasHit) return;
|
||||||
|
|
||||||
|
// Skip collision if it's on the ignore layer
|
||||||
|
if (node is PhysicsBody2D physicsBody &&
|
||||||
|
(physicsBody.CollisionLayer & _ignoreCollisionLayer) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasHit = true;
|
||||||
|
|
||||||
|
// If we hit an enemy, deal damage
|
||||||
|
if (node is Enemy enemy)
|
||||||
|
{
|
||||||
|
// Get the global position where the bullet hit
|
||||||
|
var hitPosition = GlobalPosition;
|
||||||
|
enemy.TakeDamage(Damage, hitPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Add hit effect here
|
||||||
|
// CreateHitEffect();
|
||||||
|
|
||||||
|
// Remove the bullet
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateHitEffect()
|
||||||
|
{
|
||||||
|
// You can add a hit effect here if desired
|
||||||
|
// For example, a small explosion or impact sprite
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
// Clean up signal connections
|
||||||
|
BodyEntered -= OnBodyEntered;
|
||||||
|
AreaEntered -= OnAreaEntered;
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/Entities/Bullet.cs.uid
Normal file
1
Scripts/Entities/Bullet.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://vgx2a8gm7l8b
|
13
Scripts/Entities/Enemy.cs
Normal file
13
Scripts/Entities/Enemy.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
using Godot;
|
||||||
|
using AceFieldNewHorizon.Scripts.System;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.Entities;
|
||||||
|
|
||||||
|
public partial class Enemy : BaseEnemy
|
||||||
|
{
|
||||||
|
// All the base functionality is now in BaseEnemy
|
||||||
|
// This class is kept for backward compatibility and can be used to add
|
||||||
|
// specific behaviors for the basic enemy type if needed
|
||||||
|
}
|
1
Scripts/Entities/Enemy.cs.uid
Normal file
1
Scripts/Entities/Enemy.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cvsmy820b8dwl
|
283
Scripts/Entities/EnemyBase.cs
Normal file
283
Scripts/Entities/EnemyBase.cs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using AceFieldNewHorizon.Scripts.System;
|
||||||
|
using AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.Entities;
|
||||||
|
|
||||||
|
public abstract partial class BaseEnemy : CharacterBody2D
|
||||||
|
{
|
||||||
|
public const string EnemyGroupName = "Enemy";
|
||||||
|
|
||||||
|
[Export] public float MoveSpeed = 150.0f;
|
||||||
|
[Export] public float DetectionRadius = 300.0f;
|
||||||
|
[Export] public float AttackRadius = 80.0f;
|
||||||
|
[Export] public int Damage = 10;
|
||||||
|
[Export] public float AttackCooldown = 3.0f;
|
||||||
|
[Export] public int MaxHealth = 100;
|
||||||
|
[Export] public bool ShowDamageNumbers = true;
|
||||||
|
|
||||||
|
protected BaseTile TargetTile;
|
||||||
|
protected ReactorTile Reactor;
|
||||||
|
protected float AttackTimer = 0;
|
||||||
|
protected Area2D DetectionArea;
|
||||||
|
protected Area2D AttackArea;
|
||||||
|
protected int CurrentHealthValue;
|
||||||
|
protected ProgressBar HealthBar;
|
||||||
|
protected GridManager Grid;
|
||||||
|
|
||||||
|
// Track collisions with potential targets
|
||||||
|
protected readonly HashSet<BaseTile> CollidingTiles = [];
|
||||||
|
|
||||||
|
public int CurrentHealth
|
||||||
|
{
|
||||||
|
get => CurrentHealthValue;
|
||||||
|
protected set
|
||||||
|
{
|
||||||
|
CurrentHealthValue = Mathf.Clamp(value, 0, MaxHealth);
|
||||||
|
UpdateHealthBar();
|
||||||
|
|
||||||
|
if (CurrentHealthValue <= 0)
|
||||||
|
{
|
||||||
|
Die();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDead => CurrentHealthValue <= 0;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
CurrentHealthValue = MaxHealth;
|
||||||
|
Grid = DependencyInjection.Container.GetInstance<GridManager>();
|
||||||
|
Reactor = GetTree().GetFirstNodeInGroup(ReactorTile.ReactorGroupName) as ReactorTile;
|
||||||
|
|
||||||
|
InitializeHealthBar();
|
||||||
|
InitializeDetectionArea();
|
||||||
|
InitializeAttackArea();
|
||||||
|
|
||||||
|
AddToGroup(EnemyGroupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void InitializeHealthBar()
|
||||||
|
{
|
||||||
|
HealthBar = new ProgressBar
|
||||||
|
{
|
||||||
|
MaxValue = MaxHealth,
|
||||||
|
Value = CurrentHealthValue,
|
||||||
|
Size = new Vector2(40, 4),
|
||||||
|
ShowPercentage = false,
|
||||||
|
Visible = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var healthBarContainer = new Control();
|
||||||
|
healthBarContainer.AddChild(HealthBar);
|
||||||
|
AddChild(healthBarContainer);
|
||||||
|
healthBarContainer.Position = new Vector2(-20, -20);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void InitializeDetectionArea()
|
||||||
|
{
|
||||||
|
DetectionArea = new Area2D();
|
||||||
|
var collisionShape = new CollisionShape2D();
|
||||||
|
var shape = new CircleShape2D();
|
||||||
|
shape.Radius = DetectionRadius;
|
||||||
|
collisionShape.Shape = shape;
|
||||||
|
DetectionArea.AddChild(collisionShape);
|
||||||
|
AddChild(DetectionArea);
|
||||||
|
|
||||||
|
DetectionArea.BodyEntered += OnBodyEnteredDetection;
|
||||||
|
DetectionArea.BodyExited += OnBodyExitedDetection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void InitializeAttackArea()
|
||||||
|
{
|
||||||
|
AttackArea = GetNodeOrNull<Area2D>("AttackArea");
|
||||||
|
if (AttackArea != null)
|
||||||
|
{
|
||||||
|
AttackArea.BodyEntered += OnBodyEntered;
|
||||||
|
AttackArea.BodyExited += OnBodyExited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (IsDead) return;
|
||||||
|
|
||||||
|
if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree())
|
||||||
|
{
|
||||||
|
UpdateTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveTowardsTarget();
|
||||||
|
HandleAttacks(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void MoveTowardsTarget()
|
||||||
|
{
|
||||||
|
if (TargetTile != null)
|
||||||
|
{
|
||||||
|
var direction = GlobalPosition.DirectionTo(TargetTile.GlobalPosition);
|
||||||
|
Velocity = direction * MoveSpeed;
|
||||||
|
LookAt(TargetTile.GlobalPosition);
|
||||||
|
MoveAndSlide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void HandleAttacks(double delta)
|
||||||
|
{
|
||||||
|
if (AttackTimer > 0)
|
||||||
|
{
|
||||||
|
AttackTimer -= (float)delta;
|
||||||
|
}
|
||||||
|
else if (CollidingTiles.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var tile in CollidingTiles)
|
||||||
|
{
|
||||||
|
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
|
||||||
|
{
|
||||||
|
TryAttackTile(tile);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateTarget()
|
||||||
|
{
|
||||||
|
// If we have a valid target in collision, use that
|
||||||
|
foreach (var tile in CollidingTiles)
|
||||||
|
{
|
||||||
|
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
|
||||||
|
{
|
||||||
|
TargetTile = tile;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise find the reactor
|
||||||
|
if (Reactor != null && !Reactor.IsDestroyed && Reactor.IsInsideTree())
|
||||||
|
{
|
||||||
|
TargetTile = Reactor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TargetTile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void TryAttackTile(BaseTile tile)
|
||||||
|
{
|
||||||
|
if (IsDead || tile == null || tile.IsDestroyed || !tile.IsInsideTree())
|
||||||
|
return;
|
||||||
|
|
||||||
|
AttackTimer = AttackCooldown;
|
||||||
|
bool wasDestroyed = tile.TakeDamage(Damage);
|
||||||
|
GD.Print($"Attacking {tile.Name} for {Damage} damage. Was destroyed: {wasDestroyed}");
|
||||||
|
|
||||||
|
if (wasDestroyed)
|
||||||
|
{
|
||||||
|
CollidingTiles.Remove(tile);
|
||||||
|
if (TargetTile == tile)
|
||||||
|
{
|
||||||
|
TargetTile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnBodyEntered(Node2D body)
|
||||||
|
{
|
||||||
|
if (body is BaseTile { IsDestroyed: false } tile && !body.IsInGroup("Hostile"))
|
||||||
|
{
|
||||||
|
GD.Print($"[Enemy] {body.Name} Entered attack range");
|
||||||
|
CollidingTiles.Add(tile);
|
||||||
|
if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree())
|
||||||
|
{
|
||||||
|
TargetTile = tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack immediately on collision
|
||||||
|
TryAttackTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnBodyExited(Node2D body)
|
||||||
|
{
|
||||||
|
if (body.GetParent() is BaseTile tile)
|
||||||
|
{
|
||||||
|
CollidingTiles.Remove(tile);
|
||||||
|
if (TargetTile == tile)
|
||||||
|
{
|
||||||
|
TargetTile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnBodyEnteredDetection(Node2D body)
|
||||||
|
{
|
||||||
|
// Can be overridden by derived classes
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnBodyExitedDetection(Node2D body)
|
||||||
|
{
|
||||||
|
// Can be overridden by derived classes
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void TakeDamage(int damage, Vector2? hitPosition = null)
|
||||||
|
{
|
||||||
|
if (IsDead) return;
|
||||||
|
|
||||||
|
CurrentHealth -= damage;
|
||||||
|
|
||||||
|
// Show damage number (optional)
|
||||||
|
if (ShowDamageNumbers)
|
||||||
|
{
|
||||||
|
var damageLabel = new Label
|
||||||
|
{
|
||||||
|
Text = $"-{damage}",
|
||||||
|
Position = hitPosition ?? GlobalPosition,
|
||||||
|
ZIndex = 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
GetTree().CurrentScene.AddChild(damageLabel);
|
||||||
|
|
||||||
|
// Animate and remove damage number
|
||||||
|
var tween = CreateTween();
|
||||||
|
tween.TweenProperty(damageLabel, "position:y", damageLabel.Position.Y - 30, 0.5f);
|
||||||
|
tween.TweenCallback(Callable.From(() => damageLabel.QueueFree())).SetDelay(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
var originalModulate = Modulate;
|
||||||
|
Modulate = new Color(1, 0.5f, 0.5f); // Flash red
|
||||||
|
|
||||||
|
var tweenFlash = CreateTween();
|
||||||
|
tweenFlash.TweenProperty(this, "modulate", originalModulate, 0.2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateHealthBar()
|
||||||
|
{
|
||||||
|
if (HealthBar != null)
|
||||||
|
{
|
||||||
|
HealthBar.Value = CurrentHealthValue;
|
||||||
|
HealthBar.Visible = CurrentHealthValue < MaxHealth; // Only show when damaged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Die()
|
||||||
|
{
|
||||||
|
// Play death animation/sound
|
||||||
|
// You can add a death animation here
|
||||||
|
|
||||||
|
// Disable collisions and hide
|
||||||
|
SetProcess(false);
|
||||||
|
SetPhysicsProcess(false);
|
||||||
|
Hide();
|
||||||
|
|
||||||
|
// Queue free after a delay (for any death animation/sound to play)
|
||||||
|
var timer = new Timer();
|
||||||
|
AddChild(timer);
|
||||||
|
timer.Timeout += () => QueueFree();
|
||||||
|
timer.Start(0.5f);
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/Entities/EnemyBase.cs.uid
Normal file
1
Scripts/Entities/EnemyBase.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://6oduws4kbdlf
|
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using AceFieldNewHorizon.Scripts.System;
|
using AceFieldNewHorizon.Scripts.System;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ public partial class Player : CharacterBody2D
|
|||||||
[Export] public float ZoomDecay = 0.9f;
|
[Export] public float ZoomDecay = 0.9f;
|
||||||
[Export] public float ZoomSmoothing = 10.0f;
|
[Export] public float ZoomSmoothing = 10.0f;
|
||||||
|
|
||||||
[Export] public ResourceManager Inventory;
|
public ResourceManager Inventory { get; private set; }
|
||||||
|
|
||||||
private Camera2D _camera;
|
private Camera2D _camera;
|
||||||
private Vector2 _cameraTargetZoom = Vector2.One;
|
private Vector2 _cameraTargetZoom = Vector2.One;
|
||||||
@@ -33,7 +32,10 @@ public partial class Player : CharacterBody2D
|
|||||||
_camera = GetNode<Camera2D>("Camera2D");
|
_camera = GetNode<Camera2D>("Camera2D");
|
||||||
_cameraTargetZoom = _camera.Zoom;
|
_cameraTargetZoom = _camera.Zoom;
|
||||||
|
|
||||||
|
Inventory = DependencyInjection.Container.GetInstance<ResourceManager>();
|
||||||
|
|
||||||
AddToGroup(ItemPickup.PickupGroupName);
|
AddToGroup(ItemPickup.PickupGroupName);
|
||||||
|
AddToGroup(NaturalResourceGenerator.ChunkTrackerGroupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Input(InputEvent @event)
|
public override void _Input(InputEvent @event)
|
||||||
@@ -59,13 +61,9 @@ public partial class Player : CharacterBody2D
|
|||||||
|
|
||||||
// If same direction as last time, accelerate
|
// If same direction as last time, accelerate
|
||||||
if (direction == _lastZoomDirection && (currentTime - _lastZoomTime) < 300)
|
if (direction == _lastZoomDirection && (currentTime - _lastZoomTime) < 300)
|
||||||
{
|
|
||||||
_currentZoomSpeed = Mathf.Min(_currentZoomSpeed + ZoomAcceleration, MaxZoomSpeed);
|
_currentZoomSpeed = Mathf.Min(_currentZoomSpeed + ZoomAcceleration, MaxZoomSpeed);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
_currentZoomSpeed = BaseZoomSpeed;
|
_currentZoomSpeed = BaseZoomSpeed;
|
||||||
}
|
|
||||||
|
|
||||||
_lastZoomDirection = direction;
|
_lastZoomDirection = direction;
|
||||||
_lastZoomTime = currentTime;
|
_lastZoomTime = currentTime;
|
||||||
|
27
Scripts/Root.cs
Normal file
27
Scripts/Root.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Godot;
|
||||||
|
using AceFieldNewHorizon.Scripts.System;
|
||||||
|
using SimpleInjector;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts;
|
||||||
|
|
||||||
|
public partial class Root : Node
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// Dependency Injection container is now initialized via AutoLoad (DIInitializer.cs).
|
||||||
|
|
||||||
|
// Get references to the main system nodes from the scene tree
|
||||||
|
// and inject their dependencies.
|
||||||
|
// This assumes these nodes are direct children or easily accessible.
|
||||||
|
// You might need to adjust paths based on your scene setup.
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// var resourceManager = GetNode<ResourceManager>("ResourceSystem"); // Assuming ResourceManager is a child of Root
|
||||||
|
// DependencyInjection.Container.InjectProperties(resourceManager); // If ResourceManager had properties to inject
|
||||||
|
|
||||||
|
// For now, we'll manually resolve and assign for the main system nodes.
|
||||||
|
// The actual injection will happen in the _Ready methods of the system nodes themselves,
|
||||||
|
// by resolving from the static container.
|
||||||
|
// This is a common pattern when Godot instantiates the nodes.
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/Root.cs.uid
Normal file
1
Scripts/Root.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dmint8ii0oj5g
|
@@ -40,15 +40,15 @@ public record BuildingData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class BuildingRegistry : Node
|
public class BuildingRegistry
|
||||||
{
|
{
|
||||||
private Dictionary<string, BuildingData> _registry = new();
|
private Dictionary<string, BuildingData> _registry = new();
|
||||||
|
|
||||||
[Export] public string JsonPath { get; set; } = "res://Data/Buildings.json";
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
|
public BuildingRegistry(string jsonPath)
|
||||||
{
|
{
|
||||||
LoadFromJson(JsonPath);
|
LoadFromJson(jsonPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadFromJson(string path)
|
public void LoadFromJson(string path)
|
||||||
|
23
Scripts/System/DependencyInjection.cs
Normal file
23
Scripts/System/DependencyInjection.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Godot;
|
||||||
|
using Container = SimpleInjector.Container;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.System;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static Container Container { get; private set; }
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
Container = new Container();
|
||||||
|
|
||||||
|
// Register your system services here
|
||||||
|
// As singletons, since they are typically unique global managers
|
||||||
|
Container.RegisterSingleton<ResourceManager>();
|
||||||
|
Container.RegisterSingleton<GridManager>();
|
||||||
|
Container.RegisterSingleton(() => new BuildingRegistry("res://Data/Buildings.json"));
|
||||||
|
|
||||||
|
Container.Verify();
|
||||||
|
GD.Print("[DI] Simple Injector container initialized and verified.");
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/System/DependencyInjection.cs.uid
Normal file
1
Scripts/System/DependencyInjection.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://hppsxnesg0ys
|
@@ -43,15 +43,33 @@ public partial class GridManager : Node
|
|||||||
|
|
||||||
public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building)
|
public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building)
|
||||||
{
|
{
|
||||||
|
// Get all cells that should be occupied by this building
|
||||||
var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation);
|
var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation);
|
||||||
foreach (var cell in occupiedCells)
|
|
||||||
|
// Create a list to store cells that should be removed
|
||||||
|
var cellsToRemove = new List<Vector2I>();
|
||||||
|
|
||||||
|
// First, find all cells that match this building's position and size
|
||||||
|
foreach (var cell in _layers[layer].Keys.ToList())
|
||||||
|
{
|
||||||
|
var (building, buildingSize, buildingRotation) = _layers[layer][cell];
|
||||||
|
var buildingCells = GridUtils.GetOccupiedCells(cell, buildingSize, buildingRotation);
|
||||||
|
|
||||||
|
// If any of the building's cells match our target area, mark all of its cells for removal
|
||||||
|
if (buildingCells.Any(c => occupiedCells.Contains(c)))
|
||||||
|
{
|
||||||
|
cellsToRemove.AddRange(buildingCells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all marked cells
|
||||||
|
foreach (var cell in cellsToRemove.Distinct())
|
||||||
{
|
{
|
||||||
if (_layers[layer].ContainsKey(cell))
|
|
||||||
_layers[layer].Remove(cell);
|
_layers[layer].Remove(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Node2D? GetBuildingAtCell(Vector2I cell, GridLayer layer = GridLayer.Building)
|
public Node2D? GetTileAtCell(Vector2I cell, GridLayer layer = GridLayer.Building)
|
||||||
{
|
{
|
||||||
return _layers[layer].TryGetValue(cell, out var data) ? data.Building : null;
|
return _layers[layer].TryGetValue(cell, out var data) ? data.Building : null;
|
||||||
}
|
}
|
||||||
|
102
Scripts/System/Hud.cs
Normal file
102
Scripts/System/Hud.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using Godot;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.System;
|
||||||
|
|
||||||
|
public partial class Hud : CanvasLayer
|
||||||
|
{
|
||||||
|
private ResourceManager _resourceManager;
|
||||||
|
private VBoxContainer _resourceDisplay;
|
||||||
|
private readonly Dictionary<string, Label> _resourceLabels = new();
|
||||||
|
private VBoxContainer _pickupLogContainer;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
_resourceDisplay = GetNode<VBoxContainer>("ResourceDisplay");
|
||||||
|
|
||||||
|
_pickupLogContainer = new VBoxContainer();
|
||||||
|
_pickupLogContainer.Name = "PickupLogContainer";
|
||||||
|
_pickupLogContainer.SetAnchorsPreset(Control.LayoutPreset.BottomLeft);
|
||||||
|
_pickupLogContainer.OffsetLeft = 10;
|
||||||
|
_pickupLogContainer.OffsetBottom = -10; // Negative offset from bottom anchor
|
||||||
|
_pickupLogContainer.GrowVertical = Control.GrowDirection.Begin; // Make it grow upwards
|
||||||
|
AddChild(_pickupLogContainer);
|
||||||
|
|
||||||
|
_resourceManager = DependencyInjection.Container.GetInstance<ResourceManager>();
|
||||||
|
if (_resourceManager == null)
|
||||||
|
{
|
||||||
|
GD.PushError("ResourceSystem not found in the scene tree!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resourceManager.OnResourceChanged += UpdateResourceDisplay;
|
||||||
|
_resourceManager.OnResourcePickedUp += OnResourcePickedUp;
|
||||||
|
|
||||||
|
// Initialize display with current resources
|
||||||
|
foreach (var entry in _resourceManager.GetAllResources())
|
||||||
|
{
|
||||||
|
UpdateResourceDisplay(entry.Key, entry.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateResourceDisplay(string resourceId, int newAmount)
|
||||||
|
{
|
||||||
|
if (!_resourceLabels.TryGetValue(resourceId, out Label label))
|
||||||
|
{
|
||||||
|
label = new Label();
|
||||||
|
_resourceDisplay.AddChild(label);
|
||||||
|
_resourceLabels.Add(resourceId, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAmount <= 0)
|
||||||
|
{
|
||||||
|
// Remove label if resource amount is zero or less
|
||||||
|
if (label.GetParent() != null)
|
||||||
|
{
|
||||||
|
_resourceDisplay.RemoveChild(label);
|
||||||
|
}
|
||||||
|
_resourceLabels.Remove(resourceId);
|
||||||
|
label.QueueFree(); // Free the label from memory
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
label.Text = $"{resourceId}: {newAmount}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnResourcePickedUp(string resourceId, int amount)
|
||||||
|
{
|
||||||
|
AddPickupLogEntry($"Picked up {amount} {resourceId}!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPickupLogEntry(string message)
|
||||||
|
{
|
||||||
|
var logLabel = new Label();
|
||||||
|
logLabel.Text = message;
|
||||||
|
_pickupLogContainer.AddChild(logLabel);
|
||||||
|
|
||||||
|
var timer = new Timer();
|
||||||
|
timer.WaitTime = 3.0f;
|
||||||
|
timer.OneShot = true;
|
||||||
|
timer.Autostart = true;
|
||||||
|
logLabel.AddChild(timer); // Add timer as child of the label
|
||||||
|
timer.Timeout += () => OnLogEntryTimeout(logLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLogEntryTimeout(Label logLabel)
|
||||||
|
{
|
||||||
|
// Start fading out the label
|
||||||
|
var tween = logLabel.CreateTween();
|
||||||
|
tween.TweenProperty(logLabel, "modulate", new Color(1, 1, 1, 0), 0.5f); // Fade out over 0.5 seconds
|
||||||
|
tween.TweenCallback(Callable.From(() => logLabel.QueueFree())); // Free after fading
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (_resourceManager != null)
|
||||||
|
{
|
||||||
|
_resourceManager.OnResourceChanged -= UpdateResourceDisplay;
|
||||||
|
_resourceManager.OnResourcePickedUp -= OnResourcePickedUp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/System/Hud.cs.uid
Normal file
1
Scripts/System/Hud.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ddoqqcg77f60v
|
@@ -1,25 +1,124 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using AceFieldNewHorizon.Scripts.Entities;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace AceFieldNewHorizon.Scripts.System;
|
namespace AceFieldNewHorizon.Scripts.System;
|
||||||
|
|
||||||
public partial class ItemPickup : Node2D
|
public partial class ItemPickup : Area2D
|
||||||
{
|
{
|
||||||
|
[Signal]
|
||||||
|
public delegate void StackMergedEventHandler(string itemId, int totalQuantity);
|
||||||
|
|
||||||
public const string PickupGroupName = "ItemPickupTarget";
|
public const string PickupGroupName = "ItemPickupTarget";
|
||||||
|
|
||||||
[Export] public string ItemId { get; set; } = "";
|
[Export] public string ItemId { get; set; } = "";
|
||||||
[Export] public int Quantity { get; set; } = 1;
|
[Export] public int Quantity { get; set; } = 1;
|
||||||
[Export] public bool Infinite { get; set; } = false;
|
[Export] public bool Infinite { get; set; } = false;
|
||||||
|
[Export] public float MagnetRange { get; set; } = 64f;
|
||||||
|
|
||||||
|
public int GetItemQuantity(string itemId)
|
||||||
|
{
|
||||||
|
if (itemId == ItemId)
|
||||||
|
return Quantity;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetItemQuantity(string itemId, int newQuantity)
|
||||||
|
{
|
||||||
|
if (itemId == ItemId)
|
||||||
|
{
|
||||||
|
Quantity = newQuantity;
|
||||||
|
// Update the quantity label if it exists
|
||||||
|
if (_quantityLabel != null)
|
||||||
|
{
|
||||||
|
if (Quantity > 1)
|
||||||
|
_quantityLabel.Text = Quantity.ToString();
|
||||||
|
else
|
||||||
|
_quantityLabel.Text = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasItem(string itemId)
|
||||||
|
{
|
||||||
|
return itemId == ItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddItem(string itemId, int amount)
|
||||||
|
{
|
||||||
|
if (itemId == ItemId)
|
||||||
|
{
|
||||||
|
Quantity += amount;
|
||||||
|
// Update the quantity label if it exists
|
||||||
|
if (_quantityLabel != null)
|
||||||
|
{
|
||||||
|
if (Quantity > 1)
|
||||||
|
_quantityLabel.Text = Quantity.ToString();
|
||||||
|
else
|
||||||
|
_quantityLabel.Text = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Sprite2D _sprite;
|
private Sprite2D _sprite;
|
||||||
|
private Label _quantityLabel;
|
||||||
|
private Sprite2D _shadowSprite;
|
||||||
|
private Node2D _playerTarget;
|
||||||
|
|
||||||
// Called when the node enters the scene tree
|
// Called when the node enters the scene tree
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
var area = GetNode<Area2D>("Area2D");
|
BodyEntered += OnEntered;
|
||||||
area.BodyEntered += OnBodyEntered;
|
AreaEntered += OnEntered;
|
||||||
|
|
||||||
_sprite = GetNode<Sprite2D>("Sprite2D");
|
_sprite = GetNode<Sprite2D>("Sprite2D");
|
||||||
UpdateTexture();
|
UpdateTexture();
|
||||||
|
|
||||||
|
// Get the Label node for quantity
|
||||||
|
if (HasNode("Label"))
|
||||||
|
{
|
||||||
|
_quantityLabel = GetNode<Label>("Label");
|
||||||
|
if (Quantity > 1)
|
||||||
|
_quantityLabel.Text = Quantity.ToString();
|
||||||
|
else
|
||||||
|
_quantityLabel.Text = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or update shadow sprite
|
||||||
|
if (HasNode("ShadowSprite"))
|
||||||
|
{
|
||||||
|
_shadowSprite = GetNode<Sprite2D>("ShadowSprite");
|
||||||
|
_shadowSprite.Texture = _sprite.Texture;
|
||||||
|
_shadowSprite.Modulate = new Color(0, 0, 0, 0.5f);
|
||||||
|
_shadowSprite.Position = _sprite.Position + new Vector2(0, 6);
|
||||||
|
_shadowSprite.ZIndex = _sprite.ZIndex - 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_shadowSprite = new Sprite2D();
|
||||||
|
_shadowSprite.Scale = _sprite.Scale;
|
||||||
|
_shadowSprite.Name = "ShadowSprite";
|
||||||
|
_shadowSprite.Texture = _sprite.Texture;
|
||||||
|
_shadowSprite.Modulate = new Color(0, 0, 0, 0.5f);
|
||||||
|
_shadowSprite.Position = _sprite.Position + new Vector2(0, 6);
|
||||||
|
_shadowSprite.ZIndex = _sprite.ZIndex - 1;
|
||||||
|
AddChild(_shadowSprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
_playerTarget = GetTree().GetFirstNodeInGroup(PickupGroupName) as Node2D;
|
||||||
|
if (_playerTarget == null)
|
||||||
|
_playerTarget = GetTree().GetFirstNodeInGroup("Player") as Node2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (_playerTarget != null)
|
||||||
|
{
|
||||||
|
var distance = Position.DistanceTo(_playerTarget.Position);
|
||||||
|
const float speed = 10f;
|
||||||
|
if (distance <= MagnetRange)
|
||||||
|
Position = Position.Lerp(_playerTarget.Position, (float)delta * speed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTexture()
|
private void UpdateTexture()
|
||||||
@@ -57,15 +156,32 @@ public partial class ItemPickup : Node2D
|
|||||||
_sprite.Texture = texture;
|
_sprite.Texture = texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBodyEntered(Node body)
|
private void OnEntered(Node body)
|
||||||
{
|
{
|
||||||
if (body.IsInGroup(PickupGroupName))
|
if (body is ItemPickup itemStack)
|
||||||
{
|
{
|
||||||
if (body.HasMethod("AddItem"))
|
// Only process the merge for the item with the lower instance ID to prevent double merging
|
||||||
body.Call("AddItem", ItemId, Quantity);
|
if (itemStack.ItemId != ItemId || GetInstanceId() > body.GetInstanceId())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Get current quantity and add to it
|
||||||
|
var currentQuantity = itemStack.GetItemQuantity(ItemId);
|
||||||
|
var newQuantity = currentQuantity + Quantity;
|
||||||
|
itemStack.SetItemQuantity(ItemId, newQuantity);
|
||||||
|
|
||||||
|
// Emit signal for stack merge
|
||||||
|
EmitSignal(nameof(StackMerged), ItemId, newQuantity);
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
else if (body.IsInGroup(PickupGroupName))
|
||||||
|
{
|
||||||
|
if (body is Player player)
|
||||||
|
{
|
||||||
|
player.AddItem(ItemId, Quantity);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Infinite)
|
if (!Infinite)
|
||||||
QueueFree(); // remove the pickup from the world
|
QueueFree();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,153 +1,492 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using Godot;
|
using Godot;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AceFieldNewHorizon.Scripts.Entities;
|
||||||
using AceFieldNewHorizon.Scripts.Tiles;
|
using AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
|
||||||
namespace AceFieldNewHorizon.Scripts.System;
|
namespace AceFieldNewHorizon.Scripts.System;
|
||||||
|
|
||||||
public partial class NaturalResourceGenerator : Node2D
|
public partial class NaturalResourceGenerator : Node2D
|
||||||
{
|
{
|
||||||
[Export] public GridManager Grid { get; set; }
|
public const string ChunkTrackerGroupName = "NrgTrackingTarget";
|
||||||
[Export] public BuildingRegistry Registry { get; set; }
|
|
||||||
|
|
||||||
[Export] public int MapWidth = 100;
|
public GridManager Grid { get; private set; }
|
||||||
[Export] public int MapHeight = 100;
|
public BuildingRegistry Registry { get; private set; }
|
||||||
[Export] public float StoneDensity = 0.1f; // 10% chance for stone
|
|
||||||
[Export] public float IronDensity = 0.03f; // 3% chance for iron (within stone)
|
[Export] public int ChunkSize = 16;
|
||||||
|
[Export] public int LoadDistance = 2; // Number of chunks to load in each direction
|
||||||
|
[Export] public float StoneDensity = 0.1f;
|
||||||
|
[Export] public float IronDensity = 0.03f;
|
||||||
[Export] public int MinStoneVeinSize = 1;
|
[Export] public int MinStoneVeinSize = 1;
|
||||||
[Export] public int MaxStoneVeinSize = 5;
|
[Export] public int MaxStoneVeinSize = 5;
|
||||||
[Export] public int MinIronVeinSize = 1;
|
[Export] public int MinIronVeinSize = 1;
|
||||||
[Export] public int MaxIronVeinSize = 3;
|
[Export] public int MaxIronVeinSize = 3;
|
||||||
[Export] public int Seed;
|
[Export] public int Seed;
|
||||||
|
|
||||||
|
[Export] public bool SpawnEnemyNest = true;
|
||||||
|
[Export] public int MinDistanceFromOrigin = 20; // Minimum distance from world origin (0,0)
|
||||||
|
[Export] public int MaxDistanceFromOrigin = 50; // Maximum distance from world origin
|
||||||
|
|
||||||
|
private const string LogPrefix = "[NaturalGeneration]";
|
||||||
|
|
||||||
private RandomNumberGenerator _rng;
|
private RandomNumberGenerator _rng;
|
||||||
private readonly List<Vector2I> _groundTiles = [];
|
private readonly Dictionary<Vector2I, ChunkData> _loadedChunks = new();
|
||||||
private readonly List<Vector2I> _stoneTiles = [];
|
private Vector2I _lastPlayerChunk = new(-1000, -1000);
|
||||||
private readonly List<Vector2I> _ironTiles = [];
|
|
||||||
|
private Player _player;
|
||||||
|
|
||||||
|
private readonly ConcurrentQueue<Action> _mainThreadActions = new();
|
||||||
|
private Task _generationTask;
|
||||||
|
private bool _isRunning = true;
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
Grid = DependencyInjection.Container.GetInstance<GridManager>();
|
||||||
|
Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>();
|
||||||
_rng = new RandomNumberGenerator();
|
_rng = new RandomNumberGenerator();
|
||||||
_rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi());
|
_rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi());
|
||||||
|
|
||||||
GenerateTerrain();
|
// Test if building registry is assigned
|
||||||
PlaceResources();
|
if (Registry == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} BuildingRegistry is not assigned!");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateTerrain()
|
// Test if enemy_portal is in the registry
|
||||||
|
var testBuilding = Registry.GetBuilding("enemy_portal");
|
||||||
|
if (testBuilding == null)
|
||||||
{
|
{
|
||||||
// First pass: Generate base ground tiles
|
GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!");
|
||||||
for (int x = 0; x < MapWidth; x++)
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
for (int y = 0; y < MapHeight; y++)
|
GD.Print($"{LogPrefix} Found enemy_portal in registry!");
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}");
|
||||||
|
|
||||||
|
GD.Print($"{LogPrefix} Spawning the core reactor...");
|
||||||
|
SpawnCoreReactor();
|
||||||
|
|
||||||
|
if (SpawnEnemyNest)
|
||||||
{
|
{
|
||||||
var cell = new Vector2I(x, y);
|
GD.Print($"{LogPrefix} Attempting to spawn enemy nest...");
|
||||||
_groundTiles.Add(cell);
|
SpawnRandomEnemyNest();
|
||||||
PlaceTile("ground", cell);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
// Process any pending main thread actions
|
||||||
|
while (_mainThreadActions.TryDequeue(out var action))
|
||||||
|
{
|
||||||
|
action.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rest of your existing _Process code
|
||||||
|
_player = GetTree().GetFirstNodeInGroup(ChunkTrackerGroupName) as Player;
|
||||||
|
if (_player != null)
|
||||||
|
{
|
||||||
|
UpdatePlayerPosition(_player.GlobalPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} Player not found in group: {ChunkTrackerGroupName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePlayerPosition(Vector2 playerPosition)
|
||||||
|
{
|
||||||
|
var playerChunk = WorldToChunkCoords(playerPosition);
|
||||||
|
|
||||||
|
if (playerChunk == _lastPlayerChunk) return;
|
||||||
|
_lastPlayerChunk = playerChunk;
|
||||||
|
|
||||||
|
// Start generation in background task if not already running
|
||||||
|
if (_generationTask == null || _generationTask.IsCompleted)
|
||||||
|
{
|
||||||
|
_generationTask = Task.Run(() => GenerateChunksAroundPlayer(playerChunk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateChunksAroundPlayer(Vector2I playerChunk)
|
||||||
|
{
|
||||||
|
var chunksToLoad = new List<Vector2I>();
|
||||||
|
|
||||||
|
// Determine which chunks to load/unload
|
||||||
|
for (int x = -LoadDistance; x <= LoadDistance; x++)
|
||||||
|
{
|
||||||
|
for (int y = -LoadDistance; y <= LoadDistance; y++)
|
||||||
|
{
|
||||||
|
var chunkPos = new Vector2I(playerChunk.X + x, playerChunk.Y + y);
|
||||||
|
if (!_loadedChunks.ContainsKey(chunkPos))
|
||||||
|
{
|
||||||
|
chunksToLoad.Add(chunkPos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlaceResources()
|
// Generate chunks in background
|
||||||
|
foreach (var chunkPos in chunksToLoad)
|
||||||
{
|
{
|
||||||
// Create a copy of ground tiles for iteration
|
if (!_isRunning) return;
|
||||||
var groundTilesToProcess = new List<Vector2I>(_groundTiles);
|
GenerateChunkInBackground(chunkPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Place stone veins
|
private void GenerateChunkInBackground(Vector2I chunkPos)
|
||||||
foreach (var cell in groundTilesToProcess)
|
|
||||||
{
|
{
|
||||||
|
// Skip if chunk was loaded while we were waiting
|
||||||
|
if (_loadedChunks.ContainsKey(chunkPos)) return;
|
||||||
|
|
||||||
|
var chunkData = new ChunkData();
|
||||||
|
var chunkWorldPos = ChunkToWorldCoords(chunkPos);
|
||||||
|
|
||||||
|
// Generate ground tiles
|
||||||
|
for (int x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
|
||||||
|
_mainThreadActions.Enqueue(() => PlaceTile("ground", cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate stone veins
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
|
||||||
if (_rng.Randf() < StoneDensity)
|
if (_rng.Randf() < StoneDensity)
|
||||||
{
|
{
|
||||||
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
|
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
|
||||||
PlaceVein(cell, "stone", veinSize, _stoneTiles);
|
PlaceVeinInBackground(cell, "stone", veinSize, chunkData.StoneTiles);
|
||||||
}
|
}
|
||||||
}
|
else if (_rng.Randf() < IronDensity)
|
||||||
|
|
||||||
// Create a copy of stone tiles for iteration
|
|
||||||
var stoneTilesToProcess = new List<Vector2I>(_stoneTiles);
|
|
||||||
|
|
||||||
// Place iron veins within stone
|
|
||||||
foreach (var stoneCell in stoneTilesToProcess)
|
|
||||||
{
|
|
||||||
if (_rng.Randf() < IronDensity)
|
|
||||||
{
|
{
|
||||||
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
|
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
|
||||||
PlaceVein(stoneCell, "ore_iron", veinSize, _ironTiles);
|
PlaceVeinInBackground(cell, "ore_iron", veinSize, chunkData.IronTiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlaceVein(Vector2I startCell, string tileType, int maxVeinSize, ICollection<Vector2I> tileList)
|
// Add chunk data to dictionary on main thread
|
||||||
|
_mainThreadActions.Enqueue(() => { _loadedChunks[chunkPos] = chunkData; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaceVeinInBackground(Vector2I startCell, string tileType, int maxSize, List<Vector2I> tileList)
|
||||||
{
|
{
|
||||||
var queue = new Queue<Vector2I>();
|
|
||||||
var placed = new HashSet<Vector2I>();
|
var placed = new HashSet<Vector2I>();
|
||||||
|
var queue = new Queue<Vector2I>();
|
||||||
queue.Enqueue(startCell);
|
queue.Enqueue(startCell);
|
||||||
|
|
||||||
int placedCount = 0;
|
int placedCount = 0;
|
||||||
|
|
||||||
while (queue.Count > 0 && placedCount < maxVeinSize)
|
while (queue.Count > 0 && placedCount < maxSize && _isRunning)
|
||||||
{
|
{
|
||||||
var cell = queue.Dequeue();
|
var cell = queue.Dequeue();
|
||||||
if (placed.Contains(cell)) continue;
|
if (placed.Contains(cell)) continue;
|
||||||
if (!IsInBounds(cell)) continue;
|
|
||||||
|
|
||||||
switch (tileType)
|
// Schedule tile placement on main thread
|
||||||
|
_mainThreadActions.Enqueue(() =>
|
||||||
|
{
|
||||||
|
if (PlaceTile(tileType, cell))
|
||||||
{
|
{
|
||||||
// For iron, make sure we're placing on stone
|
|
||||||
case "ore_iron" when !_stoneTiles.Contains(cell):
|
|
||||||
continue;
|
|
||||||
// Remove from previous layer if needed
|
|
||||||
case "ore_iron" when _stoneTiles.Contains(cell):
|
|
||||||
_stoneTiles.Remove(cell);
|
|
||||||
break;
|
|
||||||
case "stone" when _groundTiles.Contains(cell):
|
|
||||||
_groundTiles.Remove(cell);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaceTile(tileType, cell);
|
|
||||||
tileList.Add(cell);
|
tileList.Add(cell);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
placed.Add(cell);
|
placed.Add(cell);
|
||||||
placedCount++;
|
placedCount++;
|
||||||
|
|
||||||
// Add adjacent cells to queue
|
// Add adjacent cells
|
||||||
|
var directions = new[] { Vector2I.Up, Vector2I.Right, Vector2I.Down, Vector2I.Left };
|
||||||
|
foreach (var dir in directions)
|
||||||
|
{
|
||||||
|
var newCell = cell + dir;
|
||||||
|
if (!placed.Contains(newCell))
|
||||||
|
{
|
||||||
|
queue.Enqueue(newCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
// Clean up
|
||||||
|
_isRunning = false;
|
||||||
|
_generationTask?.Wait(); // Wait for current generation to finish
|
||||||
|
base._ExitTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateChunk(Vector2I chunkPos)
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Generating chunk at {chunkPos}");
|
||||||
|
var chunkData = new ChunkData();
|
||||||
|
var chunkWorldPos = ChunkToWorldCoords(chunkPos);
|
||||||
|
|
||||||
|
// First, place ground tiles
|
||||||
|
for (int x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (int y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
|
||||||
|
if (!PlaceTile("ground", cell))
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} Failed to place ground at {cell}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then generate stone veins
|
||||||
|
var stoneVeins = 0;
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
|
||||||
|
if (_rng.Randf() < StoneDensity)
|
||||||
|
{
|
||||||
|
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
|
||||||
|
GD.Print($"{LogPrefix} Attempting to place stone vein at {cell} with size {veinSize}");
|
||||||
|
PlaceVein(cell, "stone", veinSize, chunkData.StoneTiles);
|
||||||
|
stoneVeins++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"{LogPrefix} Placed {stoneVeins} stone veins in chunk {chunkPos}");
|
||||||
|
|
||||||
|
// Generate iron veins within stone
|
||||||
|
int ironVeins = 0;
|
||||||
|
foreach (var stoneCell in new List<Vector2I>(chunkData.StoneTiles))
|
||||||
|
{
|
||||||
|
if (_rng.Randf() < IronDensity)
|
||||||
|
{
|
||||||
|
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
|
||||||
|
GD.Print($"{LogPrefix} Attempting to place iron vein at {stoneCell} with size {veinSize}");
|
||||||
|
PlaceVein(stoneCell, "ore_iron", veinSize, chunkData.IronTiles);
|
||||||
|
ironVeins++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"{LogPrefix} Placed {ironVeins} iron veins in chunk {chunkPos}");
|
||||||
|
|
||||||
|
_loadedChunks[chunkPos] = chunkData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnloadChunk(Vector2I chunkPos)
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Unloading chunk at {chunkPos}");
|
||||||
|
if (!_loadedChunks.TryGetValue(chunkPos, out var chunkData)) return;
|
||||||
|
|
||||||
|
// Remove all tiles in this chunk
|
||||||
|
var chunkWorldPos = ChunkToWorldCoords(chunkPos);
|
||||||
|
for (var x = 0; x < ChunkSize; x++)
|
||||||
|
{
|
||||||
|
for (var y = 0; y < ChunkSize; y++)
|
||||||
|
{
|
||||||
|
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
|
||||||
|
// Free a 1x1 area for each cell
|
||||||
|
Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadedChunks.Remove(chunkPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2I WorldToChunkCoords(Vector2 worldPos)
|
||||||
|
{
|
||||||
|
var cell = GridUtils.WorldToGrid(worldPos);
|
||||||
|
return new Vector2I(
|
||||||
|
(int)Mathf.Floor((float)cell.X / ChunkSize),
|
||||||
|
(int)Mathf.Floor((float)cell.Y / ChunkSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2I ChunkToWorldCoords(Vector2I chunkPos)
|
||||||
|
{
|
||||||
|
return new Vector2I(
|
||||||
|
chunkPos.X * ChunkSize,
|
||||||
|
chunkPos.Y * ChunkSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsInLoadedChunk(Vector2I cell)
|
||||||
|
{
|
||||||
|
// Check the chunk where the cell is located
|
||||||
|
var chunkPos = new Vector2I(
|
||||||
|
(int)Mathf.Floor((float)cell.X / ChunkSize),
|
||||||
|
(int)Mathf.Floor((float)cell.Y / ChunkSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also check adjacent chunks since veins can cross chunk boundaries
|
||||||
for (var dx = -1; dx <= 1; dx++)
|
for (var dx = -1; dx <= 1; dx++)
|
||||||
{
|
{
|
||||||
for (var dy = -1; dy <= 1; dy++)
|
for (var dy = -1; dy <= 1; dy++)
|
||||||
{
|
{
|
||||||
if (dx == 0 && dy == 0) continue; // Skip self
|
var checkPos = new Vector2I(chunkPos.X + dx, chunkPos.Y + dy);
|
||||||
var neighbor = new Vector2I(cell.X + dx, cell.Y + dy);
|
if (_loadedChunks.ContainsKey(checkPos))
|
||||||
if (!placed.Contains(neighbor) && IsInBounds(neighbor))
|
|
||||||
{
|
{
|
||||||
queue.Enqueue(neighbor);
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsInBounds(Vector2I cell)
|
return false;
|
||||||
{
|
|
||||||
return cell.X >= 0 && cell.X < MapWidth &&
|
|
||||||
cell.Y >= 0 && cell.Y < MapHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlaceTile(string tileType, Vector2I cell)
|
private void PlaceVein(Vector2I startCell, string tileType, int maxSize, List<Vector2I> tileList)
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Starting to place vein of type {tileType} at {startCell} with max size {maxSize}");
|
||||||
|
|
||||||
|
var placed = new HashSet<Vector2I>();
|
||||||
|
var queue = new Queue<Vector2I>();
|
||||||
|
queue.Enqueue(startCell);
|
||||||
|
|
||||||
|
int placedCount = 0;
|
||||||
|
|
||||||
|
while (queue.Count > 0 && placedCount < maxSize)
|
||||||
|
{
|
||||||
|
var cell = queue.Dequeue();
|
||||||
|
|
||||||
|
// Skip if already placed or out of bounds
|
||||||
|
if (placed.Contains(cell))
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Skipping cell {cell} - already placed");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsInLoadedChunk(cell))
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Skipping cell {cell} - out of bounds");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to place the tile
|
||||||
|
if (PlaceTile(tileType, cell))
|
||||||
|
{
|
||||||
|
tileList.Add(cell);
|
||||||
|
placed.Add(cell);
|
||||||
|
placedCount++;
|
||||||
|
// GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} ({placedCount}/{maxSize})");
|
||||||
|
|
||||||
|
// Add adjacent cells to queue
|
||||||
|
var directions = new[] { Vector2I.Up, Vector2I.Right, Vector2I.Down, Vector2I.Left };
|
||||||
|
foreach (var dir in directions)
|
||||||
|
{
|
||||||
|
var newCell = cell + dir;
|
||||||
|
if (!placed.Contains(newCell))
|
||||||
|
{
|
||||||
|
queue.Enqueue(newCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Failed to place {tileType} at {cell}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnCoreReactor()
|
||||||
|
{
|
||||||
|
// Place the reactor tile at the center of the map
|
||||||
|
PlaceTile("reactor", new Vector2I(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnRandomEnemyNest()
|
||||||
|
{
|
||||||
|
// Generate a random position within the specified distance from origin
|
||||||
|
var angle = _rng.Randf() * Mathf.Pi * 2;
|
||||||
|
var distance = _rng.RandfRange(MinDistanceFromOrigin, MaxDistanceFromOrigin);
|
||||||
|
var offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * distance;
|
||||||
|
var nestPosition = new Vector2I((int)offset.X, (int)offset.Y);
|
||||||
|
|
||||||
|
// Try to find a valid position for the nest
|
||||||
|
var attempts = 0;
|
||||||
|
const int maxAttempts = 10;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts)
|
||||||
|
{
|
||||||
|
if (PlaceTile("enemy_portal", nestPosition))
|
||||||
|
{
|
||||||
|
GD.Print($"{LogPrefix} Placed enemy nest at {nestPosition}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try a different position if placement failed
|
||||||
|
angle = _rng.Randf() * Mathf.Pi * 2;
|
||||||
|
distance = _rng.RandfRange(MinDistanceFromOrigin, MaxDistanceFromOrigin);
|
||||||
|
offset = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * distance;
|
||||||
|
nestPosition = new Vector2I((int)offset.X, (int)offset.Y);
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.PrintErr($"{LogPrefix} Failed to place enemy nest after {maxAttempts} attempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PlaceTile(string tileType, Vector2I cell)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var building = Registry.GetBuilding(tileType);
|
var building = Registry.GetBuilding(tileType);
|
||||||
if (building == null) return;
|
if (building == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free area for ground layer if needed
|
||||||
|
if (building.Layer == GridLayer.Ground)
|
||||||
|
Grid.FreeArea(cell, building.Size, 0f, GridLayer.Ground);
|
||||||
|
else
|
||||||
|
GD.Print($"{LogPrefix} Attempting placing building tile {tileType} at {cell}.");
|
||||||
|
|
||||||
var scene = building.Scene;
|
var scene = building.Scene;
|
||||||
var instance = (BaseTile)scene.Instantiate();
|
if (scene == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Match PlacementManager's positioning logic
|
if (scene.Instantiate() is not BaseTile instance)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate position with proper offset based on building size
|
||||||
var rotatedSize = building.GetRotatedSize(0f);
|
var rotatedSize = building.GetRotatedSize(0f);
|
||||||
var offset = GridUtils.GetCenterOffset(rotatedSize, 0f);
|
var offset = GridUtils.GetCenterOffset(rotatedSize, 0f);
|
||||||
instance.Position = GridUtils.GridToWorld(cell) + offset;
|
|
||||||
|
|
||||||
instance.ZIndex = (int)building.Layer;
|
instance.ZIndex = (int)building.Layer;
|
||||||
|
instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset;
|
||||||
AddChild(instance);
|
AddChild(instance);
|
||||||
|
|
||||||
// Make sure to use the building's size from the registry
|
// Occupy the appropriate area based on building size
|
||||||
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer);
|
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"{LogPrefix} Error placing {tileType} at {cell}: {e.Message}");
|
||||||
|
GD.Print($"{LogPrefix} Stack trace: {e.StackTrace}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ChunkData
|
||||||
|
{
|
||||||
|
public List<Vector2I> StoneTiles { get; } = [];
|
||||||
|
public List<Vector2I> IronTiles { get; } = [];
|
||||||
|
}
|
@@ -8,27 +8,52 @@ namespace AceFieldNewHorizon.Scripts.System;
|
|||||||
|
|
||||||
public partial class PlacementManager : Node2D
|
public partial class PlacementManager : Node2D
|
||||||
{
|
{
|
||||||
[Export] public GridManager Grid { get; set; }
|
public GridManager Grid { get; private set; }
|
||||||
[Export] public ResourceManager Inventory { get; set; }
|
public ResourceManager Inventory { get; private set; }
|
||||||
[Export] public BuildingRegistry Registry { get; set; }
|
public BuildingRegistry Registry { get; private set; }
|
||||||
[Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor
|
|
||||||
|
|
||||||
private static readonly List<string> BuildableTiles = ["wall", "miner"];
|
[Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor
|
||||||
|
[Export] public bool Enabled { get; set; } = true;
|
||||||
|
[Export] public StringName ToggleBuildAction { get; set; } = "toggle_build";
|
||||||
|
|
||||||
|
private static readonly List<string> BuildableTiles = ["wall", "miner", "turret"];
|
||||||
private readonly Dictionary<Node2D, BuildTask> _buildTasks = new();
|
private readonly Dictionary<Node2D, BuildTask> _buildTasks = new();
|
||||||
private AudioStreamPlayer _completionSound;
|
private AudioStreamPlayer _completionSound;
|
||||||
|
private AudioStreamPlayer _buildingSound;
|
||||||
|
private AudioStreamPlayer _canceledSound;
|
||||||
|
private AudioStreamPlayer _cannotDeploySound;
|
||||||
|
private AudioStreamPlayer _notReadySound;
|
||||||
|
private AudioStreamPlayer _insufficientFundsSound;
|
||||||
|
private Node2D _currentGhost; // Keep track of the current ghost building
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
base._Ready();
|
base._Ready();
|
||||||
|
|
||||||
|
Grid = DependencyInjection.Container.GetInstance<GridManager>();
|
||||||
|
Inventory = DependencyInjection.Container.GetInstance<ResourceManager>();
|
||||||
|
Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>();
|
||||||
|
|
||||||
// Setup completion sound
|
// Setup completion sound
|
||||||
_completionSound = new AudioStreamPlayer();
|
_completionSound = CreateAudioPlayer("res://Sounds/Events/ConstructionComplete.wav");
|
||||||
AddChild(_completionSound);
|
_buildingSound = CreateAudioPlayer("res://Sounds/Events/Building.wav");
|
||||||
var sound = GD.Load<AudioStream>("res://Sounds/Events/ConstructionComplete.wav");
|
_canceledSound = CreateAudioPlayer("res://Sounds/Events/Canceled.wav");
|
||||||
|
_cannotDeploySound = CreateAudioPlayer("res://Sounds/Events/CannotDeployHere.wav");
|
||||||
|
_notReadySound = CreateAudioPlayer("res://Sounds/Events/NotReady.wav");
|
||||||
|
_insufficientFundsSound = CreateAudioPlayer("res://Sounds/Events/InsufficientFunds.wav");
|
||||||
|
}
|
||||||
|
|
||||||
|
private AudioStreamPlayer CreateAudioPlayer(string path)
|
||||||
|
{
|
||||||
|
var player = new AudioStreamPlayer();
|
||||||
|
AddChild(player);
|
||||||
|
var sound = GD.Load<AudioStream>(path);
|
||||||
if (sound != null)
|
if (sound != null)
|
||||||
{
|
{
|
||||||
_completionSound.Stream = sound;
|
player.Stream = sound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBuildCompleted()
|
private void OnBuildCompleted()
|
||||||
@@ -153,6 +178,8 @@ public partial class PlacementManager : Node2D
|
|||||||
|
|
||||||
public override void _Process(double delta)
|
public override void _Process(double delta)
|
||||||
{
|
{
|
||||||
|
if (!Enabled) return;
|
||||||
|
|
||||||
// Snap mouse to grid
|
// Snap mouse to grid
|
||||||
var mousePos = GetGlobalMousePosition();
|
var mousePos = GetGlobalMousePosition();
|
||||||
var newHoveredCell = GridUtils.WorldToGrid(mousePos);
|
var newHoveredCell = GridUtils.WorldToGrid(mousePos);
|
||||||
@@ -188,24 +215,39 @@ public partial class PlacementManager : Node2D
|
|||||||
_ghostBuilding.SetGhostMode(canPlace);
|
_ghostBuilding.SetGhostMode(canPlace);
|
||||||
|
|
||||||
// Left click to place
|
// Left click to place
|
||||||
if (Input.IsActionPressed("build_tile") && canPlace)
|
if (Input.IsActionPressed("build_tile"))
|
||||||
{
|
{
|
||||||
var building = Registry.GetBuilding(_currentBuildingId);
|
var building = Registry.GetBuilding(_currentBuildingId);
|
||||||
if (building == null) return;
|
if (building == null) return;
|
||||||
|
|
||||||
if (!CanStartNewBuild())
|
if (!CanStartNewBuild())
|
||||||
{
|
{
|
||||||
// Optionally show feedback to player that build queue is full
|
_notReadySound.Play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if area is free
|
||||||
|
if (!IsAreaFree(_hoveredCell, building.Size, _currentRotation, building.Layer))
|
||||||
|
{
|
||||||
|
// Check if the area is occupied by under-construction tiles
|
||||||
|
var occupiedCells = GridUtils.GetOccupiedCells(_hoveredCell, building.Size, _currentRotation);
|
||||||
|
var isUnderConstruction = occupiedCells.Any(cell =>
|
||||||
|
Grid.GetTileAtCell(cell, building.Layer) is BaseTile { IsConstructing: true });
|
||||||
|
|
||||||
|
if (!isUnderConstruction)
|
||||||
|
_cannotDeploySound.Play();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume resources first
|
// Consume resources first
|
||||||
if (!ConsumeBuildingResources(_currentBuildingId))
|
if (!ConsumeBuildingResources(_currentBuildingId))
|
||||||
{
|
{
|
||||||
// Optionally show feedback to player that they can't afford this building
|
_insufficientFundsSound.Play();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the building instance
|
||||||
var scene = building.Scene;
|
var scene = building.Scene;
|
||||||
var buildingInstance = (BaseTile)scene.Instantiate();
|
var buildingInstance = (BaseTile)scene.Instantiate();
|
||||||
buildingInstance.RotationDegrees = _currentRotation;
|
buildingInstance.RotationDegrees = _currentRotation;
|
||||||
@@ -213,23 +255,21 @@ public partial class PlacementManager : Node2D
|
|||||||
buildingInstance.Position = _ghostBuilding.Position;
|
buildingInstance.Position = _ghostBuilding.Position;
|
||||||
AddChild(buildingInstance);
|
AddChild(buildingInstance);
|
||||||
|
|
||||||
// Check if area is free before placing
|
// If we get here, area is free, so we can safely occupy it
|
||||||
if (!IsAreaFree(_hoveredCell, building.Size, _currentRotation, building.Layer))
|
|
||||||
{
|
|
||||||
RefundBuildingResources(_currentBuildingId);
|
|
||||||
buildingInstance.QueueFree();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Occupy the area
|
|
||||||
Grid.OccupyArea(_hoveredCell, buildingInstance, building.Size, _currentRotation, building.Layer);
|
Grid.OccupyArea(_hoveredCell, buildingInstance, building.Size, _currentRotation, building.Layer);
|
||||||
|
|
||||||
if (building.BuildTime > 0f)
|
if (building.BuildTime > 0f)
|
||||||
{
|
{
|
||||||
|
var wasQueueEmpty = _buildTasks.Count == 0;
|
||||||
var buildTask = new BuildTask(OnBuildCompleted);
|
var buildTask = new BuildTask(OnBuildCompleted);
|
||||||
_buildTasks[buildingInstance] = buildTask;
|
_buildTasks[buildingInstance] = buildTask;
|
||||||
|
|
||||||
buildingInstance.StartConstruction(building.BuildTime, () => {
|
// Play building sound only when adding to an empty queue
|
||||||
|
if (wasQueueEmpty)
|
||||||
|
_buildingSound.Play();
|
||||||
|
|
||||||
|
buildingInstance.StartConstruction(building.BuildTime, () =>
|
||||||
|
{
|
||||||
// On construction complete
|
// On construction complete
|
||||||
if (_buildTasks.TryGetValue(buildingInstance, out var task))
|
if (_buildTasks.TryGetValue(buildingInstance, out var task))
|
||||||
{
|
{
|
||||||
@@ -239,6 +279,7 @@ public partial class PlacementManager : Node2D
|
|||||||
Grid.FreeArea(_hoveredCell, building.Size, _currentRotation, building.Layer);
|
Grid.FreeArea(_hoveredCell, building.Size, _currentRotation, building.Layer);
|
||||||
buildingInstance.QueueFree();
|
buildingInstance.QueueFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
task.Complete();
|
task.Complete();
|
||||||
_buildTasks.Remove(buildingInstance);
|
_buildTasks.Remove(buildingInstance);
|
||||||
}
|
}
|
||||||
@@ -250,11 +291,30 @@ public partial class PlacementManager : Node2D
|
|||||||
!Grid.IsAreaFree(_hoveredCell, Vector2I.One, 0f))
|
!Grid.IsAreaFree(_hoveredCell, Vector2I.One, 0f))
|
||||||
{
|
{
|
||||||
// Right click to destroy from current layer
|
// Right click to destroy from current layer
|
||||||
var building = Grid.GetBuildingAtCell(_hoveredCell);
|
var building = Grid.GetTileAtCell(_hoveredCell);
|
||||||
if (building == null) return;
|
if (building == null) return;
|
||||||
|
|
||||||
// Find all cells occupied by this building
|
// Find all cells occupied by this building
|
||||||
var buildingInfo = Grid.GetBuildingInfoAtCell(_hoveredCell, GridLayer.Building);
|
var buildingInfo = Grid.GetBuildingInfoAtCell(_hoveredCell, GridLayer.Building);
|
||||||
if (buildingInfo == null) return;
|
if (buildingInfo == null) return;
|
||||||
|
|
||||||
|
// Check if this building is in the build tasks (under construction)
|
||||||
|
if (_buildTasks.TryGetValue(building, out var buildTask))
|
||||||
|
{
|
||||||
|
var buildingTile = building as BaseTile;
|
||||||
|
// Cancel the build task
|
||||||
|
buildTask.Complete(true); // Mark as cancelled
|
||||||
|
_buildTasks.Remove(building);
|
||||||
|
_canceledSound.Play();
|
||||||
|
if (buildingTile == null) return;
|
||||||
|
|
||||||
|
// Refund resources for canceled build
|
||||||
|
var buildingData = Registry.GetBuilding(buildingTile.TileId);
|
||||||
|
if (buildingData != null)
|
||||||
|
RefundBuildingResources(buildingTile.TileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the building and grid
|
||||||
building.QueueFree();
|
building.QueueFree();
|
||||||
Grid.FreeArea(buildingInfo.Value.Position, buildingInfo.Value.Size, buildingInfo.Value.Rotation);
|
Grid.FreeArea(buildingInfo.Value.Position, buildingInfo.Value.Size, buildingInfo.Value.Rotation);
|
||||||
}
|
}
|
||||||
@@ -267,6 +327,21 @@ public partial class PlacementManager : Node2D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (@event.IsActionPressed(ToggleBuildAction))
|
||||||
|
{
|
||||||
|
Enabled = !Enabled;
|
||||||
|
|
||||||
|
// Hide ghost building when disabling
|
||||||
|
if (!Enabled && _ghostBuilding != null && _ghostBuilding.IsInsideTree())
|
||||||
|
{
|
||||||
|
_ghostBuilding.QueueFree();
|
||||||
|
_ghostBuilding = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
base._ExitTree();
|
base._ExitTree();
|
||||||
@@ -353,34 +428,30 @@ public static class GridManagerExtensions
|
|||||||
public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid,
|
public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid,
|
||||||
Vector2I cell, GridLayer layer)
|
Vector2I cell, GridLayer layer)
|
||||||
{
|
{
|
||||||
if (grid.GetBuildingAtCell(cell, layer) is { } building)
|
if (grid.GetTileAtCell(cell, layer) is not { } building) return null;
|
||||||
{
|
|
||||||
// Find the top-left position of the building
|
// Find the top-left position of the building
|
||||||
for (int x = 0; x < 100; x++) // Arbitrary max size
|
for (var x = 0; x < 100; x++) // Arbitrary max size
|
||||||
{
|
{
|
||||||
for (int y = 0; y < 100; y++)
|
for (var y = 0; y < 100; y++)
|
||||||
{
|
{
|
||||||
var checkCell = new Vector2I(cell.X - x, cell.Y - y);
|
var checkCell = new Vector2I(cell.X - x, cell.Y - y);
|
||||||
if (grid.GetBuildingAtCell(checkCell, layer) == building)
|
if (grid.GetTileAtCell(checkCell, layer) != building) continue;
|
||||||
{
|
|
||||||
// Found the top-left corner, now find the size
|
// Found the top-left corner, now find the size
|
||||||
var size = Vector2I.One;
|
var size = Vector2I.One;
|
||||||
// Search right
|
// Search right
|
||||||
while (grid.GetBuildingAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
|
while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
|
||||||
building)
|
building)
|
||||||
size.X++;
|
size.X++;
|
||||||
// Search down
|
// Search down
|
||||||
while (grid.GetBuildingAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
|
while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
|
||||||
building)
|
building)
|
||||||
size.Y++;
|
size.Y++;
|
||||||
|
|
||||||
// Get rotation from the first cell
|
// Get rotation from the first cell
|
||||||
var rotation = 0f; // You'll need to store rotation in GridManager to make this work
|
var rotation = 0f; // You'll need to store rotation in Grid to make this work
|
||||||
return (checkCell, size, rotation);
|
return (checkCell, size, rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,10 @@ public partial class ResourceManager : Node
|
|||||||
[Signal]
|
[Signal]
|
||||||
public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount);
|
public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount);
|
||||||
|
|
||||||
|
// Event for when resources are picked up (added)
|
||||||
|
[Signal]
|
||||||
|
public delegate void OnResourcePickedUpEventHandler(string resourceId, int amount);
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
base._Ready();
|
base._Ready();
|
||||||
@@ -43,6 +47,7 @@ public partial class ResourceManager : Node
|
|||||||
}
|
}
|
||||||
|
|
||||||
EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]);
|
EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]);
|
||||||
|
EmitSignal(nameof(OnResourcePickedUp), resourceId, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove resources of a specific type
|
// Remove resources of a specific type
|
||||||
|
@@ -10,22 +10,45 @@ public partial class BaseTile : Node2D
|
|||||||
{
|
{
|
||||||
[Export] public string TileId { get; set; }
|
[Export] public string TileId { get; set; }
|
||||||
|
|
||||||
|
protected GridManager Grid { get; set; }
|
||||||
|
protected BuildingRegistry Registry { get; set; }
|
||||||
|
|
||||||
|
public int MaxDurability { get; private set; }
|
||||||
|
public int CurrentDurability { get; private set; }
|
||||||
|
public bool IsDestroyed { get; private set; }
|
||||||
|
|
||||||
private CollisionShape2D _collisionShape;
|
private CollisionShape2D _collisionShape;
|
||||||
private Sprite2D _sprite;
|
private Sprite2D _sprite;
|
||||||
private ColorRect _progressOverlay;
|
private ColorRect _progressOverlay;
|
||||||
private Action _onConstructionComplete;
|
private Action _onConstructionComplete;
|
||||||
|
private Tween _damageTween;
|
||||||
|
|
||||||
|
public bool IsConstructing;
|
||||||
|
public bool IsConstructed;
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
Grid = DependencyInjection.Container.GetInstance<GridManager>();
|
||||||
|
Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>();
|
||||||
|
|
||||||
_collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D");
|
_collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D");
|
||||||
_sprite = GetNodeOrNull<Sprite2D>("Sprite2D");
|
_sprite = GetNodeOrNull<Sprite2D>("Sprite2D");
|
||||||
_progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay");
|
_progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay");
|
||||||
if (_progressOverlay != null)
|
if (_progressOverlay != null)
|
||||||
_progressOverlay.Visible = false;
|
_progressOverlay.Visible = false;
|
||||||
|
|
||||||
|
// Get durability from BuildingRegistry
|
||||||
|
var buildingData = Registry?.GetBuilding(TileId);
|
||||||
|
MaxDurability = buildingData?.Durability ?? 100; // Default to 100 if not found
|
||||||
|
CurrentDurability = MaxDurability;
|
||||||
|
IsDestroyed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetGhostMode(bool canPlace)
|
public virtual void SetGhostMode(bool canPlace)
|
||||||
{
|
{
|
||||||
|
// Don't modify collision for constructing buildings
|
||||||
|
if (IsConstructing) return;
|
||||||
|
|
||||||
if (_collisionShape != null)
|
if (_collisionShape != null)
|
||||||
_collisionShape.Disabled = true;
|
_collisionShape.Disabled = true;
|
||||||
|
|
||||||
@@ -35,7 +58,7 @@ public partial class BaseTile : Node2D
|
|||||||
: new Color(1, 0, 0, 0.5f);
|
: new Color(1, 0, 0, 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FinalizePlacement()
|
public virtual void FinalizePlacement()
|
||||||
{
|
{
|
||||||
if (_collisionShape != null)
|
if (_collisionShape != null)
|
||||||
_collisionShape.Disabled = false;
|
_collisionShape.Disabled = false;
|
||||||
@@ -43,11 +66,91 @@ public partial class BaseTile : Node2D
|
|||||||
_sprite.Modulate = Colors.White;
|
_sprite.Modulate = Colors.White;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual bool TakeDamage(int damage)
|
||||||
|
{
|
||||||
|
if (IsDestroyed || IsConstructing) return false;
|
||||||
|
|
||||||
|
GD.Print($"[Tile] {TileId} {GetInstanceId()} took {damage} damage");
|
||||||
|
|
||||||
|
CurrentDurability = Mathf.Max(0, CurrentDurability - damage);
|
||||||
|
|
||||||
|
// Visual feedback for taking damage
|
||||||
|
ShowDamageEffect();
|
||||||
|
|
||||||
|
if (CurrentDurability <= 0)
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
return true; // Tile was destroyed
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // Tile is still alive
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowDamageEffect()
|
||||||
|
{
|
||||||
|
if (_sprite == null) return;
|
||||||
|
|
||||||
|
// Cancel any existing tween
|
||||||
|
_damageTween?.Kill();
|
||||||
|
_damageTween = CreateTween();
|
||||||
|
|
||||||
|
// Flash red briefly
|
||||||
|
_damageTween.TweenProperty(_sprite, "modulate", Colors.Red, 0.1f);
|
||||||
|
_damageTween.TweenProperty(_sprite, "modulate", Colors.White, 0.1f);
|
||||||
|
|
||||||
|
// Shake effect
|
||||||
|
var originalPosition = _sprite.Position;
|
||||||
|
_damageTween.TweenMethod(
|
||||||
|
Callable.From<Vector2>(pos => _sprite.Position = pos),
|
||||||
|
originalPosition + new Vector2(-2, -2),
|
||||||
|
originalPosition + new Vector2(2, 2),
|
||||||
|
0.05f
|
||||||
|
).SetTrans(Tween.TransitionType.Bounce).SetEase(Tween.EaseType.InOut);
|
||||||
|
|
||||||
|
_damageTween.TweenMethod(
|
||||||
|
Callable.From<Vector2>(pos => _sprite.Position = pos),
|
||||||
|
originalPosition + new Vector2(2, 2),
|
||||||
|
originalPosition,
|
||||||
|
0.05f
|
||||||
|
).SetTrans(Tween.TransitionType.Bounce).SetEase(Tween.EaseType.InOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Destroy()
|
||||||
|
{
|
||||||
|
if (IsDestroyed) return;
|
||||||
|
|
||||||
|
IsDestroyed = true;
|
||||||
|
|
||||||
|
// Disable collision using SetDeferred to avoid physics update conflicts
|
||||||
|
_collisionShape?.SetDeferred("disabled", true);
|
||||||
|
|
||||||
|
// Fade out and remove
|
||||||
|
var tween = CreateTween();
|
||||||
|
tween.TweenProperty(this, "modulate:a", 0f, 0.3f);
|
||||||
|
tween.TweenCallback(Callable.From(() => CallDeferred("queue_free")));
|
||||||
|
|
||||||
|
// Emit signal or call method when tile is destroyed
|
||||||
|
CallDeferred(nameof(OnTileDestroyed));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnTileDestroyed()
|
||||||
|
{
|
||||||
|
// Can be overridden by derived classes for custom destruction behavior
|
||||||
|
var cell = GridUtils.WorldToGrid(Position);
|
||||||
|
var buildingInfo = Registry.GetBuilding(TileId);
|
||||||
|
Grid.FreeArea(cell, buildingInfo.Size, Rotation, buildingInfo.Layer);
|
||||||
|
}
|
||||||
|
|
||||||
// Building progress visualization
|
// Building progress visualization
|
||||||
public void StartConstruction(float buildTime, Action onComplete = null)
|
public void StartConstruction(float buildTime, Action onComplete = null)
|
||||||
{
|
{
|
||||||
|
IsConstructing = true;
|
||||||
|
if (_collisionShape != null)
|
||||||
|
_collisionShape.Disabled = true;
|
||||||
|
|
||||||
if (_progressOverlay == null || _sprite?.Texture == null)
|
if (_progressOverlay == null || _sprite?.Texture == null)
|
||||||
{
|
{
|
||||||
|
IsConstructing = false;
|
||||||
onComplete?.Invoke();
|
onComplete?.Invoke();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -55,6 +158,10 @@ public partial class BaseTile : Node2D
|
|||||||
_onConstructionComplete = onComplete;
|
_onConstructionComplete = onComplete;
|
||||||
var texSize = new Vector2(GridUtils.TileSize, GridUtils.TileSize);
|
var texSize = new Vector2(GridUtils.TileSize, GridUtils.TileSize);
|
||||||
|
|
||||||
|
// Set initial transparency for construction
|
||||||
|
if (_sprite != null)
|
||||||
|
_sprite.Modulate = new Color(1, 1, 1, 0.8f);
|
||||||
|
|
||||||
_progressOverlay.Visible = true;
|
_progressOverlay.Visible = true;
|
||||||
_progressOverlay.Modulate = Colors.White;
|
_progressOverlay.Modulate = Colors.White;
|
||||||
_progressOverlay.Color = new Color(0, 0, 1, 0.4f); // semi-transparent blue
|
_progressOverlay.Color = new Color(0, 0, 1, 0.4f); // semi-transparent blue
|
||||||
@@ -80,8 +187,16 @@ public partial class BaseTile : Node2D
|
|||||||
// Fade out the overlay
|
// Fade out the overlay
|
||||||
await FadeOutOverlay(0.5f);
|
await FadeOutOverlay(0.5f);
|
||||||
|
|
||||||
// Notify completion
|
// Construction complete - restore full opacity and enable collision
|
||||||
|
if (_sprite != null)
|
||||||
|
_sprite.Modulate = Colors.White;
|
||||||
|
|
||||||
|
IsConstructing = false;
|
||||||
|
if (_collisionShape != null)
|
||||||
|
_collisionShape.Disabled = false;
|
||||||
|
|
||||||
_onConstructionComplete?.Invoke();
|
_onConstructionComplete?.Invoke();
|
||||||
|
IsConstructed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
RunProgress();
|
RunProgress();
|
||||||
|
129
Scripts/Tiles/EnemyPortalTile.cs
Normal file
129
Scripts/Tiles/EnemyPortalTile.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using AceFieldNewHorizon.Scripts.Entities;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
|
||||||
|
public partial class EnemyPortalTile : BaseTile
|
||||||
|
{
|
||||||
|
[Export] public PackedScene EnemyScene;
|
||||||
|
[Export] public int MaxEnemies = 5;
|
||||||
|
[Export] public float SpawnDelay = 5.0f; // Time between spawn attempts
|
||||||
|
[Export] public float SpawnRadius = 50.0f; // Radius around the nest where enemies can spawn
|
||||||
|
[Export] public bool Active = true;
|
||||||
|
|
||||||
|
private Timer _spawnTimer;
|
||||||
|
private int _currentEnemyCount;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
base._Ready();
|
||||||
|
|
||||||
|
// Add to Hostile group to prevent enemies from attacking their own nest
|
||||||
|
AddToGroup("Hostile");
|
||||||
|
|
||||||
|
// Create and configure the timer
|
||||||
|
_spawnTimer = new Timer
|
||||||
|
{
|
||||||
|
Autostart = true,
|
||||||
|
WaitTime = SpawnDelay
|
||||||
|
};
|
||||||
|
AddChild(_spawnTimer);
|
||||||
|
_spawnTimer.Timeout += OnSpawnTimerTimeout;
|
||||||
|
|
||||||
|
var sprite = GetNode<Sprite2D>("Sprite2D");
|
||||||
|
|
||||||
|
// Create and configure the shadow sprite
|
||||||
|
var shadow = new Sprite2D
|
||||||
|
{
|
||||||
|
Texture = sprite.Texture,
|
||||||
|
Scale = sprite.Scale * 1.05f, // Slightly larger than the original
|
||||||
|
Modulate = new Color(0, 0, 0, 0.5f), // Slightly more transparent
|
||||||
|
Position = new Vector2(0, 6), // Closer to the sprite (reduced from 30)
|
||||||
|
ZIndex = -1
|
||||||
|
};
|
||||||
|
AddChild(shadow);
|
||||||
|
|
||||||
|
// Create floating animation
|
||||||
|
const float floatOffset = 5.0f;
|
||||||
|
const float floatDuration = 2.0f;
|
||||||
|
|
||||||
|
var tween = CreateTween().SetLoops();
|
||||||
|
tween.TweenProperty(sprite, "position:y", -floatOffset, floatDuration)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine);
|
||||||
|
tween.TweenProperty(sprite, "position:y", floatOffset, floatDuration * 2)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine);
|
||||||
|
tween.TweenProperty(sprite, "position:y", 0, floatDuration)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine);
|
||||||
|
|
||||||
|
// Animate shadow
|
||||||
|
tween.Parallel().TweenProperty(shadow, "position:y", 12 - (floatOffset * 0.3f), floatDuration)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine);
|
||||||
|
tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.02f, floatDuration)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine);
|
||||||
|
tween.Parallel().TweenProperty(shadow, "modulate:a", 0.6f, floatDuration)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine);
|
||||||
|
|
||||||
|
tween.Parallel().TweenProperty(shadow, "position:y", 12 + (floatOffset * 0.4f), floatDuration * 2)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine)
|
||||||
|
.SetDelay(floatDuration);
|
||||||
|
tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.08f, floatDuration * 2)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine)
|
||||||
|
.SetDelay(floatDuration);
|
||||||
|
tween.Parallel().TweenProperty(shadow, "modulate:a", 0.4f, floatDuration * 2)
|
||||||
|
.SetEase(Tween.EaseType.InOut)
|
||||||
|
.SetTrans(Tween.TransitionType.Sine)
|
||||||
|
.SetDelay(floatDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSpawnTimerTimeout()
|
||||||
|
{
|
||||||
|
if (!Active || EnemyScene == null || _currentEnemyCount >= MaxEnemies)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if we can spawn more enemies
|
||||||
|
var enemies = GetTree().GetNodesInGroup("Enemy");
|
||||||
|
_currentEnemyCount = enemies.Count;
|
||||||
|
|
||||||
|
if (_currentEnemyCount >= MaxEnemies)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Spawn a new enemy
|
||||||
|
var enemy = EnemyScene.Instantiate<Enemy>();
|
||||||
|
if (enemy != null)
|
||||||
|
{
|
||||||
|
GetParent().AddChild(enemy);
|
||||||
|
|
||||||
|
// Calculate a random position within the spawn radius
|
||||||
|
var randomAngle = GD.Randf() * Mathf.Pi * 2;
|
||||||
|
var randomOffset = new Vector2(
|
||||||
|
Mathf.Cos(randomAngle) * SpawnRadius,
|
||||||
|
Mathf.Sin(randomAngle) * SpawnRadius
|
||||||
|
);
|
||||||
|
|
||||||
|
enemy.GlobalPosition = GlobalPosition + randomOffset;
|
||||||
|
_currentEnemyCount++;
|
||||||
|
|
||||||
|
// Connect to the enemy's death signal if available
|
||||||
|
enemy.TreeExiting += () => OnEnemyDied();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnemyDied()
|
||||||
|
{
|
||||||
|
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetActive(bool active)
|
||||||
|
{
|
||||||
|
Active = active;
|
||||||
|
_spawnTimer.Paused = !active;
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/Tiles/EnemyPortalTile.cs.uid
Normal file
1
Scripts/Tiles/EnemyPortalTile.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://26hl5mk4mqur
|
@@ -8,6 +8,7 @@ public partial class GroundTile : BaseTile
|
|||||||
{
|
{
|
||||||
var sprite = GetNode<Sprite2D>("Sprite2D");
|
var sprite = GetNode<Sprite2D>("Sprite2D");
|
||||||
sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker
|
sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker
|
||||||
|
sprite.ZIndex = -10;
|
||||||
|
|
||||||
base._Ready();
|
base._Ready();
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,83 @@
|
|||||||
|
using AceFieldNewHorizon.Scripts.System;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace AceFieldNewHorizon.Scripts.Tiles;
|
namespace AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
|
||||||
public partial class MinerTile : BaseTile
|
public partial class MinerTile : BaseTile
|
||||||
{
|
{
|
||||||
|
[Export] public PackedScene ItemPickup { get; set; }
|
||||||
|
[Export] public string ItemToMine { get; set; }
|
||||||
|
[Export] public int MiningRate = 1; // Items per second
|
||||||
|
|
||||||
|
private Vector2I _gridPosition;
|
||||||
|
private float _timeSinceLastMine;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
base._Ready();
|
||||||
|
_gridPosition = GridUtils.WorldToGrid(Position);
|
||||||
|
|
||||||
|
var ground = Grid.GetTileAtCell(_gridPosition, GridLayer.Ground) as BaseTile;
|
||||||
|
if (ground == null)
|
||||||
|
{
|
||||||
|
GD.Print($"[Miner] Miner {GetInstanceId()} not found available resource...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemToMine = (ground.TileId) switch
|
||||||
|
{
|
||||||
|
"stone" => "stone",
|
||||||
|
"ore_iron" => "ore_iron",
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
if (ItemToMine == null)
|
||||||
|
GD.Print($"[Miner] Miner {GetInstanceId()} not found available resource...");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (ItemToMine == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Don't mine if building is not completed
|
||||||
|
if (!IsConstructed || ItemPickup == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_timeSinceLastMine += (float)delta;
|
||||||
|
|
||||||
|
if (!(_timeSinceLastMine >= 1f / MiningRate)) return;
|
||||||
|
_timeSinceLastMine = 0f;
|
||||||
|
SpawnItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnItem()
|
||||||
|
{
|
||||||
|
var itemPickup = ItemPickup?.Instantiate<ItemPickup>();
|
||||||
|
if (itemPickup == null) return;
|
||||||
|
|
||||||
|
itemPickup.ItemId = ItemToMine;
|
||||||
|
itemPickup.Quantity = 1;
|
||||||
|
|
||||||
|
// Initial position (slightly below the spawn point)
|
||||||
|
const int halfTileSize = GridUtils.TileSize / 2;
|
||||||
|
var spawnPosition = GridUtils.GridToWorld(_gridPosition);
|
||||||
|
var targetY = spawnPosition.Y - halfTileSize; // Target Y position
|
||||||
|
var targetX = spawnPosition.X + halfTileSize + (GD.Randf() * 10f - 5f);
|
||||||
|
itemPickup.Position =
|
||||||
|
new Vector2(spawnPosition.X + halfTileSize, spawnPosition.Y + 16); // Start below
|
||||||
|
itemPickup.Scale = Vector2.Zero; // Start invisible
|
||||||
|
|
||||||
|
// Add to the scene
|
||||||
|
GetTree().CurrentScene.AddChild(itemPickup);
|
||||||
|
|
||||||
|
// Create the pop-up animation
|
||||||
|
var tween = CreateTween().SetTrans(Tween.TransitionType.Elastic).SetEase(Tween.EaseType.Out);
|
||||||
|
|
||||||
|
// Animate the pop-up effect
|
||||||
|
tween.TweenProperty(itemPickup, "position:y", targetY, 0.6f);
|
||||||
|
tween.Parallel().TweenProperty(itemPickup, "scale", Vector2.One, 0.6f);
|
||||||
|
|
||||||
|
// Optional: Add a slight horizontal wobble
|
||||||
|
tween.Parallel().TweenProperty(itemPickup, "position:x", targetX, 0.6f);
|
||||||
|
}
|
||||||
}
|
}
|
13
Scripts/Tiles/ReactorTile.cs
Normal file
13
Scripts/Tiles/ReactorTile.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
|
||||||
|
public partial class ReactorTile : BaseTile
|
||||||
|
{
|
||||||
|
public static readonly string ReactorGroupName = "Reactor";
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
base._Ready();
|
||||||
|
|
||||||
|
AddToGroup(ReactorGroupName);
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/Tiles/ReactorTile.cs.uid
Normal file
1
Scripts/Tiles/ReactorTile.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c4k3ottt7j3b1
|
127
Scripts/Tiles/TurretTile.cs
Normal file
127
Scripts/Tiles/TurretTile.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using AceFieldNewHorizon.Scripts.Entities;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace AceFieldNewHorizon.Scripts.Tiles;
|
||||||
|
|
||||||
|
public partial class TurretTile : BaseTile
|
||||||
|
{
|
||||||
|
[Export] public PackedScene BulletScene;
|
||||||
|
|
||||||
|
[Export] public float RotationSpeed = 2.0f; // Radians per second
|
||||||
|
[Export] public float AttackRange = 300.0f;
|
||||||
|
[Export] public float AttackCooldown = 0.5f;
|
||||||
|
[Export] public int Damage = 10;
|
||||||
|
[Export] public float BulletSpeed = 400.0f;
|
||||||
|
[Export] public NodePath BarrelTipPath;
|
||||||
|
|
||||||
|
private Node2D _spriteBarrel;
|
||||||
|
private Node2D _barrelTip;
|
||||||
|
private float _attackTimer = 0;
|
||||||
|
private bool _hasTarget = false;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
base._Ready();
|
||||||
|
|
||||||
|
_spriteBarrel = GetNodeOrNull<Node2D>("Barrel");
|
||||||
|
_barrelTip = GetNodeOrNull<Node2D>(BarrelTipPath);
|
||||||
|
|
||||||
|
if (_barrelTip == null && _spriteBarrel != null)
|
||||||
|
{
|
||||||
|
// If no barrel tip is specified, use the end of the barrel sprite
|
||||||
|
_barrelTip = _spriteBarrel.GetNodeOrNull<Node2D>("BarrelTip");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetGhostMode(bool canPlace)
|
||||||
|
{
|
||||||
|
base.SetGhostMode(canPlace);
|
||||||
|
|
||||||
|
if (_spriteBarrel != null)
|
||||||
|
_spriteBarrel.Modulate = canPlace
|
||||||
|
? new Color(0, 1, 0, 0.5f)
|
||||||
|
: new Color(1, 0, 0, 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void FinalizePlacement()
|
||||||
|
{
|
||||||
|
base.FinalizePlacement();
|
||||||
|
|
||||||
|
if (_spriteBarrel != null)
|
||||||
|
_spriteBarrel.Modulate = Colors.White;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
if (!IsConstructed) return;
|
||||||
|
|
||||||
|
// Update attack cooldown
|
||||||
|
if (_attackTimer > 0)
|
||||||
|
{
|
||||||
|
_attackTimer -= (float)delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the nearest enemy
|
||||||
|
var enemies = GetTree().GetNodesInGroup(Enemy.EnemyGroupName)
|
||||||
|
.OfType<Enemy>()
|
||||||
|
.Where(e => e.GlobalPosition.DistanceTo(GlobalPosition) <= AttackRange)
|
||||||
|
.OrderBy(e => e.GlobalPosition.DistanceTo(GlobalPosition))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (enemies.Count > 0)
|
||||||
|
{
|
||||||
|
var nearestEnemy = enemies[0];
|
||||||
|
_hasTarget = true;
|
||||||
|
|
||||||
|
// Calculate target angle
|
||||||
|
var direction = (nearestEnemy.GlobalPosition - _barrelTip.GlobalPosition).Normalized();
|
||||||
|
var targetAngle = direction.Angle() + 90f;
|
||||||
|
|
||||||
|
// Smoothly rotate towards target
|
||||||
|
_spriteBarrel.Rotation = Mathf.LerpAngle(_spriteBarrel.Rotation, targetAngle, (float)delta * RotationSpeed);
|
||||||
|
|
||||||
|
// Check if we're facing the target and can attack
|
||||||
|
if (Mathf.Abs(Mathf.Wrap(targetAngle - _spriteBarrel.Rotation, -Mathf.Pi, Mathf.Pi)) < 0.1f)
|
||||||
|
TryAttack(nearestEnemy.GlobalPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_hasTarget = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryAttack(Vector2 targetPosition)
|
||||||
|
{
|
||||||
|
if (_attackTimer <= 0 && BulletScene != null && _barrelTip != null)
|
||||||
|
{
|
||||||
|
// Create bullet instance
|
||||||
|
var bullet = BulletScene.Instantiate<Bullet>();
|
||||||
|
|
||||||
|
// Calculate direction and rotation
|
||||||
|
var direction = (targetPosition - _barrelTip.GlobalPosition).Normalized();
|
||||||
|
var bulletRotation = direction.Angle();
|
||||||
|
|
||||||
|
// Set bullet position and rotation
|
||||||
|
GetTree().CurrentScene.AddChild(bullet);
|
||||||
|
bullet.GlobalPosition = _barrelTip.GlobalPosition;
|
||||||
|
bullet.Rotation = bulletRotation; // Use the calculated rotation
|
||||||
|
|
||||||
|
// Initialize bullet with direction and damage
|
||||||
|
bullet.Initialize(
|
||||||
|
direction,
|
||||||
|
bullet.GlobalPosition,
|
||||||
|
bulletRotation, // Pass the calculated rotation
|
||||||
|
1 // Pass the turret's collision layer to ignore
|
||||||
|
);
|
||||||
|
bullet.Damage = Damage;
|
||||||
|
bullet.Speed = BulletSpeed;
|
||||||
|
bullet.MaxDistance = AttackRange * 1.5f; // Bullets can travel slightly further than attack range
|
||||||
|
|
||||||
|
// Reset attack cooldown
|
||||||
|
_attackTimer = AttackCooldown;
|
||||||
|
|
||||||
|
GD.Print("[Turret] Turret firing!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
Scripts/Tiles/TurretTile.cs.uid
Normal file
1
Scripts/Tiles/TurretTile.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://n5g6i0uovxfk
|
BIN
Sounds/Events/Building.wav
Normal file
BIN
Sounds/Events/Building.wav
Normal file
Binary file not shown.
24
Sounds/Events/Building.wav.import
Normal file
24
Sounds/Events/Building.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://d1trbqrntmuij"
|
||||||
|
path="res://.godot/imported/Building.wav-b8766581fd25a206c63f47b13bd2e2f5.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Sounds/Events/Building.wav"
|
||||||
|
dest_files=["res://.godot/imported/Building.wav-b8766581fd25a206c63f47b13bd2e2f5.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
BIN
Sounds/Events/Canceled.wav
Normal file
BIN
Sounds/Events/Canceled.wav
Normal file
Binary file not shown.
24
Sounds/Events/Canceled.wav.import
Normal file
24
Sounds/Events/Canceled.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://chn8ux4two1kd"
|
||||||
|
path="res://.godot/imported/Canceled.wav-6d441d9a898b5cd9be4c0664a1f489ed.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Sounds/Events/Canceled.wav"
|
||||||
|
dest_files=["res://.godot/imported/Canceled.wav-6d441d9a898b5cd9be4c0664a1f489ed.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
BIN
Sounds/Events/CannotDeployHere.wav
Normal file
BIN
Sounds/Events/CannotDeployHere.wav
Normal file
Binary file not shown.
24
Sounds/Events/CannotDeployHere.wav.import
Normal file
24
Sounds/Events/CannotDeployHere.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://7u1gw7lt5xd1"
|
||||||
|
path="res://.godot/imported/CannotDeployHere.wav-a9ec27508654f74d03ac7b8c8037059c.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Sounds/Events/CannotDeployHere.wav"
|
||||||
|
dest_files=["res://.godot/imported/CannotDeployHere.wav-a9ec27508654f74d03ac7b8c8037059c.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
BIN
Sounds/Events/InsufficientFunds.wav
Normal file
BIN
Sounds/Events/InsufficientFunds.wav
Normal file
Binary file not shown.
24
Sounds/Events/InsufficientFunds.wav.import
Normal file
24
Sounds/Events/InsufficientFunds.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://bemxvqcettqgp"
|
||||||
|
path="res://.godot/imported/InsufficientFunds.wav-7aba215cb1cd04a5285a5e9908999906.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Sounds/Events/InsufficientFunds.wav"
|
||||||
|
dest_files=["res://.godot/imported/InsufficientFunds.wav-7aba215cb1cd04a5285a5e9908999906.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
BIN
Sounds/Events/NotReady.wav
Normal file
BIN
Sounds/Events/NotReady.wav
Normal file
Binary file not shown.
24
Sounds/Events/NotReady.wav.import
Normal file
24
Sounds/Events/NotReady.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://dogbl6tfealwg"
|
||||||
|
path="res://.godot/imported/NotReady.wav-2cfa5f22feed110ca411d6478060fdea.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Sounds/Events/NotReady.wav"
|
||||||
|
dest_files=["res://.godot/imported/NotReady.wav-2cfa5f22feed110ca411d6478060fdea.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
@@ -15,8 +15,15 @@ run/main_scene="uid://c22aprj452aha"
|
|||||||
config/features=PackedStringArray("4.4", "C#", "GL Compatibility")
|
config/features=PackedStringArray("4.4", "C#", "GL Compatibility")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
ResourceManager="res://Scripts/System/ResourceManager.cs"
|
||||||
|
DiInitializer="*res://Scripts/AutoLoad/DIInitializer.cs"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1920
|
||||||
|
window/size/viewport_height=1080
|
||||||
window/stretch/mode="viewport"
|
window/stretch/mode="viewport"
|
||||||
|
|
||||||
[dotnet]
|
[dotnet]
|
||||||
@@ -75,8 +82,14 @@ switch_tile={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":93,"key_label":0,"unicode":93,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":93,"key_label":0,"unicode":93,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
toggle_build={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": []
|
||||||
|
}
|
||||||
|
|
||||||
[rendering]
|
[rendering]
|
||||||
|
|
||||||
anti_aliasing/quality/msaa_2d=3
|
anti_aliasing/quality/msaa_2d=3
|
||||||
|
anti_aliasing/quality/msaa_3d=3
|
||||||
|
anti_aliasing/quality/screen_space_aa=1
|
||||||
anti_aliasing/quality/use_taa=true
|
anti_aliasing/quality/use_taa=true
|
||||||
|
Reference in New Issue
Block a user