From 684332c6d80b6ec783e6014f06cca9c7ab78f174 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 23:29:42 +0900 Subject: [PATCH] Improve local post-move action UI --- scripts/scenes/battle_scene.gd | 87 +++++++++++++++++----------- tools/smoke_post_move_action_flow.gd | 73 ++++++++++++++++++++--- 2 files changed, 119 insertions(+), 41 deletions(-) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 7f415e6..dfef461 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -65,11 +65,10 @@ const SHOP_ITEM_ICON_SIZE := Vector2(48, 48) const HUD_UNIT_PORTRAIT_SIZE := Vector2(92, 104) const HUD_UNIT_PORTRAIT_STACK_SIZE := Vector2(88, 100) const POST_MOVE_MENU_SIZE := Vector2(228, 120) -const POST_MOVE_MENU_OFFSET := Vector2(20, -34) const POST_MOVE_PICKER_PANEL_SIZE := Vector2(320, 210) -const POST_MOVE_PICKER_PANEL_OFFSET := Vector2(20, 92) const TARGETING_HINT_PANEL_SIZE := Vector2(240, 94) -const TARGETING_HINT_PANEL_OFFSET := Vector2(18, 88) +const LOCAL_COMMAND_PANEL_GAP := 8.0 +const LOCAL_COMMAND_PANEL_BOARD_PADDING := 4.0 var state: BattleState = BattleStateScript.new() var campaign_state: CampaignState = CampaignStateScript.new() @@ -565,7 +564,7 @@ func _create_hud() -> void: post_move_picker_cancel_move_button = Button.new() post_move_picker_cancel_move_button.text = "Cancel Move" - post_move_picker_cancel_move_button.tooltip_text = "Return to the starting cell." + post_move_picker_cancel_move_button.tooltip_text = "Right-click also returns to the starting cell." post_move_picker_cancel_move_button.pressed.connect(_on_post_move_picker_cancel_move_pressed) picker_button_row.add_child(post_move_picker_cancel_move_button) @@ -3771,6 +3770,9 @@ func _on_tactic_pressed() -> void: var selected := state.get_selected_unit() if selected.is_empty(): return + if _has_pending_move(): + _on_post_move_tactic_pressed() + return if state.is_unit_skill_sealed(selected["id"]): _play_ui_cancel() selected_skill_id = "" @@ -3803,6 +3805,9 @@ func _on_item_pressed() -> void: var selected := state.get_selected_unit() if selected.is_empty(): return + if _has_pending_move(): + _on_post_move_item_pressed() + return _play_ui_click() if item_menu != null and item_menu.visible: _hide_item_menu() @@ -6076,19 +6081,51 @@ func _update_post_move_menu() -> void: post_move_cancel_button.tooltip_text = "Right-click also returns to the starting cell." +func _local_command_panel_position_for_cell(cell: Vector2i, panel_size: Vector2) -> Vector2: + if not state.is_inside(cell): + return BOARD_OFFSET + Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING) + var cell_rect := _rect_for_cell(cell) + var centered_y := cell_rect.position.y + (TILE_SIZE - panel_size.y) * 0.5 + var centered_x := cell_rect.position.x + (TILE_SIZE - panel_size.x) * 0.5 + var candidates: Array[Vector2] = [ + Vector2(cell_rect.end.x + LOCAL_COMMAND_PANEL_GAP, centered_y), + Vector2(cell_rect.position.x - panel_size.x - LOCAL_COMMAND_PANEL_GAP, centered_y), + Vector2(centered_x, cell_rect.end.y + LOCAL_COMMAND_PANEL_GAP), + Vector2(centered_x, cell_rect.position.y - panel_size.y - LOCAL_COMMAND_PANEL_GAP) + ] + var avoid_rect := cell_rect.grow(2.0) + var best_position := _clamped_local_command_panel_position(candidates[0], panel_size) + var best_score := 1000000000.0 + for candidate in candidates: + var next_position := _clamped_local_command_panel_position(candidate, panel_size) + var panel_rect := Rect2(next_position, panel_size) + var score := next_position.distance_squared_to(candidate) + if panel_rect.intersects(avoid_rect): + score += 1000000.0 + if score < best_score: + best_score = score + best_position = next_position + if score < 1.0 and not panel_rect.intersects(avoid_rect): + break + return best_position + + +func _clamped_local_command_panel_position(position: Vector2, panel_size: Vector2) -> Vector2: + var board_size := Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE + var board_min := BOARD_OFFSET + Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING) + var board_max := BOARD_OFFSET + board_size - panel_size - Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING) + board_max.x = maxf(board_min.x, board_max.x) + board_max.y = maxf(board_min.y, board_max.y) + return Vector2( + clampf(position.x, board_min.x, board_max.x), + clampf(position.y, board_min.y, board_max.y) + ) + + func _position_post_move_menu() -> void: if post_move_menu == null: return - var cell_rect := _rect_for_cell(pending_move_to_cell) - var next_position := cell_rect.position + Vector2(TILE_SIZE, 0.0) + POST_MOVE_MENU_OFFSET - var board_size := Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE - var board_min := BOARD_OFFSET + Vector2(4.0, 4.0) - var board_max := BOARD_OFFSET + board_size - POST_MOVE_MENU_SIZE - Vector2(4.0, 4.0) - board_max.x = maxf(board_min.x, board_max.x) - board_max.y = maxf(board_min.y, board_max.y) - next_position.x = clampf(next_position.x, board_min.x, board_max.x) - next_position.y = clampf(next_position.y, board_min.y, board_max.y) - post_move_menu.position = next_position + post_move_menu.position = _local_command_panel_position_for_cell(pending_move_to_cell, POST_MOVE_MENU_SIZE) func _show_post_move_picker(mode: String) -> void: @@ -6160,16 +6197,7 @@ func _clear_post_move_picker_list() -> void: func _position_post_move_picker() -> void: if post_move_picker_panel == null: return - var cell_rect := _rect_for_cell(pending_move_to_cell) - var next_position := cell_rect.position + POST_MOVE_PICKER_PANEL_OFFSET - var board_size := Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE - var board_min := BOARD_OFFSET + Vector2(4.0, 4.0) - var board_max := BOARD_OFFSET + board_size - POST_MOVE_PICKER_PANEL_SIZE - Vector2(4.0, 4.0) - board_max.x = maxf(board_min.x, board_max.x) - board_max.y = maxf(board_min.y, board_max.y) - next_position.x = clampf(next_position.x, board_min.x, board_max.x) - next_position.y = clampf(next_position.y, board_min.y, board_max.y) - post_move_picker_panel.position = next_position + post_move_picker_panel.position = _local_command_panel_position_for_cell(pending_move_to_cell, POST_MOVE_PICKER_PANEL_SIZE) func _post_move_picker_title_text(selected: Dictionary) -> String: @@ -6433,16 +6461,7 @@ func _position_targeting_hint_panel(selected: Dictionary) -> void: var cell: Vector2i = selected.get("pos", Vector2i(-1, -1)) if not state.is_inside(cell): return - var cell_rect := _rect_for_cell(cell) - var next_position := cell_rect.position + TARGETING_HINT_PANEL_OFFSET - var board_size := Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE - var board_min := BOARD_OFFSET + Vector2(4.0, 4.0) - var board_max := BOARD_OFFSET + board_size - TARGETING_HINT_PANEL_SIZE - Vector2(4.0, 4.0) - board_max.x = maxf(board_min.x, board_max.x) - board_max.y = maxf(board_min.y, board_max.y) - next_position.x = clampf(next_position.x, board_min.x, board_max.x) - next_position.y = clampf(next_position.y, board_min.y, board_max.y) - targeting_hint_panel.position = next_position + targeting_hint_panel.position = _local_command_panel_position_for_cell(cell, TARGETING_HINT_PANEL_SIZE) func _targeting_hint_title_text() -> String: diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index dcdfdbd..aa4592f 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -8,6 +8,7 @@ func _init() -> void: var failures: Array[String] = [] _check_deferred_move_events(failures) _check_scene_post_move_menu_flow(failures) + _check_scene_post_move_edge_positioning(failures) _check_scene_post_move_tactic_picker_flow(failures) _check_scene_post_move_item_picker_flow(failures) @@ -96,6 +97,8 @@ func _check_scene_post_move_menu_flow(failures: Array[String]) -> void: failures.append("scene should keep pending move metadata after move click") if scene.post_move_menu == null or not scene.post_move_menu.visible: failures.append("post-move action menu should be visible after moving") + else: + _check_local_panel_position(failures, scene, scene.post_move_menu, Vector2i(2, 3), "post-move action menu") if cao_cao.get("pos", Vector2i.ZERO) != Vector2i(2, 3) or not bool(cao_cao.get("moved", false)) or bool(cao_cao.get("acted", false)): failures.append("scene move should be pending with action still available") if scene.state.get_selected_unit().is_empty(): @@ -103,7 +106,11 @@ func _check_scene_post_move_menu_flow(failures: Array[String]) -> void: if not scene.attack_cells.is_empty(): failures.append("attack cells should wait until Attack is chosen from the post-move menu") - scene._handle_cancel_input() + var right_click := InputEventMouseButton.new() + right_click.button_index = MOUSE_BUTTON_RIGHT + right_click.pressed = true + right_click.position = _screen_for_cell(Vector2i(2, 3)) + scene._unhandled_input(right_click) cao_cao = scene.state.get_unit("cao_cao") if scene._has_pending_move() or (scene.post_move_menu != null and scene.post_move_menu.visible): failures.append("right-click cancel handler should clear pending move menu state") @@ -124,6 +131,41 @@ func _check_scene_post_move_menu_flow(failures: Array[String]) -> void: scene.free() +func _check_scene_post_move_edge_positioning(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + scene._create_hud() + if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("could not load opening battle for edge menu positioning") + scene.free() + return + scene.battle_started = true + scene.campaign_complete_screen = false + scene.briefing_panel.visible = false + scene.result_panel.visible = false + scene._clear_pending_move_state() + + if not scene.state.select_unit("cao_cao"): + failures.append("could not select Cao Cao for edge menu positioning") + scene.free() + return + var edge_cell := Vector2i(scene.state.map_size.x - 1, 3) + var cao_cao: Dictionary = scene.state.get_unit("cao_cao") + cao_cao["pos"] = edge_cell + cao_cao["moved"] = true + cao_cao["acted"] = false + scene.pending_move_unit_id = "cao_cao" + scene.pending_move_from_cell = Vector2i(edge_cell.x - 1, edge_cell.y) + scene.pending_move_to_cell = edge_cell + scene._show_post_move_menu() + + if scene.post_move_menu == null or not scene.post_move_menu.visible: + failures.append("edge post-move action menu should be visible") + else: + _check_local_panel_position(failures, scene, scene.post_move_menu, edge_cell, "edge post-move action menu") + + scene.free() + + func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() @@ -252,13 +294,13 @@ func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void: if scene.post_move_tactic_button == null or scene.post_move_tactic_button.disabled: failures.append("post-move Tactic should be enabled when Spark has a valid target") - scene._on_post_move_tactic_pressed() + scene._on_tactic_pressed() if scene.post_move_picker_panel == null or not scene.post_move_picker_panel.visible or scene.post_move_picker_mode != "tactic": - failures.append("post-move Tactic should open the local tactic picker") + failures.append("post-move Tactic shortcut should open the local tactic picker") if scene.post_move_menu != null and scene.post_move_menu.visible: failures.append("post-move action menu should hide while the tactic picker is open") if scene.tactic_menu != null and scene.tactic_menu.visible: - failures.append("post-move local tactic picker should not use the side tactic menu") + failures.append("post-move tactic shortcut should not use the side tactic menu") var escape := InputEventKey.new() escape.keycode = KEY_ESCAPE @@ -359,13 +401,13 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void: if scene.post_move_item_button == null or scene.post_move_item_button.disabled: failures.append("post-move Item should be enabled when Bean can heal an adjacent ally") - scene._on_post_move_item_pressed() + scene._on_item_pressed() if scene.post_move_picker_panel == null or not scene.post_move_picker_panel.visible or scene.post_move_picker_mode != "item": - failures.append("post-move Item should open the local item picker") + failures.append("post-move Item shortcut should open the local item picker") if scene.post_move_menu != null and scene.post_move_menu.visible: failures.append("post-move action menu should hide while the item picker is open") if scene.item_menu != null and scene.item_menu.visible: - failures.append("post-move local item picker should not use the side item menu") + failures.append("post-move item shortcut should not use the side item menu") scene._on_post_move_item_option_pressed("bean") if scene.selected_item_id != "bean": @@ -416,5 +458,22 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void: scene.free() +func _check_local_panel_position(failures: Array[String], scene, panel: Control, cell: Vector2i, label: String) -> void: + var padding := BattleSceneScript.LOCAL_COMMAND_PANEL_BOARD_PADDING + var board_position := BattleSceneScript.BOARD_OFFSET + Vector2(padding, padding) + var board_size := Vector2(scene.state.map_size.x, scene.state.map_size.y) * BattleSceneScript.TILE_SIZE - Vector2(padding * 2.0, padding * 2.0) + var board_rect := Rect2(board_position, board_size) + var panel_rect := Rect2(panel.position, panel.size) + var cell_rect: Rect2 = scene._rect_for_cell(cell).grow(2.0) + if panel_rect.size.x <= 0.0 or panel_rect.size.y <= 0.0: + failures.append("%s should have a stable visible size" % label) + if panel_rect.position.x < board_rect.position.x or panel_rect.position.y < board_rect.position.y: + failures.append("%s should stay inside the board minimum: %s" % [label, str(panel_rect)]) + if panel_rect.end.x > board_rect.end.x or panel_rect.end.y > board_rect.end.y: + failures.append("%s should stay inside the board maximum: %s" % [label, str(panel_rect)]) + if panel_rect.intersects(cell_rect): + failures.append("%s should not cover the moved unit cell: %s over %s" % [label, str(panel_rect), str(cell_rect)]) + + func _screen_for_cell(cell: Vector2i) -> Vector2: return BattleSceneScript.BOARD_OFFSET + Vector2(cell.x, cell.y) * BattleSceneScript.TILE_SIZE + Vector2.ONE * (BattleSceneScript.TILE_SIZE * 0.5)