✨ Reactor, and enemies attacking the tiles
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
[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<BaseTile> _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<GridManager>();
|
||||
|
||||
// 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<Area2D>("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);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using AceFieldNewHorizon.Scripts.System;
|
||||
using Godot;
|
||||
|
||||
|
Reference in New Issue
Block a user