Wave enemies (#9)
* Add enemy wave management and path following components * cleanup
This commit is contained in:
@@ -86,4 +86,19 @@ public partial class ChaseLevelComponent : Node
|
||||
EmitSignalChaseStopped();
|
||||
_isChasing = false;
|
||||
}
|
||||
|
||||
public void SetChasing(bool shouldChase)
|
||||
{
|
||||
if (shouldChase && !_isChasing)
|
||||
{
|
||||
_previousCameraFollowTarget = _phantomCamera.FollowTarget;
|
||||
_phantomCamera.FollowTarget = _root;
|
||||
EmitSignalChaseStarted();
|
||||
_isChasing = true;
|
||||
}
|
||||
else if (!shouldChase && _isChasing)
|
||||
{
|
||||
StopChasing();
|
||||
}
|
||||
}
|
||||
}
|
86
scripts/components/EnemyWaveManager.cs
Normal file
86
scripts/components/EnemyWaveManager.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class EnemyWaveManager : Node
|
||||
{
|
||||
[Export] public Path2D EnemyPath { get; set; }
|
||||
[Export] public Array<PackedScene> EnemyScenes { get; set; } = [];
|
||||
[Export] public float SpawnInterval { get; set; } = 1.0f; // Time between each enemy spawn
|
||||
|
||||
private int _enemiesToSpawn;
|
||||
private int _activeEnemies;
|
||||
private Timer _spawnTimer;
|
||||
|
||||
[Signal]
|
||||
public delegate void WaveCompletedEventHandler();
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_spawnTimer = new Timer();
|
||||
_spawnTimer.WaitTime = SpawnInterval;
|
||||
_spawnTimer.OneShot = false;
|
||||
_spawnTimer.Timeout += SpawnNextEnemy;
|
||||
AddChild(_spawnTimer);
|
||||
}
|
||||
|
||||
public void StartWave()
|
||||
{
|
||||
if (EnemyScenes.Count == 0 || EnemyPath == null)
|
||||
{
|
||||
GD.PrintErr("EnemyWaveManager: Enemy scenes or path not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
_enemiesToSpawn = EnemyScenes.Count;
|
||||
_activeEnemies = 0;
|
||||
_spawnTimer.Start();
|
||||
}
|
||||
|
||||
private void SpawnNextEnemy()
|
||||
{
|
||||
if (_enemiesToSpawn <= 0)
|
||||
{
|
||||
_spawnTimer.Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
var enemyIndex = EnemyScenes.Count - _enemiesToSpawn;
|
||||
var enemyScene = EnemyScenes[enemyIndex];
|
||||
|
||||
var pathFollowNode = new PathFollow2D();
|
||||
EnemyPath.AddChild(pathFollowNode);
|
||||
|
||||
var enemyInstance = enemyScene.Instantiate<Node2D>();
|
||||
pathFollowNode.AddChild(enemyInstance);
|
||||
|
||||
var pathFollowerComponent = enemyInstance.GetNodeOrNull<PathFollowerComponent>("PathFollowerComponent");
|
||||
if (pathFollowerComponent == null)
|
||||
{
|
||||
GD.PrintErr($"Enemy scene '{enemyScene.ResourcePath}' is missing a PathFollowerComponent.");
|
||||
pathFollowNode.QueueFree();
|
||||
_enemiesToSpawn--;
|
||||
return;
|
||||
}
|
||||
|
||||
pathFollowNode.Rotates = pathFollowerComponent.ShouldRotate;
|
||||
pathFollowerComponent.Initialize(pathFollowNode);
|
||||
pathFollowerComponent.StartFollowing();
|
||||
pathFollowerComponent.EnemyDestroyed += OnEnemyDestroyed;
|
||||
|
||||
_enemiesToSpawn--;
|
||||
_activeEnemies++;
|
||||
}
|
||||
|
||||
private void OnEnemyDestroyed()
|
||||
{
|
||||
_activeEnemies--;
|
||||
|
||||
if (_enemiesToSpawn == 0 && _activeEnemies == 0)
|
||||
{
|
||||
EmitSignalWaveCompleted();
|
||||
}
|
||||
}
|
||||
}
|
1
scripts/components/EnemyWaveManager.cs.uid
Normal file
1
scripts/components/EnemyWaveManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dogp7ehihnw3s
|
@@ -1,51 +0,0 @@
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
public partial class EnemyWaveTriggerComponent : Node
|
||||
{
|
||||
[Export] public Area2D Area2D { get; set; }
|
||||
[Export] public PathFollow2D PathFollowNode { get; set; }
|
||||
[Export] public float Speed { get; set; } = 100f;
|
||||
[Export] public bool Loop { get; set; } = false;
|
||||
[Export] public bool ActivateOnEnter { get; set; } = true;
|
||||
|
||||
private bool _isActive = false;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Area2D.BodyEntered += OnBodyEntered;
|
||||
|
||||
if (PathFollowNode == null) return;
|
||||
|
||||
PathFollowNode.SetProgress(0f);
|
||||
PathFollowNode.SetProcess(false);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!_isActive || PathFollowNode == null) return;
|
||||
|
||||
var progress = PathFollowNode.Progress;
|
||||
progress += (float)(delta * Speed);
|
||||
PathFollowNode.SetProgress(progress);
|
||||
|
||||
if (!(PathFollowNode.ProgressRatio >= 1f) || Loop) return;
|
||||
|
||||
_isActive = false;
|
||||
PathFollowNode.SetProcess(false);
|
||||
}
|
||||
|
||||
private void OnBodyEntered(Node2D body)
|
||||
{
|
||||
if (ActivateOnEnter) StartWave();
|
||||
}
|
||||
|
||||
private void StartWave()
|
||||
{
|
||||
if (PathFollowNode == null) return;
|
||||
|
||||
PathFollowNode.SetProcess(true);
|
||||
_isActive = true;
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
uid://d3fpwddc2j41x
|
67
scripts/components/PathFollowerComponent.cs
Normal file
67
scripts/components/PathFollowerComponent.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class PathFollowerComponent : Node2D
|
||||
{
|
||||
[Export] public float Speed { get; set; } = 200f;
|
||||
[Export] public bool ShouldRotate { get; set; } = true;
|
||||
|
||||
private PathFollow2D _pathFollowNode;
|
||||
private HealthComponent _healthComponent;
|
||||
private bool _isActive = false;
|
||||
|
||||
[Signal]
|
||||
public delegate void PathCompletedEventHandler();
|
||||
[Signal]
|
||||
public delegate void EnemyDestroyedEventHandler();
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_healthComponent = GetOwner().GetNodeOrNull<HealthComponent>("HealthComponent");
|
||||
if (_healthComponent != null)
|
||||
{
|
||||
_healthComponent.Death += OnDeath;
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!_isActive || _pathFollowNode == null) return;
|
||||
|
||||
_pathFollowNode.Progress += Speed * (float)delta;
|
||||
|
||||
if (_pathFollowNode.ProgressRatio >= 1.0f)
|
||||
{
|
||||
_isActive = false;
|
||||
EmitSignalPathCompleted();
|
||||
_pathFollowNode.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize(PathFollow2D pathFollowNode)
|
||||
{
|
||||
_pathFollowNode = pathFollowNode;
|
||||
if (ShouldRotate)
|
||||
{
|
||||
_pathFollowNode.Rotates = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pathFollowNode.Rotates = false;
|
||||
}
|
||||
|
||||
_pathFollowNode.Loop = false;
|
||||
}
|
||||
|
||||
public void StartFollowing()
|
||||
{
|
||||
_isActive = true;
|
||||
}
|
||||
|
||||
private void OnDeath()
|
||||
{
|
||||
EmitSignalEnemyDestroyed();
|
||||
}
|
||||
}
|
1
scripts/components/PathFollowerComponent.cs.uid
Normal file
1
scripts/components/PathFollowerComponent.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4hvq2i66fjhi
|
@@ -1,172 +0,0 @@
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts.interfaces;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
public partial class PlatformMovementComponent : Node2D, IMovement
|
||||
{
|
||||
[Export]
|
||||
public float Speed { get; set; } = 300.0f;
|
||||
|
||||
[Export]
|
||||
public float JumpHeight { get; set; } = 100f;
|
||||
|
||||
[Export]
|
||||
public float JumpTimeToPeak { get; set; } = 0.5f;
|
||||
|
||||
[Export]
|
||||
public float JumpTimeToDescent { get; set; } = 0.4f;
|
||||
|
||||
[Export]
|
||||
public int CoyoteFrames { get; set; } = 6;
|
||||
|
||||
[Export]
|
||||
public AudioStreamPlayer2D JumpSfx { get; set; }
|
||||
|
||||
[Export]
|
||||
public Node2D RotationTarget { get; set; }
|
||||
|
||||
[Export]
|
||||
public CharacterBody2D Body { get; set; }
|
||||
|
||||
private float _gravity;
|
||||
private bool _wasLastFloor = false;
|
||||
private bool _coyoteMode = false;
|
||||
private Timer _coyoteTimer;
|
||||
private Vector2 _lastDirection = new Vector2(1, 0);
|
||||
|
||||
private float _jumpVelocity;
|
||||
private float _jumpGravity;
|
||||
private float _fallGravity;
|
||||
|
||||
public Vector2 LastDirection => _lastDirection;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
base._Ready();
|
||||
|
||||
if (Body == null)
|
||||
return;
|
||||
|
||||
_gravity = (float)ProjectSettings.GetSetting("physics/2d/default_gravity");
|
||||
_jumpVelocity = ((2.0f * JumpHeight) / JumpTimeToPeak) * -1.0f;
|
||||
_jumpGravity = ((-2.0f * JumpHeight) / (JumpTimeToPeak * JumpTimeToPeak)) * -1.0f;
|
||||
_fallGravity = ((-2.0f * JumpHeight) / (JumpTimeToDescent * JumpTimeToDescent)) * -1.0f;
|
||||
|
||||
_coyoteTimer = new Timer
|
||||
{
|
||||
OneShot = true,
|
||||
WaitTime = CoyoteFrames / 60.0f
|
||||
};
|
||||
_coyoteTimer.Timeout += OnCoyoteTimerTimeout;
|
||||
AddChild(_coyoteTimer);
|
||||
}
|
||||
|
||||
public string MovementType { get; } = "platform";
|
||||
public bool Enabled { get; set; }
|
||||
public Vector2 PreviousVelocity { get; set; }
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
base._Process(delta);
|
||||
|
||||
if (Body == null || !Enabled)
|
||||
return;
|
||||
|
||||
if (Body.Velocity.X > 0.0f)
|
||||
RotationTarget.Rotation = Mathf.DegToRad(-10);
|
||||
else if (Body.Velocity.X < 0.0f)
|
||||
RotationTarget.Rotation = Mathf.DegToRad(10);
|
||||
else
|
||||
RotationTarget.Rotation = 0;
|
||||
|
||||
CalculateJumpVars();
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
base._PhysicsProcess(delta);
|
||||
|
||||
if (Body == null || !Enabled)
|
||||
return;
|
||||
|
||||
if (Body.IsOnFloor())
|
||||
{
|
||||
_wasLastFloor = true;
|
||||
_coyoteMode = false; // Reset coyote mode when back on the floor
|
||||
_coyoteTimer.Stop(); // Stop timer when grounded
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_wasLastFloor) // Start coyote timer only once
|
||||
{
|
||||
_coyoteMode = true;
|
||||
_coyoteTimer.Start();
|
||||
}
|
||||
_wasLastFloor = false;
|
||||
}
|
||||
|
||||
if (!Body.IsOnFloor())
|
||||
Body.Velocity += new Vector2(0, CalculateGravity()) * (float)delta;
|
||||
|
||||
if (Input.IsActionPressed("jump") && (Body.IsOnFloor() || _coyoteMode))
|
||||
Jump();
|
||||
|
||||
if (Input.IsActionJustPressed("down"))
|
||||
Body.Position += new Vector2(0, 1);
|
||||
|
||||
float direction = Input.GetAxis("left", "right");
|
||||
if (direction != 0)
|
||||
_lastDirection = HandleDirection(direction);
|
||||
|
||||
if (direction != 0)
|
||||
Body.Velocity = new Vector2(direction * Speed, Body.Velocity.Y);
|
||||
else
|
||||
Body.Velocity = new Vector2(Mathf.MoveToward(Body.Velocity.X, 0, Speed), Body.Velocity.Y);
|
||||
|
||||
PreviousVelocity = Body.Velocity;
|
||||
Body.MoveAndSlide();
|
||||
}
|
||||
|
||||
private void Jump()
|
||||
{
|
||||
if (Body == null)
|
||||
return;
|
||||
|
||||
Body.Velocity = new Vector2(Body.Velocity.X, _jumpVelocity);
|
||||
_coyoteMode = false;
|
||||
if (JumpSfx != null)
|
||||
JumpSfx.Play();
|
||||
}
|
||||
|
||||
private float CalculateGravity()
|
||||
{
|
||||
return Body.Velocity.Y < 0.0f ? _jumpGravity : _fallGravity;
|
||||
}
|
||||
|
||||
private void OnCoyoteTimerTimeout()
|
||||
{
|
||||
_coyoteMode = false;
|
||||
}
|
||||
|
||||
private Vector2 HandleDirection(float inputDir)
|
||||
{
|
||||
if (inputDir > 0)
|
||||
return new Vector2(1, 0);
|
||||
else if (inputDir < 0)
|
||||
return new Vector2(-1, 0);
|
||||
return _lastDirection;
|
||||
}
|
||||
|
||||
public void OnShipEntered()
|
||||
{
|
||||
RotationTarget.Rotation = 0;
|
||||
}
|
||||
|
||||
private void CalculateJumpVars()
|
||||
{
|
||||
_jumpVelocity = ((2.0f * JumpHeight) / JumpTimeToPeak) * -1.0f;
|
||||
_jumpGravity = ((-2.0f * JumpHeight) / (JumpTimeToPeak * JumpTimeToPeak)) * -1.0f;
|
||||
_fallGravity = ((-2.0f * JumpHeight) / (JumpTimeToDescent * JumpTimeToDescent)) * -1.0f;
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
uid://btlm1f3l70il
|
@@ -1,38 +0,0 @@
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts.interfaces;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
public partial class ShipMovementComponent : Node, IMovement
|
||||
{
|
||||
[Export] public float MaxSpeed { get; set; } = 200f;
|
||||
[Export] public float Acceleration { get; set; } = 100f;
|
||||
[Export] public float Friction { get; set; } = 50f;
|
||||
[Export] public CharacterBody2D Body { get; set; }
|
||||
|
||||
public string MovementType { get; } = "ship";
|
||||
public bool Enabled { get; set; }
|
||||
public Vector2 PreviousVelocity { get; set; }
|
||||
|
||||
private Vector2 _velocity = Vector2.Zero;
|
||||
|
||||
public Vector2 Velocity => _velocity;
|
||||
public Vector2 LastDirection => _velocity.Normalized();
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
if (Body == null || !Enabled) return;
|
||||
|
||||
var inputVector = new Vector2(
|
||||
Input.GetActionStrength("right") - Input.GetActionStrength("left"),
|
||||
Input.GetActionStrength("down") - Input.GetActionStrength("up")
|
||||
).Normalized();
|
||||
|
||||
_velocity = inputVector != Vector2.Zero ? _velocity.MoveToward(inputVector * MaxSpeed, Acceleration * (float)delta) : _velocity.MoveToward(Vector2.Zero, Friction * (float)delta);
|
||||
|
||||
_velocity = _velocity.LimitLength(MaxSpeed);
|
||||
Body.Velocity = _velocity;
|
||||
PreviousVelocity = Body.Velocity;
|
||||
Body.MoveAndSlide();
|
||||
}
|
||||
}
|
@@ -1 +0,0 @@
|
||||
uid://cty54itmnudfm
|
52
scripts/components/WaveTriggerComponent.cs
Normal file
52
scripts/components/WaveTriggerComponent.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
[GlobalClass]
|
||||
public partial class WaveTriggerComponent : Node
|
||||
{
|
||||
[Export] public Area2D TriggerArea { get; set; }
|
||||
[Export] public EnemyWaveManager WaveManager { get; set; }
|
||||
[Export] public ChaseLevelComponent ChaserToPause { get; set; } // Optional
|
||||
[Export] public bool PauseChaser { get; set; } = true;
|
||||
|
||||
private bool _hasBeenTriggered = false;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (TriggerArea == null || WaveManager == null)
|
||||
{
|
||||
GD.PrintErr("WaveTriggerComponent is missing a TriggerArea or WaveManager.");
|
||||
SetProcess(false);
|
||||
return;
|
||||
}
|
||||
|
||||
TriggerArea.BodyEntered += OnPlayerEntered;
|
||||
WaveManager.WaveCompleted += OnWaveCompleted;
|
||||
}
|
||||
|
||||
private void OnPlayerEntered(Node2D body)
|
||||
{
|
||||
if (body is PlayerController && !_hasBeenTriggered)
|
||||
{
|
||||
_hasBeenTriggered = true;
|
||||
|
||||
if (PauseChaser && ChaserToPause != null)
|
||||
{
|
||||
ChaserToPause.SetChasing(false);
|
||||
}
|
||||
|
||||
WaveManager.StartWave();
|
||||
TriggerArea.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWaveCompleted()
|
||||
{
|
||||
GD.Print("Wave completed!");
|
||||
if (PauseChaser && ChaserToPause != null)
|
||||
{
|
||||
ChaserToPause.SetChasing(true);
|
||||
}
|
||||
}
|
||||
}
|
1
scripts/components/WaveTriggerComponent.cs.uid
Normal file
1
scripts/components/WaveTriggerComponent.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctg4awndvgfd8
|
Reference in New Issue
Block a user