From c1c58337f45ea1c40c5a74cd56bf4a50a63bb4f0 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 21:23:32 +0900 Subject: [PATCH] Streamline battle HUD controls --- scripts/scenes/battle_scene.gd | 393 ++++++++++++++++++++++++++++++--- tools/smoke_visual_assets.gd | 60 +++-- 2 files changed, 399 insertions(+), 54 deletions(-) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 2ceb0cd..026726c 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -101,7 +101,8 @@ const HUD_UNIT_PORTRAIT_SIZE := Vector2(92, 104) const HUD_UNIT_PORTRAIT_STACK_SIZE := Vector2(88, 100) const HUD_COMMAND_GRID_SIZE := Vector2(420, 70) const HUD_COMMAND_BUTTON_SIZE := Vector2(132, 30) -const HUD_COMMAND_GRID_COLUMNS := 3 +const HUD_COMMAND_GRID_COLUMNS := 2 +const TOP_TOOL_BUTTON_SIZE := Vector2(42, 42) const POST_MOVE_MENU_SIZE := Vector2(240, 146) const POST_MOVE_TITLE_SIZE := Vector2(196, 22) const POST_MOVE_PICKER_PANEL_SIZE := Vector2(336, 236) @@ -290,7 +291,7 @@ var pending_move_unit_id := "" var pending_move_from_cell := Vector2i(-1, -1) var pending_move_to_cell := Vector2i(-1, -1) var show_threat_overlay := false -var mission_detail_collapsed := false +var mission_detail_collapsed := true var briefing_objective_collapsed := false var active_scenario_id := "" var battle_started := false @@ -418,6 +419,8 @@ var targeting_hint_detail_label: Label var targeting_hint_back_button: Button var targeting_hint_cancel_move_button: Button var threat_button: Button +var top_save_button: Button +var top_load_button: Button var restart_button: Button var new_campaign_button: Button var log_box: RichTextLabel @@ -800,6 +803,122 @@ func _configure_hud_command_button(button: Button) -> void: button.add_theme_font_size_override("font_size", 13) +func _configure_top_tool_button(button: Button, node_name: String, icon_kind: String, tooltip: String) -> void: + if button == null: + return + button.name = node_name + button.text = "" + button.tooltip_text = tooltip + button.custom_minimum_size = TOP_TOOL_BUTTON_SIZE + button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + button.size_flags_vertical = Control.SIZE_SHRINK_CENTER + button.icon = _make_toolbar_icon(icon_kind) + button.expand_icon = true + button.set_meta("icon_only", true) + button.add_theme_font_size_override("font_size", 1) + + +func _make_toolbar_icon(kind: String) -> Texture2D: + var image := Image.create(32, 32, false, Image.FORMAT_RGBA8) + image.fill(Color(0, 0, 0, 0)) + var ink := Color(0.96, 0.82, 0.50, 1.0) + var shadow := Color(0.07, 0.028, 0.012, 0.82) + var red := Color(UI_SEAL_RED.r, UI_SEAL_RED.g, UI_SEAL_RED.b, 1.0) + match kind: + "end_turn": + _icon_line(image, Vector2i(7, 10), Vector2i(22, 10), shadow, 4) + _icon_line(image, Vector2i(21, 10), Vector2i(21, 22), shadow, 4) + _icon_line(image, Vector2i(7, 10), Vector2i(22, 10), ink, 2) + _icon_line(image, Vector2i(21, 10), Vector2i(21, 22), ink, 2) + _icon_line(image, Vector2i(18, 18), Vector2i(21, 22), ink, 2) + _icon_line(image, Vector2i(24, 18), Vector2i(21, 22), ink, 2) + "threat": + _icon_line(image, Vector2i(6, 16), Vector2i(13, 10), shadow, 4) + _icon_line(image, Vector2i(13, 10), Vector2i(20, 10), shadow, 4) + _icon_line(image, Vector2i(20, 10), Vector2i(27, 16), shadow, 4) + _icon_line(image, Vector2i(6, 16), Vector2i(13, 22), shadow, 4) + _icon_line(image, Vector2i(13, 22), Vector2i(20, 22), shadow, 4) + _icon_line(image, Vector2i(20, 22), Vector2i(27, 16), shadow, 4) + _icon_line(image, Vector2i(6, 16), Vector2i(13, 10), ink, 2) + _icon_line(image, Vector2i(13, 10), Vector2i(20, 10), ink, 2) + _icon_line(image, Vector2i(20, 10), Vector2i(27, 16), ink, 2) + _icon_line(image, Vector2i(6, 16), Vector2i(13, 22), ink, 2) + _icon_line(image, Vector2i(13, 22), Vector2i(20, 22), ink, 2) + _icon_line(image, Vector2i(20, 22), Vector2i(27, 16), ink, 2) + _icon_circle(image, Vector2i(16, 16), 4, red) + "save": + _icon_rect(image, Rect2i(8, 6, 17, 22), shadow) + _icon_rect(image, Rect2i(7, 5, 17, 22), ink) + _icon_rect(image, Rect2i(10, 8, 11, 5), Color(0.18, 0.08, 0.03, 1.0)) + _icon_rect(image, Rect2i(11, 17, 9, 6), Color(0.18, 0.08, 0.03, 1.0)) + _icon_rect(image, Rect2i(19, 8, 2, 5), red) + "load": + _icon_rect(image, Rect2i(8, 8, 17, 18), shadow) + _icon_rect(image, Rect2i(7, 7, 17, 18), ink) + _icon_rect(image, Rect2i(10, 18, 11, 4), Color(0.18, 0.08, 0.03, 1.0)) + _icon_line(image, Vector2i(16, 24), Vector2i(16, 10), red, 3) + _icon_line(image, Vector2i(11, 15), Vector2i(16, 10), red, 3) + _icon_line(image, Vector2i(21, 15), Vector2i(16, 10), red, 3) + "restart": + _icon_circle_outline(image, Vector2i(16, 16), 9, shadow, 4) + _icon_circle_outline(image, Vector2i(16, 16), 9, ink, 2) + _icon_line(image, Vector2i(8, 15), Vector2i(6, 8), red, 3) + _icon_line(image, Vector2i(8, 15), Vector2i(14, 12), red, 3) + "new": + _icon_circle(image, Vector2i(16, 16), 10, red) + _icon_line(image, Vector2i(16, 9), Vector2i(16, 23), ink, 3) + _icon_line(image, Vector2i(9, 16), Vector2i(23, 16), ink, 3) + _: + _icon_circle(image, Vector2i(16, 16), 7, ink) + return ImageTexture.create_from_image(image) + + +func _icon_rect(image: Image, rect: Rect2i, color: Color) -> void: + for y in range(maxi(0, rect.position.y), mini(image.get_height(), rect.position.y + rect.size.y)): + for x in range(maxi(0, rect.position.x), mini(image.get_width(), rect.position.x + rect.size.x)): + image.set_pixel(x, y, color) + + +func _icon_circle(image: Image, center: Vector2i, radius: int, color: Color) -> void: + var r2 := radius * radius + for y in range(center.y - radius, center.y + radius + 1): + for x in range(center.x - radius, center.x + radius + 1): + if x < 0 or y < 0 or x >= image.get_width() or y >= image.get_height(): + continue + var dx := x - center.x + var dy := y - center.y + if dx * dx + dy * dy <= r2: + image.set_pixel(x, y, color) + + +func _icon_circle_outline(image: Image, center: Vector2i, radius: int, color: Color, width: int = 1) -> void: + var outer := radius * radius + var inner_radius := maxi(0, radius - width) + var inner := inner_radius * inner_radius + for y in range(center.y - radius, center.y + radius + 1): + for x in range(center.x - radius, center.x + radius + 1): + if x < 0 or y < 0 or x >= image.get_width() or y >= image.get_height(): + continue + var dx := x - center.x + var dy := y - center.y + var d2 := dx * dx + dy * dy + if d2 <= outer and d2 >= inner: + image.set_pixel(x, y, color) + + +func _icon_line(image: Image, from_point: Vector2i, to_point: Vector2i, color: Color, width: int = 1) -> void: + var delta := to_point - from_point + var steps: int = maxi(abs(delta.x), abs(delta.y)) + if steps <= 0: + _icon_circle(image, from_point, maxi(1, width / 2), color) + return + var radius: int = maxi(0, int(floor(float(width) * 0.5))) + for step in range(steps + 1): + var t := float(step) / float(steps) + var point := Vector2i(roundi(lerpf(from_point.x, to_point.x, t)), roundi(lerpf(from_point.y, to_point.y, t))) + _icon_circle(image, point, radius, color) + + func _apply_label_style(label: Label, font_color: Color, font_size: int = 0) -> void: if label == null: return @@ -1244,7 +1363,7 @@ func _create_hud() -> void: var top_bar := PanelContainer.new() top_bar.position = Vector2(24, 14) - top_bar.size = Vector2(1192, 72) + top_bar.size = Vector2(1232, 72) _apply_panel_style(top_bar, "compact") root.add_child(top_bar) @@ -1253,12 +1372,12 @@ func _create_hud() -> void: top_bar.add_child(top_row) status_label = Label.new() - status_label.custom_minimum_size = Vector2(154, 48) + status_label.custom_minimum_size = Vector2(118, 48) _apply_label_style(status_label, UI_PARCHMENT_TEXT) top_row.add_child(status_label) var objective_top_panel := PanelContainer.new() - objective_top_panel.custom_minimum_size = Vector2(460, 54) + objective_top_panel.custom_minimum_size = Vector2(396, 54) _apply_panel_style(objective_top_panel, "edict_compact") top_row.add_child(objective_top_panel) @@ -1269,26 +1388,53 @@ func _create_hud() -> void: objective_label = Label.new() objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - objective_label.custom_minimum_size = Vector2(382, 36) + objective_label.custom_minimum_size = Vector2(318, 36) objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_label_style(objective_label, UI_AGED_INK, OBJECTIVE_HUD_FONT_SIZE) objective_top_row.add_child(objective_label) objective_top_row.add_child(_make_bamboo_gutter(14, 36)) campaign_status_label = Label.new() - campaign_status_label.custom_minimum_size = Vector2(260, 48) + campaign_status_label.custom_minimum_size = Vector2(144, 48) _apply_label_style(campaign_status_label, UI_PARCHMENT_TEXT) top_row.add_child(campaign_status_label) + var top_tool_row := HBoxContainer.new() + top_tool_row.name = "TopToolBar" + top_tool_row.custom_minimum_size = Vector2(284, 48) + top_tool_row.add_theme_constant_override("separation", 5) + top_row.add_child(top_tool_row) + + end_turn_button = Button.new() + _configure_top_tool_button(end_turn_button, "TopEndTurnButton", "end_turn", "차례 종료\n아군 차례를 마치고 적군에게 넘깁니다.") + end_turn_button.pressed.connect(_on_end_turn_pressed) + top_tool_row.add_child(end_turn_button) + + threat_button = Button.new() + _configure_top_tool_button(threat_button, "TopThreatButton", "threat", "적정\n적의 무기와 책략 사거리를 살핍니다.") + threat_button.toggle_mode = true + threat_button.pressed.connect(_on_threat_pressed) + top_tool_row.add_child(threat_button) + + top_save_button = Button.new() + _configure_top_tool_button(top_save_button, "TopSaveButton", "save", "저장\n전투 전 군막에서 수동 전기를 봉인합니다.") + top_save_button.pressed.connect(_on_top_manual_save_pressed) + top_tool_row.add_child(top_save_button) + + top_load_button = Button.new() + _configure_top_tool_button(top_load_button, "TopLoadButton", "load", "불러오기\n수동 봉인을 열어 군막으로 돌아갑니다.") + top_load_button.pressed.connect(_on_top_manual_load_pressed) + top_tool_row.add_child(top_load_button) + restart_button = Button.new() - restart_button.text = "재정비" + _configure_top_tool_button(restart_button, "TopRestartButton", "restart", "재정비\n현재 전장을 처음부터 다시 펼칩니다.") restart_button.pressed.connect(_on_restart_pressed) - top_row.add_child(restart_button) + top_tool_row.add_child(restart_button) new_campaign_button = Button.new() - new_campaign_button.text = "새 전기 열기" + _configure_top_tool_button(new_campaign_button, "TopNewCampaignButton", "new", "새 전기 열기\n저장된 진행을 지우고 처음부터 시작합니다.") new_campaign_button.pressed.connect(_on_new_campaign_pressed) - top_row.add_child(new_campaign_button) + top_tool_row.add_child(new_campaign_button) objective_notice_panel = PanelContainer.new() objective_notice_panel.visible = false @@ -1372,13 +1518,14 @@ func _create_hud() -> void: mission_title_row.add_child(mission_title_label) mission_toggle_button = Button.new() - mission_toggle_button.text = "접기" + mission_toggle_button.text = "보기" mission_toggle_button.tooltip_text = "전투 목표 판을 접거나 펼칩니다." mission_toggle_button.custom_minimum_size = Vector2(78, 28) mission_toggle_button.pressed.connect(_on_mission_toggle_pressed) mission_title_row.add_child(mission_toggle_button) mission_detail_panel = PanelContainer.new() + mission_detail_panel.visible = false mission_detail_panel.custom_minimum_size = Vector2(420, 134) mission_detail_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_panel_style(mission_detail_panel, "bamboo_slips") @@ -1464,19 +1611,6 @@ func _create_hud() -> void: equip_button.pressed.connect(_on_equip_pressed) button_row.add_child(equip_button) - threat_button = Button.new() - threat_button.text = "적정" - _configure_hud_command_button(threat_button) - threat_button.toggle_mode = true - threat_button.pressed.connect(_on_threat_pressed) - button_row.add_child(threat_button) - - end_turn_button = Button.new() - end_turn_button.text = "차례 종료" - _configure_hud_command_button(end_turn_button) - end_turn_button.pressed.connect(_on_end_turn_pressed) - button_row.add_child(end_turn_button) - tactic_menu = VBoxContainer.new() tactic_menu.visible = false tactic_menu.add_theme_constant_override("separation", 6) @@ -2207,6 +2341,8 @@ func _create_hud() -> void: mission_toggle_button, threat_button, end_turn_button, + top_save_button, + top_load_button, post_move_attack_button, post_move_tactic_button, post_move_item_button, @@ -5661,20 +5797,24 @@ func _refresh_ranges() -> void: func _update_hud() -> void: status_label.text = state.get_status_text() + status_label.tooltip_text = state.get_status_text() objective_label.text = _format_objective_hud_text() + objective_label.tooltip_text = _format_objective_hud_detail_text() _fit_label_font_size_to_text( objective_label, objective_label.text, OBJECTIVE_HUD_FONT_SIZE, OBJECTIVE_HUD_MIN_FONT_SIZE ) - campaign_status_label.text = campaign_state.get_progress_text() + campaign_status_label.text = _format_campaign_hud_summary_text() + campaign_status_label.tooltip_text = campaign_state.get_progress_text() _update_mission_panel() _update_cell_info() _update_forecast() _update_result_panel() if inventory_label != null: - inventory_label.text = _format_inventory_status_text() + inventory_label.text = _format_inventory_status_summary_text() + inventory_label.tooltip_text = _format_inventory_status_text() var selected := state.get_selected_unit() if selected.is_empty(): @@ -5682,8 +5822,10 @@ func _update_hud() -> void: _update_hud_unit_portrait(hovered_unit) if hovered_unit.is_empty(): selected_label.text = "표식 없는 빈 전장입니다." + selected_label.tooltip_text = "마우스를 부대 위에 올리면 상세 군세가 표시됩니다." else: - selected_label.text = _format_unit_focus_text(hovered_unit, "Hover") + selected_label.text = _format_unit_focus_summary_text(hovered_unit, "Hover") + selected_label.tooltip_text = _format_unit_focus_text(hovered_unit, "Hover") _update_wait_button({}) _update_tactic_button({}) _hide_tactic_menu() @@ -5693,7 +5835,8 @@ func _update_hud() -> void: _hide_equip_menu() else: _update_hud_unit_portrait(selected) - selected_label.text = _format_unit_focus_text(selected, "Selected") + selected_label.text = _format_unit_focus_summary_text(selected, "Selected") + selected_label.tooltip_text = _format_unit_focus_text(selected, "Selected") _update_wait_button(selected) _update_tactic_button(selected) _update_item_button(selected) @@ -5708,12 +5851,23 @@ func _update_hud() -> void: _update_end_turn_button() restart_button.disabled = campaign_complete_screen _update_threat_button() + _update_top_archive_buttons() _update_post_move_menu() _update_post_move_picker() _update_targeting_hint_panel() func _format_objective_hud_text() -> String: + var progress_lines := state.get_objective_progress_lines(false, false) + if not progress_lines.is_empty(): + return "목표: %s" % _join_strings(progress_lines, " · ") + var victory := String(state.objectives.get("victory", "적군을 모두 격파하라.")).strip_edges() + if victory.is_empty(): + return "목표 대기" + return "목표: %s" % _truncate_hud_text(victory, 34) + + +func _format_objective_hud_detail_text() -> String: var lines := [] var victory := String(state.objectives.get("victory", "적군을 모두 격파하라.")) if not victory.is_empty(): @@ -5726,6 +5880,21 @@ func _format_objective_hud_text() -> String: return _join_strings(lines, "\n") +func _truncate_hud_text(text: String, max_length: int) -> String: + var normalized := text.strip_edges() + if normalized.length() <= max_length: + return normalized + return "%s..." % normalized.substr(0, maxi(0, max_length - 3)).strip_edges() + + +func _format_campaign_hud_summary_text() -> String: + return "전기 %d/%d\n%d냥" % [ + campaign_state.completed_scenarios.size(), + campaign_state.get_scenario_count(), + campaign_state.gold + ] + + func _format_unit_focus_text(unit: Dictionary, prefix: String) -> String: if unit.is_empty(): return "표식 없는 빈 전장입니다." @@ -5777,6 +5946,37 @@ func _format_unit_focus_text(unit: Dictionary, prefix: String) -> String: return _join_strings(lines, "\n") +func _format_unit_focus_summary_text(unit: Dictionary, prefix: String) -> String: + if unit.is_empty(): + return "표식 없는 빈 전장입니다." + var name := str(unit.get("name", "부대")) + var control_label := " 호위" if not bool(unit.get("controllable", true)) else "" + var status_text := _unit_action_status_text(unit).replace("군령: ", "") + var status_parts := status_text.split(",", false) + if status_parts.size() > 3: + var visible_parts: Array[String] = [] + for index in range(3): + visible_parts.append(str(status_parts[index]).strip_edges()) + status_text = "%s 외 %d" % [_join_strings(visible_parts, ", "), status_parts.size() - 3] + var summary := "%s: %s%s\n%s 품계 %d · %s\n병력 %d/%d · 기력 %d/%d\n군령: %s" % [ + _unit_focus_prefix_text(prefix), + name, + control_label, + _unit_team_text(str(unit.get("team", ""))), + int(unit.get("level", 1)), + _unit_class_display_name(unit), + int(unit.get("hp", 0)), + int(unit.get("max_hp", 1)), + int(unit.get("mp", 0)), + int(unit.get("max_mp", 0)), + status_text + ] + var status_effects := state.get_status_effect_summary(str(unit.get("id", ""))) + if not status_effects.is_empty(): + summary += "\n상태: %s" % status_effects + return summary + + func _unit_focus_prefix_text(prefix: String) -> String: if prefix == "Selected": return "군기" @@ -5972,29 +6172,79 @@ func _update_hud_unit_portrait(unit: Dictionary) -> void: return if unit.is_empty(): hud_unit_portrait_panel.visible = false + hud_unit_portrait_panel.tooltip_text = "" hud_unit_portrait_texture.texture = null hud_unit_portrait_texture.visible = false + hud_unit_portrait_texture.tooltip_text = "" hud_unit_portrait_label.text = "" hud_unit_portrait_label.visible = false + hud_unit_portrait_label.tooltip_text = "" return hud_unit_portrait_panel.visible = true + var focus_detail := _format_unit_focus_text(unit, "Hover") + hud_unit_portrait_panel.tooltip_text = focus_detail var texture := _load_portrait_texture(str(unit.get("portrait", ""))) if texture == null: texture = _load_art_texture(str(unit.get("sprite", ""))) var has_portrait := texture != null hud_unit_portrait_texture.texture = texture hud_unit_portrait_texture.visible = has_portrait + hud_unit_portrait_texture.tooltip_text = focus_detail hud_unit_portrait_label.text = _dialogue_portrait_initials(str(unit.get("name", ""))) hud_unit_portrait_label.visible = not has_portrait + hud_unit_portrait_label.tooltip_text = focus_detail func _update_cell_info() -> void: - if not state.is_inside(hover_cell): - cell_info_label.text = "지형: -" + if cell_info_label == null: return + var detail_text := _format_cell_info_detail_text(hover_cell) + cell_info_label.text = _format_cell_info_summary_text(hover_cell) + cell_info_label.tooltip_text = detail_text - var summary := state.get_cell_summary(hover_cell) - var cell: Vector2i = summary["cell"] + +func _format_cell_info_summary_text(cell: Vector2i) -> String: + if not state.is_inside(cell): + return "지형: -" + var summary := state.get_cell_summary(cell) + var text := "지형 %d,%d %s\n행군 %d · 방비 +%d · 회피 +%d%%" % [ + cell.x + 1, + cell.y + 1, + _terrain_display_name(str(summary["terrain"])), + int(summary["move_cost"]), + int(summary["defense"]), + int(summary.get("avoid", 0)) + ] + var heal := int(summary.get("heal", 0)) + if heal > 0: + text += " · 회복 +%d" % heal + var objective_cell_label := state.get_objective_cell_label(cell) + if not objective_cell_label.is_empty(): + text += "\n목표: %s" % objective_cell_label + var event_marker_label := state.get_event_marker_label(cell) + if not event_marker_label.is_empty(): + text += "\n표식: %s" % event_marker_label + var unit: Dictionary = summary["unit"] + if not unit.is_empty(): + text += "\n군기: %s · 병력 %d/%d" % [ + str(unit.get("name", "부대")), + int(unit.get("hp", 0)), + int(unit.get("max_hp", 1)) + ] + var status_effects := state.get_status_effect_summary(str(unit.get("id", ""))) + if not status_effects.is_empty(): + text += "\n상태: %s" % status_effects + if show_threat_overlay: + var threat_names := state.get_threatening_unit_names(cell, BattleState.TEAM_ENEMY) + if not threat_names.is_empty(): + text += "\n감시: %s" % _compact_name_list(threat_names, 3) + return text + + +func _format_cell_info_detail_text(cell: Vector2i) -> String: + if not state.is_inside(cell): + return "마우스를 전장 칸 위에 올리면 지형과 부대 정보를 볼 수 있습니다." + var summary := state.get_cell_summary(cell) var text := "지형 %d,%d %s\n행군 %d 방비 +%d 회피 +%d%%" % [ cell.x + 1, cell.y + 1, @@ -6048,7 +6298,7 @@ func _update_cell_info() -> void: if not skill_threats.is_empty(): text += "\n책략 위협: %s" % _format_skill_threat_preview_text(skill_threats, 3) - cell_info_label.text = text + return text func _format_physical_threat_preview_text(previews: Array[Dictionary], limit: int) -> String: @@ -8210,6 +8460,39 @@ func _hide_formation_menu() -> void: formation_menu.visible = false +func _on_top_manual_save_pressed() -> void: + _on_manual_save_pressed() + + +func _on_top_manual_load_pressed() -> void: + _on_manual_load_pressed() + + +func _update_top_archive_buttons() -> void: + var prep_available := ( + not battle_started + and not _is_prebattle_prep_locked() + and briefing_panel != null + and briefing_panel.visible + and not campaign_complete_screen + ) + if top_save_button != null: + top_save_button.disabled = not prep_available + if prep_available: + top_save_button.tooltip_text = "저장\n현재 군막 준비와 전기 진행을 수동 봉인합니다." + else: + top_save_button.tooltip_text = "저장\n전투 중에는 저장할 수 없습니다. 군막 화면에서 사용합니다." + if top_load_button != null: + var has_manual := campaign_state.has_manual_save() + top_load_button.disabled = (not prep_available) or (not has_manual) + if not prep_available: + top_load_button.tooltip_text = "불러오기\n전투 중에는 불러올 수 없습니다. 군막 화면에서 사용합니다." + elif not has_manual: + top_load_button.tooltip_text = "불러오기\n열 수 있는 수동 봉인이 없습니다." + else: + top_load_button.tooltip_text = "불러오기\n수동 봉인을 열어 군막으로 돌아갑니다." + + func _on_save_pressed() -> void: if battle_started or _is_prebattle_prep_locked() or briefing_panel == null or not briefing_panel.visible: return @@ -8282,6 +8565,7 @@ func _on_manual_save_pressed() -> void: else: _on_log_added("수동 전기를 봉하지 못했다.") _rebuild_save_menu() + _update_top_archive_buttons() func _on_manual_load_pressed() -> void: @@ -8291,6 +8575,7 @@ func _on_manual_load_pressed() -> void: if not campaign_state.load_manual_game(): _on_log_added("수동 봉인을 열지 못했다.") _rebuild_save_menu() + _update_top_archive_buttons() return if log_box != null: log_box.clear() @@ -9368,6 +9653,38 @@ func _format_inventory_status_text() -> String: return "군수: %s" % _format_inventory_by_category(inventory) +func _format_inventory_status_summary_text() -> String: + var inventory := state.get_inventory_snapshot() + if campaign_complete_screen or state.battle_status != BattleState.STATUS_ACTIVE: + inventory = campaign_state.get_inventory_snapshot() + var consumable_count := 0 + var equipment_count := 0 + var other_count := 0 + for item_id in inventory.keys(): + var key := str(item_id) + var count := int(inventory.get(key, 0)) + if key.is_empty() or count <= 0: + continue + var item := state.get_item_def(key) + var kind := str(item.get("kind", "")) + if kind == "consumable": + consumable_count += count + elif kind == "weapon" or kind == "armor" or kind == "accessory": + equipment_count += count + else: + other_count += count + var parts: Array[String] = [] + if consumable_count > 0: + parts.append("소모 %d" % consumable_count) + if equipment_count > 0: + parts.append("병장 %d" % equipment_count) + if other_count > 0: + parts.append("잡물 %d" % other_count) + if parts.is_empty(): + return "군수: 없음" + return "군수: %s" % _join_strings(parts, " · ") + + func _format_reward_items(items: Array) -> String: var counts := {} for item_id in items: @@ -10109,9 +10426,13 @@ func _on_targeting_cancel_move_pressed() -> void: func _set_action_button_state(button: Button, label: String, disabled: bool, tooltip := "") -> void: if button == null: return - button.text = label + if bool(button.get_meta("icon_only", false)): + button.text = "" + button.tooltip_text = label if tooltip.strip_edges().is_empty() else "%s\n%s" % [label, tooltip] + else: + button.text = label + button.tooltip_text = tooltip button.disabled = disabled - button.tooltip_text = tooltip func _set_action_button_blocked(button: Button, base_label: String, reason_label: String, tooltip: String) -> void: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index f290dad..f66d63a 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3028,6 +3028,7 @@ func _check_hud_focus_text(failures: Array[String]) -> void: return var cao_cao: Dictionary = scene.state.get_unit("cao_cao") var cao_text := scene._format_unit_focus_text(cao_cao, "Selected") + var cao_summary := scene._format_unit_focus_summary_text(cao_cao, "Selected") if not cao_text.contains("군령 지휘"): failures.append("Cao Cao HUD focus should describe command role: %s" % cao_text) if not cao_text.contains("행군 4 (보병)") or not cao_text.contains("타격 1-1"): @@ -3042,6 +3043,8 @@ func _check_hud_focus_text(failures: Array[String]) -> void: failures.append("Cao Cao HUD focus should include terrain move cost: %s" % cao_text) if not cao_text.contains("방비 +0"): failures.append("Cao Cao HUD focus should include terrain defense: %s" % cao_text) + if not cao_summary.contains("군기: 조조") or not cao_summary.contains("병력") or cao_summary.contains("무위") or cao_summary.contains("지형 2,7"): + failures.append("Cao Cao visible HUD summary should stay compact while tooltip keeps detail: %s" % cao_summary) var xiahou_dun: Dictionary = scene.state.get_unit("xiahou_dun") xiahou_dun["pos"] = Vector2i(4, 1) var forest_text := scene._unit_current_terrain_text(xiahou_dun) @@ -3121,8 +3124,11 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: if briefing_text.contains(clutter): failures.append("briefing objective parchment should stay concise without `%s`: %s" % [clutter, briefing_text]) var hud_objective := scene._format_objective_hud_text() - if not hud_objective.contains("승리:") or not hud_objective.contains("패배:"): - failures.append("HUD objective should use concise Korean labels: %s" % hud_objective) + var hud_objective_detail := scene._format_objective_hud_detail_text() + if not hud_objective.begins_with("목표:"): + failures.append("HUD objective should stay as a concise current-goal summary: %s" % hud_objective) + if not hud_objective_detail.contains("승리:") or not hud_objective_detail.contains("패배:"): + failures.append("HUD objective tooltip should retain win/loss details: %s" % hud_objective_detail) var mission_text := scene._format_mission_panel_text() for expected in ["승리:", "패배:", "진행:", "주의:"]: if not mission_text.contains(expected): @@ -3223,15 +3229,15 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: failures.append("dialogue continue button should use the project serif font override") if scene.mission_title_label == null or scene.mission_title_label.text != "전투 목표": failures.append("mission panel title should use clear Korean wording") - if scene.mission_toggle_button == null or scene.mission_toggle_button.text != "접기": - failures.append("mission objective panel should expose a close control") + if scene.mission_toggle_button == null or scene.mission_toggle_button.text != "보기": + failures.append("mission objective panel should start compact with an open control") else: scene._on_mission_toggle_pressed() - if scene.mission_detail_panel == null or scene.mission_detail_panel.visible or scene.mission_toggle_button.text != "보기": - failures.append("mission objective panel should collapse cleanly") - scene._on_mission_toggle_pressed() if scene.mission_detail_panel == null or not scene.mission_detail_panel.visible or scene.mission_toggle_button.text != "접기": failures.append("mission objective panel should reopen cleanly") + scene._on_mission_toggle_pressed() + if scene.mission_detail_panel == null or scene.mission_detail_panel.visible or scene.mission_toggle_button.text != "보기": + failures.append("mission objective panel should collapse cleanly") if scene.briefing_start_button == null or scene.briefing_start_button.text != "전투 시작": failures.append("briefing start button should clearly start after briefing") if scene.briefing_objective_toggle_button == null or scene.briefing_objective_toggle_button.text != "닫기": @@ -3457,29 +3463,47 @@ func _find_descendant_by_name(root: Node, node_name: String) -> Node: func _check_hud_command_grid_layout(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() - var command_grid := scene.end_turn_button.get_parent() as GridContainer + var command_grid := scene.wait_button.get_parent() as GridContainer if command_grid == null: - failures.append("HUD command buttons should live in a fixed grid") + failures.append("unit command buttons should live in a fixed grid") else: if command_grid.columns != BattleSceneScript.HUD_COMMAND_GRID_COLUMNS: - failures.append("HUD command grid should use %d columns, got %d" % [BattleSceneScript.HUD_COMMAND_GRID_COLUMNS, command_grid.columns]) + failures.append("unit command grid should use %d columns, got %d" % [BattleSceneScript.HUD_COMMAND_GRID_COLUMNS, command_grid.columns]) if command_grid.custom_minimum_size.x < BattleSceneScript.HUD_COMMAND_GRID_SIZE.x or command_grid.custom_minimum_size.y < BattleSceneScript.HUD_COMMAND_GRID_SIZE.y: - failures.append("HUD command grid should reserve stable space: %s" % str(command_grid.custom_minimum_size)) + failures.append("unit command grid should reserve stable space: %s" % str(command_grid.custom_minimum_size)) for button in [ scene.wait_button, scene.tactic_button, scene.item_button, - scene.equip_button, - scene.threat_button, - scene.end_turn_button + scene.equip_button ]: if button == null: - failures.append("HUD command button missing") + failures.append("unit command button missing") continue + if button.get_parent() != command_grid: + failures.append("unit command button should stay in the side command grid: %s" % str(button.name)) if button.custom_minimum_size.x < BattleSceneScript.HUD_COMMAND_BUTTON_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.HUD_COMMAND_BUTTON_SIZE.y: - failures.append("HUD command button should reserve fixed Korean label space: %s" % str(button.custom_minimum_size)) - if scene.end_turn_button.text != "차례 종료": - failures.append("end turn command should use a short visible Korean label: %s" % scene.end_turn_button.text) + failures.append("unit command button should reserve fixed Korean label space: %s" % str(button.custom_minimum_size)) + var top_toolbar := scene.end_turn_button.get_parent() as HBoxContainer + if top_toolbar == null or top_toolbar.name != "TopToolBar": + failures.append("global battle commands should move to the top icon toolbar") + for button in [ + scene.end_turn_button, + scene.threat_button, + scene.top_save_button, + scene.top_load_button, + scene.restart_button, + scene.new_campaign_button + ]: + if button == null: + failures.append("top tool button missing") + continue + if button.get_parent() != top_toolbar: + failures.append("top tool button should share the top toolbar: %s" % str(button.name)) + if not bool(button.get_meta("icon_only", false)) or button.text != "" or button.icon == null: + failures.append("top tool button should be icon-only with tooltip detail: %s text=%s icon=%s" % [str(button.name), button.text, str(button.icon)]) + if button.custom_minimum_size.x < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.TOP_TOOL_BUTTON_SIZE.y: + failures.append("top tool button should reserve stable icon space: %s" % str(button.custom_minimum_size)) scene.free()