From 32f96d488d849da181837fe01efdd8b545cf32b6 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 30 Aug 2025 02:06:58 +0800 Subject: [PATCH] :sparkles: Enemy and nest --- Data/Buildings.json | 9 +++ Scenes/Entities/Enemy.jpg.import | 34 ++++++++ Scenes/Entities/Enemy.tscn | 17 ++++ Scenes/Tiles/EnemyNest.jpg.import | 34 ++++++++ Scenes/Tiles/EnemyNest.tscn | 19 +++++ Scenes/Tiles/OreIronTile.tscn | 2 +- Scripts/Entities/Enemy.cs | 89 +++++++++++++++++++++ Scripts/Entities/Enemy.cs.uid | 1 + Scripts/System/NaturalResourceGenerator.cs | 93 ++++++++++++++++++++-- Scripts/Tiles/EnemyNestTile.cs | 35 ++++++++ Scripts/Tiles/EnemyNestTile.cs.uid | 1 + project.godot | 3 +- 12 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 Scenes/Entities/Enemy.jpg.import create mode 100644 Scenes/Entities/Enemy.tscn create mode 100644 Scenes/Tiles/EnemyNest.jpg.import create mode 100644 Scenes/Tiles/EnemyNest.tscn create mode 100644 Scripts/Entities/Enemy.cs create mode 100644 Scripts/Entities/Enemy.cs.uid create mode 100644 Scripts/Tiles/EnemyNestTile.cs create mode 100644 Scripts/Tiles/EnemyNestTile.cs.uid diff --git a/Data/Buildings.json b/Data/Buildings.json index ccb6a7a..f185b09 100644 --- a/Data/Buildings.json +++ b/Data/Buildings.json @@ -21,6 +21,15 @@ "layer": 1, "size": [1, 1] }, + "enemy_nest": { + "scene": "res://Scenes/Tiles/EnemyNest.tscn", + "cost": {}, + "durability": 200, + "buildTime": 0.0, + "allowedRotations": [0], + "layer": 1, + "size": [1, 1] + }, "ground": { "scene": "res://Scenes/Tiles/GroundTile.tscn", "cost": {}, diff --git a/Scenes/Entities/Enemy.jpg.import b/Scenes/Entities/Enemy.jpg.import new file mode 100644 index 0000000..ed25537 --- /dev/null +++ b/Scenes/Entities/Enemy.jpg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dlhpiyxtmp707" +path="res://.godot/imported/Enemy.jpg-862b79047d7834ee48aa6bbd7e126824.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Scenes/Entities/Enemy.jpg" +dest_files=["res://.godot/imported/Enemy.jpg-862b79047d7834ee48aa6bbd7e126824.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 diff --git a/Scenes/Entities/Enemy.tscn b/Scenes/Entities/Enemy.tscn new file mode 100644 index 0000000..eb6ae57 --- /dev/null +++ b/Scenes/Entities/Enemy.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=4 format=3 uid="uid://b3ffcucytwmk"] + +[ext_resource type="Texture2D" uid="uid://dlhpiyxtmp707" path="res://Scenes/Entities/Enemy.jpg" id="1_8q37v"] +[ext_resource type="Script" uid="uid://cvsmy820b8dwl" path="res://Scripts/Entities/Enemy.cs" id="1_jajit"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_jajit"] +size = Vector2(60, 74) + +[node name="Enemy" type="CharacterBody2D"] +script = ExtResource("1_jajit") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_jajit") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.1, 0.1) +texture = ExtResource("1_8q37v") diff --git a/Scenes/Tiles/EnemyNest.jpg.import b/Scenes/Tiles/EnemyNest.jpg.import new file mode 100644 index 0000000..7dc6bfe --- /dev/null +++ b/Scenes/Tiles/EnemyNest.jpg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vwfs68ftvjr4" +path="res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Scenes/Tiles/EnemyNest.jpg" +dest_files=["res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.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 diff --git a/Scenes/Tiles/EnemyNest.tscn b/Scenes/Tiles/EnemyNest.tscn new file mode 100644 index 0000000..8aed99b --- /dev/null +++ b/Scenes/Tiles/EnemyNest.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=5 format=3 uid="uid://dup2su0s3ybcy"] + +[ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyNestTile.cs" id="1_4g0ff"] +[ext_resource type="Texture2D" uid="uid://vwfs68ftvjr4" path="res://Scenes/Tiles/EnemyNest.jpg" id="1_id484"] +[ext_resource type="PackedScene" uid="uid://b3ffcucytwmk" path="res://Scenes/Entities/Enemy.tscn" id="2_pka71"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_id484"] +size = Vector2(54, 54) + +[node name="EnemyNest" type="CharacterBody2D"] +script = ExtResource("1_4g0ff") +EnemyScene = ExtResource("2_pka71") + +[node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.056, 0.056) +texture = ExtResource("1_id484") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_id484") diff --git a/Scenes/Tiles/OreIronTile.tscn b/Scenes/Tiles/OreIronTile.tscn index e2d6e63..6fb6877 100644 --- a/Scenes/Tiles/OreIronTile.tscn +++ b/Scenes/Tiles/OreIronTile.tscn @@ -9,7 +9,7 @@ size = Vector2(54, 54) [node name="OreIronTile" type="StaticBody2D"] collision_layer = 0 script = ExtResource("1_exnim") -TileId = "stone_iron" +TileId = "ore_iron" [node name="Sprite2D" type="Sprite2D" parent="."] position = Vector2(1.49012e-08, -9.53674e-07) diff --git a/Scripts/Entities/Enemy.cs b/Scripts/Entities/Enemy.cs new file mode 100644 index 0000000..eeac6e2 --- /dev/null +++ b/Scripts/Entities/Enemy.cs @@ -0,0 +1,89 @@ +using Godot; + +namespace AceFieldNewHorizon.Scripts.Entities; + +public partial class Enemy : CharacterBody2D +{ + [Export] public float MoveSpeed = 150.0f; + [Export] public float DetectionRadius = 300.0f; + [Export] public float AttackRange = 50.0f; + [Export] public int Damage = 10; + [Export] public float AttackCooldown = 1.0f; + + private Player _player; + private float _attackTimer = 0; + private bool _isPlayerInRange = false; + private Area2D _detectionArea; + + public override void _Ready() + { + // Create detection area + _detectionArea = new Area2D(); + var collisionShape = new CollisionShape2D(); + var shape = new CircleShape2D(); + shape.Radius = DetectionRadius; + collisionShape.Shape = shape; + _detectionArea.AddChild(collisionShape); + AddChild(_detectionArea); + + // Connect signals + _detectionArea.BodyEntered += OnBodyEnteredDetection; + _detectionArea.BodyExited += OnBodyExitedDetection; + } + + public override void _Process(double delta) + { + if (_player != null && _isPlayerInRange) + { + // Face the player + LookAt(_player.GlobalPosition); + + // Move towards player if not in attack range + var direction = GlobalPosition.DirectionTo(_player.GlobalPosition); + var distance = GlobalPosition.DistanceTo(_player.GlobalPosition); + + if (distance > AttackRange) + { + Velocity = direction * MoveSpeed; + } + else + { + Velocity = Vector2.Zero; + TryAttackPlayer(delta); + } + + MoveAndSlide(); + } + } + + private void TryAttackPlayer(double delta) + { + _attackTimer += (float)delta; + + if (_attackTimer >= AttackCooldown) + { + _attackTimer = 0; + // Here you can implement the attack logic + // For example: _player.TakeDamage(Damage); + GD.Print("Enemy attacks player!"); + } + } + + private void OnBodyEnteredDetection(Node2D body) + { + if (body is Player player) + { + _player = player; + _isPlayerInRange = true; + } + } + + private void OnBodyExitedDetection(Node2D body) + { + if (body == _player) + { + _isPlayerInRange = false; + Velocity = Vector2.Zero; + } + } +} \ No newline at end of file diff --git a/Scripts/Entities/Enemy.cs.uid b/Scripts/Entities/Enemy.cs.uid new file mode 100644 index 0000000..a3de25d --- /dev/null +++ b/Scripts/Entities/Enemy.cs.uid @@ -0,0 +1 @@ +uid://cvsmy820b8dwl diff --git a/Scripts/System/NaturalResourceGenerator.cs b/Scripts/System/NaturalResourceGenerator.cs index 49d9731..5a09b81 100644 --- a/Scripts/System/NaturalResourceGenerator.cs +++ b/Scripts/System/NaturalResourceGenerator.cs @@ -25,6 +25,10 @@ public partial class NaturalResourceGenerator : Node2D [Export] public int MaxIronVeinSize = 3; [Export] public int Seed; + [Export] public bool SpawnEnemyNest = true; + [Export] public int MinDistanceFromOrigin = 20; // Minimum distance from world origin (0,0) + [Export] public int MaxDistanceFromOrigin = 50; // Maximum distance from world origin + private const string LogPrefix = "[NaturalGeneration]"; private RandomNumberGenerator _rng; @@ -41,6 +45,32 @@ public partial class NaturalResourceGenerator : Node2D { _rng = new RandomNumberGenerator(); _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_nest is in the registry + var testBuilding = Registry.GetBuilding("enemy_nest"); + if (testBuilding == null) + { + GD.PrintErr($"{LogPrefix} 'enemy_nest' is not found in BuildingRegistry!"); + } + else + { + GD.Print($"{LogPrefix} Found enemy_nest in registry!"); + } + + GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}"); + + if (SpawnEnemyNest) + { + GD.Print($"{LogPrefix} Attempting to spawn enemy nest..."); + SpawnRandomEnemyNest(); + } } public override void _Process(double delta) @@ -361,12 +391,52 @@ public partial class NaturalResourceGenerator : Node2D GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles"); } + 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 + int attempts = 0; + const int maxAttempts = 10; + + while (attempts < maxAttempts) + { + if (PlaceTile("enemy_nest", 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 { - // First, remove any existing tile at this position - Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground); + // For enemy nest, we want to place it on top of existing ground + if (tileType != "enemy_nest") + { + // Original behavior for other tile types + Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground); + } + else + { + // For enemy nest, check if there's ground below (skip for now) + GD.Print($"{LogPrefix} Attempting placing nest at {cell}."); + } var building = Registry.GetBuilding(tileType); if (building == null) @@ -374,6 +444,7 @@ public partial class NaturalResourceGenerator : Node2D GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}"); return false; } + // GD.Print($"{LogPrefix} Found building in registry: {tileType}"); var scene = building.Scene; if (scene == null) @@ -381,27 +452,35 @@ public partial class NaturalResourceGenerator : Node2D GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}"); return false; } + // GD.Print($"{LogPrefix} Scene loaded for {tileType}"); if (scene.Instantiate() is not BaseTile instance) { GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}"); return false; } + // GD.Print($"{LogPrefix} Successfully instantiated {tileType}"); // Use the same positioning logic as PlacementManager - var rotatedSize = building.GetRotatedSize(0f); // 0f for no rotation - var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); // 0f for no rotation - instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset; + var rotatedSize = building.GetRotatedSize(0f); + var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); + instance.ZIndex = (int)building.Layer; + instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset; instance.Grid = Grid; AddChild(instance); - Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); - // GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell}"); + + // For enemy nest, use Building layer + var layer = tileType == "enemy_nest" ? GridLayer.Building : building.Layer; + Grid.OccupyArea(cell, instance, building.Size, 0f, layer); + + // GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} on layer {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; } } diff --git a/Scripts/Tiles/EnemyNestTile.cs b/Scripts/Tiles/EnemyNestTile.cs new file mode 100644 index 0000000..94e8498 --- /dev/null +++ b/Scripts/Tiles/EnemyNestTile.cs @@ -0,0 +1,35 @@ +using Godot; + +namespace AceFieldNewHorizon.Scripts.Tiles; + +public partial class EnemyNestTile : BaseTile +{ + [Export] public PackedScene EnemyScene; + private Timer _spawnTimer; + + public override void _Ready() + { + base._Ready(); + + // Create and configure the timer + _spawnTimer = new Timer(); + AddChild(_spawnTimer); + _spawnTimer.Timeout += OnSpawnTimerTimeout; + _spawnTimer.Start(1.0f); // Start with 1 second interval + } + + private void OnSpawnTimerTimeout() + { + if (EnemyScene != null) + { + var enemy = EnemyScene.Instantiate(); + GetParent().AddChild(enemy); + + // Position the enemy at the nest's position + if (enemy is Node2D enemy2D) + { + enemy2D.GlobalPosition = GlobalPosition; + } + } + } +} \ No newline at end of file diff --git a/Scripts/Tiles/EnemyNestTile.cs.uid b/Scripts/Tiles/EnemyNestTile.cs.uid new file mode 100644 index 0000000..1bb94fb --- /dev/null +++ b/Scripts/Tiles/EnemyNestTile.cs.uid @@ -0,0 +1 @@ +uid://26hl5mk4mqur diff --git a/project.godot b/project.godot index 38d968e..fbcd135 100644 --- a/project.godot +++ b/project.godot @@ -77,8 +77,7 @@ switch_tile={ } toggle_build={ "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]