Wave enemies (#9)

* Add enemy wave management and path following components

* cleanup
This commit is contained in:
2025-08-31 02:08:35 +02:00
committed by GitHub
parent 36d1cac284
commit 604520cad5
15 changed files with 267 additions and 285 deletions

View File

@@ -1,4 +1,4 @@
[gd_scene load_steps=29 format=3 uid="uid://xp4njljog0x2"]
[gd_scene load_steps=30 format=3 uid="uid://xp4njljog0x2"]
[ext_resource type="Texture2D" uid="uid://22k1u37j6k8y" path="res://sprites/flying_enemy.png" id="1_30hhw"]
[ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="1_uyhuj"]
@@ -17,6 +17,7 @@
[ext_resource type="PackedScene" uid="uid://dx80ivlvuuew4" path="res://objects/fxs/fire_fx.tscn" id="14_mrjm6"]
[ext_resource type="Script" uid="uid://d1388lhp2gpgr" path="res://scripts/components/IceEffectComponent.cs" id="14_pkino"]
[ext_resource type="PackedScene" uid="uid://ck6nml06tm6ue" path="res://objects/fxs/ice_fx.tscn" id="15_pkino"]
[ext_resource type="Script" uid="uid://b4hvq2i66fjhi" path="res://scripts/components/PathFollowerComponent.cs" id="18_q78ru"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_hil2i"]
radius = 6.0
@@ -190,3 +191,8 @@ collision_mask = 20
[node name="CollisionShape2D" type="CollisionShape2D" parent="Hitbox"]
position = Vector2(0, 2)
shape = SubResource("RectangleShape2D_cmp1h")
[node name="PathFollowerComponent" type="Node2D" parent="."]
script = ExtResource("18_q78ru")
ShouldRotate = false
metadata/_custom_type_script = "uid://b4hvq2i66fjhi"

File diff suppressed because one or more lines are too long

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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