284 lines
7.8 KiB
C#
284 lines
7.8 KiB
C#
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<BaseTile> 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<GridManager>();
|
|
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<Area2D>("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);
|
|
}
|
|
}
|