diff --git a/README.md b/README.md index f73c0d3..8a33290 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - First named equipment rewards exist in the northern campaign arc and show a `[Named]` or `Named` tag in reward, inventory, shop, Armory, and equipment menus. - Scenario-specific pre-battle shop purchases, optional finite shop stock, and 50% sell-back save immediately and sync into the upcoming battle inventory. - Pre-battle shop rows emphasize item artwork, compact price/status badges, and hover details instead of dense inline text. -- Pre-battle Talk can list scenario camp conversations by officer or topic, branch them from saved campaign choices, grant one-time pre-battle supplies, and shop stock can include merchant flavor lines before a battle. +- Pre-battle Talk can list scenario camp conversations by officer or topic with portrait rows, compact topic/supply badges, hover dialogue previews, saved-branch filtering, one-time pre-battle supplies, and merchant flavor lines before a battle. - Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock, with portrait and item-icon rows plus hover change details. - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits, using portrait rows, compact status badges, and hover details. - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, coordinate badges, and hover placement instructions. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 4236c84..42f4dcf 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -10008,48 +10008,60 @@ func _rebuild_talk_menu() -> void: func _add_camp_conversation_row(conversation: Dictionary) -> void: + var tooltip_text := _format_camp_conversation_tooltip_text(conversation) var row := HBoxContainer.new() - row.custom_minimum_size = Vector2(580, 64) + row.custom_minimum_size = Vector2(580, 62) row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text talk_list.add_child(row) - row.add_child(_make_camp_conversation_portrait(conversation)) + row.add_child(_make_camp_conversation_portrait(conversation, tooltip_text)) var column := VBoxContainer.new() column.size_flags_horizontal = Control.SIZE_EXPAND_FILL - column.add_theme_constant_override("separation", 3) + column.add_theme_constant_override("separation", 4) + column.tooltip_text = tooltip_text 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.custom_minimum_size = Vector2(500, 30) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.tooltip_text = tooltip_text _apply_button_style(button) 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) - _apply_label_style(preview_label, UI_AGED_INK) - column.add_child(preview_label) + var badge_row := HBoxContainer.new() + badge_row.custom_minimum_size = Vector2(500, 22) + badge_row.add_theme_constant_override("separation", 5) + badge_row.tooltip_text = tooltip_text + column.add_child(badge_row) + + for badge_text in _format_camp_conversation_visible_badges(conversation): + var text := str(badge_text).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_armory_info_badge(text, tooltip_text)) -func _make_camp_conversation_portrait(conversation: Dictionary) -> PanelContainer: +func _make_camp_conversation_portrait(conversation: Dictionary, tooltip_text: String = "") -> PanelContainer: var portrait_panel := PanelContainer.new() portrait_panel.custom_minimum_size = CAMP_TALK_PORTRAIT_SIZE + portrait_panel.tooltip_text = tooltip_text _apply_panel_style(portrait_panel, "portrait") var stack := Control.new() stack.custom_minimum_size = CAMP_TALK_PORTRAIT_SIZE + stack.tooltip_text = tooltip_text 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 + texture_rect.tooltip_text = tooltip_text stack.add_child(texture_rect) var initials_label := Label.new() @@ -10057,6 +10069,7 @@ func _make_camp_conversation_portrait(conversation: Dictionary) -> PanelContaine initials_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER initials_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER initials_label.add_theme_font_size_override("font_size", 18) + initials_label.tooltip_text = tooltip_text _apply_label_style(initials_label, UI_PARCHMENT_TEXT) stack.add_child(initials_label) @@ -10143,16 +10156,37 @@ func _format_camp_conversation_button_text(conversation: Dictionary) -> String: func _format_camp_conversation_row_title(conversation: Dictionary) -> String: var label := str(conversation.get("label", "군막 담화")) var speaker := _camp_conversation_preview_speaker(conversation) - var group := _camp_conversation_group_text(str(conversation.get("group", "topic"))) - var parts := [group, label] + var parts := [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_visible_badges(conversation: Dictionary) -> Array: + var badges := [ + _camp_conversation_group_text(str(conversation.get("group", "topic"))) + ] + var speaker := _camp_conversation_preview_speaker(conversation) + if not speaker.is_empty(): + badges.append(speaker) + if _camp_conversation_has_grant_item_effect(conversation): + badges.append(_format_camp_conversation_supply_state_text(conversation)) + else: + badges.append("대화") + return badges + + +func _format_camp_conversation_tooltip_text(conversation: Dictionary) -> String: + var lines := [ + _format_camp_conversation_button_text(conversation) + ] + var preview := _format_camp_conversation_preview_text(conversation) + if not preview.is_empty(): + lines.append(preview) + lines.append("클릭하면 군막 대화를 엽니다.") + return _join_strings(lines, "\n") + + func _format_camp_conversation_preview_text(conversation: Dictionary) -> String: var parts := [] var summary := str(conversation.get("summary", "")).strip_edges() @@ -10218,6 +10252,20 @@ func _camp_conversation_preview_portrait_path(conversation: Dictionary) -> Strin return "" +func _camp_conversation_has_grant_item_effect(conversation: Dictionary) -> bool: + for effect in conversation.get("effects", []): + if typeof(effect) == TYPE_DICTIONARY and str(effect.get("type", "")) == "grant_item": + return true + return false + + +func _format_camp_conversation_supply_state_text(conversation: Dictionary) -> String: + var conversation_id := str(conversation.get("id", "")) + if campaign_state.has_claimed_camp_conversation_effects(active_scenario_id, conversation_id): + return "보급 완료" + 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: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 560e6cb..96be224 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2955,8 +2955,8 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: 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("장수"): - failures.append("camp conversation row title should include group") + elif not scene._format_camp_conversation_row_title(strategy).contains("조조 - 군략") or not scene._format_camp_conversation_row_title(strategy).contains("조조"): + failures.append("camp conversation row title should stay compact with label/speaker") elif not scene._format_camp_conversation_preview_text(strategy).contains("영천"): failures.append("camp conversation preview should include first dialogue text") var cache := scene._camp_conversation_by_id("northern_woods_cache") @@ -3008,6 +3008,17 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: failures.append("shop row tooltip should retain full item detail: %s" % shop_tooltips) if not _has_descendant_texture(scene.shop_list): failures.append("shop rows should use item icon artwork") + scene._rebuild_talk_menu() + var talk_visible_text := _collect_child_text(scene.talk_list) + var talk_tooltips := _collect_child_tooltips(scene.talk_list) + if not talk_visible_text.contains("조조 - 군략") or not talk_visible_text.contains("장수") or not talk_visible_text.contains("보급 대기"): + failures.append("talk rows should show compact labels and status badges: %s" % talk_visible_text) + if talk_visible_text.contains("영천") or talk_visible_text.contains("첫 행군") or talk_visible_text.contains("보급 콩") or talk_visible_text.contains("수령 가능"): + failures.append("talk rows should keep preview and supply details in hover text: %s" % talk_visible_text) + if not talk_tooltips.contains("영천") or not talk_tooltips.contains("보급 콩") or not talk_tooltips.contains("수령 가능"): + failures.append("talk row tooltip should retain dialogue preview and supply detail: %s" % talk_tooltips) + if not _has_descendant_texture(scene.talk_list): + failures.append("talk rows should use speaker portrait artwork") scene._rebuild_formation_menu() var formation_visible_text := _collect_child_text(scene.formation_list) var formation_tooltips := _collect_child_tooltips(scene.formation_list)