Compare commits
	
		
			23 Commits
		
	
	
		
			0d3c84491b
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2c5e0459ad | |||
| f2c243ecf6 | |||
| b424aafeab | |||
| 09511b37c9 | |||
| c72353716f | |||
| 1cc941d893 | |||
| 88647b1c41 | |||
| 7438ba407a | |||
| 1c6c03cd41 | |||
| 32f96d488d | |||
| 630dbf0800 | |||
| ac1d8cfab9 | |||
| 60b6d6f989 | |||
| 483773f042 | |||
| 56cd4c2db2 | |||
| 7720e74a3d | |||
| 885d2c0075 | |||
| 8073ed23c0 | |||
| 862f11d445 | |||
| 9408651ea8 | |||
| 75cd807187 | |||
| fac5e5a597 | |||
| 850628ca72 | 
| @@ -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> | ||||||
							
								
								
									
										
											BIN
										
									
								
								Assets/Items/OreIronItem.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 887 KiB | 
							
								
								
									
										34
									
								
								Assets/Items/OreIronItem.png.import
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| After Width: | Height: | Size: 699 KiB | 
							
								
								
									
										34
									
								
								Assets/Items/StoneItem.png.import
									
									
									
									
									
										Normal 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 | ||||||
| @@ -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
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | { | ||||||
|  | 	"stone": "res://Assets/Items/StoneItem.png", | ||||||
|  | 	"ore_iron": "res://Assets/Items/OreIronItem.png" | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Entities/Bullet.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 659 KiB | 
							
								
								
									
										34
									
								
								Scenes/Entities/Bullet.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://b5mb8tu15rc2p" | ||||||
|  | path="res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Entities/Bullet.png" | ||||||
|  | dest_files=["res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										18
									
								
								Scenes/Entities/Bullet.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| After Width: | Height: | Size: 807 KiB | 
							
								
								
									
										34
									
								
								Scenes/Entities/Enemy.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://x4u6oatvsm8y" | ||||||
|  | path="res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Entities/Enemy.png" | ||||||
|  | dest_files=["res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										21
									
								
								Scenes/Entities/Enemy.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | [gd_scene load_steps=3 format=3 uid="uid://b3ffcucytwmk"] | ||||||
|  |  | ||||||
|  | [ext_resource type="Script" uid="uid://cvsmy820b8dwl" path="res://Scripts/Entities/Enemy.cs" id="1_jajit"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://x4u6oatvsm8y" path="res://Scenes/Entities/Enemy.png" id="2_jajit"] | ||||||
|  |  | ||||||
|  | [node name="Enemy" type="CharacterBody2D"] | ||||||
|  | collision_layer = 2 | ||||||
|  | collision_mask = 3 | ||||||
|  | script = ExtResource("1_jajit") | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionPolygon2D" parent="."] | ||||||
|  | polygon = PackedVector2Array(-2, -21, 3, -21, 24, 6, 24, 10, 20, 14, -21, 14, -24, 11, -24, 7) | ||||||
|  |  | ||||||
|  | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|  | scale = Vector2(0.05, 0.05) | ||||||
|  | texture = ExtResource("2_jajit") | ||||||
|  |  | ||||||
|  | [node name="AttackArea" type="Area2D" parent="."] | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionPolygon2D" parent="AttackArea"] | ||||||
|  | polygon = PackedVector2Array(-3, -24, 4, -24, 27, 5, 27, 14, 20, 19, -21, 19, -27, 14, -27, 5) | ||||||
| @@ -4,7 +4,9 @@ | |||||||
| [ext_resource type="Texture2D" uid="uid://jye6c2ehuxtg" path="res://Scenes/Entities/Player.png" id="1_ucweq"] | [ext_resource type="Texture2D" uid="uid://jye6c2ehuxtg" path="res://Scenes/Entities/Player.png" id="1_ucweq"] | ||||||
|  |  | ||||||
| [node name="Player" type="CharacterBody2D"] | [node name="Player" type="CharacterBody2D"] | ||||||
|  | collision_mask = 3 | ||||||
| script = ExtResource("1_08t41") | script = ExtResource("1_08t41") | ||||||
|  | MinZoom = 0.1 | ||||||
| MaxZoom = 5.0 | MaxZoom = 5.0 | ||||||
|  |  | ||||||
| [node name="Sprite2D" type="Sprite2D" parent="."] | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|   | |||||||
| @@ -1,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
									
								
							
							
						
						| @@ -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 | ||||||
							
								
								
									
										26
									
								
								Scenes/System/ItemPickup.tscn
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Tiles/EnemyPortalTile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 431 KiB | 
							
								
								
									
										34
									
								
								Scenes/Tiles/EnemyPortalTile.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://dv2xwfyshxdtp" | ||||||
|  | path="res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Tiles/EnemyPortalTile.png" | ||||||
|  | dest_files=["res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										23
									
								
								Scenes/Tiles/EnemyPortalTile.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | [gd_scene load_steps=5 format=3 uid="uid://dup2su0s3ybcy"] | ||||||
|  |  | ||||||
|  | [ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyPortalTile.cs" id="1_o543x"] | ||||||
|  | [ext_resource type="PackedScene" uid="uid://b3ffcucytwmk" path="res://Scenes/Entities/Enemy.tscn" id="2_nh7ff"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://dv2xwfyshxdtp" path="res://Scenes/Tiles/EnemyPortalTile.png" id="3_i4us4"] | ||||||
|  |  | ||||||
|  | [sub_resource type="RectangleShape2D" id="RectangleShape2D_id484"] | ||||||
|  | size = Vector2(54, 79) | ||||||
|  |  | ||||||
|  | [node name="EnemyPortal" type="StaticBody2D"] | ||||||
|  | collision_layer = 0 | ||||||
|  | collision_mask = 0 | ||||||
|  | script = ExtResource("1_o543x") | ||||||
|  | EnemyScene = ExtResource("2_nh7ff") | ||||||
|  | TileId = "enemy_portal" | ||||||
|  |  | ||||||
|  | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|  | scale = Vector2(0.1, 0.1) | ||||||
|  | texture = ExtResource("3_i4us4") | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] | ||||||
|  | position = Vector2(0, -0.5) | ||||||
|  | shape = SubResource("RectangleShape2D_id484") | ||||||
| @@ -8,6 +8,7 @@ size = Vector2(54, 54) | |||||||
|  |  | ||||||
| [node name="GroundTile" type="StaticBody2D"] | [node name="GroundTile" type="StaticBody2D"] | ||||||
| collision_layer = 0 | collision_layer = 0 | ||||||
|  | collision_mask = 0 | ||||||
| script = ExtResource("1_mqsaf") | script = ExtResource("1_mqsaf") | ||||||
| TileId = "ground" | TileId = "ground" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,15 @@ | |||||||
| [gd_scene load_steps=4 format=3 uid="uid://cbu81slklwq3u"] | [gd_scene load_steps=5 format=3 uid="uid://cbu81slklwq3u"] | ||||||
|  |  | ||||||
| [ext_resource type="Script" uid="uid://dyubkyqtpcg3a" path="res://Scripts/Tiles/MinerTile.cs" id="1_mecoy"] | [ext_resource type="Script" uid="uid://dyubkyqtpcg3a" path="res://Scripts/Tiles/MinerTile.cs" id="1_mecoy"] | ||||||
| [ext_resource type="Texture2D" uid="uid://bt6xmcgrbb078" path="res://Scenes/Tiles/MinerTile.png" id="2_mecoy"] | [ext_resource type="Texture2D" uid="uid://bt6xmcgrbb078" path="res://Scenes/Tiles/MinerTile.png" id="2_mecoy"] | ||||||
|  | [ext_resource type="PackedScene" uid="uid://xwkplaxmye3v" path="res://Scenes/System/ItemPickup.tscn" id="2_xhk0k"] | ||||||
|  |  | ||||||
| [sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"] | [sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"] | ||||||
| size = Vector2(54, 54) | size = Vector2(54, 54) | ||||||
|  |  | ||||||
| [node name="MinerTile" type="StaticBody2D"] | [node name="MinerTile" type="StaticBody2D"] | ||||||
| scale = Vector2(3, 3) |  | ||||||
| script = ExtResource("1_mecoy") | script = ExtResource("1_mecoy") | ||||||
|  | ItemPickup = ExtResource("2_xhk0k") | ||||||
| TileId = "miner" | TileId = "miner" | ||||||
|  |  | ||||||
| [node name="Sprite2D" type="Sprite2D" parent="."] | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|   | |||||||
| @@ -8,8 +8,9 @@ size = Vector2(54, 54) | |||||||
|  |  | ||||||
| [node name="OreIronTile" type="StaticBody2D"] | [node name="OreIronTile" type="StaticBody2D"] | ||||||
| collision_layer = 0 | collision_layer = 0 | ||||||
|  | collision_mask = 0 | ||||||
| script = ExtResource("1_exnim") | script = ExtResource("1_exnim") | ||||||
| TileId = "stone_iron" | TileId = "ore_iron" | ||||||
|  |  | ||||||
| [node name="Sprite2D" type="Sprite2D" parent="."] | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
| position = Vector2(1.49012e-08, -9.53674e-07) | position = Vector2(1.49012e-08, -9.53674e-07) | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Tiles/ReactorTile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 544 KiB | 
							
								
								
									
										34
									
								
								Scenes/Tiles/ReactorTile.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://fg03qxqphp7n" | ||||||
|  | path="res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Tiles/ReactorTile.png" | ||||||
|  | dest_files=["res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										26
									
								
								Scenes/Tiles/ReactorTile.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | |||||||
|  | [gd_scene load_steps=4 format=3 uid="uid://w6ni678js7cu"] | ||||||
|  |  | ||||||
|  | [ext_resource type="Script" uid="uid://c4k3ottt7j3b1" path="res://Scripts/Tiles/ReactorTile.cs" id="1_yldg2"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://fg03qxqphp7n" path="res://Scenes/Tiles/ReactorTile.png" id="3_fk1vt"] | ||||||
|  |  | ||||||
|  | [sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"] | ||||||
|  | size = Vector2(54, 54) | ||||||
|  |  | ||||||
|  | [node name="ReactorTile" type="StaticBody2D"] | ||||||
|  | script = ExtResource("1_yldg2") | ||||||
|  | TileId = "reactor" | ||||||
|  |  | ||||||
|  | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|  | scale = Vector2(0.3, 0.3) | ||||||
|  | texture = ExtResource("3_fk1vt") | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] | ||||||
|  | scale = Vector2(3, 3) | ||||||
|  | shape = SubResource("RectangleShape2D_8o613") | ||||||
|  |  | ||||||
|  | [node name="ProgressOverlay" type="ColorRect" parent="."] | ||||||
|  | offset_left = -81.0 | ||||||
|  | offset_top = -81.0 | ||||||
|  | offset_right = -27.0 | ||||||
|  | offset_bottom = -27.0 | ||||||
|  | scale = Vector2(3, 3) | ||||||
| @@ -8,6 +8,7 @@ size = Vector2(54, 54) | |||||||
|  |  | ||||||
| [node name="StoneTile" type="StaticBody2D"] | [node name="StoneTile" type="StaticBody2D"] | ||||||
| collision_layer = 0 | collision_layer = 0 | ||||||
|  | collision_mask = 0 | ||||||
| script = ExtResource("1_rndy8") | script = ExtResource("1_rndy8") | ||||||
| TileId = "stone" | TileId = "stone" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Tiles/TurretTile.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 448 KiB | 
							
								
								
									
										34
									
								
								Scenes/Tiles/TurretTile.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://ckssi7soymu7g" | ||||||
|  | path="res://.godot/imported/TurretTile.png-d55543f854deaa0fedf248ba979d1cb4.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Tiles/TurretTile.png" | ||||||
|  | dest_files=["res://.godot/imported/TurretTile.png-d55543f854deaa0fedf248ba979d1cb4.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										39
									
								
								Scenes/Tiles/TurretTile.tscn
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,39 @@ | |||||||
|  | [gd_scene load_steps=6 format=3 uid="uid://dbup2pvjl8het"] | ||||||
|  |  | ||||||
|  | [ext_resource type="Script" uid="uid://n5g6i0uovxfk" path="res://Scripts/Tiles/TurretTile.cs" id="1_j3157"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://ckssi7soymu7g" path="res://Scenes/Tiles/TurretTile.png" id="2_7ljeh"] | ||||||
|  | [ext_resource type="PackedScene" uid="uid://erqawdsydh6a" path="res://Scenes/Entities/Bullet.tscn" id="2_gfad6"] | ||||||
|  | [ext_resource type="Texture2D" uid="uid://dmbbkwgff7dej" path="res://Scenes/Tiles/TurretTileBarrel.png" id="3_gfad6"] | ||||||
|  |  | ||||||
|  | [sub_resource type="RectangleShape2D" id="RectangleShape2D_pndcb"] | ||||||
|  | size = Vector2(54, 54) | ||||||
|  |  | ||||||
|  | [node name="Turret" type="StaticBody2D"] | ||||||
|  | script = ExtResource("1_j3157") | ||||||
|  | BulletScene = ExtResource("2_gfad6") | ||||||
|  | BarrelTipPath = NodePath("Barrel/Marker2D") | ||||||
|  | TileId = "turret" | ||||||
|  |  | ||||||
|  | [node name="Sprite2D" type="Sprite2D" parent="."] | ||||||
|  | scale = Vector2(0.1, 0.1) | ||||||
|  | texture = ExtResource("2_7ljeh") | ||||||
|  |  | ||||||
|  | [node name="Barrel" type="Sprite2D" parent="."] | ||||||
|  | position = Vector2(0, -3) | ||||||
|  | scale = Vector2(0.08, 0.08) | ||||||
|  | texture = ExtResource("3_gfad6") | ||||||
|  | offset = Vector2(0, -54) | ||||||
|  |  | ||||||
|  | [node name="Marker2D" type="Marker2D" parent="Barrel"] | ||||||
|  | position = Vector2(0, -225) | ||||||
|  | scale = Vector2(12.5, 12.5) | ||||||
|  |  | ||||||
|  | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] | ||||||
|  | shape = SubResource("RectangleShape2D_pndcb") | ||||||
|  |  | ||||||
|  | [node name="ProgressOverlay" type="ColorRect" parent="."] | ||||||
|  | visible = false | ||||||
|  | offset_left = -27.0 | ||||||
|  | offset_top = -27.0 | ||||||
|  | offset_right = 27.0 | ||||||
|  | offset_bottom = 27.0 | ||||||
							
								
								
									
										
											BIN
										
									
								
								Scenes/Tiles/TurretTileBarrel.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 185 KiB | 
							
								
								
									
										34
									
								
								Scenes/Tiles/TurretTileBarrel.png.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="texture" | ||||||
|  | type="CompressedTexture2D" | ||||||
|  | uid="uid://dmbbkwgff7dej" | ||||||
|  | path="res://.godot/imported/TurretTileBarrel.png-a9f6a29579c44d5d6ed4cc8f6c09fd72.ctex" | ||||||
|  | metadata={ | ||||||
|  | "vram_texture": false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Scenes/Tiles/TurretTileBarrel.png" | ||||||
|  | dest_files=["res://.godot/imported/TurretTileBarrel.png-a9f6a29579c44d5d6ed4cc8f6c09fd72.ctex"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | compress/mode=0 | ||||||
|  | compress/high_quality=false | ||||||
|  | compress/lossy_quality=0.7 | ||||||
|  | compress/hdr_compression=1 | ||||||
|  | compress/normal_map=0 | ||||||
|  | compress/channel_pack=0 | ||||||
|  | mipmaps/generate=false | ||||||
|  | mipmaps/limit=-1 | ||||||
|  | roughness/mode=0 | ||||||
|  | roughness/src_normal="" | ||||||
|  | process/fix_alpha_border=true | ||||||
|  | process/premult_alpha=false | ||||||
|  | process/normal_map_invert_y=false | ||||||
|  | process/hdr_as_srgb=false | ||||||
|  | process/hdr_clamp_exposure=false | ||||||
|  | process/size_limit=0 | ||||||
|  | detect_3d/compress_to=1 | ||||||
							
								
								
									
										14
									
								
								Scripts/AutoLoad/DIInitializer.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | |||||||
|  | using Godot; | ||||||
|  | using AceFieldNewHorizon.Scripts.System; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.AutoLoad; | ||||||
|  |  | ||||||
|  | public partial class DIInitializer : Node | ||||||
|  | { | ||||||
|  |     public override void _Ready() | ||||||
|  |     { | ||||||
|  |         // Initialize the Simple Injector container as early as possible | ||||||
|  |         DependencyInjection.Initialize(); | ||||||
|  |         GD.Print("[DIInitializer] Dependency Injection container initialized via AutoLoad."); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/AutoLoad/DIInitializer.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://cr2a8w6ur4uei | ||||||
							
								
								
									
										97
									
								
								Scripts/Entities/Bullet.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,97 @@ | |||||||
|  | using Godot; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.Entities; | ||||||
|  |  | ||||||
|  | public partial class Bullet : Area2D | ||||||
|  | { | ||||||
|  |     [Export] public float Speed = 400.0f; | ||||||
|  |     [Export] public int Damage = 10; | ||||||
|  |     [Export] public float MaxDistance = 1000.0f; | ||||||
|  |      | ||||||
|  |     private Vector2 _direction = Vector2.Right; | ||||||
|  |     private Vector2 _startPosition; | ||||||
|  |     private float _distanceTraveled = 0f; | ||||||
|  |     private bool _hasHit = false; | ||||||
|  |     private uint _ignoreCollisionLayer = 0; // Layer to ignore (will be set by turret) | ||||||
|  |  | ||||||
|  |     public void Initialize(Vector2 direction, Vector2 position, float rotation, uint ignoreLayer = 0) | ||||||
|  |     { | ||||||
|  |         _direction = direction.Normalized(); | ||||||
|  |         Position = position; | ||||||
|  |         Rotation = rotation; | ||||||
|  |         _startPosition = position; | ||||||
|  |         _ignoreCollisionLayer = ignoreLayer; | ||||||
|  |          | ||||||
|  |         // Connect the area entered signal | ||||||
|  |         BodyEntered += OnBodyEntered; | ||||||
|  |         AreaEntered += OnAreaEntered; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override void _PhysicsProcess(double delta) | ||||||
|  |     { | ||||||
|  |         if (_hasHit) return; | ||||||
|  |          | ||||||
|  |         // Move the bullet | ||||||
|  |         var movement = _direction * Speed * (float)delta; | ||||||
|  |         Position += movement; | ||||||
|  |         _distanceTraveled += movement.Length(); | ||||||
|  |          | ||||||
|  |         // Check if bullet has traveled max distance | ||||||
|  |         if (_distanceTraveled >= MaxDistance) | ||||||
|  |         { | ||||||
|  |             QueueFree(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private void OnBodyEntered(Node2D body) | ||||||
|  |     { | ||||||
|  |         HandleCollision(body); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private void OnAreaEntered(Area2D area) | ||||||
|  |     { | ||||||
|  |         HandleCollision(area); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private void HandleCollision(Node2D node) | ||||||
|  |     { | ||||||
|  |         if (_hasHit) return; | ||||||
|  |          | ||||||
|  |         // Skip collision if it's on the ignore layer | ||||||
|  |         if (node is PhysicsBody2D physicsBody &&  | ||||||
|  |             (physicsBody.CollisionLayer & _ignoreCollisionLayer) != 0) | ||||||
|  |         { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         _hasHit = true; | ||||||
|  |          | ||||||
|  |         // If we hit an enemy, deal damage | ||||||
|  |         if (node is Enemy enemy) | ||||||
|  |         { | ||||||
|  |             // Get the global position where the bullet hit | ||||||
|  |             var hitPosition = GlobalPosition; | ||||||
|  |             enemy.TakeDamage(Damage, hitPosition); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Optional: Add hit effect here | ||||||
|  |         // CreateHitEffect(); | ||||||
|  |          | ||||||
|  |         // Remove the bullet | ||||||
|  |         QueueFree(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     private void CreateHitEffect() | ||||||
|  |     { | ||||||
|  |         // You can add a hit effect here if desired | ||||||
|  |         // For example, a small explosion or impact sprite | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     public override void _ExitTree() | ||||||
|  |     { | ||||||
|  |         // Clean up signal connections | ||||||
|  |         BodyEntered -= OnBodyEntered; | ||||||
|  |         AreaEntered -= OnAreaEntered; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Entities/Bullet.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://vgx2a8gm7l8b | ||||||
							
								
								
									
										13
									
								
								Scripts/Entities/Enemy.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  | using Godot; | ||||||
|  | using AceFieldNewHorizon.Scripts.System; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.Entities; | ||||||
|  |  | ||||||
|  | public partial class Enemy : BaseEnemy | ||||||
|  | { | ||||||
|  | 	// All the base functionality is now in BaseEnemy | ||||||
|  | 	// This class is kept for backward compatibility and can be used to add | ||||||
|  | 	// specific behaviors for the basic enemy type if needed | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Entities/Enemy.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://cvsmy820b8dwl | ||||||
							
								
								
									
										283
									
								
								Scripts/Entities/EnemyBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,283 @@ | |||||||
|  | using System.Collections.Generic; | ||||||
|  | using AceFieldNewHorizon.Scripts.System; | ||||||
|  | using AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  | using Godot; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.Entities; | ||||||
|  |  | ||||||
|  | public abstract partial class BaseEnemy : CharacterBody2D | ||||||
|  | { | ||||||
|  |     public const string EnemyGroupName = "Enemy"; | ||||||
|  |  | ||||||
|  |     [Export] public float MoveSpeed = 150.0f; | ||||||
|  |     [Export] public float DetectionRadius = 300.0f; | ||||||
|  |     [Export] public float AttackRadius = 80.0f; | ||||||
|  |     [Export] public int Damage = 10; | ||||||
|  |     [Export] public float AttackCooldown = 3.0f; | ||||||
|  |     [Export] public int MaxHealth = 100; | ||||||
|  |     [Export] public bool ShowDamageNumbers = true; | ||||||
|  |  | ||||||
|  |     protected BaseTile TargetTile; | ||||||
|  |     protected ReactorTile Reactor; | ||||||
|  |     protected float AttackTimer = 0; | ||||||
|  |     protected Area2D DetectionArea; | ||||||
|  |     protected Area2D AttackArea; | ||||||
|  |     protected int CurrentHealthValue; | ||||||
|  |     protected ProgressBar HealthBar; | ||||||
|  |     protected GridManager Grid; | ||||||
|  |  | ||||||
|  |     // Track collisions with potential targets | ||||||
|  |     protected readonly HashSet<BaseTile> CollidingTiles = []; | ||||||
|  |  | ||||||
|  |     public int CurrentHealth | ||||||
|  |     { | ||||||
|  |         get => CurrentHealthValue; | ||||||
|  |         protected set | ||||||
|  |         { | ||||||
|  |             CurrentHealthValue = Mathf.Clamp(value, 0, MaxHealth); | ||||||
|  |             UpdateHealthBar(); | ||||||
|  |  | ||||||
|  |             if (CurrentHealthValue <= 0) | ||||||
|  |             { | ||||||
|  |                 Die(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public bool IsDead => CurrentHealthValue <= 0; | ||||||
|  |  | ||||||
|  |     public override void _Ready() | ||||||
|  |     { | ||||||
|  |         CurrentHealthValue = MaxHealth; | ||||||
|  |         Grid = DependencyInjection.Container.GetInstance<GridManager>(); | ||||||
|  |         Reactor = GetTree().GetFirstNodeInGroup(ReactorTile.ReactorGroupName) as ReactorTile; | ||||||
|  |  | ||||||
|  |         InitializeHealthBar(); | ||||||
|  |         InitializeDetectionArea(); | ||||||
|  |         InitializeAttackArea(); | ||||||
|  |          | ||||||
|  |         AddToGroup(EnemyGroupName); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void InitializeHealthBar() | ||||||
|  |     { | ||||||
|  |         HealthBar = new ProgressBar | ||||||
|  |         { | ||||||
|  |             MaxValue = MaxHealth, | ||||||
|  |             Value = CurrentHealthValue, | ||||||
|  |             Size = new Vector2(40, 4), | ||||||
|  |             ShowPercentage = false, | ||||||
|  |             Visible = false | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         var healthBarContainer = new Control(); | ||||||
|  |         healthBarContainer.AddChild(HealthBar); | ||||||
|  |         AddChild(healthBarContainer); | ||||||
|  |         healthBarContainer.Position = new Vector2(-20, -20); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void InitializeDetectionArea() | ||||||
|  |     { | ||||||
|  |         DetectionArea = new Area2D(); | ||||||
|  |         var collisionShape = new CollisionShape2D(); | ||||||
|  |         var shape = new CircleShape2D(); | ||||||
|  |         shape.Radius = DetectionRadius; | ||||||
|  |         collisionShape.Shape = shape; | ||||||
|  |         DetectionArea.AddChild(collisionShape); | ||||||
|  |         AddChild(DetectionArea); | ||||||
|  |  | ||||||
|  |         DetectionArea.BodyEntered += OnBodyEnteredDetection; | ||||||
|  |         DetectionArea.BodyExited += OnBodyExitedDetection; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void InitializeAttackArea() | ||||||
|  |     { | ||||||
|  |         AttackArea = GetNodeOrNull<Area2D>("AttackArea"); | ||||||
|  |         if (AttackArea != null) | ||||||
|  |         { | ||||||
|  |             AttackArea.BodyEntered += OnBodyEntered; | ||||||
|  |             AttackArea.BodyExited += OnBodyExited; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override void _Process(double delta) | ||||||
|  |     { | ||||||
|  |         if (IsDead) return; | ||||||
|  |  | ||||||
|  |         if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree()) | ||||||
|  |         { | ||||||
|  |             UpdateTarget(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         MoveTowardsTarget(); | ||||||
|  |         HandleAttacks(delta); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void MoveTowardsTarget() | ||||||
|  |     { | ||||||
|  |         if (TargetTile != null) | ||||||
|  |         { | ||||||
|  |             var direction = GlobalPosition.DirectionTo(TargetTile.GlobalPosition); | ||||||
|  |             Velocity = direction * MoveSpeed; | ||||||
|  |             LookAt(TargetTile.GlobalPosition); | ||||||
|  |             MoveAndSlide(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void HandleAttacks(double delta) | ||||||
|  |     { | ||||||
|  |         if (AttackTimer > 0) | ||||||
|  |         { | ||||||
|  |             AttackTimer -= (float)delta; | ||||||
|  |         } | ||||||
|  |         else if (CollidingTiles.Count > 0) | ||||||
|  |         { | ||||||
|  |             foreach (var tile in CollidingTiles) | ||||||
|  |             { | ||||||
|  |                 if (tile != null && !tile.IsDestroyed && tile.IsInsideTree()) | ||||||
|  |                 { | ||||||
|  |                     TryAttackTile(tile); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void UpdateTarget() | ||||||
|  |     { | ||||||
|  |         // If we have a valid target in collision, use that | ||||||
|  |         foreach (var tile in CollidingTiles) | ||||||
|  |         { | ||||||
|  |             if (tile != null && !tile.IsDestroyed && tile.IsInsideTree()) | ||||||
|  |             { | ||||||
|  |                 TargetTile = tile; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Otherwise find the reactor | ||||||
|  |         if (Reactor != null && !Reactor.IsDestroyed && Reactor.IsInsideTree()) | ||||||
|  |         { | ||||||
|  |             TargetTile = Reactor; | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             TargetTile = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void TryAttackTile(BaseTile tile) | ||||||
|  |     { | ||||||
|  |         if (IsDead || tile == null || tile.IsDestroyed || !tile.IsInsideTree()) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|  |         AttackTimer = AttackCooldown; | ||||||
|  |         bool wasDestroyed = tile.TakeDamage(Damage); | ||||||
|  |         GD.Print($"Attacking {tile.Name} for {Damage} damage. Was destroyed: {wasDestroyed}"); | ||||||
|  |  | ||||||
|  |         if (wasDestroyed) | ||||||
|  |         { | ||||||
|  |             CollidingTiles.Remove(tile); | ||||||
|  |             if (TargetTile == tile) | ||||||
|  |             { | ||||||
|  |                 TargetTile = null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void OnBodyEntered(Node2D body) | ||||||
|  |     { | ||||||
|  |         if (body is BaseTile { IsDestroyed: false } tile && !body.IsInGroup("Hostile")) | ||||||
|  |         { | ||||||
|  |             GD.Print($"[Enemy] {body.Name} Entered attack range"); | ||||||
|  |             CollidingTiles.Add(tile); | ||||||
|  |             if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree()) | ||||||
|  |             { | ||||||
|  |                 TargetTile = tile; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Attack immediately on collision | ||||||
|  |             TryAttackTile(tile); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void OnBodyExited(Node2D body) | ||||||
|  |     { | ||||||
|  |         if (body.GetParent() is BaseTile tile) | ||||||
|  |         { | ||||||
|  |             CollidingTiles.Remove(tile); | ||||||
|  |             if (TargetTile == tile) | ||||||
|  |             { | ||||||
|  |                 TargetTile = null; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void OnBodyEnteredDetection(Node2D body) | ||||||
|  |     { | ||||||
|  |         // Can be overridden by derived classes | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void OnBodyExitedDetection(Node2D body) | ||||||
|  |     { | ||||||
|  |         // Can be overridden by derived classes | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public virtual void TakeDamage(int damage, Vector2? hitPosition = null) | ||||||
|  |     { | ||||||
|  |         if (IsDead) return; | ||||||
|  |  | ||||||
|  |         CurrentHealth -= damage; | ||||||
|  |  | ||||||
|  |         // Show damage number (optional) | ||||||
|  |         if (ShowDamageNumbers) | ||||||
|  |         { | ||||||
|  |             var damageLabel = new Label | ||||||
|  |             { | ||||||
|  |                 Text = $"-{damage}", | ||||||
|  |                 Position = hitPosition ?? GlobalPosition, | ||||||
|  |                 ZIndex = 1000 | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             GetTree().CurrentScene.AddChild(damageLabel); | ||||||
|  |  | ||||||
|  |             // Animate and remove damage number | ||||||
|  |             var tween = CreateTween(); | ||||||
|  |             tween.TweenProperty(damageLabel, "position:y", damageLabel.Position.Y - 30, 0.5f); | ||||||
|  |             tween.TweenCallback(Callable.From(() => damageLabel.QueueFree())).SetDelay(0.5f); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Visual feedback | ||||||
|  |         var originalModulate = Modulate; | ||||||
|  |         Modulate = new Color(1, 0.5f, 0.5f); // Flash red | ||||||
|  |  | ||||||
|  |         var tweenFlash = CreateTween(); | ||||||
|  |         tweenFlash.TweenProperty(this, "modulate", originalModulate, 0.2f); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void UpdateHealthBar() | ||||||
|  |     { | ||||||
|  |         if (HealthBar != null) | ||||||
|  |         { | ||||||
|  |             HealthBar.Value = CurrentHealthValue; | ||||||
|  |             HealthBar.Visible = CurrentHealthValue < MaxHealth; // Only show when damaged | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void Die() | ||||||
|  |     { | ||||||
|  |         // Play death animation/sound | ||||||
|  |         // You can add a death animation here | ||||||
|  |  | ||||||
|  |         // Disable collisions and hide | ||||||
|  |         SetProcess(false); | ||||||
|  |         SetPhysicsProcess(false); | ||||||
|  |         Hide(); | ||||||
|  |  | ||||||
|  |         // Queue free after a delay (for any death animation/sound to play) | ||||||
|  |         var timer = new Timer(); | ||||||
|  |         AddChild(timer); | ||||||
|  |         timer.Timeout += () => QueueFree(); | ||||||
|  |         timer.Start(0.5f); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Entities/EnemyBase.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://6oduws4kbdlf | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| using System; | using AceFieldNewHorizon.Scripts.System; | ||||||
| using Godot; | using Godot; | ||||||
|  |  | ||||||
| namespace AceFieldNewHorizon.Scripts.Entities; | namespace AceFieldNewHorizon.Scripts.Entities; | ||||||
| @@ -19,6 +19,8 @@ public partial class Player : CharacterBody2D | |||||||
|     [Export] public float ZoomDecay = 0.9f; |     [Export] public float ZoomDecay = 0.9f; | ||||||
|     [Export] public float ZoomSmoothing = 10.0f; |     [Export] public float ZoomSmoothing = 10.0f; | ||||||
|  |  | ||||||
|  |     public ResourceManager Inventory { get; private set; } | ||||||
|  |          | ||||||
|     private Camera2D _camera; |     private Camera2D _camera; | ||||||
|     private Vector2 _cameraTargetZoom = Vector2.One; |     private Vector2 _cameraTargetZoom = Vector2.One; | ||||||
|     private float _currentZoomSpeed; |     private float _currentZoomSpeed; | ||||||
| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://dmint8ii0oj5g | ||||||
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								Scripts/System/DependencyInjection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | |||||||
|  | using Godot; | ||||||
|  | using Container = SimpleInjector.Container; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.System; | ||||||
|  |  | ||||||
|  | public static class DependencyInjection | ||||||
|  | { | ||||||
|  |     public static Container Container { get; private set; } | ||||||
|  |  | ||||||
|  |     public static void Initialize() | ||||||
|  |     { | ||||||
|  |         Container = new Container(); | ||||||
|  |  | ||||||
|  |         // Register your system services here | ||||||
|  |         // As singletons, since they are typically unique global managers | ||||||
|  |         Container.RegisterSingleton<ResourceManager>(); | ||||||
|  |         Container.RegisterSingleton<GridManager>(); | ||||||
|  |         Container.RegisterSingleton(() => new BuildingRegistry("res://Data/Buildings.json")); | ||||||
|  |  | ||||||
|  |         Container.Verify(); | ||||||
|  |         GD.Print("[DI] Simple Injector container initialized and verified."); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/System/DependencyInjection.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://hppsxnesg0ys | ||||||
| @@ -43,15 +43,33 @@ public partial class GridManager : Node | |||||||
|  |  | ||||||
|     public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building) |     public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building) | ||||||
|     { |     { | ||||||
|  |         // Get all cells that should be occupied by this building | ||||||
|         var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); |         var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); | ||||||
|         foreach (var cell in occupiedCells) |      | ||||||
|  |         // Create a list to store cells that should be removed | ||||||
|  |         var cellsToRemove = new List<Vector2I>(); | ||||||
|  |      | ||||||
|  |         // First, find all cells that match this building's position and size | ||||||
|  |         foreach (var cell in _layers[layer].Keys.ToList()) | ||||||
|         { |         { | ||||||
|             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
									
								
							
							
						
						| @@ -0,0 +1,102 @@ | |||||||
|  | using Godot; | ||||||
|  | using System.Collections.Generic; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.System; | ||||||
|  |  | ||||||
|  | public partial class Hud : CanvasLayer | ||||||
|  | { | ||||||
|  | 	private ResourceManager _resourceManager; | ||||||
|  | 	private VBoxContainer _resourceDisplay; | ||||||
|  | 	private readonly Dictionary<string, Label> _resourceLabels = new(); | ||||||
|  | 	private VBoxContainer _pickupLogContainer; | ||||||
|  |  | ||||||
|  | 	public override void _Ready() | ||||||
|  | 	{ | ||||||
|  | 		_resourceDisplay = GetNode<VBoxContainer>("ResourceDisplay"); | ||||||
|  | 		 | ||||||
|  | 		_pickupLogContainer = new VBoxContainer(); | ||||||
|  | 		_pickupLogContainer.Name = "PickupLogContainer"; | ||||||
|  | 		_pickupLogContainer.SetAnchorsPreset(Control.LayoutPreset.BottomLeft); | ||||||
|  | 		_pickupLogContainer.OffsetLeft = 10; | ||||||
|  | 		_pickupLogContainer.OffsetBottom = -10; // Negative offset from bottom anchor | ||||||
|  | 		_pickupLogContainer.GrowVertical = Control.GrowDirection.Begin; // Make it grow upwards | ||||||
|  | 		AddChild(_pickupLogContainer); | ||||||
|  | 		 | ||||||
|  | 		_resourceManager = DependencyInjection.Container.GetInstance<ResourceManager>();  | ||||||
|  | 		if (_resourceManager == null) | ||||||
|  | 		{ | ||||||
|  | 			GD.PushError("ResourceSystem not found in the scene tree!"); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		_resourceManager.OnResourceChanged += UpdateResourceDisplay; | ||||||
|  | 		_resourceManager.OnResourcePickedUp += OnResourcePickedUp; | ||||||
|  | 		 | ||||||
|  | 		// Initialize display with current resources | ||||||
|  | 		foreach (var entry in _resourceManager.GetAllResources()) | ||||||
|  | 		{ | ||||||
|  | 			UpdateResourceDisplay(entry.Key, entry.Value); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void UpdateResourceDisplay(string resourceId, int newAmount) | ||||||
|  | 	{ | ||||||
|  | 		if (!_resourceLabels.TryGetValue(resourceId, out Label label)) | ||||||
|  | 		{ | ||||||
|  | 			label = new Label(); | ||||||
|  | 			_resourceDisplay.AddChild(label); | ||||||
|  | 			_resourceLabels.Add(resourceId, label); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (newAmount <= 0) | ||||||
|  | 		{ | ||||||
|  | 			// Remove label if resource amount is zero or less | ||||||
|  | 			if (label.GetParent() != null) | ||||||
|  | 			{ | ||||||
|  | 				_resourceDisplay.RemoveChild(label); | ||||||
|  | 			} | ||||||
|  | 			_resourceLabels.Remove(resourceId); | ||||||
|  | 			label.QueueFree(); // Free the label from memory | ||||||
|  | 		} | ||||||
|  | 		else | ||||||
|  | 		{ | ||||||
|  | 			label.Text = $"{resourceId}: {newAmount}"; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void OnResourcePickedUp(string resourceId, int amount) | ||||||
|  | 	{ | ||||||
|  | 		AddPickupLogEntry($"Picked up {amount} {resourceId}!"); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void AddPickupLogEntry(string message) | ||||||
|  | 	{ | ||||||
|  | 		var logLabel = new Label(); | ||||||
|  | 		logLabel.Text = message; | ||||||
|  | 		_pickupLogContainer.AddChild(logLabel); | ||||||
|  |  | ||||||
|  | 		var timer = new Timer(); | ||||||
|  | 		timer.WaitTime = 3.0f; | ||||||
|  | 		timer.OneShot = true; | ||||||
|  | 		timer.Autostart = true; | ||||||
|  | 		logLabel.AddChild(timer); // Add timer as child of the label | ||||||
|  | 		timer.Timeout += () => OnLogEntryTimeout(logLabel); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void OnLogEntryTimeout(Label logLabel) | ||||||
|  | 	{ | ||||||
|  | 		// Start fading out the label | ||||||
|  | 		var tween = logLabel.CreateTween(); | ||||||
|  | 		tween.TweenProperty(logLabel, "modulate", new Color(1, 1, 1, 0), 0.5f); // Fade out over 0.5 seconds | ||||||
|  | 		tween.TweenCallback(Callable.From(() => logLabel.QueueFree())); // Free after fading | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public override void _ExitTree() | ||||||
|  | 	{ | ||||||
|  | 		if (_resourceManager != null) | ||||||
|  | 		{ | ||||||
|  | 			_resourceManager.OnResourceChanged -= UpdateResourceDisplay; | ||||||
|  | 			_resourceManager.OnResourcePickedUp -= OnResourcePickedUp; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/System/Hud.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://ddoqqcg77f60v | ||||||
							
								
								
									
										187
									
								
								Scripts/System/ItemPickup.cs
									
									
									
									
									
										Normal 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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/System/ItemPickup.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://qgcue2doj2lf | ||||||
| @@ -1,153 +1,492 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Concurrent; | ||||||
| using Godot; | using Godot; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using AceFieldNewHorizon.Scripts.Entities; | ||||||
| using AceFieldNewHorizon.Scripts.Tiles; | using AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  |  | ||||||
| namespace AceFieldNewHorizon.Scripts.System; | namespace AceFieldNewHorizon.Scripts.System; | ||||||
|  |  | ||||||
| public partial class NaturalResourceGenerator : Node2D | public partial class NaturalResourceGenerator : Node2D | ||||||
| { | { | ||||||
| 	[Export] public GridManager Grid { get; set; } |     public const string ChunkTrackerGroupName = "NrgTrackingTarget"; | ||||||
| 	[Export] public BuildingRegistry Registry { get; set; } |  | ||||||
|  |  | ||||||
| 	[Export] public int MapWidth = 100; |     public GridManager Grid { get; private set; } | ||||||
| 	[Export] public int MapHeight = 100; |     public BuildingRegistry Registry { get; private set; } | ||||||
| 	[Export] public float StoneDensity = 0.1f;  // 10% chance for stone |  | ||||||
| 	[Export] public float IronDensity = 0.03f;  // 3% chance for iron (within stone) |  | ||||||
| 	[Export] public int MinStoneVeinSize = 1; |  | ||||||
| 	[Export] public int MaxStoneVeinSize = 5; |  | ||||||
| 	[Export] public int MinIronVeinSize = 1; |  | ||||||
| 	[Export] public int MaxIronVeinSize = 3; |  | ||||||
| 	[Export] public int Seed; |  | ||||||
|  |  | ||||||
| 	private RandomNumberGenerator _rng; |     [Export] public int ChunkSize = 16; | ||||||
| 	private readonly List<Vector2I> _groundTiles = []; |     [Export] public int LoadDistance = 2; // Number of chunks to load in each direction | ||||||
| 	private readonly List<Vector2I> _stoneTiles = []; |     [Export] public float StoneDensity = 0.1f; | ||||||
| 	private readonly List<Vector2I> _ironTiles = []; |     [Export] public float IronDensity = 0.03f; | ||||||
|  |     [Export] public int MinStoneVeinSize = 1; | ||||||
|  |     [Export] public int MaxStoneVeinSize = 5; | ||||||
|  |     [Export] public int MinIronVeinSize = 1; | ||||||
|  |     [Export] public int MaxIronVeinSize = 3; | ||||||
|  |     [Export] public int Seed; | ||||||
|  |  | ||||||
| 	public override void _Ready() |     [Export] public bool SpawnEnemyNest = true; | ||||||
| 	{ |     [Export] public int MinDistanceFromOrigin = 20; // Minimum distance from world origin (0,0) | ||||||
| 		_rng = new RandomNumberGenerator(); |     [Export] public int MaxDistanceFromOrigin = 50; // Maximum distance from world origin | ||||||
| 		_rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); |  | ||||||
|  |  | ||||||
| 		GenerateTerrain(); |     private const string LogPrefix = "[NaturalGeneration]"; | ||||||
| 		PlaceResources(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	private void GenerateTerrain() |     private RandomNumberGenerator _rng; | ||||||
| 	{ |     private readonly Dictionary<Vector2I, ChunkData> _loadedChunks = new(); | ||||||
| 		// First pass: Generate base ground tiles |     private Vector2I _lastPlayerChunk = new(-1000, -1000); | ||||||
| 		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 Player _player; | ||||||
| 	{ |  | ||||||
| 		// Create a copy of ground tiles for iteration |  | ||||||
| 		var groundTilesToProcess = new List<Vector2I>(_groundTiles); |  | ||||||
|  |  | ||||||
| 		// Place stone veins |     private readonly ConcurrentQueue<Action> _mainThreadActions = new(); | ||||||
| 		foreach (var cell in groundTilesToProcess) |     private Task _generationTask; | ||||||
| 		{ |     private bool _isRunning = true; | ||||||
| 			if (_rng.Randf() < StoneDensity) |  | ||||||
| 			{ |  | ||||||
| 				var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize); |  | ||||||
| 				PlaceVein(cell, "stone", veinSize, _stoneTiles); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Create a copy of stone tiles for iteration |     public override void _Ready() | ||||||
| 		var stoneTilesToProcess = new List<Vector2I>(_stoneTiles); |     { | ||||||
|  |         Grid = DependencyInjection.Container.GetInstance<GridManager>(); | ||||||
|  |         Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>(); | ||||||
|  |         _rng = new RandomNumberGenerator(); | ||||||
|  |         _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); | ||||||
|      |      | ||||||
| 		// Place iron veins within stone |         // Test if building registry is assigned | ||||||
| 		foreach (var stoneCell in stoneTilesToProcess) |         if (Registry == null) | ||||||
| 		{ |         { | ||||||
| 			if (_rng.Randf() < IronDensity) |             GD.PrintErr($"{LogPrefix} BuildingRegistry is not assigned!"); | ||||||
| 			{ |             return; | ||||||
| 				var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize); |         } | ||||||
| 				PlaceVein(stoneCell, "ore_iron", veinSize, _ironTiles); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|      |      | ||||||
| 	private void PlaceVein(Vector2I startCell, string tileType, int maxVeinSize, ICollection<Vector2I> tileList) |         // Test if enemy_portal is in the registry | ||||||
| 	{ |         var testBuilding = Registry.GetBuilding("enemy_portal"); | ||||||
| 		var queue = new Queue<Vector2I>(); |         if (testBuilding == null) | ||||||
| 		var placed = new HashSet<Vector2I>(); |         { | ||||||
| 		queue.Enqueue(startCell); |             GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!"); | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             GD.Print($"{LogPrefix} Found enemy_portal in registry!"); | ||||||
|  |         } | ||||||
|      |      | ||||||
| 		int placedCount = 0; |         GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}"); | ||||||
|          |          | ||||||
| 		while (queue.Count > 0 && placedCount < maxVeinSize) |         GD.Print($"{LogPrefix} Spawning the core reactor..."); | ||||||
| 		{ |         SpawnCoreReactor(); | ||||||
| 			var cell = queue.Dequeue(); |  | ||||||
| 			if (placed.Contains(cell)) continue; |  | ||||||
| 			if (!IsInBounds(cell)) continue; |  | ||||||
|      |      | ||||||
| 			switch (tileType) |         if (SpawnEnemyNest) | ||||||
| 			{ |         { | ||||||
| 				// For iron, make sure we're placing on stone |             GD.Print($"{LogPrefix} Attempting to spawn enemy nest..."); | ||||||
| 				case "ore_iron" when !_stoneTiles.Contains(cell): |             SpawnRandomEnemyNest(); | ||||||
| 					continue; |         } | ||||||
| 				// Remove from previous layer if needed |     } | ||||||
| 				case "ore_iron" when _stoneTiles.Contains(cell): |  | ||||||
| 					_stoneTiles.Remove(cell); |  | ||||||
| 					break; |  | ||||||
| 				case "stone" when _groundTiles.Contains(cell): |  | ||||||
| 					_groundTiles.Remove(cell); |  | ||||||
| 					break; |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			PlaceTile(tileType, cell); |     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) | ||||||
|  |         { | ||||||
|  |             _generationTask = Task.Run(() => GenerateChunksAroundPlayer(playerChunk)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
| 		// Match PlacementManager's positioning logic |     private void GenerateChunksAroundPlayer(Vector2I playerChunk) | ||||||
| 		var rotatedSize = building.GetRotatedSize(0f); |     { | ||||||
| 		var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); |         var chunksToLoad = new List<Vector2I>(); | ||||||
| 		instance.Position = GridUtils.GridToWorld(cell) + offset; |  | ||||||
|  |  | ||||||
| 		instance.ZIndex = (int)building.Layer; |         // Determine which chunks to load/unload | ||||||
| 		AddChild(instance); |         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); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
| 		// Make sure to use the building's size from the registry |         // Generate chunks in background | ||||||
| 		Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); |         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; } = []; | ||||||
| } | } | ||||||
| @@ -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); | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										150
									
								
								Scripts/System/ResourceManager.cs
									
									
									
									
									
										Normal 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}"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/System/ResourceManager.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://dfi2snip78eq6 | ||||||
| @@ -10,22 +10,45 @@ public partial class BaseTile : Node2D | |||||||
| { | { | ||||||
|     [Export] public string TileId { get; set; } |     [Export] public string TileId { get; set; } | ||||||
|  |  | ||||||
|  |     protected GridManager Grid { get; set; } | ||||||
|  |     protected BuildingRegistry Registry { get; set; } | ||||||
|  |  | ||||||
|  |     public int MaxDurability { get; private set; } | ||||||
|  |     public int CurrentDurability { get; private set; } | ||||||
|  |     public bool IsDestroyed { get; private set; } | ||||||
|  |  | ||||||
|     private CollisionShape2D _collisionShape; |     private CollisionShape2D _collisionShape; | ||||||
|     private Sprite2D _sprite; |     private Sprite2D _sprite; | ||||||
|     private ColorRect _progressOverlay; |     private ColorRect _progressOverlay; | ||||||
|     private Action _onConstructionComplete; |     private Action _onConstructionComplete; | ||||||
|  |     private Tween _damageTween; | ||||||
|  |  | ||||||
|  |     public bool IsConstructing; | ||||||
|  |     public bool IsConstructed; | ||||||
|  |  | ||||||
|     public override void _Ready() |     public override void _Ready() | ||||||
|     { |     { | ||||||
|  |         Grid = DependencyInjection.Container.GetInstance<GridManager>(); | ||||||
|  |         Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>(); | ||||||
|  |  | ||||||
|         _collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D"); |         _collisionShape = GetNodeOrNull<CollisionShape2D>("CollisionShape2D"); | ||||||
|         _sprite = GetNodeOrNull<Sprite2D>("Sprite2D"); |         _sprite = GetNodeOrNull<Sprite2D>("Sprite2D"); | ||||||
|         _progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay"); |         _progressOverlay = GetNodeOrNull<ColorRect>("ProgressOverlay"); | ||||||
|         if (_progressOverlay != null) |         if (_progressOverlay != null) | ||||||
|             _progressOverlay.Visible = false; |             _progressOverlay.Visible = false; | ||||||
|  |  | ||||||
|  |         // Get durability from BuildingRegistry | ||||||
|  |         var buildingData = Registry?.GetBuilding(TileId); | ||||||
|  |         MaxDurability = buildingData?.Durability ?? 100; // Default to 100 if not found | ||||||
|  |         CurrentDurability = MaxDurability; | ||||||
|  |         IsDestroyed = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void SetGhostMode(bool canPlace) |     public virtual void SetGhostMode(bool canPlace) | ||||||
|     { |     { | ||||||
|  |         // Don't modify collision for constructing buildings | ||||||
|  |         if (IsConstructing) return; | ||||||
|  |  | ||||||
|         if (_collisionShape != null) |         if (_collisionShape != null) | ||||||
|             _collisionShape.Disabled = true; |             _collisionShape.Disabled = true; | ||||||
|  |  | ||||||
| @@ -35,7 +58,7 @@ public partial class BaseTile : Node2D | |||||||
|                 : new Color(1, 0, 0, 0.5f); |                 : new Color(1, 0, 0, 0.5f); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void FinalizePlacement() |     public virtual void FinalizePlacement() | ||||||
|     { |     { | ||||||
|         if (_collisionShape != null) |         if (_collisionShape != null) | ||||||
|             _collisionShape.Disabled = false; |             _collisionShape.Disabled = false; | ||||||
| @@ -43,11 +66,91 @@ public partial class BaseTile : Node2D | |||||||
|             _sprite.Modulate = Colors.White; |             _sprite.Modulate = Colors.White; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public virtual bool TakeDamage(int damage) | ||||||
|  |     { | ||||||
|  |         if (IsDestroyed || IsConstructing) return false; | ||||||
|  |  | ||||||
|  |         GD.Print($"[Tile] {TileId} {GetInstanceId()} took {damage} damage"); | ||||||
|  |  | ||||||
|  |         CurrentDurability = Mathf.Max(0, CurrentDurability - damage); | ||||||
|  |  | ||||||
|  |         // Visual feedback for taking damage | ||||||
|  |         ShowDamageEffect(); | ||||||
|  |  | ||||||
|  |         if (CurrentDurability <= 0) | ||||||
|  |         { | ||||||
|  |             Destroy(); | ||||||
|  |             return true; // Tile was destroyed | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; // Tile is still alive | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void ShowDamageEffect() | ||||||
|  |     { | ||||||
|  |         if (_sprite == null) return; | ||||||
|  |  | ||||||
|  |         // Cancel any existing tween | ||||||
|  |         _damageTween?.Kill(); | ||||||
|  |         _damageTween = CreateTween(); | ||||||
|  |  | ||||||
|  |         // Flash red briefly | ||||||
|  |         _damageTween.TweenProperty(_sprite, "modulate", Colors.Red, 0.1f); | ||||||
|  |         _damageTween.TweenProperty(_sprite, "modulate", Colors.White, 0.1f); | ||||||
|  |  | ||||||
|  |         // Shake effect | ||||||
|  |         var originalPosition = _sprite.Position; | ||||||
|  |         _damageTween.TweenMethod( | ||||||
|  |             Callable.From<Vector2>(pos => _sprite.Position = pos), | ||||||
|  |             originalPosition + new Vector2(-2, -2), | ||||||
|  |             originalPosition + new Vector2(2, 2), | ||||||
|  |             0.05f | ||||||
|  |         ).SetTrans(Tween.TransitionType.Bounce).SetEase(Tween.EaseType.InOut); | ||||||
|  |  | ||||||
|  |         _damageTween.TweenMethod( | ||||||
|  |             Callable.From<Vector2>(pos => _sprite.Position = pos), | ||||||
|  |             originalPosition + new Vector2(2, 2), | ||||||
|  |             originalPosition, | ||||||
|  |             0.05f | ||||||
|  |         ).SetTrans(Tween.TransitionType.Bounce).SetEase(Tween.EaseType.InOut); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public virtual void Destroy() | ||||||
|  |     { | ||||||
|  |         if (IsDestroyed) return; | ||||||
|  |  | ||||||
|  |         IsDestroyed = true; | ||||||
|  |  | ||||||
|  |         // Disable collision using SetDeferred to avoid physics update conflicts | ||||||
|  |         _collisionShape?.SetDeferred("disabled", true); | ||||||
|  |  | ||||||
|  |         // Fade out and remove | ||||||
|  |         var tween = CreateTween(); | ||||||
|  |         tween.TweenProperty(this, "modulate:a", 0f, 0.3f); | ||||||
|  |         tween.TweenCallback(Callable.From(() => CallDeferred("queue_free"))); | ||||||
|  |  | ||||||
|  |         // Emit signal or call method when tile is destroyed | ||||||
|  |         CallDeferred(nameof(OnTileDestroyed)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected virtual void OnTileDestroyed() | ||||||
|  |     { | ||||||
|  |         // Can be overridden by derived classes for custom destruction behavior | ||||||
|  |         var cell = GridUtils.WorldToGrid(Position); | ||||||
|  |         var buildingInfo = Registry.GetBuilding(TileId); | ||||||
|  |         Grid.FreeArea(cell, buildingInfo.Size, Rotation, buildingInfo.Layer); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Building progress visualization |     // Building progress visualization | ||||||
|     public void StartConstruction(float buildTime, Action onComplete = null) |     public void StartConstruction(float buildTime, Action onComplete = null) | ||||||
|     { |     { | ||||||
|  |         IsConstructing = true; | ||||||
|  |         if (_collisionShape != null) | ||||||
|  |             _collisionShape.Disabled = true; | ||||||
|  |  | ||||||
|         if (_progressOverlay == null || _sprite?.Texture == null) |         if (_progressOverlay == null || _sprite?.Texture == null) | ||||||
|         { |         { | ||||||
|  |             IsConstructing = false; | ||||||
|             onComplete?.Invoke(); |             onComplete?.Invoke(); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| @@ -55,6 +158,10 @@ public partial class BaseTile : Node2D | |||||||
|         _onConstructionComplete = onComplete; |         _onConstructionComplete = onComplete; | ||||||
|         var texSize = new Vector2(GridUtils.TileSize, GridUtils.TileSize); |         var texSize = new Vector2(GridUtils.TileSize, GridUtils.TileSize); | ||||||
|  |  | ||||||
|  |         // Set initial transparency for construction | ||||||
|  |         if (_sprite != null) | ||||||
|  |             _sprite.Modulate = new Color(1, 1, 1, 0.8f); | ||||||
|  |  | ||||||
|         _progressOverlay.Visible = true; |         _progressOverlay.Visible = true; | ||||||
|         _progressOverlay.Modulate = Colors.White; |         _progressOverlay.Modulate = Colors.White; | ||||||
|         _progressOverlay.Color = new Color(0, 0, 1, 0.4f); // semi-transparent blue |         _progressOverlay.Color = new Color(0, 0, 1, 0.4f); // semi-transparent blue | ||||||
| @@ -80,8 +187,16 @@ public partial class BaseTile : Node2D | |||||||
|             // Fade out the overlay |             // Fade out the overlay | ||||||
|             await FadeOutOverlay(0.5f); |             await FadeOutOverlay(0.5f); | ||||||
|  |  | ||||||
|             // Notify completion |             // Construction complete - restore full opacity and enable collision | ||||||
|  |             if (_sprite != null) | ||||||
|  |                 _sprite.Modulate = Colors.White; | ||||||
|  |  | ||||||
|  |             IsConstructing = false; | ||||||
|  |             if (_collisionShape != null) | ||||||
|  |                 _collisionShape.Disabled = false; | ||||||
|  |  | ||||||
|             _onConstructionComplete?.Invoke(); |             _onConstructionComplete?.Invoke(); | ||||||
|  |             IsConstructed = true; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         RunProgress(); |         RunProgress(); | ||||||
|   | |||||||
							
								
								
									
										129
									
								
								Scripts/Tiles/EnemyPortalTile.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,129 @@ | |||||||
|  | using AceFieldNewHorizon.Scripts.Entities; | ||||||
|  | using Godot; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  |  | ||||||
|  | public partial class EnemyPortalTile : BaseTile | ||||||
|  | { | ||||||
|  | 	[Export] public PackedScene EnemyScene; | ||||||
|  | 	[Export] public int MaxEnemies = 5; | ||||||
|  | 	[Export] public float SpawnDelay = 5.0f; // Time between spawn attempts | ||||||
|  | 	[Export] public float SpawnRadius = 50.0f; // Radius around the nest where enemies can spawn | ||||||
|  | 	[Export] public bool Active = true; | ||||||
|  | 	 | ||||||
|  | 	private Timer _spawnTimer; | ||||||
|  | 	private int _currentEnemyCount; | ||||||
|  |  | ||||||
|  | 	public override void _Ready() | ||||||
|  | 	{ | ||||||
|  | 		base._Ready(); | ||||||
|  | 		 | ||||||
|  | 		// Add to Hostile group to prevent enemies from attacking their own nest | ||||||
|  | 		AddToGroup("Hostile"); | ||||||
|  | 		 | ||||||
|  | 		// Create and configure the timer | ||||||
|  | 		_spawnTimer = new Timer | ||||||
|  | 		{ | ||||||
|  | 			Autostart = true, | ||||||
|  | 			WaitTime = SpawnDelay | ||||||
|  | 		}; | ||||||
|  | 		AddChild(_spawnTimer); | ||||||
|  | 		_spawnTimer.Timeout += OnSpawnTimerTimeout; | ||||||
|  |  | ||||||
|  | 		var sprite = GetNode<Sprite2D>("Sprite2D"); | ||||||
|  | 		 | ||||||
|  | 		// Create and configure the shadow sprite | ||||||
|  | 		var shadow = new Sprite2D | ||||||
|  | 		{ | ||||||
|  | 			Texture = sprite.Texture, | ||||||
|  | 			Scale = sprite.Scale * 1.05f, // Slightly larger than the original | ||||||
|  | 			Modulate = new Color(0, 0, 0, 0.5f), // Slightly more transparent | ||||||
|  | 			Position = new Vector2(0, 6), // Closer to the sprite (reduced from 30) | ||||||
|  | 			ZIndex = -1 | ||||||
|  | 		}; | ||||||
|  | 		AddChild(shadow); | ||||||
|  | 		 | ||||||
|  | 		// Create floating animation | ||||||
|  | 		const float floatOffset = 5.0f; | ||||||
|  | 		const float floatDuration = 2.0f; | ||||||
|  | 		 | ||||||
|  | 		var tween = CreateTween().SetLoops(); | ||||||
|  | 		tween.TweenProperty(sprite, "position:y", -floatOffset, floatDuration) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine); | ||||||
|  | 		tween.TweenProperty(sprite, "position:y", floatOffset, floatDuration * 2) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine); | ||||||
|  | 		tween.TweenProperty(sprite, "position:y", 0, floatDuration) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine); | ||||||
|  | 		 | ||||||
|  | 		// Animate shadow | ||||||
|  | 		tween.Parallel().TweenProperty(shadow, "position:y", 12 - (floatOffset * 0.3f), floatDuration) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine); | ||||||
|  | 		tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.02f, floatDuration) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine); | ||||||
|  | 		tween.Parallel().TweenProperty(shadow, "modulate:a", 0.6f, floatDuration) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine); | ||||||
|  | 		 | ||||||
|  | 		tween.Parallel().TweenProperty(shadow, "position:y", 12 + (floatOffset * 0.4f), floatDuration * 2) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine) | ||||||
|  | 			.SetDelay(floatDuration); | ||||||
|  | 		tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.08f, floatDuration * 2) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine) | ||||||
|  | 			.SetDelay(floatDuration); | ||||||
|  | 		tween.Parallel().TweenProperty(shadow, "modulate:a", 0.4f, floatDuration * 2) | ||||||
|  | 			.SetEase(Tween.EaseType.InOut) | ||||||
|  | 			.SetTrans(Tween.TransitionType.Sine) | ||||||
|  | 			.SetDelay(floatDuration); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private void OnSpawnTimerTimeout() | ||||||
|  | 	{ | ||||||
|  | 		if (!Active || EnemyScene == null || _currentEnemyCount >= MaxEnemies) | ||||||
|  | 			return; | ||||||
|  |  | ||||||
|  | 		// Check if we can spawn more enemies | ||||||
|  | 		var enemies = GetTree().GetNodesInGroup("Enemy"); | ||||||
|  | 		_currentEnemyCount = enemies.Count; | ||||||
|  | 		 | ||||||
|  | 		if (_currentEnemyCount >= MaxEnemies) | ||||||
|  | 			return; | ||||||
|  |  | ||||||
|  | 		// Spawn a new enemy | ||||||
|  | 		var enemy = EnemyScene.Instantiate<Enemy>(); | ||||||
|  | 		if (enemy != null) | ||||||
|  | 		{ | ||||||
|  | 			GetParent().AddChild(enemy); | ||||||
|  | 			 | ||||||
|  | 			// Calculate a random position within the spawn radius | ||||||
|  | 			var randomAngle = GD.Randf() * Mathf.Pi * 2; | ||||||
|  | 			var randomOffset = new Vector2( | ||||||
|  | 				Mathf.Cos(randomAngle) * SpawnRadius, | ||||||
|  | 				Mathf.Sin(randomAngle) * SpawnRadius | ||||||
|  | 			); | ||||||
|  | 			 | ||||||
|  | 			enemy.GlobalPosition = GlobalPosition + randomOffset; | ||||||
|  | 			_currentEnemyCount++; | ||||||
|  | 			 | ||||||
|  | 			// Connect to the enemy's death signal if available | ||||||
|  | 			enemy.TreeExiting += () => OnEnemyDied(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	private void OnEnemyDied() | ||||||
|  | 	{ | ||||||
|  | 		_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	public void SetActive(bool active) | ||||||
|  | 	{ | ||||||
|  | 		Active = active; | ||||||
|  | 		_spawnTimer.Paused = !active; | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Tiles/EnemyPortalTile.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://26hl5mk4mqur | ||||||
| @@ -8,6 +8,7 @@ public partial class GroundTile : BaseTile | |||||||
| 	{ | 	{ | ||||||
| 		var sprite = GetNode<Sprite2D>("Sprite2D"); | 		var sprite = GetNode<Sprite2D>("Sprite2D"); | ||||||
| 		sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker	 | 		sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker	 | ||||||
|  | 		sprite.ZIndex = -10; | ||||||
| 		 | 		 | ||||||
| 		base._Ready(); | 		base._Ready(); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,8 +1,83 @@ | |||||||
|  | using AceFieldNewHorizon.Scripts.System; | ||||||
| using Godot; | using Godot; | ||||||
|  |  | ||||||
| namespace AceFieldNewHorizon.Scripts.Tiles; | namespace AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  |  | ||||||
| public partial class MinerTile : BaseTile | public partial class MinerTile : BaseTile | ||||||
| { | { | ||||||
|  |     [Export] public PackedScene ItemPickup { get; set; } | ||||||
|  |     [Export] public string ItemToMine { get; set; } | ||||||
|  |     [Export] public int MiningRate = 1; // Items per second | ||||||
|  |  | ||||||
|  |     private Vector2I _gridPosition; | ||||||
|  |     private float _timeSinceLastMine; | ||||||
|  |  | ||||||
|  |     public override void _Ready() | ||||||
|  |     { | ||||||
|  |         base._Ready(); | ||||||
|  |         _gridPosition = GridUtils.WorldToGrid(Position); | ||||||
|  |  | ||||||
|  |         var ground = Grid.GetTileAtCell(_gridPosition, GridLayer.Ground) as BaseTile; | ||||||
|  |         if (ground == null) | ||||||
|  |         { | ||||||
|  |             GD.Print($"[Miner] Miner {GetInstanceId()} not found available resource..."); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ItemToMine = (ground.TileId) switch | ||||||
|  |         { | ||||||
|  |             "stone" => "stone", | ||||||
|  |             "ore_iron" => "ore_iron", | ||||||
|  |             _ => null | ||||||
|  |         }; | ||||||
|  |         if (ItemToMine == null) | ||||||
|  |             GD.Print($"[Miner] Miner {GetInstanceId()} not found available resource..."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override void _Process(double delta) | ||||||
|  |     { | ||||||
|  |         if (ItemToMine == null) | ||||||
|  |             return; | ||||||
|  |          | ||||||
|  |         // Don't mine if building is not completed | ||||||
|  |         if (!IsConstructed || ItemPickup == null) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|  |         _timeSinceLastMine += (float)delta; | ||||||
|  |  | ||||||
|  |         if (!(_timeSinceLastMine >= 1f / MiningRate)) return; | ||||||
|  |         _timeSinceLastMine = 0f; | ||||||
|  |         SpawnItem(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void SpawnItem() | ||||||
|  |     { | ||||||
|  |         var itemPickup = ItemPickup?.Instantiate<ItemPickup>(); | ||||||
|  |         if (itemPickup == null) return; | ||||||
|  |  | ||||||
|  |         itemPickup.ItemId = ItemToMine; | ||||||
|  |         itemPickup.Quantity = 1; | ||||||
|  |  | ||||||
|  |         // Initial position (slightly below the spawn point) | ||||||
|  |         const int halfTileSize = GridUtils.TileSize / 2; | ||||||
|  |         var spawnPosition = GridUtils.GridToWorld(_gridPosition); | ||||||
|  |         var targetY = spawnPosition.Y - halfTileSize; // Target Y position | ||||||
|  |         var targetX = spawnPosition.X + halfTileSize + (GD.Randf() * 10f - 5f); | ||||||
|  |         itemPickup.Position = | ||||||
|  |             new Vector2(spawnPosition.X + halfTileSize, spawnPosition.Y + 16); // Start below | ||||||
|  |         itemPickup.Scale = Vector2.Zero; // Start invisible | ||||||
|  |  | ||||||
|  |         // Add to the scene | ||||||
|  |         GetTree().CurrentScene.AddChild(itemPickup); | ||||||
|  |  | ||||||
|  |         // Create the pop-up animation | ||||||
|  |         var tween = CreateTween().SetTrans(Tween.TransitionType.Elastic).SetEase(Tween.EaseType.Out); | ||||||
|  |  | ||||||
|  |         // Animate the pop-up effect | ||||||
|  |         tween.TweenProperty(itemPickup, "position:y", targetY, 0.6f); | ||||||
|  |         tween.Parallel().TweenProperty(itemPickup, "scale", Vector2.One, 0.6f); | ||||||
|  |  | ||||||
|  |         // Optional: Add a slight horizontal wobble | ||||||
|  |         tween.Parallel().TweenProperty(itemPickup, "position:x", targetX, 0.6f); | ||||||
|  |     } | ||||||
| } | } | ||||||
							
								
								
									
										13
									
								
								Scripts/Tiles/ReactorTile.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | namespace AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  |  | ||||||
|  | public partial class ReactorTile : BaseTile | ||||||
|  | { | ||||||
|  |     public static readonly string ReactorGroupName = "Reactor"; | ||||||
|  |  | ||||||
|  |     public override void _Ready() | ||||||
|  |     { | ||||||
|  |         base._Ready(); | ||||||
|  |          | ||||||
|  |         AddToGroup(ReactorGroupName); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Tiles/ReactorTile.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://c4k3ottt7j3b1 | ||||||
							
								
								
									
										127
									
								
								Scripts/Tiles/TurretTile.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,127 @@ | |||||||
|  | using System.Linq; | ||||||
|  | using AceFieldNewHorizon.Scripts.Entities; | ||||||
|  | using Godot; | ||||||
|  |  | ||||||
|  | namespace AceFieldNewHorizon.Scripts.Tiles; | ||||||
|  |  | ||||||
|  | public partial class TurretTile : BaseTile | ||||||
|  | { | ||||||
|  |     [Export] public PackedScene BulletScene; | ||||||
|  |      | ||||||
|  |     [Export] public float RotationSpeed = 2.0f; // Radians per second | ||||||
|  |     [Export] public float AttackRange = 300.0f; | ||||||
|  |     [Export] public float AttackCooldown = 0.5f; | ||||||
|  |     [Export] public int Damage = 10; | ||||||
|  |     [Export] public float BulletSpeed = 400.0f; | ||||||
|  |     [Export] public NodePath BarrelTipPath; | ||||||
|  |      | ||||||
|  |     private Node2D _spriteBarrel; | ||||||
|  |     private Node2D _barrelTip; | ||||||
|  |     private float _attackTimer = 0; | ||||||
|  |     private bool _hasTarget = false; | ||||||
|  |  | ||||||
|  |     public override void _Ready() | ||||||
|  |     { | ||||||
|  |         base._Ready(); | ||||||
|  |  | ||||||
|  |         _spriteBarrel = GetNodeOrNull<Node2D>("Barrel"); | ||||||
|  |         _barrelTip = GetNodeOrNull<Node2D>(BarrelTipPath); | ||||||
|  |          | ||||||
|  |         if (_barrelTip == null && _spriteBarrel != null) | ||||||
|  |         { | ||||||
|  |             // If no barrel tip is specified, use the end of the barrel sprite | ||||||
|  |             _barrelTip = _spriteBarrel.GetNodeOrNull<Node2D>("BarrelTip"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override void SetGhostMode(bool canPlace) | ||||||
|  |     { | ||||||
|  |         base.SetGhostMode(canPlace); | ||||||
|  |  | ||||||
|  |         if (_spriteBarrel != null) | ||||||
|  |             _spriteBarrel.Modulate = canPlace | ||||||
|  |                 ? new Color(0, 1, 0, 0.5f) | ||||||
|  |                 : new Color(1, 0, 0, 0.5f); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override void FinalizePlacement() | ||||||
|  |     { | ||||||
|  |         base.FinalizePlacement(); | ||||||
|  |  | ||||||
|  |         if (_spriteBarrel != null) | ||||||
|  |             _spriteBarrel.Modulate = Colors.White; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public override void _Process(double delta) | ||||||
|  |     { | ||||||
|  |         if (!IsConstructed) return; | ||||||
|  |  | ||||||
|  |         // Update attack cooldown | ||||||
|  |         if (_attackTimer > 0) | ||||||
|  |         { | ||||||
|  |             _attackTimer -= (float)delta; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Find the nearest enemy | ||||||
|  |         var enemies = GetTree().GetNodesInGroup(Enemy.EnemyGroupName) | ||||||
|  |             .OfType<Enemy>() | ||||||
|  |             .Where(e => e.GlobalPosition.DistanceTo(GlobalPosition) <= AttackRange) | ||||||
|  |             .OrderBy(e => e.GlobalPosition.DistanceTo(GlobalPosition)) | ||||||
|  |             .ToList(); | ||||||
|  |  | ||||||
|  |         if (enemies.Count > 0) | ||||||
|  |         { | ||||||
|  |             var nearestEnemy = enemies[0]; | ||||||
|  |             _hasTarget = true; | ||||||
|  |              | ||||||
|  |             // Calculate target angle | ||||||
|  |             var direction = (nearestEnemy.GlobalPosition - _barrelTip.GlobalPosition).Normalized(); | ||||||
|  |             var targetAngle = direction.Angle() + 90f; | ||||||
|  |  | ||||||
|  |             // Smoothly rotate towards target | ||||||
|  |             _spriteBarrel.Rotation = Mathf.LerpAngle(_spriteBarrel.Rotation, targetAngle, (float)delta * RotationSpeed); | ||||||
|  |  | ||||||
|  |             // Check if we're facing the target and can attack | ||||||
|  |             if (Mathf.Abs(Mathf.Wrap(targetAngle - _spriteBarrel.Rotation, -Mathf.Pi, Mathf.Pi)) < 0.1f) | ||||||
|  |                 TryAttack(nearestEnemy.GlobalPosition); | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             _hasTarget = false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void TryAttack(Vector2 targetPosition) | ||||||
|  |     { | ||||||
|  |         if (_attackTimer <= 0 && BulletScene != null && _barrelTip != null) | ||||||
|  |         { | ||||||
|  |             // Create bullet instance | ||||||
|  |             var bullet = BulletScene.Instantiate<Bullet>(); | ||||||
|  |              | ||||||
|  |             // Calculate direction and rotation | ||||||
|  |             var direction = (targetPosition - _barrelTip.GlobalPosition).Normalized(); | ||||||
|  |             var bulletRotation = direction.Angle(); | ||||||
|  |              | ||||||
|  |             // Set bullet position and rotation | ||||||
|  |             GetTree().CurrentScene.AddChild(bullet); | ||||||
|  |             bullet.GlobalPosition = _barrelTip.GlobalPosition; | ||||||
|  |             bullet.Rotation = bulletRotation; // Use the calculated rotation | ||||||
|  |              | ||||||
|  |             // Initialize bullet with direction and damage | ||||||
|  |             bullet.Initialize( | ||||||
|  |                 direction,  | ||||||
|  |                 bullet.GlobalPosition,  | ||||||
|  |                 bulletRotation, // Pass the calculated rotation | ||||||
|  |                 1 // Pass the turret's collision layer to ignore | ||||||
|  |             ); | ||||||
|  |             bullet.Damage = Damage; | ||||||
|  |             bullet.Speed = BulletSpeed; | ||||||
|  |             bullet.MaxDistance = AttackRange * 1.5f; // Bullets can travel slightly further than attack range | ||||||
|  |              | ||||||
|  |             // Reset attack cooldown | ||||||
|  |             _attackTimer = AttackCooldown; | ||||||
|  |              | ||||||
|  |             GD.Print("[Turret] Turret firing!"); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								Scripts/Tiles/TurretTile.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | uid://n5g6i0uovxfk | ||||||
							
								
								
									
										
											BIN
										
									
								
								Sounds/Events/Building.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										24
									
								
								Sounds/Events/Building.wav.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="wav" | ||||||
|  | type="AudioStreamWAV" | ||||||
|  | uid="uid://d1trbqrntmuij" | ||||||
|  | path="res://.godot/imported/Building.wav-b8766581fd25a206c63f47b13bd2e2f5.sample" | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Sounds/Events/Building.wav" | ||||||
|  | dest_files=["res://.godot/imported/Building.wav-b8766581fd25a206c63f47b13bd2e2f5.sample"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | force/8_bit=false | ||||||
|  | force/mono=false | ||||||
|  | force/max_rate=false | ||||||
|  | force/max_rate_hz=44100 | ||||||
|  | edit/trim=false | ||||||
|  | edit/normalize=false | ||||||
|  | edit/loop_mode=0 | ||||||
|  | edit/loop_begin=0 | ||||||
|  | edit/loop_end=-1 | ||||||
|  | compress/mode=2 | ||||||
							
								
								
									
										
											BIN
										
									
								
								Sounds/Events/Canceled.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										24
									
								
								Sounds/Events/Canceled.wav.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="wav" | ||||||
|  | type="AudioStreamWAV" | ||||||
|  | uid="uid://chn8ux4two1kd" | ||||||
|  | path="res://.godot/imported/Canceled.wav-6d441d9a898b5cd9be4c0664a1f489ed.sample" | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Sounds/Events/Canceled.wav" | ||||||
|  | dest_files=["res://.godot/imported/Canceled.wav-6d441d9a898b5cd9be4c0664a1f489ed.sample"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | force/8_bit=false | ||||||
|  | force/mono=false | ||||||
|  | force/max_rate=false | ||||||
|  | force/max_rate_hz=44100 | ||||||
|  | edit/trim=false | ||||||
|  | edit/normalize=false | ||||||
|  | edit/loop_mode=0 | ||||||
|  | edit/loop_begin=0 | ||||||
|  | edit/loop_end=-1 | ||||||
|  | compress/mode=2 | ||||||
							
								
								
									
										
											BIN
										
									
								
								Sounds/Events/CannotDeployHere.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										24
									
								
								Sounds/Events/CannotDeployHere.wav.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="wav" | ||||||
|  | type="AudioStreamWAV" | ||||||
|  | uid="uid://7u1gw7lt5xd1" | ||||||
|  | path="res://.godot/imported/CannotDeployHere.wav-a9ec27508654f74d03ac7b8c8037059c.sample" | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Sounds/Events/CannotDeployHere.wav" | ||||||
|  | dest_files=["res://.godot/imported/CannotDeployHere.wav-a9ec27508654f74d03ac7b8c8037059c.sample"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | force/8_bit=false | ||||||
|  | force/mono=false | ||||||
|  | force/max_rate=false | ||||||
|  | force/max_rate_hz=44100 | ||||||
|  | edit/trim=false | ||||||
|  | edit/normalize=false | ||||||
|  | edit/loop_mode=0 | ||||||
|  | edit/loop_begin=0 | ||||||
|  | edit/loop_end=-1 | ||||||
|  | compress/mode=2 | ||||||
							
								
								
									
										
											BIN
										
									
								
								Sounds/Events/InsufficientFunds.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										24
									
								
								Sounds/Events/InsufficientFunds.wav.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="wav" | ||||||
|  | type="AudioStreamWAV" | ||||||
|  | uid="uid://bemxvqcettqgp" | ||||||
|  | path="res://.godot/imported/InsufficientFunds.wav-7aba215cb1cd04a5285a5e9908999906.sample" | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Sounds/Events/InsufficientFunds.wav" | ||||||
|  | dest_files=["res://.godot/imported/InsufficientFunds.wav-7aba215cb1cd04a5285a5e9908999906.sample"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | force/8_bit=false | ||||||
|  | force/mono=false | ||||||
|  | force/max_rate=false | ||||||
|  | force/max_rate_hz=44100 | ||||||
|  | edit/trim=false | ||||||
|  | edit/normalize=false | ||||||
|  | edit/loop_mode=0 | ||||||
|  | edit/loop_begin=0 | ||||||
|  | edit/loop_end=-1 | ||||||
|  | compress/mode=2 | ||||||
							
								
								
									
										
											BIN
										
									
								
								Sounds/Events/NotReady.wav
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										24
									
								
								Sounds/Events/NotReady.wav.import
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | |||||||
|  | [remap] | ||||||
|  |  | ||||||
|  | importer="wav" | ||||||
|  | type="AudioStreamWAV" | ||||||
|  | uid="uid://dogbl6tfealwg" | ||||||
|  | path="res://.godot/imported/NotReady.wav-2cfa5f22feed110ca411d6478060fdea.sample" | ||||||
|  |  | ||||||
|  | [deps] | ||||||
|  |  | ||||||
|  | source_file="res://Sounds/Events/NotReady.wav" | ||||||
|  | dest_files=["res://.godot/imported/NotReady.wav-2cfa5f22feed110ca411d6478060fdea.sample"] | ||||||
|  |  | ||||||
|  | [params] | ||||||
|  |  | ||||||
|  | force/8_bit=false | ||||||
|  | force/mono=false | ||||||
|  | force/max_rate=false | ||||||
|  | force/max_rate_hz=44100 | ||||||
|  | edit/trim=false | ||||||
|  | edit/normalize=false | ||||||
|  | edit/loop_mode=0 | ||||||
|  | edit/loop_begin=0 | ||||||
|  | edit/loop_end=-1 | ||||||
|  | compress/mode=2 | ||||||
| @@ -15,8 +15,15 @@ run/main_scene="uid://c22aprj452aha" | |||||||
| config/features=PackedStringArray("4.4", "C#", "GL Compatibility") | config/features=PackedStringArray("4.4", "C#", "GL Compatibility") | ||||||
| config/icon="res://icon.svg" | config/icon="res://icon.svg" | ||||||
|  |  | ||||||
|  | [autoload] | ||||||
|  |  | ||||||
|  | ResourceManager="res://Scripts/System/ResourceManager.cs" | ||||||
|  | DiInitializer="*res://Scripts/AutoLoad/DIInitializer.cs" | ||||||
|  |  | ||||||
| [display] | [display] | ||||||
|  |  | ||||||
|  | window/size/viewport_width=1920 | ||||||
|  | window/size/viewport_height=1080 | ||||||
| window/stretch/mode="viewport" | window/stretch/mode="viewport" | ||||||
|  |  | ||||||
| [dotnet] | [dotnet] | ||||||
| @@ -75,8 +82,14 @@ switch_tile={ | |||||||
| "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":93,"key_label":0,"unicode":93,"location":0,"echo":false,"script":null) | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":93,"key_label":0,"unicode":93,"location":0,"echo":false,"script":null) | ||||||
| ] | ] | ||||||
| } | } | ||||||
|  | toggle_build={ | ||||||
|  | "deadzone": 0.2, | ||||||
|  | "events": [] | ||||||
|  | } | ||||||
|  |  | ||||||
| [rendering] | [rendering] | ||||||
|  |  | ||||||
| anti_aliasing/quality/msaa_2d=3 | anti_aliasing/quality/msaa_2d=3 | ||||||
|  | anti_aliasing/quality/msaa_3d=3 | ||||||
|  | anti_aliasing/quality/screen_space_aa=1 | ||||||
| anti_aliasing/quality/use_taa=true | anti_aliasing/quality/use_taa=true | ||||||
|   | |||||||