neon-sprawl/docs/plans/NEO-11-implementation-plan.md

127 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NEO-11 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-11 |
| **Title** | E1.M1: Client path-follow baseline (navigation + visible locomotion) |
| **Linear** | [NEO-11](https://linear.app/neon-sprawl/issue/NEO-11) |
| **Parent context** | [E1.M1 InputAndMovementRuntime](https://linear.app/neon-sprawl/project/e1m1-inputandmovementruntime-20eb28dd3db4) · [Epic 1](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) · Slice 1 — Movement and position sync |
| **Decomposition** | [E1.M1 — InputAndMovementRuntime](../decomposition/modules/E1_M1_InputAndMovementRuntime.md) |
**Delivery:** Ship **`docs/plans/NEO-11-implementation-plan.md`** on the **same branch / PR** as the NEO-11 implementation (plan + code together), matching the NEO-10 pattern.
## Goal, scope, and out-of-scope
**Goal:** Deliver **visible** click-to-move: the avatar **moves** toward the verified server target using **`NavigationAgent3D`** + a **baked mesh** where applicable, instead of teleporting on the verifying **`GET`**. Aligns with E1.M1: path-follow baseline under server authority.
**In scope**
- **Godot:** `NavigationRegion3D` (baked mesh) over the prototype walkable floor in `main.tscn` (obstacle geometry included in bake for future use); **`NavigationAgent3D`** on the player driving **`CharacterBody3D`** motion (`move_and_slide` toward next path position when the client is following the mesh — Godot 4.x pattern).
- **Pick integration:** Reuse **`ground_pick.gd`** (`walkable` group, upward normal); clicked point becomes **`NavigationAgent3D.target_position`** (after any needed Y / agent height adjustment consistent with current capsule).
- **Server authority (unchanged contract):** One **`MoveCommand`** per successful pick with the **same world target** as today ([NEO-7](NEO-7-implementation-plan.md)); server **v1 snap** + **[NEO-10](NEO-10-implementation-plan.md)** validation unchanged.
**Out of scope (per Linear)**
- Full prediction/reconciliation, MMO-grade netcode, server-side nav mesh validation.
- Replacing **`InteractionRequest`** / range rules.
## Locked — client nav vs server authority (this slice)
| Topic | Decision |
|--------|----------|
| **When to POST** | **Once per click**, immediately with the **picked destination** (same JSON as today). No waypoint streaming to the server in this story. |
| **Server truth** | Still **snap to target** on **200** after validation; **`GET /position`** after POST remains the reconciliation read (existing `PositionAuthorityClient` flow). |
| **Straight-line vs path** | NEO-10 validates **displacement** from **current authoritative position** to **target** (horizontal + vertical limits). It does **not** check path clearance through geometry. **Client** may follow nav waypoints when the descend bypass is inactive; **automatic obstacle detours are not required** for this slice (see **Locked — prototype movement tradeoff**). |
| **After successful move** | **Do not** teleport the avatar on the verifying **`GET`** when the player is expected to walk: apply the authoritative position as a **navigation goal** and let the agent complete the path; **initial boot** `sync_from_server` remains a **hard snap** so spawn matches the server. |
| **POST 400** | Existing behavior: emit **`move_rejected`**, show NEO-10 UX; **do not** start (or clear) a nav path to that target. |
| **Distance limits** | Default **`MaxHorizontalStep`** (e.g. 18 m) remains compatible with **single-command** long clicks across the prototype; if a future scene needs shorter steps, chain commands or tune limits in a later story. |
## Locked — prototype movement tradeoff
| Topic | Decision |
|--------|----------|
| **Uneven surfaces vs obstacles** | The client uses a **descend bypass** in `player.gd`: when the authoritative goal **Y** is below the body origin (typical for floor picks vs mid-capsule), steering goes **straight in xz** toward the goal instead of following nav waypoints first. This keeps **stepped bumps** smooth; it does **not** guarantee routing around tall **static obstacles** on a **single** click. |
| **Player expectation** | **Multi-click** (or chained moves) is acceptable to navigate around geometry the client would otherwise bee-line into. **No** automatic obstacle navigation is required for NEO-11 closure. |
| **Residual polish** | Visible idle jitter at rest is tracked as tech debt — [NEO-14](https://linear.app/neon-sprawl/issue/NEO-14) (not a blocker for this story). |
## Acceptance criteria checklist
- [x] Clicking a valid floor target produces **continuous motion** toward the verified target without sliding through geometry in the common case; **navmesh waypoints** are used when the descend bypass does not apply. **Obstacle detours on one click are not required** (tradeoff above).
- [x] Authoritative position still **converges** with the server (`MoveCommand` + `GET`); no regression on NEO-10 rejection UX (`reasonCode` / label timeout).
- [x] Short note in **`server/README.md`** and/or **client script header**: client nav vs server authority for this slice.
## Technical approach
1. **`main.tscn` / navigation**
- Add **`NavigationRegion3D`** covering walkable floor; include obstacle **`StaticBody3D`** geometry in baking as required by Godot 4 so paths wrap obstacles.
- Bake after layout changes; document any **editor step** in `client/README.md` if bake is not fully reproducible from scene alone.
2. **Player / agent**
- Attach **`NavigationAgent3D`**; replace NEO-5-style **direct steering toward `_goal`** with **path following** (velocity toward `get_next_path_position()` or equivalent, `move_and_slide`).
- Keep **`snap_to_server`** for **boot** and any **hard reconcile** case you explicitly document (default: boot only).
3. **Wiring (`main.gd` / `PositionAuthorityClient`)**
- On **`target_chosen`**: call **`submit_move_target`** as today (POST → GET).
- Distinguish **authoritative position from boot** vs **after successful move** so **`main.gd`** snaps on boot but sets **nav target** (no snap) after verify when implementing path-follow.
- Optional small API: second signal, enum on existing signal, or `sync_from_server`-only snap path — pick the smallest change that stays readable per [godot-client-script-organization](../../.cursor/rules/godot-client-script-organization.md).
4. **Documentation**
- **`client/README.md`:** Movement section: nav agent + server authority, and the **uneven-surface vs obstacle** tradeoff (multi-click around obstacles).
- **`server/README.md`:** Server validates **end target** and **step**; **client** nav is **presentational** only (no server navmesh); obstacle detours not guaranteed.
## Resolved — agent shape vs capsule (was risk)
Prototype player collider in [`main.tscn`](../../client/scenes/main.tscn): **`CapsuleShape3D_player`** — **`radius = 0.4`**, **`height = 1.0`**.
| Setting | Locked choice |
|--------|----------------|
| **`NavigationAgent3D.radius` / `height`** | Match the **physics** capsule (**0.4** / **1.0**) so nav clearance matches what `move_and_slide` actually sweeps. |
| **Navmesh bake agent** | In **`NavigationRegion3D`** bake settings, use the **same** radius/height (or Godots equivalent **agent** parameters) so baked polygons match runtime pathfinding. |
| **Corner clipping vs “stuck”** | If the mesh clips into box corners, **increase** agent radius slightly and **rebake** (wider corridor on the mesh) before shrinking physics. Do not silently shrink the capsule without an explicit design change. |
| **Vertical alignment** | Keep **`path_height_offset`** (or equivalent) at **0** unless the capsule foot position and floor snap misalign; the pick already supplies floor **`Y`**. |
| **Arrival** | Align **`target_desired_distance`** (and related agent tolerances) with existing player **`ARRIVE_EPS`** (~**0.35** m) so stop distance matches prior steering feel unless QA says otherwise. |
## Resolved — navigation sync / frame order (was risk)
| Topic | Locked choice |
|--------|----------------|
| **Where motion runs** | Path queries (`get_next_path_position`, velocity toward next point) and **`move_and_slide`** run only in **`Player._physics_process`**, not in `_process` or UI thread. |
| **When `target_position` is set** | `main.gd` (or signal handler) may set the agent target in response to **`target_chosen`** / authority callbacks; the **first** consumable segment appears after the navigation map updates. |
| **First frame after new target** | If **`NavigationAgent3D.is_navigation_finished()`** or a zero-length first step appears on the **same** frame as `target_position` assignment, **defer** one tick: `await get_tree().physics_frame` **once** after setting target **or** simply rely on **next** `_physics_process` (preferred — avoid await in hot path). **Do not** poll path from `_input` without deferral. |
| **Map readiness** | After adding **`NavigationRegion3D`**, ensure the region is **baked** and enabled before F5; startup **`sync_from_server`** snap does not need a path. |
## Files to add (expected)
| Path | Purpose |
|------|---------|
| None required by name | Prefer extending existing `player.gd` / scene unless a dedicated `player_navigation.gd` reduces `main.gd` bloat. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `client/scenes/main.tscn` | `NavigationRegion3D`, bake source meshes, agent node setup. |
| `client/scripts/player.gd` | Path-follow via `NavigationAgent3D`; clarify header (NEO-5 → NEO-11 evolution). |
| `client/scripts/main.gd` | Boot snap vs post-move nav goal wiring. |
| `client/scripts/position_authority_client.gd` | If needed: signal shape or phase so main can snap only on boot. |
| `client/README.md` | Manual check steps for path-follow + server. |
| `server/README.md` | Client nav vs authority note (AC). |
| `docs/decomposition/modules/E1_M1_InputAndMovementRuntime.md` | Implementation snapshot when NEO-11 ships. |
## Tests
| Action | What to cover |
|--------|----------------|
| **Manual (required)** | Server running; click-move toward targets (including stepped bumps); NEO-10 reject targets — same rejection behavior as before; cold start — player matches server position. Optional: confirm multi-click can route around the prototype **`Obstacle`** when a single click bee-lines. |
| **Automated** | None required for this story unless an existing headless hook can assert nav baking (unlikely); prefer manual for Godot nav. |
## Open questions / risks
**Prototype tradeoff** is explicit under **Locked — prototype movement tradeoff** (bumps vs single-click obstacle routing). Agent/bake alignment and physics-frame path consumption remain locked in **Resolved — agent shape vs capsule** and **Resolved — navigation sync / frame order**. Reopen if scene scale or avatar collider changes materially.
## PR / review
Cross-check [E1.M1](../decomposition/modules/E1_M1_InputAndMovementRuntime.md), [client README](../../client/README.md), and [server README](../../server/README.md). Confirm NEO-10 props in `main.tscn` still behave after nav region changes.