From c40bccf04462216a1fb83fd84108b6a58f5c2bf0 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 18:11:05 +0900 Subject: [PATCH] Track tactical map focus --- scripts/scenes/battle_scene.gd | 60 +++++++++++++++++++++++++++++++ tools/smoke_chapter_one_polish.gd | 51 ++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 86d1695..384926f 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -55,6 +55,7 @@ const MINIMAP_SIZE := Vector2(178, 134) const MINIMAP_PADDING := 8.0 const MINIMAP_TITLE_HEIGHT := 18.0 const MINIMAP_MARGIN := Vector2(18, 18) +const MAP_FOCUS_MARGIN := 96.0 const UNIT_MOVE_ANIMATION_DURATION := 0.18 const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22) const LOW_HP_WARNING_RATIO := 0.35 @@ -4792,6 +4793,57 @@ func _scroll_board_to_minimap_position(screen_position: Vector2) -> bool: return previous_offset.distance_squared_to(board_scroll_offset) > 0.01 +func _board_scroll_offset_for_cell_focus(cell: Vector2i, force_center := false) -> Vector2: + if not state.is_inside(cell): + return _clamped_board_scroll_offset(board_scroll_offset) + var view_rect := _map_view_rect() + var cell_center := BOARD_OFFSET + board_scroll_offset + Vector2(cell.x + 0.5, cell.y + 0.5) * TILE_SIZE + if force_center: + var target_local := Vector2(cell.x + 0.5, cell.y + 0.5) * TILE_SIZE + return _clamped_board_scroll_offset(view_rect.get_center() - BOARD_OFFSET - target_local) + + var margin := minf( + MAP_FOCUS_MARGIN, + maxf(12.0, minf(view_rect.size.x, view_rect.size.y) * 0.22) + ) + var next_offset := board_scroll_offset + var min_x := view_rect.position.x + margin + var max_x := view_rect.end.x - margin + var min_y := view_rect.position.y + margin + var max_y := view_rect.end.y - margin + if min_x > max_x: + min_x = view_rect.position.x + TILE_SIZE * 0.5 + max_x = view_rect.end.x - TILE_SIZE * 0.5 + if min_y > max_y: + min_y = view_rect.position.y + TILE_SIZE * 0.5 + max_y = view_rect.end.y - TILE_SIZE * 0.5 + if cell_center.x < min_x: + next_offset.x += min_x - cell_center.x + elif cell_center.x > max_x: + next_offset.x -= cell_center.x - max_x + if cell_center.y < min_y: + next_offset.y += min_y - cell_center.y + elif cell_center.y > max_y: + next_offset.y -= cell_center.y - max_y + return _clamped_board_scroll_offset(next_offset) + + +func _focus_board_on_cell(cell: Vector2i, force_center := false) -> bool: + if not battle_started or not state.is_inside(cell): + return false + var previous_offset := board_scroll_offset + board_scroll_offset = _board_scroll_offset_for_cell_focus(cell, force_center) + if previous_offset.distance_squared_to(board_scroll_offset) <= 0.01: + return false + if post_move_menu != null and post_move_menu.visible: + _position_post_move_menu() + if post_move_picker_panel != null and post_move_picker_panel.visible: + _position_post_move_picker() + if targeting_hint_panel != null and targeting_hint_panel.visible: + _update_targeting_hint_panel() + return true + + func _is_minimap_visible() -> bool: return battle_started and state.map_size.x > 0 and state.map_size.y > 0 and state.battle_status == BattleState.STATUS_ACTIVE @@ -5670,6 +5722,10 @@ func _on_state_changed() -> void: selected_skill_id = "" selected_item_id = "" _clear_pending_move_state() + else: + var selected := state.get_selected_unit() + if not selected.is_empty(): + _focus_board_on_cell(selected.get("pos", Vector2i(-1, -1))) _refresh_ranges() _update_hud() queue_redraw() @@ -5732,6 +5788,7 @@ func _on_combat_feedback_requested(unit_id: String, text: String, kind: String) var cell: Vector2i = unit.get("pos", Vector2i(-1, -1)) if not state.is_inside(cell): return + _focus_board_on_cell(cell, true) var jitter := float((floating_texts.size() % 3) - 1) * 12.0 floating_texts.append({ "text": text, @@ -5746,6 +5803,7 @@ func _on_combat_feedback_requested(unit_id: String, text: String, kind: String) func _on_unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Vector2i) -> void: if unit_id.is_empty() or from_cell == to_cell: return + _focus_board_on_cell(to_cell) unit_motion_by_unit[unit_id] = { "from": from_cell, "to": to_cell, @@ -5762,6 +5820,8 @@ func _on_unit_action_motion_requested(unit_id: String, from_cell: Vector2i, to_c var profile := _unit_action_motion_profile(unit, from_cell, to_cell, action_kind) if from_cell == to_cell and not _action_motion_allows_same_cell(profile): return + var focus_cell := to_cell if state.is_inside(to_cell) else from_cell + _focus_board_on_cell(focus_cell, true) unit_action_motion_by_unit[unit_id] = { "from": from_cell, "to": to_cell, diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index bd58d08..fdd71c0 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -21,6 +21,7 @@ func _init() -> void: _check_chapter_one_scenarios_have_visual_grounding(failures) _check_edge_scroll_layout_contract(failures) _check_minimap_navigation_contract(failures) + _check_map_focus_contract(failures) _check_readability_contract(failures) _check_dialogue_localization_and_side(failures) _check_event_dialogue_side_passthrough(failures) @@ -239,6 +240,56 @@ func _check_minimap_navigation_contract(failures: Array[String]) -> void: scene.free() +func _check_map_focus_contract(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/009_xiapi_siege.json"): + failures.append("could not load large chapter one map for map focus") + scene.free() + return + scene.battle_started = true + for method_name in [ + "_board_scroll_offset_for_cell_focus", + "_focus_board_on_cell" + ]: + if not scene.has_method(method_name): + failures.append("missing map focus helper: %s" % method_name) + + var view_rect: Rect2 = scene._map_view_rect() + var far_cell := Vector2i(scene.state.map_size.x - 1, 4) + scene.board_scroll_offset = Vector2.ZERO + var far_offset: Vector2 = scene._board_scroll_offset_for_cell_focus(far_cell) + if far_offset.x >= -0.01: + failures.append("far map focus should scroll horizontally on a large map: %s" % str(far_offset)) + var tile_size := float(scene.TILE_SIZE) + var far_center: Vector2 = scene.BOARD_OFFSET + far_offset + Vector2(far_cell.x + 0.5, far_cell.y + 0.5) * tile_size + if far_center.x > view_rect.end.x - 12.0 or far_center.x < view_rect.position.x + 12.0: + failures.append("far map focus should keep the target cell inside the visible view: %s in %s" % [str(far_center), str(view_rect)]) + + var center_cell := Vector2i(scene.state.map_size.x / 2, 4) + var center_offset: Vector2 = scene._board_scroll_offset_for_cell_focus(center_cell, true) + var centered_local: Vector2 = view_rect.get_center() - (scene.BOARD_OFFSET + center_offset) + var expected_local: Vector2 = Vector2(center_cell.x + 0.5, center_cell.y + 0.5) * tile_size + if scene._board_size().x > view_rect.size.x and absf(centered_local.x - expected_local.x) > 1.0: + failures.append("forced map focus should center the target X for action feedback: %s vs %s" % [str(centered_local), str(expected_local)]) + + scene.board_scroll_offset = scene._clamped_board_scroll_offset(Vector2(-9999.0, -9999.0)) + var origin_offset: Vector2 = scene._board_scroll_offset_for_cell_focus(Vector2i(0, 0)) + if origin_offset.x > 0.01 or origin_offset.y > 0.01: + failures.append("origin map focus should never scroll past the map origin: %s" % str(origin_offset)) + + scene.board_scroll_offset = Vector2.ZERO + var near_offset: Vector2 = scene._board_scroll_offset_for_cell_focus(Vector2i(2, 4)) + if near_offset.distance_squared_to(Vector2.ZERO) > 0.01: + failures.append("map focus should not move when the target is already comfortably visible: %s" % str(near_offset)) + + scene.board_scroll_offset = Vector2.ZERO + if not scene._focus_board_on_cell(far_cell): + failures.append("map focus action should report movement for an off-screen cell") + if scene.board_scroll_offset.distance_squared_to(far_offset) > 0.01: + failures.append("map focus action should apply the calculated offset: %s vs %s" % [str(scene.board_scroll_offset), str(far_offset)]) + scene.free() + + func _check_readability_contract(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud()