Compact camp conversation rows

This commit is contained in:
2026-06-20 00:24:20 +09:00
parent 18b073f937
commit 3971ff07e6
3 changed files with 78 additions and 19 deletions

View File

@@ -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.

View File

@@ -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:

View File

@@ -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)