diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 5dec1fd..aff8f15 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -166,10 +166,8 @@ const TITLE_STORY_PREVIEW_IMAGE_FRAME_SIZE := Vector2(140, 74) const TITLE_STORY_PREVIEW_IMAGE_SIZE := Vector2(132, 66) const TITLE_STORY_PREVIEW_LABEL_SIZE := Vector2(140, 16) const TITLE_MENU_ICON_MAX_WIDTH := 42 -const OPENING_STORY_BEAT_CARD_SIZE := Vector2(136, 74) -const OPENING_STORY_BEAT_IMAGE_FRAME_SIZE := Vector2(126, 46) -const OPENING_STORY_BEAT_IMAGE_SIZE := Vector2(124, 38) -const OPENING_STORY_BEAT_LABEL_SIZE := Vector2(96, 18) +const OPENING_STORY_PROGRESS_CARD_SIZE := Vector2(50, 24) +const OPENING_STORY_PROGRESS_KNOT_SIZE := Vector2(18, 18) const COMMAND_NOTICE_ICON_SIZE := Vector2(32, 32) const COMMAND_NOTICE_MAX_ICONS := 4 const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54) @@ -1059,17 +1057,17 @@ func _make_button_texture_style(state_key: String, content_margin: int = 10, tex return style -func _make_icon_button_texture_style(state_key: String, content_margin: int = 6) -> StyleBoxTexture: +func _make_icon_button_texture_style(state_key: String, content_margin: int = 6, texture_margin := Vector2i(18, 18)) -> StyleBoxTexture: var texture := _load_art_texture(ICON_BUTTON_TEXTURE_PATH_TEMPLATE % state_key) if texture == null: return null var style := StyleBoxTexture.new() style.texture = texture style.draw_center = true - style.texture_margin_left = 72 - style.texture_margin_right = 72 - style.texture_margin_top = 72 - style.texture_margin_bottom = 72 + style.texture_margin_left = texture_margin.x + style.texture_margin_right = texture_margin.x + style.texture_margin_top = texture_margin.y + style.texture_margin_bottom = texture_margin.y var resolved_margin: int = maxi(content_margin, 5) style.content_margin_left = resolved_margin style.content_margin_right = resolved_margin @@ -1078,16 +1076,55 @@ func _make_icon_button_texture_style(state_key: String, content_margin: int = 6) return style +func _control_texture_target_size(control: Control, fallback := Vector2(96.0, 42.0)) -> Vector2: + if control == null: + return fallback + var size := control.custom_minimum_size + if size.x <= 0.0: + size.x = control.size.x + if size.y <= 0.0: + size.y = control.size.y + if size.x <= 0.0: + size.x = fallback.x + if size.y <= 0.0: + size.y = fallback.y + return size + + +func _text_button_texture_margin_for(button: Button, title_button: bool, opening_story_button: bool) -> Vector2i: + var target := _control_texture_target_size(button, Vector2(148.0, 44.0)) + var max_x := maxi(8, int(floor(target.x * 0.5)) - 3) + var max_y := maxi(6, int(floor(target.y * 0.5)) - 3) + var desired_x := int(floor(target.x * (0.15 if not title_button else 0.13))) + var desired_y := int(floor(target.y * (0.30 if not opening_story_button else 0.24))) + return Vector2i( + mini(max_x, maxi(16, mini(92, desired_x))), + mini(max_y, maxi(8, mini(42, desired_y))) + ) + + +func _icon_button_texture_margin_for(button: Button) -> Vector2i: + var target := _control_texture_target_size(button, TOP_TOOL_BUTTON_SIZE) + var max_x := maxi(6, int(floor(target.x * 0.5)) - 3) + var max_y := maxi(6, int(floor(target.y * 0.5)) - 3) + var desired_x := int(floor(target.x * 0.30)) + var desired_y := int(floor(target.y * 0.30)) + var margin := mini(mini(max_x, max_y), maxi(10, mini(desired_x, desired_y))) + return Vector2i(margin, margin) + + func _apply_generated_button_style(button: Button, important: bool = false) -> bool: var icon_only := bool(button.get_meta("icon_only", false)) var title_button := bool(button.get_meta("title_menu_button", false)) - var button_margin := 8 if title_button else 10 - var button_texture_margin := Vector2i(46, 18) if title_button else Vector2i(92, 42) - var normal_style := _make_icon_button_texture_style("pressed" if important else "normal") if icon_only else _make_button_texture_style("pressed" if important else "normal", button_margin, button_texture_margin) - var hover_style := _make_icon_button_texture_style("hover") if icon_only else _make_button_texture_style("hover", button_margin, button_texture_margin) - var pressed_style := _make_icon_button_texture_style("pressed") if icon_only else _make_button_texture_style("pressed", button_margin, button_texture_margin) - var focus_style := _make_icon_button_texture_style("hover") if icon_only else _make_button_texture_style("hover", button_margin, button_texture_margin) - var disabled_style := _make_icon_button_texture_style("disabled") if icon_only else _make_button_texture_style("disabled", button_margin, button_texture_margin) + var opening_story_button := bool(button.get_meta("opening_story_button", false)) + var button_margin := 6 if opening_story_button else (8 if title_button else 10) + var button_texture_margin := _text_button_texture_margin_for(button, title_button, opening_story_button) + var icon_texture_margin := _icon_button_texture_margin_for(button) + var normal_style := _make_icon_button_texture_style("pressed" if important else "normal", button_margin, icon_texture_margin) if icon_only else _make_button_texture_style("pressed" if important else "normal", button_margin, button_texture_margin) + var hover_style := _make_icon_button_texture_style("hover", button_margin, icon_texture_margin) if icon_only else _make_button_texture_style("hover", button_margin, button_texture_margin) + var pressed_style := _make_icon_button_texture_style("pressed", button_margin, icon_texture_margin) if icon_only else _make_button_texture_style("pressed", button_margin, button_texture_margin) + var focus_style := _make_icon_button_texture_style("hover", button_margin, icon_texture_margin) if icon_only else _make_button_texture_style("hover", button_margin, button_texture_margin) + var disabled_style := _make_icon_button_texture_style("disabled", button_margin, icon_texture_margin) if icon_only else _make_button_texture_style("disabled", button_margin, button_texture_margin) if normal_style == null or hover_style == null or pressed_style == null or focus_style == null or disabled_style == null: return false button.add_theme_stylebox_override("normal", normal_style) @@ -2645,17 +2682,38 @@ func _make_title_story_preview_card(title: String, image_path: String, index: in func _make_opening_story_beat_strip() -> PanelContainer: var panel := PanelContainer.new() panel.name = "OpeningStoryBeatStrip" - panel.position = Vector2(632, 24) - panel.size = Vector2(592, 88) - panel.custom_minimum_size = Vector2(592, 88) - panel.modulate = Color(1.0, 1.0, 1.0, 0.90) - _apply_panel_style(panel, "transparent_overlay") + panel.position = Vector2(900, 28) + panel.size = Vector2(318, 42) + panel.custom_minimum_size = Vector2(318, 42) + panel.modulate = Color(1.0, 1.0, 1.0, 0.88) + _apply_panel_style(panel, "caption") + var panel_style := panel.get_theme_stylebox("panel") + if panel_style != null: + panel_style.content_margin_left = 8 + panel_style.content_margin_right = 8 + panel_style.content_margin_top = 7 + panel_style.content_margin_bottom = 7 + + var rail_row := HBoxContainer.new() + rail_row.name = "OpeningStoryProgressRail" + rail_row.custom_minimum_size = Vector2(298, 26) + rail_row.add_theme_constant_override("separation", 8) + panel.add_child(rail_row) + + var rail_label := Label.new() + rail_label.name = "OpeningStoryProgressCaption" + rail_label.text = "전개" + rail_label.custom_minimum_size = Vector2(42, 24) + rail_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + rail_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _apply_label_style(rail_label, UI_TITLE_MUTED_TEXT, 12) + rail_row.add_child(rail_label) opening_prologue_story_strip = HBoxContainer.new() opening_prologue_story_strip.name = "OpeningStoryBeatRow" - opening_prologue_story_strip.custom_minimum_size = Vector2(568, 74) - opening_prologue_story_strip.add_theme_constant_override("separation", 8) - panel.add_child(opening_prologue_story_strip) + opening_prologue_story_strip.custom_minimum_size = Vector2(238, 24) + opening_prologue_story_strip.add_theme_constant_override("separation", 0) + rail_row.add_child(opening_prologue_story_strip) return panel @@ -2672,74 +2730,49 @@ func _rebuild_opening_story_beat_strip() -> void: func _make_opening_story_beat_card(page: Dictionary, index: int, is_current: bool) -> PanelContainer: var title := str(page.get("title", "장면")) - var image_path := str(page.get("image", "")) var tooltip := "장면 %d. %s" % [index + 1, title] var card := PanelContainer.new() card.name = "OpeningStoryBeatCard%d" % (index + 1) - card.custom_minimum_size = OPENING_STORY_BEAT_CARD_SIZE + card.custom_minimum_size = OPENING_STORY_PROGRESS_CARD_SIZE card.tooltip_text = tooltip card.set_meta("current_story_beat", is_current) - _apply_panel_style(card, "edict_compact" if is_current else "caption") + card.set_meta("story_progress_knot", true) + _apply_panel_style(card, "transparent_overlay") var card_style := card.get_theme_stylebox("panel") if card_style != null: - card_style.content_margin_left = 5 - 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.72, 0.70, 0.64, 0.94) + card_style.content_margin_left = 0 + card_style.content_margin_right = 0 + card_style.content_margin_top = 0 + card_style.content_margin_bottom = 0 + card.modulate = Color(1.0, 1.0, 1.0, 1.0) if is_current else Color(0.70, 0.68, 0.60, 0.78) - var column := VBoxContainer.new() - column.custom_minimum_size = Vector2(126, 66) - column.add_theme_constant_override("separation", 3) - column.tooltip_text = tooltip - card.add_child(column) + var row := HBoxContainer.new() + row.custom_minimum_size = OPENING_STORY_PROGRESS_CARD_SIZE + row.add_theme_constant_override("separation", 3) + row.tooltip_text = tooltip + card.add_child(row) - var title_row := HBoxContainer.new() - title_row.custom_minimum_size = Vector2(126, 18) - title_row.add_theme_constant_override("separation", 4) - title_row.tooltip_text = tooltip - column.add_child(title_row) + var thread := ColorRect.new() + thread.name = "OpeningStoryBeatThread" + thread.custom_minimum_size = Vector2(22, 3) + thread.size_flags_vertical = Control.SIZE_SHRINK_CENTER + thread.color = Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.0 if index == 0 else (0.82 if is_current else 0.42)) + thread.tooltip_text = tooltip + row.add_child(thread) - var seal := _make_seal_tile("%d" % (index + 1), 18) + var seal := _make_generated_ornament_panel("OpeningStoryBeatSeal", OPENING_STORY_PROGRESS_KNOT_SIZE, "command_seal") seal.name = "OpeningStoryBeatSeal" - 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() - label.name = "OpeningStoryBeatLabel" - label.text = title - label.custom_minimum_size = OPENING_STORY_BEAT_LABEL_SIZE - label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT - label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - label.clip_text = true - label.tooltip_text = tooltip - 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() - image_frame.name = "OpeningStoryBeatImageFrame" - image_frame.custom_minimum_size = OPENING_STORY_BEAT_IMAGE_FRAME_SIZE - image_frame.tooltip_text = tooltip - _apply_panel_style(image_frame, "caption") - var image_frame_style := image_frame.get_theme_stylebox("panel") - if image_frame_style != null: - image_frame_style.content_margin_left = 3 - image_frame_style.content_margin_right = 3 - image_frame_style.content_margin_top = 3 - image_frame_style.content_margin_bottom = 3 - column.add_child(image_frame) - - var image := TextureRect.new() - image.name = "OpeningStoryBeatImage" - image.custom_minimum_size = OPENING_STORY_BEAT_IMAGE_SIZE - image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED - image.texture = _load_art_texture(image_path) - image.modulate = Color(0.98, 0.99, 0.97, 0.99) if is_current else Color(0.74, 0.76, 0.72, 0.72) - image.tooltip_text = tooltip - image_frame.add_child(image) + seal.tooltip_text = tooltip + seal.modulate = Color(1.0, 0.95, 0.78, 1.0) if is_current else Color(0.62, 0.58, 0.48, 0.82) + if is_current: + var inner := ColorRect.new() + inner.name = "OpeningStoryBeatCurrentGlow" + inner.color = Color(UI_SEAL_RED.r, UI_SEAL_RED.g, UI_SEAL_RED.b, 0.46) + inner.position = Vector2(5, 5) + inner.size = Vector2(8, 8) + inner.mouse_filter = Control.MOUSE_FILTER_IGNORE + seal.add_child(inner) + row.add_child(seal) _set_control_tree_tooltip(card, tooltip) return card @@ -3851,28 +3884,32 @@ func _create_hud() -> void: opening_caption_row.add_child(opening_button_column) opening_prologue_skip_button = Button.new() - opening_prologue_skip_button.text = "건너뛰기" - opening_prologue_skip_button.icon = _make_toolbar_icon("cancel") + opening_prologue_skip_button.text = "바로 군막" + opening_prologue_skip_button.icon = _make_toolbar_icon("campaign") opening_prologue_skip_button.custom_minimum_size = Vector2(148, 40) - opening_prologue_skip_button.tooltip_text = "오프닝을 닫고 군막으로 이동합니다." + opening_prologue_skip_button.tooltip_text = "전기를 접고 군막으로 이동합니다." opening_prologue_skip_button.expand_icon = false - opening_prologue_skip_button.add_theme_constant_override("icon_max_width", 24) - opening_prologue_skip_button.add_theme_constant_override("h_separation", 8) + opening_prologue_skip_button.add_theme_constant_override("icon_max_width", 20) + opening_prologue_skip_button.add_theme_constant_override("h_separation", 7) opening_prologue_skip_button.icon_alignment = HORIZONTAL_ALIGNMENT_LEFT opening_prologue_skip_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + opening_prologue_skip_button.add_theme_font_size_override("font_size", 15) + opening_prologue_skip_button.set_meta("opening_story_button", true) _apply_button_style(opening_prologue_skip_button) opening_prologue_skip_button.pressed.connect(_on_opening_prologue_skip_pressed) opening_prologue_next_button = Button.new() - opening_prologue_next_button.text = "다음" + opening_prologue_next_button.text = "이어 보기" opening_prologue_next_button.icon = _make_toolbar_icon("move") opening_prologue_next_button.custom_minimum_size = Vector2(148, 48) - opening_prologue_next_button.tooltip_text = "다음 이야기로 넘깁니다." + opening_prologue_next_button.tooltip_text = "다음 장면을 펼칩니다." opening_prologue_next_button.expand_icon = false - opening_prologue_next_button.add_theme_constant_override("icon_max_width", 24) - opening_prologue_next_button.add_theme_constant_override("h_separation", 8) + opening_prologue_next_button.add_theme_constant_override("icon_max_width", 20) + opening_prologue_next_button.add_theme_constant_override("h_separation", 7) opening_prologue_next_button.icon_alignment = HORIZONTAL_ALIGNMENT_LEFT opening_prologue_next_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + opening_prologue_next_button.add_theme_font_size_override("font_size", 16) + opening_prologue_next_button.set_meta("opening_story_button", true) _apply_button_style(opening_prologue_next_button, true) opening_prologue_next_button.pressed.connect(_on_opening_prologue_next_pressed) opening_button_column.add_child(opening_prologue_next_button) @@ -11051,11 +11088,48 @@ func _format_battle_start_notice_body() -> String: var defeat := str(state.objectives.get("defeat", "")).strip_edges() if not defeat.is_empty(): lines.append("주의: %s" % _short_preview_text(defeat, 42)) + var marker_summary := _format_battle_start_marker_summary() + if not marker_summary.is_empty(): + lines.append("표식: %s" % marker_summary) if lines.is_empty(): lines.append("목표를 확인하고 첫 군령을 내리십시오.") return _join_strings(lines, "\n") +func _format_battle_start_marker_summary() -> String: + var labels: Array[String] = [] + var seen := {} + for marker in state.get_event_markers(): + var label := str(marker.get("label", "")).strip_edges() + if label.is_empty(): + continue + var kind := str(marker.get("kind", "event")).strip_edges().to_lower() + if not _battle_start_marker_is_tactical(label, kind): + continue + var text := _battle_start_marker_text(label, kind) + if text.is_empty() or seen.has(text): + continue + seen[text] = true + labels.append(text) + if labels.size() >= BRIEFING_TACTICAL_MARKER_MAX_COUNT: + break + return _join_strings(labels, " · ") + + +func _battle_start_marker_is_tactical(label: String, kind: String) -> bool: + if kind == "tactic" or kind == "supply" or kind == "gold" or kind == "recover": + return true + return label.contains("유인") or label.contains("보급") or label.contains("마을") or label.contains("숲") + + +func _battle_start_marker_text(label: String, kind: String) -> String: + if kind == "tactic" and not label.contains("유인"): + return "유인 %s" % label + if kind == "supply" and not label.contains("보급"): + return "보급 %s" % label + return label + + func _show_command_notice(title: String, body: String) -> void: var notice_lines := [title] var normalized_body := body.strip_edges() @@ -14038,9 +14112,9 @@ func _update_opening_prologue_page() -> void: _apply_opening_caption_text_style(opening_prologue_step_label, UI_OPENING_CAPTION_MUTED) if opening_prologue_next_button != null: var is_last_page := opening_prologue_index >= page_count - 1 - opening_prologue_next_button.text = "군막으로" if is_last_page else "다음" + opening_prologue_next_button.text = "군막으로" if is_last_page else "이어 보기" opening_prologue_next_button.icon = _make_toolbar_icon("campaign" if is_last_page else "move") - opening_prologue_next_button.tooltip_text = "군막으로 이동합니다." if is_last_page else "다음 이야기로 넘깁니다." + opening_prologue_next_button.tooltip_text = "군막으로 이동합니다." if is_last_page else "다음 장면을 펼칩니다." _rebuild_opening_story_beat_strip() diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 6520a7d..db09a79 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -567,14 +567,19 @@ func _check_battle_start_notice_sequence(failures: Array[String]) -> void: guard += 1 if scene._is_dialogue_visible(): failures.append("opening battle dialogue should be advanceable during start notice test") + var start_notice_text: String = "" if scene.objective_notice_label == null else scene.objective_notice_label.text 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") + elif not start_notice_text.contains("전투 개시"): + failures.append("battle start notice should use a clear Korean title: %s" % start_notice_text) + elif not start_notice_text.contains("목표:") or not start_notice_text.contains("주의:"): + failures.append("battle start notice should summarize objective and risk: %s" % start_notice_text) + elif not start_notice_text.contains("표식:") or not start_notice_text.contains("유인선") or not start_notice_text.contains("북숲 보급고") or not start_notice_text.contains("마을 보급"): + failures.append("battle start notice should summarize tactical event markers without making the player hunt for them: %s" % start_notice_text) + elif start_notice_text.contains("(20") or start_notice_text.contains("(21") or start_notice_text.contains(",1"): + failures.append("battle start notice should not expose raw grid coordinates: %s" % start_notice_text) + if scene.objective_notice_icon_strip == null or scene.objective_notice_icon_strip.get_child_count() < 3: + failures.append("battle start notice should use generated objective, warning, and marker icons") scene.free() diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index 183ef6f..43a027e 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -74,6 +74,15 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("title screen should show a high-resolution battlefield backdrop") elif BattleSceneScript.TITLE_BACKGROUND_PATH == BattleSceneScript.DEFAULT_BATTLE_BACKGROUND_PATH: failures.append("title screen should use a dedicated cinematic background, not the first battle map backdrop") + for button_spec in [ + {"button": scene.top_settings_button, "context": "top settings"}, + {"button": scene.end_turn_button, "context": "top end turn"}, + {"button": scene.wait_button, "context": "HUD wait"}, + {"button": scene.briefing_start_button, "context": "briefing start"} + ]: + var sampled_button := button_spec["button"] as Button + if sampled_button != null: + _check_button_texture_slice_fit(failures, sampled_button, str(button_spec["context"])) if scene.title_portrait_texture_rect == null or scene.title_portrait_texture_rect.texture == null: failures.append("title screen should show Cao Cao portrait art") elif scene.title_portrait_texture_rect.material == null or not bool(scene.title_portrait_texture_rect.material.get_meta("title_portrait_feather_material", false)): @@ -236,36 +245,45 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: if scene.opening_prologue_root != null: opening_story_strip = scene.opening_prologue_root.find_child("OpeningStoryBeatStrip", true, false) if opening_story_strip == null: - failures.append("opening prologue should show a generated four-beat story strip") + failures.append("opening prologue should show a compact four-beat story progress rail") else: + var progress_caption := opening_story_strip.find_child("OpeningStoryProgressCaption", true, false) as Label + if progress_caption == null or progress_caption.text != "전개": + failures.append("opening story progress rail should use a subtle Korean caption instead of exposed numeric cards") var opening_story_cards: Array = opening_story_strip.find_children("OpeningStoryBeatCard*", "", true, false) if opening_story_cards.size() != 4: - failures.append("opening story strip should expose four generated story beats: %d" % opening_story_cards.size()) + failures.append("opening story progress rail should expose four story knots: %d" % opening_story_cards.size()) var current_count := 0 var current_index := 0 for opening_story_card in opening_story_cards: var card_node := opening_story_card as Node - var beat_frame := card_node.find_child("OpeningStoryBeatImageFrame", true, false) as PanelContainer var beat_seal := card_node.find_child("OpeningStoryBeatSeal", true, false) as PanelContainer - var beat_image := card_node.find_child("OpeningStoryBeatImage", true, false) as TextureRect - var beat_label := card_node.find_child("OpeningStoryBeatLabel", true, false) as Label - if beat_frame == null or not bool(beat_frame.get_meta("generated_panel_texture", false)): - failures.append("opening story beat image should sit inside a generated frame") + var beat_thread := card_node.find_child("OpeningStoryBeatThread", true, false) as ColorRect + if not bool(card_node.get_meta("story_progress_knot", false)): + failures.append("opening story beat should be rendered as a compact progress knot") if beat_seal == null or not bool(beat_seal.get_meta("generated_panel_texture", false)): - failures.append("opening story beat should use a generated seal number") - if beat_image == null or beat_image.texture == null: - failures.append("opening story beat card should load generated artwork") - elif beat_image.custom_minimum_size.x < 116.0: - 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) + failures.append("opening story beat should use a generated knot seal") + if beat_thread == null: + failures.append("opening story beat should join knots with a subdued thread") + var numeric_labels := card_node.find_children("*", "Label", true, false) + for numeric_label in numeric_labels: + if str((numeric_label as Label).text).strip_edges() in ["1", "2", "3", "4"]: + failures.append("opening story progress should not expose bare numeric labels") if bool(card_node.get_meta("current_story_beat", false)): current_count += 1 current_index = opening_story_cards.find(opening_story_card) + 1 if current_count != 1 or current_index != 1: failures.append("opening story strip should highlight the first story beat on entry") + if opening_story_strip.custom_minimum_size.x > 360.0 or opening_story_strip.custom_minimum_size.y > 56.0: + failures.append("opening story progress rail should stay compact in the upper-right corner: %s" % str(opening_story_strip.custom_minimum_size)) + if scene.opening_prologue_next_button == null or scene.opening_prologue_next_button.text != "이어 보기": + failures.append("opening prologue next button should read as a story action") + else: + _check_opening_story_button_art(failures, scene.opening_prologue_next_button, "opening next") + if scene.opening_prologue_skip_button == null or scene.opening_prologue_skip_button.text != "바로 군막": + failures.append("opening prologue skip button should read as a contextual camp shortcut") + else: + _check_opening_story_button_art(failures, scene.opening_prologue_skip_button, "opening skip") if scene.opening_prologue_scene_texture_rect == null or scene.opening_prologue_scene_texture_rect.texture == null: failures.append("opening prologue should show generated story artwork") elif scene.opening_prologue_scene_texture_rect.custom_minimum_size.x <= scene.opening_prologue_scene_texture_rect.custom_minimum_size.y: @@ -479,10 +497,48 @@ func _check_title_menu_button_art(failures: Array[String], button: Button, expec failures.append("%s generated button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) if style.texture_margin_top > 24 or style.texture_margin_bottom > 24: failures.append("%s generated button slice should stay shallow enough for the 720p title menu: %d/%d" % [context, style.texture_margin_top, style.texture_margin_bottom]) + _check_button_texture_slice_fit(failures, button, context) if str(button.tooltip_text).strip_edges().is_empty(): failures.append("%s should describe its command through a tooltip" % context) +func _check_opening_story_button_art(failures: Array[String], button: Button, context: String) -> void: + if button == null: + failures.append("%s button missing" % context) + return + if not bool(button.get_meta("opening_story_button", false)): + failures.append("%s should use the compact opening-story button profile" % context) + if button.expand_icon: + failures.append("%s icon should not stretch in QHD fullscreen" % context) + if button.get_theme_constant("icon_max_width") > 22: + failures.append("%s icon should stay small beside Korean text" % context) + var stylebox := button.get_theme_stylebox("normal") + if not (stylebox is StyleBoxTexture): + failures.append("%s should keep a generated button texture" % context) + return + _check_button_texture_slice_fit(failures, button, context) + if button.get_theme_font_size("font_size") > 17: + failures.append("%s should stay visually quiet in the story caption" % context) + + +func _check_button_texture_slice_fit(failures: Array[String], button: Button, context: String) -> void: + if button == null: + return + var stylebox := button.get_theme_stylebox("normal") + if not (stylebox is StyleBoxTexture): + return + var style := stylebox as StyleBoxTexture + var size := button.custom_minimum_size + if size.x <= 0.0 or size.y <= 0.0: + size = button.get_combined_minimum_size() + var max_x := maxi(1, int(floor(size.x * 0.5)) - 2) + var max_y := maxi(1, int(floor(size.y * 0.5)) - 2) + if style.texture_margin_left > max_x or style.texture_margin_right > max_x: + failures.append("%s generated button horizontal slices should fit the control width: margins %d/%d size %.1f" % [context, style.texture_margin_left, style.texture_margin_right, size.x]) + if style.texture_margin_top > max_y or style.texture_margin_bottom > max_y: + failures.append("%s generated button vertical slices should fit the control height: margins %d/%d size %.1f" % [context, style.texture_margin_top, style.texture_margin_bottom, size.y]) + + func _check_title_command_fit(failures: Array[String], button: Button, context: String) -> void: if button == null: return diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index b2a460e..694eedb 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -4362,8 +4362,7 @@ func _check_button_uses_generated_surface(failures: Array[String], button: Butto failures.append("%s generated icon button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) if abs(style.texture.get_width() - style.texture.get_height()) > 4: failures.append("%s generated icon button texture should be square or near-square: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) - if style.texture_margin_left < 48 or style.texture_margin_top < 48: - failures.append("%s generated icon button texture should define visible frame slice margins" % context) + _check_button_texture_slice_for_control(failures, button, style, context, 10, 10) if style.content_margin_left < 5 or style.content_margin_top < 5: failures.append("%s generated icon button texture should reserve icon content margins" % context) else: @@ -4371,12 +4370,36 @@ func _check_button_uses_generated_surface(failures: Array[String], button: Butto failures.append("%s text button should keep the wide generated button surface" % context) if style.texture.get_width() < 512 or style.texture.get_height() < 192: failures.append("%s generated button texture should keep high-resolution source pixels: %dx%d" % [context, style.texture.get_width(), style.texture.get_height()]) - if style.texture_margin_left < 48 or style.texture_margin_top < 24: - failures.append("%s generated button texture should define visible frame slice margins" % context) - if style.content_margin_left < 8 or style.content_margin_top < 5: + var min_top_margin := 8 if bool(button.get_meta("opening_story_button", false)) else 10 + _check_button_texture_slice_for_control(failures, button, style, context, 16, min_top_margin) + var min_content_margin := 6 if bool(button.get_meta("opening_story_button", false)) else 8 + if style.content_margin_left < min_content_margin or style.content_margin_top < 5: failures.append("%s generated button texture should reserve readable content margins" % context) +func _check_button_texture_slice_for_control( + failures: Array[String], + button: Button, + style: StyleBoxTexture, + context: String, + min_x: int, + min_y: int +) -> void: + var size := button.custom_minimum_size + if size.x <= 0.0 or size.y <= 0.0: + size = button.get_combined_minimum_size() + var max_x := maxi(1, int(floor(size.x * 0.5)) - 2) + var max_y := maxi(1, int(floor(size.y * 0.5)) - 2) + if style.texture_margin_left < min_x or style.texture_margin_right < min_x: + failures.append("%s generated button texture should keep visible horizontal frame slices: %d/%d" % [context, style.texture_margin_left, style.texture_margin_right]) + if style.texture_margin_top < min_y or style.texture_margin_bottom < min_y: + failures.append("%s generated button texture should keep visible vertical frame slices: %d/%d" % [context, style.texture_margin_top, style.texture_margin_bottom]) + if style.texture_margin_left > max_x or style.texture_margin_right > max_x: + failures.append("%s generated button horizontal slices should fit the control width: margins %d/%d size %.1f" % [context, style.texture_margin_left, style.texture_margin_right, size.x]) + if style.texture_margin_top > max_y or style.texture_margin_bottom > max_y: + failures.append("%s generated button vertical slices should fit the control height: margins %d/%d size %.1f" % [context, style.texture_margin_top, style.texture_margin_bottom, size.y]) + + func _check_local_command_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void: if button == null: failures.append("local command button missing: %s" % expected_label)