neon-sprawl/docs/plans/NS-23-implementation-plan.md

119 lines
9.5 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.

# NS-23 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NS-23 |
| **Title** | E1.M1: Client path-follow baseline (navigation + visible locomotion) |
| **Jira** | [NS-23](https://neon-sprawl.atlassian.net/browse/NS-23) |
| **Parent context** | [NS-10 — E1.M1 InputAndMovementRuntime](https://neon-sprawl.atlassian.net/browse/NS-10) · [NS-1 — Epic 1](https://neon-sprawl.atlassian.net/browse/NS-1) · Slice 1 — Movement and position sync |
| **Decomposition** | [E1.M1 — InputAndMovementRuntime](../decomposition/modules/E1_M1_InputAndMovementRuntime.md) |
**Delivery:** Ship **`docs/plans/NS-23-implementation-plan.md`** on the **same branch / PR** as the NS-23 implementation (plan + code together), matching the NS-19 pattern.
## Goal, scope, and out-of-scope
**Goal:** Deliver **visible** click-to-move: the avatar **follows a path** toward the clicked point using a **navigation mesh** baseline (obstacle-aware), not only steering at a goal or snapping on server ack alone. Aligns with E1.M1: path-follow baseline under server authority.
**In scope**
- **Godot:** `NavigationRegion3D` (baked mesh) over the prototype walkable floor in `main.tscn`, respecting existing static obstacles; **`NavigationAgent3D`** (on player or child node) driving **`CharacterBody3D`** motion (`move_and_slide` toward next path position — follow Godot 4.x recommended 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 ([NS-16](NS-16-implementation-plan.md)); server **v1 snap** + **[NS-19](NS-19-implementation-plan.md)** validation unchanged.
**Out of scope (per Jira)**
- 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** | NS-19 validates **displacement** from **current authoritative position** to **target** (horizontal + vertical limits). It does **not** check path clearance through geometry; **client nav** is responsible for **visible** obstacle avoidance. |
| **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 NS-19 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. |
## Acceptance criteria checklist
- [x] Clicking a valid floor target produces **continuous motion** along a plausible path around static obstacles in the prototype scene (not sliding through walls).
- [x] Authoritative position still **converges** with the server (`MoveCommand` + `GET`); no regression on NS-19 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 NS-14-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`:** Update movement section: navmesh path-follow + server authority one-liner.
- **`server/README.md`:** One short subsection or bullet: server validates **end target** and **step**; **client** nav is **presentational** for obstacle following in this slice.
## 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 (NS-14 → NS-23 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 NS-23 ships. |
## Tests
| Action | What to cover |
|--------|----------------|
| **Manual (required)** | Server running; click around obstacles — path goes around; click NS-19 reject targets — same rejection behavior as before; cold start — player matches server position. |
| **Automated** | None required for this story unless an existing headless hook can assert nav baking (unlikely); prefer manual for Godot nav. |
## Open questions / risks
None — agent/bake alignment and physics-frame path consumption are locked in **Resolved — agent shape vs capsule** and **Resolved — navigation sync / frame order** above. Reopen only if the 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 NS-19 props in `main.tscn` still behave after nav region changes.