diff --git a/Autoloads/ConfigFileHandler.cs b/Autoloads/ConfigFileHandler.cs
index 5799faf..67acdae 100644
--- a/Autoloads/ConfigFileHandler.cs
+++ b/Autoloads/ConfigFileHandler.cs
@@ -5,7 +5,9 @@ namespace Mr.BrickAdventures.Autoloads;
public partial class ConfigFileHandler : Node
{
private ConfigFile _settingsConfig = new();
- private const string SettingsPath = "user://settings.ini";
+ public const string SettingsPath = "user://settings.ini";
+
+ public ConfigFile SettingsConfig => _settingsConfig;
public override void _Ready()
{
diff --git a/Mr. Brick Adventures.csproj b/Mr. Brick Adventures.csproj
index 3fcf8b5..35922f6 100644
--- a/Mr. Brick Adventures.csproj
+++ b/Mr. Brick Adventures.csproj
@@ -92,6 +92,7 @@
+
diff --git a/scripts/UI/AudioSettings.cs b/scripts/UI/AudioSettings.cs
new file mode 100644
index 0000000..0d605b3
--- /dev/null
+++ b/scripts/UI/AudioSettings.cs
@@ -0,0 +1,102 @@
+using Godot;
+using Mr.BrickAdventures.Autoloads;
+
+namespace Mr.BrickAdventures.scripts.UI;
+
+public partial class AudioSettings : Node
+{
+ [Export] public Slider MasterVolumeSlider { get; set; }
+ [Export] public Slider MusicVolumeSlider { get; set; }
+ [Export] public Slider SfxVolumeSlider { get; set; }
+ [Export] public Control AudioSettingsControl { get; set; }
+ [Export] public float MuteThreshold { get; set; } = -20f;
+
+ private UIManager _uiManager;
+ private ConfigFileHandler _configFileHandler;
+
+ public override void _Ready()
+ {
+ _uiManager = GetNode("/root/UIManager");
+ _configFileHandler = GetNode("/root/ConfigFileHandler");
+ Initialize();
+ MasterVolumeSlider.ValueChanged += OnMasterVolumeChanged;
+ MusicVolumeSlider.ValueChanged += OnMusicVolumeChanged;
+ SfxVolumeSlider.ValueChanged += OnSfxVolumeChanged;
+ }
+
+ public override void _UnhandledInput(InputEvent @event)
+ {
+ if (!@event.IsActionReleased("ui_cancel")) return;
+ if (!_uiManager.IsScreenOnTop(AudioSettingsControl)) return;
+
+ SaveSettings();
+ _uiManager.PopScreen();
+ }
+
+ private void OnSfxVolumeChanged(double value)
+ {
+ AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("sfx"), (float)value);
+ HandleMute(AudioServer.GetBusIndex("sfx"), (float)value);
+ }
+
+ private void OnMusicVolumeChanged(double value)
+ {
+ AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("music"), (float)value);
+ HandleMute(AudioServer.GetBusIndex("music"), (float)value);
+ }
+
+ private void OnMasterVolumeChanged(double value)
+ {
+ AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("Master"), (float)value);
+ HandleMute(AudioServer.GetBusIndex("Master"), (float)value);
+ }
+
+ private void Initialize()
+ {
+ var volumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("Master"));
+ MasterVolumeSlider.Value = volumeDb;
+ MasterVolumeSlider.MinValue = MuteThreshold;
+ MasterVolumeSlider.MaxValue = 0f;
+
+ var musicVolumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("music"));
+ MusicVolumeSlider.Value = musicVolumeDb;
+ MusicVolumeSlider.MinValue = MuteThreshold;
+ MusicVolumeSlider.MaxValue = 0f;
+
+ var sfxVolumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("sfx"));
+ SfxVolumeSlider.Value = sfxVolumeDb;
+ SfxVolumeSlider.MinValue = MuteThreshold;
+ SfxVolumeSlider.MaxValue = 0f;
+ }
+
+ private void HandleMute(int busIndex, float value)
+ {
+ AudioServer.SetBusMute(busIndex, value <= MuteThreshold);
+ }
+
+ private void SaveSettings()
+ {
+ var settingsConfig = _configFileHandler.SettingsConfig;
+ settingsConfig.SetValue("audio_settings", "master_volume", MasterVolumeSlider.Value);
+ settingsConfig.SetValue("audio_settings", "music_volume", MusicVolumeSlider.Value);
+ settingsConfig.SetValue("audio_settings", "sfx_volume", SfxVolumeSlider.Value);
+ settingsConfig.SetValue("audio_settings", "mute_threshold", MuteThreshold);
+ settingsConfig.Save(ConfigFileHandler.SettingsPath);
+ }
+
+ private void LoadSettings()
+ {
+ var settingsConfig = _configFileHandler.SettingsConfig;
+ if (!settingsConfig.HasSection("audio_settings")) return;
+
+ var masterVolume = (float)settingsConfig.GetValue("audio_settings", "master_volume", MasterVolumeSlider.Value);
+ var musicVolume = (float)settingsConfig.GetValue("audio_settings", "music_volume", MusicVolumeSlider.Value);
+ var sfxVolume = (float)settingsConfig.GetValue("audio_settings", "sfx_volume", SfxVolumeSlider.Value);
+ var muteThreshold = (float)settingsConfig.GetValue("audio_settings", "mute_threshold", MuteThreshold);
+
+ MasterVolumeSlider.Value = masterVolume;
+ MusicVolumeSlider.Value = musicVolume;
+ SfxVolumeSlider.Value = sfxVolume;
+ MuteThreshold = muteThreshold;
+ }
+}
\ No newline at end of file
diff --git a/scripts/components/platform_movement.gd b/scripts/components/platform_movement.gd
new file mode 100644
index 0000000..38773da
--- /dev/null
+++ b/scripts/components/platform_movement.gd
@@ -0,0 +1,118 @@
+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