From 67ae1fe6f32708d36cebc93b1215805a6c6aac23 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 00:04:31 +0900 Subject: [PATCH] Compact prebattle shop rows --- README.md | 1 + scripts/scenes/battle_scene.gd | 85 ++++++++++++++++++++++++++++------ tools/smoke_visual_assets.gd | 41 +++++++++++++++- 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1f46f9a..dbc3f0f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Weapon, armor, and accessory rewards can be stored, displayed, compared, equipped, and unequipped from the battle HUD; some weapons deal bonus physical damage against matching move types. - 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 Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock. - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b84f2e8..3de6410 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -11535,6 +11535,7 @@ func _rebuild_shop_buy_list() -> void: var remaining := stock_limit - purchased if stock_limit >= 0 else -1 var buy_button := Button.new() var detail_text := _format_shop_buy_item_detail_text(normalized_id, item, price, owned, stock_limit, remaining) + var visible_badges := _format_shop_buy_visible_badges(item, price, owned, stock_limit, remaining) buy_button.text = _format_shop_buy_action_text(normalized_id, item, price) buy_button.custom_minimum_size = Vector2(500, 32) buy_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -11542,7 +11543,7 @@ func _rebuild_shop_buy_list() -> void: buy_button.disabled = campaign_state.gold < price or (stock_limit >= 0 and remaining <= 0) _apply_button_style(buy_button) buy_button.pressed.connect(_on_shop_item_pressed.bind(normalized_id)) - _add_shop_item_row(normalized_id, item, buy_button, detail_text) + _add_shop_item_row(normalized_id, item, buy_button, detail_text, visible_badges) func _rebuild_shop_sell_list() -> void: @@ -11562,13 +11563,14 @@ func _rebuild_shop_sell_list() -> void: var owned := int(inventory.get(normalized_id, 0)) var sell_button := Button.new() var detail_text := _format_shop_sell_item_detail_text(normalized_id, item, sale_price, owned) + var visible_badges := _format_shop_sell_visible_badges(item, owned) sell_button.text = _format_shop_sell_action_text(normalized_id, item, sale_price) sell_button.custom_minimum_size = Vector2(500, 32) sell_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL sell_button.tooltip_text = _format_shop_item_tooltip_text(_format_shop_sell_item_button_text(normalized_id, item, sale_price, owned), detail_text) _apply_button_style(sell_button) sell_button.pressed.connect(_on_shop_sell_item_pressed.bind(normalized_id)) - _add_shop_item_row(normalized_id, item, sell_button, detail_text) + _add_shop_item_row(normalized_id, item, sell_button, detail_text, visible_badges) func _clear_shop_list() -> void: @@ -11629,42 +11631,55 @@ func _make_initials_badge(label_text: String, badge_size: Vector2, font_size: in return panel -func _add_shop_item_row(item_id: String, item: Dictionary, action_button: Button, detail_text: String) -> void: +func _add_shop_item_row(item_id: String, item: Dictionary, action_button: Button, detail_text: String, visible_badges: Array) -> void: + var tooltip_text := action_button.tooltip_text + if tooltip_text.is_empty(): + tooltip_text = detail_text var row := HBoxContainer.new() - row.custom_minimum_size = Vector2(580, 58) + row.custom_minimum_size = Vector2(580, 64) row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text shop_list.add_child(row) - row.add_child(_make_shop_item_icon_panel(item_id, item)) + row.add_child(_make_shop_item_icon_panel(item_id, item, tooltip_text)) var column := VBoxContainer.new() column.size_flags_horizontal = Control.SIZE_EXPAND_FILL column.add_theme_constant_override("separation", 3) + column.tooltip_text = tooltip_text row.add_child(column) column.add_child(action_button) - var detail_label := Label.new() - detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - detail_label.custom_minimum_size = Vector2(500, 20) - detail_label.text = detail_text - _apply_label_style(detail_label, UI_AGED_INK) - column.add_child(detail_label) + var badge_row := HBoxContainer.new() + badge_row.custom_minimum_size = Vector2(500, 24) + badge_row.add_theme_constant_override("separation", 5) + badge_row.tooltip_text = tooltip_text + column.add_child(badge_row) + + for badge_text in visible_badges: + var text := str(badge_text).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_shop_item_info_badge(text, tooltip_text)) -func _make_shop_item_icon_panel(item_id: String, item: Dictionary) -> PanelContainer: +func _make_shop_item_icon_panel(item_id: String, item: Dictionary, tooltip_text: String = "") -> PanelContainer: var icon_panel := PanelContainer.new() icon_panel.custom_minimum_size = SHOP_ITEM_ICON_SIZE + icon_panel.tooltip_text = tooltip_text _apply_panel_style(icon_panel, "portrait") var stack := Control.new() stack.custom_minimum_size = SHOP_ITEM_ICON_SIZE + stack.tooltip_text = tooltip_text icon_panel.add_child(stack) var icon_rect := TextureRect.new() icon_rect.set_anchors_preset(Control.PRESET_FULL_RECT) icon_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon_rect.tooltip_text = tooltip_text stack.add_child(icon_rect) var initials_label := Label.new() @@ -11672,6 +11687,7 @@ func _make_shop_item_icon_panel(item_id: String, item: Dictionary) -> PanelConta initials_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER initials_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER initials_label.add_theme_font_size_override("font_size", 16) + initials_label.tooltip_text = tooltip_text _apply_label_style(initials_label, UI_PARCHMENT_TEXT) stack.add_child(initials_label) @@ -11685,11 +11701,52 @@ func _make_shop_item_icon_panel(item_id: String, item: Dictionary) -> PanelConta func _format_shop_buy_action_text(item_id: String, _item: Dictionary, price: int) -> String: - return "매입 %s %d냥" % [_item_display_name(item_id), price] + return "%s %d냥" % [_item_display_name(item_id), price] func _format_shop_sell_action_text(item_id: String, _item: Dictionary, sale_price: int) -> String: - return "매각 %s %d냥" % [_item_display_name(item_id), sale_price] + return "%s %d냥" % [_item_display_name(item_id), sale_price] + + +func _format_shop_buy_visible_badges(item: Dictionary, price: int, owned: int, stock_limit: int = -1, remaining: int = -1) -> Array: + var badges := _shop_item_common_visible_badges(item, owned) + if stock_limit >= 0: + badges.append("잔량 %d/%d" % [max(0, remaining), stock_limit]) + if stock_limit >= 0 and remaining <= 0: + badges.append("매진") + elif campaign_state.gold < price: + badges.append("부족 %d냥" % (price - campaign_state.gold)) + return badges + + +func _format_shop_sell_visible_badges(item: Dictionary, owned: int) -> Array: + var badges := _shop_item_common_visible_badges(item, owned) + badges.append("반값") + return badges + + +func _shop_item_common_visible_badges(item: Dictionary, owned: int) -> Array: + return [ + _item_kind_text(str(item.get("kind", "item"))), + "보유 %d" % owned + ] + + +func _make_shop_item_info_badge(text: String, tooltip_text: String) -> PanelContainer: + var panel := PanelContainer.new() + panel.custom_minimum_size = Vector2(92, 24) + panel.tooltip_text = tooltip_text + _apply_panel_style(panel, "caption") + + 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.text = text + label.tooltip_text = tooltip_text + _apply_label_style(label, UI_PARCHMENT_TEXT, 11) + panel.add_child(label) + return panel func _format_shop_buy_item_detail_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 9dfb40b..27ff995 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2986,14 +2986,28 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: if not sell_detail.contains("반값으로 넘김") or not sell_detail.contains("보유 2점"): failures.append("shop sell detail should summarize sale rule and owned count") var bean_action := scene._format_shop_buy_action_text("bean", bean, int(bean.get("price", 0))) - if not bean_action.contains("매입 콩") or not bean_action.contains("50냥"): - failures.append("shop buy action should be concise and priced") + if bean_action.contains("매입") or not bean_action.contains("콩") or not bean_action.contains("50냥"): + failures.append("shop buy action should stay compact and priced") var bean_tooltip := scene._format_shop_item_tooltip_text( scene._format_shop_item_button_text("bean", bean, int(bean.get("price", 0)), 0), bean_buy_detail ) if not bean_tooltip.contains("\n") or not bean_tooltip.contains("보유 0점"): failures.append("shop item tooltip should preserve full item detail") + scene._create_hud() + scene.campaign_state.gold = 320 + scene.shop_sell_mode = false + scene._rebuild_shop_menu() + var shop_visible_text := _collect_child_text(scene.shop_list) + var shop_tooltips := _collect_child_tooltips(scene.shop_list) + if shop_visible_text.contains("병력 +20") or shop_visible_text.contains("보유 0점") or shop_visible_text.contains("잔량 0/"): + failures.append("shop rows should keep effects and inventory details in hover text: %s" % shop_visible_text) + if not shop_visible_text.contains("콩") or not shop_visible_text.contains("50냥"): + failures.append("shop row should still show item name and price compactly: %s" % shop_visible_text) + if not shop_tooltips.contains("병력 +20") or not shop_tooltips.contains("보유 0점"): + 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.free() _check_scenario_background_texture_loading(failures) @@ -3556,6 +3570,29 @@ func _collect_child_tooltips_into(root: Node, parts: Array[String]) -> void: _collect_child_tooltips_into(child, parts) +func _collect_child_text(root: Node) -> String: + if root == null: + return "" + var parts: Array[String] = [] + _collect_child_text_into(root, parts) + return "\n".join(parts) + + +func _collect_child_text_into(root: Node, parts: Array[String]) -> void: + if root == null: + return + if root is Button: + var button_text := str((root as Button).text).strip_edges() + if not button_text.is_empty(): + parts.append(button_text) + elif root is Label: + var label_text := str((root as Label).text).strip_edges() + if not label_text.is_empty(): + parts.append(label_text) + for child in root.get_children(): + _collect_child_text_into(child, parts) + + func _has_descendant_texture(root: Node) -> bool: if root == null: return false