# 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)
