diff --git a/README.md b/README.md index c17132b..0c8377b 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,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, Cao Cao portrait art, and a compact left-side command board with `처음부터`, `로드하기`, and `환경설정`; its generated command board and title buttons keep safer source-art proportions, linear filtering, tile-fit 9-slice surfaces, and aspect-locked canvas scaling for QHD fullscreen, while story/briefing action buttons use crisp flat readable surfaces. New campaigns then move into a generated four-panel Yellow Turban rebellion, Cao Cao resolve, Xiahou Dun meeting, and Yingchuan contact prologue with full-screen story artwork, a chapter seal, a text-only chapter-side story ledger instead of exposed numeric cards or tick marks, `다음 기록`/`군막으로`/`전투 준비` story controls, and a bottom scroll caption panel. Briefing and right-side battle information panels now favor high-contrast readable paper/card surfaces over busy decorative frames. Title and in-game settings expose clearer background-music, combat-SFX, edge-scroll, and fullscreen display controls, include one-click audio recovery, and persist preferences in `user://heros_settings.json`. +- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop, Cao Cao portrait art, and a compact left-side command board with `처음부터`, `로드하기`, and `환경설정`; its generated command board and title buttons keep safer source-art proportions, linear filtering, tile-fit 9-slice surfaces, and aspect-locked canvas scaling for QHD fullscreen, while story/briefing action buttons use crisp flat readable surfaces. New campaigns then move into a generated four-panel Yellow Turban rebellion, Cao Cao resolve, Xiahou Dun meeting, and Yingchuan contact prologue with full-screen story artwork, a chapter seal, a text-only chapter-side story ledger instead of exposed numeric cards or tick marks, `다음 기록`/`군막으로`/`전투 준비` story controls, and a bottom scroll caption panel. Briefing, dialogue, notice, auto-turn, and right-side battle information panels now favor high-contrast flat paper/ink text surfaces, larger Korean type, and explicit line spacing over busy decorative frames. Title and in-game settings expose clearer background-music, combat-SFX, edge-scroll, and fullscreen display controls, show the active music/SFX mix beside the preview button, include one-click audio recovery, and persist preferences 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/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index ade6a7f..11fd40d 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -26,6 +26,10 @@ 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_camp_dawn_theme.wav" const BGM_BATTLE_PATH := "res://audio/bgm/battle_yingchuan_march.wav" +const BGM_TRACK_LABELS := { + "menu": "군막의 새벽", + "battle": "영천 진군" +} const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png" const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png" const TITLE_MENU_SURFACE_PATH := "res://art/ui/panels/panel_title_command.png" @@ -92,6 +96,7 @@ const DEFAULT_AUDIO_VOLUME_PERCENT := 75 const AUDIO_MUTED_DB := -80.0 const MAX_RECENT_SFX_KEYS := 32 const AUDIO_PREVIEW_SFX_KEYS := ["menu_confirm", "footstep", "slash"] +const AUDIO_PREVIEW_LABEL := "확인음 · 발소리 · 타격음" const FLOATING_TEXT_LIFETIME := 1.0 const FLOATING_TEXT_RISE := 42.0 const OBJECTIVE_NOTICE_DURATION := 2.6 @@ -136,17 +141,17 @@ const DIALOGUE_PANEL_POSITION := Vector2(40, 324) const DIALOGUE_PANEL_SIZE := Vector2(1200, 356) const DIALOGUE_PORTRAIT_SIZE := Vector2(220, 310) const DIALOGUE_COLUMN_SIZE := Vector2(850, 318) -const DIALOGUE_TEXT_SIZE := Vector2(830, 164) -const OBJECTIVE_HUD_FONT_SIZE := 13 -const OBJECTIVE_HUD_MIN_FONT_SIZE := 11 -const OBJECTIVE_NOTICE_FONT_SIZE := 15 -const OBJECTIVE_NOTICE_MIN_FONT_SIZE := 12 -const BRIEFING_OBJECTIVE_FONT_SIZE := 15 -const BRIEFING_OBJECTIVE_MIN_FONT_SIZE := 12 -const DIALOGUE_SPEAKER_FONT_SIZE := 18 -const DIALOGUE_SPEAKER_MIN_FONT_SIZE := 15 -const DIALOGUE_TEXT_FONT_SIZE := 17 -const DIALOGUE_TEXT_MIN_FONT_SIZE := 14 +const DIALOGUE_TEXT_SIZE := Vector2(830, 188) +const OBJECTIVE_HUD_FONT_SIZE := 14 +const OBJECTIVE_HUD_MIN_FONT_SIZE := 12 +const OBJECTIVE_NOTICE_FONT_SIZE := 16 +const OBJECTIVE_NOTICE_MIN_FONT_SIZE := 14 +const BRIEFING_OBJECTIVE_FONT_SIZE := 17 +const BRIEFING_OBJECTIVE_MIN_FONT_SIZE := 14 +const DIALOGUE_SPEAKER_FONT_SIZE := 20 +const DIALOGUE_SPEAKER_MIN_FONT_SIZE := 16 +const DIALOGUE_TEXT_FONT_SIZE := 19 +const DIALOGUE_TEXT_MIN_FONT_SIZE := 16 const LOCAL_COMMAND_TITLE_FONT_SIZE := 14 const LOCAL_COMMAND_TITLE_MIN_FONT_SIZE := 10 const LOCAL_COMMAND_DETAIL_FONT_SIZE := 12 @@ -212,7 +217,7 @@ const BATTLE_UNIT_LIST_CLASS_CREST_ICON_SIZE := Vector2(17, 17) const BATTLE_UNIT_LIST_BADGE_SIZE := Vector2(72, 22) const BATTLE_UNIT_LIST_BADGE_ICON_SIZE := Vector2(15, 15) const BATTLE_UNIT_LIST_HP_BAR_SIZE := Vector2(40, 7) -const SETTINGS_PANEL_SIZE := Vector2(420, 368) +const SETTINGS_PANEL_SIZE := Vector2(420, 414) const LOCAL_COMMAND_BUTTON_SIZE := Vector2(92, 38) const LOCAL_COMMAND_CANCEL_BUTTON_SIZE := Vector2(196, 30) const LOCAL_COMMAND_HEADER_ICON_SIZE := Vector2(24, 24) @@ -234,6 +239,9 @@ const UI_RICE_PAPER_DARK := Color(0.18, 0.17, 0.14, 0.98) const UI_READABLE_PAPER := Color(0.66, 0.63, 0.53, 0.998) const UI_READABLE_CARD := Color(0.58, 0.55, 0.47, 0.998) const UI_HUD_READABLE := Color(0.060, 0.056, 0.048, 0.982) +const UI_TEXT_PAPER_SURFACE := Color(0.72, 0.69, 0.58, 0.998) +const UI_TEXT_DARK_SURFACE := Color(0.030, 0.028, 0.024, 0.996) +const UI_TEXT_NOTICE_SURFACE := Color(0.68, 0.65, 0.55, 0.998) const UI_AGED_INK := Color(0.055, 0.060, 0.055, 1.0) const UI_DARK_LACQUER := Color(0.045, 0.038, 0.034, 0.988) const UI_LACQUER_EDGE := Color(0.025, 0.023, 0.020, 1.0) @@ -597,6 +605,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_status_label: Label var settings_audio_reset_button: Button var restart_button: Button var new_campaign_button: Button @@ -635,6 +644,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_status_label: Label var title_audio_reset_button: Button var title_settings_back_button: Button var title_screen_visible := false @@ -838,6 +848,16 @@ func _panel_texture_spec(variant: String) -> Dictionary: return {} +func _panel_variant_prefers_flat_text_surface(variant: String) -> bool: + return variant in [ + "dialogue", + "speech_scroll", + "notice_edict", + "cinematic_caption", + "story_caption" + ] + + func _age_panel_style(style: StyleBoxFlat, variant: String, border_width: int) -> void: if style == null: return @@ -922,11 +942,11 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: margin = 6 shadow_size = 4 "notice_edict": - fill = Color(0.42, 0.40, 0.34, 0.998) - border = UI_LACQUER_EDGE - border_width = 7 - radius = 11 - margin = 14 + fill = UI_TEXT_NOTICE_SURFACE + border = Color(0.10, 0.085, 0.060, 1.0) + border_width = 5 + radius = 10 + margin = 18 shadow_size = 14 "seal": fill = Color(0.080, 0.030, 0.022, 1.0) @@ -947,18 +967,18 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: radius = 10 margin = 3 "dialogue": - fill = Color(0.045, 0.045, 0.040, 0.982) - border = Color(0.18, 0.17, 0.14, 1.0) - border_width = 8 - radius = 12 - margin = 20 + fill = UI_TEXT_DARK_SURFACE + border = Color(0.28, 0.23, 0.16, 1.0) + border_width = 7 + radius = 10 + margin = 22 shadow_size = 18 "speech_scroll": - fill = Color(0.58, 0.55, 0.48, 0.996) - border = Color(0.13, 0.12, 0.10, 0.98) - border_width = 5 - radius = 11 - margin = 15 + fill = UI_TEXT_PAPER_SURFACE + border = Color(0.12, 0.10, 0.072, 1.0) + border_width = 4 + radius = 9 + margin = 18 shadow_size = 6 "hud_info": fill = Color(0.045, 0.050, 0.048, 0.90) @@ -1015,14 +1035,14 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: margin = 5 shadow_size = 2 "cinematic_caption": - fill = Color(0.010, 0.010, 0.009, 0.996) + fill = Color(0.008, 0.008, 0.007, 0.998) border = UI_TARNISHED_BRONZE border_width = 4 radius = 10 margin = 14 shadow_size = 22 "story_caption": - fill = Color(0.15, 0.13, 0.10, 0.94) + fill = Color(0.030, 0.026, 0.020, 0.988) border = UI_TARNISHED_BRONZE border_width = 3 radius = 10 @@ -1067,7 +1087,7 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: pass var style := _make_panel_style(fill, border, border_width, radius, margin, shadow_size) _age_panel_style(style, variant, border_width) - var texture_style := _make_panel_texture_style(variant, margin) + var texture_style := _make_panel_texture_style(variant, margin) if not _panel_variant_prefers_flat_text_surface(variant) else null if texture_style != null: panel.add_theme_stylebox_override("panel", texture_style) panel.set_meta("generated_panel_texture", true) @@ -1280,12 +1300,14 @@ func _apply_text_legibility(control: Control, font_color: Color, font_size: int var resolved_size := font_size if resolved_size <= 0: resolved_size = control.get_theme_font_size("font_size") - var outline_size := 2 if resolved_size >= 13 else 1 + var outline_size := 3 if resolved_size >= 16 else (2 if resolved_size >= 12 else 1) control.add_theme_color_override("font_outline_color", _text_outline_color_for(font_color)) control.add_theme_constant_override("outline_size", outline_size) control.add_theme_color_override("font_shadow_color", UI_TEXT_SHADOW) control.add_theme_constant_override("shadow_offset_x", 1) control.add_theme_constant_override("shadow_offset_y", 1) + if control is Label: + control.add_theme_constant_override("line_spacing", 4 if resolved_size >= 16 else 3) func _apply_strong_text_legibility(control: Control, font_color: Color, font_size: int = 0) -> void: @@ -2366,7 +2388,7 @@ func _estimated_wrapped_line_count(text: String, width: float, font_size: int) - func _estimated_line_height(font_size: int) -> float: - return ceilf(float(font_size) * 1.36) + return ceilf(float(font_size) * 1.46 + 3.0) func _weighted_text_length(text: String) -> float: @@ -2895,7 +2917,7 @@ func _make_map_lattice_line(position: Vector2, size: Vector2, alpha: float) -> C func _make_section_caption(text: String, width: float = 0.0) -> Control: var panel := PanelContainer.new() - panel.custom_minimum_size = Vector2(width, 24) + panel.custom_minimum_size = Vector2(width, 30) panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_panel_style(panel, "caption") var row := HBoxContainer.new() @@ -2904,11 +2926,11 @@ func _make_section_caption(text: String, width: float = 0.0) -> Control: row.add_child(_make_seal_tick()) var label := Label.new() label.text = text - label.custom_minimum_size = Vector2(maxf(0.0, width - 68.0), 18) + label.custom_minimum_size = Vector2(maxf(0.0, width - 68.0), 22) label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - _apply_label_style(label, UI_PARCHMENT_TEXT, 13) + _apply_label_style(label, UI_PARCHMENT_TEXT, 15) row.add_child(label) row.add_child(_make_seal_tick()) return panel @@ -3049,7 +3071,7 @@ func _create_hud() -> void: top_objective_chip = PanelContainer.new() top_objective_chip.name = "TopObjectiveChip" top_objective_chip.custom_minimum_size = Vector2(396, 54) - _apply_panel_style(top_objective_chip, "edict_compact") + _apply_panel_style(top_objective_chip, "hud_readable") top_row.add_child(top_objective_chip) var objective_top_row := HBoxContainer.new() @@ -3062,7 +3084,7 @@ func _create_hud() -> void: objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART objective_label.custom_minimum_size = Vector2(274, 36) objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _apply_label_style(objective_label, UI_AGED_INK, OBJECTIVE_HUD_FONT_SIZE) + _apply_label_style(objective_label, UI_PARCHMENT_TEXT, OBJECTIVE_HUD_FONT_SIZE) objective_top_row.add_child(objective_label) objective_top_row.add_child(_make_bamboo_gutter(10, 36)) @@ -3314,6 +3336,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_status_label = Label.new() + settings_audio_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + settings_audio_status_label.custom_minimum_size = Vector2(374, 42) + settings_audio_status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(settings_audio_status_label, UI_PARCHMENT_TEXT, 12) + settings_column.add_child(settings_audio_status_label) + settings_audio_reset_button = Button.new() settings_audio_reset_button.text = "기본값 듣기" settings_audio_reset_button.custom_minimum_size = Vector2(374, 32) @@ -3338,7 +3367,8 @@ func _create_hud() -> void: objective_notice_panel = PanelContainer.new() objective_notice_panel.visible = false objective_notice_panel.position = Vector2(338, 88) - objective_notice_panel.size = Vector2(604, 170) + objective_notice_panel.size = Vector2(604, 190) + objective_notice_panel.custom_minimum_size = Vector2(604, 190) objective_notice_panel.mouse_filter = Control.MOUSE_FILTER_PASS _apply_panel_style(objective_notice_panel, "notice_edict") root.add_child(objective_notice_panel) @@ -3361,7 +3391,7 @@ func _create_hud() -> void: objective_notice_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER objective_notice_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER objective_notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - objective_notice_label.custom_minimum_size = Vector2(548, 62) + objective_notice_label.custom_minimum_size = Vector2(548, 72) _apply_label_style(objective_notice_label, UI_AGED_INK, OBJECTIVE_NOTICE_FONT_SIZE) objective_notice_column.add_child(objective_notice_label) objective_notice_column.add_child(_make_scroll_rod(548, 3)) @@ -3373,7 +3403,7 @@ func _create_hud() -> void: auto_end_turn_prompt_panel.position = Vector2(408, 112) auto_end_turn_prompt_panel.size = Vector2(464, 154) auto_end_turn_prompt_panel.custom_minimum_size = Vector2(464, 154) - _apply_panel_style(auto_end_turn_prompt_panel, "command_seal") + _apply_panel_style(auto_end_turn_prompt_panel, "dialogue") root.add_child(auto_end_turn_prompt_panel) var auto_end_column := VBoxContainer.new() @@ -3955,7 +3985,7 @@ func _create_hud() -> void: var opening_caption_readability_mat := ColorRect.new() opening_caption_readability_mat.name = "OpeningCaptionTextBacking" - opening_caption_readability_mat.color = Color(0.0, 0.0, 0.0, 0.62) + opening_caption_readability_mat.color = Color(0.0, 0.0, 0.0, 0.74) opening_caption_readability_mat.mouse_filter = Control.MOUSE_FILTER_IGNORE opening_caption_readability_mat.position = Vector2(30, 488) opening_caption_readability_mat.size = Vector2(1220, 198) @@ -4117,7 +4147,7 @@ func _create_hud() -> void: briefing_objective_header.add_child(briefing_objective_toggle_button) briefing_objective_panel = PanelContainer.new() - briefing_objective_panel.custom_minimum_size = Vector2(680, 124) + briefing_objective_panel.custom_minimum_size = Vector2(680, 148) _apply_panel_style(briefing_objective_panel, "briefing_card") briefing_column.add_child(briefing_objective_panel) @@ -4127,9 +4157,9 @@ func _create_hud() -> void: briefing_objective_label = Label.new() briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_objective_label.custom_minimum_size = Vector2(648, 96) + briefing_objective_label.custom_minimum_size = Vector2(648, 120) briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _apply_label_style(briefing_objective_label, UI_AGED_INK, 17) + _apply_label_style(briefing_objective_label, UI_AGED_INK, BRIEFING_OBJECTIVE_FONT_SIZE) briefing_objective_row.add_child(briefing_objective_label) briefing_column.add_child(_make_section_caption("전장도와 군막 보고", 680)) @@ -4185,7 +4215,7 @@ func _create_hud() -> void: briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART briefing_camp_overview_label.custom_minimum_size = Vector2(286, 86) briefing_camp_overview_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 15) + _apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 16) briefing_camp_overview_row.add_child(briefing_camp_overview_label) briefing_tactical_marker_list = VBoxContainer.new() @@ -4196,13 +4226,13 @@ func _create_hud() -> void: briefing_camp_overview_row.add_child(briefing_tactical_marker_list) var briefing_scroll := ScrollContainer.new() - briefing_scroll.custom_minimum_size = Vector2(680, 84) + briefing_scroll.custom_minimum_size = Vector2(680, 96) briefing_column.add_child(briefing_scroll) briefing_label = Label.new() briefing_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART briefing_label.custom_minimum_size = Vector2(656, 0) - _apply_label_style(briefing_label, UI_AGED_INK, 16) + _apply_label_style(briefing_label, UI_AGED_INK, 17) briefing_scroll.add_child(briefing_label) var prep_button_row := HBoxContainer.new() @@ -4498,13 +4528,17 @@ func _create_hud() -> void: dialogue_previous_button = Button.new() dialogue_previous_button.text = "이전" dialogue_previous_button.disabled = true - dialogue_previous_button.custom_minimum_size = Vector2(88, 32) + dialogue_previous_button.custom_minimum_size = Vector2(96, 38) + dialogue_previous_button.add_theme_font_size_override("font_size", 15) + dialogue_previous_button.set_meta("flat_readable_button", true) dialogue_previous_button.pressed.connect(_retreat_dialogue) dialogue_control_row.add_child(dialogue_previous_button) dialogue_continue_button = Button.new() dialogue_continue_button.text = "다음" - dialogue_continue_button.custom_minimum_size = Vector2(120, 32) + dialogue_continue_button.custom_minimum_size = Vector2(128, 38) + dialogue_continue_button.add_theme_font_size_override("font_size", 16) + dialogue_continue_button.set_meta("flat_readable_button", true) dialogue_continue_button.pressed.connect(_advance_dialogue) dialogue_control_row.add_child(dialogue_continue_button) @@ -4809,7 +4843,7 @@ func _create_hud() -> void: title_settings_panel = PanelContainer.new() title_settings_panel.visible = false - title_settings_panel.custom_minimum_size = Vector2(390, 314) + title_settings_panel.custom_minimum_size = Vector2(390, 362) _apply_panel_style(title_settings_panel, "hud_info") title_menu_column.add_child(title_settings_panel) @@ -4901,6 +4935,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_status_label = Label.new() + title_audio_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + title_audio_status_label.custom_minimum_size = Vector2(350, 42) + title_audio_status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(title_audio_status_label, UI_PARCHMENT_TEXT, 12) + title_settings_column.add_child(title_audio_status_label) + title_audio_reset_button = Button.new() _configure_title_menu_button( title_audio_reset_button, @@ -5129,6 +5170,35 @@ func _refresh_title_volume_controls() -> void: title_sfx_volume_value_label.text = "%d%%" % title_sfx_volume_percent if settings_sfx_volume_value_label != null: settings_sfx_volume_value_label.text = "%d%%" % title_sfx_volume_percent + var audio_status := _format_audio_status_text() + var audio_tooltip := _format_audio_status_tooltip() + if title_audio_status_label != null: + title_audio_status_label.text = audio_status + title_audio_status_label.tooltip_text = audio_tooltip + if settings_audio_status_label != null: + settings_audio_status_label.text = audio_status + settings_audio_status_label.tooltip_text = audio_tooltip + + +func _format_audio_status_text() -> String: + var bgm_text := "음악 꺼짐" + if title_bgm_enabled: + bgm_text = "현재 음악: %s %d%%" % [_current_bgm_display_name(), title_bgm_volume_percent] + var sfx_text := "효과음 꺼짐" + if title_sfx_enabled: + sfx_text = "효과음 %d%%" % title_sfx_volume_percent + return "%s\n%s · 미리듣기: %s" % [bgm_text, sfx_text, AUDIO_PREVIEW_LABEL] + + +func _format_audio_status_tooltip() -> String: + return "현재 재생되는 음악과 기본값 듣기에서 확인할 효과음입니다.\n%s" % _format_audio_status_text() + + +func _current_bgm_display_name() -> String: + var key := current_bgm_key + if key.is_empty(): + key = "menu" + return str(BGM_TRACK_LABELS.get(key, key)) func _on_bgm_finished() -> void: @@ -5145,6 +5215,7 @@ func _play_bgm(key: String) -> void: if not title_bgm_enabled: current_bgm_key = key bgm_player.stop() + _refresh_title_volume_controls() return bgm_player.volume_db = _bgm_volume_db() if current_bgm_key == key and bgm_player.playing: @@ -5153,6 +5224,7 @@ func _play_bgm(key: String) -> void: if stream == null: return current_bgm_key = key + _refresh_title_volume_controls() bgm_player.stop() bgm_player.stream = stream if not bgm_player.is_inside_tree(): diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 4adb974..c096ec5 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -705,8 +705,8 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: var dialogue_area: Vector2 = scene.dialogue_text_label.custom_minimum_size if dialogue_area.x <= 0.0 or dialogue_area.y <= 0.0: dialogue_area = Vector2(666.0, 144.0) - var short_dialogue_size := int(scene._fit_font_size_for_text("군령을 전하라.", 17, 14, dialogue_area)) - if short_dialogue_size != 17: + var short_dialogue_size := int(scene._fit_font_size_for_text("군령을 전하라.", scene.DIALOGUE_TEXT_FONT_SIZE, scene.DIALOGUE_TEXT_MIN_FONT_SIZE, dialogue_area)) + if short_dialogue_size != scene.DIALOGUE_TEXT_FONT_SIZE: failures.append("short dialogue text should keep the base font size, got %d" % short_dialogue_size) var long_dialogue := "「" \ + "서영의 매복은 아직 끝나지 않았다. 숲 뒤 봉쇄병이 동쪽 길목을 닫기 전에 전열을 유지하고, 부상병을 마을로 물려 회복시킨 뒤 다시 밀어붙여라. " \ @@ -714,8 +714,8 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: + "마지막 봉쇄병이 모습을 드러내면 조급히 흩어지지 말고, 마을과 성채의 회복지를 번갈아 쓰며 전열을 앞으로 밀어라. " \ + "적이 다시 숲을 흔들어도 군령은 하나다. 길목을 열고 남은 병력을 정리한 뒤 추격로를 확보하라. " \ + "북쪽 비탈과 남쪽 수풀을 동시에 살피고, 어느 한 장수가 고립되지 않도록 서로의 이동 거리를 맞추어라.」" - var fitted_dialogue_size := int(scene._fit_font_size_for_text(long_dialogue, 17, 14, dialogue_area)) - if fitted_dialogue_size < 14 or fitted_dialogue_size > 17: + var fitted_dialogue_size := int(scene._fit_font_size_for_text(long_dialogue, scene.DIALOGUE_TEXT_FONT_SIZE, scene.DIALOGUE_TEXT_MIN_FONT_SIZE, dialogue_area)) + if fitted_dialogue_size < scene.DIALOGUE_TEXT_MIN_FONT_SIZE or fitted_dialogue_size > scene.DIALOGUE_TEXT_FONT_SIZE: failures.append("long dialogue font fit should stay inside its configured bounds") var fitted_dialogue_height := float(scene._estimated_wrapped_text_height(long_dialogue, dialogue_area.x, fitted_dialogue_size)) if fitted_dialogue_height > dialogue_area.y: @@ -723,8 +723,8 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: var objective_area := Vector2(500.0, 104.0) var long_objective := "승리: 서영의 후군을 꺾었다. 동쪽 길목을 장악하고 뒤따르는 봉쇄병까지 정리하라.\n패배: 조조가 퇴각하거나 제17턴이 시작되면 패한다.\n보상: 군자금 700냥, 콩, 술, 철검" - var fitted_objective_size := int(scene._fit_font_size_for_text(long_objective, 15, 12, objective_area)) - if fitted_objective_size < 12 or fitted_objective_size > 15: + var fitted_objective_size := int(scene._fit_font_size_for_text(long_objective, scene.BRIEFING_OBJECTIVE_FONT_SIZE, scene.BRIEFING_OBJECTIVE_MIN_FONT_SIZE, objective_area)) + if fitted_objective_size < scene.BRIEFING_OBJECTIVE_MIN_FONT_SIZE or fitted_objective_size > scene.BRIEFING_OBJECTIVE_FONT_SIZE: failures.append("briefing objective font fit should stay inside its configured bounds") var fitted_objective_height := float(scene._estimated_wrapped_text_height(long_objective, objective_area.x, fitted_objective_size)) if fitted_objective_height > objective_area.y: diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index 4eccb0a..d6b4541 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -194,9 +194,15 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("title fullscreen toggle should be clearly named") 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") + if scene.title_audio_status_label == null: + failures.append("title settings should describe the current audio mix") + elif not scene.title_audio_status_label.text.contains("현재 음악: 군막의 새벽") or not scene.title_audio_status_label.text.contains("미리듣기: 확인음"): + failures.append("title audio status should name the current BGM and preview SFX: %s" % scene.title_audio_status_label.text) scene._on_title_bgm_toggled(false) if scene.title_bgm_enabled: failures.append("BGM toggle should disable background music") + if scene.title_audio_status_label != null and not scene.title_audio_status_label.text.contains("음악 꺼짐"): + failures.append("title audio status should reflect disabled BGM: %s" % scene.title_audio_status_label.text) 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") @@ -241,6 +247,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: 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") + if scene.title_audio_status_label != null and (not scene.title_audio_status_label.text.contains("군막의 새벽") or not scene.title_audio_status_label.text.contains("%d%%" % BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT)): + failures.append("title audio status should refresh after defaults preview: %s" % scene.title_audio_status_label.text) if not scene.recent_sfx_keys.has("menu_confirm"): failures.append("title audio preview should play an audible confirmation SFX") if not scene.recent_sfx_keys.has("footstep"): @@ -405,6 +413,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: audio_scene._start_battle_from_briefing() if audio_scene.current_bgm_key != "battle" or audio_scene.bgm_player == null or audio_scene.bgm_player.stream == null: failures.append("battle start should switch from menu music to an audible battle BGM stream") + if not audio_scene._format_audio_status_text().contains("영천 진군"): + failures.append("battle audio status should name the battle march: %s" % audio_scene._format_audio_status_text()) audio_scene._show_title_menu() if audio_scene.current_bgm_key != "menu" or audio_scene.bgm_player == null or audio_scene.bgm_player.stream == null: failures.append("returning to title should restore the menu BGM stream") @@ -466,6 +476,10 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("in-game fullscreen toggle should be clearly named") if scene.settings_bgm_volume_slider == null or scene.settings_sfx_volume_slider == null: failures.append("in-game settings panel should expose BGM and SFX volume sliders") + if scene.settings_audio_status_label == null: + failures.append("in-game settings should describe the active audio mix") + elif not scene.settings_audio_status_label.text.contains("현재 음악:"): + failures.append("in-game audio status should name the current BGM: %s" % scene.settings_audio_status_label.text) scene._on_settings_bgm_toggled(true) scene._on_settings_sfx_toggled(true) scene._on_settings_edge_scroll_toggled(true) @@ -480,6 +494,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("in-game settings should expose a clear audio defaults preview button") elif not scene.settings_audio_reset_button.tooltip_text.contains("현재 음악"): failures.append("in-game audio preview should explain that it plays the current music") + if scene.settings_audio_status_label != null and (not scene.settings_audio_status_label.text.contains("군막의 새벽") or not scene.settings_audio_status_label.text.contains("효과음 60%")): + failures.append("in-game audio status should reflect battle BGM and SFX volume: %s" % scene.settings_audio_status_label.text) scene._on_settings_bgm_toggled(false) scene._on_settings_sfx_toggled(false) scene._on_settings_bgm_volume_changed(20.0) @@ -490,6 +506,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("in-game audio recovery should re-enable BGM and SFX") if scene.title_bgm_volume_percent != BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT or scene.title_sfx_volume_percent != BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT: failures.append("in-game audio recovery should restore audible volume defaults") + if scene.settings_audio_status_label != null and (not scene.settings_audio_status_label.text.contains("군막의 새벽") or not scene.settings_audio_status_label.text.contains("%d%%" % BattleSceneScript.DEFAULT_AUDIO_VOLUME_PERCENT)): + failures.append("in-game audio status should refresh after defaults preview: %s" % scene.settings_audio_status_label.text) if not scene.recent_sfx_keys.has("menu_confirm"): failures.append("in-game audio preview should play an audible confirmation SFX") if not scene.recent_sfx_keys.has("footstep"): diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index d1e628e..5406f7e 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3338,7 +3338,7 @@ func _check_core_text_visibility(failures: Array[String]) -> void: scene._show_opening_prologue() _check_readable_text_control(failures, scene.opening_prologue_title_label, "opening title", 16) - _check_readable_text_control(failures, scene.opening_prologue_body_label, "opening body", 13) + _check_readable_text_control(failures, scene.opening_prologue_body_label, "opening body", 18) _check_readable_text_control(failures, scene.opening_prologue_step_label, "opening step", 11) _check_readable_text_control(failures, scene.opening_prologue_next_button, "opening next button", 12) _check_readable_text_control(failures, scene.opening_prologue_skip_button, "opening skip button", 11) @@ -3349,18 +3349,18 @@ func _check_core_text_visibility(failures: Array[String]) -> void: return scene._show_briefing() _check_readable_text_control(failures, scene.briefing_title_label, "briefing title", 18) - _check_readable_text_control(failures, scene.briefing_objective_label, "briefing objective", 12) - _check_readable_text_control(failures, scene.briefing_label, "briefing body", 12) + _check_readable_text_control(failures, scene.briefing_objective_label, "briefing objective", 14) + _check_readable_text_control(failures, scene.briefing_label, "briefing body", 15) scene._hide_opening_prologue() scene.battle_started = true scene.briefing_panel.visible = false scene.result_panel.visible = false scene._update_hud() _check_readable_text_control(failures, scene.status_label, "top turn status", 11) - _check_readable_text_control(failures, scene.objective_label, "top objective", 11) + _check_readable_text_control(failures, scene.objective_label, "top objective", 12) _check_readable_text_control(failures, scene.campaign_status_label, "top campaign status", 11) scene._show_command_notice("군령 갱신", "목표: 성채를 장악하라.\n주의: 조조가 퇴각하면 패한다.") - _check_readable_text_control(failures, scene.objective_notice_label, "command notice", 13) + _check_readable_text_control(failures, scene.objective_notice_label, "command notice", 14) _check_readable_text_control(failures, scene.auto_end_turn_prompt_label, "auto end turn prompt", 13) _check_readable_text_control(failures, scene.auto_end_turn_yes_button, "auto end turn confirm", 12) _check_readable_text_control(failures, scene.auto_end_turn_no_button, "auto end turn decline", 12) @@ -3369,8 +3369,8 @@ func _check_core_text_visibility(failures: Array[String]) -> void: scene.active_dialogue_lines = lines scene.active_dialogue_index = 0 scene._render_dialogue_line() - _check_readable_text_control(failures, scene.dialogue_speaker_label, "dialogue speaker", 14) - _check_readable_text_control(failures, scene.dialogue_text_label, "dialogue body", 13) + _check_readable_text_control(failures, scene.dialogue_speaker_label, "dialogue speaker", 16) + _check_readable_text_control(failures, scene.dialogue_text_label, "dialogue body", 16) scene.free() @@ -3399,12 +3399,16 @@ func _check_readable_text_control(failures: Array[String], control: Control, con failures.append("%s text should carry a thin outline for image-backed panels" % context) if font_size >= 13 and outline_size < 2: failures.append("%s text should carry a stronger outline on generated panels: %d" % [context, outline_size]) + if font_size >= 16 and outline_size < 3: + failures.append("%s large text should carry enough outline for QHD scaling: %d" % [context, outline_size]) var outline_color := control.get_theme_color("font_outline_color") if outline_color.a < 0.25: failures.append("%s outline should be visible enough: %s" % [context, str(outline_color)]) var shadow_color := control.get_theme_color("font_shadow_color") if shadow_color.a < 0.32: failures.append("%s text should keep a soft shadow: %s" % [context, str(shadow_color)]) + if control is Label and control.get_theme_constant("line_spacing") < 3: + failures.append("%s label should reserve readable Korean line spacing" % context) func _check_qhd_design_layout_contract(failures: Array[String]) -> void: @@ -3701,15 +3705,15 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_panel_style_frame(failures, scene.briefing_objective_panel, "briefing objective readable panel", Color(0.16, 0.13, 0.090, 1.0), 3) _check_panel_style_fill(failures, scene.briefing_camp_overview_panel, "briefing readable map panel", Color(0.58, 0.55, 0.47, 0.998)) _check_panel_style_frame(failures, scene.briefing_camp_overview_panel, "briefing readable map panel", Color(0.16, 0.13, 0.090, 1.0), 3) - _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.045, 0.045, 0.040, 0.982)) - _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.18, 0.17, 0.14, 1.0), 9) - _check_panel_style_fill(failures, scene.auto_end_turn_prompt_panel, "auto end turn prompt panel", Color(0.050, 0.045, 0.040, 0.992)) + _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.030, 0.028, 0.024, 0.996)) + _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.28, 0.23, 0.16, 1.0), 8) + _check_panel_style_fill(failures, scene.auto_end_turn_prompt_panel, "auto end turn prompt panel", Color(0.030, 0.028, 0.024, 0.996)) _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.025, 0.023, 0.020, 1.0)) _check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.26, 0.035, 0.030, 0.995)) - _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.58, 0.55, 0.48, 0.996)) - _check_panel_uses_generated_texture(failures, scene.dialogue_panel, "dialogue generated frame") - _check_panel_uses_generated_texture(failures, scene.dialogue_text_panel, "dialogue text generated frame") - _check_panel_uses_generated_texture(failures, scene.auto_end_turn_prompt_panel, "auto end turn generated frame") + _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.72, 0.69, 0.58, 0.998)) + _check_panel_uses_flat_text_surface(failures, scene.dialogue_panel, "dialogue flat text frame") + _check_panel_uses_flat_text_surface(failures, scene.dialogue_text_panel, "dialogue flat text scroll") + _check_panel_uses_flat_text_surface(failures, scene.auto_end_turn_prompt_panel, "auto end turn flat text frame") if scene.screen_backdrop == null: failures.append("ancient briefing screen should create an ink-wash backdrop") else: @@ -4308,6 +4312,23 @@ func _check_panel_uses_generated_texture(failures: Array[String], panel: PanelCo _check_panel_style_texture(failures, stylebox as StyleBoxTexture, label) +func _check_panel_uses_flat_text_surface(failures: Array[String], panel: PanelContainer, label: String) -> void: + if panel == null: + failures.append("%s missing" % label) + return + var stylebox := panel.get_theme_stylebox("panel") + if not (stylebox is StyleBoxFlat): + failures.append("%s should use a flat panel so text is not painted over a busy texture" % label) + return + if bool(panel.get_meta("generated_panel_texture", false)): + failures.append("%s should not mark a generated texture behind text" % label) + var style := stylebox as StyleBoxFlat + if style.bg_color.a < 0.98: + failures.append("%s should keep an opaque text backing: %s" % [label, str(style.bg_color)]) + if style.content_margin_left < 14 or style.content_margin_top < 10: + failures.append("%s should reserve breathing room around readable text" % label) + + func _check_panel_uses_generated_ornament_texture(failures: Array[String], panel: PanelContainer, label: String) -> void: if panel == null: failures.append("%s missing" % label)