From 6b0c41bbfdbbaa1fce1ece963ac47ee26995a8e7 Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Fri, 17 Apr 2026 12:22:27 +0200 Subject: [PATCH] fix(core): resolve review findings in Sprint 3-4 - Fix SaveManager reset_game to use DirAccess.remove correctly - Add null check for FileAccess.open in save_game - Fix AudioManager crossfade tween callback chain - Replace fragile absolute HUD path with relative onready - Guard character position save against empty id - Add GameState.has_character_position helper - Emit room_changed signal after tween completes - Add target_floor validation in ElevatorButton - Persist audio settings via GameState - Add class_name to RoomNavigator, AudioManager, HUD Co-Authored-By: Claude Sonnet 4.6 --- scripts/autoload/AudioManager.gd | 10 +++++----- scripts/autoload/GameState.gd | 12 ++++++++++++ scripts/autoload/SaveManager.gd | 5 ++++- scripts/characters/character.gd | 2 ++ scripts/main/main.gd | 7 ++++--- scripts/objects/elevator_button.gd | 2 ++ scripts/systems/hud.gd | 4 ++-- scripts/systems/room_navigator.gd | 6 +++--- scripts/systems/settings_menu.gd | 2 ++ 9 files changed, 36 insertions(+), 14 deletions(-) diff --git a/scripts/autoload/AudioManager.gd b/scripts/autoload/AudioManager.gd index da09072..c8e6dcb 100644 --- a/scripts/autoload/AudioManager.gd +++ b/scripts/autoload/AudioManager.gd @@ -1,5 +1,5 @@ ## AudioManager — music playback with cross-fade, SFX playback, volume control. -extends Node +class_name AudioManager extends Node const DEFAULT_MUSIC_VOLUME: float = 0.6 const CROSSFADE_DURATION: float = 1.0 @@ -31,13 +31,13 @@ func play_music(stream: AudioStream) -> void: next_player.stream = stream next_player.volume_db = linear_to_db(0.0) next_player.play() + var prev_player: AudioStreamPlayer = _active_player + _active_player = next_player var tween: Tween = create_tween() tween.set_parallel(true) - tween.tween_property(_active_player, "volume_db", linear_to_db(0.0), CROSSFADE_DURATION) + tween.tween_property(prev_player, "volume_db", linear_to_db(0.0), CROSSFADE_DURATION) tween.tween_property(next_player, "volume_db", linear_to_db(_music_volume), CROSSFADE_DURATION) - var prev_player: AudioStreamPlayer = _active_player - tween.tween_callback(prev_player.stop).set_delay(CROSSFADE_DURATION) - _active_player = next_player + tween.chain().tween_callback(prev_player.stop) func play_sfx(stream: AudioStream) -> void: diff --git a/scripts/autoload/GameState.gd b/scripts/autoload/GameState.gd index ba86893..e06b95e 100644 --- a/scripts/autoload/GameState.gd +++ b/scripts/autoload/GameState.gd @@ -7,6 +7,12 @@ signal character_moved(character_id: String, position: Vector2) var _character_positions: Dictionary = {} var _object_states: Dictionary = {} var current_room: String = "reception" +var music_volume: float = 0.6 +var sfx_volume: float = 1.0 + + +func has_character_position(id: String) -> bool: + return _character_positions.has(id) func get_character_position(id: String) -> Vector2: @@ -33,6 +39,8 @@ func get_save_data() -> Dictionary: "character_positions": _character_positions, "object_states": _object_states, "current_room": current_room, + "music_volume": music_volume, + "sfx_volume": sfx_volume, } @@ -43,3 +51,7 @@ func apply_save_data(data: Dictionary) -> void: _object_states = data["object_states"] if data.has("current_room"): current_room = data["current_room"] + if data.has("music_volume"): + music_volume = data["music_volume"] + if data.has("sfx_volume"): + sfx_volume = data["sfx_volume"] diff --git a/scripts/autoload/SaveManager.gd b/scripts/autoload/SaveManager.gd index d567534..5c2256b 100644 --- a/scripts/autoload/SaveManager.gd +++ b/scripts/autoload/SaveManager.gd @@ -12,6 +12,7 @@ func save_game() -> void: var data: Dictionary = GameState.get_save_data() var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE) if file == null: + push_error("SaveManager: cannot open save file for writing") return file.store_string(JSON.stringify(data)) file.close() @@ -32,7 +33,9 @@ func load_game() -> void: func reset_game() -> void: if FileAccess.file_exists(SAVE_PATH): - DirAccess.remove_absolute(SAVE_PATH) + var dir: DirAccess = DirAccess.open("user://") + if dir != null: + dir.remove("savegame.json") GameState.apply_save_data({}) diff --git a/scripts/characters/character.gd b/scripts/characters/character.gd index 15d5b71..bbf6e85 100644 --- a/scripts/characters/character.gd +++ b/scripts/characters/character.gd @@ -61,5 +61,7 @@ func _on_drag_picked_up(_pos: Vector2) -> void: func _on_drag_released(pos: Vector2) -> void: _is_held = false + if data == null or data.id.is_empty(): + return GameState.set_character_position(character_id, global_position) character_placed.emit(self, global_position) diff --git a/scripts/main/main.gd b/scripts/main/main.gd index 6bd5426..00d1062 100644 --- a/scripts/main/main.gd +++ b/scripts/main/main.gd @@ -5,12 +5,13 @@ extends Node2D func _ready() -> void: RoomNavigator.initialize($Camera2D) SaveManager.load_game() + AudioManager.set_music_volume(GameState.music_volume) + AudioManager.set_sfx_volume(GameState.sfx_volume) _apply_saved_state() func _apply_saved_state() -> void: for character in $Characters.get_children(): if character is Character and character.data != null: - var pos: Vector2 = GameState.get_character_position(character.data.id) - if pos != Vector2.ZERO: - character.global_position = pos + if GameState.has_character_position(character.data.id): + character.global_position = GameState.get_character_position(character.data.id) diff --git a/scripts/objects/elevator_button.gd b/scripts/objects/elevator_button.gd index dde7cf7..5a624f2 100644 --- a/scripts/objects/elevator_button.gd +++ b/scripts/objects/elevator_button.gd @@ -20,6 +20,8 @@ func _on_input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> voi func _on_pressed() -> void: + if target_floor < 0: + return RoomNavigator.go_to_floor(target_floor) _play_bounce() diff --git a/scripts/systems/hud.gd b/scripts/systems/hud.gd index 72f063c..2b12edf 100644 --- a/scripts/systems/hud.gd +++ b/scripts/systems/hud.gd @@ -1,11 +1,11 @@ ## HUD — heads-up display with back button, music toggle, and settings access. -extends CanvasLayer +class_name HUD extends CanvasLayer const MUSIC_ON_SYMBOL: String = "♪" const MUSIC_OFF_SYMBOL: String = "✕" var _music_enabled: bool = true -@onready var _settings_menu: SettingsMenu = get_node_or_null("/root/Main/UI/SettingsMenu") as SettingsMenu +@onready var _settings_menu: SettingsMenu = get_node_or_null("../SettingsMenu") as SettingsMenu func _on_back_button_pressed() -> void: diff --git a/scripts/systems/room_navigator.gd b/scripts/systems/room_navigator.gd index 6f40296..f2f8001 100644 --- a/scripts/systems/room_navigator.gd +++ b/scripts/systems/room_navigator.gd @@ -1,5 +1,5 @@ ## RoomNavigator — autoload that moves the Camera2D smoothly between hospital floors. -extends Node +class_name RoomNavigator extends Node signal room_changed(floor_index: int) @@ -19,11 +19,11 @@ func go_to_floor(floor_index: int) -> void: return _current_floor = floor_index var target_y: float = floor_index * -FLOOR_HEIGHT - var tween: Tween = _camera.create_tween() + var tween: Tween = create_tween() tween.set_ease(Tween.EASE_IN_OUT) tween.set_trans(Tween.TRANS_SINE) tween.tween_property(_camera, "position:y", target_y, CAMERA_TWEEN_DURATION) - room_changed.emit(floor_index) + tween.finished.connect(func() -> void: room_changed.emit(floor_index)) func get_current_floor() -> int: diff --git a/scripts/systems/settings_menu.gd b/scripts/systems/settings_menu.gd index 749e638..daa93a1 100644 --- a/scripts/systems/settings_menu.gd +++ b/scripts/systems/settings_menu.gd @@ -16,10 +16,12 @@ func hide_menu() -> void: func _on_music_slider_value_changed(value: float) -> void: AudioManager.set_music_volume(value) + GameState.music_volume = value func _on_sfx_slider_value_changed(value: float) -> void: AudioManager.set_sfx_volume(value) + GameState.sfx_volume = value func _on_reset_button_pressed() -> void: