diff --git a/README.md b/README.md index 82533f5..e17dfdd 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Campaign completion screen. - New campaign save reset. - Manual campaign checkpoint save/load. -- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop and Cao Cao portrait art, offers start/load/settings, leads new campaigns through a generated four-panel Cao Cao decision and Xiahou Dun opening prologue with full-screen story artwork plus a bottom scroll caption panel, exposes the same audio/edge-scroll settings from the top in-game toolbar, and persists BGM/SFX volume, sound toggles, and edge-scroll preference in `user://heros_settings.json`. +- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop and Cao Cao portrait art, offers start/load/settings, leads new campaigns through a generated four-panel Cao Cao decision and Xiahou Dun opening prologue with full-screen story artwork plus a bottom scroll caption panel, exposes the same audio/edge-scroll settings from the top in-game toolbar, includes a one-click audio recovery control, and persists BGM/SFX volume, sound toggles, and edge-scroll preference in `user://heros_settings.json`. - Victory and defeat result overlay. - Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback. - Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a6fd80f..e71a999 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -56,7 +56,7 @@ `BattleState` owns tactical runtime fields. `CampaignState` saves only campaign-facing data: completed scenario ids, gold, inventory counts, joined officers, campaign flags, and a player roster snapshot. It does not save map positions, pre-battle Roster/Formation choices, action flags, temporary status effects, or transient battle occupancy. -Presentation preferences are separate from campaign progression. `BattleScene` reads and writes `user://heros_settings.json` for BGM/SFX toggles, BGM/SFX volume percentages, and edge-scroll preference, exposing the same values through the title settings menu and the top in-game toolbar settings panel. These values apply to menu and battle audio playback without mutating `CampaignState`. +Presentation preferences are separate from campaign progression. `BattleScene` reads and writes `user://heros_settings.json` for BGM/SFX toggles, BGM/SFX volume percentages, and edge-scroll preference, exposing the same values through the title settings menu and the top in-game toolbar settings panel. Both settings surfaces include a one-click audio recovery control that re-enables BGM/SFX, restores audible default volumes, and plays an immediate confirmation cue. These values apply to menu and battle audio playback without mutating `CampaignState`. Joined officers gate whether `requires_joined` player deployments are loaded at all. Rewards and post-battle choices can add or remove joined officers without deleting their saved roster snapshot, so a later rejoin can preserve progression. When a save loads, `CampaignState` scans completed scenarios in campaign order and reapplies only officer join/leave transitions from rewards and the saved `applied_post_battle_choices` ledger, allowing added recruitment rewards to reach older saves without replaying gold or items. Old saves without the ledger can backfill a choice id only when exactly one completed scenario choice matches saved flags. Roster entries are keyed by `officer_id` when available, so future scenarios can deploy the same joined officer under different scenario-specific `unit_id` values. Saved progression overlays final battle stats for now; a later equipment refactor should split base stats from equipment bonuses before adding item leveling or stat recalculation. New battles currently start player units healed to their saved maximum HP/MP. Skill access is carried in roster snapshots, with class defaults still applied for old saves that do not include skills. Promoted class ids are saved in the roster, and the next battle rehydrates class-derived movement, growth, skills, and range from the saved class id before equipment range is recalculated. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index cf78e45..df47aff 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -163,7 +163,7 @@ const BATTLE_UNIT_LIST_PANEL_SIZE := Vector2(420, 360) const BATTLE_UNIT_LIST_ROW_SIZE := Vector2(372, 58) const BATTLE_UNIT_LIST_ICON_SIZE := Vector2(46, 46) const BATTLE_UNIT_LIST_BADGE_SIZE := Vector2(72, 22) -const SETTINGS_PANEL_SIZE := Vector2(420, 286) +const SETTINGS_PANEL_SIZE := Vector2(420, 330) const LOCAL_COMMAND_BUTTON_SIZE := Vector2(92, 38) const LOCAL_COMMAND_CANCEL_BUTTON_SIZE := Vector2(196, 30) const POST_MOVE_MENU_SIZE := Vector2(240, 146) @@ -520,6 +520,7 @@ var settings_bgm_volume_slider: HSlider var settings_sfx_volume_slider: HSlider var settings_bgm_volume_value_label: Label var settings_sfx_volume_value_label: Label +var settings_audio_reset_button: Button var restart_button: Button var new_campaign_button: Button var opening_prologue_root: Control @@ -553,6 +554,7 @@ var title_bgm_volume_slider: HSlider var title_sfx_volume_slider: HSlider var title_bgm_volume_value_label: Label var title_sfx_volume_value_label: Label +var title_audio_reset_button: Button var title_settings_back_button: Button var title_screen_visible := false var title_bgm_enabled := true @@ -2419,6 +2421,13 @@ func _create_hud() -> void: _apply_label_style(settings_sfx_volume_value_label, UI_PARCHMENT_TEXT, 13) settings_sfx_volume_row.add_child(settings_sfx_volume_value_label) + settings_audio_reset_button = Button.new() + settings_audio_reset_button.text = "소리 켜기" + settings_audio_reset_button.custom_minimum_size = Vector2(374, 32) + settings_audio_reset_button.tooltip_text = "배경음과 효과음을 켜고 음량을 기본값으로 되돌린 뒤 확인음을 재생합니다." + settings_audio_reset_button.pressed.connect(_on_settings_audio_reset_pressed) + settings_column.add_child(settings_audio_reset_button) + settings_edge_scroll_toggle = CheckButton.new() settings_edge_scroll_toggle.text = "가장자리 스크롤" settings_edge_scroll_toggle.custom_minimum_size = Vector2(374, 30) @@ -3692,7 +3701,7 @@ func _create_hud() -> void: title_settings_panel = PanelContainer.new() title_settings_panel.visible = false - title_settings_panel.custom_minimum_size = Vector2(432, 236) + title_settings_panel.custom_minimum_size = Vector2(432, 268) _apply_panel_style(title_settings_panel, "hud_info") title_menu_column.add_child(title_settings_panel) @@ -3784,6 +3793,13 @@ func _create_hud() -> void: _apply_label_style(title_sfx_volume_value_label, UI_PARCHMENT_TEXT, 13) title_sfx_volume_row.add_child(title_sfx_volume_value_label) + title_audio_reset_button = Button.new() + title_audio_reset_button.text = "소리 켜기" + title_audio_reset_button.custom_minimum_size = Vector2(392, 32) + title_audio_reset_button.tooltip_text = "배경음과 효과음을 켜고 음량을 기본값으로 되돌린 뒤 확인음을 재생합니다." + title_audio_reset_button.pressed.connect(_on_title_audio_reset_pressed) + title_settings_column.add_child(title_audio_reset_button) + title_edge_scroll_toggle = CheckButton.new() title_edge_scroll_toggle.text = "가장자리 스크롤" title_edge_scroll_toggle.custom_minimum_size = Vector2(392, 30) @@ -3850,7 +3866,9 @@ func _create_hud() -> void: title_load_auto_button, title_load_manual_button, title_load_back_button, - title_settings_back_button + title_settings_back_button, + title_audio_reset_button, + settings_audio_reset_button ]: _apply_button_style(hud_button) _apply_button_style(title_bgm_toggle) @@ -11302,6 +11320,10 @@ func _on_settings_sfx_volume_changed(value: float) -> void: _on_title_sfx_volume_changed(value) +func _on_settings_audio_reset_pressed() -> void: + _restore_audio_defaults(true) + + func _on_battle_unit_list_pressed() -> void: if battle_unit_list_button == null or battle_unit_list_button.disabled: return @@ -11943,6 +11965,24 @@ func _on_title_sfx_volume_changed(value: float) -> void: _play_sfx("ui_click", UI_SFX_VOLUME_DB) +func _on_title_audio_reset_pressed() -> void: + _restore_audio_defaults(true) + + +func _restore_audio_defaults(play_preview: bool) -> void: + title_bgm_enabled = true + title_sfx_enabled = true + title_bgm_volume_percent = 100 + title_sfx_volume_percent = 100 + _apply_audio_settings() + _save_title_settings() + _refresh_title_volume_controls() + _update_title_menu_state() + _play_bgm(current_bgm_key if not current_bgm_key.is_empty() else "menu") + if play_preview: + _play_sfx("menu_confirm", UI_SFX_VOLUME_DB) + + func _update_top_archive_buttons() -> void: var prep_available := ( not battle_started diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index 46b6829..660f2a4 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -122,6 +122,15 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: if persisted_scene.title_bgm_volume_percent != 45 or persisted_scene.title_sfx_volume_percent != 35: failures.append("title settings volume values should persist into a new scene instance") persisted_scene.free() + if scene.title_audio_reset_button == null or scene.title_audio_reset_button.text != "소리 켜기": + failures.append("title settings should expose a one-click audio recovery button") + scene._on_title_audio_reset_pressed() + if not scene.title_bgm_enabled or not scene.title_sfx_enabled: + failures.append("title audio recovery should re-enable BGM and SFX") + if scene.title_bgm_volume_percent != 100 or scene.title_sfx_volume_percent != 100: + failures.append("title audio recovery should restore audible volume defaults") + if scene.bgm_player == null or scene.bgm_player.stream == null or scene.current_bgm_key.is_empty(): + failures.append("title audio recovery should leave a playable BGM stream selected") scene._on_title_settings_back_pressed() if scene.title_settings_panel != null and scene.title_settings_panel.visible: failures.append("settings back should hide the title settings panel") @@ -228,6 +237,17 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("in-game settings toggles should update the shared settings state") if scene.title_bgm_volume_percent != 70 or scene.title_sfx_volume_percent != 60: failures.append("in-game settings sliders should update the shared volume state") + if scene.settings_audio_reset_button == null or scene.settings_audio_reset_button.text != "소리 켜기": + failures.append("in-game settings should expose a one-click audio recovery button") + scene._on_settings_bgm_toggled(false) + scene._on_settings_sfx_toggled(false) + scene._on_settings_bgm_volume_changed(20.0) + scene._on_settings_sfx_volume_changed(15.0) + scene._on_settings_audio_reset_pressed() + if not scene.title_bgm_enabled or not scene.title_sfx_enabled: + failures.append("in-game audio recovery should re-enable BGM and SFX") + if scene.title_bgm_volume_percent != 100 or scene.title_sfx_volume_percent != 100: + failures.append("in-game audio recovery should restore audible volume defaults") scene._hide_settings_panel() scene._on_new_campaign_pressed() if not scene._is_opening_prologue_visible():