Files
MaidEngine/docs/superpowers/specs/2026-04-02-input-centralization-design.md
2026-04-02 23:02:18 -04:00

4.0 KiB

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)