diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 9b38a8c..e3b2b16 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -650,6 +650,7 @@ var recent_sfx_keys: Array[String] = [] var last_announced_battle_status := "" var floating_texts: Array[Dictionary] = [] var objective_notice_timer := 0.0 +var battle_start_notice_pending := false var unit_motion_by_unit: Dictionary = {} var unit_action_motion_by_unit: Dictionary = {} var battle_presentation_sequence_active := false @@ -2676,7 +2677,7 @@ func _make_opening_story_beat_card(page: Dictionary, index: int, is_current: boo card_style.content_margin_right = 5 card_style.content_margin_top = 4 card_style.content_margin_bottom = 3 - card.modulate = Color(1.0, 1.0, 1.0, 1.0) if is_current else Color(0.56, 0.55, 0.50, 0.88) + card.modulate = Color(1.0, 1.0, 1.0, 1.0) if is_current else Color(0.72, 0.70, 0.64, 0.94) var column := VBoxContainer.new() column.custom_minimum_size = Vector2(126, 66) @@ -2692,7 +2693,7 @@ func _make_opening_story_beat_card(page: Dictionary, index: int, is_current: boo var seal := _make_seal_tile("%d" % (index + 1), 18) seal.name = "OpeningStoryBeatSeal" - seal.modulate = Color(1.0, 1.0, 1.0, 1.0) if is_current else Color(0.70, 0.68, 0.62, 0.90) + seal.modulate = Color(1.0, 1.0, 1.0, 1.0) if is_current else Color(0.92, 0.87, 0.74, 0.96) title_row.add_child(seal) var label := Label.new() @@ -2703,7 +2704,9 @@ func _make_opening_story_beat_card(page: Dictionary, index: int, is_current: boo label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.clip_text = true label.tooltip_text = tooltip - _apply_label_style(label, UI_PARCHMENT_TEXT if is_current else UI_OLD_BRONZE, 9) + var label_color := UI_TITLE_TEXT if is_current else UI_TITLE_MUTED_TEXT + _apply_label_style(label, label_color, 10 if is_current else 9) + _apply_strong_text_legibility(label, label_color, 10 if is_current else 9) title_row.add_child(label) var image_frame := PanelContainer.new() @@ -11003,6 +11006,34 @@ func _on_objective_updated(victory: String, defeat: String) -> void: _show_command_notice("군령 갱신", _join_strings(body_lines, "\n")) +func _show_battle_start_notice_if_ready() -> void: + if not battle_start_notice_pending or _is_dialogue_visible(): + return + battle_start_notice_pending = false + _show_command_notice("전투 개시", _format_battle_start_notice_body()) + + +func _format_battle_start_notice_body() -> String: + var lines: Array[String] = [] + var progress_lines := state.get_objective_progress_lines(false, false) + if not progress_lines.is_empty(): + lines.append("목표: %s" % _short_preview_text(_join_strings(progress_lines, " · "), 52)) + else: + var victory := str(state.objectives.get("victory", "")).strip_edges() + if not victory.is_empty(): + lines.append("목표: %s" % _short_preview_text(victory, 52)) + var risk_lines := state.get_defeat_progress_lines() + if not risk_lines.is_empty(): + lines.append("주의: %s" % _short_preview_text(_join_strings(risk_lines, " · "), 42)) + else: + var defeat := str(state.objectives.get("defeat", "")).strip_edges() + if not defeat.is_empty(): + lines.append("주의: %s" % _short_preview_text(defeat, 42)) + if lines.is_empty(): + lines.append("목표를 확인하고 첫 군령을 내리십시오.") + return _join_strings(lines, "\n") + + func _show_command_notice(title: String, body: String) -> void: var notice_lines := [title] var normalized_body := body.strip_edges() @@ -11638,9 +11669,10 @@ func _advance_dialogue() -> void: _render_dialogue_line() return _hide_dialogue_panel() - if _can_show_dialogue_now(): + if _can_show_dialogue_now() and not dialogue_queue.is_empty(): _show_next_dialogue() else: + _show_battle_start_notice_if_ready() _update_hud() queue_redraw() @@ -11832,6 +11864,7 @@ func _on_restart_pressed() -> void: if log_box != null: log_box.clear() battle_started = false + battle_start_notice_pending = false battle_result_applied = false battle_result_summary.clear() post_battle_dialogue_started = false @@ -11880,6 +11913,7 @@ func _load_scenario(scenario_id: String) -> void: push_error("Missing scenario path for %s." % active_scenario_id) return battle_started = false + battle_start_notice_pending = false battle_result_applied = false battle_result_summary.clear() post_battle_dialogue_started = false @@ -12543,6 +12577,7 @@ func _on_begin_battle_pressed() -> void: func _start_battle_from_briefing() -> void: battle_started = true + battle_start_notice_pending = true _play_bgm("battle") if briefing_panel != null: briefing_panel.visible = false @@ -12558,6 +12593,7 @@ func _start_battle_from_briefing() -> void: state.run_battle_begin_events() _focus_board_on_player_deployment(true) _show_next_dialogue() + _show_battle_start_notice_if_ready() _update_hud() queue_redraw() diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 06ab533..6520a7d 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -23,6 +23,7 @@ func _init() -> void: _check_minimap_navigation_contract(failures) _check_map_focus_contract(failures) _check_battle_presentation_sequence_contract(failures) + _check_battle_start_notice_sequence(failures) _check_readability_contract(failures) _check_korean_fallback_labels(failures) _check_dialogue_localization_and_side(failures) @@ -550,6 +551,33 @@ func _check_battle_presentation_sequence_contract(failures: Array[String]) -> vo scene.free() +func _check_battle_start_notice_sequence(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + scene._ready() + scene._load_scenario("001_yellow_turbans") + scene._show_briefing() + scene._start_battle_from_briefing() + if not scene._is_dialogue_visible(): + failures.append("opening battle should begin with contextual dialogue before command notices") + if scene.objective_notice_label != null and scene.objective_notice_label.text.contains("전투 개시"): + failures.append("battle start notice should wait until opening dialogue is closed") + var guard := 0 + while scene._is_dialogue_visible() and guard < 12: + scene._advance_dialogue() + guard += 1 + if scene._is_dialogue_visible(): + failures.append("opening battle dialogue should be advanceable during start notice test") + if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: + failures.append("battle start should show a compact tactical command notice after dialogue") + elif scene.objective_notice_label == null or not scene.objective_notice_label.text.contains("전투 개시"): + failures.append("battle start notice should use a clear Korean title: %s" % ("" if scene.objective_notice_label == null else scene.objective_notice_label.text)) + elif not scene.objective_notice_label.text.contains("목표:") or not scene.objective_notice_label.text.contains("주의:"): + failures.append("battle start notice should summarize objective and risk: %s" % scene.objective_notice_label.text) + if scene.objective_notice_icon_strip == null or scene.objective_notice_icon_strip.get_child_count() < 2: + failures.append("battle start notice should use generated objective and warning icons") + scene.free() + + func _check_readability_contract(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index 91cff6c..76b8393 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -259,6 +259,8 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("opening story beat artwork should stay readable in the strip: %s" % str(beat_image.custom_minimum_size)) if beat_label == null or beat_label.text.strip_edges().is_empty(): failures.append("opening story beat card should name its scene") + elif beat_label.get_theme_color("font_color").get_luminance() < 0.66 or beat_label.get_theme_constant("outline_size") < 3: + failures.append("opening story beat label should remain readable over generated artwork: %s" % beat_label.text) if bool(card_node.get_meta("current_story_beat", false)): current_count += 1 current_index = opening_story_cards.find(opening_story_card) + 1