Turret shooting and damaing

This commit is contained in:
2025-08-30 13:05:50 +08:00
parent 1c6c03cd41
commit 7438ba407a
14 changed files with 410 additions and 14 deletions

View File

@@ -0,0 +1,97 @@
using Godot;
namespace AceFieldNewHorizon.Scripts.Entities;
public partial class Bullet : Area2D
{
[Export] public float Speed = 400.0f;
[Export] public int Damage = 10;
[Export] public float MaxDistance = 1000.0f;
private Vector2 _direction = Vector2.Right;
private Vector2 _startPosition;
private float _distanceTraveled = 0f;
private bool _hasHit = false;
private uint _ignoreCollisionLayer = 0; // Layer to ignore (will be set by turret)
public void Initialize(Vector2 direction, Vector2 position, float rotation, uint ignoreLayer = 0)
{
_direction = direction.Normalized();
Position = position;
Rotation = rotation;
_startPosition = position;
_ignoreCollisionLayer = ignoreLayer;
// Connect the area entered signal
BodyEntered += OnBodyEntered;
AreaEntered += OnAreaEntered;
}
public override void _PhysicsProcess(double delta)
{
if (_hasHit) return;
// Move the bullet
var movement = _direction * Speed * (float)delta;
Position += movement;
_distanceTraveled += movement.Length();
// Check if bullet has traveled max distance
if (_distanceTraveled >= MaxDistance)
{
QueueFree();
return;
}
}
private void OnBodyEntered(Node2D body)
{
HandleCollision(body);
}
private void OnAreaEntered(Area2D area)
{
HandleCollision(area);
}
private void HandleCollision(Node2D node)
{
if (_hasHit) return;
// Skip collision if it's on the ignore layer
if (node is PhysicsBody2D physicsBody &&
(physicsBody.CollisionLayer & _ignoreCollisionLayer) != 0)
{
return;
}
_hasHit = true;
// If we hit an enemy, deal damage
if (node is Enemy enemy)
{
// Get the global position where the bullet hit
var hitPosition = GlobalPosition;
enemy.TakeDamage(Damage, hitPosition);
}
// Optional: Add hit effect here
// CreateHitEffect();
// Remove the bullet
QueueFree();
}
private void CreateHitEffect()
{
// You can add a hit effect here if desired
// For example, a small explosion or impact sprite
}
public override void _ExitTree()
{
// Clean up signal connections
BodyEntered -= OnBodyEntered;
AreaEntered -= OnAreaEntered;
}
}

View File

@@ -0,0 +1 @@
uid://vgx2a8gm7l8b

View File

@@ -11,14 +11,52 @@ public partial class Enemy : CharacterBody2D
[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 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();
@@ -37,6 +75,8 @@ public partial class Enemy : CharacterBody2D
public override void _Process(double delta)
{
if (IsDead) return;
if (_player != null && _isPlayerInRange)
{
// Face the player
@@ -60,8 +100,68 @@ public partial class Enemy : CharacterBody2D
}
}
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)

View File

@@ -1,3 +1,4 @@
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.Tiles;
@@ -5,31 +6,69 @@ namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class EnemyNestTile : BaseTile
{
[Export] public PackedScene EnemyScene;
[Export] public int MaxEnemies = 5;
[Export] public float SpawnDelay = 5.0f; // Time between spawn attempts
[Export] public float SpawnRadius = 50.0f; // Radius around the nest where enemies can spawn
[Export] public bool Active = true;
private Timer _spawnTimer;
private int _currentEnemyCount = 0;
public override void _Ready()
{
base._Ready();
// Create and configure the timer
_spawnTimer = new Timer();
_spawnTimer = new Timer
{
Autostart = true,
WaitTime = SpawnDelay
};
AddChild(_spawnTimer);
_spawnTimer.Timeout += OnSpawnTimerTimeout;
_spawnTimer.Start(1.0f); // Start with 1 second interval
}
private void OnSpawnTimerTimeout()
{
if (EnemyScene != null)
if (!Active || EnemyScene == null || _currentEnemyCount >= MaxEnemies)
return;
// Check if we can spawn more enemies
var enemies = GetTree().GetNodesInGroup("Enemy");
_currentEnemyCount = enemies.Count;
if (_currentEnemyCount >= MaxEnemies)
return;
// Spawn a new enemy
var enemy = EnemyScene.Instantiate<Enemy>();
if (enemy != null)
{
var enemy = EnemyScene.Instantiate();
GetParent().AddChild(enemy);
// Position the enemy at the nest's position
if (enemy is Node2D enemy2D)
{
enemy2D.GlobalPosition = GlobalPosition;
}
// Calculate a random position within the spawn radius
var randomAngle = GD.Randf() * Mathf.Pi * 2;
var randomOffset = new Vector2(
Mathf.Cos(randomAngle) * SpawnRadius,
Mathf.Sin(randomAngle) * SpawnRadius
);
enemy.GlobalPosition = GlobalPosition + randomOffset;
_currentEnemyCount++;
// Connect to the enemy's death signal if available
enemy.TreeExiting += () => OnEnemyDied();
}
}
private void OnEnemyDied()
{
_currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1);
}
public void SetActive(bool active)
{
Active = active;
_spawnTimer.Paused = !active;
}
}

View File

@@ -1,22 +1,43 @@
using System.Linq;
using AceFieldNewHorizon.Scripts.Entities;
using Godot;
namespace AceFieldNewHorizon.Scripts.Tiles;
public partial class TurretTile : BaseTile
{
private Sprite2D _spriteBarrel;
[Export] public PackedScene BulletScene;
[Export] public float RotationSpeed = 2.0f; // Radians per second
[Export] public float AttackRange = 300.0f;
[Export] public float AttackCooldown = 0.5f;
[Export] public int Damage = 10;
[Export] public float BulletSpeed = 400.0f;
[Export] public NodePath BarrelTipPath;
private Node2D _spriteBarrel;
private Node2D _barrelTip;
private float _attackTimer = 0;
private bool _hasTarget = false;
public override void _Ready()
{
base._Ready();
_spriteBarrel = GetNodeOrNull<Node2D>("Barrel");
_barrelTip = GetNodeOrNull<Node2D>(BarrelTipPath);
_spriteBarrel = GetNodeOrNull<Sprite2D>("Barrel");
if (_barrelTip == null && _spriteBarrel != null)
{
// If no barrel tip is specified, use the end of the barrel sprite
_barrelTip = _spriteBarrel.GetNodeOrNull<Node2D>("BarrelTip");
}
}
public override void SetGhostMode(bool canPlace)
{
base.SetGhostMode(canPlace);
if (_spriteBarrel != null)
_spriteBarrel.Modulate = canPlace
? new Color(0, 1, 0, 0.5f)
@@ -26,8 +47,81 @@ public partial class TurretTile : BaseTile
public override void FinalizePlacement()
{
base.FinalizePlacement();
if (_spriteBarrel != null)
_spriteBarrel.Modulate = Colors.White;
}
public override void _Process(double delta)
{
if (!IsConstructed) return;
// Update attack cooldown
if (_attackTimer > 0)
{
_attackTimer -= (float)delta;
}
// Find the nearest enemy
var enemies = GetTree().GetNodesInGroup(Enemy.EnemyGroupName)
.OfType<Enemy>()
.Where(e => e.GlobalPosition.DistanceTo(GlobalPosition) <= AttackRange)
.OrderBy(e => e.GlobalPosition.DistanceTo(GlobalPosition))
.ToList();
if (enemies.Count > 0)
{
var nearestEnemy = enemies[0];
_hasTarget = true;
// Calculate target angle
var direction = (nearestEnemy.GlobalPosition - _spriteBarrel.GlobalPosition).Normalized();
var targetAngle = Mathf.Atan2(direction.Y, direction.X);
// Smoothly rotate towards target
_spriteBarrel.Rotation = Mathf.LerpAngle(_spriteBarrel.Rotation, targetAngle, (float)delta * RotationSpeed);
// Check if we're facing the target and can attack
if (Mathf.Abs(Mathf.Wrap(targetAngle - _spriteBarrel.Rotation, -Mathf.Pi, Mathf.Pi)) < 0.1f)
TryAttack(nearestEnemy.GlobalPosition);
}
else
{
_hasTarget = false;
}
}
private void TryAttack(Vector2 targetPosition)
{
if (_attackTimer <= 0 && BulletScene != null && _barrelTip != null)
{
// Create bullet instance
var bullet = BulletScene.Instantiate<Bullet>();
// Calculate direction and rotation
var direction = (targetPosition - _barrelTip.GlobalPosition).Normalized();
var bulletRotation = direction.Angle();
// Set bullet position and rotation
GetTree().CurrentScene.AddChild(bullet);
bullet.GlobalPosition = _barrelTip.GlobalPosition;
bullet.Rotation = bulletRotation; // Use the calculated rotation
// Initialize bullet with direction and damage
bullet.Initialize(
direction,
bullet.GlobalPosition,
bulletRotation, // Pass the calculated rotation
1 // Pass the turret's collision layer to ignore
);
bullet.Damage = Damage;
bullet.Speed = BulletSpeed;
bullet.MaxDistance = AttackRange * 1.5f; // Bullets can travel slightly further than attack range
// Reset attack cooldown
_attackTimer = AttackCooldown;
GD.Print("Turret attacking enemy!");
}
}
}