From 3615b64196f2fc7cdebc1eeaf839a1d42cdbf024 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 23:14:24 +0900 Subject: [PATCH] Persist title audio settings --- README.md | 1 + docs/ARCHITECTURE.md | 2 + scripts/scenes/battle_scene.gd | 192 +++++++++++++++++++++++++++++++-- tools/smoke_title_menu.gd | 20 ++++ 4 files changed, 209 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5974ef4..36f168e 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,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. +- Title screen offers start, load, and settings; BGM/SFX volume, sound toggles, and edge-scroll preference persist in `user://heros_settings.json`. - Victory and defeat result overlay. - Dialogue portrait slot with officer default image paths, optional per-line overrides, and speaker-initial 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 d65c943..d38b747 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -55,6 +55,8 @@ `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 title-menu BGM/SFX toggles, BGM/SFX volume percentages, and edge-scroll preference, then applies those values 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. Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress and can load completed or current battles without mutating the save. Briefings preview first-clear rewards, can expose `briefing.camp_conversations` through a selectable Talk menu with `briefing.camp_dialogue` as a fallback, can filter those conversations by saved campaign flags, can claim saved one-time camp conversation supply effects through `CampaignState` and refresh the loaded battle inventory copy, and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps, and `shop.merchant` lines are presentation-only camp flavor. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Camp conversation supply claims are also campaign transactions: they save a per-scenario conversation claim ledger and inventory immediately, then refresh the already-loaded battle inventory. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks, and `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info, and can be expressed as one `pos` or a multi-cell `cells` list; gated victory conditions can also expose the `unit_reaches_tile` event that unlocks them as the next visible objective marker. Rewarding movement events that grant items or gold can expose optional battlefield side markers and Korean tile-info labels through `when.label`, without being treated as victory objectives. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item and gold pickups, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP, cure matching poison/seal/snare/disarm statuses, or cleanse negative stat debuffs, and in-battle item use, event pickup grants, event gold pickups, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items or gold, or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, event gold pickups, save writes, current-scenario advancement, finite-stock purchases, or camp conversation supply claims; pre-battle Shop, Armory, Roster, Formation, and Save are disabled on completed-scenario replays to avoid side effects. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 13bf0c5..ddc3b7c 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -26,6 +26,8 @@ const THREAT_OVERLAY_COLOR := Color(0.76, 0.060, 0.035, 0.20) const THREAT_BORDER_COLOR := Color(0.86, 0.18, 0.10, 0.48) const BGM_MENU_PATH := "res://audio/bgm/menu_theme_placeholder.wav" const BGM_BATTLE_PATH := "res://audio/bgm/battle_loop_placeholder.wav" +const SETTINGS_PATH := "user://heros_settings.json" +const SETTINGS_SAVE_VERSION := 1 const SFX_BOW_RELEASE_PATH := "res://audio/sfx/bow_release_01.wav" const SFX_DEFEAT_STING_PATH := "res://audio/sfx/defeat_sting_01.wav" const SFX_FIRE_SKILL_PATH := "res://audio/sfx/fire_skill_01.wav" @@ -44,6 +46,7 @@ const UI_FONT_BOLD_PATH := "res://art/ui/fonts/GowunBatang-Bold.ttf" const BGM_VOLUME_DB := -14.0 const SFX_VOLUME_DB := -4.0 const UI_SFX_VOLUME_DB := -8.0 +const AUDIO_MUTED_DB := -80.0 const FLOATING_TEXT_LIFETIME := 1.0 const FLOATING_TEXT_RISE := 42.0 const OBJECTIVE_NOTICE_DURATION := 2.6 @@ -471,11 +474,17 @@ var title_settings_panel: PanelContainer var title_bgm_toggle: CheckButton var title_sfx_toggle: CheckButton var title_edge_scroll_toggle: CheckButton +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_settings_back_button: Button var title_screen_visible := false var title_bgm_enabled := true var title_sfx_enabled := true var title_edge_scroll_enabled := true +var title_bgm_volume_percent := 100 +var title_sfx_volume_percent := 100 var log_box: RichTextLabel var dialogue_queue: Array = [] var active_dialogue_lines: Array = [] @@ -504,6 +513,7 @@ var unit_idle_time := 0.0 func _ready() -> void: texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR + _load_title_settings() _create_hud() _create_audio() state.changed.connect(_on_state_changed) @@ -3069,7 +3079,7 @@ func _create_hud() -> void: title_settings_panel = PanelContainer.new() title_settings_panel.visible = false - title_settings_panel.custom_minimum_size = Vector2(432, 134) + title_settings_panel.custom_minimum_size = Vector2(432, 236) _apply_panel_style(title_settings_panel, "hud_info") title_menu_column.add_child(title_settings_panel) @@ -3077,22 +3087,93 @@ func _create_hud() -> void: title_settings_column.add_theme_constant_override("separation", 6) title_settings_panel.add_child(title_settings_column) + var title_settings_caption := Label.new() + title_settings_caption.text = "환경 설정" + title_settings_caption.custom_minimum_size = Vector2(392, 24) + title_settings_caption.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title_settings_caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(title_settings_caption, UI_OLD_BRONZE, 15) + title_settings_column.add_child(title_settings_caption) + title_bgm_toggle = CheckButton.new() title_bgm_toggle.text = "배경음" + title_bgm_toggle.custom_minimum_size = Vector2(392, 30) title_bgm_toggle.button_pressed = title_bgm_enabled title_bgm_toggle.tooltip_text = "배경 음악을 켜거나 끕니다." title_bgm_toggle.toggled.connect(_on_title_bgm_toggled) title_settings_column.add_child(title_bgm_toggle) + var title_bgm_volume_row := HBoxContainer.new() + title_bgm_volume_row.custom_minimum_size = Vector2(392, 34) + title_bgm_volume_row.add_theme_constant_override("separation", 8) + title_settings_column.add_child(title_bgm_volume_row) + + var title_bgm_volume_caption := Label.new() + title_bgm_volume_caption.text = "배경음 크기" + title_bgm_volume_caption.custom_minimum_size = Vector2(94, 30) + title_bgm_volume_caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(title_bgm_volume_caption, UI_PARCHMENT_TEXT, 13) + title_bgm_volume_row.add_child(title_bgm_volume_caption) + + title_bgm_volume_slider = HSlider.new() + title_bgm_volume_slider.min_value = 0.0 + title_bgm_volume_slider.max_value = 100.0 + title_bgm_volume_slider.step = 5.0 + title_bgm_volume_slider.value = float(title_bgm_volume_percent) + title_bgm_volume_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_bgm_volume_slider.custom_minimum_size = Vector2(216, 30) + title_bgm_volume_slider.tooltip_text = "배경 음악의 크기를 조절합니다." + title_bgm_volume_slider.value_changed.connect(_on_title_bgm_volume_changed) + title_bgm_volume_row.add_child(title_bgm_volume_slider) + + title_bgm_volume_value_label = Label.new() + title_bgm_volume_value_label.custom_minimum_size = Vector2(48, 30) + title_bgm_volume_value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + title_bgm_volume_value_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(title_bgm_volume_value_label, UI_PARCHMENT_TEXT, 13) + title_bgm_volume_row.add_child(title_bgm_volume_value_label) + title_sfx_toggle = CheckButton.new() title_sfx_toggle.text = "효과음" + title_sfx_toggle.custom_minimum_size = Vector2(392, 30) title_sfx_toggle.button_pressed = title_sfx_enabled title_sfx_toggle.tooltip_text = "버튼과 전투 효과음을 켜거나 끕니다." title_sfx_toggle.toggled.connect(_on_title_sfx_toggled) title_settings_column.add_child(title_sfx_toggle) + var title_sfx_volume_row := HBoxContainer.new() + title_sfx_volume_row.custom_minimum_size = Vector2(392, 34) + title_sfx_volume_row.add_theme_constant_override("separation", 8) + title_settings_column.add_child(title_sfx_volume_row) + + var title_sfx_volume_caption := Label.new() + title_sfx_volume_caption.text = "효과음 크기" + title_sfx_volume_caption.custom_minimum_size = Vector2(94, 30) + title_sfx_volume_caption.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(title_sfx_volume_caption, UI_PARCHMENT_TEXT, 13) + title_sfx_volume_row.add_child(title_sfx_volume_caption) + + title_sfx_volume_slider = HSlider.new() + title_sfx_volume_slider.min_value = 0.0 + title_sfx_volume_slider.max_value = 100.0 + title_sfx_volume_slider.step = 5.0 + title_sfx_volume_slider.value = float(title_sfx_volume_percent) + title_sfx_volume_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_sfx_volume_slider.custom_minimum_size = Vector2(216, 30) + title_sfx_volume_slider.tooltip_text = "버튼과 전투 효과음의 크기를 조절합니다." + title_sfx_volume_slider.value_changed.connect(_on_title_sfx_volume_changed) + title_sfx_volume_row.add_child(title_sfx_volume_slider) + + title_sfx_volume_value_label = Label.new() + title_sfx_volume_value_label.custom_minimum_size = Vector2(48, 30) + title_sfx_volume_value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + title_sfx_volume_value_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(title_sfx_volume_value_label, UI_PARCHMENT_TEXT, 13) + title_sfx_volume_row.add_child(title_sfx_volume_value_label) + title_edge_scroll_toggle = CheckButton.new() title_edge_scroll_toggle.text = "가장자리 스크롤" + title_edge_scroll_toggle.custom_minimum_size = Vector2(392, 30) title_edge_scroll_toggle.button_pressed = title_edge_scroll_enabled title_edge_scroll_toggle.tooltip_text = "마우스를 전장 가장자리에 두면 화면을 움직입니다." title_edge_scroll_toggle.toggled.connect(_on_title_edge_scroll_toggled) @@ -3161,9 +3242,79 @@ func _create_hud() -> void: func _create_audio() -> void: bgm_player = AudioStreamPlayer.new() bgm_player.name = "BgmPlayer" - bgm_player.volume_db = BGM_VOLUME_DB + bgm_player.volume_db = _bgm_volume_db() bgm_player.finished.connect(_on_bgm_finished) add_child(bgm_player) + _apply_audio_settings() + + +func _load_title_settings() -> void: + if not FileAccess.file_exists(SETTINGS_PATH): + return + var parsed = JSON.parse_string(FileAccess.get_file_as_string(SETTINGS_PATH)) + if not parsed is Dictionary: + return + title_bgm_enabled = bool(parsed.get("bgm_enabled", title_bgm_enabled)) + title_sfx_enabled = bool(parsed.get("sfx_enabled", title_sfx_enabled)) + title_edge_scroll_enabled = bool(parsed.get("edge_scroll_enabled", title_edge_scroll_enabled)) + title_bgm_volume_percent = _clamp_volume_percent(parsed.get("bgm_volume_percent", title_bgm_volume_percent)) + title_sfx_volume_percent = _clamp_volume_percent(parsed.get("sfx_volume_percent", title_sfx_volume_percent)) + + +func _save_title_settings() -> bool: + var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE) + if file == null: + push_warning("Unable to save title settings: %s" % SETTINGS_PATH) + return false + var payload := { + "save_version": SETTINGS_SAVE_VERSION, + "bgm_enabled": title_bgm_enabled, + "bgm_volume_percent": title_bgm_volume_percent, + "sfx_enabled": title_sfx_enabled, + "sfx_volume_percent": title_sfx_volume_percent, + "edge_scroll_enabled": title_edge_scroll_enabled + } + file.store_string(JSON.stringify(payload, "\t")) + return true + + +func _apply_audio_settings() -> void: + if bgm_player != null: + bgm_player.volume_db = _bgm_volume_db() + if not title_bgm_enabled: + bgm_player.stop() + + +func _clamp_volume_percent(value) -> int: + return mini(maxi(int(value), 0), 100) + + +func _bgm_volume_db() -> float: + return _volume_percent_to_db(BGM_VOLUME_DB, title_bgm_volume_percent) + + +func _effective_sfx_volume_db(base_volume_db: float) -> float: + return _volume_percent_to_db(base_volume_db, title_sfx_volume_percent) + + +func _volume_percent_to_db(base_volume_db: float, percent: int) -> float: + var clamped := _clamp_volume_percent(percent) + if clamped <= 0: + return AUDIO_MUTED_DB + return base_volume_db + linear_to_db(float(clamped) / 100.0) + + +func _refresh_title_volume_controls() -> void: + if title_bgm_volume_slider != null: + title_bgm_volume_slider.set_value_no_signal(float(title_bgm_volume_percent)) + title_bgm_volume_slider.editable = title_bgm_enabled + if title_sfx_volume_slider != null: + title_sfx_volume_slider.set_value_no_signal(float(title_sfx_volume_percent)) + title_sfx_volume_slider.editable = title_sfx_enabled + if title_bgm_volume_value_label != null: + title_bgm_volume_value_label.text = "%d%%" % title_bgm_volume_percent + if title_sfx_volume_value_label != null: + title_sfx_volume_value_label.text = "%d%%" % title_sfx_volume_percent func _on_bgm_finished() -> void: @@ -3181,6 +3332,7 @@ func _play_bgm(key: String) -> void: current_bgm_key = key bgm_player.stop() return + bgm_player.volume_db = _bgm_volume_db() if current_bgm_key == key and bgm_player.playing: return var stream := _bgm_stream_for(key) @@ -3209,7 +3361,7 @@ func _play_sfx(key: String, volume_db := SFX_VOLUME_DB) -> void: var player := AudioStreamPlayer.new() player.name = "Sfx_%s" % key player.stream = stream - player.volume_db = volume_db + player.volume_db = _effective_sfx_volume_db(volume_db) player.finished.connect(player.queue_free) add_child(player) player.play() @@ -9880,11 +10032,12 @@ func _update_title_menu_state() -> void: if title_settings_back_button != null: title_settings_back_button.visible = title_settings_panel != null and title_settings_panel.visible if title_bgm_toggle != null: - title_bgm_toggle.button_pressed = title_bgm_enabled + title_bgm_toggle.set_pressed_no_signal(title_bgm_enabled) if title_sfx_toggle != null: - title_sfx_toggle.button_pressed = title_sfx_enabled + title_sfx_toggle.set_pressed_no_signal(title_sfx_enabled) if title_edge_scroll_toggle != null: - title_edge_scroll_toggle.button_pressed = title_edge_scroll_enabled + title_edge_scroll_toggle.set_pressed_no_signal(title_edge_scroll_enabled) + _refresh_title_volume_controls() func _format_title_status_text(has_load: bool) -> String: @@ -9967,6 +10120,14 @@ func _on_title_settings_back_pressed() -> void: func _set_title_settings_visible(visible: bool) -> void: + if title_status_label != null: + title_status_label.visible = not visible + if title_start_button != null: + title_start_button.visible = not visible + if title_load_button != null: + title_load_button.visible = not visible + if title_settings_button != null: + title_settings_button.visible = not visible if title_settings_panel != null: title_settings_panel.visible = visible if title_settings_back_button != null: @@ -9979,6 +10140,8 @@ func _on_title_bgm_toggled(enabled: bool) -> void: _play_bgm(current_bgm_key if not current_bgm_key.is_empty() else "menu") elif bgm_player != null: bgm_player.stop() + _apply_audio_settings() + _save_title_settings() _update_title_menu_state() @@ -9986,15 +10149,32 @@ func _on_title_sfx_toggled(enabled: bool) -> void: title_sfx_enabled = enabled if title_sfx_enabled: _play_sfx("ui_click", UI_SFX_VOLUME_DB) + _save_title_settings() _update_title_menu_state() func _on_title_edge_scroll_toggled(enabled: bool) -> void: title_edge_scroll_enabled = enabled + _save_title_settings() _update_title_menu_state() queue_redraw() +func _on_title_bgm_volume_changed(value: float) -> void: + title_bgm_volume_percent = _clamp_volume_percent(value) + _apply_audio_settings() + _save_title_settings() + _refresh_title_volume_controls() + + +func _on_title_sfx_volume_changed(value: float) -> void: + title_sfx_volume_percent = _clamp_volume_percent(value) + _save_title_settings() + _refresh_title_volume_controls() + if title_sfx_enabled: + _play_sfx("ui_click", 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 9a777fb..e585c72 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -4,14 +4,17 @@ const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") const SAVE_PATH := "user://campaign_save.json" const MANUAL_SAVE_PATH := "user://campaign_manual_save.json" +const SETTINGS_PATH := "user://heros_settings.json" func _init() -> void: var save_backup := _backup_user_file(SAVE_PATH) var manual_backup := _backup_user_file(MANUAL_SAVE_PATH) + var settings_backup := _backup_user_file(SETTINGS_PATH) var failures: Array[String] = [] _clear_user_file(SAVE_PATH) _clear_user_file(MANUAL_SAVE_PATH) + _clear_user_file(SETTINGS_PATH) _check_title_menu_start_load_and_settings(failures) @@ -19,6 +22,8 @@ func _init() -> void: failures.append("could not restore campaign save after title menu smoke") if not _restore_user_file(MANUAL_SAVE_PATH, manual_backup): failures.append("could not restore manual campaign save after title menu smoke") + if not _restore_user_file(SETTINGS_PATH, settings_backup): + failures.append("could not restore title settings after title menu smoke") if failures.is_empty(): print("title menu smoke ok") @@ -52,17 +57,32 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("title settings panel should open from the title screen") if scene.title_bgm_toggle == null or scene.title_sfx_toggle == null or scene.title_edge_scroll_toggle == null: failures.append("title settings should expose BGM, SFX, and edge-scroll toggles") + if scene.title_bgm_volume_slider == null or scene.title_sfx_volume_slider == null: + failures.append("title settings should expose BGM and SFX volume sliders") scene._on_title_bgm_toggled(false) if scene.title_bgm_enabled: failures.append("BGM toggle should disable background music") + scene._on_title_bgm_volume_changed(45.0) + if scene.title_bgm_volume_percent != 45: + failures.append("BGM volume slider should update the saved BGM volume percent") scene._on_title_sfx_toggled(false) if scene.title_sfx_enabled: failures.append("SFX toggle should disable sound effects") + scene._on_title_sfx_volume_changed(35.0) + if scene.title_sfx_volume_percent != 35: + failures.append("SFX volume slider should update the saved SFX volume percent") scene._on_title_edge_scroll_toggled(false) if scene.title_edge_scroll_enabled: failures.append("edge-scroll toggle should disable edge scrolling") if scene._update_edge_scroll(0.25): failures.append("edge scrolling should not move while disabled in settings") + var persisted_scene = BattleSceneScript.new() + persisted_scene._ready() + if persisted_scene.title_bgm_enabled or persisted_scene.title_sfx_enabled or persisted_scene.title_edge_scroll_enabled: + failures.append("title settings toggles should persist into a new scene instance") + 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() 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")