diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index dffaebf..b877365 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -55,8 +55,8 @@ const UNIT_ARROW_ATTACK_DURATION := 0.36 const UNIT_COMMAND_ATTACK_DURATION := 0.38 const UNIT_HEAVY_ATTACK_DURATION := 0.34 const UNIT_TACTIC_CAST_DURATION := 0.42 -const DIALOGUE_PANEL_POSITION := Vector2(72, 356) -const DIALOGUE_PANEL_SIZE := Vector2(1100, 304) +const DIALOGUE_PANEL_POSITION := Vector2(60, 356) +const DIALOGUE_PANEL_SIZE := Vector2(1160, 304) const DIALOGUE_PORTRAIT_SIZE := Vector2(184, 262) const DIALOGUE_COLUMN_SIZE := Vector2(850, 266) const DIALOGUE_TEXT_SIZE := Vector2(830, 112) @@ -143,6 +143,7 @@ var briefing_location_label: Label var briefing_seal_ribbon: HBoxContainer var briefing_objective_panel: PanelContainer var briefing_objective_label: Label +var briefing_camp_overview_panel: PanelContainer var briefing_camp_overview_row: HBoxContainer var briefing_camp_overview_texture: TextureRect var briefing_camp_overview_fallback_label: Label @@ -184,6 +185,8 @@ var manual_save_button: Button var manual_load_button: Button var dialogue_row: HBoxContainer var dialogue_panel: PanelContainer +var dialogue_left_tassel: VBoxContainer +var dialogue_right_tassel: VBoxContainer var dialogue_portrait_panel: PanelContainer var dialogue_portrait_texture: TextureRect var dialogue_portrait_label: Label @@ -341,7 +344,7 @@ func _age_panel_style(style: StyleBoxFlat, variant: String, border_width: int) - style.set_border_width(SIDE_RIGHT, border_width + 1) style.content_margin_top += 2 style.content_margin_bottom += 2 - "edict", "notice_edict", "speech_scroll", "bamboo_slips": + "edict", "notice_edict", "speech_scroll", "bamboo_slips", "silk_map": style.set_border_width(SIDE_TOP, border_width + 2) style.set_border_width(SIDE_BOTTOM, border_width + 2) "edict_compact": @@ -425,6 +428,12 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: border = UI_INK_WASH border_width = 5 margin = 4 + "silk_map": + fill = Color(0.54, 0.38, 0.18, 0.992) + border = UI_SCROLL_EDGE + border_width = 4 + margin = 10 + shadow_size = 5 "compact": fill = Color(0.052, 0.078, 0.052, 0.982) border = UI_JADE_EDGE @@ -753,6 +762,64 @@ func _make_ornament_bar(width: float = 4.0, height: float = 0.0) -> ColorRect: return bar +func _make_edict_marker_stack(labels: Array, height: float = 84.0) -> VBoxContainer: + var stack := VBoxContainer.new() + stack.custom_minimum_size = Vector2(30, height) + stack.size_flags_vertical = Control.SIZE_EXPAND_FILL + stack.mouse_filter = Control.MOUSE_FILTER_IGNORE + stack.add_theme_constant_override("separation", 3) + for index in range(labels.size()): + var panel := PanelContainer.new() + panel.custom_minimum_size = Vector2(30, 22) + panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER + _apply_panel_style(panel, "speaker_seal" if index == 0 else "caption") + stack.add_child(panel) + + var label := Label.new() + label.text = str(labels[index]) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + label.custom_minimum_size = Vector2(18, 16) + _apply_label_style(label, UI_PARCHMENT_TEXT, 13) + panel.add_child(label) + return stack + + +func _make_hanging_tassel(height: float = 262.0) -> VBoxContainer: + var tassel := VBoxContainer.new() + tassel.custom_minimum_size = Vector2(18, height) + tassel.size_flags_vertical = Control.SIZE_EXPAND_FILL + tassel.mouse_filter = Control.MOUSE_FILTER_IGNORE + tassel.add_theme_constant_override("separation", 4) + + var top_seal := ColorRect.new() + top_seal.color = UI_SEAL_RED + top_seal.custom_minimum_size = Vector2(18, 18) + top_seal.mouse_filter = Control.MOUSE_FILTER_IGNORE + tassel.add_child(top_seal) + + var cords := HBoxContainer.new() + cords.custom_minimum_size = Vector2(18, maxf(0.0, height - 44.0)) + cords.size_flags_vertical = Control.SIZE_EXPAND_FILL + cords.mouse_filter = Control.MOUSE_FILTER_IGNORE + cords.add_theme_constant_override("separation", 4) + for index in range(2): + var cord := ColorRect.new() + cord.color = UI_TARNISHED_BRONZE if index == 0 else UI_CINNABAR_DARK + cord.custom_minimum_size = Vector2(6, height - 44.0) + cord.size_flags_vertical = Control.SIZE_EXPAND_FILL + cord.mouse_filter = Control.MOUSE_FILTER_IGNORE + cords.add_child(cord) + tassel.add_child(cords) + + var bottom_seal := ColorRect.new() + bottom_seal.color = UI_SEAL_RED_DARK + bottom_seal.custom_minimum_size = Vector2(18, 18) + bottom_seal.mouse_filter = Control.MOUSE_FILTER_IGNORE + tassel.add_child(bottom_seal) + return tassel + + func _create_hud() -> void: var layer := CanvasLayer.new() add_child(layer) @@ -1200,22 +1267,35 @@ func _create_hud() -> void: briefing_objective_panel.add_child(briefing_objective_row) briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) + briefing_objective_row.add_child(_make_edict_marker_stack(["令", "勝", "敗"], 84)) briefing_objective_label = Label.new() briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_objective_label.custom_minimum_size = Vector2(586, 84) + briefing_objective_label.custom_minimum_size = Vector2(500, 84) briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_label_style(briefing_objective_label, UI_AGED_INK, 15) briefing_objective_row.add_child(briefing_objective_label) + briefing_objective_row.add_child(_make_edict_marker_stack(["軍", "功", "封"], 84)) briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) briefing_column.add_child(_make_ink_wash_rule(680, 7)) briefing_column.add_child(_make_section_caption("비단 전장도 · 군막 장부", 680)) + briefing_camp_overview_panel = PanelContainer.new() + briefing_camp_overview_panel.custom_minimum_size = Vector2(680, 102) + _apply_panel_style(briefing_camp_overview_panel, "silk_map") + briefing_column.add_child(briefing_camp_overview_panel) + + var briefing_camp_overview_shell := HBoxContainer.new() + briefing_camp_overview_shell.add_theme_constant_override("separation", 10) + briefing_camp_overview_panel.add_child(briefing_camp_overview_shell) + briefing_camp_overview_shell.add_child(_make_edict_marker_stack(["圖", "地"], 72)) + briefing_camp_overview_row = HBoxContainer.new() - briefing_camp_overview_row.custom_minimum_size = Vector2(680, 84) + briefing_camp_overview_row.custom_minimum_size = Vector2(580, 84) briefing_camp_overview_row.add_theme_constant_override("separation", 10) - briefing_column.add_child(briefing_camp_overview_row) + briefing_camp_overview_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + briefing_camp_overview_shell.add_child(briefing_camp_overview_row) var camp_thumbnail_panel := PanelContainer.new() camp_thumbnail_panel.custom_minimum_size = BRIEFING_CAMP_THUMBNAIL_SIZE @@ -1243,10 +1323,11 @@ func _create_hud() -> void: briefing_camp_overview_label = Label.new() briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_camp_overview_label.custom_minimum_size = Vector2(520, 72) + briefing_camp_overview_label.custom_minimum_size = Vector2(420, 72) briefing_camp_overview_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 14) briefing_camp_overview_row.add_child(briefing_camp_overview_label) + briefing_camp_overview_shell.add_child(_make_bamboo_gutter(14, 72)) briefing_column.add_child(_make_scroll_rod(680, 3)) @@ -1474,9 +1555,12 @@ func _create_hud() -> void: root.add_child(dialogue_panel) dialogue_row = HBoxContainer.new() - dialogue_row.add_theme_constant_override("separation", 14) + dialogue_row.add_theme_constant_override("separation", 10) dialogue_panel.add_child(dialogue_row) + dialogue_left_tassel = _make_hanging_tassel(DIALOGUE_PORTRAIT_SIZE.y) + dialogue_row.add_child(dialogue_left_tassel) + dialogue_portrait_panel = PanelContainer.new() dialogue_portrait_panel.custom_minimum_size = DIALOGUE_PORTRAIT_SIZE _apply_panel_style(dialogue_portrait_panel, "portrait") @@ -1506,6 +1590,9 @@ func _create_hud() -> void: dialogue_column.add_theme_constant_override("separation", 7) dialogue_row.add_child(dialogue_column) + dialogue_right_tassel = _make_hanging_tassel(DIALOGUE_PORTRAIT_SIZE.y) + dialogue_row.add_child(dialogue_right_tassel) + dialogue_column.add_child(_make_tablet_binding(DIALOGUE_TEXT_SIZE.x, 13)) dialogue_seal_ribbon = _make_seal_ribbon(DIALOGUE_TEXT_SIZE.x, 18) @@ -4380,8 +4467,10 @@ func _normalized_dialogue_lines(lines: Array) -> Array: var text := str(line.get("text", "")) if text.is_empty(): continue + var speaker := str(line.get("speaker", "")) + var display_speaker := str(line.get("display_speaker", speaker)) result.append({ - "speaker": str(line.get("speaker", "")), + "speaker": display_speaker, "text": text, "portrait": str(line.get("portrait", "")), "side": _normalized_dialogue_side(line.get("side", "left")) @@ -4437,12 +4526,13 @@ func _render_dialogue_line() -> void: return var line: Dictionary = active_dialogue_lines[active_dialogue_index] var speaker := str(line.get("speaker", "")) + var display_speaker := str(line.get("display_speaker", speaker)) var side := _normalized_dialogue_side(line.get("side", "left")) _apply_dialogue_side(side) - dialogue_speaker_label.text = speaker - dialogue_speaker_label.visible = not speaker.is_empty() + dialogue_speaker_label.text = display_speaker + dialogue_speaker_label.visible = not display_speaker.is_empty() dialogue_text_label.text = str(line.get("text", "")) - _update_dialogue_portrait(speaker, str(line.get("portrait", ""))) + _update_dialogue_portrait(display_speaker, str(line.get("portrait", ""))) _update_dialogue_controls() dialogue_panel.visible = true _update_hud() @@ -4460,7 +4550,7 @@ func _update_dialogue_controls() -> void: func _apply_dialogue_side(side: String) -> void: if dialogue_row == null or dialogue_portrait_panel == null or dialogue_column == null: return - var portrait_index := 1 if side == "right" else 0 + var portrait_index: int = maxi(0, dialogue_row.get_child_count() - 2) if side == "right" else 1 dialogue_row.move_child(dialogue_portrait_panel, portrait_index) dialogue_speaker_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT if side == "right" else HORIZONTAL_ALIGNMENT_LEFT dialogue_text_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT if side == "right" else HORIZONTAL_ALIGNMENT_LEFT @@ -4880,6 +4970,8 @@ func _update_briefing_camp_overview(briefing: Dictionary, prep_locked: bool) -> return var overview_text := _format_briefing_camp_overview_text(briefing, prep_locked) briefing_camp_overview_row.visible = not overview_text.is_empty() + if briefing_camp_overview_panel != null: + briefing_camp_overview_panel.visible = not overview_text.is_empty() if briefing_camp_overview_label != null: briefing_camp_overview_label.text = overview_text var texture := _current_battle_background_texture() diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 9075e5a..4bb1841 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2650,6 +2650,8 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.20, 0.11, 0.045, 1.0), 10) _check_panel_style_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.66, 0.52, 0.31, 0.995)) _check_panel_style_frame(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.20, 0.11, 0.045, 1.0), 6) + _check_panel_style_fill(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.54, 0.38, 0.18, 0.992)) + _check_panel_style_frame(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.43, 0.25, 0.095, 0.996), 4) _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.070, 0.018, 0.010, 0.997)) _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.78, 0.58, 0.27, 1.0), 9) _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.035, 0.014, 0.008, 1.0)) @@ -2660,11 +2662,33 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_seal_ribbon(failures, scene.result_seal_ribbon, "result seal ribbon") _check_bamboo_gutter_row(failures, scene.briefing_objective_panel, "briefing objective bamboo gutters") _check_bamboo_gutter_row(failures, scene.dialogue_text_panel, "dialogue scroll bamboo gutters") + _check_edict_marker_stack(failures, scene.briefing_objective_panel, "briefing objective edict markers") + _check_hanging_tassel(failures, scene.dialogue_left_tassel, "dialogue left tassel") + _check_hanging_tassel(failures, scene.dialogue_right_tassel, "dialogue right tassel") _check_dialogue_column_budget(failures, scene) if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "다음 죽간": failures.append("dialogue continue button should use ancient slip text") if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "이전 죽간": failures.append("dialogue previous button should use ancient slip text") + var localized_lines := scene._normalized_dialogue_lines([{"speaker": "Cao Cao", "display_speaker": "조조", "text": "군령을 전하라."}]) + if localized_lines.is_empty() or str((localized_lines[0] as Dictionary).get("speaker", "")) != "조조": + failures.append("dialogue normalization should preserve display_speaker for localized names") + else: + scene.active_dialogue_lines = localized_lines + scene.active_dialogue_index = 0 + scene._render_dialogue_line() + if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.text != "조조": + failures.append("dialogue speaker label should render localized display_speaker") + scene._hide_dialogue_panel() + var right_lines := scene._normalized_dialogue_lines([{"speaker": "Xiahou Dun", "display_speaker": "하후돈", "side": "right", "text": "우익에서 응하겠습니다."}]) + if right_lines.is_empty(): + failures.append("dialogue normalization should keep right-side lines") + else: + scene.active_dialogue_lines = right_lines + scene.active_dialogue_index = 0 + scene._render_dialogue_line() + _check_right_side_dialogue_layout(failures, scene) + scene._hide_dialogue_panel() if scene._ui_font(false) == null: failures.append("ancient UI regular font should load from project assets") if scene._ui_font(true) == null: @@ -2748,6 +2772,57 @@ func _check_bamboo_gutter(failures: Array[String], node: Node, label: String) -> failures.append("%s should use three bamboo slips" % label) +func _check_edict_marker_stack(failures: Array[String], panel: PanelContainer, label: String) -> void: + if panel == null or panel.get_child_count() <= 0: + failures.append("%s missing row" % label) + return + var row := panel.get_child(0) as HBoxContainer + if row == null or row.get_child_count() < 5: + failures.append("%s should include side marker stacks" % label) + return + _check_marker_stack(failures, row.get_child(1), "%s left stack" % label) + _check_marker_stack(failures, row.get_child(row.get_child_count() - 2), "%s right stack" % label) + + +func _check_marker_stack(failures: Array[String], node: Node, label: String) -> void: + var stack := node as VBoxContainer + if stack == null: + failures.append("%s should be a vertical seal stack" % label) + return + if stack.get_child_count() < 2: + failures.append("%s should include multiple seal plaques" % label) + + +func _check_hanging_tassel(failures: Array[String], tassel: VBoxContainer, label: String) -> void: + if tassel == null: + failures.append("%s missing" % label) + return + if tassel.get_child_count() != 3: + failures.append("%s should use top seal, hanging cords, and bottom seal" % label) + return + var cords := tassel.get_child(1) as HBoxContainer + if cords == null or cords.get_child_count() != 2: + failures.append("%s should include two hanging cord strips" % label) + + +func _check_right_side_dialogue_layout(failures: Array[String], scene) -> void: + if scene.dialogue_row == null or scene.dialogue_row.get_child_count() != 4: + failures.append("right-side dialogue should keep four major columns") + return + if scene.dialogue_row.get_child(0) != scene.dialogue_left_tassel: + failures.append("right-side dialogue should keep the left tassel at the outer edge") + if scene.dialogue_row.get_child(1) != scene.dialogue_column: + failures.append("right-side dialogue should place speech scroll before the portrait") + if scene.dialogue_row.get_child(2) != scene.dialogue_portrait_panel: + failures.append("right-side dialogue should place portrait near the right tassel") + if scene.dialogue_row.get_child(3) != scene.dialogue_right_tassel: + failures.append("right-side dialogue should keep the right tassel at the outer edge") + if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.horizontal_alignment != HORIZONTAL_ALIGNMENT_RIGHT: + failures.append("right-side dialogue speaker label should align right") + if scene.dialogue_text_label == null or scene.dialogue_text_label.horizontal_alignment != HORIZONTAL_ALIGNMENT_RIGHT: + failures.append("right-side dialogue text should align right") + + func _check_dialogue_column_budget(failures: Array[String], scene) -> void: if scene.dialogue_column == null: failures.append("dialogue column missing")