diff --git a/Data/Buildings.json b/Data/Buildings.json index 4dca6bc..cc3cfc7 100644 --- a/Data/Buildings.json +++ b/Data/Buildings.json @@ -33,6 +33,15 @@ "layer": 1, "size": [1, 1] }, + "reactor": { + "scene": "res://Scenes/Tiles/ReactorTile.tscn", + "cost": {}, + "durability": 200, + "buildTime": 5.0, + "allowedRotations": [0], + "layer": 1, + "size": [3, 3] + }, "enemy_nest": { "scene": "res://Scenes/Tiles/EnemyNest.tscn", "cost": {}, diff --git a/Scenes/Entities/Enemy.tscn b/Scenes/Entities/Enemy.tscn index 9e8e0df..995af85 100644 --- a/Scenes/Entities/Enemy.tscn +++ b/Scenes/Entities/Enemy.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=4 format=3 uid="uid://b3ffcucytwmk"] +[gd_scene load_steps=5 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"] @@ -6,6 +6,9 @@ [sub_resource type="RectangleShape2D" id="RectangleShape2D_jajit"] size = Vector2(60, 74) +[sub_resource type="RectangleShape2D" id="RectangleShape2D_wmvnc"] +size = Vector2(70, 84) + [node name="Enemy" type="CharacterBody2D"] collision_layer = 2 collision_mask = 3 @@ -17,3 +20,8 @@ shape = SubResource("RectangleShape2D_jajit") [node name="Sprite2D" type="Sprite2D" parent="."] scale = Vector2(0.1, 0.1) texture = ExtResource("1_8q37v") + +[node name="AttackArea" type="Area2D" parent="."] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="AttackArea"] +shape = SubResource("RectangleShape2D_wmvnc") diff --git a/Scenes/Root.tscn b/Scenes/Root.tscn index 4bad00f..390de8e 100644 --- a/Scenes/Root.tscn +++ b/Scenes/Root.tscn @@ -14,9 +14,8 @@ script = ExtResource("2_oss8w") [node name="PlacementSystem" type="Node2D" parent="."] script = ExtResource("2_sxhdm") -[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) -Inventory = NodePath("") [node name="HUD" parent="." instance=ExtResource("8_hud_scene")] diff --git a/Scenes/Tiles/ReactorTile.png b/Scenes/Tiles/ReactorTile.png new file mode 100644 index 0000000..bd63d1e Binary files /dev/null and b/Scenes/Tiles/ReactorTile.png differ diff --git a/Scenes/Tiles/ReactorTile.png.import b/Scenes/Tiles/ReactorTile.png.import new file mode 100644 index 0000000..5fdd134 --- /dev/null +++ b/Scenes/Tiles/ReactorTile.png.import @@ -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 diff --git a/Scenes/Tiles/ReactorTile.tscn b/Scenes/Tiles/ReactorTile.tscn new file mode 100644 index 0000000..391be55 --- /dev/null +++ b/Scenes/Tiles/ReactorTile.tscn @@ -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) diff --git a/Scripts/Entities/Enemy.cs b/Scripts/Entities/Enemy.cs index 29a0ea3..77c729a 100644 --- a/Scripts/Entities/Enemy.cs +++ b/Scripts/Entities/Enemy.cs @@ -1,193 +1,270 @@ +using System.Collections.Generic; +using AceFieldNewHorizon.Scripts.Tiles; using Godot; +using AceFieldNewHorizon.Scripts.System; namespace AceFieldNewHorizon.Scripts.Entities; public partial class Enemy : CharacterBody2D { - public const string EnemyGroupName = "Enemy"; - - [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; - [Export] public int MaxHealth = 100; - [Export] public bool ShowDamageNumbers = true; - - private Player _player; - private float _attackTimer = 0; - private bool _isPlayerInRange = false; - private Area2D _detectionArea; - private int _currentHealth; - private ProgressBar _healthBar; - - public int CurrentHealth - { - get => _currentHealth; - private set - { - _currentHealth = Mathf.Clamp(value, 0, MaxHealth); - UpdateHealthBar(); - - if (_currentHealth <= 0) - { - Die(); - } - } - } - - public bool IsDead => _currentHealth <= 0; + public const string EnemyGroupName = "Enemy"; - public override void _Ready() - { - _currentHealth = MaxHealth; - - // 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); // Adjust position as needed - - // 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; - - AddToGroup(EnemyGroupName); - } - - public override void _Process(double delta) - { - if (IsDead) return; - - 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(); - } - } - - 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); - } - - private void TryAttackPlayer(double delta) - { - if (IsDead) return; - - _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 + [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); + } +} diff --git a/Scripts/Entities/Player.cs b/Scripts/Entities/Player.cs index 0b8b70c..e01ad1a 100644 --- a/Scripts/Entities/Player.cs +++ b/Scripts/Entities/Player.cs @@ -1,4 +1,3 @@ -using System; using AceFieldNewHorizon.Scripts.System; using Godot; diff --git a/Scripts/System/Hud.cs b/Scripts/System/Hud.cs index 932e8ca..5cd9d5d 100644 --- a/Scripts/System/Hud.cs +++ b/Scripts/System/Hud.cs @@ -8,11 +8,20 @@ public partial class Hud : CanvasLayer private ResourceManager _resourceManager; private VBoxContainer _resourceDisplay; private readonly Dictionary _resourceLabels = new(); + private VBoxContainer _pickupLogContainer; public override void _Ready() { _resourceDisplay = GetNode("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(); if (_resourceManager == null) { @@ -21,6 +30,7 @@ public partial class Hud : CanvasLayer } _resourceManager.OnResourceChanged += UpdateResourceDisplay; + _resourceManager.OnResourcePickedUp += OnResourcePickedUp; // Initialize display with current resources foreach (var entry in _resourceManager.GetAllResources()) @@ -54,11 +64,39 @@ public partial class Hud : CanvasLayer } } + 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; } } } diff --git a/Scripts/System/NaturalResourceGenerator.cs b/Scripts/System/NaturalResourceGenerator.cs index 34294c7..598a662 100644 --- a/Scripts/System/NaturalResourceGenerator.cs +++ b/Scripts/System/NaturalResourceGenerator.cs @@ -67,6 +67,9 @@ public partial class NaturalResourceGenerator : Node2D } GD.Print($"{LogPrefix} NaturalResourceGenerator ready, SpawnEnemyNest = {SpawnEnemyNest}"); + + GD.Print($"{LogPrefix} Spawning the core reactor..."); + SpawnCoreReactor(); if (SpawnEnemyNest) { @@ -393,6 +396,12 @@ public partial class NaturalResourceGenerator : Node2D 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 @@ -402,7 +411,7 @@ public partial class NaturalResourceGenerator : Node2D var nestPosition = new Vector2I((int)offset.X, (int)offset.Y); // Try to find a valid position for the nest - int attempts = 0; + var attempts = 0; const int maxAttempts = 10; while (attempts < maxAttempts) @@ -428,25 +437,18 @@ public partial class NaturalResourceGenerator : Node2D { try { - // 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) { GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}"); return false; } - // GD.Print($"{LogPrefix} Found building in registry: {tileType}"); + + // Free area for ground layer if needed + if (building.Layer == GridLayer.Ground) + Grid.FreeArea(cell, building.Size, 0f, GridLayer.Ground); + else + GD.Print($"{LogPrefix} Attempting placing building tile {tileType} at {cell}."); var scene = building.Scene; if (scene == null) @@ -454,29 +456,24 @@ 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 + // Calculate position with proper offset based on building size var rotatedSize = building.GetRotatedSize(0f); var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); instance.ZIndex = (int)building.Layer; instance.GlobalPosition = GridUtils.GridToWorld(cell) + offset; - instance.Grid = Grid; AddChild(instance); - // For enemy nest, use Building layer - var layer = tileType == "enemy_nest" ? GridLayer.Building : building.Layer; - Grid.OccupyArea(cell, instance, building.Size, 0f, layer); + // Occupy the appropriate area based on building size + Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); - // GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} on layer {layer}"); return true; } catch (Exception e) diff --git a/Scripts/System/PlacementManager.cs b/Scripts/System/PlacementManager.cs index 2e20f5b..4e2398d 100644 --- a/Scripts/System/PlacementManager.cs +++ b/Scripts/System/PlacementManager.cs @@ -203,7 +203,6 @@ public partial class PlacementManager : Node2D var scene = building.Scene; _ghostBuilding = (BaseTile)scene.Instantiate(); - _ghostBuilding.Grid = Grid; _ghostBuilding.SetGhostMode(true); _ghostBuilding.RotationDegrees = _currentRotation; _ghostBuilding.ZAsRelative = false; @@ -251,7 +250,6 @@ public partial class PlacementManager : Node2D // Create the building instance var scene = building.Scene; var buildingInstance = (BaseTile)scene.Instantiate(); - buildingInstance.Grid = Grid; buildingInstance.RotationDegrees = _currentRotation; buildingInstance.ZIndex = (int)building.Layer; buildingInstance.Position = _ghostBuilding.Position; diff --git a/Scripts/System/ResourceManager.cs b/Scripts/System/ResourceManager.cs index 9918bee..f3127d7 100644 --- a/Scripts/System/ResourceManager.cs +++ b/Scripts/System/ResourceManager.cs @@ -13,6 +13,10 @@ public partial class ResourceManager : Node // Event for when resource amounts change [Signal] public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount); + + // Event for when resources are picked up (added) + [Signal] + public delegate void OnResourcePickedUpEventHandler(string resourceId, int amount); public override void _Ready() { @@ -43,6 +47,7 @@ public partial class ResourceManager : Node } EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]); + EmitSignal(nameof(OnResourcePickedUp), resourceId, amount); } // Remove resources of a specific type diff --git a/Scripts/Tiles/BaseTile.cs b/Scripts/Tiles/BaseTile.cs index 2059957..768206f 100644 --- a/Scripts/Tiles/BaseTile.cs +++ b/Scripts/Tiles/BaseTile.cs @@ -9,30 +9,46 @@ namespace AceFieldNewHorizon.Scripts.Tiles; public partial class BaseTile : Node2D { [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 Sprite2D _sprite; private ColorRect _progressOverlay; private Action _onConstructionComplete; - + private Tween _damageTween; + public bool IsConstructing; public bool IsConstructed; public override void _Ready() { + Grid = DependencyInjection.Container.GetInstance(); + Registry = DependencyInjection.Container.GetInstance(); + _collisionShape = GetNodeOrNull("CollisionShape2D"); _sprite = GetNodeOrNull("Sprite2D"); _progressOverlay = GetNodeOrNull("ProgressOverlay"); if (_progressOverlay != null) _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 virtual void SetGhostMode(bool canPlace) { // Don't modify collision for constructing buildings if (IsConstructing) return; - + if (_collisionShape != null) _collisionShape.Disabled = true; @@ -50,6 +66,78 @@ public partial class BaseTile : Node2D _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(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(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 + } + // Building progress visualization public void StartConstruction(float buildTime, Action onComplete = null) { @@ -57,7 +145,7 @@ public partial class BaseTile : Node2D if (_collisionShape != null) _collisionShape.Disabled = true; - if (_progressOverlay == null || _sprite?.Texture == null) + if (_progressOverlay == null || _sprite?.Texture == null) { IsConstructing = false; onComplete?.Invoke(); @@ -95,15 +183,15 @@ public partial class BaseTile : Node2D // Fade out the overlay await FadeOutOverlay(0.5f); - + // Construction complete - restore full opacity and enable collision if (_sprite != null) _sprite.Modulate = Colors.White; - + IsConstructing = false; if (_collisionShape != null) _collisionShape.Disabled = false; - + _onConstructionComplete?.Invoke(); IsConstructed = true; } diff --git a/Scripts/Tiles/EnemyNestTile.cs b/Scripts/Tiles/EnemyNestTile.cs index e8a935b..2b64972 100644 --- a/Scripts/Tiles/EnemyNestTile.cs +++ b/Scripts/Tiles/EnemyNestTile.cs @@ -18,6 +18,9 @@ public partial class EnemyNestTile : BaseTile { base._Ready(); + // Add to Hostile group to prevent enemies from attacking their own nest + AddToGroup("Hostile"); + // Create and configure the timer _spawnTimer = new Timer { diff --git a/Scripts/Tiles/ReactorTile.cs b/Scripts/Tiles/ReactorTile.cs new file mode 100644 index 0000000..b2ea44e --- /dev/null +++ b/Scripts/Tiles/ReactorTile.cs @@ -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); + } +} \ No newline at end of file diff --git a/Scripts/Tiles/ReactorTile.cs.uid b/Scripts/Tiles/ReactorTile.cs.uid new file mode 100644 index 0000000..e1413bb --- /dev/null +++ b/Scripts/Tiles/ReactorTile.cs.uid @@ -0,0 +1 @@ +uid://c4k3ottt7j3b1 diff --git a/Scripts/Tiles/TurretTile.cs b/Scripts/Tiles/TurretTile.cs index dbf1ecd..01fa601 100644 --- a/Scripts/Tiles/TurretTile.cs +++ b/Scripts/Tiles/TurretTile.cs @@ -121,7 +121,7 @@ public partial class TurretTile : BaseTile // Reset attack cooldown _attackTimer = AttackCooldown; - GD.Print("Turret attacking enemy!"); + GD.Print("[Turret] Turret firing!"); } } } \ No newline at end of file