Persist title audio settings

This commit is contained in:
2026-06-19 23:14:24 +09:00
parent ab3f0a1124
commit 3615b64196
4 changed files with 209 additions and 6 deletions

View File

@@ -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.

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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")