From bf11d6a9cb3de7f9b7cc2a1a3e95843cf86f5a7d Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 12 Aug 2025 13:12:16 +0200 Subject: [PATCH] Add new components: BrickThrowComponent, BulletComponent, CageComponent, ChaseLevelComponent, CleanupComponent, and ThrowInputResource classes; implement game saving and loading logic in SaveSystem --- Autoloads/SaveSystem.cs | 27 +++++-- scripts/Resources/ChargeThrowInputResource.cs | 70 ++++++++++++++++ scripts/Resources/TapThrowInputResource.cs | 20 +++++ scripts/Resources/ThrowInputResource.cs | 24 ++++++ scripts/components/BrickThrowComponent.cs | 70 ++++++++++++++++ scripts/components/BulletComponent.cs | 45 +++++++++++ scripts/components/CageComponent.cs | 47 +++++++++++ scripts/components/ChaseLevelComponent.cs | 79 +++++++++++++++++++ scripts/components/CleanupComponent.cs | 11 +++ scripts/components/ShipMovementComponent.cs | 1 + scripts/interfaces/IMovement.cs | 1 + scripts/interfaces/IThrowInput.cs | 10 +++ 12 files changed, 399 insertions(+), 6 deletions(-) create mode 100644 scripts/Resources/ChargeThrowInputResource.cs create mode 100644 scripts/Resources/TapThrowInputResource.cs create mode 100644 scripts/Resources/ThrowInputResource.cs create mode 100644 scripts/components/BrickThrowComponent.cs create mode 100644 scripts/components/BulletComponent.cs create mode 100644 scripts/components/CageComponent.cs create mode 100644 scripts/components/ChaseLevelComponent.cs create mode 100644 scripts/components/CleanupComponent.cs create mode 100644 scripts/interfaces/IThrowInput.cs diff --git a/Autoloads/SaveSystem.cs b/Autoloads/SaveSystem.cs index 0e47221..d5ce18a 100644 --- a/Autoloads/SaveSystem.cs +++ b/Autoloads/SaveSystem.cs @@ -1,5 +1,6 @@ using Godot; using Godot.Collections; +using Mr.BrickAdventures.scripts.Resources; namespace Mr.BrickAdventures.Autoloads; @@ -8,22 +9,28 @@ public partial class SaveSystem : Node [Export] public string SavePath { get; set; } = "user://savegame.save"; [Export] public int Version { get; set; } = 1; - //private GM _gm; + private GameManager _gameManager; public override void _Ready() { - //_gm = GetNode("/root/GameManager"); + _gameManager = GetNode("/root/GameManager"); } public void SaveGame() { - //TODO: Implement saving logic + var saveData = new Dictionary + { + { "player_state", _gameManager.PlayerState}, + { "version", Version} + }; + + using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write); + file.StoreVar(saveData); + GD.Print("Game state saved to: ", SavePath); } public bool LoadGame() { - //TODO: Implement loading logic - if (!FileAccess.FileExists(SavePath)) return false; @@ -38,7 +45,15 @@ public partial class SaveSystem : Node GD.Print("Game state loaded from: ", SavePath); GD.Print("Player state: ", saveDataObj["player_state"]); - + _gameManager.PlayerState = (Dictionary)saveDataObj["player_state"]; + + var skills = new Array(); + foreach (var skill in (Array)_gameManager.PlayerState["unlocked_skills"]) + { + skills.Add(skill); + } + + _gameManager.UnlockSkills(skills); return true; } diff --git a/scripts/Resources/ChargeThrowInputResource.cs b/scripts/Resources/ChargeThrowInputResource.cs new file mode 100644 index 0000000..a781748 --- /dev/null +++ b/scripts/Resources/ChargeThrowInputResource.cs @@ -0,0 +1,70 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.Resources; + +public partial class ChargeThrowInputResource : ThrowInputResource +{ + [Export] public float MinPower { get; set; } = 0.5f; + [Export] public float MaxPower { get; set; } = 2.0f; + [Export] public float MaxChargeTime { get; set; } = 2.0f; + [Export] public float MinChargeDuration { get; set; } = 0.1f; + + private bool _isCharging = false; + private float _chargeStartTime = 0f; + + [Signal] public delegate void ChargeStartedEventHandler(); + [Signal] public delegate void ChargeUpdatedEventHandler(float chargeRatio); + [Signal] public delegate void ChargeStoppedEventHandler(); + + public override void ProcessInput(InputEvent @event) + { + if (@event.IsActionPressed("attack")) + { + _isCharging = true; + _chargeStartTime = Time.GetTicksMsec() / 1000f; + EmitSignalChargeStarted(); + } + + if (@event.IsActionReleased("attack") && _isCharging) + { + var power = CalculatePower(); + _isCharging = false; + EmitSignalThrowRequested(power); + EmitSignalChargeStopped(); + } + } + + public override void Update(double delta) + { + if (!_isCharging) return; + + var t = Mathf.Clamp(GetChargeRatio(), MinPower, MaxPower); + EmitSignalChargeUpdated(t); + } + + public override bool SupportsCharging() + { + return true; + } + + private float CalculatePower() + { + var now = Time.GetTicksMsec() / 1000f; + var heldTime = now - _chargeStartTime; + if (heldTime < MinChargeDuration) + return MinPower; + + var t = Mathf.Clamp(heldTime / MaxChargeTime, 0f, 1f); + return Mathf.Lerp(MinPower, MaxPower, t); + } + + private float GetChargeRatio() + { + if (!_isCharging) return MinPower; + + var now = Time.GetTicksMsec() / 1000f; + var heldTime = now - _chargeStartTime; + var t = Mathf.Clamp(heldTime / MaxChargeTime, 0f, 1f); + return Mathf.Lerp(MinPower, MaxPower, t); + } +} \ No newline at end of file diff --git a/scripts/Resources/TapThrowInputResource.cs b/scripts/Resources/TapThrowInputResource.cs new file mode 100644 index 0000000..97079e8 --- /dev/null +++ b/scripts/Resources/TapThrowInputResource.cs @@ -0,0 +1,20 @@ +using Godot; +using Mr.BrickAdventures.scripts.interfaces; + +namespace Mr.BrickAdventures.scripts.Resources; + +public partial class TapThrowInputResource : ThrowInputResource +{ + public override void Update(double delta) + { + if (Input.IsActionPressed("attack")) + { + EmitSignalThrowRequested(1f); + } + } + + public override bool SupportsCharging() + { + return false; + } +} \ No newline at end of file diff --git a/scripts/Resources/ThrowInputResource.cs b/scripts/Resources/ThrowInputResource.cs new file mode 100644 index 0000000..e7285e7 --- /dev/null +++ b/scripts/Resources/ThrowInputResource.cs @@ -0,0 +1,24 @@ +using Godot; +using Mr.BrickAdventures.scripts.interfaces; + +namespace Mr.BrickAdventures.scripts.Resources; + +public abstract partial class ThrowInputResource : Resource, IThrowInput +{ + [Signal] public delegate void ThrowRequestedEventHandler(float powerMultiplier = 1f); + + public virtual void ProcessInput(InputEvent @event) + { + throw new System.NotImplementedException(); + } + + public virtual void Update(double delta) + { + throw new System.NotImplementedException(); + } + + public virtual bool SupportsCharging() + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/scripts/components/BrickThrowComponent.cs b/scripts/components/BrickThrowComponent.cs new file mode 100644 index 0000000..e55f3cf --- /dev/null +++ b/scripts/components/BrickThrowComponent.cs @@ -0,0 +1,70 @@ +using Godot; +using Mr.BrickAdventures.scripts.interfaces; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class BrickThrowComponent : Node +{ + [Export] public PackedScene BrickScene { get; set; } + [Export] public float FireRate { get; set; } = 1.0f; + [Export] public PlayerController PlayerController { get; set; } + [Export] public ThrowInputResource ThrowInputBehavior { get; set; } + + private bool _canThrow = true; + private Timer _timer; + + public override void _Ready() + { + SetupTimer(); + _canThrow = true; + + if (ThrowInputBehavior != null) ThrowInputBehavior.ThrowRequested += ThrowBrick; + } + + public override void _Input(InputEvent @event) + { + ThrowInputBehavior?.ProcessInput(@event); + } + + public override void _Process(double delta) + { + ThrowInputBehavior?.Update(delta); + } + + private void SetupTimer() + { + _timer.WaitTime = FireRate; + _timer.OneShot = false; + _timer.Autostart = false; + _timer.Timeout += OnTimerTimeout; + } + + private void OnTimerTimeout() + { + _canThrow = true; + } + + private void ThrowBrick(float powerMultiplier = 1f) + { + if (!_canThrow || PlayerController == null || BrickScene == null) + return; + + var instance = BrickScene.Instantiate(); + var init = instance.GetNodeOrNull("ProjectileInitComponent"); + if (init != null && PlayerController.CurrentMovement is PlatformMovementComponent) + { + init.Initialize(new ProjectileInitParams() + { + Position = PlayerController.GlobalPosition, + Rotation = PlayerController.Rotation, + Direction = PlayerController.CurrentMovement.LastDirection, + PowerMultiplier = powerMultiplier, + }); + } + + GetTree().CurrentScene.AddChild(instance); + _canThrow = false; + _timer.Start(); + } +} \ No newline at end of file diff --git a/scripts/components/BulletComponent.cs b/scripts/components/BulletComponent.cs new file mode 100644 index 0000000..cd138e8 --- /dev/null +++ b/scripts/components/BulletComponent.cs @@ -0,0 +1,45 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class BulletComponent : Node +{ + [Export] public Area2D Area { get; set; } + [Export] public TerrainHitFx TerrainHitFx { get; set; } + [Export] public Sprite2D BulletSprite { get; set; } + + public override void _Ready() + { + Area.BodyEntered += OnBodyEntered; + } + + private void OnBodyEntered(Node2D body) + { + if (body is TileMapLayer) + { + if (BulletSprite != null) + { + BulletSprite.Visible = false; + } + + PlayTerrainHitFx(); + return; + } + + Owner.QueueFree(); + } + + private void OnAreaEntered(Area2D area) + { + Owner.QueueFree(); + } + + private void PlayTerrainHitFx() + { + if (TerrainHitFx == null) return; + + TerrainHitFx.TriggerFx(); + + Owner.QueueFree(); + } +} \ No newline at end of file diff --git a/scripts/components/CageComponent.cs b/scripts/components/CageComponent.cs new file mode 100644 index 0000000..b509104 --- /dev/null +++ b/scripts/components/CageComponent.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class CageComponent : Node +{ + [Export] public LeverComponent Lever { get; set; } + [Export] public Vector2 MoveValue { get; set; } = new(0, -100f); + [Export] public float TweenDuration { get; set; } = 0.5f; + [Export] public bool ShouldFree { get; set; } = true; + + private const string LeverGroupName = "levers"; + + public override async void _Ready() + { + await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame); + if (Lever == null) + { + var leverNodes = GetTree().GetNodesInGroup(LeverGroupName); + foreach (var leverNode in leverNodes) + { + var lever = leverNode.GetNodeOrNull("LeverComponent"); + if (lever != null) lever.Activated += OnLeverActivated; + } + } + } + + private void OnLeverActivated() + { + var tween = CreateTween(); + if (Owner is Node2D root) + { + var endPosition = root.Position + MoveValue; + tween.TweenProperty(root, "position", endPosition, TweenDuration); + } + + tween.TweenCallback(Callable.From(OnTweenCompleted)); + + } + + private void OnTweenCompleted() + { + if (!ShouldFree) return; + Owner.QueueFree(); + } +} \ No newline at end of file diff --git a/scripts/components/ChaseLevelComponent.cs b/scripts/components/ChaseLevelComponent.cs new file mode 100644 index 0000000..d22a499 --- /dev/null +++ b/scripts/components/ChaseLevelComponent.cs @@ -0,0 +1,79 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class ChaseLevelComponent : Node +{ + [Export] public float ChaseSpeed { get; set; } = 200.0f; + [Export] public Marker2D ChaseTarget { get; set; } + [Export] public GodotObject PhantomCamera { get; set; } + [Export] public float MinimumDistance { get; set; } = 10f; + + [Signal] + public delegate void ChaseStartedEventHandler(); + + [Signal] + public delegate void ChaseStoppedEventHandler(); + + private bool _isChasing = false; + private Node2D _previousCameraFollowTarget = null; + + public override void _Process(double delta) + { + if (!_isChasing) return; + if (ChaseTarget == null) return; + + if (CheckIfReachedTarget()) + { + StopChasing(); + return; + } + + var targetPosition = ChaseTarget.GlobalPosition; + + if (Owner is not Node2D root) return; + + var direction = (targetPosition - root.GlobalPosition).Normalized(); + root.GlobalPosition += direction * ChaseSpeed * (float)delta; + } + + public void OnShipEntered() + { + if (ChaseTarget == null || PhantomCamera == null) + return; + + if (_isChasing) return; + + _previousCameraFollowTarget = (Node2D)PhantomCamera.Call("get_follow_target"); + PhantomCamera.Call("set_follow_target", Owner as Node2D); + EmitSignalChaseStarted(); + _isChasing = true; + } + + public void OnShipExited() + { + StopChasing(); + } + + private bool CheckIfReachedTarget() + { + if (ChaseTarget == null) + return false; + + if (Owner is not Node2D root) return false; + + var targetPosition = ChaseTarget.GlobalPosition; + var currentPosition = root.GlobalPosition; + return currentPosition.DistanceTo(targetPosition) <= MinimumDistance; + + } + + private void StopChasing() + { + if (PhantomCamera == null) return; + + PhantomCamera.Call("set_follow_target", _previousCameraFollowTarget); + EmitSignalChaseStopped(); + _isChasing = false; + } +} \ No newline at end of file diff --git a/scripts/components/CleanupComponent.cs b/scripts/components/CleanupComponent.cs new file mode 100644 index 0000000..577291e --- /dev/null +++ b/scripts/components/CleanupComponent.cs @@ -0,0 +1,11 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class CleanupComponent : Node +{ + public void CleanUp() + { + Owner.QueueFree(); + } +} \ No newline at end of file diff --git a/scripts/components/ShipMovementComponent.cs b/scripts/components/ShipMovementComponent.cs index 547da4c..8ac70ac 100644 --- a/scripts/components/ShipMovementComponent.cs +++ b/scripts/components/ShipMovementComponent.cs @@ -17,6 +17,7 @@ public partial class ShipMovementComponent : Node, IMovement private Vector2 _velocity = Vector2.Zero; public Vector2 Velocity => _velocity; + public Vector2 LastDirection => _velocity.Normalized(); public override void _PhysicsProcess(double delta) { diff --git a/scripts/interfaces/IMovement.cs b/scripts/interfaces/IMovement.cs index ece7b69..f679885 100644 --- a/scripts/interfaces/IMovement.cs +++ b/scripts/interfaces/IMovement.cs @@ -7,6 +7,7 @@ public interface IMovement string MovementType { get; } bool Enabled { get; set; } Vector2 PreviousVelocity { get; set; } + Vector2 LastDirection { get; } void _Process(double delta); void _PhysicsProcess(double delta); diff --git a/scripts/interfaces/IThrowInput.cs b/scripts/interfaces/IThrowInput.cs new file mode 100644 index 0000000..d0507a2 --- /dev/null +++ b/scripts/interfaces/IThrowInput.cs @@ -0,0 +1,10 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.interfaces; + +public interface IThrowInput +{ + public void ProcessInput(InputEvent @event); + public void Update(double delta); + public bool SupportsCharging(); +} \ No newline at end of file