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