Refactor GameManager session state handling and add new components: CanBeLaunchedComponent, IceEffectComponent, JumpPadComponent, KillPlayerOutOfScreenComponent, KnockbackComponent, LifetimeComponent, MagneticSkillComponent, OutOfScreenComponent, PeriodicShootingComponent, PlayerDeathComponent, ProgressiveDamageComponent, ProjectileComponent, ProjectileInitComponent, RequirementComponent, ScoreComponent, ShipMovementComponent, ShipShooterComponent, and SideToSideMovementComponent

This commit is contained in:
2025-08-12 03:38:23 +02:00
parent f3aa2631f2
commit ef4d128869
28 changed files with 869 additions and 145 deletions

View File

@@ -6,9 +6,9 @@ namespace Mr.BrickAdventures.scripts.Resources;
public partial class SkillData : Resource
{
[Export] public String Name { get; set; } = "New Skill";
[Export] public String Description { get; set; } = "New Skill";
[Export] public Dictionary<String, Variant> Config { get; set; } = new();
[Export] public string Name { get; set; } = "New Skill";
[Export] public string Description { get; set; } = "New Skill";
[Export] public Dictionary<string, Variant> Config { get; set; } = new();
[Export] public int Cost { get; set; } = 0;
[Export] public Texture2D Icon { get; set; }
[Export] public bool IsActive { get; set; } = false;

View File

@@ -0,0 +1,8 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class CanBeLaunchedComponent : Node
{
}

View File

@@ -6,7 +6,7 @@ public partial class FlipComponent : Node2D
{
[Export] public Sprite2D LeftEye { get; set; }
[Export] public Sprite2D RightEye { get; set; }
[Export] public PlatformMovement PlatformMovement { get; set; }
[Export] public PlatformMovementComponent PlatformMovement { get; set; }
public override void _Process(double delta)
{

View File

@@ -0,0 +1,70 @@
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class IceEffectComponent : Node
{
[Export] public Array<Node> ComponentsToDisable { get; set; } = [];
[Export] public StatusEffectComponent StatusEffectComponent { get; set; }
[Export] public Node2D IceFx { get; set; }
private StatusEffectDataResource _data = null;
private int _iceEffectsApplied = 0;
public override void _Ready()
{
StatusEffectComponent.EffectApplied += OnEffectApplied;
StatusEffectComponent.EffectRemoved += OnEffectRemoved;
}
private void OnEffectApplied(StatusEffect statusEffect)
{
if (statusEffect.EffectData.Type != StatusEffectType.Ice) return;
_data = statusEffect.EffectData;
_iceEffectsApplied++;
ApplyFreeze();
}
private void OnEffectRemoved(StatusEffectType type)
{
if (type != StatusEffectType.Ice) return;
_data = null;
_iceEffectsApplied--;
RemoveFreeze();
}
private void ApplyFreeze()
{
if (IceFx != null)
{
IceFx.Visible = true;
}
foreach (var component in ComponentsToDisable)
{
if (component == null || _iceEffectsApplied == 0) continue;
component.ProcessMode = ProcessModeEnum.Disabled;
}
}
private void RemoveFreeze()
{
if (_iceEffectsApplied > 0) return;
if (IceFx != null)
{
IceFx.Visible = false;
}
foreach (var component in ComponentsToDisable)
{
if (component == null) continue;
component.ProcessMode = ProcessModeEnum.Inherit;
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class JumpPadComponent : Node
{
[Export] public float JumpForce { get; set; } = 10f;
[Export] public Area2D Area { get; set; }
[Export] public Sprite2D Sprite { get; set; }
[Export] public int StartAnimationIndex { get; set; } = 0;
[Export] public float AnimationDuration { get; set; } = 0.5f;
public override void _Ready()
{
Area.BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node2D body)
{
var canBeLaunched = body.GetNodeOrNull<CanBeLaunchedComponent>("CanBeLaunchedComponent");
if (canBeLaunched == null) return;
if (body is not PlayerController { CurrentMovement: PlatformMovementComponent movement }) return;
_ = HandleLaunchPadAnimation();
movement.Body.Velocity = new Vector2(movement.Body.Velocity.X, -JumpForce);
movement.JumpSfx?.Play();
}
private async Task HandleLaunchPadAnimation()
{
if (Sprite == null) return;
var timer = GetTree().CreateTimer(AnimationDuration);
Sprite.Frame = StartAnimationIndex + 1;
await ToSignal(timer, Timer.SignalName.Timeout);
Sprite.Frame = StartAnimationIndex;
}
}

View File

@@ -0,0 +1,21 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class KillPlayerOutOfScreenComponent : Node
{
[Export] public VisibleOnScreenNotifier2D ScreenNotifier { get; set; }
[Export] public HealthComponent HealthComponent { get; set; }
private const float Damage = 6000f;
public override void _Ready()
{
ScreenNotifier.ScreenExited += HandleOutOfScreen;
}
private void HandleOutOfScreen()
{
HealthComponent?.DecreaseHealth(Damage);
}
}

View File

@@ -0,0 +1,48 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class KnockbackComponent : Node
{
[Export] public CharacterBody2D Body { get; set; }
[Export] public float KnockbackForce { get; set; } = 25f;
[Export] public HealthComponent HealthComponent { get; set; }
private bool _knockbackMode = false;
private int _knockbackFrames = 0;
public override void _Ready()
{
HealthComponent.HealthChanged += OnHealthChanged;
}
public override void _Process(double delta)
{
if (_knockbackMode) _knockbackFrames++;
if (_knockbackFrames <= 1) return;
_knockbackMode = false;
_knockbackFrames = 0;
}
public override void _PhysicsProcess(double delta)
{
if (_knockbackMode) ApplyKnockback((float)delta);
}
private void OnHealthChanged(float delta, float totalHealth)
{
if (totalHealth <= 0f || delta >= 0f) return;
_knockbackMode = true;
}
private void ApplyKnockback(float delta)
{
var velocity = Body.Velocity.Normalized();
var knockbackDirection = new Vector2(Mathf.Sign(velocity.X), 0.4f);
var knockbackVector = -knockbackDirection * KnockbackForce * delta;
Body.Velocity += knockbackVector;
}
}

View File

@@ -0,0 +1,27 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class LifetimeComponent : Node
{
[Export] public float LifeTime { get; set; } = 5.0f;
private Timer _lifetimeTimer;
public override void _Ready()
{
_lifetimeTimer = new Timer();
_lifetimeTimer.WaitTime = LifeTime;
_lifetimeTimer.OneShot = true;
_lifetimeTimer.Autostart = true;
_lifetimeTimer.Timeout += OnLifetimeTimeout;
AddChild(_lifetimeTimer);
_lifetimeTimer.Start();
}
private void OnLifetimeTimeout()
{
Owner.QueueFree();
}
}

View File

@@ -0,0 +1,78 @@
using System;
using Godot;
using Godot.Collections;
namespace Mr.BrickAdventures.scripts.components;
public partial class MagneticSkillComponent : Node
{
[Export] public Area2D MagneticArea { get; set; }
[Export] public float MagneticMoveDuration { get; set; } = 1.25f;
private Array<Node2D> _collectablesToPickUp = [];
public override void _Ready()
{
MagneticArea.AreaEntered += OnAreaEntered;
MagneticArea.BodyEntered += OnBodyEntered;
}
public override void _Process(double delta)
{
foreach (var collectable in _collectablesToPickUp)
{
if (!IsInstanceValid(collectable))
{
_collectablesToPickUp.Remove(collectable);
continue;
}
MoveCollectableToOwner(collectable);
}
}
private void OnBodyEntered(Node2D body)
{
if (!HasComponentInChildren(body, "Collectable")) return;
if (_collectablesToPickUp.Contains(body)) return;
_collectablesToPickUp.Add(body);
}
private void OnAreaEntered(Area2D area)
{
if (!HasComponentInChildren(area, "Collectable")) return;
if (_collectablesToPickUp.Contains(area)) return;
_collectablesToPickUp.Add(area);
}
private bool HasComponentInChildren(Node node, string componentName)
{
if (node == null) return false;
if (node.HasNode(componentName)) return true;
foreach (var child in node.GetChildren())
{
if (child is { } childNode && HasComponentInChildren(childNode, componentName))
{
return true;
}
}
return false;
}
private void MoveCollectableToOwner(Node2D collectable)
{
if (!IsInstanceValid(collectable)) return;
if (Owner is not Node2D root) return;
var direction = (root.GlobalPosition - collectable.GlobalPosition).Normalized();
var speed = direction.Length() / MagneticMoveDuration;
collectable.GlobalPosition += direction.Normalized() * speed;
}
}

View File

@@ -0,0 +1,18 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class OutOfScreenComponent : Node
{
[Export] public VisibleOnScreenNotifier2D VisibilityNotifier { get; set; }
public override void _Ready()
{
VisibilityNotifier.ScreenExited += OnScreenExited;
}
private void OnScreenExited()
{
Owner?.QueueFree();
}
}

View File

@@ -0,0 +1,71 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class PeriodicShootingComponent : Node
{
[Export] public PackedScene BulletScene { get; set; }
[Export] public float ShootInterval { get; set; } = 1.0f;
[Export] public Vector2 ShootDirection { get; set; } = Vector2.Right;
[Export] public SideToSideMovementComponent SideToSideMovement { get; set; }
[Export] public Node2D BulletSpawnRight { get; set; }
[Export] public Node2D BulletSpawnLeft { get; set; }
[Export] public float ShootingIntervalVariation { get; set; } = 0.0f;
private Timer _timer;
public override void _Ready()
{
SetupTimer();
}
public override void _Process(double delta)
{
if (SideToSideMovement == null) return;
ShootDirection = SideToSideMovement.Direction != Vector2.Zero ? SideToSideMovement.Direction : Vector2.Right;
}
private void SetupTimer()
{
_timer = new Timer();
_timer.WaitTime = GetShootInterval();
_timer.OneShot = false;
_timer.Autostart = true;
_timer.Timeout += OnTimerTimeout;
AddChild(_timer);
}
private void OnTimerTimeout()
{
Shoot();
_timer.Start();
}
private double GetShootInterval()
{
if (ShootingIntervalVariation == 0f) return ShootInterval;
var rng = new RandomNumberGenerator();
return ShootInterval + rng.RandfRange(-ShootingIntervalVariation, ShootingIntervalVariation);
}
private void Shoot()
{
if (ShootDirection == Vector2.Zero) return;
var root = Owner as Node2D;
var bulletInstance = BulletScene.Instantiate<Node2D>();
var launchComponent = bulletInstance.GetNodeOrNull<LaunchComponent>("LaunchComponent");
var spawnPosition = ShootDirection == Vector2.Right ? BulletSpawnRight.GlobalPosition : BulletSpawnLeft.GlobalPosition;
if (launchComponent != null)
{
launchComponent.InitialDirection = ShootDirection;
launchComponent.SpawnPosition = spawnPosition;
if (root != null) launchComponent.SpawnRotation = root.Rotation;
}
bulletInstance.Position = spawnPosition;
GetTree().CurrentScene.AddChild(bulletInstance);
}
}

View File

@@ -3,7 +3,7 @@ using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlatformMovement : Node2D, IMovement
public partial class PlatformMovementComponent : Node2D, IMovement
{
[Export]
public float Speed { get; set; } = 300.0f;

View File

@@ -15,7 +15,7 @@ public partial class PlayerController : Node2D
[Export]
public Sprite2D ShipSprite { get; set; }
private IMovement _currentMovement = null;
public IMovement CurrentMovement = null;
[Signal]
public delegate void MovementSwitchedEventHandler(string movementType);
@@ -48,20 +48,20 @@ public partial class PlayerController : Node2D
private void SwitchMovement(string movementType)
{
if (_currentMovement != null)
if (CurrentMovement != null)
{
_currentMovement.Enabled = false;
CurrentMovement.Enabled = false;
}
if (MovementTypes.TryGetValue(movementType, out var movement))
{
_currentMovement = GetNodeOrNull<IMovement>(movement);
if (_currentMovement == null)
CurrentMovement = GetNodeOrNull<IMovement>(movement);
if (CurrentMovement == null)
{
GD.PushError($"Movement type '{movementType}' not found in MovementTypes.");
return;
}
_currentMovement.Enabled = true;
CurrentMovement.Enabled = true;
EmitSignalMovementSwitched(movementType);
}
else
@@ -69,7 +69,7 @@ public partial class PlayerController : Node2D
GD.PushError($"Movement type '{movementType}' not found in MovementTypes.");
}
if (_currentMovement == null)
if (CurrentMovement == null)
{
GD.PushError("No current movement set after switching.");
}
@@ -78,7 +78,7 @@ public partial class PlayerController : Node2D
private string GetNextMovementType()
{
var keys = new List<string>(MovementTypes.Keys);
var currentIndex = keys.IndexOf(_currentMovement?.MovementType);
var currentIndex = keys.IndexOf(CurrentMovement?.MovementType);
if (currentIndex == -1)
{

View File

@@ -0,0 +1,36 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlayerDeathComponent : Node2D
{
[Export] public AudioStreamPlayer2D DeathSfx { get; set; }
[Export] public PackedScene DeathEffect { get; set; }
[Export] public HealthComponent HealthComponent { get; set; }
[Export] public Vector2 EffectScale { get; set; } = new Vector2(1.5f, 1.5f);
private GameManager _gameManager;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/gameManager");
HealthComponent.Death += OnDeath;
}
private void OnDeath()
{
DeathSfx?.Play();
if (DeathEffect != null)
{
var effect = DeathEffect.Instantiate<Node2D>();
GetParent().AddChild(effect);
effect.GlobalPosition = GlobalPosition;
effect.Scale = EffectScale;
}
_gameManager.RemoveLives(1);
_gameManager.ResetCurrentSessionState();
}
}

View File

@@ -0,0 +1,65 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ProgressiveDamageComponent : Node
{
[Export] public HealthComponent HealthComponent { get; set; }
[Export] public Sprite2D Sprite { get; set; }
[Export] public PlatformMovementComponent PlatformMovement { get; set; }
[Export] public float MinJumpHeight { get; set; } = 60f;
[Export] public float JumpReductionPercentage { get; set; } = 0.1f; // this is a percentage of the jump height per hit
private float _maxHealth;
private float _ogJumpHeight;
public override void _Ready()
{
_maxHealth = HealthComponent.MaxHealth;
HealthComponent.HealthChanged += OnHealthChanged;
if (PlatformMovement != null)
{
_ogJumpHeight = PlatformMovement.JumpHeight;
}
}
private void OnHealthChanged(float delta, float totalHealth)
{
var frame = GetDamageFrame();
if (frame < 0 || frame >= Sprite.GetHframes()) return;
Sprite.Frame = frame;
if (PlatformMovement != null)
{
PlatformMovement.JumpHeight = GetJumpHeight();
}
}
private int GetDamageFrame()
{
if (Sprite == null || HealthComponent == null) return 0;
var framesCount = Sprite.GetHframes();
if (framesCount == 0) return 0;
var currentHealth = HealthComponent.Health;
var healthRatio = currentHealth / _maxHealth;
return (int)(framesCount * (1f - healthRatio));
}
private float GetJumpHeight()
{
if (PlatformMovement == null) return 0f;
var jumpHeight = _ogJumpHeight;
if (jumpHeight <= 0f) return 0f;
var damageFrame = GetDamageFrame();
if (damageFrame < 0 || damageFrame >= Sprite.GetHframes()) return jumpHeight;
var reduction = JumpReductionPercentage * jumpHeight;
var calculatedJumpHeight = jumpHeight - (damageFrame * reduction);
return Mathf.Max(calculatedJumpHeight, MinJumpHeight);
}
}

View File

@@ -0,0 +1,26 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ProjectileComponent : Node2D
{
[Export] public float Speed { get; set; } = 16f;
[Export] public float AngleDirection { get; set; }
[Export] public Vector2 SpawnPosition { get; set; } = Vector2.Zero;
[Export] public float SpawnRotation { get; set; } = 0f;
[Export] public CharacterBody2D Body { get; set; }
public override void _Ready()
{
GlobalPosition = SpawnPosition;
GlobalRotation = SpawnRotation;
}
public override void _PhysicsProcess(double delta)
{
if (Body == null) return;
Body.Velocity += new Vector2(0f, -Speed).Rotated(AngleDirection);
Body.MoveAndSlide();
}
}

View File

@@ -0,0 +1,37 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ProjectileInitParams
{
public Vector2 Position { get; set; } = Vector2.Zero;
public Vector2 Direction { get; set; } = Vector2.Right;
public float Rotation { get; set; } = 0f;
public float PowerMultiplier { get; set; } = 1f;
}
public partial class ProjectileInitComponent : Node
{
[Export] public LaunchComponent LaunchComponent { get; set; }
public void Initialize(ProjectileInitParams p)
{
var position = p.Position;
var direction = p.Direction;
var rotation = p.Rotation;
var power = p.PowerMultiplier;
if (Owner is Node2D root)
{
root.GlobalPosition = position;
root.GlobalRotation = rotation;
}
if (LaunchComponent == null) return;
LaunchComponent.InitialDirection = direction;
LaunchComponent.SpawnPosition = position;
LaunchComponent.SpawnRotation = rotation;
LaunchComponent.Speed *= power;
}
}

View File

@@ -0,0 +1,44 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class RequirementComponent : Node
{
[Export] public CollectableType RequirementType { get; set; }
[Export] public int RequirementAmount { get; set; } = 1;
private int _currentAmount = 0;
private const string CollectableGroupName = "Collectables";
[Signal]
public delegate void RequirementMetEventHandler(CollectableType requirementType);
public override void _Ready()
{
var collectables = GetTree().GetNodesInGroup(CollectableGroupName);
foreach (var collectable in collectables)
{
var c = collectable.GetNodeOrNull<CollectableComponent>("CollectableComponent");
if (c != null && c.Data.Type == RequirementType)
{
c.Collected += OnCollected;
}
}
}
private void OnCollected(Variant amount, CollectableType type, Node2D body)
{
AddProgress(amount.As<int>());
}
private void AddProgress(int amount = 1)
{
_currentAmount += amount;
if (_currentAmount >= RequirementAmount)
{
EmitSignalRequirementMet(RequirementType);
}
}
}

View File

@@ -0,0 +1,42 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class ScoreComponent : Node
{
private GameManager _gameManager;
private const string CoinsGroupName = "Coins";
public override async void _Ready()
{
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
_gameManager = GetNode<GameManager>("/root/GameManager");
if (_gameManager == null)
{
GD.PrintErr("GameManager not found in the scene tree.");
return;
}
var coins = GetTree().GetNodesInGroup("Coins");
foreach (var coin in coins)
{
var c = coin.GetNodeOrNull<CollectableComponent>("CollectableComponent");
if (c != null)
{
c.Collected += OnCollected;
}
}
}
private void OnCollected(Variant amount, CollectableType type, Node2D body)
{
if (type != CollectableType.Coin) return;
var coinAmount = amount.As<int>();
var currentCoins = (int)_gameManager.CurrentSessionState["coins_collected"];
_gameManager.CurrentSessionState["coins_collected"] = currentCoins + coinAmount;
}
}

View File

@@ -0,0 +1,37 @@
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 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

@@ -0,0 +1,59 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ShipShooterComponent : Node
{
[Export] public PackedScene BulletScene { get; set; }
[Export] public float FireRate { get; set; } = 0.2f;
[Export] public Marker2D BulletSpawn { get; set; }
[Export] public AudioStreamPlayer2D ShootSfx { get; set; }
private bool _canShoot = false;
public override void _Ready()
{
SetProcess(false);
}
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("attack") && _canShoot)
{
_ = Shoot();
}
}
private async Task Shoot()
{
if (!_canShoot) return;
var bullet = BulletScene.Instantiate<Node2D>();
var init = bullet.GetNodeOrNull<ProjectileInitComponent>("ProjectileInitComponent");
init?.Initialize(new ProjectileInitParams()
{
Position = BulletSpawn.GlobalPosition,
});
GetTree().CurrentScene.AddChild(bullet);
ShootSfx?.Play();
_canShoot = false;
await ToSignal(GetTree().CreateTimer(FireRate), Timer.SignalName.Timeout);
_canShoot = true;
}
private void OnShipEntered()
{
_canShoot = true;
SetProcess(true);
}
private void OnShipExited()
{
_canShoot = false;
SetProcess(false);
ShootSfx?.Stop();
}
}

View File

@@ -0,0 +1,117 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class SideToSideMovementComponent : Node
{
[Export] public Sprite2D Sprite { get; set; }
[Export] public float Speed { get; set; } = 10.0f;
[Export] public float WaitTime { get; set; } = 1.0f;
[Export] public RayCast2D LeftRay { get; set; }
[Export] public RayCast2D RightRay { get; set; }
[Export] public RayCast2D LeftWallRay { get; set; }
[Export] public RayCast2D RightWallRay { get; set; }
private Vector2 _direction = Vector2.Left;
private Vector2 _newDirection = Vector2.Left;
private Timer _timer;
private bool _triggeredDirectionChange = false;
[Signal]
public delegate void DirectionChangedEventHandler();
public Vector2 Direction => _direction;
public override void _Ready()
{
SetupTimer();
DirectionChanged += OnDirectionChanged;
}
public override void _PhysicsProcess(double delta)
{
HandleDirection();
HandleSpriteFlip();
HandleMovement(delta);
}
private void HandleDirection()
{
// Check if we are colliding with the left wall
if (LeftWallRay.IsColliding())
{
_newDirection = Vector2.Right;
EmitSignalDirectionChanged();
return;
}
// Check if we are colliding with the right wall
if (RightWallRay.IsColliding())
{
_newDirection = Vector2.Left;
EmitSignalDirectionChanged();
return;
}
// We are not colliding with anything, which means we don't have ground to walk on. Stop moving.
if (!LeftRay.IsColliding() && !RightRay.IsColliding())
{
_newDirection = Vector2.Zero;
return;
}
// If the left ray is not colliding and the right ray is colliding, that means we have ground to the right and we should change direction to the right.
if (!LeftRay.IsColliding() && RightRay.IsColliding())
{
_newDirection = Vector2.Right;
EmitSignalDirectionChanged();
return;
}
// If the right ray is not colliding and the left ray is colliding, that means we have ground to the left and we should change direction to the left.
if (!RightRay.IsColliding() && LeftRay.IsColliding())
{
_newDirection = Vector2.Left;
EmitSignalDirectionChanged();
return;
}
}
private void HandleSpriteFlip()
{
Sprite.FlipH = _direction == Vector2.Left;
}
private void HandleMovement(double delta)
{
var root = Owner as Node2D;
if (root == null) return;
root.Position += _direction * Speed * (float)delta;
}
private void OnDirectionChanged()
{
if (_direction == _newDirection || _triggeredDirectionChange)
return;
_triggeredDirectionChange = true;
_direction = Vector2.Zero;
_timer.Start();
}
private void OnTimerTimeout()
{
_timer.Stop();
_direction = _newDirection;
_triggeredDirectionChange = false;
}
private void SetupTimer()
{
_timer = new Timer();
AddChild(_timer);
_timer.WaitTime = WaitTime;
_timer.OneShot = true;
_timer.Timeout += OnTimerTimeout;
}
}

View File

@@ -1,118 +0,0 @@
class_name PlatformMovement
extends PlayerMovement
@export var speed: float = 300.0
@export var jump_height: float = 100
@export var jump_time_to_peak: float = 0.5
@export var jump_time_to_descent: float = 0.4
@export var coyote_frames: int = 6
@export var jump_sfx: AudioStreamPlayer2D
@export var rotation_target: Node2D
@export var body: CharacterBody2D
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var was_last_floor := false
var coyote_mode := false
var coyote_timer: Timer
var last_direction := Vector2.RIGHT
@onready var jump_velocity: float = ((2.0 * jump_height) / jump_time_to_peak) * -1.0
@onready var jump_gravity: float = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0
@onready var fall_gravity: float = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0
func _ready() -> void:
if not body:
return
coyote_timer = Timer.new()
coyote_timer.one_shot = true
coyote_timer.wait_time = coyote_frames / 60.0
coyote_timer.timeout.connect(on_coyote_timer_timeout)
add_child(coyote_timer)
func _process(_delta: float) -> void:
if not body or not enabled:
return
if body.velocity.x > 0.0:
rotation_target.rotation = deg_to_rad(-10)
elif body.velocity.x < 0.0:
rotation_target.rotation = deg_to_rad(10)
else:
rotation_target.rotation = 0
calculate_jump_vars()
func _physics_process(delta) -> void:
if not body or not enabled:
return
if body.is_on_floor():
was_last_floor = true
coyote_mode = false # Reset coyote mode when back on the floor
coyote_timer.stop() # Stop timer when grounded
else:
if was_last_floor: # Start coyote timer only once
coyote_mode = true
coyote_timer.start()
was_last_floor = false
if not body.is_on_floor():
body.velocity.y += calculate_gravity() * delta
if Input.is_action_pressed("jump") and (body.is_on_floor() or coyote_mode):
jump()
if Input.is_action_just_pressed("down"):
body.position.y += 1
var direction := Input.get_axis("left", "right")
if direction != 0:
last_direction = handle_direction(direction)
if direction:
body.velocity.x = direction * speed
else:
body.velocity.x = move_toward(body.velocity.x, 0, speed)
previous_velocity = body.velocity
body.move_and_slide()
func jump() -> void:
if not body:
return
body.velocity.y = jump_velocity
coyote_mode = false
if jump_sfx:
jump_sfx.play()
func calculate_gravity() -> float:
return jump_gravity if body.velocity.y < 0.0 else fall_gravity
func on_coyote_timer_timeout() -> void:
coyote_mode = false
func handle_direction(input_dir: float) -> Vector2:
if input_dir > 0:
return Vector2.RIGHT
elif input_dir < 0:
return Vector2.LEFT
return last_direction
func on_ship_entered() -> void:
rotation_target.rotation = 0
func calculate_jump_vars() -> void:
jump_velocity = ((2.0 * jump_height) / jump_time_to_peak) * -1.0
jump_gravity = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0
fall_gravity = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0

View File

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