Compare commits

..

12 Commits

Author SHA1 Message Date
2c5e0459ad 💄 Optimize stuff 2025-08-31 19:30:16 +08:00
f2c243ecf6 🍱 Retexture the enemies 2025-08-31 18:54:30 +08:00
b424aafeab ♻️ Optimizations of the various system
🍱 Retexture of the enemy portal
2025-08-31 18:26:45 +08:00
09511b37c9 Reactor, and enemies attacking the tiles 2025-08-31 14:30:18 +08:00
c72353716f ♻️ Rebuild with DI 2025-08-31 01:44:18 +08:00
1cc941d893 Hud basis 2025-08-30 23:41:49 +08:00
88647b1c41 🐛 Bug fixes 2025-08-30 23:26:19 +08:00
7438ba407a Turret shooting and damaing 2025-08-30 13:05:50 +08:00
1c6c03cd41 Turret tile basis 2025-08-30 12:03:26 +08:00
32f96d488d Enemy and nest 2025-08-30 02:06:58 +08:00
630dbf0800 :bugS: Fix natural resource generate missing iron 2025-08-29 18:11:00 +08:00
ac1d8cfab9 ♻️ Better miner tile 2025-08-29 18:05:30 +08:00
55 changed files with 1519 additions and 95 deletions

View File

@@ -4,4 +4,7 @@
<EnableDynamicLoading>true</EnableDynamicLoading> <EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>AceFieldNewHorizon</RootNamespace> <RootNamespace>AceFieldNewHorizon</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.5.0" />
</ItemGroup>
</Project> </Project>

View File

@@ -21,6 +21,36 @@
"layer": 1, "layer": 1,
"size": [1, 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]
},
"enemy_portal": {
"scene": "res://Scenes/Tiles/EnemyPortalTile.tscn",
"cost": {},
"durability": 200,
"buildTime": 0.0,
"allowedRotations": [0],
"layer": 1,
"size": [1, 2]
},
"ground": { "ground": {
"scene": "res://Scenes/Tiles/GroundTile.tscn", "scene": "res://Scenes/Tiles/GroundTile.tscn",
"cost": {}, "cost": {},

BIN
Scenes/Entities/Bullet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b5mb8tu15rc2p"
path="res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Scenes/Entities/Bullet.png"
dest_files=["res://.godot/imported/Bullet.png-db4c50cb16094f39ca6a1b9de30a2fe2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,18 @@
[gd_scene load_steps=4 format=3 uid="uid://erqawdsydh6a"]
[ext_resource type="Texture2D" uid="uid://b5mb8tu15rc2p" path="res://Scenes/Entities/Bullet.png" id="1_fi8au"]
[ext_resource type="Script" uid="uid://vgx2a8gm7l8b" path="res://Scripts/Entities/Bullet.cs" id="1_k5b1m"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_fi8au"]
size = Vector2(44, 12)
[node name="Bullet" type="Area2D"]
collision_mask = 2
script = ExtResource("1_k5b1m")
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.03, 0.03)
texture = ExtResource("1_fi8au")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_fi8au")

BIN
Scenes/Entities/Enemy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://x4u6oatvsm8y"
path="res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Scenes/Entities/Enemy.png"
dest_files=["res://.godot/imported/Enemy.png-7a121c0bc2e7a40a7fe012e488d00452.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,21 @@
[gd_scene load_steps=3 format=3 uid="uid://b3ffcucytwmk"]
[ext_resource type="Script" uid="uid://cvsmy820b8dwl" path="res://Scripts/Entities/Enemy.cs" id="1_jajit"]
[ext_resource type="Texture2D" uid="uid://x4u6oatvsm8y" path="res://Scenes/Entities/Enemy.png" id="2_jajit"]
[node name="Enemy" type="CharacterBody2D"]
collision_layer = 2
collision_mask = 3
script = ExtResource("1_jajit")
[node name="CollisionShape2D" type="CollisionPolygon2D" parent="."]
polygon = PackedVector2Array(-2, -21, 3, -21, 24, 6, 24, 10, 20, 14, -21, 14, -24, 11, -24, 7)
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.05, 0.05)
texture = ExtResource("2_jajit")
[node name="AttackArea" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionPolygon2D" parent="AttackArea"]
polygon = PackedVector2Array(-3, -24, 4, -24, 27, 5, 27, 14, 20, 19, -21, 19, -27, 14, -27, 5)

View File

@@ -4,6 +4,7 @@
[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 MinZoom = 0.1
MaxZoom = 5.0 MaxZoom = 5.0

View File

@@ -1,38 +1,23 @@
[gd_scene load_steps=8 format=3 uid="uid://c22aprj452aha"] [gd_scene load_steps=6 format=3 uid="uid://c22aprj452aha"]
[ext_resource type="Script" uid="uid://cudpc3w17mbsw" path="res://Scripts/System/GridManager.cs" id="1_knkkn"]
[ext_resource type="Script" uid="uid://dfi2snip78eq6" path="res://Scripts/System/ResourceManager.cs" id="1_pl8e4"]
[ext_resource type="Script" uid="uid://cfbj72nm0eovg" path="res://Scripts/System/BuildingRegistry.cs" id="1_sxhdm"]
[ext_resource type="Script" uid="uid://cugfbvw70clgd" path="res://Scripts/System/NaturalResourceGenerator.cs" id="2_oss8w"] [ext_resource type="Script" uid="uid://cugfbvw70clgd" path="res://Scripts/System/NaturalResourceGenerator.cs" id="2_oss8w"]
[ext_resource type="Script" uid="uid://bx1wj7gn6vrqe" path="res://Scripts/System/PlacementManager.cs" id="2_sxhdm"] [ext_resource type="Script" uid="uid://bx1wj7gn6vrqe" path="res://Scripts/System/PlacementManager.cs" id="2_sxhdm"]
[ext_resource type="PackedScene" uid="uid://doxy60afddg1m" path="res://Scenes/Entities/Player.tscn" id="3_oss8w"] [ext_resource type="PackedScene" uid="uid://doxy60afddg1m" path="res://Scenes/Entities/Player.tscn" id="3_oss8w"]
[ext_resource type="PackedScene" uid="uid://xwkplaxmye3v" path="res://Scenes/System/ItemPickup.tscn" id="7_is6ib"] [ext_resource type="PackedScene" uid="uid://xwkplaxmye3v" path="res://Scenes/System/ItemPickup.tscn" id="7_is6ib"]
[ext_resource type="PackedScene" uid="uid://byv2vu0k2drdd" path="res://Scenes/System/HUD.tscn" id="8_hud_scene"]
[node name="Root" type="Node2D"] [node name="Root" type="Node2D"]
[node name="ResourceSystem" type="Node" parent="."] [node name="NaturalResourceGenerator" type="Node2D" parent="."]
script = ExtResource("1_pl8e4")
[node name="BuildingRegistry" type="Node" parent="."]
script = ExtResource("1_sxhdm")
[node name="NaturalResourceGenerator" type="Node2D" parent="." node_paths=PackedStringArray("Grid", "Registry")]
script = ExtResource("2_oss8w") script = ExtResource("2_oss8w")
Grid = NodePath("../GridSystem")
Registry = NodePath("../BuildingRegistry")
[node name="GridSystem" type="Node2D" parent="."] [node name="PlacementSystem" type="Node2D" parent="."]
script = ExtResource("1_knkkn")
[node name="PlacementSystem" type="Node2D" parent="." node_paths=PackedStringArray("Grid", "Inventory", "Registry")]
script = ExtResource("2_sxhdm") script = ExtResource("2_sxhdm")
Grid = NodePath("../GridSystem")
Inventory = NodePath("../ResourceSystem")
Registry = NodePath("../BuildingRegistry")
[node name="Player" parent="." node_paths=PackedStringArray("Inventory") instance=ExtResource("3_oss8w")] [node name="Player" parent="." instance=ExtResource("3_oss8w")]
scale = Vector2(0.35, 0.35) scale = Vector2(0.35, 0.35)
Inventory = NodePath("../ResourceSystem")
[node name="HUD" parent="." instance=ExtResource("8_hud_scene")]
[node name="ItemPickup" parent="." instance=ExtResource("7_is6ib")] [node name="ItemPickup" parent="." instance=ExtResource("7_is6ib")]
position = Vector2(-496, -245) position = Vector2(-496, -245)

13
Scenes/System/HUD.tscn Normal file
View File

@@ -0,0 +1,13 @@
[gd_scene load_steps=2 format=3 uid="uid://byv2vu0k2drdd"]
[ext_resource type="Script" uid="uid://ddoqqcg77f60v" path="res://Scripts/System/Hud.cs" id="1_yw4ou"]
[node name="HUD" type="CanvasLayer"]
script = ExtResource("1_yw4ou")
[node name="ResourceDisplay" type="VBoxContainer" parent="."]
layout_mode = 0
offset_left = 10.0
offset_top = 10.0
offset_right = 100.0
offset_bottom = 100.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dv2xwfyshxdtp"
path="res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Scenes/Tiles/EnemyPortalTile.png"
dest_files=["res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,23 @@
[gd_scene load_steps=5 format=3 uid="uid://dup2su0s3ybcy"]
[ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyPortalTile.cs" id="1_o543x"]
[ext_resource type="PackedScene" uid="uid://b3ffcucytwmk" path="res://Scenes/Entities/Enemy.tscn" id="2_nh7ff"]
[ext_resource type="Texture2D" uid="uid://dv2xwfyshxdtp" path="res://Scenes/Tiles/EnemyPortalTile.png" id="3_i4us4"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_id484"]
size = Vector2(54, 79)
[node name="EnemyPortal" type="StaticBody2D"]
collision_layer = 0
collision_mask = 0
script = ExtResource("1_o543x")
EnemyScene = ExtResource("2_nh7ff")
TileId = "enemy_portal"
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.1, 0.1)
texture = ExtResource("3_i4us4")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, -0.5)
shape = SubResource("RectangleShape2D_id484")

View File

@@ -8,6 +8,7 @@ size = Vector2(54, 54)
[node name="GroundTile" type="StaticBody2D"] [node name="GroundTile" type="StaticBody2D"]
collision_layer = 0 collision_layer = 0
collision_mask = 0
script = ExtResource("1_mqsaf") script = ExtResource("1_mqsaf")
TileId = "ground" TileId = "ground"

View File

@@ -8,8 +8,9 @@ size = Vector2(54, 54)
[node name="OreIronTile" type="StaticBody2D"] [node name="OreIronTile" type="StaticBody2D"]
collision_layer = 0 collision_layer = 0
collision_mask = 0
script = ExtResource("1_exnim") script = ExtResource("1_exnim")
TileId = "stone_iron" TileId = "ore_iron"
[node name="Sprite2D" type="Sprite2D" parent="."] [node name="Sprite2D" type="Sprite2D" parent="."]
position = Vector2(1.49012e-08, -9.53674e-07) position = Vector2(1.49012e-08, -9.53674e-07)

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://fg03qxqphp7n"
path="res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Scenes/Tiles/ReactorTile.png"
dest_files=["res://.godot/imported/ReactorTile.png-f6f5bfaa813b044011d6b0a5736b9bc6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,26 @@
[gd_scene load_steps=4 format=3 uid="uid://w6ni678js7cu"]
[ext_resource type="Script" uid="uid://c4k3ottt7j3b1" path="res://Scripts/Tiles/ReactorTile.cs" id="1_yldg2"]
[ext_resource type="Texture2D" uid="uid://fg03qxqphp7n" path="res://Scenes/Tiles/ReactorTile.png" id="3_fk1vt"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_8o613"]
size = Vector2(54, 54)
[node name="ReactorTile" type="StaticBody2D"]
script = ExtResource("1_yldg2")
TileId = "reactor"
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.3, 0.3)
texture = ExtResource("3_fk1vt")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
scale = Vector2(3, 3)
shape = SubResource("RectangleShape2D_8o613")
[node name="ProgressOverlay" type="ColorRect" parent="."]
offset_left = -81.0
offset_top = -81.0
offset_right = -27.0
offset_bottom = -27.0
scale = Vector2(3, 3)

View File

@@ -8,6 +8,7 @@ size = Vector2(54, 54)
[node name="StoneTile" type="StaticBody2D"] [node name="StoneTile" type="StaticBody2D"]
collision_layer = 0 collision_layer = 0
collision_mask = 0
script = ExtResource("1_rndy8") script = ExtResource("1_rndy8")
TileId = "stone" TileId = "stone"

BIN
Scenes/Tiles/TurretTile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ckssi7soymu7g"
path="res://.godot/imported/TurretTile.png-d55543f854deaa0fedf248ba979d1cb4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Scenes/Tiles/TurretTile.png"
dest_files=["res://.godot/imported/TurretTile.png-d55543f854deaa0fedf248ba979d1cb4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,39 @@
[gd_scene load_steps=6 format=3 uid="uid://dbup2pvjl8het"]
[ext_resource type="Script" uid="uid://n5g6i0uovxfk" path="res://Scripts/Tiles/TurretTile.cs" id="1_j3157"]
[ext_resource type="Texture2D" uid="uid://ckssi7soymu7g" path="res://Scenes/Tiles/TurretTile.png" id="2_7ljeh"]
[ext_resource type="PackedScene" uid="uid://erqawdsydh6a" path="res://Scenes/Entities/Bullet.tscn" id="2_gfad6"]
[ext_resource type="Texture2D" uid="uid://dmbbkwgff7dej" path="res://Scenes/Tiles/TurretTileBarrel.png" id="3_gfad6"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_pndcb"]
size = Vector2(54, 54)
[node name="Turret" type="StaticBody2D"]
script = ExtResource("1_j3157")
BulletScene = ExtResource("2_gfad6")
BarrelTipPath = NodePath("Barrel/Marker2D")
TileId = "turret"
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.1, 0.1)
texture = ExtResource("2_7ljeh")
[node name="Barrel" type="Sprite2D" parent="."]
position = Vector2(0, -3)
scale = Vector2(0.08, 0.08)
texture = ExtResource("3_gfad6")
offset = Vector2(0, -54)
[node name="Marker2D" type="Marker2D" parent="Barrel"]
position = Vector2(0, -225)
scale = Vector2(12.5, 12.5)
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_pndcb")
[node name="ProgressOverlay" type="ColorRect" parent="."]
visible = false
offset_left = -27.0
offset_top = -27.0
offset_right = 27.0
offset_bottom = 27.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dmbbkwgff7dej"
path="res://.godot/imported/TurretTileBarrel.png-a9f6a29579c44d5d6ed4cc8f6c09fd72.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Scenes/Tiles/TurretTileBarrel.png"
dest_files=["res://.godot/imported/TurretTileBarrel.png-a9f6a29579c44d5d6ed4cc8f6c09fd72.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,14 @@
using Godot;
using AceFieldNewHorizon.Scripts.System;
namespace AceFieldNewHorizon.Scripts.AutoLoad;
public partial class DIInitializer : Node
{
public override void _Ready()
{
// Initialize the Simple Injector container as early as possible
DependencyInjection.Initialize();
GD.Print("[DIInitializer] Dependency Injection container initialized via AutoLoad.");
}
}

View File

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

View File

@@ -0,0 +1,97 @@
using Godot;
namespace AceFieldNewHorizon.Scripts.Entities;
public partial class Bullet : Area2D
{
[Export] public float Speed = 400.0f;
[Export] public int Damage = 10;
[Export] public float MaxDistance = 1000.0f;
private Vector2 _direction = Vector2.Right;
private Vector2 _startPosition;
private float _distanceTraveled = 0f;
private bool _hasHit = false;
private uint _ignoreCollisionLayer = 0; // Layer to ignore (will be set by turret)
public void Initialize(Vector2 direction, Vector2 position, float rotation, uint ignoreLayer = 0)
{
_direction = direction.Normalized();
Position = position;
Rotation = rotation;
_startPosition = position;
_ignoreCollisionLayer = ignoreLayer;
// Connect the area entered signal
BodyEntered += OnBodyEntered;
AreaEntered += OnAreaEntered;
}
public override void _PhysicsProcess(double delta)
{
if (_hasHit) return;
// Move the bullet
var movement = _direction * Speed * (float)delta;
Position += movement;
_distanceTraveled += movement.Length();
// Check if bullet has traveled max distance
if (_distanceTraveled >= MaxDistance)
{
QueueFree();
return;
}
}
private void OnBodyEntered(Node2D body)
{
HandleCollision(body);
}
private void OnAreaEntered(Area2D area)
{
HandleCollision(area);
}
private void HandleCollision(Node2D node)
{
if (_hasHit) return;
// Skip collision if it's on the ignore layer
if (node is PhysicsBody2D physicsBody &&
(physicsBody.CollisionLayer & _ignoreCollisionLayer) != 0)
{
return;
}
_hasHit = true;
// If we hit an enemy, deal damage
if (node is Enemy enemy)
{
// Get the global position where the bullet hit
var hitPosition = GlobalPosition;
enemy.TakeDamage(Damage, hitPosition);
}
// Optional: Add hit effect here
// CreateHitEffect();
// Remove the bullet
QueueFree();
}
private void CreateHitEffect()
{
// You can add a hit effect here if desired
// For example, a small explosion or impact sprite
}
public override void _ExitTree()
{
// Clean up signal connections
BodyEntered -= OnBodyEntered;
AreaEntered -= OnAreaEntered;
}
}

View File

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

13
Scripts/Entities/Enemy.cs Normal file
View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using AceFieldNewHorizon.Scripts.Tiles;
using Godot;
using AceFieldNewHorizon.Scripts.System;
namespace AceFieldNewHorizon.Scripts.Entities;
public partial class Enemy : BaseEnemy
{
// All the base functionality is now in BaseEnemy
// This class is kept for backward compatibility and can be used to add
// specific behaviors for the basic enemy type if needed
}

View File

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

View File

@@ -0,0 +1,283 @@
using System.Collections.Generic;
using AceFieldNewHorizon.Scripts.System;
using AceFieldNewHorizon.Scripts.Tiles;
using Godot;
namespace AceFieldNewHorizon.Scripts.Entities;
public abstract partial class BaseEnemy : CharacterBody2D
{
public const string EnemyGroupName = "Enemy";
[Export] public float MoveSpeed = 150.0f;
[Export] public float DetectionRadius = 300.0f;
[Export] public float AttackRadius = 80.0f;
[Export] public int Damage = 10;
[Export] public float AttackCooldown = 3.0f;
[Export] public int MaxHealth = 100;
[Export] public bool ShowDamageNumbers = true;
protected BaseTile TargetTile;
protected ReactorTile Reactor;
protected float AttackTimer = 0;
protected Area2D DetectionArea;
protected Area2D AttackArea;
protected int CurrentHealthValue;
protected ProgressBar HealthBar;
protected GridManager Grid;
// Track collisions with potential targets
protected readonly HashSet<BaseTile> CollidingTiles = [];
public int CurrentHealth
{
get => CurrentHealthValue;
protected set
{
CurrentHealthValue = Mathf.Clamp(value, 0, MaxHealth);
UpdateHealthBar();
if (CurrentHealthValue <= 0)
{
Die();
}
}
}
public bool IsDead => CurrentHealthValue <= 0;
public override void _Ready()
{
CurrentHealthValue = MaxHealth;
Grid = DependencyInjection.Container.GetInstance<GridManager>();
Reactor = GetTree().GetFirstNodeInGroup(ReactorTile.ReactorGroupName) as ReactorTile;
InitializeHealthBar();
InitializeDetectionArea();
InitializeAttackArea();
AddToGroup(EnemyGroupName);
}
protected virtual void InitializeHealthBar()
{
HealthBar = new ProgressBar
{
MaxValue = MaxHealth,
Value = CurrentHealthValue,
Size = new Vector2(40, 4),
ShowPercentage = false,
Visible = false
};
var healthBarContainer = new Control();
healthBarContainer.AddChild(HealthBar);
AddChild(healthBarContainer);
healthBarContainer.Position = new Vector2(-20, -20);
}
protected virtual void InitializeDetectionArea()
{
DetectionArea = new Area2D();
var collisionShape = new CollisionShape2D();
var shape = new CircleShape2D();
shape.Radius = DetectionRadius;
collisionShape.Shape = shape;
DetectionArea.AddChild(collisionShape);
AddChild(DetectionArea);
DetectionArea.BodyEntered += OnBodyEnteredDetection;
DetectionArea.BodyExited += OnBodyExitedDetection;
}
protected virtual void InitializeAttackArea()
{
AttackArea = GetNodeOrNull<Area2D>("AttackArea");
if (AttackArea != null)
{
AttackArea.BodyEntered += OnBodyEntered;
AttackArea.BodyExited += OnBodyExited;
}
}
public override void _Process(double delta)
{
if (IsDead) return;
if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree())
{
UpdateTarget();
}
MoveTowardsTarget();
HandleAttacks(delta);
}
protected virtual void MoveTowardsTarget()
{
if (TargetTile != null)
{
var direction = GlobalPosition.DirectionTo(TargetTile.GlobalPosition);
Velocity = direction * MoveSpeed;
LookAt(TargetTile.GlobalPosition);
MoveAndSlide();
}
}
protected virtual void HandleAttacks(double delta)
{
if (AttackTimer > 0)
{
AttackTimer -= (float)delta;
}
else if (CollidingTiles.Count > 0)
{
foreach (var tile in CollidingTiles)
{
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
{
TryAttackTile(tile);
break;
}
}
}
}
protected virtual void UpdateTarget()
{
// If we have a valid target in collision, use that
foreach (var tile in CollidingTiles)
{
if (tile != null && !tile.IsDestroyed && tile.IsInsideTree())
{
TargetTile = tile;
return;
}
}
// Otherwise find the reactor
if (Reactor != null && !Reactor.IsDestroyed && Reactor.IsInsideTree())
{
TargetTile = Reactor;
}
else
{
TargetTile = null;
}
}
protected virtual void TryAttackTile(BaseTile tile)
{
if (IsDead || tile == null || tile.IsDestroyed || !tile.IsInsideTree())
return;
AttackTimer = AttackCooldown;
bool wasDestroyed = tile.TakeDamage(Damage);
GD.Print($"Attacking {tile.Name} for {Damage} damage. Was destroyed: {wasDestroyed}");
if (wasDestroyed)
{
CollidingTiles.Remove(tile);
if (TargetTile == tile)
{
TargetTile = null;
}
}
}
protected virtual void OnBodyEntered(Node2D body)
{
if (body is BaseTile { IsDestroyed: false } tile && !body.IsInGroup("Hostile"))
{
GD.Print($"[Enemy] {body.Name} Entered attack range");
CollidingTiles.Add(tile);
if (TargetTile == null || TargetTile.IsDestroyed || !TargetTile.IsInsideTree())
{
TargetTile = tile;
}
// Attack immediately on collision
TryAttackTile(tile);
}
}
protected virtual void OnBodyExited(Node2D body)
{
if (body.GetParent() is BaseTile tile)
{
CollidingTiles.Remove(tile);
if (TargetTile == tile)
{
TargetTile = null;
}
}
}
protected virtual void OnBodyEnteredDetection(Node2D body)
{
// Can be overridden by derived classes
}
protected virtual void OnBodyExitedDetection(Node2D body)
{
// Can be overridden by derived classes
}
public virtual void TakeDamage(int damage, Vector2? hitPosition = null)
{
if (IsDead) return;
CurrentHealth -= damage;
// Show damage number (optional)
if (ShowDamageNumbers)
{
var damageLabel = new Label
{
Text = $"-{damage}",
Position = hitPosition ?? GlobalPosition,
ZIndex = 1000
};
GetTree().CurrentScene.AddChild(damageLabel);
// Animate and remove damage number
var tween = CreateTween();
tween.TweenProperty(damageLabel, "position:y", damageLabel.Position.Y - 30, 0.5f);
tween.TweenCallback(Callable.From(() => damageLabel.QueueFree())).SetDelay(0.5f);
}
// Visual feedback
var originalModulate = Modulate;
Modulate = new Color(1, 0.5f, 0.5f); // Flash red
var tweenFlash = CreateTween();
tweenFlash.TweenProperty(this, "modulate", originalModulate, 0.2f);
}
protected virtual void UpdateHealthBar()
{
if (HealthBar != null)
{
HealthBar.Value = CurrentHealthValue;
HealthBar.Visible = CurrentHealthValue < MaxHealth; // Only show when damaged
}
}
protected virtual void Die()
{
// Play death animation/sound
// You can add a death animation here
// Disable collisions and hide
SetProcess(false);
SetPhysicsProcess(false);
Hide();
// Queue free after a delay (for any death animation/sound to play)
var timer = new Timer();
AddChild(timer);
timer.Timeout += () => QueueFree();
timer.Start(0.5f);
}
}

View File

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

View File

@@ -1,4 +1,3 @@
using System;
using AceFieldNewHorizon.Scripts.System; using AceFieldNewHorizon.Scripts.System;
using Godot; using Godot;
@@ -20,7 +19,7 @@ public partial class Player : CharacterBody2D
[Export] public float ZoomDecay = 0.9f; [Export] public float ZoomDecay = 0.9f;
[Export] public float ZoomSmoothing = 10.0f; [Export] public float ZoomSmoothing = 10.0f;
[Export] public ResourceManager Inventory; public ResourceManager Inventory { get; private set; }
private Camera2D _camera; private Camera2D _camera;
private Vector2 _cameraTargetZoom = Vector2.One; private Vector2 _cameraTargetZoom = Vector2.One;
@@ -33,6 +32,8 @@ public partial class Player : CharacterBody2D
_camera = GetNode<Camera2D>("Camera2D"); _camera = GetNode<Camera2D>("Camera2D");
_cameraTargetZoom = _camera.Zoom; _cameraTargetZoom = _camera.Zoom;
Inventory = DependencyInjection.Container.GetInstance<ResourceManager>();
AddToGroup(ItemPickup.PickupGroupName); AddToGroup(ItemPickup.PickupGroupName);
AddToGroup(NaturalResourceGenerator.ChunkTrackerGroupName); AddToGroup(NaturalResourceGenerator.ChunkTrackerGroupName);
} }
@@ -60,13 +61,9 @@ public partial class Player : CharacterBody2D
// If same direction as last time, accelerate // If same direction as last time, accelerate
if (direction == _lastZoomDirection && (currentTime - _lastZoomTime) < 300) if (direction == _lastZoomDirection && (currentTime - _lastZoomTime) < 300)
{
_currentZoomSpeed = Mathf.Min(_currentZoomSpeed + ZoomAcceleration, MaxZoomSpeed); _currentZoomSpeed = Mathf.Min(_currentZoomSpeed + ZoomAcceleration, MaxZoomSpeed);
}
else else
{
_currentZoomSpeed = BaseZoomSpeed; _currentZoomSpeed = BaseZoomSpeed;
}
_lastZoomDirection = direction; _lastZoomDirection = direction;
_lastZoomTime = currentTime; _lastZoomTime = currentTime;

27
Scripts/Root.cs Normal file
View File

@@ -0,0 +1,27 @@
using Godot;
using AceFieldNewHorizon.Scripts.System;
using SimpleInjector;
namespace AceFieldNewHorizon.Scripts;
public partial class Root : Node
{
public override void _Ready()
{
// Dependency Injection container is now initialized via AutoLoad (DIInitializer.cs).
// Get references to the main system nodes from the scene tree
// and inject their dependencies.
// This assumes these nodes are direct children or easily accessible.
// You might need to adjust paths based on your scene setup.
// Example:
// var resourceManager = GetNode<ResourceManager>("ResourceSystem"); // Assuming ResourceManager is a child of Root
// DependencyInjection.Container.InjectProperties(resourceManager); // If ResourceManager had properties to inject
// For now, we'll manually resolve and assign for the main system nodes.
// The actual injection will happen in the _Ready methods of the system nodes themselves,
// by resolving from the static container.
// This is a common pattern when Godot instantiates the nodes.
}
}

1
Scripts/Root.cs.uid Normal file
View File

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

View File

@@ -40,15 +40,15 @@ public record BuildingData(
} }
} }
public partial class BuildingRegistry : Node public class BuildingRegistry
{ {
private Dictionary<string, BuildingData> _registry = new(); private Dictionary<string, BuildingData> _registry = new();
[Export] public string JsonPath { get; set; } = "res://Data/Buildings.json";
public override void _Ready() public BuildingRegistry(string jsonPath)
{ {
LoadFromJson(JsonPath); LoadFromJson(jsonPath);
} }
public void LoadFromJson(string path) public void LoadFromJson(string path)

View File

@@ -0,0 +1,23 @@
using Godot;
using Container = SimpleInjector.Container;
namespace AceFieldNewHorizon.Scripts.System;
public static class DependencyInjection
{
public static Container Container { get; private set; }
public static void Initialize()
{
Container = new Container();
// Register your system services here
// As singletons, since they are typically unique global managers
Container.RegisterSingleton<ResourceManager>();
Container.RegisterSingleton<GridManager>();
Container.RegisterSingleton(() => new BuildingRegistry("res://Data/Buildings.json"));
Container.Verify();
GD.Print("[DI] Simple Injector container initialized and verified.");
}
}

View File

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

View File

@@ -43,15 +43,33 @@ public partial class GridManager : Node
public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building) public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building)
{ {
// Get all cells that should be occupied by this building
var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation);
foreach (var cell in occupiedCells)
// Create a list to store cells that should be removed
var cellsToRemove = new List<Vector2I>();
// First, find all cells that match this building's position and size
foreach (var cell in _layers[layer].Keys.ToList())
{ {
if (_layers[layer].ContainsKey(cell)) var (building, buildingSize, buildingRotation) = _layers[layer][cell];
_layers[layer].Remove(cell); var buildingCells = GridUtils.GetOccupiedCells(cell, buildingSize, buildingRotation);
// If any of the building's cells match our target area, mark all of its cells for removal
if (buildingCells.Any(c => occupiedCells.Contains(c)))
{
cellsToRemove.AddRange(buildingCells);
}
}
// Remove all marked cells
foreach (var cell in cellsToRemove.Distinct())
{
_layers[layer].Remove(cell);
} }
} }
public Node2D? GetBuildingAtCell(Vector2I cell, GridLayer layer = GridLayer.Building) public Node2D? GetTileAtCell(Vector2I cell, GridLayer layer = GridLayer.Building)
{ {
return _layers[layer].TryGetValue(cell, out var data) ? data.Building : null; return _layers[layer].TryGetValue(cell, out var data) ? data.Building : null;
} }

102
Scripts/System/Hud.cs Normal file
View File

@@ -0,0 +1,102 @@
using Godot;
using System.Collections.Generic;
namespace AceFieldNewHorizon.Scripts.System;
public partial class Hud : CanvasLayer
{
private ResourceManager _resourceManager;
private VBoxContainer _resourceDisplay;
private readonly Dictionary<string, Label> _resourceLabels = new();
private VBoxContainer _pickupLogContainer;
public override void _Ready()
{
_resourceDisplay = GetNode<VBoxContainer>("ResourceDisplay");
_pickupLogContainer = new VBoxContainer();
_pickupLogContainer.Name = "PickupLogContainer";
_pickupLogContainer.SetAnchorsPreset(Control.LayoutPreset.BottomLeft);
_pickupLogContainer.OffsetLeft = 10;
_pickupLogContainer.OffsetBottom = -10; // Negative offset from bottom anchor
_pickupLogContainer.GrowVertical = Control.GrowDirection.Begin; // Make it grow upwards
AddChild(_pickupLogContainer);
_resourceManager = DependencyInjection.Container.GetInstance<ResourceManager>();
if (_resourceManager == null)
{
GD.PushError("ResourceSystem not found in the scene tree!");
return;
}
_resourceManager.OnResourceChanged += UpdateResourceDisplay;
_resourceManager.OnResourcePickedUp += OnResourcePickedUp;
// Initialize display with current resources
foreach (var entry in _resourceManager.GetAllResources())
{
UpdateResourceDisplay(entry.Key, entry.Value);
}
}
private void UpdateResourceDisplay(string resourceId, int newAmount)
{
if (!_resourceLabels.TryGetValue(resourceId, out Label label))
{
label = new Label();
_resourceDisplay.AddChild(label);
_resourceLabels.Add(resourceId, label);
}
if (newAmount <= 0)
{
// Remove label if resource amount is zero or less
if (label.GetParent() != null)
{
_resourceDisplay.RemoveChild(label);
}
_resourceLabels.Remove(resourceId);
label.QueueFree(); // Free the label from memory
}
else
{
label.Text = $"{resourceId}: {newAmount}";
}
}
private void OnResourcePickedUp(string resourceId, int amount)
{
AddPickupLogEntry($"Picked up {amount} {resourceId}!");
}
private void AddPickupLogEntry(string message)
{
var logLabel = new Label();
logLabel.Text = message;
_pickupLogContainer.AddChild(logLabel);
var timer = new Timer();
timer.WaitTime = 3.0f;
timer.OneShot = true;
timer.Autostart = true;
logLabel.AddChild(timer); // Add timer as child of the label
timer.Timeout += () => OnLogEntryTimeout(logLabel);
}
private void OnLogEntryTimeout(Label logLabel)
{
// Start fading out the label
var tween = logLabel.CreateTween();
tween.TweenProperty(logLabel, "modulate", new Color(1, 1, 1, 0), 0.5f); // Fade out over 0.5 seconds
tween.TweenCallback(Callable.From(() => logLabel.QueueFree())); // Free after fading
}
public override void _ExitTree()
{
if (_resourceManager != null)
{
_resourceManager.OnResourceChanged -= UpdateResourceDisplay;
_resourceManager.OnResourcePickedUp -= OnResourcePickedUp;
}
}
}

View File

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

View File

@@ -12,8 +12,8 @@ public partial class NaturalResourceGenerator : Node2D
{ {
public const string ChunkTrackerGroupName = "NrgTrackingTarget"; public const string ChunkTrackerGroupName = "NrgTrackingTarget";
[Export] public GridManager Grid { get; set; } public GridManager Grid { get; private set; }
[Export] public BuildingRegistry Registry { get; set; } public BuildingRegistry Registry { get; private set; }
[Export] public int ChunkSize = 16; [Export] public int ChunkSize = 16;
[Export] public int LoadDistance = 2; // Number of chunks to load in each direction [Export] public int LoadDistance = 2; // Number of chunks to load in each direction
@@ -25,6 +25,10 @@ public partial class NaturalResourceGenerator : Node2D
[Export] public int MaxIronVeinSize = 3; [Export] public int MaxIronVeinSize = 3;
[Export] public int Seed; [Export] public int Seed;
[Export] public bool SpawnEnemyNest = true;
[Export] public int MinDistanceFromOrigin = 20; // Minimum distance from world origin (0,0)
[Export] public int MaxDistanceFromOrigin = 50; // Maximum distance from world origin
private const string LogPrefix = "[NaturalGeneration]"; private const string LogPrefix = "[NaturalGeneration]";
private RandomNumberGenerator _rng; private RandomNumberGenerator _rng;
@@ -39,8 +43,39 @@ public partial class NaturalResourceGenerator : Node2D
public override void _Ready() public override void _Ready()
{ {
Grid = DependencyInjection.Container.GetInstance<GridManager>();
Registry = DependencyInjection.Container.GetInstance<BuildingRegistry>();
_rng = new RandomNumberGenerator(); _rng = new RandomNumberGenerator();
_rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi());
// Test if building registry is assigned
if (Registry == null)
{
GD.PrintErr($"{LogPrefix} BuildingRegistry is not assigned!");
return;
}
// Test if enemy_portal is in the registry
var testBuilding = Registry.GetBuilding("enemy_portal");
if (testBuilding == null)
{
GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!");
}
else
{
GD.Print($"{LogPrefix} Found enemy_portal in registry!");
}
GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}");
GD.Print($"{LogPrefix} Spawning the core reactor...");
SpawnCoreReactor();
if (SpawnEnemyNest)
{
GD.Print($"{LogPrefix} Attempting to spawn enemy nest...");
SpawnRandomEnemyNest();
}
} }
public override void _Process(double delta) public override void _Process(double delta)
@@ -121,9 +156,9 @@ public partial class NaturalResourceGenerator : Node2D
} }
// Generate stone veins // Generate stone veins
for (int x = 0; x < ChunkSize; x++) for (var x = 0; x < ChunkSize; x++)
{ {
for (int y = 0; y < ChunkSize; y++) for (var y = 0; y < ChunkSize; y++)
{ {
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
if (_rng.Randf() < StoneDensity) if (_rng.Randf() < StoneDensity)
@@ -131,6 +166,11 @@ public partial class NaturalResourceGenerator : Node2D
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize); var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
PlaceVeinInBackground(cell, "stone", veinSize, chunkData.StoneTiles); PlaceVeinInBackground(cell, "stone", veinSize, chunkData.StoneTiles);
} }
else if (_rng.Randf() < IronDensity)
{
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
PlaceVeinInBackground(cell, "ore_iron", veinSize, chunkData.IronTiles);
}
} }
} }
@@ -247,9 +287,9 @@ public partial class NaturalResourceGenerator : Node2D
// Remove all tiles in this chunk // Remove all tiles in this chunk
var chunkWorldPos = ChunkToWorldCoords(chunkPos); var chunkWorldPos = ChunkToWorldCoords(chunkPos);
for (int x = 0; x < ChunkSize; x++) for (var x = 0; x < ChunkSize; x++)
{ {
for (int y = 0; y < ChunkSize; y++) for (var y = 0; y < ChunkSize; y++)
{ {
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
// Free a 1x1 area for each cell // Free a 1x1 area for each cell
@@ -356,13 +396,47 @@ public partial class NaturalResourceGenerator : Node2D
GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles"); 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) private bool PlaceTile(string tileType, Vector2I cell)
{ {
try try
{ {
// First, remove any existing tile at this position
Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground);
var building = Registry.GetBuilding(tileType); var building = Registry.GetBuilding(tileType);
if (building == null) if (building == null)
{ {
@@ -370,6 +444,12 @@ public partial class NaturalResourceGenerator : Node2D
return false; return false;
} }
// Free area for ground layer if needed
if (building.Layer == GridLayer.Ground)
Grid.FreeArea(cell, building.Size, 0f, GridLayer.Ground);
else
GD.Print($"{LogPrefix} Attempting placing building tile {tileType} at {cell}.");
var scene = building.Scene; var scene = building.Scene;
if (scene == null) if (scene == null)
{ {
@@ -383,20 +463,23 @@ public partial class NaturalResourceGenerator : Node2D
return false; return false;
} }
// Use the same positioning logic as PlacementManager // Calculate position with proper offset based on building size
var rotatedSize = building.GetRotatedSize(0f); // 0f for no rotation var rotatedSize = building.GetRotatedSize(0f);
var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); // 0f for no rotation var offset = GridUtils.GetCenterOffset(rotatedSize, 0f);
instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset;
instance.ZIndex = (int)building.Layer; instance.ZIndex = (int)building.Layer;
instance.Grid = Grid; instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset;
AddChild(instance); AddChild(instance);
// Occupy the appropriate area based on building size
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer);
// GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell}");
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
GD.PrintErr($"{LogPrefix} Error placing {tileType} at {cell}: {e.Message}"); GD.PrintErr($"{LogPrefix} Error placing {tileType} at {cell}: {e.Message}");
GD.Print($"{LogPrefix} Stack trace: {e.StackTrace}");
return false; return false;
} }
} }

View File

@@ -8,15 +8,15 @@ namespace AceFieldNewHorizon.Scripts.System;
public partial class PlacementManager : Node2D public partial class PlacementManager : Node2D
{ {
[Export] public GridManager Grid { get; set; } public GridManager Grid { get; private set; }
[Export] public ResourceManager Inventory { get; set; } public ResourceManager Inventory { get; private set; }
[Export] public BuildingRegistry Registry { get; set; } public BuildingRegistry Registry { get; private set; }
[Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor [Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor
[Export] public bool Enabled { get; set; } = true; [Export] public bool Enabled { get; set; } = true;
[Export] public StringName ToggleBuildAction { get; set; } = "toggle_build"; [Export] public StringName ToggleBuildAction { get; set; } = "toggle_build";
private static readonly List<string> BuildableTiles = ["wall", "miner"]; private static readonly List<string> BuildableTiles = ["wall", "miner", "turret"];
private readonly Dictionary<Node2D, BuildTask> _buildTasks = new(); private readonly Dictionary<Node2D, BuildTask> _buildTasks = new();
private AudioStreamPlayer _completionSound; private AudioStreamPlayer _completionSound;
private AudioStreamPlayer _buildingSound; private AudioStreamPlayer _buildingSound;
@@ -30,6 +30,10 @@ public partial class PlacementManager : Node2D
{ {
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 = CreateAudioPlayer("res://Sounds/Events/ConstructionComplete.wav"); _completionSound = CreateAudioPlayer("res://Sounds/Events/ConstructionComplete.wav");
_buildingSound = CreateAudioPlayer("res://Sounds/Events/Building.wav"); _buildingSound = CreateAudioPlayer("res://Sounds/Events/Building.wav");
@@ -199,7 +203,6 @@ public partial class PlacementManager : Node2D
var scene = building.Scene; var scene = building.Scene;
_ghostBuilding = (BaseTile)scene.Instantiate(); _ghostBuilding = (BaseTile)scene.Instantiate();
_ghostBuilding.Grid = Grid;
_ghostBuilding.SetGhostMode(true); _ghostBuilding.SetGhostMode(true);
_ghostBuilding.RotationDegrees = _currentRotation; _ghostBuilding.RotationDegrees = _currentRotation;
_ghostBuilding.ZAsRelative = false; _ghostBuilding.ZAsRelative = false;
@@ -229,7 +232,7 @@ public partial class PlacementManager : Node2D
// Check if the area is occupied by under-construction tiles // Check if the area is occupied by under-construction tiles
var occupiedCells = GridUtils.GetOccupiedCells(_hoveredCell, building.Size, _currentRotation); var occupiedCells = GridUtils.GetOccupiedCells(_hoveredCell, building.Size, _currentRotation);
var isUnderConstruction = occupiedCells.Any(cell => var isUnderConstruction = occupiedCells.Any(cell =>
Grid.GetBuildingAtCell(cell, building.Layer) is BaseTile { IsConstructing: true }); Grid.GetTileAtCell(cell, building.Layer) is BaseTile { IsConstructing: true });
if (!isUnderConstruction) if (!isUnderConstruction)
_cannotDeploySound.Play(); _cannotDeploySound.Play();
@@ -247,7 +250,6 @@ public partial class PlacementManager : Node2D
// Create the building instance // Create the building instance
var scene = building.Scene; var scene = building.Scene;
var buildingInstance = (BaseTile)scene.Instantiate(); var buildingInstance = (BaseTile)scene.Instantiate();
buildingInstance.Grid = Grid;
buildingInstance.RotationDegrees = _currentRotation; buildingInstance.RotationDegrees = _currentRotation;
buildingInstance.ZIndex = (int)building.Layer; buildingInstance.ZIndex = (int)building.Layer;
buildingInstance.Position = _ghostBuilding.Position; buildingInstance.Position = _ghostBuilding.Position;
@@ -289,7 +291,7 @@ 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
@@ -426,32 +428,28 @@ public static class GridManagerExtensions
public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid, public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid,
Vector2I cell, GridLayer layer) Vector2I cell, GridLayer layer)
{ {
if (grid.GetBuildingAtCell(cell, layer) is { } building) if (grid.GetTileAtCell(cell, layer) is not { } building) return null;
// Find the top-left position of the building
for (var x = 0; x < 100; x++) // Arbitrary max size
{ {
// Find the top-left position of the building for (var y = 0; y < 100; y++)
for (int x = 0; x < 100; x++) // Arbitrary max size
{ {
for (int y = 0; y < 100; y++) var checkCell = new Vector2I(cell.X - x, cell.Y - y);
{ if (grid.GetTileAtCell(checkCell, layer) != building) continue;
var checkCell = new Vector2I(cell.X - x, cell.Y - y); // Found the top-left corner, now find the size
if (grid.GetBuildingAtCell(checkCell, layer) == building) var size = Vector2I.One;
{ // Search right
// Found the top-left corner, now find the size while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) ==
var size = Vector2I.One; building)
// Search right size.X++;
while (grid.GetBuildingAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == // Search down
building) while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
size.X++; building)
// Search down size.Y++;
while (grid.GetBuildingAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) ==
building)
size.Y++;
// Get rotation from the first cell // Get rotation from the first cell
var rotation = 0f; // You'll need to store rotation in GridManager to make this work var rotation = 0f; // You'll need to store rotation in Grid to make this work
return (checkCell, size, rotation); return (checkCell, size, rotation);
}
}
} }
} }

View File

@@ -13,6 +13,10 @@ public partial class ResourceManager : Node
// Event for when resource amounts change // Event for when resource amounts change
[Signal] [Signal]
public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount); public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount);
// Event for when resources are picked up (added)
[Signal]
public delegate void OnResourcePickedUpEventHandler(string resourceId, int amount);
public override void _Ready() public override void _Ready()
{ {
@@ -43,6 +47,7 @@ public partial class ResourceManager : Node
} }
EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]); EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]);
EmitSignal(nameof(OnResourcePickedUp), resourceId, amount);
} }
// Remove resources of a specific type // Remove resources of a specific type

View File

@@ -9,30 +9,46 @@ namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class BaseTile : Node2D public partial class BaseTile : Node2D
{ {
[Export] public string TileId { get; set; } [Export] public string TileId { get; set; }
[Export] public GridManager Grid { 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 IsConstructing;
public bool IsConstructed; 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 // Don't modify collision for constructing buildings
if (IsConstructing) return; if (IsConstructing) return;
if (_collisionShape != null) if (_collisionShape != null)
_collisionShape.Disabled = true; _collisionShape.Disabled = true;
@@ -42,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;
@@ -50,6 +66,81 @@ 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)
{ {
@@ -57,7 +148,7 @@ public partial class BaseTile : Node2D
if (_collisionShape != null) if (_collisionShape != null)
_collisionShape.Disabled = true; _collisionShape.Disabled = true;
if (_progressOverlay == null || _sprite?.Texture == null) if (_progressOverlay == null || _sprite?.Texture == null)
{ {
IsConstructing = false; IsConstructing = false;
onComplete?.Invoke(); onComplete?.Invoke();
@@ -95,15 +186,15 @@ public partial class BaseTile : Node2D
// Fade out the overlay // Fade out the overlay
await FadeOutOverlay(0.5f); await FadeOutOverlay(0.5f);
// Construction complete - restore full opacity and enable collision // Construction complete - restore full opacity and enable collision
if (_sprite != null) if (_sprite != null)
_sprite.Modulate = Colors.White; _sprite.Modulate = Colors.White;
IsConstructing = false; IsConstructing = false;
if (_collisionShape != null) if (_collisionShape != null)
_collisionShape.Disabled = false; _collisionShape.Disabled = false;
_onConstructionComplete?.Invoke(); _onConstructionComplete?.Invoke();
IsConstructed = true; IsConstructed = true;
} }

View File

@@ -0,0 +1,129 @@
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class EnemyPortalTile : BaseTile
{
[Export] public PackedScene EnemyScene;
[Export] public int MaxEnemies = 5;
[Export] public float SpawnDelay = 5.0f; // Time between spawn attempts
[Export] public float SpawnRadius = 50.0f; // Radius around the nest where enemies can spawn
[Export] public bool Active = true;
private Timer _spawnTimer;
private int _currentEnemyCount;
public override void _Ready()
{
base._Ready();
// Add to Hostile group to prevent enemies from attacking their own nest
AddToGroup("Hostile");
// Create and configure the timer
_spawnTimer = new Timer
{
Autostart = true,
WaitTime = SpawnDelay
};
AddChild(_spawnTimer);
_spawnTimer.Timeout += OnSpawnTimerTimeout;
var sprite = GetNode<Sprite2D>("Sprite2D");
// Create and configure the shadow sprite
var shadow = new Sprite2D
{
Texture = sprite.Texture,
Scale = sprite.Scale * 1.05f, // Slightly larger than the original
Modulate = new Color(0, 0, 0, 0.5f), // Slightly more transparent
Position = new Vector2(0, 6), // Closer to the sprite (reduced from 30)
ZIndex = -1
};
AddChild(shadow);
// Create floating animation
const float floatOffset = 5.0f;
const float floatDuration = 2.0f;
var tween = CreateTween().SetLoops();
tween.TweenProperty(sprite, "position:y", -floatOffset, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(sprite, "position:y", floatOffset, floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(sprite, "position:y", 0, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
// Animate shadow
tween.Parallel().TweenProperty(shadow, "position:y", 12 - (floatOffset * 0.3f), floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.02f, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.Parallel().TweenProperty(shadow, "modulate:a", 0.6f, floatDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine);
tween.Parallel().TweenProperty(shadow, "position:y", 12 + (floatOffset * 0.4f), floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine)
.SetDelay(floatDuration);
tween.Parallel().TweenProperty(shadow, "scale", sprite.Scale * 1.08f, floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine)
.SetDelay(floatDuration);
tween.Parallel().TweenProperty(shadow, "modulate:a", 0.4f, floatDuration * 2)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Sine)
.SetDelay(floatDuration);
}
private void OnSpawnTimerTimeout()
{
if (!Active || EnemyScene == null || _currentEnemyCount >= MaxEnemies)
return;
// Check if we can spawn more enemies
var enemies = GetTree().GetNodesInGroup("Enemy");
_currentEnemyCount = enemies.Count;
if (_currentEnemyCount >= MaxEnemies)
return;
// Spawn a new enemy
var enemy = EnemyScene.Instantiate<Enemy>();
if (enemy != null)
{
GetParent().AddChild(enemy);
// Calculate a random position within the spawn radius
var randomAngle = GD.Randf() * Mathf.Pi * 2;
var randomOffset = new Vector2(
Mathf.Cos(randomAngle) * SpawnRadius,
Mathf.Sin(randomAngle) * SpawnRadius
);
enemy.GlobalPosition = GlobalPosition + randomOffset;
_currentEnemyCount++;
// Connect to the enemy's death signal if available
enemy.TreeExiting += () => OnEnemyDied();
}
}
private void OnEnemyDied()
{
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
}
public void SetActive(bool active)
{
Active = active;
_spawnTimer.Paused = !active;
}
}

View File

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

View File

@@ -8,6 +8,7 @@ public partial class GroundTile : BaseTile
{ {
var sprite = GetNode<Sprite2D>("Sprite2D"); var sprite = GetNode<Sprite2D>("Sprite2D");
sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker
sprite.ZIndex = -10;
base._Ready(); base._Ready();
} }

View File

@@ -6,7 +6,7 @@ namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class MinerTile : BaseTile public partial class MinerTile : BaseTile
{ {
[Export] public PackedScene ItemPickup { get; set; } [Export] public PackedScene ItemPickup { get; set; }
[Export] public string ItemToMine { get; set; } = "OreIron"; [Export] public string ItemToMine { get; set; }
[Export] public int MiningRate = 1; // Items per second [Export] public int MiningRate = 1; // Items per second
private Vector2I _gridPosition; private Vector2I _gridPosition;
@@ -16,10 +16,29 @@ public partial class MinerTile : BaseTile
{ {
base._Ready(); base._Ready();
_gridPosition = GridUtils.WorldToGrid(Position); _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) public override void _Process(double delta)
{ {
if (ItemToMine == null)
return;
// Don't mine if building is not completed // Don't mine if building is not completed
if (!IsConstructed || ItemPickup == null) if (!IsConstructed || ItemPickup == null)
return; return;

View File

@@ -0,0 +1,13 @@
namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class ReactorTile : BaseTile
{
public static readonly string ReactorGroupName = "Reactor";
public override void _Ready()
{
base._Ready();
AddToGroup(ReactorGroupName);
}
}

View File

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

127
Scripts/Tiles/TurretTile.cs Normal file
View File

@@ -0,0 +1,127 @@
using System.Linq;
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class TurretTile : BaseTile
{
[Export] public PackedScene BulletScene;
[Export] public float RotationSpeed = 2.0f; // Radians per second
[Export] public float AttackRange = 300.0f;
[Export] public float AttackCooldown = 0.5f;
[Export] public int Damage = 10;
[Export] public float BulletSpeed = 400.0f;
[Export] public NodePath BarrelTipPath;
private Node2D _spriteBarrel;
private Node2D _barrelTip;
private float _attackTimer = 0;
private bool _hasTarget = false;
public override void _Ready()
{
base._Ready();
_spriteBarrel = GetNodeOrNull<Node2D>("Barrel");
_barrelTip = GetNodeOrNull<Node2D>(BarrelTipPath);
if (_barrelTip == null && _spriteBarrel != null)
{
// If no barrel tip is specified, use the end of the barrel sprite
_barrelTip = _spriteBarrel.GetNodeOrNull<Node2D>("BarrelTip");
}
}
public override void SetGhostMode(bool canPlace)
{
base.SetGhostMode(canPlace);
if (_spriteBarrel != null)
_spriteBarrel.Modulate = canPlace
? new Color(0, 1, 0, 0.5f)
: new Color(1, 0, 0, 0.5f);
}
public override void FinalizePlacement()
{
base.FinalizePlacement();
if (_spriteBarrel != null)
_spriteBarrel.Modulate = Colors.White;
}
public override void _Process(double delta)
{
if (!IsConstructed) return;
// Update attack cooldown
if (_attackTimer > 0)
{
_attackTimer -= (float)delta;
}
// Find the nearest enemy
var enemies = GetTree().GetNodesInGroup(Enemy.EnemyGroupName)
.OfType<Enemy>()
.Where(e => e.GlobalPosition.DistanceTo(GlobalPosition) <= AttackRange)
.OrderBy(e => e.GlobalPosition.DistanceTo(GlobalPosition))
.ToList();
if (enemies.Count > 0)
{
var nearestEnemy = enemies[0];
_hasTarget = true;
// Calculate target angle
var direction = (nearestEnemy.GlobalPosition - _barrelTip.GlobalPosition).Normalized();
var targetAngle = direction.Angle() + 90f;
// Smoothly rotate towards target
_spriteBarrel.Rotation = Mathf.LerpAngle(_spriteBarrel.Rotation, targetAngle, (float)delta * RotationSpeed);
// Check if we're facing the target and can attack
if (Mathf.Abs(Mathf.Wrap(targetAngle - _spriteBarrel.Rotation, -Mathf.Pi, Mathf.Pi)) < 0.1f)
TryAttack(nearestEnemy.GlobalPosition);
}
else
{
_hasTarget = false;
}
}
private void TryAttack(Vector2 targetPosition)
{
if (_attackTimer <= 0 && BulletScene != null && _barrelTip != null)
{
// Create bullet instance
var bullet = BulletScene.Instantiate<Bullet>();
// Calculate direction and rotation
var direction = (targetPosition - _barrelTip.GlobalPosition).Normalized();
var bulletRotation = direction.Angle();
// Set bullet position and rotation
GetTree().CurrentScene.AddChild(bullet);
bullet.GlobalPosition = _barrelTip.GlobalPosition;
bullet.Rotation = bulletRotation; // Use the calculated rotation
// Initialize bullet with direction and damage
bullet.Initialize(
direction,
bullet.GlobalPosition,
bulletRotation, // Pass the calculated rotation
1 // Pass the turret's collision layer to ignore
);
bullet.Damage = Damage;
bullet.Speed = BulletSpeed;
bullet.MaxDistance = AttackRange * 1.5f; // Bullets can travel slightly further than attack range
// Reset attack cooldown
_attackTimer = AttackCooldown;
GD.Print("[Turret] Turret firing!");
}
}
}

View File

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

View File

@@ -15,8 +15,15 @@ run/main_scene="uid://c22aprj452aha"
config/features=PackedStringArray("4.4", "C#", "GL Compatibility") config/features=PackedStringArray("4.4", "C#", "GL Compatibility")
config/icon="res://icon.svg" config/icon="res://icon.svg"
[autoload]
ResourceManager="res://Scripts/System/ResourceManager.cs"
DiInitializer="*res://Scripts/AutoLoad/DIInitializer.cs"
[display] [display]
window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="viewport" window/stretch/mode="viewport"
[dotnet] [dotnet]
@@ -77,11 +84,12 @@ switch_tile={
} }
toggle_build={ toggle_build={
"deadzone": 0.2, "deadzone": 0.2,
"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":66,"key_label":0,"unicode":98,"location":0,"echo":false,"script":null) "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