From 2ad8686ad5ccc2034cb75c731d8f0ad940cc1584 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Thu, 2 Apr 2026 23:02:18 -0400 Subject: [PATCH] Reorganized input --- .../plans/2026-04-02-input-centralization.md | 336 ++++++++++++++++++ .../2026-04-02-input-centralization-design.md | 94 +++++ nodes/combat_map.gd | 3 - nodes/player_controller.gd | 94 ++++- nodes/strategy_phase.gd | 22 +- scripts/camera_controller.gd | 41 +-- scripts/tile_highlight.gd | 18 +- 7 files changed, 533 insertions(+), 75 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-02-input-centralization.md create mode 100644 docs/superpowers/specs/2026-04-02-input-centralization-design.md diff --git a/docs/superpowers/plans/2026-04-02-input-centralization.md b/docs/superpowers/plans/2026-04-02-input-centralization.md new file mode 100644 index 0000000..eb93c39 --- /dev/null +++ b/docs/superpowers/plans/2026-04-02-input-centralization.md @@ -0,0 +1,336 @@ +# Input Centralization Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Centralize all non-UI input handling in PlayerController so gameplay input can be disabled with a single flag. + +**Architecture:** PlayerController owns all mouse input (clicks, drags, grid tracking) and emits signals. CameraController and TileHighlight become purely reactive — no direct input reading. `strategy_phase.gd` wires everything together and toggles `input_disabled` during overlays. + +**Tech Stack:** Godot 4, GDScript + +--- + +### Task 1: Add drag detection and input_disabled to PlayerController + +**Files:** +- Modify: `nodes/player_controller.gd` + +- [ ] **Step 1: Add new signals, properties, and drag state variables** + +Add after the existing `signal combat_requested` line (line 7) and update the variable block: + +```gdscript +signal combat_requested(attacker: Unit, defender: Unit) +signal mouse_grid_changed(coords: Vector2i) +signal camera_drag(delta: Vector2) + +var input_disabled := false + +var _selected_unit: Unit = null +var _target_pos: Vector2 +var _goal_pos: Vector2 +var _moving := false + +var _left_pending := false +var _drag_start := Vector2.ZERO +var _dragging := false +var _current_grid_coords := Vector2i(-99999, -99999) + +const DRAG_THRESHOLD := 8.0 +``` + +- [ ] **Step 2: Add mouse grid tracking to `_process`** + +Add a new `_process` method. This replaces the mouse tracking that `tile_highlight.gd` currently does: + +```gdscript +func _process(_delta: float) -> void: + if input_disabled: + return + var mouse_pos := get_viewport().get_canvas_transform().affine_inverse() * get_viewport().get_mouse_position() + var coords := dl_map.world_to_coords(mouse_pos) + if coords != _current_grid_coords: + _current_grid_coords = coords + mouse_grid_changed.emit(coords) +``` + +- [ ] **Step 3: Add drag handling to `_unhandled_input`** + +Replace the existing `_unhandled_input` method with this version that integrates drag detection (ported from `camera_controller.gd`): + +```gdscript +func _unhandled_input(event: InputEvent) -> void: + if input_disabled: + return + + if event is InputEventMouseButton: + match event.button_index: + MOUSE_BUTTON_LEFT: + if event.pressed: + _left_pending = true + _drag_start = event.position + else: + if _dragging: + _dragging = false + _left_pending = false + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + get_viewport().set_input_as_handled() + else: + _left_pending = false + _handle_left_click(event.position) + MOUSE_BUTTON_MIDDLE: + if event.pressed: + _dragging = true + _drag_start = event.position + Input.set_default_cursor_shape(Input.CURSOR_DRAG) + else: + _dragging = false + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + get_viewport().set_input_as_handled() + + elif event is InputEventMouseMotion: + if _left_pending and not _dragging: + if event.position.distance_to(_drag_start) >= DRAG_THRESHOLD: + _dragging = true + _left_pending = false + Input.set_default_cursor_shape(Input.CURSOR_DRAG) + if _dragging: + var delta: Vector2 = _drag_start - event.position + _drag_start = event.position + camera_drag.emit(delta) + get_viewport().set_input_as_handled() +``` + +- [ ] **Step 4: Extract click logic into `_handle_left_click`** + +Add this method — it's the existing click logic from `_unhandled_input`, refactored out so the drag/click split is clean: + +```gdscript +func _handle_left_click(screen_pos: Vector2) -> void: + var world_pos: Vector2 = get_viewport().get_canvas_transform().affine_inverse() * screen_pos + var clicked_unit := _get_unit_at(world_pos) + + if clicked_unit: + if _selected_unit and clicked_unit != _selected_unit and _selected_unit.is_alive() and clicked_unit.is_alive(): + combat_requested.emit(_selected_unit, clicked_unit) + else: + _select_unit(clicked_unit) + get_viewport().set_input_as_handled() + elif _selected_unit: + var snapped_pos := dl_map.snap_to_grid(world_pos) + var grid_coords := dl_map.world_to_coords(world_pos) + if dl_map.is_wall(grid_coords): + return + _goal_pos = snapped_pos + get_viewport().set_input_as_handled() +``` + +- [ ] **Step 5: Verify in editor** + +Run the scene. Confirm: +- Clicking units still selects/moves them +- Left-dragging pans camera (after 8px threshold) +- Middle-dragging pans camera immediately +- Grid highlight still works (will be wired in Task 4, but shouldn't crash) + +- [ ] **Step 6: Commit** + +```bash +git add nodes/player_controller.gd +git commit -m "feat: add drag detection and input_disabled to PlayerController" +``` + +--- + +### Task 2: Make CameraController reactive + +**Files:** +- Modify: `scripts/camera_controller.gd` + +- [ ] **Step 1: Replace CameraController with reactive version** + +Replace the entire file content with: + +```gdscript +class_name CameraController extends Camera2D + + +func apply_drag(delta: Vector2) -> void: + position += delta / zoom +``` + +- [ ] **Step 2: Verify in editor** + +Run the scene. Camera drag won't work yet (signal not wired until Task 4) but the scene should load without errors. + +- [ ] **Step 3: Commit** + +```bash +git add scripts/camera_controller.gd +git commit -m "refactor: make CameraController reactive, remove direct input handling" +``` + +--- + +### Task 3: Make TileHighlight reactive + +**Files:** +- Modify: `scripts/tile_highlight.gd` + +- [ ] **Step 1: Replace TileHighlight with reactive version** + +Replace the entire file content with: + +```gdscript +extends ColorRect + +@export var tile_size: float = 48.0 + +var _time: float = 0.0 + + +func _ready() -> void: + size = Vector2(tile_size, tile_size) + color = Color(1.0, 1.0, 1.0, 0.25) + mouse_filter = Control.MOUSE_FILTER_IGNORE + + +func _process(delta: float) -> void: + _time += delta + color.a = 0.25 + 0.1 * sin(_time * 4.0) + + +func set_grid_coords(coords: Vector2i) -> void: + global_position = Vector2(coords) * tile_size + + +func _notification(what: int) -> void: + if what == NOTIFICATION_WM_MOUSE_EXIT: + hide() + elif what == NOTIFICATION_WM_MOUSE_ENTER: + show() +``` + +The `tile_hovered` signal is removed — PlayerController now owns `mouse_grid_changed`. The `_process` only handles the pulse animation. `set_grid_coords` positions the highlight from external coords. + +- [ ] **Step 2: Verify in editor** + +Run the scene. Tile highlight won't track mouse yet (not wired until Task 4) but the scene should load without errors and the pulse animation should work if visible. + +- [ ] **Step 3: Commit** + +```bash +git add scripts/tile_highlight.gd +git commit -m "refactor: make TileHighlight reactive, remove direct mouse tracking" +``` + +--- + +### Task 4: Wire signals in strategy_phase.gd and update CombatMap + +**Files:** +- Modify: `nodes/strategy_phase.gd` +- Modify: `nodes/combat_map.gd` + +- [ ] **Step 1: Update CombatMap.set_highlight_enabled to accept no args** + +In `nodes/combat_map.gd`, the `set_highlight_enabled` method currently controls tile highlight visibility. It stays, but now it also needs to be callable from strategy_phase. No changes needed to the method itself — it already does what we need. However, remove the `tile_hovered` signal from CombatMap since PlayerController now owns grid change signaling. + +Remove line 4 from `combat_map.gd`: + +```gdscript +signal tile_hovered(coords: Vector2i) +``` + +And update `target_tile` (line 83-85) to remove the signal emit: + +```gdscript +func target_tile(coords: Vector2i) -> void: + highlight_map.target_tile(coords) +``` + +- [ ] **Step 2: Wire signals in strategy_phase.gd** + +Replace the entire file content of `nodes/strategy_phase.gd`: + +```gdscript +class_name StrategyPhase extends Node2D + +@onready var player_controller: PlayerController = $PlayerController +@onready var combat_system: CombatSystem = $CombatSystem +@onready var combat_ui: CombatUI = $CombatUI +@onready var combat_map: CombatMap = $CombatMap +@onready var camera: CameraController = $Camera2D + +func _ready() -> void: + player_controller.combat_requested.connect(_on_combat_requested) + player_controller.mouse_grid_changed.connect(_on_mouse_grid_changed) + player_controller.camera_drag.connect(camera.apply_drag) + combat_ui.fight_confirmed.connect(_on_fight_confirmed) + combat_ui.fight_cancelled.connect(_on_fight_cancelled) + + +func _on_mouse_grid_changed(coords: Vector2i) -> void: + combat_map.target_tile(coords) + combat_map.tile_highlight.set_grid_coords(coords) + + +func _on_combat_requested(attacker: Unit, defender: Unit) -> void: + var proposal := combat_system.create_proposal(attacker, defender) + _set_input_disabled(true) + combat_ui.show_proposal(proposal) + + +func _on_fight_confirmed(proposal: CombatProposal) -> void: + combat_system.apply_proposal(proposal) + _set_input_disabled(false) + + +func _on_fight_cancelled() -> void: + _set_input_disabled(false) + + +func _set_input_disabled(disabled: bool) -> void: + player_controller.input_disabled = disabled + combat_map.set_highlight_enabled(not disabled) +``` + +- [ ] **Step 3: Verify in editor** + +Run the scene. Confirm all behaviors work end-to-end: +- Mouse movement highlights tiles on the grid +- Left-click selects units and sets movement targets +- Left-drag (past threshold) pans the camera +- Middle-drag pans the camera immediately +- Opening combat proposal disables all input (no highlight, no clicks, no drags) +- Confirming or cancelling combat re-enables input +- Cursor resets to arrow when drag ends + +- [ ] **Step 4: Commit** + +```bash +git add nodes/strategy_phase.gd nodes/combat_map.gd +git commit -m "feat: wire centralized input signals, toggle input_disabled on overlays" +``` + +--- + +### Task 5: Clean up unused code + +**Files:** +- Modify: `nodes/combat_map.gd` (verify no remaining references to removed signal) + +- [ ] **Step 1: Check for remaining references to `tile_hovered` signal on CombatMap** + +Search the codebase for any connections to `combat_map.tile_hovered` or `CombatMap.tile_hovered`. If found, remove or update them. + +- [ ] **Step 2: Verify full scene runs cleanly** + +Run the scene one final time. Confirm no errors in the Godot console and all input behaviors work as expected. + +- [ ] **Step 3: Commit if any cleanup was needed** + +```bash +git add -A +git commit -m "chore: remove stale tile_hovered references" +``` diff --git a/docs/superpowers/specs/2026-04-02-input-centralization-design.md b/docs/superpowers/specs/2026-04-02-input-centralization-design.md new file mode 100644 index 0000000..d111af4 --- /dev/null +++ b/docs/superpowers/specs/2026-04-02-input-centralization-design.md @@ -0,0 +1,94 @@ +# Input Centralization Design + +## Problem + +Input handling is scattered across `PlayerController`, `CameraController`, and `TileHighlight`, each independently reading mouse state. There's no way to globally disable gameplay input during overlays or animations. + +## Design + +### PlayerController — single input authority + +`PlayerController` becomes the only script that reads raw input. All other gameplay scripts react to its signals. + +**Signals:** + +| Signal | Emitted when | +|--------|-------------| +| `mouse_grid_changed(coords: Vector2i)` | Hovered grid cell changes (every frame check in `_process()`) | +| `camera_drag(delta: Vector2)` | Mouse is dragged (left-drag past 8px threshold, or any middle-drag) | +| `combat_requested(attacker, defender)` | Existing — click on enemy unit while friendly selected | + +**Properties:** + +| Property | Purpose | +|----------|---------| +| `input_disabled: bool` | When true, all input processing stops — no signals emitted, no click handling, no movement | + +**Input handling (`_unhandled_input`):** + +- Left click release (no drag): unit selection or movement target (existing logic) +- Left click release on enemy unit while friendly selected: emit `combat_requested` (existing logic) +- Left mouse drag: track press position, once delta exceeds 8px threshold, enter drag mode and emit `camera_drag(delta)` on each `InputEventMouseMotion` +- Middle mouse drag: immediately enter drag mode, emit `camera_drag(delta)` on each motion +- All of the above gated by `if input_disabled: return` at the top + +**Mouse grid tracking (`_process`):** + +- Read `get_global_mouse_position()`, snap to grid, convert to `Vector2i` +- If coords changed from previous frame, emit `mouse_grid_changed(coords)` +- Gated by `input_disabled` + +**Left-drag vs left-click disambiguation:** + +- On left button press: record press position, set `_drag_candidate = true` +- On mouse motion while `_drag_candidate`: if distance from press > 8px, enter drag mode (`_dragging = true`), stop treating this as a click +- On left button release: if `_dragging`, end drag and reset state; if not, treat as click (existing selection/movement logic) +- This matches the existing `CameraController` behavior + +### CameraController — reactive + +- Remove `_unhandled_input()` entirely +- Remove all drag detection state (`_dragging`, `_drag_start`, etc.) +- Expose a method (e.g., `apply_drag(delta: Vector2)`) or connect directly to `camera_drag` signal +- Apply delta to camera position, respecting any existing bounds/smoothing + +### TileHighlight — reactive + +- Remove `_process()` mouse tracking and `get_global_mouse_position()` calls +- Keep the pulse animation (can run in its own `_process` gated on visibility) +- Expose a method like `set_grid_coords(coords: Vector2i)` that positions the highlight +- Hide when input is disabled (connected to a signal or called directly) +- Keep `_notification` for `WM_MOUSE_EXIT` / `WM_MOUSE_ENTER` if still relevant, or remove if PlayerController handles this + +### strategy_phase.gd — wiring + +Connects signals in `_ready()`: + +``` +player_controller.mouse_grid_changed → tile_highlight.set_grid_coords (or similar) +player_controller.camera_drag → camera_controller.apply_drag +``` + +Overlays toggle input: + +``` +# When showing overlay: +player_controller.input_disabled = true + +# When hiding overlay: +player_controller.input_disabled = false +``` + +## What doesn't change + +- `CombatUI._unhandled_input()` for right-click dismiss — this is UI-layer input, not gameplay input +- Unit signals (`unit_died`, `unit_selected_changed`) +- CombatSystem logic +- All existing click behavior (selection, movement, combat requests) — just consolidated under the `input_disabled` gate + +## Migration notes + +- `CameraController` drag state variables and `_unhandled_input` are deleted, not left as dead code +- `TileHighlight._process` mouse reading is deleted +- `TileHighlight` keeps its `tile_size` export and grid-snapping math (used by `set_grid_coords`) +- PlayerController needs `dl_map` reference for grid coordinate conversion (already has it) diff --git a/nodes/combat_map.gd b/nodes/combat_map.gd index 323700f..5539a8a 100644 --- a/nodes/combat_map.gd +++ b/nodes/combat_map.gd @@ -1,8 +1,6 @@ class_name CombatMap extends Node2D -signal tile_hovered(coords: Vector2i) - @export var tile_set: DLTileset @onready var tile_map: TileMapLayer = %TerrainLayer @onready var highlight_map: GridOverlay = %OverlayLayer @@ -82,7 +80,6 @@ func remove_unit(unit: Unit) -> void: func target_tile(coords: Vector2i) -> void: highlight_map.target_tile(coords) - tile_hovered.emit(coords) func set_highlight_enabled(enabled: bool) -> void: diff --git a/nodes/player_controller.gd b/nodes/player_controller.gd index 4e844a0..b171aa0 100644 --- a/nodes/player_controller.gd +++ b/nodes/player_controller.gd @@ -5,12 +5,23 @@ const SPEED = 192.0 @export var dl_map: CombatMap signal combat_requested(attacker: Unit, defender: Unit) +signal mouse_grid_changed(coords: Vector2i) +signal camera_drag(delta: Vector2) + +var input_disabled := false var _selected_unit: Unit = null var _target_pos: Vector2 var _goal_pos: Vector2 var _moving := false +var _left_pending := false +var _drag_start := Vector2.ZERO +var _dragging := false +var _current_grid_coords := Vector2i(-99999, -99999) + +const DRAG_THRESHOLD := 8.0 + func _ready() -> void: for unit: Unit in get_tree().get_nodes_in_group("units"): @@ -30,23 +41,55 @@ func _on_unit_died(unit: Unit) -> void: _moving = false -func _unhandled_input(event: InputEvent) -> void: - if event is InputEventMouseButton and not event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - var world_pos: Vector2 = get_viewport().get_canvas_transform().affine_inverse() * event.position - var clicked_unit := _get_unit_at(world_pos) +func _process(_delta: float) -> void: + if input_disabled: + return + var mouse_pos := get_viewport().get_canvas_transform().affine_inverse() * get_viewport().get_mouse_position() + var coords := dl_map.world_to_coords(mouse_pos) + if coords != _current_grid_coords: + _current_grid_coords = coords + mouse_grid_changed.emit(coords) - if clicked_unit: - if _selected_unit and clicked_unit != _selected_unit and _selected_unit.is_alive() and clicked_unit.is_alive(): - combat_requested.emit(_selected_unit, clicked_unit) - else: - _select_unit(clicked_unit) - get_viewport().set_input_as_handled() - elif _selected_unit: - var snapped_pos := dl_map.snap_to_grid(world_pos) - var grid_coords := dl_map.world_to_coords(world_pos) - if dl_map.is_wall(grid_coords): - return - _goal_pos = snapped_pos + +func _unhandled_input(event: InputEvent) -> void: + if input_disabled: + return + + if event is InputEventMouseButton: + match event.button_index: + MOUSE_BUTTON_LEFT: + if event.pressed: + _left_pending = true + _drag_start = event.position + else: + if _dragging: + _dragging = false + _left_pending = false + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + get_viewport().set_input_as_handled() + else: + _left_pending = false + _handle_left_click(event.position) + MOUSE_BUTTON_MIDDLE: + if event.pressed: + _dragging = true + _drag_start = event.position + Input.set_default_cursor_shape(Input.CURSOR_DRAG) + else: + _dragging = false + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + get_viewport().set_input_as_handled() + + elif event is InputEventMouseMotion: + if _left_pending and not _dragging: + if event.position.distance_to(_drag_start) >= DRAG_THRESHOLD: + _dragging = true + _left_pending = false + Input.set_default_cursor_shape(Input.CURSOR_DRAG) + if _dragging: + var delta: Vector2 = _drag_start - event.position + _drag_start = event.position + camera_drag.emit(delta) get_viewport().set_input_as_handled() @@ -83,6 +126,25 @@ func _physics_process(delta: float) -> void: _moving = true +func _handle_left_click(screen_pos: Vector2) -> void: + var world_pos: Vector2 = get_viewport().get_canvas_transform().affine_inverse() * screen_pos + var clicked_unit := _get_unit_at(world_pos) + + if clicked_unit: + if _selected_unit and clicked_unit != _selected_unit and _selected_unit.is_alive() and clicked_unit.is_alive(): + combat_requested.emit(_selected_unit, clicked_unit) + else: + _select_unit(clicked_unit) + get_viewport().set_input_as_handled() + elif _selected_unit: + var snapped_pos := dl_map.snap_to_grid(world_pos) + var grid_coords := dl_map.world_to_coords(world_pos) + if dl_map.is_wall(grid_coords): + return + _goal_pos = snapped_pos + get_viewport().set_input_as_handled() + + func _select_unit(unit: Unit) -> void: if _selected_unit: _selected_unit.set_selected(false) diff --git a/nodes/strategy_phase.gd b/nodes/strategy_phase.gd index 53b6d98..2baf789 100644 --- a/nodes/strategy_phase.gd +++ b/nodes/strategy_phase.gd @@ -4,20 +4,36 @@ class_name StrategyPhase extends Node2D @onready var combat_system: CombatSystem = $CombatSystem @onready var combat_ui: CombatUI = $CombatUI @onready var combat_map: CombatMap = $CombatMap +@onready var camera: CameraController = $Camera2D func _ready() -> void: player_controller.combat_requested.connect(_on_combat_requested) + player_controller.mouse_grid_changed.connect(_on_mouse_grid_changed) + player_controller.camera_drag.connect(camera.apply_drag) combat_ui.fight_confirmed.connect(_on_fight_confirmed) combat_ui.fight_cancelled.connect(_on_fight_cancelled) + +func _on_mouse_grid_changed(coords: Vector2i) -> void: + combat_map.target_tile(coords) + combat_map.tile_highlight.set_grid_coords(coords) + + func _on_combat_requested(attacker: Unit, defender: Unit) -> void: var proposal := combat_system.create_proposal(attacker, defender) - combat_map.set_highlight_enabled(false) + _set_input_disabled(true) combat_ui.show_proposal(proposal) + func _on_fight_confirmed(proposal: CombatProposal) -> void: combat_system.apply_proposal(proposal) - combat_map.set_highlight_enabled(true) + _set_input_disabled(false) + func _on_fight_cancelled() -> void: - combat_map.set_highlight_enabled(true) + _set_input_disabled(false) + + +func _set_input_disabled(disabled: bool) -> void: + player_controller.input_disabled = disabled + combat_map.set_highlight_enabled(not disabled) diff --git a/scripts/camera_controller.gd b/scripts/camera_controller.gd index 5dbfac1..ef12d45 100644 --- a/scripts/camera_controller.gd +++ b/scripts/camera_controller.gd @@ -1,42 +1,5 @@ class_name CameraController extends Camera2D -const DRAG_THRESHOLD := 8.0 -var _dragging := false -var _left_pending := false -var _drag_start := Vector2.ZERO - - -func _unhandled_input(event: InputEvent) -> void: - if event is InputEventMouseButton: - match event.button_index: - MOUSE_BUTTON_LEFT: - if event.pressed: - _left_pending = true - _drag_start = event.position - else: - _left_pending = false - if _dragging: - _dragging = false - Input.set_default_cursor_shape(Input.CURSOR_ARROW) - get_viewport().set_input_as_handled() - MOUSE_BUTTON_MIDDLE: - if event.pressed: - _dragging = true - _drag_start = event.position - Input.set_default_cursor_shape(Input.CURSOR_DRAG) - else: - _dragging = false - Input.set_default_cursor_shape(Input.CURSOR_ARROW) - get_viewport().set_input_as_handled() - elif event is InputEventMouseMotion: - if _left_pending and not _dragging: - if event.position.distance_to(_drag_start) >= DRAG_THRESHOLD: - _dragging = true - _left_pending = false - Input.set_default_cursor_shape(Input.CURSOR_DRAG) - if _dragging: - var delta: Vector2 = _drag_start - event.position - _drag_start = event.position - position += delta / zoom - get_viewport().set_input_as_handled() +func apply_drag(delta: Vector2) -> void: + position += delta / zoom diff --git a/scripts/tile_highlight.gd b/scripts/tile_highlight.gd index bf82c3a..e43fb7a 100644 --- a/scripts/tile_highlight.gd +++ b/scripts/tile_highlight.gd @@ -1,11 +1,8 @@ extends ColorRect -signal tile_hovered(coords: Vector2i) - @export var tile_size: float = 48.0 var _time: float = 0.0 -var _previous_coords := Vector2i(INF, INF) func _ready() -> void: @@ -17,13 +14,10 @@ func _ready() -> void: func _process(delta: float) -> void: _time += delta color.a = 0.25 + 0.1 * sin(_time * 4.0) - var mouse_pos := get_global_mouse_position() - var snapped_pos := _snap_to_grid(mouse_pos) - global_position = snapped_pos - var coords := Vector2i(snapped_pos / tile_size) - if coords != _previous_coords: - _previous_coords = coords - tile_hovered.emit(coords) + + +func set_grid_coords(coords: Vector2i) -> void: + global_position = Vector2(coords) * tile_size func _notification(what: int) -> void: @@ -31,7 +25,3 @@ func _notification(what: int) -> void: hide() elif what == NOTIFICATION_WM_MOUSE_ENTER: show() - - -func _snap_to_grid(pos: Vector2) -> Vector2: - return Vector2(floorf(pos.x / tile_size), floorf(pos.y / tile_size)) * tile_size