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 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); } }