From b1884274ec19877fd140f1cd80907dfbe4dbd789 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 17:54:09 +0900 Subject: [PATCH] Improve readable battle text previews --- scripts/scenes/battle_scene.gd | 177 +++++++++++++++++++++++++----- tools/smoke_chapter_one_polish.gd | 34 +++++- 2 files changed, 184 insertions(+), 27 deletions(-) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 6150864..3f22c30 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -146,12 +146,20 @@ 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_TITLE_FONT_SIZE := 22 +const BRIEFING_TITLE_MIN_FONT_SIZE := 18 +const BRIEFING_LOCATION_FONT_SIZE := 16 +const BRIEFING_LOCATION_MIN_FONT_SIZE := 13 const BRIEFING_OBJECTIVE_FONT_SIZE := 17 const BRIEFING_OBJECTIVE_MIN_FONT_SIZE := 14 const BRIEFING_BODY_FONT_SIZE := 17 const BRIEFING_BODY_MIN_FONT_SIZE := 15 const BRIEFING_OVERVIEW_FONT_SIZE := 16 const BRIEFING_OVERVIEW_MIN_FONT_SIZE := 13 +const BRIEFING_VISIBLE_BODY_LINES := 3 +const BRIEFING_VISIBLE_BODY_CHARS := 46 +const COMMAND_NOTICE_VISIBLE_BODY_LINES := 2 +const COMMAND_NOTICE_VISIBLE_BODY_CHARS := 44 const DIALOGUE_SPEAKER_FONT_SIZE := 20 const DIALOGUE_SPEAKER_MIN_FONT_SIZE := 16 const DIALOGUE_TEXT_FONT_SIZE := 19 @@ -2406,6 +2414,55 @@ func _set_fitted_label_text(label: Label, text: String, base_size: int, min_size _fit_label_font_size_to_text(label, text, base_size, min_size, available_size) +func _set_readable_preview_label_text( + label: Label, + full_text: String, + base_size: int, + min_size: int, + available_size: Vector2, + max_lines: int, + max_chars_per_line: int +) -> void: + if label == null: + return + var preview := _compact_visible_text(full_text, max_lines, max_chars_per_line) + label.text = preview + label.tooltip_text = full_text.strip_edges() + _fit_label_font_size_to_text(label, preview, base_size, min_size, available_size) + + +func _compact_visible_text(text: String, max_lines: int, max_chars_per_line: int) -> String: + var normalized := text.strip_edges() + if normalized.is_empty(): + return "" + var visible_lines: Array[String] = [] + var source_lines := normalized.split("\n") + var truncated := false + for raw_line in source_lines: + var line := str(raw_line).strip_edges() + if line.is_empty(): + continue + if visible_lines.size() >= max_lines: + truncated = true + break + var preview := _short_preview_text(line, max_chars_per_line) + if preview != line: + truncated = true + visible_lines.append(preview) + if visible_lines.is_empty(): + return "" + if truncated: + var last_index := visible_lines.size() - 1 + var last_line := visible_lines[last_index] + if not last_line.ends_with("..."): + var trimmed_limit: int = maxi(4, max_chars_per_line) + var trimmed_line := last_line + if trimmed_line.length() > trimmed_limit - 3: + trimmed_line = trimmed_line.substr(0, trimmed_limit - 3).strip_edges() + visible_lines[last_index] = "%s..." % trimmed_line + return _join_strings(visible_lines, "\n") + + func _fit_font_size_for_text(text: String, base_size: int, min_size: int, available_size: Vector2) -> int: if text.strip_edges().is_empty(): return base_size @@ -2897,7 +2954,13 @@ func _rebuild_opening_story_beat_strip() -> void: var page_title := str(page.get("title", "서문")) var tooltip := "%s · %s" % [record_text, page_title] if opening_prologue_progress_label != null: - opening_prologue_progress_label.text = tooltip + _set_fitted_label_text( + opening_prologue_progress_label, + tooltip, + 14, + 11, + opening_prologue_progress_label.custom_minimum_size + ) opening_prologue_progress_label.tooltip_text = "현재 기록: %s" % tooltip opening_prologue_story_strip.visible = false @@ -4152,7 +4215,7 @@ func _create_hud() -> void: briefing_title_label.custom_minimum_size = Vector2(656, 34) briefing_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER briefing_title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - _apply_label_style(briefing_title_label, UI_PARCHMENT_TEXT, 22) + _apply_label_style(briefing_title_label, UI_PARCHMENT_TEXT, BRIEFING_TITLE_FONT_SIZE) briefing_title_panel.add_child(briefing_title_label) briefing_seal_ribbon = _make_seal_ribbon(680, 10) @@ -4163,7 +4226,7 @@ func _create_hud() -> void: briefing_location_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART briefing_location_label.custom_minimum_size = Vector2(680, 26) briefing_location_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - _apply_label_style(briefing_location_label, UI_AGED_INK, 16) + _apply_label_style(briefing_location_label, UI_AGED_INK, BRIEFING_LOCATION_FONT_SIZE) briefing_column.add_child(briefing_location_label) briefing_start_button = Button.new() @@ -11506,20 +11569,14 @@ func _show_battle_start_notice_if_ready() -> void: 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)) + var goal_summary := _battle_start_goal_notice_summary() + var risk_summary := _battle_start_risk_notice_summary() + if not goal_summary.is_empty() and not risk_summary.is_empty(): + lines.append("목표: %s · 주의: %s" % [goal_summary, risk_summary]) + elif not goal_summary.is_empty(): + lines.append("목표: %s" % goal_summary) + elif not risk_summary.is_empty(): + lines.append("주의: %s" % risk_summary) var marker_summary := _format_battle_start_marker_summary() if not marker_summary.is_empty(): lines.append("표식: %s" % marker_summary) @@ -11528,6 +11585,50 @@ func _format_battle_start_notice_body() -> String: return _join_strings(lines, "\n") +func _battle_start_goal_notice_summary() -> String: + var progress_lines := state.get_objective_progress_lines(false, false) + var joined_progress := _join_strings(progress_lines, " · ") + if joined_progress.contains("성채"): + return "성채 장악" + if joined_progress.contains("깃발"): + return "깃발 확보" + if joined_progress.contains("격파"): + return "적군 격파" + if not joined_progress.is_empty(): + return _short_preview_text(joined_progress.replace(" (대기)", ""), 16) + var victory := str(state.objectives.get("victory", "")).strip_edges() + if victory.contains("성채"): + return "성채 장악" + if victory.contains("격파") or victory.contains("꺾"): + return "적군 격파" + return _short_preview_text(victory, 16) + + +func _battle_start_risk_notice_summary() -> String: + var risk_lines := state.get_defeat_progress_lines() + if not risk_lines.is_empty(): + var compact_parts: Array[String] = [] + for line in risk_lines: + var text := str(line).strip_edges() + if text.is_empty(): + continue + if text.contains("조조"): + compact_parts.append("조조 건재") + elif text.contains("잔여 군령"): + compact_parts.append(text.replace("잔여 군령 ", "") + "군령") + else: + compact_parts.append(_short_preview_text(text, 7)) + if compact_parts.size() >= 2: + break + return _short_preview_text(_join_strings(compact_parts, "/"), 14) + var defeat := str(state.objectives.get("defeat", "")).strip_edges() + if defeat.contains("조조"): + return "조조 퇴각" + if defeat.contains("턴") or defeat.contains("군령"): + return "군령 제한" + return _short_preview_text(defeat, 14) + + func _format_battle_start_marker_summary() -> String: var labels: Array[String] = [] var seen := {} @@ -11570,11 +11671,20 @@ func _battle_start_marker_text(label: String, kind: String, hint: String = "") - func _show_command_notice(title: String, body: String) -> void: var notice_lines := [title] var normalized_body := body.strip_edges() + var visible_body := _compact_visible_text( + normalized_body, + COMMAND_NOTICE_VISIBLE_BODY_LINES, + COMMAND_NOTICE_VISIBLE_BODY_CHARS + ) if not normalized_body.is_empty(): - notice_lines.append(normalized_body) + notice_lines.append(visible_body) + var full_notice_text := title + if not normalized_body.is_empty(): + full_notice_text = "%s\n%s" % [title, normalized_body] _rebuild_command_notice_icon_strip(title, normalized_body) if objective_notice_label != null: objective_notice_label.text = "\n".join(notice_lines) + objective_notice_label.tooltip_text = full_notice_text _fit_label_font_size_to_text( objective_notice_label, objective_notice_label.text, @@ -11584,6 +11694,7 @@ func _show_command_notice(title: String, body: String) -> void: ) if objective_notice_panel != null: objective_notice_panel.visible = true + objective_notice_panel.tooltip_text = full_notice_text objective_notice_timer = OBJECTIVE_NOTICE_DURATION if status_label != null: _update_hud() @@ -13326,25 +13437,39 @@ func _show_briefing() -> void: body += "\n" body += str(line) - briefing_title_label.text = _format_briefing_title(briefing) - briefing_location_label.text = _format_briefing_location(briefing) + _set_fitted_label_text( + briefing_title_label, + _format_briefing_title(briefing), + BRIEFING_TITLE_FONT_SIZE, + BRIEFING_TITLE_MIN_FONT_SIZE, + briefing_title_label.custom_minimum_size + ) + _set_fitted_label_text( + briefing_location_label, + _format_briefing_location(briefing), + BRIEFING_LOCATION_FONT_SIZE, + BRIEFING_LOCATION_MIN_FONT_SIZE, + briefing_location_label.custom_minimum_size + ) briefing_location_label.visible = not briefing_location_label.text.is_empty() briefing_objective_collapsed = false - briefing_objective_label.text = _format_briefing_objectives() - _fit_label_font_size_to_text( + _set_fitted_label_text( briefing_objective_label, - briefing_objective_label.text, + _format_briefing_objectives(), BRIEFING_OBJECTIVE_FONT_SIZE, - BRIEFING_OBJECTIVE_MIN_FONT_SIZE + BRIEFING_OBJECTIVE_MIN_FONT_SIZE, + briefing_objective_label.custom_minimum_size ) briefing_objective_label.visible = not briefing_objective_label.text.is_empty() _update_briefing_objective_visibility() - _set_fitted_label_text( + _set_readable_preview_label_text( briefing_label, body, BRIEFING_BODY_FONT_SIZE, BRIEFING_BODY_MIN_FONT_SIZE, - briefing_label.custom_minimum_size + briefing_label.custom_minimum_size, + BRIEFING_VISIBLE_BODY_LINES, + BRIEFING_VISIBLE_BODY_CHARS ) briefing_panel.visible = true _refresh_screen_backdrop_visibility() diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 9a72f49..e1b78e3 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -26,6 +26,7 @@ func _init() -> void: _check_battle_start_notice_sequence(failures) _check_readability_contract(failures) _check_first_battle_rendered_text_fit(failures) + _check_command_notice_readable_preview(failures) _check_korean_fallback_labels(failures) _check_dialogue_localization_and_side(failures) _check_event_dialogue_side_passthrough(failures) @@ -698,7 +699,9 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: "_fit_font_size_for_text", "_estimated_wrapped_text_height", "_estimated_wrapped_line_count", - "_weighted_text_length" + "_weighted_text_length", + "_compact_visible_text", + "_set_readable_preview_label_text" ]: if not scene.has_method(method_name): failures.append("missing text fit helper: %s" % method_name) @@ -730,6 +733,29 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: 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: failures.append("fitted briefing objective text should stay within its panel") + var compact_lines := str(scene._compact_visible_text( + "첫 줄은 그대로 보인다.\n둘째 줄도 보인다.\n셋째 줄은 숨긴다.", + 2, + 18 + )).split("\n") + if compact_lines.size() > 2: + failures.append("compact visible text should respect the requested line budget: %s" % str(compact_lines)) + + +func _check_command_notice_readable_preview(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + scene._create_hud() + var long_body := "목표: 조조나 하후돈으로 동쪽 성채에 올라 깃발을 붙들고, 제11군령 이후 나타나는 마지막 황건 잔당까지 격파하라.\n주의: 조조가 퇴각하거나 제22턴이 시작되면 패한다.\n보급: 마을과 치료소를 오가며 장기전을 버틴다." + scene._show_command_notice("전투 개시", long_body) + _check_label_fits_visible_area(scene, failures, scene.objective_notice_label, "long command notice") + var notice_lines: Array = [] + if scene.objective_notice_label != null: + notice_lines = str(scene.objective_notice_label.text).split("\n") + if notice_lines.size() > 3: + failures.append("long command notice should keep only title plus two readable body lines: %s" % str(notice_lines)) + if scene.objective_notice_label != null and not scene.objective_notice_label.tooltip_text.contains("마지막 황건 잔당"): + failures.append("long command notice should preserve the full text in its tooltip") + scene.free() func _check_first_battle_rendered_text_fit(failures: Array[String]) -> void: @@ -753,6 +779,12 @@ func _check_first_battle_rendered_text_fit(failures: Array[String]) -> void: _check_label_fits_visible_area(scene, failures, scene.briefing_objective_label, "opening briefing objective") _check_label_fits_visible_area(scene, failures, scene.briefing_camp_overview_label, "opening briefing map overview") _check_label_fits_visible_area(scene, failures, scene.briefing_label, "opening briefing body") + if scene.briefing_label != null: + var briefing_lines := str(scene.briefing_label.text).split("\n") + if briefing_lines.size() > BattleSceneScript.BRIEFING_VISIBLE_BODY_LINES: + failures.append("opening briefing body should keep a short visible summary: %s" % str(briefing_lines)) + if not scene.briefing_label.tooltip_text.contains("적은 마을과 성채"): + failures.append("opening briefing body should keep the full briefing text in its tooltip") var briefing: Dictionary = scene.state.get_briefing() var dialogue_lines: Array = []