From f3aa2631f233fdb0f8fcba3b4e178b9644aefcf7 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 10 Aug 2025 22:38:19 +0200 Subject: [PATCH] Add new components: HealComponent, HitComponent, HomingMissileMotionComponent, LeverComponent, and TriggerLeverComponent --- scripts/components/HealComponent.cs | 48 ++++++++++++ scripts/components/HitComponent.cs | 75 +++++++++++++++++++ .../HomingMissileMotionComponent.cs | 64 ++++++++++++++++ scripts/components/LeverComponent.cs | 66 ++++++++++++++++ scripts/components/TriggerLeverComponent.cs | 8 ++ 5 files changed, 261 insertions(+) create mode 100644 scripts/components/HealComponent.cs create mode 100644 scripts/components/HitComponent.cs create mode 100644 scripts/components/HomingMissileMotionComponent.cs create mode 100644 scripts/components/LeverComponent.cs create mode 100644 scripts/components/TriggerLeverComponent.cs diff --git a/scripts/components/HealComponent.cs b/scripts/components/HealComponent.cs new file mode 100644 index 0000000..897a6a4 --- /dev/null +++ b/scripts/components/HealComponent.cs @@ -0,0 +1,48 @@ +using Godot; +using Mr.BrickAdventures.scripts.Resources; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class HealComponent : Node +{ + [Export] public GpuParticles2D HealFx { get; set; } + [Export] public CollectableComponent Collectable { get; set; } + + public override void _Ready() + { + if (Collectable == null) + { + GD.PushError("HealComponent: Collectable is not set."); + return; + } + + Collectable.Collected += OnCollected; + } + + private void OnCollected(Variant amount, CollectableType type, Node2D body) + { + if (type != CollectableType.Health) return; + + if (Collectable == null) return; + + var healthComponent = body.GetNodeOrNull("HealthComponent"); + if (healthComponent == null) return; + + var value = amount.AsSingle(); + healthComponent.IncreaseHealth(value); + if (HealFx != null) + { + PlayHealFx(); + } + + Owner.QueueFree(); + } + + private void PlayHealFx() + { + if (HealFx == null) return; + + HealFx.Restart(); + HealFx.Emitting = true; + } +} \ No newline at end of file diff --git a/scripts/components/HitComponent.cs b/scripts/components/HitComponent.cs new file mode 100644 index 0000000..d64f10e --- /dev/null +++ b/scripts/components/HitComponent.cs @@ -0,0 +1,75 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class HitComponent : Node +{ + [Export] public Sprite2D Sprite { get; set; } + [Export] public HealthComponent Health { get; set; } + [Export] public float HitDuration { get; set; } = 0.1f; + [Export] public GpuParticles2D HitFx { get; set; } + [Export] public bool FlashMode { get; set; } = true; + + public override void _Ready() + { + if (Health != null) + { + Health.HealthChanged += OnHealthChange; + Health.Death += OnDeath; + } + + if (Sprite == null) + { + GD.PushError("HitComponent: Sprite is null"); + return; + } + + if (Sprite.Material != null && FlashMode) + { + Sprite.Material = (Material)Sprite.Material.Duplicate(); + } + } + + private void Activate() + { + if (!FlashMode) return; + + Sprite.SetInstanceShaderParameter("enabled", true); + } + + private void Deactivate() + { + if (!FlashMode) return; + + Sprite.SetInstanceShaderParameter("enabled", false); + } + + private async void OnHealthChange(float delta, float totalHealth) + { + if (!(delta < 0f)) return; + + Activate(); + await ToSignal(GetTree().CreateTimer(HitDuration), Timer.SignalName.Timeout); + Deactivate(); + + if (totalHealth > 0f && delta < 0f) + { + HandleHitFx(); + } + } + + private async void OnDeath() + { + Activate(); + await ToSignal(GetTree().CreateTimer(HitDuration), Timer.SignalName.Timeout); + Deactivate(); + } + + private void HandleHitFx() + { + if (HitFx == null) return; + + HitFx.Restart(); + HitFx.Emitting = true; + } +} \ No newline at end of file diff --git a/scripts/components/HomingMissileMotionComponent.cs b/scripts/components/HomingMissileMotionComponent.cs new file mode 100644 index 0000000..bd41dd8 --- /dev/null +++ b/scripts/components/HomingMissileMotionComponent.cs @@ -0,0 +1,64 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class HomingMissileMotionComponent : Node +{ + [Export] public LaunchComponent Launch { get; set; } + [Export] public float MaxSpeed { get; set; } = 16f; + [Export] public float Acceleration { get; set; } = 8f; + [Export] public float MaxTurnRate { get; set; } = 180f; // degrees per second + [Export] public float WobbleStrength { get; set; } = 5f; // degrees + [Export] public float Drag { get; set; } = 0.98f; + [Export] public float SteeringLerp { get; set; } = 0.05f; // low = sluggish, high = responsive + [Export] public Area2D DetectionArea { get; set; } + + private Vector2 _steeringDirection = Vector2.Zero; + private Vector2 _velocity = Vector2.Zero; + private Node2D _target = null; + + public override void _Ready() + { + DetectionArea.BodyEntered += OnBodyEntered; + _velocity = Launch.GetInitialVelocity(); + } + + public override void _PhysicsProcess(double delta) + { + if (Launch == null) return; + if (Owner is not Node2D owner) return; + if (_target == null) + { + owner.Position += _velocity * (float)delta; + return; + } + + var toTarget = (_target.GlobalPosition - owner.GlobalPosition).Normalized(); + _steeringDirection = _steeringDirection.Lerp(toTarget, SteeringLerp); + + var angleToTarget = _velocity.AngleTo(_steeringDirection); + var maxAngle = Mathf.DegToRad(MaxTurnRate) * (float)delta; + var clampedAngle = Mathf.Clamp(angleToTarget, -maxAngle, maxAngle); + + var rng = new RandomNumberGenerator(); + var wobble = Mathf.DegToRad(rng.RandfRange(-WobbleStrength, WobbleStrength)); + clampedAngle += wobble; + + _velocity = _velocity.Rotated(clampedAngle); + _velocity *= Drag; + + var desiredSpeed = Mathf.Min(MaxSpeed, _velocity.Length() + Acceleration * (float)delta); + _velocity = _velocity.Normalized() * desiredSpeed; + + owner.Position += _velocity * (float)delta; + owner.Rotation = _velocity.Angle(); + } + + private void OnBodyEntered(Node2D body) + { + if (_target != null) return; + if (body == null) return; + + _target = body; + } +} \ No newline at end of file diff --git a/scripts/components/LeverComponent.cs b/scripts/components/LeverComponent.cs new file mode 100644 index 0000000..5c059ce --- /dev/null +++ b/scripts/components/LeverComponent.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class LeverComponent : Node +{ + [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; + [Export] public AudioStreamPlayer2D Sfx { get; set; } + + [Signal] + public delegate void ActivatedEventHandler(); + + public override void _Ready() + { + if (Area == null) + { + GD.PushError("LeverComponent: Area is not set."); + return; + } + + if (Sprite == null) + { + GD.PushError("LeverComponent: Sprite is not set."); + return; + } + + Area.BodyEntered += OnBodyEntered; + Area.AreaEntered += OnAreaEntered; + } + + private void OnAreaEntered(Area2D area) + { + HandleTriggerLogic(area); + } + + private void OnBodyEntered(Node2D body) + { + HandleTriggerLogic(body); + } + + private async Task Activate() + { + EmitSignalActivated(); + Sfx?.Play(); + Sprite.Frame = StartAnimationIndex + 1; + var timer = GetTree().CreateTimer(AnimationDuration); + await timer.ToSignal(timer, Timer.SignalName.Timeout); + Sprite.Frame = StartAnimationIndex; + } + + private void HandleTriggerLogic(Node2D obj) + { + var triggerLever = obj.GetNodeOrNull("TriggerLeverComponent"); + if (triggerLever == null) + { + GD.PushWarning("LeverComponent: TriggerLeverComponent not found in body."); + return; + } + + _ = Activate(); + } +} \ No newline at end of file diff --git a/scripts/components/TriggerLeverComponent.cs b/scripts/components/TriggerLeverComponent.cs new file mode 100644 index 0000000..50ca915 --- /dev/null +++ b/scripts/components/TriggerLeverComponent.cs @@ -0,0 +1,8 @@ +using Godot; + +namespace Mr.BrickAdventures.scripts.components; + +public partial class TriggerLeverComponent : Node +{ + +} \ No newline at end of file