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

128 lines
8.9 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-16 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NS-16 |
| **Title** | E1.M1: Submit MoveCommand → server updates PositionState |
| **Jira** | [NS-16](https://neon-sprawl.atlassian.net/browse/NS-16) |
| **Parent context** | [NS-10 — E1.M1 InputAndMovementRuntime](https://neon-sprawl.atlassian.net/browse/NS-10) |
## Goal, scope, and out-of-scope
**Goal:** End-to-end **intent → authority**: the client sends a move destination; the server updates in-memory state; the client can read back authoritative `PositionState`.
**In scope**
- **Server:** HTTP endpoint accepting a **MoveCommand** JSON payload: at minimum **target position** + **player id** (route and/or body; dev session using configured `Game:DevPlayerId` is sufficient).
- **Server rule (document in plan + XML on types):** apply movement with a single clear rule. **Planned v1 rule:** **snap** authoritative position to the commands target immediately (no fixed-step simulation). Increment **`sequence`** on each successful apply so consumers can detect updates (NS-15 left `sequence` at `0`; this story advances semantics).
- **Client:** Build on [NS-14](NS-14-implementation-plan.md) click-to-move: add a minimal **HTTP** client to **submit** the command and **confirm** state via **`GET`** (poll or follow-up GET after POST — both satisfy “read back”).
- **Wire format:** JSON for the prototype (explicit throwaway per [`docs/decomposition/modules/contracts.md`](../decomposition/modules/contracts.md) until protobuf).
**Out of scope (per Jira)**
- Prediction / reconciliation, PostgreSQL, speed anti-cheat.
**Dependencies**
- [NS-15](NS-15-implementation-plan.md): in-memory `PositionState` **read** API and store.
- [NS-14](NS-14-implementation-plan.md): click-to-move scaffold.
## Acceptance criteria checklist
- [ ] After sending a command, subsequent **`GET /game/players/{id}/position`** reflects the new authoritative position.
- [ ] Happy path demo: click in Godot → HTTP request → server → **displayed** avatar position matches server (snapping acceptable; no smooth interpolation required).
- [ ] Request/response shapes documented as **v1 `MoveCommand`** and **`PositionState`** (XML on DTOs + `server/README.md` / PR blurb; **reuse** existing `PositionStateResponse` shape for successful command responses where practical so one authoritative snapshot type stays consistent).
## Technical approach
1. **MoveCommand (v1 JSON)**
- New request DTO (e.g. `MoveCommandRequest`) with top-level **`schemaVersion`** (`1`) and **`target`**: object with **`x`**, **`y`**, **`z`** (same numeric style as `PositionVector`).
- **Endpoint:** `POST /game/players/{id}/move` with JSON body; `{id}` identifies the player (trimmed, case-insensitive match to store, consistent with NS-15 `GET`).
- **Responses:** **200** + body same shape as **`PositionStateResponse`** after apply; **404** if player unknown; **400** if `schemaVersion` ≠ 1 or payload invalid / missing `target`.
2. **Store mutation**
- Extend **`IPositionStateStore`** with an apply method (e.g. `TryApplyMoveTarget`) that updates position and increments **`sequence`**, using **`ConcurrentDictionary`**-safe update (e.g. `TryUpdate` loop) consistent with `InMemoryPositionStateStore`.
3. **Routing**
- Add the new route inside existing **`PositionStateApi.MapPositionStateApi()`** (or equivalent dedicated class); keep **`Program.cs`** limited to extension calls only.
4. **Tests** (`NeonSprawl.Server.Tests`)
- Integration: `POST` move for dev player → **200**, body position matches target and **`sequence`** increased; then **`GET`** matches.
- Unknown player → **404**; bad `schemaVersion`**400**.
5. **Godot client****Split scripts by concern** (required repo policy: [godot-client-script-organization.md](../../.cursor/rules/godot-client-script-organization.md)); do **not** grow a monolithic `main.gd` with pick + HTTP + wiring.
- **`ground_pick.gd`** on a new **`Node3D`** child (e.g. `GroundPick`): owns walkable **ray pick** (logic moved out of NS-14 `main.gd`). Exposes **`target_chosen(Vector3)`** when the user clicks valid ground. **`main.gd`** sets **`fallback_camera`** (or equivalent) in `_ready`.
- **`position_authority_client.gd`** on a new **`Node`** child: `@export` **base URL** (default `http://127.0.0.1:5253`, match `launchSettings.json`) and **dev player id** (`dev-local-1`). Creates **`HTTPRequest`** in **`_ready`** via **`add_child`** (not as a node hand-placed in `main.tscn`). Flow: **`POST`** `MoveCommand`, then **`GET`** position (tests must show **GET** reflects apply). Emits **`authoritative_position_received(Vector3)`** so **`main`** only snaps the player.
- **`main.gd`:** Thin composer only—connect signals, call **`sync_from_server()`** on run, delegate pick and HTTP to the two child scripts.
- Align **client README** with NS-14 note: server authority path replaces pure local steering for this demo.
6. **Documentation**
- **`server/README.md`:** new subsection for move submission: method, path, sample **`curl`**, v1 snap rule, link to DTO XML.
- **`client/README.md`:** how to run server + client for NS-16 demo, exported vars, expectation of snap-to-server.
## Files to add
| Path | Purpose |
|------|--------|
| `server/NeonSprawl.Server/Game/PositionState/MoveCommandRequest.cs` | Versioned JSON request DTO + XML contract blurb for v1 `MoveCommand`. |
| `client/scripts/ground_pick.gd` | Walkable ground ray pick + **`target_chosen`** signal; **moved out of `main.gd`** per client script organization policy. |
| `client/scripts/position_authority_client.gd` | HTTP **`POST`** move + **`GET`** position; **`HTTPRequest`** created in **`_ready`** (`add_child`); config exports; signal when authoritative position is known. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/PositionState/IPositionStateStore.cs` | Declare authoritative apply API. |
| `server/NeonSprawl.Server/Game/PositionState/InMemoryPositionStateStore.cs` | Implement snap + sequence increment. |
| `server/NeonSprawl.Server/Game/PositionState/PositionStateApi.cs` | Map `POST /game/players/{id}/move`. |
| `server/NeonSprawl.Server/Game/PositionState/PositionStateResponse.cs` | XML: clarify **`sequence`** increments after moves (postNS-15). |
| `server/NeonSprawl.Server.Tests/Game/PositionState/MoveCommandApiTests.cs` | `POST` + `GET` integration and error cases. |
| `server/NeonSprawl.Server.Tests/Game/PositionState/PositionStateApiTests.cs` | Existing `GET` integration tests (same feature folder). |
| `client/scripts/main.gd` | **Thin composer:** add/wire `GroundPick` + `PositionAuthorityClient`, connect signals, snap player to server (remove inlined pick + HTTP). |
| `client/scenes/main.tscn` | Add child nodes that attach **`ground_pick.gd`** and **`position_authority_client.gd`** only. **Do not** add **`HTTPRequest`** to the scene—the authority script owns and instantiates it. |
| `client/scripts/player.gd` | Only if needed (e.g. **`velocity = ZERO`** on snap, or helper **`snap_to(world_pos)`**); prefer minimal changes. |
| `server/README.md` | Move command + curl + v1 behavior. |
| `client/README.md` | NS-16 runbook and authority note. |
## Tests
- **Automated:** xUnit integration tests via `WebApplicationFactory<Program>``POST` then `GET` equality, `404` / `400` as above.
- **Manual:** Godot F5 with server running; click floor; avatar snaps to server-reported position.
- **No new client automated test harness** in scope unless one already exists (none planned for NS-16).
## Pull request description (Jira AC) — draft for merge
Paste when opening the PR (adjust if shapes differ slightly in implementation).
**MoveCommand v1 — request**
- **Endpoint:** `POST /game/players/{id}/move`
- **Body (example):**
```json
{
"schemaVersion": 1,
"target": { "x": 1.5, "y": 0, "z": -2.0 }
}
```
| JSON property | Type (v1) | Meaning |
|---------------|-----------|---------|
| `schemaVersion` | integer | Must be `1` for v1. |
| `target` | object | Destination world position. |
| `target.x` / `y` / `z` | number | Coordinates (snap applied server-side). |
**PositionState v1 — response** (unchanged from NS-15; **`sequence`** increments after each successful move)
- Same as NS-15 **`GET`** response body; successful **`POST`** returns the updated snapshot.
**Server rule:** position is set to **`target`** immediately (**snap**); **`sequence`** increases by one per successful apply.
## Open questions / risks
- **Concurrent clicks from the client:** minimal approach is last-write-wins on the server; optional client-side “in flight” flag to reduce spam.
- **Axis / height:** server stores full **`y`** from the command; client should send the same **`y`** as the ray hit (or document if server flattens — plan is **no flatten** unless playtest demands).
- **WebSocket:** explicitly deferred; HTTP only for this story.