diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 97d105a..6e75c1d 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -105,6 +105,8 @@ 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 LOCAL_COMMAND_BUTTON_SIZE := Vector2(92, 38) +const LOCAL_COMMAND_CANCEL_BUTTON_SIZE := Vector2(196, 30) 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) @@ -829,6 +831,24 @@ func _configure_top_tool_button(button: Button, node_name: String, icon_kind: St button.add_theme_font_size_override("font_size", 1) +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 + button.text = "" + button.tooltip_text = "" + button.custom_minimum_size = minimum_size + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.icon = _make_toolbar_icon(icon_kind) + button.expand_icon = false + 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) + button.mouse_entered.connect(_on_local_command_button_mouse_entered.bind(button)) + button.mouse_exited.connect(_on_local_command_button_mouse_exited) + + func _make_toolbar_icon(kind: String) -> Texture2D: var image := Image.create(32, 32, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) @@ -879,6 +899,12 @@ func _make_toolbar_icon(kind: String) -> Texture2D: _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) + "attack": + _icon_line(image, Vector2i(8, 25), Vector2i(24, 9), shadow, 5) + _icon_line(image, Vector2i(8, 25), Vector2i(24, 9), ink, 3) + _icon_line(image, Vector2i(20, 7), Vector2i(26, 13), ink, 3) + _icon_line(image, Vector2i(7, 13), Vector2i(13, 19), red, 3) + _icon_line(image, Vector2i(6, 20), Vector2i(13, 27), red, 3) "wait": _icon_rect(image, Rect2i(9, 9, 15, 17), shadow) _icon_line(image, Vector2i(16, 7), Vector2i(24, 11), ink, 3) @@ -906,6 +932,13 @@ func _make_toolbar_icon(kind: String) -> Texture2D: _icon_line(image, Vector2i(20, 7), Vector2i(25, 12), ink, 3) _icon_line(image, Vector2i(10, 18), Vector2i(16, 24), red, 3) _icon_line(image, Vector2i(7, 21), Vector2i(13, 27), red, 3) + "cancel": + _icon_line(image, Vector2i(22, 8), Vector2i(9, 16), shadow, 5) + _icon_line(image, Vector2i(9, 16), Vector2i(22, 24), shadow, 5) + _icon_line(image, Vector2i(22, 8), Vector2i(9, 16), ink, 3) + _icon_line(image, Vector2i(9, 16), Vector2i(22, 24), ink, 3) + _icon_line(image, Vector2i(10, 16), Vector2i(17, 9), red, 3) + _icon_line(image, Vector2i(10, 16), Vector2i(17, 23), red, 3) _: _icon_circle(image, Vector2i(16, 16), 7, ink) return ImageTexture.create_from_image(image) @@ -1718,32 +1751,27 @@ func _create_hud() -> void: post_move_column.add_child(post_move_grid) post_move_attack_button = Button.new() - post_move_attack_button.text = "타격령" - _suppress_local_command_tooltip(post_move_attack_button) + _configure_local_command_button(post_move_attack_button, "attack", "타격령", "무기 공격") post_move_attack_button.pressed.connect(_on_post_move_attack_pressed) post_move_grid.add_child(post_move_attack_button) post_move_tactic_button = Button.new() - post_move_tactic_button.text = "책략첩" - _suppress_local_command_tooltip(post_move_tactic_button) + _configure_local_command_button(post_move_tactic_button, "tactic", "책략첩", "책략 표식") post_move_tactic_button.pressed.connect(_on_post_move_tactic_pressed) post_move_grid.add_child(post_move_tactic_button) post_move_item_button = Button.new() - post_move_item_button.text = "보급첩" - _suppress_local_command_tooltip(post_move_item_button) + _configure_local_command_button(post_move_item_button, "item", "보급첩", "보급 사용") post_move_item_button.pressed.connect(_on_post_move_item_pressed) post_move_grid.add_child(post_move_item_button) post_move_wait_button = Button.new() - post_move_wait_button.text = "대기령" - _suppress_local_command_tooltip(post_move_wait_button) + _configure_local_command_button(post_move_wait_button, "wait", "대기령", "행동 종료") post_move_wait_button.pressed.connect(_on_post_move_wait_pressed) post_move_grid.add_child(post_move_wait_button) post_move_cancel_button = Button.new() - post_move_cancel_button.text = "발걸음 거두기" - _suppress_local_command_tooltip(post_move_cancel_button) + _configure_local_command_button(post_move_cancel_button, "cancel", "발걸음 거두기", "이동 취소", LOCAL_COMMAND_CANCEL_BUTTON_SIZE) post_move_cancel_button.pressed.connect(_on_post_move_cancel_pressed) post_move_column.add_child(post_move_cancel_button) @@ -10142,36 +10170,90 @@ func _update_post_move_menu() -> void: return _position_post_move_menu() if post_move_title_label != null: - _set_fitted_label_text( - post_move_title_label, - "%s: 군령 선택" % str(selected.get("name", "부대")), - LOCAL_COMMAND_TITLE_FONT_SIZE, - LOCAL_COMMAND_TITLE_MIN_FONT_SIZE, - POST_MOVE_TITLE_SIZE - ) + _refresh_post_move_title(selected) if post_move_attack_button != null: var attack_blocked := not _has_basic_attack_target(pending_move_unit_id) - post_move_attack_button.text = "타격 없음" if attack_blocked else "타격령" - post_move_attack_button.disabled = attack_blocked - _suppress_local_command_tooltip(post_move_attack_button) + _set_local_command_button_state( + post_move_attack_button, + "타격령", + "무기 공격", + attack_blocked, + "타격 없음" + ) if post_move_tactic_button != null: var skill_blocked := not _unit_has_usable_skill_target(pending_move_unit_id) - post_move_tactic_button.text = "책략 없음" if skill_blocked else "책략첩" - post_move_tactic_button.disabled = skill_blocked - _suppress_local_command_tooltip(post_move_tactic_button) + _set_local_command_button_state( + post_move_tactic_button, + "책략첩", + "책략 표식", + skill_blocked, + "책략 없음" + ) if post_move_item_button != null: var item_blocked := not _unit_has_usable_item_target(pending_move_unit_id) - post_move_item_button.text = "보급 없음" if item_blocked else "보급첩" - post_move_item_button.disabled = item_blocked - _suppress_local_command_tooltip(post_move_item_button) + _set_local_command_button_state( + post_move_item_button, + "보급첩", + "보급 사용", + item_blocked, + "보급 없음" + ) if post_move_wait_button != null: - post_move_wait_button.text = "대기령" - post_move_wait_button.disabled = false - _suppress_local_command_tooltip(post_move_wait_button) + _set_local_command_button_state(post_move_wait_button, "대기령", "행동 종료", false, "") if post_move_cancel_button != null: - post_move_cancel_button.text = "발걸음 거두기" - post_move_cancel_button.disabled = false - _suppress_local_command_tooltip(post_move_cancel_button) + _set_local_command_button_state(post_move_cancel_button, "발걸음 거두기", "이동 취소", false, "") + + +func _set_local_command_button_state(button: Button, label: String, hint: String, disabled: bool, disabled_hint: String) -> void: + if button == null: + return + button.text = "" + button.disabled = disabled + button.tooltip_text = "" + button.set_meta("command_label", label) + button.set_meta("command_hint", hint) + button.set_meta("disabled_hint", disabled_hint) + + +func _refresh_post_move_title(selected: Dictionary = {}) -> void: + if post_move_title_label == null: + return + var focus := selected + if focus.is_empty(): + focus = state.get_selected_unit() + var unit_name := str(focus.get("name", "부대")) if not focus.is_empty() else "부대" + _set_fitted_label_text( + post_move_title_label, + "%s: 군령 선택" % unit_name, + LOCAL_COMMAND_TITLE_FONT_SIZE, + LOCAL_COMMAND_TITLE_MIN_FONT_SIZE, + POST_MOVE_TITLE_SIZE + ) + + +func _on_local_command_button_mouse_entered(button: Button) -> void: + if button == null or post_move_title_label == null: + return + var label := str(button.get_meta("command_label", "군령")) + var hint := str(button.get_meta("command_hint", "")) + if button.disabled: + var disabled_hint := str(button.get_meta("disabled_hint", "")) + if not disabled_hint.is_empty(): + hint = disabled_hint + var text := label + if not hint.is_empty(): + text = "%s · %s" % [label, hint] + _set_fitted_label_text( + post_move_title_label, + text, + LOCAL_COMMAND_TITLE_FONT_SIZE, + LOCAL_COMMAND_TITLE_MIN_FONT_SIZE, + POST_MOVE_TITLE_SIZE + ) + + +func _on_local_command_button_mouse_exited() -> void: + _refresh_post_move_title() func _local_command_panel_position_for_cell(cell: Vector2i, panel_size: Vector2) -> Vector2: diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index d8b247a..591715f 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -352,17 +352,21 @@ func _check_scene_post_move_blocked_command_labels(failures: Array[String]) -> v if scene.post_move_menu == null or not scene.post_move_menu.visible: failures.append("blocked command label setup should show the post-move menu") else: - if scene.post_move_attack_button == null or not scene.post_move_attack_button.disabled or scene.post_move_attack_button.text != "타격 없음": - failures.append("blocked attack command should explain itself through button text") - if scene.post_move_tactic_button == null or not scene.post_move_tactic_button.disabled or scene.post_move_tactic_button.text != "책략 없음": - failures.append("blocked tactic command should explain itself through button text") - if scene.post_move_item_button == null or not scene.post_move_item_button.disabled or scene.post_move_item_button.text != "보급 없음": - failures.append("blocked item command should explain itself through button text") + if scene.post_move_attack_button == null or not scene.post_move_attack_button.disabled or scene.post_move_attack_button.text != "" or str(scene.post_move_attack_button.get_meta("disabled_hint", "")) != "타격 없음": + failures.append("blocked attack command should explain itself through local hover metadata") + if scene.post_move_tactic_button == null or not scene.post_move_tactic_button.disabled or scene.post_move_tactic_button.text != "" or str(scene.post_move_tactic_button.get_meta("disabled_hint", "")) != "책략 없음": + failures.append("blocked tactic command should explain itself through local hover metadata") + if scene.post_move_item_button == null or not scene.post_move_item_button.disabled or scene.post_move_item_button.text != "" or str(scene.post_move_item_button.get_meta("disabled_hint", "")) != "보급 없음": + failures.append("blocked item command should explain itself through local hover metadata") _check_local_command_button_tooltips_suppressed(failures, [ scene.post_move_attack_button, scene.post_move_tactic_button, scene.post_move_item_button ], "blocked post-move commands") + scene._on_local_command_button_mouse_entered(scene.post_move_attack_button) + if scene.post_move_title_label == null or not scene.post_move_title_label.text.contains("타격 없음"): + failures.append("blocked attack hover should surface disabled reason in the local title plate: %s" % scene.post_move_title_label.text) + scene._on_local_command_button_mouse_exited() scene.free() @@ -409,8 +413,12 @@ func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void: scene._handle_board_click(_screen_for_cell(Vector2i(2, 3))) if not scene._has_pending_move() or scene.post_move_menu == null or not scene.post_move_menu.visible: failures.append("attack targeting setup should leave the post-move menu visible") - if scene.post_move_attack_button == null or scene.post_move_attack_button.disabled or scene.post_move_attack_button.text != "타격령": - failures.append("available attack command should stay compact and enabled") + if scene.post_move_attack_button == null or scene.post_move_attack_button.disabled or scene.post_move_attack_button.text != "" or str(scene.post_move_attack_button.get_meta("command_label", "")) != "타격령": + failures.append("available attack command should stay icon-first and enabled") + scene._on_local_command_button_mouse_entered(scene.post_move_attack_button) + if scene.post_move_title_label == null or not scene.post_move_title_label.text.contains("타격령"): + failures.append("available attack hover should explain the icon in the local title plate") + scene._on_local_command_button_mouse_exited() if scene.basic_attack_targeting: failures.append("attack targeting should be off before pressing Attack") if not scene.attack_cells.is_empty(): diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 7cc08c2..119602f 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3269,16 +3269,11 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: failures.append("briefing shop sell button should avoid modern sale wording") if scene.post_move_title_label == null or scene.post_move_title_label.text != "군령 선택": failures.append("post-move command title should use order wording") - if scene.post_move_attack_button == null or scene.post_move_attack_button.text != "타격령": - failures.append("post-move attack button should use old order wording") - if scene.post_move_tactic_button == null or scene.post_move_tactic_button.text != "책략첩": - failures.append("post-move tactic button should use old slip wording") - if scene.post_move_item_button == null or scene.post_move_item_button.text != "보급첩": - failures.append("post-move item button should use old supply-slip wording") - if scene.post_move_wait_button == null or scene.post_move_wait_button.text != "대기령": - failures.append("post-move wait button should use old order wording") - if scene.post_move_cancel_button == null or scene.post_move_cancel_button.text != "발걸음 거두기": - failures.append("post-move cancel button should use recall step wording") + _check_local_command_icon_button(failures, scene.post_move_attack_button, "타격령", "attack") + _check_local_command_icon_button(failures, scene.post_move_tactic_button, "책략첩", "tactic") + _check_local_command_icon_button(failures, scene.post_move_item_button, "보급첩", "item") + _check_local_command_icon_button(failures, scene.post_move_wait_button, "대기령", "wait") + _check_local_command_icon_button(failures, scene.post_move_cancel_button, "발걸음 거두기", "cancel") if scene.post_move_picker_back_button == null or scene.post_move_picker_back_button.text != "죽간 거두기": failures.append("post-move picker back button should use bamboo-slip wording") if scene.targeting_hint_title_label == null or scene.targeting_hint_title_label.text != "적장 지목": @@ -3460,6 +3455,22 @@ func _find_descendant_by_name(root: Node, node_name: String) -> Node: return null +func _check_local_command_icon_button(failures: Array[String], button: Button, expected_label: String, expected_icon: String) -> void: + if button == null: + failures.append("local command button missing: %s" % expected_label) + return + if not bool(button.get_meta("icon_only", false)) or button.text != "": + failures.append("local command 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("local command should carry an immediate visual 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("local command should keep old wording in hover metadata: %s meta=%s" % [expected_label, str(button.get_meta("command_label", ""))]) + if not str(button.tooltip_text).is_empty(): + failures.append("local command should keep guidance in the local panel instead of native tooltip: %s" % expected_label) + if button.custom_minimum_size.x <= 0.0 or button.custom_minimum_size.y <= 0.0: + failures.append("local command should reserve stable icon space: %s" % expected_label) + + func _check_hud_command_grid_layout(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud()