Reorganized input
This commit is contained in:
336
docs/superpowers/plans/2026-04-02-input-centralization.md
Normal file
336
docs/superpowers/plans/2026-04-02-input-centralization.md
Normal file
@@ -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"
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
class_name CombatMap
|
class_name CombatMap
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
signal tile_hovered(coords: Vector2i)
|
|
||||||
|
|
||||||
@export var tile_set: DLTileset
|
@export var tile_set: DLTileset
|
||||||
@onready var tile_map: TileMapLayer = %TerrainLayer
|
@onready var tile_map: TileMapLayer = %TerrainLayer
|
||||||
@onready var highlight_map: GridOverlay = %OverlayLayer
|
@onready var highlight_map: GridOverlay = %OverlayLayer
|
||||||
@@ -82,7 +80,6 @@ func remove_unit(unit: Unit) -> void:
|
|||||||
|
|
||||||
func target_tile(coords: Vector2i) -> void:
|
func target_tile(coords: Vector2i) -> void:
|
||||||
highlight_map.target_tile(coords)
|
highlight_map.target_tile(coords)
|
||||||
tile_hovered.emit(coords)
|
|
||||||
|
|
||||||
|
|
||||||
func set_highlight_enabled(enabled: bool) -> void:
|
func set_highlight_enabled(enabled: bool) -> void:
|
||||||
|
|||||||
@@ -5,12 +5,23 @@ const SPEED = 192.0
|
|||||||
@export var dl_map: CombatMap
|
@export var dl_map: CombatMap
|
||||||
|
|
||||||
signal combat_requested(attacker: Unit, defender: Unit)
|
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 _selected_unit: Unit = null
|
||||||
var _target_pos: Vector2
|
var _target_pos: Vector2
|
||||||
var _goal_pos: Vector2
|
var _goal_pos: Vector2
|
||||||
var _moving := false
|
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:
|
func _ready() -> void:
|
||||||
for unit: Unit in get_tree().get_nodes_in_group("units"):
|
for unit: Unit in get_tree().get_nodes_in_group("units"):
|
||||||
@@ -30,23 +41,55 @@ func _on_unit_died(unit: Unit) -> void:
|
|||||||
_moving = false
|
_moving = false
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if event is InputEventMouseButton and not event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if input_disabled:
|
||||||
var world_pos: Vector2 = get_viewport().get_canvas_transform().affine_inverse() * event.position
|
return
|
||||||
var clicked_unit := _get_unit_at(world_pos)
|
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():
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
combat_requested.emit(_selected_unit, clicked_unit)
|
if input_disabled:
|
||||||
else:
|
return
|
||||||
_select_unit(clicked_unit)
|
|
||||||
get_viewport().set_input_as_handled()
|
if event is InputEventMouseButton:
|
||||||
elif _selected_unit:
|
match event.button_index:
|
||||||
var snapped_pos := dl_map.snap_to_grid(world_pos)
|
MOUSE_BUTTON_LEFT:
|
||||||
var grid_coords := dl_map.world_to_coords(world_pos)
|
if event.pressed:
|
||||||
if dl_map.is_wall(grid_coords):
|
_left_pending = true
|
||||||
return
|
_drag_start = event.position
|
||||||
_goal_pos = snapped_pos
|
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()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +126,25 @@ func _physics_process(delta: float) -> void:
|
|||||||
_moving = true
|
_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:
|
func _select_unit(unit: Unit) -> void:
|
||||||
if _selected_unit:
|
if _selected_unit:
|
||||||
_selected_unit.set_selected(false)
|
_selected_unit.set_selected(false)
|
||||||
|
|||||||
@@ -4,20 +4,36 @@ class_name StrategyPhase extends Node2D
|
|||||||
@onready var combat_system: CombatSystem = $CombatSystem
|
@onready var combat_system: CombatSystem = $CombatSystem
|
||||||
@onready var combat_ui: CombatUI = $CombatUI
|
@onready var combat_ui: CombatUI = $CombatUI
|
||||||
@onready var combat_map: CombatMap = $CombatMap
|
@onready var combat_map: CombatMap = $CombatMap
|
||||||
|
@onready var camera: CameraController = $Camera2D
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
player_controller.combat_requested.connect(_on_combat_requested)
|
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_confirmed.connect(_on_fight_confirmed)
|
||||||
combat_ui.fight_cancelled.connect(_on_fight_cancelled)
|
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:
|
func _on_combat_requested(attacker: Unit, defender: Unit) -> void:
|
||||||
var proposal := combat_system.create_proposal(attacker, defender)
|
var proposal := combat_system.create_proposal(attacker, defender)
|
||||||
combat_map.set_highlight_enabled(false)
|
_set_input_disabled(true)
|
||||||
combat_ui.show_proposal(proposal)
|
combat_ui.show_proposal(proposal)
|
||||||
|
|
||||||
|
|
||||||
func _on_fight_confirmed(proposal: CombatProposal) -> void:
|
func _on_fight_confirmed(proposal: CombatProposal) -> void:
|
||||||
combat_system.apply_proposal(proposal)
|
combat_system.apply_proposal(proposal)
|
||||||
combat_map.set_highlight_enabled(true)
|
_set_input_disabled(false)
|
||||||
|
|
||||||
|
|
||||||
func _on_fight_cancelled() -> void:
|
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)
|
||||||
|
|||||||
@@ -1,42 +1,5 @@
|
|||||||
class_name CameraController extends Camera2D
|
class_name CameraController extends Camera2D
|
||||||
|
|
||||||
const DRAG_THRESHOLD := 8.0
|
|
||||||
|
|
||||||
var _dragging := false
|
func apply_drag(delta: Vector2) -> void:
|
||||||
var _left_pending := false
|
position += delta / zoom
|
||||||
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()
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
extends ColorRect
|
extends ColorRect
|
||||||
|
|
||||||
signal tile_hovered(coords: Vector2i)
|
|
||||||
|
|
||||||
@export var tile_size: float = 48.0
|
@export var tile_size: float = 48.0
|
||||||
|
|
||||||
var _time: float = 0.0
|
var _time: float = 0.0
|
||||||
var _previous_coords := Vector2i(INF, INF)
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -17,13 +14,10 @@ func _ready() -> void:
|
|||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
_time += delta
|
_time += delta
|
||||||
color.a = 0.25 + 0.1 * sin(_time * 4.0)
|
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
|
func set_grid_coords(coords: Vector2i) -> void:
|
||||||
var coords := Vector2i(snapped_pos / tile_size)
|
global_position = Vector2(coords) * tile_size
|
||||||
if coords != _previous_coords:
|
|
||||||
_previous_coords = coords
|
|
||||||
tile_hovered.emit(coords)
|
|
||||||
|
|
||||||
|
|
||||||
func _notification(what: int) -> void:
|
func _notification(what: int) -> void:
|
||||||
@@ -31,7 +25,3 @@ func _notification(what: int) -> void:
|
|||||||
hide()
|
hide()
|
||||||
elif what == NOTIFICATION_WM_MOUSE_ENTER:
|
elif what == NOTIFICATION_WM_MOUSE_ENTER:
|
||||||
show()
|
show()
|
||||||
|
|
||||||
|
|
||||||
func _snap_to_grid(pos: Vector2) -> Vector2:
|
|
||||||
return Vector2(floorf(pos.x / tile_size), floorf(pos.y / tile_size)) * tile_size
|
|
||||||
|
|||||||
Reference in New Issue
Block a user