From abeca3dbe3114cb94c86ed5a30afecaafd0b8411 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 00:58:21 +0900 Subject: [PATCH] Iconify prebattle prep commands --- README.md | 1 + scripts/scenes/battle_scene.gd | 78 +++++++++++++++++++++++++++++++--- tools/smoke_visual_assets.gd | 27 +++++++++--- 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f1c78e0..11f06ce 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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. - Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview. +- Pre-battle prep commands use compact icon buttons with Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records. - Pre-battle Chapters overview shows story-arc progress with map thumbnail rows, compact status badges, hover details, and can open completed or current battles. - Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save, using compact seal-style slot rows and hover checkpoint details. - The title screen exposes Start, Load, and Settings, and Load now lets the player choose between the automatic record and the manual checkpoint. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b301457..7765d1a 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -142,6 +142,7 @@ const HUD_COMMAND_GRID_SIZE := Vector2(420, 46) const HUD_COMMAND_BUTTON_SIZE := Vector2(72, 42) const HUD_COMMAND_GRID_COLUMNS := 4 const TOP_TOOL_BUTTON_SIZE := Vector2(42, 42) +const PREP_MENU_BUTTON_SIZE := Vector2(48, 38) const BATTLE_UNIT_LIST_PANEL_SIZE := Vector2(420, 360) const BATTLE_UNIT_LIST_ROW_SIZE := Vector2(372, 58) const BATTLE_UNIT_LIST_ICON_SIZE := Vector2(46, 46) @@ -965,6 +966,23 @@ func _configure_top_tool_button(button: Button, node_name: String, icon_kind: St button.add_theme_font_size_override("font_size", 1) +func _configure_prep_menu_button(button: Button, node_name: String, icon_kind: String, label: String, hint: String) -> void: + if button == null: + return + button.name = node_name + button.text = "" + button.tooltip_text = _format_local_command_tooltip(label, hint) + button.custom_minimum_size = PREP_MENU_BUTTON_SIZE + button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + button.icon = _make_toolbar_icon(icon_kind) + button.expand_icon = true + button.add_theme_font_size_override("font_size", 1) + button.set_meta("icon_only", true) + button.set_meta("command_icon", icon_kind) + button.set_meta("command_label", label) + button.set_meta("command_hint", hint) + + func _configure_local_command_button(button: Button, icon_kind: String, label: String, hint: String, minimum_size := LOCAL_COMMAND_BUTTON_SIZE) -> void: if button == null: return @@ -1313,6 +1331,21 @@ func _make_toolbar_icon(kind: String) -> Texture2D: _icon_rect(image, Rect2i(10, 15, 10, 2), Color(0.16, 0.07, 0.03, 1.0)) _icon_rect(image, Rect2i(10, 20, 8, 2), Color(0.16, 0.07, 0.03, 1.0)) _icon_circle(image, Vector2i(24, 9), 3, red) + "shop": + _icon_rect(image, Rect2i(8, 11, 18, 14), shadow) + _icon_rect(image, Rect2i(7, 10, 18, 14), ink) + _icon_line(image, Vector2i(10, 10), Vector2i(16, 5), red, 3) + _icon_line(image, Vector2i(22, 10), Vector2i(16, 5), red, 3) + _icon_circle(image, Vector2i(22, 22), 5, red) + _icon_circle(image, Vector2i(22, 22), 2, ink) + "talk": + _icon_rect(image, Rect2i(6, 8, 17, 10), shadow) + _icon_rect(image, Rect2i(5, 7, 17, 10), ink) + _icon_line(image, Vector2i(9, 11), Vector2i(18, 11), Color(0.16, 0.07, 0.03, 1.0), 2) + _icon_rect(image, Rect2i(10, 17, 17, 10), shadow) + _icon_rect(image, Rect2i(9, 16, 17, 10), ink) + _icon_line(image, Vector2i(13, 20), Vector2i(22, 20), Color(0.16, 0.07, 0.03, 1.0), 2) + _icon_circle(image, Vector2i(24, 8), 3, red) "unit_list": _icon_circle(image, Vector2i(11, 10), 4, shadow) _icon_circle(image, Vector2i(21, 10), 4, shadow) @@ -1348,6 +1381,16 @@ func _make_toolbar_icon(kind: String) -> Texture2D: _icon_line(image, Vector2i(13, 12), Vector2i(18, 18), ink, 3) _icon_line(image, Vector2i(18, 18), Vector2i(25, 8), ink, 3) _icon_line(image, Vector2i(6, 24), Vector2i(26, 24), red, 3) + "formation": + for x in [8, 16, 24]: + _icon_line(image, Vector2i(x, 6), Vector2i(x, 26), shadow, 3) + _icon_line(image, Vector2i(x, 6), Vector2i(x, 26), ink, 1) + for y in [8, 16, 24]: + _icon_line(image, Vector2i(6, y), Vector2i(26, y), shadow, 3) + _icon_line(image, Vector2i(6, y), Vector2i(26, y), ink, 1) + _icon_circle(image, Vector2i(8, 8), 3, red) + _icon_circle(image, Vector2i(16, 16), 3, red) + _icon_circle(image, Vector2i(24, 24), 3, red) "forecast": _icon_circle_outline(image, Vector2i(16, 16), 10, shadow, 4) _icon_circle_outline(image, Vector2i(16, 16), 10, ink, 2) @@ -2944,37 +2987,37 @@ func _create_hud() -> void: briefing_column.add_child(prep_button_row) chapter_button = Button.new() - chapter_button.text = "군기록" + _configure_prep_menu_button(chapter_button, "PrepChapterButton", "campaign", "군기록", "전기 병서와 열린 전장을 살핍니다.") chapter_button.pressed.connect(_on_chapter_pressed) prep_button_row.add_child(chapter_button) shop_button = Button.new() - shop_button.text = "군상" + _configure_prep_menu_button(shop_button, "PrepShopButton", "shop", "군상", "군막 상인에게 물자를 사고팝니다.") shop_button.pressed.connect(_on_shop_pressed) prep_button_row.add_child(shop_button) talk_button = Button.new() - talk_button.text = "군막 회의" + _configure_prep_menu_button(talk_button, "PrepTalkButton", "talk", "군막 회의", "장수와 상인을 만나 전투 전 일을 처리합니다.") talk_button.pressed.connect(_on_talk_pressed) prep_button_row.add_child(talk_button) armory_button = Button.new() - armory_button.text = "병장고" + _configure_prep_menu_button(armory_button, "PrepArmoryButton", "equip", "병장고", "장수의 무기, 갑옷, 장신구를 바꿉니다.") armory_button.pressed.connect(_on_armory_pressed) prep_button_row.add_child(armory_button) roster_button = Button.new() - roster_button.text = "출진 명부" + _configure_prep_menu_button(roster_button, "PrepRosterButton", "unit_list", "출진 명부", "출진과 대기 장수를 조정합니다.") roster_button.pressed.connect(_on_roster_pressed) prep_button_row.add_child(roster_button) formation_button = Button.new() - formation_button.text = "진형 배치" + _configure_prep_menu_button(formation_button, "PrepFormationButton", "formation", "진형 배치", "전투 전 시작 위치를 바꿉니다.") formation_button.pressed.connect(_on_formation_pressed) prep_button_row.add_child(formation_button) save_button = Button.new() - save_button.text = "전기록" + _configure_prep_menu_button(save_button, "PrepSaveButton", "save", "전기록", "수동 봉인을 저장하거나 엽니다.") save_button.pressed.connect(_on_save_pressed) prep_button_row.add_child(save_button) @@ -9771,18 +9814,25 @@ func _show_briefing() -> void: _clear_pending_move_state() if chapter_button != null: chapter_button.disabled = campaign_state.get_chapter_progress_entries().is_empty() + chapter_button.tooltip_text = _prep_menu_tooltip("군기록", "전기 병서와 열린 전장을 살핍니다.", "열람할 군기록이 없습니다." if chapter_button.disabled else "") if shop_button != null: shop_button.disabled = prep_locked or (state.get_shop_item_ids().is_empty() and _get_sellable_item_ids().is_empty()) + shop_button.tooltip_text = _prep_menu_tooltip("군상", "군막 상인에게 물자를 사고팝니다.", _prep_locked_or_empty_reason(prep_locked, "사고팔 물자가 없습니다.") if shop_button.disabled else "") if talk_button != null: talk_button.disabled = prep_locked or _camp_conversation_entries().is_empty() + talk_button.tooltip_text = _prep_menu_tooltip("군막 회의", "장수와 상인을 만나 전투 전 일을 처리합니다.", _prep_locked_or_empty_reason(prep_locked, "나눌 군막 회의가 없습니다.") if talk_button.disabled else "") if armory_button != null: armory_button.disabled = prep_locked or state.get_controllable_player_units().is_empty() + armory_button.tooltip_text = _prep_menu_tooltip("병장고", "장수의 무기, 갑옷, 장신구를 바꿉니다.", _prep_locked_or_empty_reason(prep_locked, "병장을 바꿀 장수가 없습니다.") if armory_button.disabled else "") if roster_button != null: roster_button.disabled = prep_locked or not state.has_deployment_roster_choices() + roster_button.tooltip_text = _prep_menu_tooltip("출진 명부", "출진과 대기 장수를 조정합니다.", _prep_locked_or_empty_reason(prep_locked, "이 전장은 출진 명부를 바꿀 수 없습니다.") if roster_button.disabled else "") if formation_button != null: formation_button.disabled = prep_locked or state.get_formation_cells().is_empty() + formation_button.tooltip_text = _prep_menu_tooltip("진형 배치", "전투 전 시작 위치를 바꿉니다.", _prep_locked_or_empty_reason(prep_locked, "배치할 진형 칸이 없습니다.") if formation_button.disabled else "") if save_button != null: save_button.disabled = prep_locked + save_button.tooltip_text = _prep_menu_tooltip("전기록", "수동 봉인을 저장하거나 엽니다.", "전후 선택을 먼저 마쳐야 합니다." if save_button.disabled else "") func _on_briefing_objective_toggle_pressed() -> void: @@ -9790,6 +9840,20 @@ func _on_briefing_objective_toggle_pressed() -> void: _set_briefing_objective_collapsed(not briefing_objective_collapsed) +func _prep_menu_tooltip(label: String, hint: String, reason: String = "") -> String: + var tooltip := _format_local_command_tooltip(label, hint) + var normalized_reason := reason.strip_edges() + if normalized_reason.is_empty(): + return tooltip + return "%s\n%s" % [tooltip, normalized_reason] + + +func _prep_locked_or_empty_reason(prep_locked: bool, empty_reason: String) -> String: + if prep_locked: + return "전후 선택을 먼저 마쳐야 합니다." + return empty_reason + + func _set_briefing_objective_collapsed(collapsed: bool) -> bool: if briefing_objective_label == null or briefing_objective_label.text.strip_edges().is_empty(): return false diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 2ffa96f..bab6fd6 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3405,12 +3405,13 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: if not scene.briefing_objective_collapsed or scene.battle_started: failures.append("briefing Escape should collapse conditions before starting battle") scene.briefing_panel.visible = false - if scene.shop_button == null or scene.shop_button.text != "군상": - failures.append("briefing shop button should use sutler wording") - if scene.talk_button == null or scene.talk_button.text != "군막 회의": - failures.append("briefing talk button should use war council wording") - if scene.formation_button == null or scene.formation_button.text != "진형 배치": - failures.append("briefing formation button should use battle array wording") + _check_prep_menu_icon_button(failures, scene.chapter_button, "군기록", "campaign") + _check_prep_menu_icon_button(failures, scene.shop_button, "군상", "shop") + _check_prep_menu_icon_button(failures, scene.talk_button, "군막 회의", "talk") + _check_prep_menu_icon_button(failures, scene.armory_button, "병장고", "equip") + _check_prep_menu_icon_button(failures, scene.roster_button, "출진 명부", "unit_list") + _check_prep_menu_icon_button(failures, scene.formation_button, "진형 배치", "formation") + _check_prep_menu_icon_button(failures, scene.save_button, "전기록", "save") if scene.shop_buy_button == null or scene.shop_buy_button.text != "사들이기": failures.append("briefing shop buy button should avoid modern purchase wording") if scene.shop_sell_button == null or scene.shop_sell_button.text != "내다팔기": @@ -3853,6 +3854,20 @@ func _row_texts_contain(container: VBoxContainer, needle: String) -> bool: return _collect_child_text(container).contains(needle) +func _check_prep_menu_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void: + if button == null: + failures.append("pre-battle prep button missing: %s" % expected_label) + return + if not bool(button.get_meta("icon_only", false)) or button.text != "": + failures.append("pre-battle prep button should be icon-first without visible text: %s text=%s" % [expected_label, button.text]) + if button.icon == null or str(button.get_meta("command_icon", "")) != expected_icon: + failures.append("pre-battle prep button should carry a distinct icon: %s icon=%s meta=%s" % [expected_label, str(button.icon), str(button.get_meta("command_icon", ""))]) + if str(button.get_meta("command_label", "")) != expected_label: + failures.append("pre-battle prep button should preserve Korean label in metadata: %s meta=%s" % [expected_label, str(button.get_meta("command_label", ""))]) + if str(button.tooltip_text).strip_edges().is_empty() or not str(button.tooltip_text).contains(expected_label): + failures.append("pre-battle prep button should expose its function through hover tooltip: %s tooltip=%s" % [expected_label, str(button.tooltip_text)]) + + func _check_action_button_disabled_reasons(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene.wait_button = Button.new()