127 lines
11 KiB
Markdown
127 lines
11 KiB
Markdown
# 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 Godot’s 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.
|