# 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"
```
