Add core game components including ConfigFileHandler, GameManager, SaveSystem, and UIManager

This commit is contained in:
2025-08-10 01:35:35 +02:00
parent 4326ca850d
commit b54d886145
31 changed files with 1347 additions and 2 deletions

13
scripts/components/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.components.iml
/contentModel.xml
/modules.xml
/projectSettingsUpdater.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

4
scripts/components/.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
scripts/components/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,50 @@
using System;
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class CollectableComponent : Node
{
private bool _hasFadeAway = false;
[Export] public Area2D Area2D { get; set; }
[Export] public CollisionShape2D CollisionShape { get; set; }
[Export] public CollectableResource Data { get; set; }
[Export] public AudioStreamPlayer2D Sfx {get; set; }
[Signal] public delegate void CollectedEventHandler(Variant amount, CollectableType type, Node2D body);
public override void _Ready()
{
if (Area2D != null)
Area2D.BodyEntered += OnArea2DBodyEntered;
else
GD.PushError("Collectable node missing Area2D node.");
if (Owner.HasNode("FadeAwayComponent"))
_hasFadeAway = true;
}
private async void OnArea2DBodyEntered(Node2D body)
{
try
{
if (!body.HasNode("CanPickUpComponent")) return;
EmitSignalCollected(Data.Amount, Data.Type, body);
CollisionShape?.CallDeferred("set_disabled", true);
Sfx?.Play();
if (_hasFadeAway) return;
if (Sfx != null)
await ToSignal(Sfx, AudioStreamPlayer2D.SignalName.Finished);
Owner.QueueFree();
}
catch (Exception e)
{
GD.PushError($"Error in CollectableComponent.OnArea2DBodyEntered: {e.Message}");
}
}
}

View File

@@ -0,0 +1,105 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class DamageComponent : Node
{
[Export] public float Damage { get; set; } = 0.25f;
[Export] public Area2D Area { get; set; }
[Export] public StatusEffectDataResource StatusEffectData { get; set; }
[Export] public Timer DamageTimer { get; set; }
private Node _currentTarget = null;
[Signal] public delegate void EffectInflictedEventHandler(Node2D target, StatusEffectDataResource effect);
public override void _Ready()
{
if (Area == null)
{
GD.PushError($"DamageComponent: Area2D node is not set.");
return;
}
Area.BodyEntered += OnAreaBodyEntered;
Area.BodyExited += OnAreaBodyExited;
Area.AreaEntered += OnAreaAreaEntered;
if (DamageTimer != null)
{
DamageTimer.Timeout += OnDamageTimerTimeout;
}
}
public override void _Process(double delta)
{
if (_currentTarget == null) return;
if (DamageTimer != null) return;
ProcessEntityAndApplyDamage(_currentTarget as Node2D);
}
public void DealDamage(HealthComponent target) => target.DecreaseHealth(Damage);
private void OnAreaAreaEntered(Area2D area)
{
if (!CheckIfProcessingIsOn())
return;
if (area == Area) return;
var parent = area.GetParent();
if (parent.HasNode("DamageComponent"))
ProcessEntityAndApplyDamage(parent as Node2D);
}
private void OnAreaBodyExited(Node2D body)
{
if (body != _currentTarget) return;
_currentTarget = null;
DamageTimer?.Stop();
}
private void OnAreaBodyEntered(Node2D body)
{
_currentTarget = body;
if (!CheckIfProcessingIsOn())
return;
DamageTimer?.Start();
ProcessEntityAndApplyDamage(body);
}
private void OnDamageTimerTimeout()
{
if (_currentTarget == null) return;
ProcessEntityAndApplyDamage(_currentTarget as Node2D);
}
private void ProcessEntityAndApplyDamage(Node2D body)
{
if (body == null) return;
if (!body.HasNode("HealthComponent")) return;
var health = body.GetNode<HealthComponent>("HealthComponent");
var inv = body.GetNodeOrNull<InvulnerabilityComponent>("InvulnerabilityComponent");
if (inv != null && inv.IsInvulnerable())
return;
if (StatusEffectData != null && StatusEffectData.Type != StatusEffectType.None)
EmitSignalEffectInflicted(body, StatusEffectData);
DealDamage(health);
inv?.Activate();
}
private bool CheckIfProcessingIsOn()
{
return ProcessMode is ProcessModeEnum.Inherit or ProcessModeEnum.Always;
}
}

View File

@@ -0,0 +1,83 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class FlashingComponent : Node
{
[Export] public Node2D Sprite { get; set; }
[Export] public float FlashDuration { get; set; } = 0.5f;
[Export] public float FlashTime { get; set; } = 0.1f;
[Export] public bool UseModulate { get; set; } = true;
[Export] public HealthComponent HealthComponent { get; set; }
private Tween _tween;
public override void _Ready()
{
if (HealthComponent != null)
{
HealthComponent.HealthChanged += OnHealthChanged;
HealthComponent.Death += OnDeath;
}
if (Sprite == null)
{
GD.PushError("FlashingComponent: Sprite node is not set.");
return;
}
}
public void StartFlashing()
{
if (Sprite == null) return;
_tween?.Kill();
_tween = CreateTween();
_tween.SetParallel(true);
var flashes = (int)(FlashDuration / FlashTime);
for (var i = 0; i < flashes; i++)
{
if (UseModulate)
{
var opacity = i % 2 == 0 ? 1.0f : 0.3f;
_tween.TweenProperty(Sprite, "modulate:a", opacity, FlashTime);
}
else
{
var visible = i % 2 == 0;
_tween.TweenProperty(Sprite, "visible", visible, FlashTime);
}
}
_tween.TweenCallback(Callable.From(StopFlashing));
}
public void StopFlashing()
{
if (UseModulate)
{
var modulateColor = Sprite.GetModulate();
modulateColor.A = 1.0f;
Sprite.SetModulate(modulateColor);
}
else
{
Sprite.SetVisible(true);
}
}
private void OnHealthChanged(float delta, float totalHealth)
{
if (delta < 0f)
{
StartFlashing();
}
}
private void OnDeath()
{
StopFlashing();
}
}

View File

@@ -0,0 +1,65 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class HealthComponent : Node
{
[Export] public float Health { get; set; } = 1.0f;
[Export] public float MaxHealth { get; set; } = 1.0f;
[Export] public AudioStreamPlayer2D HurtSfx { get; set; }
[Export] public AudioStreamPlayer2D HealSfx { get; set; }
[Signal] public delegate void HealthChangedEventHandler(float delta, float totalHealth);
[Signal] public delegate void DeathEventHandler();
public void SetHealth(float newValue)
{
_ = ApplyHealthChange(newValue);
}
public void IncreaseHealth(float delta)
{
_ = ApplyHealthChange(Health + delta);
}
public void DecreaseHealth(float delta)
{
_ = ApplyHealthChange(Health - delta);
}
public float GetDelta(float newValue) => newValue - Health;
private async Task ApplyHealthChange(float newHealth, bool playSfx = true)
{
newHealth = Mathf.Clamp(newHealth, 0.0f, MaxHealth);
var delta = newHealth - Health;
if (delta == 0.0f)
return;
if (playSfx)
{
if (delta > 0f && HealSfx != null)
{
HealSfx.Play();
}
else if (delta < 0f && HurtSfx != null)
{
HurtSfx.Play();
await HurtSfx.ToSignal(HurtSfx, AudioStreamPlayer2D.SignalName.Finished);
}
}
Health = newHealth;
if (Health <= 0f)
{
EmitSignalDeath();
}
else
{
EmitSignalHealthChanged(delta, Health);
}
}
}

View File

@@ -0,0 +1,31 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class InvulnerabilityComponent : Node
{
[Export] public float Duration { get; set; } = 1f;
[Export] public FlashingComponent FlashingComponent { get; set; }
private bool _isInvulnerable = false;
public void Activate()
{
if (_isInvulnerable)
return;
_isInvulnerable = true;
FlashingComponent?.StartFlashing();
var timer = GetTree().CreateTimer(Duration);
timer.Timeout += Deactivate;
}
private void Deactivate()
{
_isInvulnerable = false;
FlashingComponent?.StopFlashing();
}
public bool IsInvulnerable() => _isInvulnerable;
}

View File

@@ -0,0 +1,169 @@
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlatformMovement : Node2D, IMovement
{
[Export]
public float Speed { get; set; } = 300.0f;
[Export]
public float JumpHeight { get; set; } = 100f;
[Export]
public float JumpTimeToPeak { get; set; } = 0.5f;
[Export]
public float JumpTimeToDescent { get; set; } = 0.4f;
[Export]
public int CoyoteFrames { get; set; } = 6;
[Export]
public AudioStreamPlayer2D JumpSfx { get; set; }
[Export]
public Node2D RotationTarget { get; set; }
[Export]
public CharacterBody2D Body { get; set; }
private float _gravity;
private bool _wasLastFloor = false;
private bool _coyoteMode = false;
private Timer _coyoteTimer;
private Vector2 _lastDirection = new Vector2(1, 0);
private float _jumpVelocity;
private float _jumpGravity;
private float _fallGravity;
public override void _Ready()
{
base._Ready();
if (Body == null)
return;
_gravity = (float)ProjectSettings.GetSetting("physics/2d/default_gravity");
_jumpVelocity = ((2.0f * JumpHeight) / JumpTimeToPeak) * -1.0f;
_jumpGravity = ((-2.0f * JumpHeight) / (JumpTimeToPeak * JumpTimeToPeak)) * -1.0f;
_fallGravity = ((-2.0f * JumpHeight) / (JumpTimeToDescent * JumpTimeToDescent)) * -1.0f;
_coyoteTimer = new Timer
{
OneShot = true,
WaitTime = CoyoteFrames / 60.0f
};
_coyoteTimer.Timeout += OnCoyoteTimerTimeout;
AddChild(_coyoteTimer);
}
public string MovementType { get; } = "platform";
public bool Enabled { get; set; }
public Vector2 PreviousVelocity { get; set; }
public override void _Process(double delta)
{
base._Process(delta);
if (Body == null || !Enabled)
return;
if (Body.Velocity.X > 0.0f)
RotationTarget.Rotation = Mathf.DegToRad(-10);
else if (Body.Velocity.X < 0.0f)
RotationTarget.Rotation = Mathf.DegToRad(10);
else
RotationTarget.Rotation = 0;
CalculateJumpVars();
}
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
if (Body == null || !Enabled)
return;
if (Body.IsOnFloor())
{
_wasLastFloor = true;
_coyoteMode = false; // Reset coyote mode when back on the floor
_coyoteTimer.Stop(); // Stop timer when grounded
}
else
{
if (_wasLastFloor) // Start coyote timer only once
{
_coyoteMode = true;
_coyoteTimer.Start();
}
_wasLastFloor = false;
}
if (!Body.IsOnFloor())
Body.Velocity += new Vector2(0, CalculateGravity()) * (float)delta;
if (Input.IsActionPressed("jump") && (Body.IsOnFloor() || _coyoteMode))
Jump();
if (Input.IsActionJustPressed("down"))
Body.Position += new Vector2(0, 1);
float direction = Input.GetAxis("left", "right");
if (direction != 0)
_lastDirection = HandleDirection(direction);
if (direction != 0)
Body.Velocity = new Vector2(direction * Speed, Body.Velocity.Y);
else
Body.Velocity = new Vector2(Mathf.MoveToward(Body.Velocity.X, 0, Speed), Body.Velocity.Y);
Body.MoveAndSlide();
}
private void Jump()
{
if (Body == null)
return;
Body.Velocity = new Vector2(Body.Velocity.X, _jumpVelocity);
_coyoteMode = false;
if (JumpSfx != null)
JumpSfx.Play();
}
private float CalculateGravity()
{
return Body.Velocity.Y < 0.0f ? _jumpGravity : _fallGravity;
}
private void OnCoyoteTimerTimeout()
{
_coyoteMode = false;
}
private Vector2 HandleDirection(float inputDir)
{
if (inputDir > 0)
return new Vector2(1, 0);
else if (inputDir < 0)
return new Vector2(-1, 0);
return _lastDirection;
}
public void OnShipEntered()
{
RotationTarget.Rotation = 0;
}
private void CalculateJumpVars()
{
_jumpVelocity = ((2.0f * JumpHeight) / JumpTimeToPeak) * -1.0f;
_jumpGravity = ((-2.0f * JumpHeight) / (JumpTimeToPeak * JumpTimeToPeak)) * -1.0f;
_fallGravity = ((-2.0f * JumpHeight) / (JumpTimeToDescent * JumpTimeToDescent)) * -1.0f;
}
}

View File

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

View File

@@ -0,0 +1,103 @@
using System.Collections.Generic;
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlayerController : Node2D
{
[Export]
public string DefaultMovementType { get; set; } = "platform";
[Export]
public Godot.Collections.Dictionary<string, NodePath> MovementTypes { get; set; }
[Export]
public Sprite2D ShipSprite { get; set; }
private IMovement _currentMovement = null;
[Signal]
public delegate void MovementSwitchedEventHandler(string movementType);
public override void _Ready()
{
base._Ready();
foreach (var movementType in MovementTypes.Keys)
{
var movementNode = GetNodeOrNull(movementType);
if (movementNode is IMovement playerMovement)
{
playerMovement.Enabled = false;
}
}
SwitchMovement(DefaultMovementType);
}
public override void _UnhandledInput(InputEvent @event)
{
base._UnhandledInput(@event);
if (@event is InputEventKey inputEventKey && inputEventKey.IsActionPressed("switch_movement"))
{
var nextMovementType = GetNextMovementType();
SwitchMovement(nextMovementType);
}
}
private void SwitchMovement(string movementType)
{
if (_currentMovement != null)
{
_currentMovement.Enabled = false;
}
if (MovementTypes.TryGetValue(movementType, out var movement))
{
_currentMovement = GetNodeOrNull<IMovement>(movement);
if (_currentMovement == null)
{
GD.PushError($"Movement type '{movementType}' not found in MovementTypes.");
return;
}
_currentMovement.Enabled = true;
EmitSignalMovementSwitched(movementType);
}
else
{
GD.PushError($"Movement type '{movementType}' not found in MovementTypes.");
}
if (_currentMovement == null)
{
GD.PushError("No current movement set after switching.");
}
}
private string GetNextMovementType()
{
var keys = new List<string>(MovementTypes.Keys);
var currentIndex = keys.IndexOf(_currentMovement?.MovementType);
if (currentIndex == -1)
{
return DefaultMovementType;
}
currentIndex = (currentIndex + 1) % keys.Count;
return keys[currentIndex];
}
public void OnSpaceshipEntered()
{
SwitchMovement("ship");
ShipSprite.Visible = true;
}
public void OnSpaceshipExited()
{
SwitchMovement(DefaultMovementType);
ShipSprite.Visible = false;
}
}

View File

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