diff --git a/README.md b/README.md index ac2ca16..457c2da 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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 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. - Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index e23afaf..986d3d4 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -11473,12 +11473,7 @@ func _rebuild_armory_menu() -> void: for unit in units: var unit_id := str(unit.get("id", "")) - var unit_button := Button.new() - unit_button.text = _format_armory_unit_button_text(unit) - _apply_button_style(unit_button) - unit_button.disabled = unit_id == armory_unit_id - unit_button.pressed.connect(_on_armory_unit_pressed.bind(unit_id)) - armory_list.add_child(unit_button) + _add_armory_unit_row(unit, unit_id == armory_unit_id) var selected := state.get_unit(armory_unit_id) if selected.is_empty(): @@ -11486,7 +11481,8 @@ func _rebuild_armory_menu() -> void: var equipment_label := Label.new() equipment_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - equipment_label.text = "소지 병장: %s" % _format_unit_equipment(selected) + equipment_label.text = "선택: %s" % str(selected.get("name", "장수")) + equipment_label.tooltip_text = "소지 병장: %s" % _format_unit_equipment(selected) _apply_label_style(equipment_label, UI_AGED_INK) armory_list.add_child(equipment_label) @@ -11502,10 +11498,18 @@ func _rebuild_armory_menu() -> void: var item_id := _equipment_item_id(equipment, normalized_slot) var item := state.get_item_def(item_id) var unequip_button := Button.new() - unequip_button.text = _format_equipment_unequip_text(selected, normalized_slot, item_id, item) + var tooltip_text := "%s\n클릭하면 군수고로 돌려보냅니다." % _format_equipment_unequip_text(selected, normalized_slot, item_id, item) + unequip_button.text = _format_armory_unequip_button_text(item_id) + unequip_button.tooltip_text = tooltip_text _apply_button_style(unequip_button) unequip_button.pressed.connect(_on_armory_unequip_pressed.bind(normalized_slot)) - armory_list.add_child(unequip_button) + _add_armory_equipment_row( + item_id, + item, + unequip_button, + tooltip_text, + _format_armory_unequip_badges(normalized_slot, item) + ) var item_ids := state.get_equippable_item_ids(armory_unit_id) if item_ids.is_empty(): @@ -11519,15 +11523,24 @@ func _rebuild_armory_menu() -> void: var normalized_id := str(item_id) var item := state.get_item_def(normalized_id) var armory_equip_button := Button.new() - armory_equip_button.text = _format_equipment_option_text( + var owned := int(inventory.get(normalized_id, 0)) + var tooltip_text := "%s\n클릭하면 선택 장수에게 장착합니다." % _format_equipment_option_text( selected, normalized_id, item, - int(inventory.get(normalized_id, 0)) + owned ) + armory_equip_button.text = _format_armory_equip_button_text(normalized_id) + armory_equip_button.tooltip_text = tooltip_text _apply_button_style(armory_equip_button) armory_equip_button.pressed.connect(_on_armory_equip_pressed.bind(normalized_id)) - armory_list.add_child(armory_equip_button) + _add_armory_equipment_row( + normalized_id, + item, + armory_equip_button, + tooltip_text, + _format_armory_equip_badges(item, owned) + ) func _clear_armory_list() -> void: @@ -11544,13 +11557,168 @@ func _find_unit_in_list(units: Array, unit_id: String) -> Dictionary: func _format_armory_unit_button_text(unit: Dictionary) -> String: - var marker := "> " if str(unit.get("id", "")) == armory_unit_id else "" - return "%s%s 품계 %d %s" % [ - marker, - str(unit.get("name", "장수")), - int(unit.get("level", 1)), - _format_unit_equipment(unit) + return str(unit.get("name", "장수")) + + +func _add_armory_unit_row(unit: Dictionary, selected: bool) -> void: + var unit_id := str(unit.get("id", "")) + var tooltip_text := _format_armory_unit_tooltip(unit, selected) + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 62) + row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text + armory_list.add_child(row) + + row.add_child(_make_roster_unit_portrait(unit, tooltip_text)) + + var column := VBoxContainer.new() + column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + column.add_theme_constant_override("separation", 4) + column.tooltip_text = tooltip_text + row.add_child(column) + + var unit_button := Button.new() + unit_button.text = _format_armory_unit_button_text(unit) + unit_button.custom_minimum_size = Vector2(500, 30) + unit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + unit_button.disabled = selected + unit_button.tooltip_text = tooltip_text + _apply_button_style(unit_button) + unit_button.pressed.connect(_on_armory_unit_pressed.bind(unit_id)) + column.add_child(unit_button) + + 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_armory_unit_badges(unit, selected): + var text := str(badge_text).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_armory_info_badge(text, tooltip_text)) + + +func _format_armory_unit_badges(unit: Dictionary, selected: bool) -> Array: + var badges := [ + "선택" if selected else "대상", + "품계 %d" % int(unit.get("level", 1)), + _unit_class_display_name(unit) ] + return badges + + +func _format_armory_unit_tooltip(unit: Dictionary, selected: bool) -> String: + var lines := [ + "%s · 품계 %d · %s" % [ + str(unit.get("name", "장수")), + int(unit.get("level", 1)), + _unit_class_display_name(unit) + ], + "병장: %s" % _format_unit_equipment(unit) + ] + if selected: + lines.append("현재 병장을 바꿀 장수입니다.") + else: + lines.append("클릭하면 이 장수의 병장 장부를 엽니다.") + return _join_strings(lines, "\n") + + +func _add_armory_equipment_row(item_id: String, item: Dictionary, action_button: Button, tooltip_text: String, visible_badges: Array) -> void: + var row := HBoxContainer.new() + row.custom_minimum_size = Vector2(580, 62) + row.add_theme_constant_override("separation", 8) + row.tooltip_text = tooltip_text + armory_list.add_child(row) + + 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", 4) + column.tooltip_text = tooltip_text + row.add_child(column) + + action_button.custom_minimum_size = Vector2(500, 30) + action_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + column.add_child(action_button) + + 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 visible_badges: + var text := str(badge_text).strip_edges() + if text.is_empty(): + continue + badge_row.add_child(_make_armory_info_badge(text, tooltip_text)) + + +func _format_armory_equip_button_text(item_id: String) -> String: + return _item_display_name(item_id) + + +func _format_armory_unequip_button_text(item_id: String) -> String: + return "해제 %s" % _item_display_name(item_id) + + +func _format_armory_equip_badges(item: Dictionary, owned: int) -> Array: + var slot := _equipment_slot_for_display(item) + var badges := [ + _equipment_slot_display_name(slot), + "보유 %d" % owned + ] + var stat_text := _format_equipment_primary_badge(item) + if not stat_text.is_empty(): + badges.append(stat_text) + return badges + + +func _format_armory_unequip_badges(slot: String, item: Dictionary) -> Array: + var badges := [ + "해제", + _equipment_slot_display_name(slot) + ] + var stat_text := _format_equipment_primary_badge(item) + if not stat_text.is_empty(): + badges.append(stat_text) + return badges + + +func _format_equipment_primary_badge(item: Dictionary) -> String: + if item.is_empty(): + return "" + var rarity_text := _format_equipment_rarity_text(item) + if not rarity_text.is_empty(): + return rarity_text + var bonuses: Dictionary = item.get("bonuses", {}) + for stat in ["atk", "def", "int", "agi", "hp", "mp"]: + var amount := int(bonuses.get(stat, 0)) + if amount == 0: + continue + var sign := "+" if amount > 0 else "" + return "%s %s%d" % [_battle_stat_display_name(stat), sign, amount] + return _item_kind_text(str(item.get("kind", "item"))) + + +func _make_armory_info_badge(text: String, tooltip_text: String) -> PanelContainer: + var panel := PanelContainer.new() + panel.custom_minimum_size = Vector2(86, 22) + 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 _on_armory_unit_pressed(unit_id: String) -> void: diff --git a/tools/smoke_equipment_unequip.gd b/tools/smoke_equipment_unequip.gd index 0c75200..6a44ec0 100644 --- a/tools/smoke_equipment_unequip.gd +++ b/tools/smoke_equipment_unequip.gd @@ -191,8 +191,14 @@ func _check_unequip_ui_entries(failures: Array[String]) -> void: scene.armory_unit_id = "cao_cao" scene._rebuild_armory_menu() - if not _container_has_button_text(scene.armory_list, "병장 해제:"): - failures.append("pre-battle Armory should include an Unequip Weapon button") + if not _container_has_button_text(scene.armory_list, "해제"): + failures.append("pre-battle Armory should expose a compact unequip button") + if _container_has_button_text(scene.armory_list, "병장 해제:"): + failures.append("pre-battle Armory should move long unequip wording into tooltips") + if not _container_has_tooltip_text(scene.armory_list, "병장 해제:"): + failures.append("pre-battle Armory tooltip should preserve full unequip detail") + if not _container_has_texture(scene.armory_list): + failures.append("pre-battle Armory should show item or officer artwork") scene.free() @@ -201,6 +207,26 @@ func _container_has_button_text(container: Node, fragment: String) -> bool: for child in container.get_children(): if child is Button and str(child.text).contains(fragment): return true + if _container_has_button_text(child, fragment): + return true + return false + + +func _container_has_tooltip_text(container: Node, fragment: String) -> bool: + if container is Control and str((container as Control).tooltip_text).contains(fragment): + return true + for child in container.get_children(): + if _container_has_tooltip_text(child, fragment): + return true + return false + + +func _container_has_texture(container: Node) -> bool: + if container is TextureRect and (container as TextureRect).texture != null: + return true + for child in container.get_children(): + if _container_has_texture(child): + return true return false