11 KiB
11 KiB
NEO-22 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-22 |
| Title | Retire prototype click-to-move; ship WASD locomotion on the client |
| Linear | NEO-22 |
| Parent | E1.M1 InputAndMovementRuntime (or current movement project) |
| Supersedes / deprecates (prototype) | NEO-5, NEO-11 click-to-move surface; NEO-14 seam/jitter mitigations tied to nav + goal steering |
Goal, scope, and out-of-scope
Goal: Replace mouse click → ground pick → MoveCommand → nav path → player.gd goal steering with keyboard WASD (and optional gamepad later) as the primary locomotion input in the Godot prototype. Movement should feel predictable on stepped geometry without the column / nav-ribbon / slide-normal fight that click-to-move accumulated.
In scope
- Input:
project.godotactions (or code) for W/A/S/D; read them in a thin place (main.gdor a smalllocomotion_input.gd) and drive the player’s horizontal intent each physics tick. - Locomotion model:
CharacterBody3D+move_and_slide()with velocity derived from wish direction × speed, camera-relative or world-relative (decide in Decisions); noNavigationAgent3Dpath goal for routine walking unless a later slice reintroduces hybrid “click to mark + auto-run” explicitly. - Remove / disable left-click ground pick as the default move trigger (
ground_pick.gd→main.gd→PositionAuthorityClient.submit_move_target). Either delete the flow for this slice or gate it behind a dev flag documented as off by default. - Server / authority: Option C — server accepts a stream of small validated moves (see Decisions); document wire shape and cadence in
client/README.md+server/README.md+ decomposition. NEO-7POST …/movemay remain for click/debug targets or be narrowed once the stream path owns routine locomotion (decide before merge). - Cleanup: Delete or drastically narrow NEO-11 / NEO-14 / NEO-16 workaround blocks in
player.gd(column seam damp, nav column steering, idle trace scaffolding tied to click goals) only after WASD path is stable; prefer one commit that switches input then a follow-up that deletes dead code. - Docs: Update
client/README.md,docs/decomposition/modules/E1_M1_InputAndMovementRuntime.md, and this plan’s checklist when shipped.
Out of scope
- Full client prediction + server reconciliation for FPS-grade netcode (unless explicitly pulled into this story by product).
- Click-to-interact changes (NEO-12-style interaction) except where click conflicts with removed move binding (document if unchanged).
- Occlusion / camera feature work (NEO-17/20) beyond what’s needed so WASD direction feels correct with the follow camera.
- Production navmesh tooling for NPCs — only player move input changes here.
Acceptance criteria checklist
- WASD moves the avatar on the default
main.tscnfloor with no left-click requirement; speed and direction are stable on flat ground and on stepped QA props without the prior ±MOVE_SPEEDseam oscillation at column/stair corners (subjective + optionaldebug_idle_tracespot-check). - Left-click no longer starts a walk by default (or is clearly dev-only and off in normal play, documented).
- Authoritative position still converges with the server via the stream / small-step contract (option C); README + tests describe batching or per-tick cadence so CI + manual steps do not lie.
- Cold boot
snap_to_serverstill works; no spawn underground / stale goal regressions. - NEO-10 rejection UX: if clicks are removed, another path still exercises reject payloads in dev (automated test or documented console-only command) or clicks remain only for “set goal” debug, not default locomotion.
- GdUnit (or existing harness) covers at least one pure helper for wish-dir / speed clamp if logic is non-trivial.
Technical approach
- Linear + branch: 2026-04-17 kickoff — Linear In Progress; story branch
NEO-22-retire-click-to-move-wasd-locomotion(issue id first per linear-git-naming). - Authority model: Option C — introduce a continuous-locomotion server path (small validated steps or batched steps per HTTP call in this slice; WebSocket / tick pipe deferred unless explicitly pulled in). Reuse or mirror NEO-10-style limits (max step, ΔY) where applicable; assign sequence / ordering rules so out-of-order repeats are safe. NEO-7
POST …/movestays available for debug or one-shot targets until stream-only is intentional. Document the contract inclient/README.mdandserver/README.mdbefore merge. - Input layer: Map WASD to
Vector2in screen/camera space → world XZ tangent toCamera3Dbasis (same pattern as many third-person controllers); normalize; zero when no keys. player.gdrefactor: Replace_has_walk_goal/_auth_walk_goalsteering path with wish velocity from parent or injected each_physics_process; keep gravity, floor snap policy, andsnap_to_serverentry points. StripNavigationAgent3Ddependency for default walk if decision says so (agent node can stay in scene unused until a later hybrid story).- Remove click wiring: Disconnect
target_chosen→submit_move_targetinmain.gdfor default build; optionally keepground_pick.gdfor debug “teleport goal” behindOS.is_debug_build()flag. - Tests + manual: Run README manual path; add unit test for direction math if extracted.
Decisions
| Topic | Choice | Rationale |
|---|---|---|
| Issue id | NEO-22 | Created in Linear 2026-04-16; plan filename matches this key. |
| Kickoff | 2026-04-17 | Branch NEO-22-retire-click-to-move-wasd-locomotion; status In Progress in Linear. |
| Camera vs world steering | Camera-relative XZ | Implemented in main.gd via _camera basis and Input.get_vector on move_* actions. |
| NavAgent | Default off for routine WASD in this slice | Removes nav-ribbon + column latch class of bugs; re-add only with explicit hybrid design. |
| Authority (WASD → server) | Option C — stream of small moves | Aligns wire semantics with continuous input; leaves room for batched HTTP now and a tick/WebSocket transport later without rebranding “every WASD sample as a click destination.” |
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/PositionState/MoveStreamRequest.cs (name at implementer discretion) |
Versioned JSON body for one or more small authoritative steps (or equivalent “locomotion frame”) applied in order; pairs with new MapPost route. |
server/NeonSprawl.Server.Tests/Game/PositionState/MoveStreamApiTests.cs (matching test file) |
Happy path, rejection, unknown player, and ordering/sequence behavior for the new endpoint. |
client/scripts/locomotion_input.gd (optional) |
Thin WASD → wish vector if main.gd would otherwise grow; omit if logic stays trivially inline. |
Files to modify
| Path | Rationale |
|---|---|
client/project.godot |
Declare move_forward / move_back / move_left / move_right (or reuse Godot defaults) bound to WASD. |
client/scripts/main.gd |
Stop wiring click pick to submit_move_target by default; optionally forward WASD intent to player/authority per chosen model. |
client/scripts/ground_pick.gd |
Disable default _input pick or gate behind export/debug; update header comments (NS-16 lineage). |
client/scripts/player.gd |
Replace goal/nav horizontal steering with WASD-driven velocity; delete obsolete seam/nav state once stable. |
client/scripts/position_authority_client.gd |
Add stream submit path (batch or frequent small-step POSTs), backoff/coalesce while in flight, and alignment with server sequence after locomotion frames. |
server/NeonSprawl.Server/Game/PositionState/PositionStateApi.cs |
Register option C route; delegate to store / validation (may loop TryApplyMoveTarget or new store helper for multi-step). |
server/NeonSprawl.Server/Game/PositionState/MoveCommandValidation.cs (and/or new validator) |
Reuse or factor shared rules so stream steps and legacy MoveCommand stay consistent where intended. |
server/README.md |
Document new movement stream endpoint, schema version, and relationship to NEO-7 POST …/move. |
client/scenes/main.tscn |
Only if NavigationAgent3D or nodes are removed/changed or input singleton paths change. |
client/README.md |
Replace “click-move” manual checks with WASD + server steps; note deprecation of click-to-move prototype. |
docs/decomposition/modules/E1_M1_InputAndMovementRuntime.md |
Snapshot: click-to-move retired in favor of WASD for prototype; link this plan. |
docs/plans/NEO-11-implementation-plan.md |
Add one-line “superseded for locomotion input by NEO-22” in header or Resolution if policy is to annotate predecessors. |
Tests
| File | Coverage |
|---|---|
client/test/player_test.gd |
Change: remove or replace tests that assumed _has_walk_goal / nav steering; add tests for any extracted wish-direction or speed clamp helper. |
client/test/position_authority_client_test.gd |
Change: align with stream submit cadence, pending queue, and sequence handling. |
server/NeonSprawl.Server.Tests/Game/PositionState/MoveCommandApiTests.cs |
Change only if legacy POST …/move behavior or shared validation changes; otherwise leave as regression guard for one-shot moves. |
| Manual (required) | README path: boot with server, WASD on flat + stepped geometry, snap_to_server, and whichever NEO-10 rejection path remains after click removal. |
Open questions / risks
- Wire shape for C: Single-step POST at high cadence vs batched array per POST vs early WebSocket — pick for NEO-22 to control server load and client complexity; document in README.
- Legacy
MoveCommand: Whether NEO-7 remains for debug/teleport only or is unified with stream handler — avoid two conflicting sources of truth without documenting precedence. - Interaction vs pick: Confirm E / interaction does not rely on the same mouse button in a way that breaks when pick is disabled.
- Gamepad: Out of scope unless trivial (
Input.get_vector); note for follow-up story. - Jira naming: If the team uses NEON-* keys elsewhere, align Linear/Git subject lines per linear-git-naming.
Client follow-up (done)
- Click-to-move removed: Deleted
client/scripts/ground_pick.gdandclient/test/ground_pick_test.gd, removedGroundPickfromclient/scenes/main.tscn, droppedPositionAuthorityClient.submit_move_targetandPOST_MOVE/VERIFY_GETphases.main.gdno longer wires pick → move. BootGETstill emitsauthoritative_position_received(..., true)forsnap_to_server; theapply_as_snap == falsebranch is unused until a future server-driven nav goal exists.