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

@@ -18,7 +18,7 @@ public partial class GameManager : Node
{ "unlocked_skills", new Array<SkillData>() } { "unlocked_skills", new Array<SkillData>() }
}; };
private Dictionary _currentSessionState = new() public Dictionary CurrentSessionState { get; private set; } = new()
{ {
{ "coins_collected", 0 }, { "coins_collected", 0 },
{ "skills_unlocked", new Array<SkillData>() } { "skills_unlocked", new Array<SkillData>() }
@@ -31,19 +31,19 @@ public partial class GameManager : Node
public void SetCoins(int amount) => PlayerState["coins"] = Mathf.Max(0, amount); 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) public void RemoveCoins(int amount)
{ {
var sessionCoins = (int)_currentSessionState["coins_collected"]; var sessionCoins = (int)CurrentSessionState["coins_collected"];
if (amount <= sessionCoins) if (amount <= sessionCoins)
{ {
_currentSessionState["coins_collected"] = sessionCoins - amount; CurrentSessionState["coins_collected"] = sessionCoins - amount;
} }
else else
{ {
var remaining = amount - sessionCoins; 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"] - remaining);
} }
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]); PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]);
@@ -57,7 +57,7 @@ public partial class GameManager : Node
public bool IsSkillUnlocked(SkillData skill) public bool IsSkillUnlocked(SkillData skill)
{ {
return ((Array)PlayerState["unlocked_skills"]).Contains(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) public void UnlockSkill(SkillData skill)
@@ -123,7 +123,7 @@ public partial class GameManager : Node
public void ResetCurrentSessionState() public void ResetCurrentSessionState()
{ {
_currentSessionState = new Dictionary CurrentSessionState = new Dictionary
{ {
{ "coins_collected", 0 }, { "coins_collected", 0 },
{ "skills_unlocked", new Array<SkillData>() } { "skills_unlocked", new Array<SkillData>() }
@@ -172,8 +172,8 @@ public partial class GameManager : Node
{ {
var levelIndex = (int)PlayerState["current_level"]; var levelIndex = (int)PlayerState["current_level"];
MarkLevelComplete(levelIndex); MarkLevelComplete(levelIndex);
AddCoins((int)_currentSessionState["coins_collected"]); AddCoins((int)CurrentSessionState["coins_collected"]);
foreach (var s in (Array)_currentSessionState["skills_unlocked"]) foreach (var s in (Array)CurrentSessionState["skills_unlocked"])
UnlockSkill((SkillData)s); UnlockSkill((SkillData)s);
ResetCurrentSessionState(); ResetCurrentSessionState();
@@ -184,7 +184,7 @@ public partial class GameManager : Node
public Array GetUnlockedSkills() public Array GetUnlockedSkills()
{ {
var unlocked = (Array<SkillData>)PlayerState["unlocked_skills"]; var unlocked = (Array<SkillData>)PlayerState["unlocked_skills"];
var session = (Array<SkillData>)_currentSessionState["skills_unlocked"]; var session = (Array<SkillData>)CurrentSessionState["skills_unlocked"];
if ((((Array)session)!).Count == 0) return (Array)unlocked; if ((((Array)session)!).Count == 0) return (Array)unlocked;
if ((((Array)unlocked)!).Count == 0) return (Array)session; if ((((Array)unlocked)!).Count == 0) return (Array)session;
var joined = new Array(); var joined = new Array();

View File

@@ -91,9 +91,7 @@
<Content Include="scripts\components\out_of_screen_component.gd.uid" /> <Content Include="scripts\components\out_of_screen_component.gd.uid" />
<Content Include="scripts\components\periodic_shooting.gd" /> <Content Include="scripts\components\periodic_shooting.gd" />
<Content Include="scripts\components\periodic_shooting.gd.uid" /> <Content Include="scripts\components\periodic_shooting.gd.uid" />
<Content Include="scripts\components\PlatformMovement.cs.uid" /> <Content Include="scripts\components\PlatformMovementComponent.cs.uid" />
<Content Include="scripts\components\platform_movement.gd" />
<Content Include="scripts\components\platform_movement.gd.uid" />
<Content Include="scripts\components\PlayerController.cs.uid" /> <Content Include="scripts\components\PlayerController.cs.uid" />
<Content Include="scripts\components\player_death.gd" /> <Content Include="scripts\components\player_death.gd" />
<Content Include="scripts\components\player_death.gd.uid" /> <Content Include="scripts\components\player_death.gd.uid" />

View File

@@ -1,4 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACanvasItem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fef7b819b226fab796d1dfe66d415dd7510bcac87675020ddb8f03a828e763_003FCanvasItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollisionShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ca9b7334678f5c97c7c2a9fbe4837be71cae11b6a30408dd4791b18f997e4a_003FCollisionShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollisionShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ca9b7334678f5c97c7c2a9fbe4837be71cae11b6a30408dd4791b18f997e4a_003FCollisionShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F86db9cd834346aad02d74c1b66dd9c64d6ef3147435dd9c9c9477b48f7_003FNode2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARectangleShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fa1cc98873548652da0c14ecefa4737431426fcbb24a7f0641e3d9c266c3_003FRectangleShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARectangleShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fa1cc98873548652da0c14ecefa4737431426fcbb24a7f0641e3d9c266c3_003FRectangleShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3671dbbd9b17cdf2bf9075b468b6bd7e3ab13fc3be7a116484085d3b6cc9fe_003FShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3671dbbd9b17cdf2bf9075b468b6bd7e3ab13fc3be7a116484085d3b6cc9fe_003FShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View File

@@ -6,9 +6,9 @@ namespace Mr.BrickAdventures.scripts.Resources;
public partial class SkillData : Resource public partial class SkillData : Resource
{ {
[Export] public String Name { get; set; } = "New Skill"; [Export] public string Name { get; set; } = "New Skill";
[Export] public String Description { get; set; } = "New Skill"; [Export] public string Description { get; set; } = "New Skill";
[Export] public Dictionary<String, Variant> Config { get; set; } = new(); [Export] public Dictionary<string, Variant> Config { get; set; } = new();
[Export] public int Cost { get; set; } = 0; [Export] public int Cost { get; set; } = 0;
[Export] public Texture2D Icon { get; set; } [Export] public Texture2D Icon { get; set; }
[Export] public bool IsActive { get; set; } = false; [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 LeftEye { get; set; }
[Export] public Sprite2D RightEye { 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) 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; namespace Mr.BrickAdventures.scripts.components;
public partial class PlatformMovement : Node2D, IMovement public partial class PlatformMovementComponent : Node2D, IMovement
{ {
[Export] [Export]
public float Speed { get; set; } = 300.0f; public float Speed { get; set; } = 300.0f;

View File

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