diff --git a/Data/Buildings.json b/Data/Buildings.json index cc3cfc7..28d510f 100644 --- a/Data/Buildings.json +++ b/Data/Buildings.json @@ -42,14 +42,14 @@ "layer": 1, "size": [3, 3] }, - "enemy_nest": { - "scene": "res://Scenes/Tiles/EnemyNest.tscn", + "enemy_portal": { + "scene": "res://Scenes/Tiles/EnemyPortalTile.tscn", "cost": {}, "durability": 200, "buildTime": 0.0, "allowedRotations": [0], "layer": 1, - "size": [1, 1] + "size": [1, 2] }, "ground": { "scene": "res://Scenes/Tiles/GroundTile.tscn", diff --git a/Scenes/Tiles/EnemyNest.tscn b/Scenes/Tiles/EnemyNest.tscn deleted file mode 100644 index 3db4ad7..0000000 --- a/Scenes/Tiles/EnemyNest.tscn +++ /dev/null @@ -1,20 +0,0 @@ -[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="StaticBody2D"] -script = ExtResource("1_4g0ff") -EnemyScene = ExtResource("2_pka71") -TileId = "enemy_nest" - -[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/EnemyPortalTile.png b/Scenes/Tiles/EnemyPortalTile.png new file mode 100644 index 0000000..4ee9249 Binary files /dev/null and b/Scenes/Tiles/EnemyPortalTile.png differ diff --git a/Scenes/Tiles/EnemyNest.jpg.import b/Scenes/Tiles/EnemyPortalTile.png.import similarity index 67% rename from Scenes/Tiles/EnemyNest.jpg.import rename to Scenes/Tiles/EnemyPortalTile.png.import index 7dc6bfe..3b55e01 100644 --- a/Scenes/Tiles/EnemyNest.jpg.import +++ b/Scenes/Tiles/EnemyPortalTile.png.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://vwfs68ftvjr4" -path="res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex" +uid="uid://dv2xwfyshxdtp" +path="res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://Scenes/Tiles/EnemyNest.jpg" -dest_files=["res://.godot/imported/EnemyNest.jpg-9a1f582f2843b75fdeff38422d3798b9.ctex"] +source_file="res://Scenes/Tiles/EnemyPortalTile.png" +dest_files=["res://.godot/imported/EnemyPortalTile.png-3904776a211e67c58254b1bdc9aba071.ctex"] [params] diff --git a/Scenes/Tiles/EnemyPortalTile.tscn b/Scenes/Tiles/EnemyPortalTile.tscn new file mode 100644 index 0000000..2aefdfd --- /dev/null +++ b/Scenes/Tiles/EnemyPortalTile.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=4 format=3 uid="uid://dup2su0s3ybcy"] + +[ext_resource type="Script" uid="uid://26hl5mk4mqur" path="res://Scripts/Tiles/EnemyPortalTile.cs" id="1_o543x"] +[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") +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") diff --git a/Scripts/Entities/Enemy.cs b/Scripts/Entities/Enemy.cs index 77c729a..eb93611 100644 --- a/Scripts/Entities/Enemy.cs +++ b/Scripts/Entities/Enemy.cs @@ -5,266 +5,9 @@ using AceFieldNewHorizon.Scripts.System; namespace AceFieldNewHorizon.Scripts.Entities; -public partial class Enemy : CharacterBody2D +public partial class Enemy : BaseEnemy { - 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 = 1.0f; - [Export] public int MaxHealth = 100; - [Export] public bool ShowDamageNumbers = true; - - private BaseTile _targetTile; - private ReactorTile _reactor; - private float _attackTimer = 0; - private Area2D _detectionArea; - private Area2D _attackArea; - private int _currentHealth; - private ProgressBar _healthBar; - private GridManager _gridManager; - - // Track collisions with potential targets - private readonly HashSet _collidingTiles = []; - - public int CurrentHealth - { - get => _currentHealth; - private set - { - _currentHealth = Mathf.Clamp(value, 0, MaxHealth); - UpdateHealthBar(); - - if (_currentHealth <= 0) - { - Die(); - } - } - } - - public bool IsDead => _currentHealth <= 0; - - public override void _Ready() - { - _currentHealth = MaxHealth; - _gridManager = DependencyInjection.Container.GetInstance(); - - // Find the reactor in the scene - _reactor = GetTree().GetFirstNodeInGroup(ReactorTile.ReactorGroupName) as ReactorTile; - - // Create health bar - _healthBar = new ProgressBar - { - MaxValue = MaxHealth, - Value = _currentHealth, - Size = new Vector2(40, 4), - ShowPercentage = false, - Visible = false - }; - - var healthBarContainer = new Control(); - healthBarContainer.AddChild(_healthBar); - AddChild(healthBarContainer); - healthBarContainer.Position = new Vector2(-20, -20); - - // Create detection area for finding targets - _detectionArea = new Area2D(); - var collisionShape = new CollisionShape2D(); - var shape = new CircleShape2D(); - shape.Radius = DetectionRadius; - collisionShape.Shape = shape; - _detectionArea.AddChild(collisionShape); - AddChild(_detectionArea); - - _attackArea = GetNodeOrNull("AttackArea"); - - // Connect signals - _detectionArea.BodyEntered += OnBodyEnteredDetection; - _detectionArea.BodyExited += OnBodyExitedDetection; - if (_attackArea != null) - { - _attackArea.BodyEntered += OnBodyEntered; - _attackArea.BodyExited += OnBodyExited; - } - - AddToGroup(EnemyGroupName); - } - - public override void _Process(double delta) - { - if (IsDead) return; - - // Find the best target if we don't have one - if (_targetTile == null || _targetTile.IsDestroyed || !_targetTile.IsInsideTree()) - { - UpdateTarget(); - } - - // Move towards current target - if (_targetTile != null) - { - var direction = GlobalPosition.DirectionTo(_targetTile.GlobalPosition); - Velocity = direction * MoveSpeed; - LookAt(_targetTile.GlobalPosition); - MoveAndSlide(); - } - - // Attack any colliding tiles - if (_attackTimer > 0) - { - _attackTimer -= (float)delta; - } - else if (_collidingTiles.Count > 0) - { - // Attack the first colliding tile - foreach (var tile in _collidingTiles) - { - if (tile != null && !tile.IsDestroyed && tile.IsInsideTree()) - { - TryAttackTile(tile); - break; - } - } - } - } - - private 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; - } - } - - private 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; - } - } - } - - private 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); - } - } - - private void OnBodyExited(Node2D body) - { - if (body.GetParent() is BaseTile tile) - { - _collidingTiles.Remove(tile); - if (_targetTile == tile) - { - _targetTile = null; - } - } - } - - private void OnBodyEnteredDetection(Node2D body) - { - // Keep this for initial target detection if needed - } - - private void OnBodyExitedDetection(Node2D body) - { - // Keep this for target cleanup if needed - } - - public 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); - } - - private void UpdateHealthBar() - { - if (_healthBar != null) - { - _healthBar.Value = _currentHealth; - _healthBar.Visible = _currentHealth < MaxHealth; // Only show when damaged - } - } - - private 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); - } + // 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 } diff --git a/Scripts/Entities/EnemyBase.cs b/Scripts/Entities/EnemyBase.cs new file mode 100644 index 0000000..1f37813 --- /dev/null +++ b/Scripts/Entities/EnemyBase.cs @@ -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 = 1.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 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(); + 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("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); + } +} diff --git a/Scripts/Entities/EnemyBase.cs.uid b/Scripts/Entities/EnemyBase.cs.uid new file mode 100644 index 0000000..39983f9 --- /dev/null +++ b/Scripts/Entities/EnemyBase.cs.uid @@ -0,0 +1 @@ +uid://6oduws4kbdlf diff --git a/Scripts/System/GridManager.cs b/Scripts/System/GridManager.cs index d469623..7f8d359 100644 --- a/Scripts/System/GridManager.cs +++ b/Scripts/System/GridManager.cs @@ -43,11 +43,29 @@ public partial class GridManager : Node 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); - foreach (var cell in occupiedCells) + + // Create a list to store cells that should be removed + var cellsToRemove = new List(); + + // 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)) - _layers[layer].Remove(cell); + var (building, buildingSize, buildingRotation) = _layers[layer][cell]; + var buildingCells = GridUtils.GetOccupiedCells(cell, buildingSize, buildingRotation); + + // If any of the building's cells match our target area, mark all of its cells for removal + if (buildingCells.Any(c => occupiedCells.Contains(c))) + { + cellsToRemove.AddRange(buildingCells); + } + } + + // Remove all marked cells + foreach (var cell in cellsToRemove.Distinct()) + { + _layers[layer].Remove(cell); } } diff --git a/Scripts/System/NaturalResourceGenerator.cs b/Scripts/System/NaturalResourceGenerator.cs index 598a662..2f1cd8e 100644 --- a/Scripts/System/NaturalResourceGenerator.cs +++ b/Scripts/System/NaturalResourceGenerator.cs @@ -55,15 +55,15 @@ public partial class NaturalResourceGenerator : Node2D return; } - // Test if enemy_nest is in the registry - var testBuilding = Registry.GetBuilding("enemy_nest"); + // Test if enemy_portal is in the registry + var testBuilding = Registry.GetBuilding("enemy_portal"); if (testBuilding == null) { - GD.PrintErr($"{LogPrefix} 'enemy_nest' is not found in BuildingRegistry!"); + GD.PrintErr($"{LogPrefix} 'enemy_portal' is not found in BuildingRegistry!"); } else { - GD.Print($"{LogPrefix} Found enemy_nest in registry!"); + GD.Print($"{LogPrefix} Found enemy_portal in registry!"); } GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}"); @@ -287,9 +287,9 @@ public partial class NaturalResourceGenerator : Node2D // Remove all tiles in this chunk 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); // Free a 1x1 area for each cell @@ -416,7 +416,7 @@ public partial class NaturalResourceGenerator : Node2D while (attempts < maxAttempts) { - if (PlaceTile("enemy_nest", nestPosition)) + if (PlaceTile("enemy_portal", nestPosition)) { GD.Print($"{LogPrefix} Placed enemy nest at {nestPosition}"); return; diff --git a/Scripts/System/PlacementManager.cs b/Scripts/System/PlacementManager.cs index 4e2398d..225354c 100644 --- a/Scripts/System/PlacementManager.cs +++ b/Scripts/System/PlacementManager.cs @@ -428,32 +428,28 @@ public static class GridManagerExtensions public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid, Vector2I cell, GridLayer layer) { - if (grid.GetTileAtCell(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 (int x = 0; x < 100; x++) // Arbitrary max size + for (var y = 0; y < 100; y++) { - for (int y = 0; y < 100; y++) - { - var checkCell = new Vector2I(cell.X - x, cell.Y - y); - if (grid.GetTileAtCell(checkCell, layer) == building) - { - // Found the top-left corner, now find the size - var size = Vector2I.One; - // Search right - while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == - building) - size.X++; - // Search down - while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) == - building) - size.Y++; + var checkCell = new Vector2I(cell.X - x, cell.Y - y); + if (grid.GetTileAtCell(checkCell, layer) != building) continue; + // Found the top-left corner, now find the size + var size = Vector2I.One; + // Search right + while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == + building) + size.X++; + // Search down + while (grid.GetTileAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) == + building) + size.Y++; - // Get rotation from the first cell - var rotation = 0f; // You'll need to store rotation in GridManager to make this work - return (checkCell, size, rotation); - } - } + // Get rotation from the first cell + var rotation = 0f; // You'll need to store rotation in Grid to make this work + return (checkCell, size, rotation); } } diff --git a/Scripts/Tiles/BaseTile.cs b/Scripts/Tiles/BaseTile.cs index 768206f..3d0ed0b 100644 --- a/Scripts/Tiles/BaseTile.cs +++ b/Scripts/Tiles/BaseTile.cs @@ -69,7 +69,7 @@ public partial class BaseTile : Node2D 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); @@ -136,6 +136,9 @@ public partial class BaseTile : Node2D 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 diff --git a/Scripts/Tiles/EnemyNestTile.cs b/Scripts/Tiles/EnemyNestTile.cs deleted file mode 100644 index 2b64972..0000000 --- a/Scripts/Tiles/EnemyNestTile.cs +++ /dev/null @@ -1,77 +0,0 @@ -using AceFieldNewHorizon.Scripts.Entities; -using Godot; - -namespace AceFieldNewHorizon.Scripts.Tiles; - -public partial class EnemyNestTile : 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 = 0; - - 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; - } - - 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(); - 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; - } -} \ No newline at end of file diff --git a/Scripts/Tiles/EnemyPortalTile.cs b/Scripts/Tiles/EnemyPortalTile.cs new file mode 100644 index 0000000..293d476 --- /dev/null +++ b/Scripts/Tiles/EnemyPortalTile.cs @@ -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 = 0; + + 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"); + + // 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, 12), // 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(); + 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; + } +} diff --git a/Scripts/Tiles/EnemyNestTile.cs.uid b/Scripts/Tiles/EnemyPortalTile.cs.uid similarity index 100% rename from Scripts/Tiles/EnemyNestTile.cs.uid rename to Scripts/Tiles/EnemyPortalTile.cs.uid diff --git a/Scripts/Tiles/GroundTile.cs b/Scripts/Tiles/GroundTile.cs index b27233f..a09ee25 100644 --- a/Scripts/Tiles/GroundTile.cs +++ b/Scripts/Tiles/GroundTile.cs @@ -8,6 +8,7 @@ public partial class GroundTile : BaseTile { var sprite = GetNode("Sprite2D"); sprite.Modulate = new Color(0.75f, 0.75f, 0.75f); // Makes the sprite 25% darker + sprite.ZIndex = -10; base._Ready(); }