# 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 command’s 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 (post–NS-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` — `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.