From 18b073f937c4f78d6877f57544779ccec73bb554 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 00:19:46 +0900 Subject: [PATCH] Add portrait formation rows --- README.md | 2 +- scripts/scenes/battle_scene.gd | 83 ++++++++++++++++++++++++++++------ tools/smoke_visual_assets.gd | 13 ++++++ 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 457c2da..f73c0d3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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, 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 Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, coordinate badges, and hover placement instructions. - Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview. - Pre-battle Chapters overview shows story-arc progress and can open completed or current battles. - Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 986d3d4..4236c84 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -11381,11 +11381,10 @@ func _rebuild_formation_menu() -> void: var selected := state.get_unit(formation_unit_id) if selected.is_empty(): formation_status_label.text = "진형" + formation_status_label.tooltip_text = "출진 장수를 고른 뒤 전장도의 금빛 진형 칸을 클릭해 배치합니다." else: - formation_status_label.text = "진형: %s · %s" % [ - str(selected.get("name", "장수")), - _format_cell_label(selected.get("pos", Vector2i.ZERO)) - ] + formation_status_label.text = "진형: %s" % str(selected.get("name", "장수")) + formation_status_label.tooltip_text = _format_formation_unit_tooltip(selected, true) if units.is_empty(): var empty_label := Label.new() @@ -11396,12 +11395,7 @@ func _rebuild_formation_menu() -> void: for unit in units: var unit_id := str(unit.get("id", "")) - var unit_button := Button.new() - unit_button.text = _format_formation_unit_button_text(unit) - _apply_button_style(unit_button) - unit_button.disabled = unit_id == formation_unit_id - unit_button.pressed.connect(_on_formation_unit_pressed.bind(unit_id)) - formation_list.add_child(unit_button) + _add_formation_unit_row(unit, unit_id == formation_unit_id) func _clear_formation_list() -> void: @@ -11411,14 +11405,73 @@ func _clear_formation_list() -> void: func _format_formation_unit_button_text(unit: Dictionary) -> String: - var marker := "> " if str(unit.get("id", "")) == formation_unit_id else "" - return "%s%s %s" % [ - marker, - str(unit.get("name", "장수")), - _format_cell_label(unit.get("pos", Vector2i.ZERO)) + return str(unit.get("name", "장수")) + + +func _add_formation_unit_row(unit: Dictionary, selected: bool) -> void: + var unit_id := str(unit.get("id", "")) + var tooltip_text := _format_formation_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 + formation_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_formation_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_formation_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_formation_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_formation_unit_badges(unit: Dictionary, selected: bool) -> Array: + return [ + "선택" if selected else "대상", + "좌표 %s" % _format_cell_label(unit.get("pos", Vector2i.ZERO)), + _unit_class_display_name(unit) ] +func _format_formation_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_cell_label(unit.get("pos", Vector2i.ZERO)) + ] + if selected: + lines.append("전장도의 금빛 진형 칸을 클릭하면 이 장수를 옮깁니다.") + else: + lines.append("클릭하면 배치할 장수로 선택합니다.") + return _join_strings(lines, "\n") + + func _format_cell_label(value) -> String: if typeof(value) == TYPE_VECTOR2I: return "%d,%d" % [value.x + 1, value.y + 1] diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 27ff995..560e6cb 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3008,6 +3008,19 @@ 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_formation_menu() + var formation_visible_text := _collect_child_text(scene.formation_list) + var formation_tooltips := _collect_child_tooltips(scene.formation_list) + if not formation_visible_text.contains("조조") or not formation_visible_text.contains("좌표") or not formation_visible_text.contains("선택"): + failures.append("formation rows should show officer name and compact placement badges: %s" % formation_visible_text) + if formation_visible_text.contains("전장도의") or formation_visible_text.contains("클릭하면"): + failures.append("formation rows should keep placement instructions in hover text: %s" % formation_visible_text) + if not formation_tooltips.contains("금빛 진형 칸") or not formation_tooltips.contains("현재 위치"): + failures.append("formation row tooltip should retain placement instructions: %s" % formation_tooltips) + if not _has_descendant_texture(scene.formation_list): + failures.append("formation rows should reuse officer portrait artwork") + if scene._format_formation_unit_button_text(scene.state.get_unit("cao_cao")).contains(","): + failures.append("formation unit button text should stay compact") scene.free() _check_scenario_background_texture_loading(failures)