From 65d6dff0f0d324709678eb057b055e11ae3f3598 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 22:51:06 +0900 Subject: [PATCH] Improve camp talk and merchant previews --- scripts/scenes/battle_scene.gd | 186 ++++++++++++++++++++++++++++++--- tools/smoke_visual_assets.gd | 11 ++ 2 files changed, 185 insertions(+), 12 deletions(-) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 4ece2ff..5071e5d 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -58,6 +58,8 @@ const DIALOGUE_PANEL_SIZE := Vector2(1040, 210) const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186) const DIALOGUE_COLUMN_SIZE := Vector2(820, 186) const DIALOGUE_TEXT_SIZE := Vector2(800, 104) +const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58) +const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50) const HUD_UNIT_PORTRAIT_SIZE := Vector2(92, 104) const HUD_UNIT_PORTRAIT_STACK_SIZE := Vector2(88, 100) const POST_MOVE_MENU_SIZE := Vector2(228, 120) @@ -4230,11 +4232,66 @@ func _rebuild_talk_menu() -> void: talk_list.add_child(empty_label) return for conversation in conversations: - var conversation_id := str(conversation.get("id", "")) - var button := Button.new() - button.text = _format_camp_conversation_button_text(conversation) - button.pressed.connect(_on_camp_conversation_pressed.bind(conversation_id)) - talk_list.add_child(button) + _add_camp_conversation_row(conversation) + + +func _add_camp_conversation_row(conversation: Dictionary) -> void: + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 64) + row.add_theme_constant_override("separation", 8) + talk_list.add_child(row) + + row.add_child(_make_camp_conversation_portrait(conversation)) + + var column := VBoxContainer.new() + column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + column.add_theme_constant_override("separation", 3) + row.add_child(column) + + var conversation_id := str(conversation.get("id", "")) + var button := Button.new() + button.text = _format_camp_conversation_row_title(conversation) + button.custom_minimum_size = Vector2(500, 32) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.pressed.connect(_on_camp_conversation_pressed.bind(conversation_id)) + column.add_child(button) + + var preview_label := Label.new() + preview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + preview_label.custom_minimum_size = Vector2(500, 24) + preview_label.text = _format_camp_conversation_preview_text(conversation) + column.add_child(preview_label) + + +func _make_camp_conversation_portrait(conversation: Dictionary) -> PanelContainer: + var portrait_panel := PanelContainer.new() + portrait_panel.custom_minimum_size = CAMP_TALK_PORTRAIT_SIZE + + var stack := Control.new() + stack.custom_minimum_size = CAMP_TALK_PORTRAIT_SIZE + portrait_panel.add_child(stack) + + var texture_rect := TextureRect.new() + texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + stack.add_child(texture_rect) + + var initials_label := Label.new() + initials_label.set_anchors_preset(Control.PRESET_FULL_RECT) + initials_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + initials_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + initials_label.add_theme_font_size_override("font_size", 18) + stack.add_child(initials_label) + + var speaker := _camp_conversation_preview_speaker(conversation) + var texture := _load_art_texture(_camp_conversation_preview_portrait_path(conversation)) + var has_texture := texture != null + texture_rect.texture = texture + texture_rect.visible = has_texture + initials_label.text = _dialogue_portrait_initials(speaker) + initials_label.visible = not has_texture + return portrait_panel func _format_talk_status_text(conversations: Array) -> String: @@ -4307,6 +4364,83 @@ func _format_camp_conversation_button_text(conversation: Dictionary) -> String: return _join_strings(parts, " - ") +func _format_camp_conversation_row_title(conversation: Dictionary) -> String: + var label := str(conversation.get("label", "Camp Talk")) + var speaker := _camp_conversation_preview_speaker(conversation) + var group := str(conversation.get("group", "topic")).capitalize() + var parts := [group, label] + if not speaker.is_empty(): + parts.append(speaker) + var effects_text := _format_camp_conversation_effects_text(conversation) + if not effects_text.is_empty(): + parts.append(effects_text) + return _join_strings(parts, " - ") + + +func _format_camp_conversation_preview_text(conversation: Dictionary) -> String: + var parts := [] + var summary := str(conversation.get("summary", "")).strip_edges() + if not summary.is_empty(): + parts.append(summary) + var first_text := _camp_conversation_preview_line(conversation) + if not first_text.is_empty(): + parts.append(_short_preview_text(first_text, 96)) + if parts.is_empty(): + return "No preview text" + return _join_strings(parts, " | ") + + +func _camp_conversation_preview_speaker(conversation: Dictionary) -> String: + var speaker := str(conversation.get("speaker", "")).strip_edges() + if not speaker.is_empty(): + return speaker + for line in conversation.get("lines", []): + if typeof(line) != TYPE_DICTIONARY: + continue + speaker = str(line.get("speaker", "")).strip_edges() + if not speaker.is_empty(): + return speaker + return "" + + +func _camp_conversation_preview_line(conversation: Dictionary) -> String: + for line in conversation.get("lines", []): + if typeof(line) != TYPE_DICTIONARY: + continue + var text := str(line.get("text", "")).strip_edges() + if not text.is_empty(): + return text + return "" + + +func _camp_conversation_preview_portrait_path(conversation: Dictionary) -> String: + var portrait := str(conversation.get("portrait", "")).strip_edges() + if not portrait.is_empty(): + return portrait + for line in conversation.get("lines", []): + if typeof(line) != TYPE_DICTIONARY: + continue + portrait = str(line.get("portrait", "")).strip_edges() + if not portrait.is_empty(): + return portrait + var officer_id := str(conversation.get("officer_id", "")).strip_edges() + if not officer_id.is_empty(): + portrait = state.data_catalog.get_portrait_for_officer(officer_id) + if not portrait.is_empty(): + return portrait + var speaker := _camp_conversation_preview_speaker(conversation) + if not speaker.is_empty(): + return state.data_catalog.get_portrait_for_speaker(speaker) + return "" + + +func _short_preview_text(text: String, max_length: int) -> String: + var normalized := text.strip_edges() + if max_length <= 3 or normalized.length() <= max_length: + return normalized + return "%s..." % normalized.substr(0, max_length - 3).strip_edges() + + func _on_camp_conversation_pressed(conversation_id: String) -> void: var conversation := _camp_conversation_by_id(conversation_id) if conversation.is_empty(): @@ -4978,23 +5112,51 @@ func _clear_shop_list() -> void: func _add_shop_merchant_notice() -> void: var merchant := state.get_shop_merchant() - if merchant.is_empty(): + var notice_text := _format_shop_merchant_notice_text(merchant) + if notice_text.is_empty(): return + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 58) + row.add_theme_constant_override("separation", 8) + shop_list.add_child(row) + + row.add_child(_make_initials_badge(str(merchant.get("name", "Merchant")), SHOP_MERCHANT_BADGE_SIZE, 18)) + + var merchant_label := Label.new() + merchant_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + merchant_label.custom_minimum_size = Vector2(500, 0) + merchant_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + merchant_label.text = notice_text + row.add_child(merchant_label) + + +func _format_shop_merchant_notice_text(merchant: Dictionary) -> String: + if merchant.is_empty(): + return "" var lines := [] for line in merchant.get("lines", []): var text := str(line).strip_edges() if not text.is_empty(): lines.append(text) if lines.is_empty(): - return - var merchant_label := Label.new() - merchant_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - merchant_label.custom_minimum_size = Vector2(560, 0) - merchant_label.text = "%s: %s" % [ + return "" + return "%s: %s" % [ str(merchant.get("name", "Merchant")), _join_strings(lines, " ") ] - shop_list.add_child(merchant_label) + + +func _make_initials_badge(label_text: String, badge_size: Vector2, font_size: int) -> PanelContainer: + var panel := PanelContainer.new() + panel.custom_minimum_size = badge_size + var label := Label.new() + label.set_anchors_preset(Control.PRESET_FULL_RECT) + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + label.add_theme_font_size_override("font_size", font_size) + label.text = _dialogue_portrait_initials(label_text) + panel.add_child(label) + return panel func _format_shop_item_button_text(item_id: String, item: Dictionary, price: int, owned: int, stock_limit: int = -1, remaining: int = -1) -> String: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 5088ee5..8452696 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -179,11 +179,22 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: failures.append("battle scene should find Cao Cao camp conversation by id") elif not scene._format_camp_conversation_button_text(strategy).contains("Cao Cao"): failures.append("camp conversation button text should include label/speaker") + elif scene._camp_conversation_preview_portrait_path(strategy).is_empty(): + failures.append("camp conversation preview should resolve Cao Cao portrait") + elif scene._load_art_texture(scene._camp_conversation_preview_portrait_path(strategy)) == null: + failures.append("camp conversation preview portrait should load") + elif not scene._format_camp_conversation_row_title(strategy).contains("Officer"): + failures.append("camp conversation row title should include group") + elif not scene._format_camp_conversation_preview_text(strategy).contains("Yingchuan"): + failures.append("camp conversation preview should include first dialogue text") var cache := scene._camp_conversation_by_id("northern_woods_cache") if cache.is_empty(): failures.append("battle scene should find cache camp conversation by id") elif not scene._format_camp_conversation_button_text(cache).contains("Supply Bean"): failures.append("camp conversation button text should preview supply effect") + var merchant_notice := scene._format_shop_merchant_notice_text(scene.state.get_shop_merchant()) + if not merchant_notice.contains("Camp Merchant") or not merchant_notice.contains("Roadside stores"): + failures.append("shop merchant notice should include name and flavor lines") scene.free() _check_scenario_background_texture_loading(failures)