From ef4d128869a21c744c2eb812541f1757e9b0abcd Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 12 Aug 2025 03:38:23 +0200 Subject: [PATCH] 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 --- Autoloads/GameManager.cs | 20 +-- Mr. Brick Adventures.csproj | 4 +- Mr. Brick Adventures.sln.DotSettings.user | 2 + scripts/Resources/SkillData.cs | 6 +- scripts/components/CanBeLaunchedComponent.cs | 8 ++ scripts/components/FlipComponent.cs | 2 +- scripts/components/IceEffectComponent.cs | 70 +++++++++++ scripts/components/JumpPadComponent.cs | 39 ++++++ .../KillPlayerOutOfScreenComponent.cs | 21 ++++ scripts/components/KnockbackComponent.cs | 48 +++++++ scripts/components/LifetimeComponent.cs | 27 ++++ scripts/components/MagneticSkillComponent.cs | 78 ++++++++++++ scripts/components/OutOfScreenComponent.cs | 18 +++ .../components/PeriodicShootingComponent.cs | 71 +++++++++++ ...vement.cs => PlatformMovementComponent.cs} | 2 +- ...s.uid => PlatformMovementComponent.cs.uid} | 0 scripts/components/PlayerController.cs | 16 +-- scripts/components/PlayerDeathComponent.cs | 36 ++++++ .../components/ProgressiveDamageComponent.cs | 65 ++++++++++ scripts/components/ProjectileComponent.cs | 26 ++++ scripts/components/ProjectileInitComponent.cs | 37 ++++++ scripts/components/RequirementComponent.cs | 44 +++++++ scripts/components/ScoreComponent.cs | 42 +++++++ scripts/components/ShipMovementComponent.cs | 37 ++++++ scripts/components/ShipShooterComponent.cs | 59 +++++++++ .../components/SideToSideMovementComponent.cs | 117 +++++++++++++++++ scripts/components/platform_movement.gd | 118 ------------------ scripts/components/platform_movement.gd.uid | 1 - 28 files changed, 869 insertions(+), 145 deletions(-) create mode 100644 scripts/components/CanBeLaunchedComponent.cs create mode 100644 scripts/components/IceEffectComponent.cs create mode 100644 scripts/components/JumpPadComponent.cs create mode 100644 scripts/components/KillPlayerOutOfScreenComponent.cs create mode 100644 scripts/components/KnockbackComponent.cs create mode 100644 scripts/components/LifetimeComponent.cs create mode 100644 scripts/components/MagneticSkillComponent.cs create mode 100644 scripts/components/OutOfScreenComponent.cs create mode 100644 scripts/components/PeriodicShootingComponent.cs rename scripts/components/{PlatformMovement.cs => PlatformMovementComponent.cs} (98%) rename scripts/components/{PlatformMovement.cs.uid => PlatformMovementComponent.cs.uid} (100%) create mode 100644 scripts/components/PlayerDeathComponent.cs create mode 100644 scripts/components/ProgressiveDamageComponent.cs create mode 100644 scripts/components/ProjectileComponent.cs create mode 100644 scripts/components/ProjectileInitComponent.cs create mode 100644 scripts/components/RequirementComponent.cs create mode 100644 scripts/components/ScoreComponent.cs create mode 100644 scripts/components/ShipMovementComponent.cs create mode 100644 scripts/components/ShipShooterComponent.cs create mode 100644 scripts/components/SideToSideMovementComponent.cs delete mode 100644 scripts/components/platform_movement.gd delete mode 100644 scripts/components/platform_movement.gd.uid diff --git a/Autoloads/GameManager.cs b/Autoloads/GameManager.cs index 588fc65..40bf0a8 100644 --- a/Autoloads/GameManager.cs +++ b/Autoloads/GameManager.cs @@ -18,7 +18,7 @@ public partial class GameManager : Node { "unlocked_skills", new Array() } }; - private Dictionary _currentSessionState = new() + public Dictionary CurrentSessionState { get; private set; } = new() { { "coins_collected", 0 }, { "skills_unlocked", new Array() } @@ -31,19 +31,19 @@ public partial class GameManager : Node public void SetCoins(int amount) => PlayerState["coins"] = Mathf.Max(0, amount); - public int GetCoins() => (int)PlayerState["coins"] + (int)_currentSessionState["coins_collected"]; + public int GetCoins() => (int)PlayerState["coins"] + (int)CurrentSessionState["coins_collected"]; public void RemoveCoins(int amount) { - var sessionCoins = (int)_currentSessionState["coins_collected"]; + var sessionCoins = (int)CurrentSessionState["coins_collected"]; if (amount <= sessionCoins) { - _currentSessionState["coins_collected"] = sessionCoins - amount; + CurrentSessionState["coins_collected"] = sessionCoins - amount; } else { var remaining = amount - sessionCoins; - _currentSessionState["coins_collected"] = 0; + CurrentSessionState["coins_collected"] = 0; PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] - remaining); } PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]); @@ -57,7 +57,7 @@ public partial class GameManager : Node public bool IsSkillUnlocked(SkillData skill) { return ((Array)PlayerState["unlocked_skills"]).Contains(skill) - || ((Array)_currentSessionState["skills_unlocked"]).Contains(skill); + || ((Array)CurrentSessionState["skills_unlocked"]).Contains(skill); } public void UnlockSkill(SkillData skill) @@ -123,7 +123,7 @@ public partial class GameManager : Node public void ResetCurrentSessionState() { - _currentSessionState = new Dictionary + CurrentSessionState = new Dictionary { { "coins_collected", 0 }, { "skills_unlocked", new Array() } @@ -172,8 +172,8 @@ public partial class GameManager : Node { var levelIndex = (int)PlayerState["current_level"]; MarkLevelComplete(levelIndex); - AddCoins((int)_currentSessionState["coins_collected"]); - foreach (var s in (Array)_currentSessionState["skills_unlocked"]) + AddCoins((int)CurrentSessionState["coins_collected"]); + foreach (var s in (Array)CurrentSessionState["skills_unlocked"]) UnlockSkill((SkillData)s); ResetCurrentSessionState(); @@ -184,7 +184,7 @@ public partial class GameManager : Node public Array GetUnlockedSkills() { var unlocked = (Array)PlayerState["unlocked_skills"]; - var session = (Array)_currentSessionState["skills_unlocked"]; + var session = (Array)CurrentSessionState["skills_unlocked"]; if ((((Array)session)!).Count == 0) return (Array)unlocked; if ((((Array)unlocked)!).Count == 0) return (Array)session; var joined = new Array(); diff --git a/Mr. Brick Adventures.csproj b/Mr. Brick Adventures.csproj index b7064e9..3fcf8b5 100644 --- a/Mr. Brick Adventures.csproj +++ b/Mr. Brick Adventures.csproj @@ -91,9 +91,7 @@ - - - + diff --git a/Mr. Brick Adventures.sln.DotSettings.user b/Mr. Brick Adventures.sln.DotSettings.user index e8e9aa0..38db013 100644 --- a/Mr. Brick Adventures.sln.DotSettings.user +++ b/Mr. Brick Adventures.sln.DotSettings.user @@ -1,4 +1,6 @@  + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded \ No newline at end of file diff --git a/scripts/Resources/SkillData.cs b/scripts/Resources/SkillData.cs index 2204c6f..a96a8b1 100644 --- a/scripts/Resources/SkillData.cs +++ b/scripts/Resources/SkillData.cs @@ -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 Config { get; set; } = new(); + [Export] public string Name { get; set; } = "New Skill"; + [Export] public string Description { get; set; } = "New Skill"; + [Export] public Dictionary Config { get; set; } = new(); [Export] public int Cost { get; set; } = 0; [Export] public Texture2D Icon { get; set; } [Export] public bool IsActive { get; set; } = false; diff --git a/scripts/components/CanBeLaunchedComponent.cs b/scripts/components/CanBeLaunchedComponent.cs new file mode 100644 index 0000000..787c39a --- /dev/null +++ b/scripts/components/CanBeLaunchedComponent.cs @@ -0,0 +1,8 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class CanBeLaunchedComponent : Node +{ + +} \ No newline at end of file diff --git a/scripts/components/FlipComponent.cs b/scripts/components/FlipComponent.cs index 62e263d..69ce6d8 100644 --- a/scripts/components/FlipComponent.cs +++ b/scripts/components/FlipComponent.cs @@ -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) { diff --git a/scripts/components/IceEffectComponent.cs b/scripts/components/IceEffectComponent.cs new file mode 100644 index 0000000..39d63e8 --- /dev/null +++ b/scripts/components/IceEffectComponent.cs @@ -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 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; + } + } +} \ No newline at end of file diff --git a/scripts/components/JumpPadComponent.cs b/scripts/components/JumpPadComponent.cs new file mode 100644 index 0000000..e203bf0 --- /dev/null +++ b/scripts/components/JumpPadComponent.cs @@ -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"); + 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; + } +} \ No newline at end of file diff --git a/scripts/components/KillPlayerOutOfScreenComponent.cs b/scripts/components/KillPlayerOutOfScreenComponent.cs new file mode 100644 index 0000000..9d039e7 --- /dev/null +++ b/scripts/components/KillPlayerOutOfScreenComponent.cs @@ -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); + } +} \ No newline at end of file diff --git a/scripts/components/KnockbackComponent.cs b/scripts/components/KnockbackComponent.cs new file mode 100644 index 0000000..588625a --- /dev/null +++ b/scripts/components/KnockbackComponent.cs @@ -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; + } +} \ No newline at end of file diff --git a/scripts/components/LifetimeComponent.cs b/scripts/components/LifetimeComponent.cs new file mode 100644 index 0000000..50929e0 --- /dev/null +++ b/scripts/components/LifetimeComponent.cs @@ -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(); + } +} \ No newline at end of file diff --git a/scripts/components/MagneticSkillComponent.cs b/scripts/components/MagneticSkillComponent.cs new file mode 100644 index 0000000..3d92090 --- /dev/null +++ b/scripts/components/MagneticSkillComponent.cs @@ -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 _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; + } +} \ No newline at end of file diff --git a/scripts/components/OutOfScreenComponent.cs b/scripts/components/OutOfScreenComponent.cs new file mode 100644 index 0000000..be71168 --- /dev/null +++ b/scripts/components/OutOfScreenComponent.cs @@ -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(); + } +} \ No newline at end of file diff --git a/scripts/components/PeriodicShootingComponent.cs b/scripts/components/PeriodicShootingComponent.cs new file mode 100644 index 0000000..13efd59 --- /dev/null +++ b/scripts/components/PeriodicShootingComponent.cs @@ -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(); + var launchComponent = bulletInstance.GetNodeOrNull("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); + } +} \ No newline at end of file diff --git a/scripts/components/PlatformMovement.cs b/scripts/components/PlatformMovementComponent.cs similarity index 98% rename from scripts/components/PlatformMovement.cs rename to scripts/components/PlatformMovementComponent.cs index 6cdd526..8f4f7c6 100644 --- a/scripts/components/PlatformMovement.cs +++ b/scripts/components/PlatformMovementComponent.cs @@ -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; diff --git a/scripts/components/PlatformMovement.cs.uid b/scripts/components/PlatformMovementComponent.cs.uid similarity index 100% rename from scripts/components/PlatformMovement.cs.uid rename to scripts/components/PlatformMovementComponent.cs.uid diff --git a/scripts/components/PlayerController.cs b/scripts/components/PlayerController.cs index 1f2bf5b..3006488 100644 --- a/scripts/components/PlayerController.cs +++ b/scripts/components/PlayerController.cs @@ -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(movement); - if (_currentMovement == null) + CurrentMovement = GetNodeOrNull(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(MovementTypes.Keys); - var currentIndex = keys.IndexOf(_currentMovement?.MovementType); + var currentIndex = keys.IndexOf(CurrentMovement?.MovementType); if (currentIndex == -1) { diff --git a/scripts/components/PlayerDeathComponent.cs b/scripts/components/PlayerDeathComponent.cs new file mode 100644 index 0000000..2c62493 --- /dev/null +++ b/scripts/components/PlayerDeathComponent.cs @@ -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("/root/gameManager"); + HealthComponent.Death += OnDeath; + } + + private void OnDeath() + { + DeathSfx?.Play(); + + if (DeathEffect != null) + { + var effect = DeathEffect.Instantiate(); + GetParent().AddChild(effect); + effect.GlobalPosition = GlobalPosition; + effect.Scale = EffectScale; + } + + _gameManager.RemoveLives(1); + _gameManager.ResetCurrentSessionState(); + } +} \ No newline at end of file diff --git a/scripts/components/ProgressiveDamageComponent.cs b/scripts/components/ProgressiveDamageComponent.cs new file mode 100644 index 0000000..ebd6b63 --- /dev/null +++ b/scripts/components/ProgressiveDamageComponent.cs @@ -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); + } +} \ No newline at end of file diff --git a/scripts/components/ProjectileComponent.cs b/scripts/components/ProjectileComponent.cs new file mode 100644 index 0000000..5741ca2 --- /dev/null +++ b/scripts/components/ProjectileComponent.cs @@ -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(); + } +} \ No newline at end of file diff --git a/scripts/components/ProjectileInitComponent.cs b/scripts/components/ProjectileInitComponent.cs new file mode 100644 index 0000000..4821ad6 --- /dev/null +++ b/scripts/components/ProjectileInitComponent.cs @@ -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; + } +} \ No newline at end of file diff --git a/scripts/components/RequirementComponent.cs b/scripts/components/RequirementComponent.cs new file mode 100644 index 0000000..70c83d7 --- /dev/null +++ b/scripts/components/RequirementComponent.cs @@ -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"); + if (c != null && c.Data.Type == RequirementType) + { + c.Collected += OnCollected; + } + } + } + + private void OnCollected(Variant amount, CollectableType type, Node2D body) + { + AddProgress(amount.As()); + } + + private void AddProgress(int amount = 1) + { + _currentAmount += amount; + if (_currentAmount >= RequirementAmount) + { + EmitSignalRequirementMet(RequirementType); + } + } +} \ No newline at end of file diff --git a/scripts/components/ScoreComponent.cs b/scripts/components/ScoreComponent.cs new file mode 100644 index 0000000..c612990 --- /dev/null +++ b/scripts/components/ScoreComponent.cs @@ -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("/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"); + if (c != null) + { + c.Collected += OnCollected; + } + } + } + + private void OnCollected(Variant amount, CollectableType type, Node2D body) + { + if (type != CollectableType.Coin) return; + + var coinAmount = amount.As(); + var currentCoins = (int)_gameManager.CurrentSessionState["coins_collected"]; + _gameManager.CurrentSessionState["coins_collected"] = currentCoins + coinAmount; + } +} \ No newline at end of file diff --git a/scripts/components/ShipMovementComponent.cs b/scripts/components/ShipMovementComponent.cs new file mode 100644 index 0000000..547da4c --- /dev/null +++ b/scripts/components/ShipMovementComponent.cs @@ -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(); + } +} \ No newline at end of file diff --git a/scripts/components/ShipShooterComponent.cs b/scripts/components/ShipShooterComponent.cs new file mode 100644 index 0000000..cb6d4d9 --- /dev/null +++ b/scripts/components/ShipShooterComponent.cs @@ -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(); + var init = bullet.GetNodeOrNull("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(); + } +} \ No newline at end of file diff --git a/scripts/components/SideToSideMovementComponent.cs b/scripts/components/SideToSideMovementComponent.cs new file mode 100644 index 0000000..bfad24f --- /dev/null +++ b/scripts/components/SideToSideMovementComponent.cs @@ -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; + } +} \ No newline at end of file diff --git a/scripts/components/platform_movement.gd b/scripts/components/platform_movement.gd deleted file mode 100644 index 38773da..0000000 --- a/scripts/components/platform_movement.gd +++ /dev/null @@ -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 \ No newline at end of file diff --git a/scripts/components/platform_movement.gd.uid b/scripts/components/platform_movement.gd.uid deleted file mode 100644 index 80f7a5f..0000000 --- a/scripts/components/platform_movement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c1wtrgw0x77xo