Compare commits

..

23 Commits

Author SHA1 Message Date
2c5e0459ad 💄 Optimize stuff 2025-08-31 19:30:16 +08:00
f2c243ecf6 🍱 Retexture the enemies 2025-08-31 18:54:30 +08:00
b424aafeab ♻️ Optimizations of the various system
🍱 Retexture of the enemy portal
2025-08-31 18:26:45 +08:00
09511b37c9 Reactor, and enemies attacking the tiles 2025-08-31 14:30:18 +08:00
c72353716f ♻️ Rebuild with DI 2025-08-31 01:44:18 +08:00
1cc941d893 Hud basis 2025-08-30 23:41:49 +08:00
88647b1c41 🐛 Bug fixes 2025-08-30 23:26:19 +08:00
7438ba407a Turret shooting and damaing 2025-08-30 13:05:50 +08:00
1c6c03cd41 Turret tile basis 2025-08-30 12:03:26 +08:00
32f96d488d Enemy and nest 2025-08-30 02:06:58 +08:00
630dbf0800 :bugS: Fix natural resource generate missing iron 2025-08-29 18:11:00 +08:00
ac1d8cfab9 ♻️ Better miner tile 2025-08-29 18:05:30 +08:00
60b6d6f989 💄 Optimize the unable to place sound plays 2025-08-29 17:54:31 +08:00
483773f042 Merge stack
💫 Better miner tile vfx
2025-08-29 17:44:49 +08:00
56cd4c2db2 Miner tile 2025-08-29 16:59:59 +08:00
7720e74a3d New sfx and vfx and features in building 2025-08-29 14:36:05 +08:00
885d2c0075 Toggle build
🐛 Fix world gen tiles didn't align with placed ones
2025-08-29 13:46:18 +08:00
8073ed23c0 Multi threading world gen 2025-08-29 13:38:46 +08:00
862f11d445 Infinite world gen 2025-08-29 13:35:03 +08:00
9408651ea8 Item magnet 2025-08-29 12:41:08 +08:00
75cd807187 💄 Optimize vfx of item pickup 2025-08-29 12:39:23 +08:00
fac5e5a597 Item pickup 2025-08-29 12:33:10 +08:00
850628ca72 Complete resource manager 2025-08-29 00:10:19 +08:00
75 changed files with 2765 additions and 237 deletions

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://caoyy241xsoer"
path="res://.godot/imported/OreIronItem.png-9b5b0a6a8d0e3616bac40fde1dc14f49.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Items/OreIronItem.png"
dest_files=["res://.godot/imported/OreIronItem.png-9b5b0a6a8d0e3616bac40fde1dc14f49.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

BIN
Assets/Items/StoneItem.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3mn15csjooch"
path="res://.godot/imported/StoneItem.png-cc89310dd113aeb437107818aa1ce10c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Assets/Items/StoneItem.png"
dest_files=["res://.godot/imported/StoneItem.png-cc89310dd113aeb437107818aa1ce10c.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

View File

@@ -12,15 +12,45 @@
"miner": { "miner": {
"scene": "res://Scenes/Tiles/MinerTile.tscn", "scene": "res://Scenes/Tiles/MinerTile.tscn",
"cost": { "cost": {
"wood": 10, "stone": 10,
"stone": 3 "ore_iron": 3
}, },
"durability": 200, "durability": 200,
"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": {},

4
Data/ItemTextures.json Normal file
View File

@@ -0,0 +1,4 @@
{
"stone": "res://Assets/Items/StoneItem.png",
"ore_iron": "res://Assets/Items/OreIronItem.png"
}

BIN
Scenes/Entities/Bullet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

View 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

View 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)

View File

@@ -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="."]

View File

@@ -1,28 +1,30 @@
[gd_scene load_steps=6 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://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://byv2vu0k2drdd" path="res://Scenes/System/HUD.tscn" id="8_hud_scene"]
[node name="Root" type="Node2D"] [node name="Root" type="Node2D"]
[node name="BuildingRegistry" type="Node" parent="."] [node name="NaturalResourceGenerator" type="Node2D" 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", "Registry")]
script = ExtResource("2_sxhdm") script = ExtResource("2_sxhdm")
Grid = NodePath("../GridSystem")
Registry = NodePath("../BuildingRegistry")
[node name="Player" parent="." instance=ExtResource("3_oss8w")] [node name="Player" parent="." instance=ExtResource("3_oss8w")]
scale = Vector2(0.35, 0.35) scale = Vector2(0.35, 0.35)
[node name="HUD" parent="." instance=ExtResource("8_hud_scene")]
[node name="ItemPickup" parent="." instance=ExtResource("7_is6ib")]
position = Vector2(-496, -245)
ItemId = "stone"
Quantity = 64
[node name="ItemPickup2" parent="." instance=ExtResource("7_is6ib")]
position = Vector2(-495, 5)
ItemId = "ore_iron"
Quantity = 16

13
Scenes/System/HUD.tscn Normal file
View 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

View File

@@ -0,0 +1,26 @@
[gd_scene load_steps=4 format=3 uid="uid://xwkplaxmye3v"]
[ext_resource type="Texture2D" uid="uid://d3mn15csjooch" path="res://Assets/Items/StoneItem.png" id="1_4weev"]
[ext_resource type="Script" uid="uid://qgcue2doj2lf" path="res://Scripts/System/ItemPickup.cs" id="1_ps3kh"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_4weev"]
size = Vector2(50, 50)
[node name="ItemPickup" type="Area2D"]
script = ExtResource("1_ps3kh")
MagnetRange = 96.0
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.05, 0.05)
texture = ExtResource("1_4weev")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View 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

View 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")

View File

@@ -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"

View File

@@ -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="."]

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

View 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

View 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)

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

View 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

View 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.");
}
}

View File

@@ -0,0 +1 @@
uid://cr2a8w6ur4uei

View 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;
}
}

View File

@@ -0,0 +1 @@
uid://vgx2a8gm7l8b

13
Scripts/Entities/Enemy.cs Normal file
View 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
}

View File

@@ -0,0 +1 @@
uid://cvsmy820b8dwl

View 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);
}
}

View File

@@ -0,0 +1 @@
uid://6oduws4kbdlf

View File

@@ -1,4 +1,4 @@
using System; using AceFieldNewHorizon.Scripts.System;
using Godot; using Godot;
namespace AceFieldNewHorizon.Scripts.Entities; namespace AceFieldNewHorizon.Scripts.Entities;
@@ -18,6 +18,8 @@ public partial class Player : CharacterBody2D
[Export] public float ZoomAcceleration = 0.05f; [Export] public float ZoomAcceleration = 0.05f;
[Export] public float ZoomDecay = 0.9f; [Export] public float ZoomDecay = 0.9f;
[Export] public float ZoomSmoothing = 10.0f; [Export] public float ZoomSmoothing = 10.0f;
public ResourceManager Inventory { get; private set; }
private Camera2D _camera; private Camera2D _camera;
private Vector2 _cameraTargetZoom = Vector2.One; private Vector2 _cameraTargetZoom = Vector2.One;
@@ -29,6 +31,11 @@ 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(NaturalResourceGenerator.ChunkTrackerGroupName);
} }
public override void _Input(InputEvent @event) public override void _Input(InputEvent @event)
@@ -54,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;
@@ -135,4 +138,10 @@ public partial class Player : CharacterBody2D
// Apply the movement // Apply the movement
MoveAndSlide(); MoveAndSlide();
} }
public void AddItem(string itemId, int quantity)
{
Inventory.AddResource(itemId, quantity);
GD.Print($"[Player] Picked up {quantity} x {itemId}. Total: {Inventory.GetResourceAmount(itemId)}");
}
} }

27
Scripts/Root.cs Normal file
View 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
View File

@@ -0,0 +1 @@
uid://dmint8ii0oj5g

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Godot; using Godot;
using static System.Int32;
namespace AceFieldNewHorizon.Scripts.System; namespace AceFieldNewHorizon.Scripts.System;
@@ -39,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)
@@ -70,7 +71,7 @@ public partial class BuildingRegistry : Node
return; return;
} }
var dict = (Godot.Collections.Dictionary)json.Data; var dict = json.Data.AsGodotDictionary();
foreach (string key in dict.Keys) foreach (string key in dict.Keys)
{ {
@@ -101,12 +102,22 @@ public partial class BuildingRegistry : Node
{ {
int val; int val;
var obj = costDict[mat]; var obj = costDict[mat];
if (obj.VariantType == Variant.Type.PackedInt64Array) switch (obj.VariantType)
val = (int)obj.AsInt64(); {
else if (obj.VariantType == Variant.Type.PackedInt32Array) case Variant.Type.PackedInt64Array:
val = obj.AsInt32(); val = (int)obj.AsInt64();
else break;
int.TryParse(obj.ToString(), out val); case Variant.Type.PackedInt32Array:
val = obj.AsInt32();
break;
case Variant.Type.Float:
val = (int)obj.AsDouble();
break;
default:
if (!TryParse(obj.ToString(), out val))
GD.PrintErr($"[BuildingRegistry] Failed to parse cost for '{key}': {obj.ToString()}");
break;
}
cost[mat] = val; cost[mat] = val;
} }
} }
@@ -120,7 +131,7 @@ public partial class BuildingRegistry : Node
else if (dObj.VariantType == Variant.Type.PackedInt32Array) else if (dObj.VariantType == Variant.Type.PackedInt32Array)
durability = dObj.AsInt32(); durability = dObj.AsInt32();
else else
int.TryParse(dObj.ToString(), out durability); TryParse(dObj.ToString(), out durability);
} }
// Parse buildTime // Parse buildTime

View 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.");
}
}

View File

@@ -0,0 +1 @@
uid://hppsxnesg0ys

View File

@@ -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())
{ {
if (_layers[layer].ContainsKey(cell)) var (building, buildingSize, buildingRotation) = _layers[layer][cell];
_layers[layer].Remove(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())
{
_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
View 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;
}
}
}

View File

@@ -0,0 +1 @@
uid://ddoqqcg77f60v

View File

@@ -0,0 +1,187 @@
using System.Collections;
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.System;
public partial class ItemPickup : Area2D
{
[Signal]
public delegate void StackMergedEventHandler(string itemId, int totalQuantity);
public const string PickupGroupName = "ItemPickupTarget";
[Export] public string ItemId { get; set; } = "";
[Export] public int Quantity { get; set; } = 1;
[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 Label _quantityLabel;
private Sprite2D _shadowSprite;
private Node2D _playerTarget;
// Called when the node enters the scene tree
public override void _Ready()
{
BodyEntered += OnEntered;
AreaEntered += OnEntered;
_sprite = GetNode<Sprite2D>("Sprite2D");
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()
{
var file = FileAccess.Open("res://Data/ItemTextures.json", FileAccess.ModeFlags.Read);
if (file == null)
{
GD.PrintErr("Failed to open ItemTextures.json");
return;
}
var text = file.GetAsText();
file.Close();
var json = new Json();
var err = json.Parse(text);
if (err != Error.Ok)
{
GD.PrintErr(json.GetErrorMessage());
return;
}
var dict = json.Data.AsGodotDictionary();
if (!dict.TryGetValue(ItemId, out var value))
return;
var texturePath = value.AsString();
if (string.IsNullOrEmpty(texturePath))
return;
var texture = GD.Load<Texture2D>(texturePath);
if (texture == null)
return;
_sprite.Texture = texture;
}
private void OnEntered(Node body)
{
if (body is ItemPickup itemStack)
{
// Only process the merge for the item with the lower instance ID to prevent double merging
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)
QueueFree();
}
}
}

View File

@@ -0,0 +1 @@
uid://qgcue2doj2lf

View File

@@ -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;
[Export] public int MapHeight = 100;
[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 MinStoneVeinSize = 1;
[Export] public int MaxStoneVeinSize = 5;
[Export] public int MinIronVeinSize = 1;
[Export] public int MaxIronVeinSize = 3;
[Export] public int Seed;
private RandomNumberGenerator _rng; public GridManager Grid { get; private set; }
private readonly List<Vector2I> _groundTiles = []; public BuildingRegistry Registry { get; private set; }
private readonly List<Vector2I> _stoneTiles = [];
private readonly List<Vector2I> _ironTiles = [];
public override void _Ready() [Export] public int ChunkSize = 16;
{ [Export] public int LoadDistance = 2; // Number of chunks to load in each direction
_rng = new RandomNumberGenerator(); [Export] public float StoneDensity = 0.1f;
_rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); [Export] public float IronDensity = 0.03f;
[Export] public int MinStoneVeinSize = 1;
GenerateTerrain(); [Export] public int MaxStoneVeinSize = 5;
PlaceResources(); [Export] public int MinIronVeinSize = 1;
} [Export] public int MaxIronVeinSize = 3;
[Export] public int Seed;
private void GenerateTerrain() [Export] public bool SpawnEnemyNest = true;
{ [Export] public int MinDistanceFromOrigin = 20; // Minimum distance from world origin (0,0)
// First pass: Generate base ground tiles [Export] public int MaxDistanceFromOrigin = 50; // Maximum distance from world origin
for (int x = 0; x < MapWidth; x++)
{
for (int y = 0; y < MapHeight; y++)
{
var cell = new Vector2I(x, y);
_groundTiles.Add(cell);
PlaceTile("ground", cell);
}
}
}
private void PlaceResources() private const string LogPrefix = "[NaturalGeneration]";
{
// Create a copy of ground tiles for iteration
var groundTilesToProcess = new List<Vector2I>(_groundTiles);
// Place stone veins
foreach (var cell in groundTilesToProcess)
{
if (_rng.Randf() < StoneDensity)
{
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
PlaceVein(cell, "stone", veinSize, _stoneTiles);
}
}
// Create a copy of stone tiles for iteration private RandomNumberGenerator _rng;
var stoneTilesToProcess = new List<Vector2I>(_stoneTiles); private readonly Dictionary<Vector2I, ChunkData> _loadedChunks = new();
private Vector2I _lastPlayerChunk = new(-1000, -1000);
// Place iron veins within stone
foreach (var stoneCell in stoneTilesToProcess)
{
if (_rng.Randf() < IronDensity)
{
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
PlaceVein(stoneCell, "ore_iron", veinSize, _ironTiles);
}
}
}
private void PlaceVein(Vector2I startCell, string tileType, int maxVeinSize, ICollection<Vector2I> tileList) private Player _player;
{
var queue = new Queue<Vector2I>();
var placed = new HashSet<Vector2I>();
queue.Enqueue(startCell);
int placedCount = 0; private readonly ConcurrentQueue<Action> _mainThreadActions = new();
private Task _generationTask;
while (queue.Count > 0 && placedCount < maxVeinSize) private bool _isRunning = true;
{
var cell = queue.Dequeue();
if (placed.Contains(cell)) continue;
if (!IsInBounds(cell)) continue;
switch (tileType) public override void _Ready()
{ {
// For iron, make sure we're placing on stone Grid = DependencyInjection.Container.GetInstance<GridManager>();
case "ore_iron" when !_stoneTiles.Contains(cell): Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>();
continue; _rng = new RandomNumberGenerator();
// Remove from previous layer if needed _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi());
case "ore_iron" when _stoneTiles.Contains(cell):
_stoneTiles.Remove(cell); // Test if building registry is assigned
break; if (Registry == null)
case "stone" when _groundTiles.Contains(cell): {
_groundTiles.Remove(cell); GD.PrintErr($"{LogPrefix} BuildingRegistry is not assigned!");
break; return;
} }
// Test if enemy_portal is in the registry
var testBuilding = Registry.GetBuilding("enemy_portal");
if (testBuilding == null)
{
GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!");
}
else
{
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)
{
GD.Print($"{LogPrefix} Attempting to spawn enemy nest...");
SpawnRandomEnemyNest();
}
}
PlaceTile(tileType, cell); public override void _Process(double delta)
tileList.Add(cell); {
placed.Add(cell); // Process any pending main thread actions
placedCount++; while (_mainThreadActions.TryDequeue(out var action))
{
action.Invoke();
}
// Add adjacent cells to queue // Rest of your existing _Process code
for (var dx = -1; dx <= 1; dx++) _player = GetTree().GetFirstNodeInGroup(ChunkTrackerGroupName) as Player;
{ if (_player != null)
for (var dy = -1; dy <= 1; dy++) {
{ UpdatePlayerPosition(_player.GlobalPosition);
if (dx == 0 && dy == 0) continue; // Skip self }
var neighbor = new Vector2I(cell.X + dx, cell.Y + dy); else
if (!placed.Contains(neighbor) && IsInBounds(neighbor)) {
{ GD.PrintErr($"{LogPrefix} Player not found in group: {ChunkTrackerGroupName}");
queue.Enqueue(neighbor); }
} }
}
}
}
}
private bool IsInBounds(Vector2I cell) private void UpdatePlayerPosition(Vector2 playerPosition)
{ {
return cell.X >= 0 && cell.X < MapWidth && var playerChunk = WorldToChunkCoords(playerPosition);
cell.Y >= 0 && cell.Y < MapHeight;
}
private void PlaceTile(string tileType, Vector2I cell) if (playerChunk == _lastPlayerChunk) return;
{ _lastPlayerChunk = playerChunk;
var building = Registry.GetBuilding(tileType);
if (building == null) return;
var scene = building.Scene; // Start generation in background task if not already running
var instance = (BaseTile)scene.Instantiate(); if (_generationTask == null || _generationTask.IsCompleted)
{
// Match PlacementManager's positioning logic _generationTask = Task.Run(() => GenerateChunksAroundPlayer(playerChunk));
var rotatedSize = building.GetRotatedSize(0f); }
var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); }
instance.Position = GridUtils.GridToWorld(cell) + offset;
instance.ZIndex = (int)building.Layer;
AddChild(instance);
// Make sure to use the building's size from the registry private void GenerateChunksAroundPlayer(Vector2I playerChunk)
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); {
} 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);
}
}
}
// Generate chunks in background
foreach (var chunkPos in chunksToLoad)
{
if (!_isRunning) return;
GenerateChunkInBackground(chunkPos);
}
}
private void GenerateChunkInBackground(Vector2I chunkPos)
{
// 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)
{
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
PlaceVeinInBackground(cell, "stone", veinSize, chunkData.StoneTiles);
}
else if (_rng.Randf() < IronDensity)
{
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
PlaceVeinInBackground(cell, "ore_iron", veinSize, chunkData.IronTiles);
}
}
}
// 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 placed = new HashSet<Vector2I>();
var queue = new Queue<Vector2I>();
queue.Enqueue(startCell);
int placedCount = 0;
while (queue.Count > 0 && placedCount < maxSize && _isRunning)
{
var cell = queue.Dequeue();
if (placed.Contains(cell)) continue;
// Schedule tile placement on main thread
_mainThreadActions.Enqueue(() =>
{
if (PlaceTile(tileType, cell))
{
tileList.Add(cell);
}
});
placed.Add(cell);
placedCount++;
// 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 dy = -1; dy <= 1; dy++)
{
var checkPos = new Vector2I(chunkPos.X + dx, chunkPos.Y + dy);
if (_loadedChunks.ContainsKey(checkPos))
{
return true;
}
}
}
return false;
}
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);
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;
if (scene == null)
{
GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}");
return false;
}
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 offset = GridUtils.GetCenterOffset(rotatedSize, 0f);
instance.ZIndex = (int)building.Layer;
instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset;
AddChild(instance);
// Occupy the appropriate area based on building size
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; } = [];
}

View File

@@ -8,35 +8,67 @@ 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 BuildingRegistry Registry { get; set; } public ResourceManager Inventory { get; private set; }
[Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor public BuildingRegistry Registry { get; private set; }
private static readonly List<string> BuildableTiles = ["wall", "miner"]; [Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor
private readonly List<BuildTask> _activeBuilds = new(); [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 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()
{ {
// Remove all completed builds // Remove all completed builds
_activeBuilds.RemoveAll(task => task.IsCompleted); var completed = _buildTasks.Where(kvp => kvp.Value.IsCompleted)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in completed)
{
_buildTasks.Remove(key);
}
// If no builds left, play the completion sound // If no builds left, play the completion sound
if (_activeBuilds.Count == 0) if (_buildTasks.Count == 0)
{ {
_completionSound.Play(); _completionSound.Play();
} }
@@ -46,30 +78,34 @@ public partial class PlacementManager : Node2D
private bool CanStartNewBuild() private bool CanStartNewBuild()
{ {
// Remove completed builds // Remove completed builds
_activeBuilds.RemoveAll(task => task.IsCompleted); var completed = _buildTasks.Where(kvp => kvp.Value.IsCompleted)
return _activeBuilds.Count < MaxConcurrentBuilds; .Select(kvp => kvp.Key)
} .ToList();
foreach (var key in completed)
private class BuildTask : IDisposable
{
private readonly Action _onCompleted;
public bool IsCompleted { get; private set; }
public BuildTask(Action onCompleted)
{ {
_onCompleted = onCompleted; _buildTasks.Remove(key);
} }
public void Complete() return _buildTasks.Count < MaxConcurrentBuilds;
}
private class BuildTask(Action onCompleted) : IDisposable
{
public bool IsCompleted { get; private set; }
public bool WasCancelled { get; private set; }
public void Complete(bool wasCancelled = false)
{ {
if (IsCompleted) return; if (IsCompleted) return;
IsCompleted = true; IsCompleted = true;
_onCompleted?.Invoke(); WasCancelled = wasCancelled;
onCompleted?.Invoke();
} }
public void Dispose() public void Dispose()
{ {
Complete(); Complete(true); // Mark as cancelled if disposed before completion
} }
} }
@@ -142,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);
@@ -177,17 +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; 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;
}
// Consume resources first
if (!ConsumeBuildingResources(_currentBuildingId))
{
_insufficientFundsSound.Play();
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;
@@ -195,13 +255,35 @@ public partial class PlacementManager : Node2D
buildingInstance.Position = _ghostBuilding.Position; buildingInstance.Position = _ghostBuilding.Position;
AddChild(buildingInstance); AddChild(buildingInstance);
// If we get here, area is free, so we can safely occupy it
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);
_activeBuilds.Add(buildTask); _buildTasks[buildingInstance] = buildTask;
buildingInstance.StartConstruction(building.BuildTime, buildTask.Complete);
// Play building sound only when adding to an empty queue
if (wasQueueEmpty)
_buildingSound.Play();
buildingInstance.StartConstruction(building.BuildTime, () =>
{
// On construction complete
if (_buildTasks.TryGetValue(buildingInstance, out var task))
{
if (task.WasCancelled)
{
RefundBuildingResources(_currentBuildingId);
Grid.FreeArea(_hoveredCell, building.Size, _currentRotation, building.Layer);
buildingInstance.QueueFree();
}
task.Complete();
_buildTasks.Remove(buildingInstance);
}
});
} }
} }
@@ -209,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);
} }
@@ -226,14 +327,79 @@ 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()
{
base._ExitTree();
_buildTasks.Clear();
}
private bool CanAffordBuilding(string buildingId)
{
if (Inventory == null) return false;
var buildingData = Registry.GetBuilding(buildingId);
if (buildingData == null) return false;
// Check if we have enough of each required resource
foreach (var (resourceId, amount) in buildingData.Cost)
if (!Inventory.HasResource(resourceId, amount))
return false;
return true;
}
private bool ConsumeBuildingResources(string buildingId)
{
if (Inventory == null) return false;
var buildingData = Registry.GetBuilding(buildingId);
if (buildingData == null) return false;
// First verify we can afford it
if (!CanAffordBuilding(buildingId)) return false;
// Then consume each resource
foreach (var (resourceId, amount) in buildingData.Cost)
Inventory.RemoveResource(resourceId, amount);
return true;
}
private void RefundBuildingResources(string buildingId)
{
if (Inventory == null) return;
var buildingData = Registry.GetBuilding(buildingId);
if (buildingData == null) return;
// Refund each resource
foreach (var (resourceId, amount) in buildingData.Cost)
{
Inventory.AddResource(resourceId, amount);
}
}
private bool CanPlaceBuilding() private bool CanPlaceBuilding()
{ {
var buildingData = Registry.GetBuilding(_currentBuildingId); var buildingData = Registry.GetBuilding(_currentBuildingId);
if (buildingData == null) return false; if (buildingData == null) return false;
// Check if rotation is allowed // Check if we can afford the building
if (!buildingData.IsRotationAllowed(_currentRotation)) if (!CanAffordBuilding(_currentBuildingId)) return false;
return false;
// Check if area is free // Check if area is free
var rotatedSize = buildingData.GetRotatedSize(_currentRotation); var rotatedSize = buildingData.GetRotatedSize(_currentRotation);
@@ -250,6 +416,11 @@ public partial class PlacementManager : Node2D
_ => [] _ => []
}; };
} }
private bool IsAreaFree(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer)
{
return !Grid.IsAreaOccupied(topLeft, size, rotation, GetBlockingLayers(layer));
}
} }
public static class GridManagerExtensions public static class GridManagerExtensions
@@ -257,32 +428,28 @@ 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
for (var x = 0; x < 100; x++) // Arbitrary max size
{ {
// Find the top-left position of the building for (var y = 0; y < 100; y++)
for (int x = 0; x < 100; x++) // Arbitrary max size
{ {
for (int y = 0; y < 100; y++) var checkCell = new Vector2I(cell.X - x, cell.Y - y);
{ if (grid.GetTileAtCell(checkCell, layer) != building) continue;
var checkCell = new Vector2I(cell.X - x, cell.Y - y); // Found the top-left corner, now find the size
if (grid.GetBuildingAtCell(checkCell, layer) == building) var size = Vector2I.One;
{ // Search right
// Found the top-left corner, now find the size while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
var size = Vector2I.One; building)
// Search right size.X++;
while (grid.GetBuildingAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == // Search down
building) while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
size.X++; building)
// Search down size.Y++;
while (grid.GetBuildingAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
building)
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);
}
}
} }
} }

View File

@@ -0,0 +1,150 @@
using Godot;
using System.Collections.Generic;
using System.Linq;
namespace AceFieldNewHorizon.Scripts.System;
public partial class ResourceManager : Node
{
// Dictionary to store resources with their IDs and quantities
private readonly Dictionary<string, int> _resources = new();
private static readonly StringName LoggerName = "ResourceManager";
// Event for when resource amounts change
[Signal]
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()
{
base._Ready();
GD.Print($"[{LoggerName}] ResourceManager initialized");
}
// Add resources of a specific type
public void AddResource(string resourceId, int amount = 1)
{
if (amount <= 0)
{
GD.PushWarning($"[{LoggerName}] Attempted to add non-positive amount ({amount}) for resource: {resourceId}");
return;
}
bool isNew = !_resources.ContainsKey(resourceId);
if (!_resources.TryAdd(resourceId, amount))
{
var oldAmount = _resources[resourceId];
_resources[resourceId] += amount;
GD.Print($"[{LoggerName}] Added {amount} {resourceId}. New total: {_resources[resourceId]} (was {oldAmount})");
}
else
{
GD.Print($"[{LoggerName}] Added new resource: {resourceId} with amount: {amount}");
}
EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]);
EmitSignal(nameof(OnResourcePickedUp), resourceId, amount);
}
// Remove resources of a specific type
public bool RemoveResource(string resourceId, int amount = 1)
{
if (amount <= 0)
{
GD.PushWarning($"[{LoggerName}] Attempted to remove non-positive amount ({amount}) for resource: {resourceId}");
return false;
}
if (!_resources.ContainsKey(resourceId))
{
GD.Print($"[{LoggerName}] Failed to remove {amount} {resourceId}: Resource not found");
return false;
}
if (_resources[resourceId] < amount)
{
GD.Print($"[{LoggerName}] Insufficient {resourceId}. Requested: {amount}, Available: {_resources[resourceId]}");
return false;
}
var oldAmount = _resources[resourceId];
_resources[resourceId] -= amount;
GD.Print($"[{LoggerName}] Removed {amount} {resourceId}. New total: {_resources[resourceId]} (was {oldAmount})");
// Remove the entry if quantity reaches zero
if (_resources[resourceId] <= 0)
{
_resources.Remove(resourceId);
GD.Print($"[{LoggerName}] Removed resource entry for {resourceId} (reached zero)");
EmitSignal(nameof(OnResourceChanged), resourceId, 0);
}
else
{
EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]);
}
return true;
}
// Get the current amount of a specific resource
public int GetResourceAmount(string resourceId)
{
// Silently return 0 for non-existent resources
return _resources.GetValueOrDefault(resourceId, 0);
}
// Check if there are at least 'amount' of a specific resource
public bool HasResource(string resourceId, int amount = 1)
{
if (amount <= 0) return true; // Always true for zero or negative amounts
var exists = _resources.TryGetValue(resourceId, out var currentAmount);
var hasEnough = exists && currentAmount >= amount;
return hasEnough;
}
// Get all resources as a read-only dictionary
public IReadOnlyDictionary<string, int> GetAllResources()
{
GD.Print($"[{LoggerName}] Getting all resources. Total types: {_resources.Count}");
return new Dictionary<string, int>(_resources);
}
// Clear all resources
public void Clear()
{
GD.Print($"[{LoggerName}] Clearing all resources. Total types before clear: {_resources.Count}");
var keys = new List<string>(_resources.Keys);
foreach (var resourceId in keys)
{
_resources[resourceId] = 0;
EmitSignal(nameof(OnResourceChanged), resourceId, 0);
}
_resources.Clear();
GD.Print($"[{LoggerName}] All resources cleared");
}
// Helper method to log current resource state
public void LogResourceState()
{
if (_resources.Count == 0)
{
GD.Print($"[{LoggerName}] No resources currently stored");
return;
}
GD.Print($"[{LoggerName}] Current resource state:");
foreach (var (id, amount) in _resources.OrderBy(x => x.Key))
{
GD.Print($" - {id}: {amount}");
}
}
}

View File

@@ -0,0 +1 @@
uid://dfi2snip78eq6

View File

@@ -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)
{ {
if (_progressOverlay == null || _sprite?.Texture == null) IsConstructing = true;
if (_collisionShape != null)
_collisionShape.Disabled = true;
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
@@ -79,9 +186,17 @@ 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();

View 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;
}
}

View File

@@ -0,0 +1 @@
uid://26hl5mk4mqur

View File

@@ -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();
} }

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -0,0 +1 @@
uid://c4k3ottt7j3b1

127
Scripts/Tiles/TurretTile.cs Normal file
View 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!");
}
}
}

View File

@@ -0,0 +1 @@
uid://n5g6i0uovxfk

BIN
Sounds/Events/Building.wav Normal file

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

View File

@@ -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