# NEO-7 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-7 |
| **Title** | E1.M1: Submit MoveCommand → server updates PositionState |
| **Linear** | [NEO-7](https://linear.app/neon-sprawl/issue/NEO-7) |
| **Parent context** | [E1.M1 InputAndMovementRuntime](https://linear.app/neon-sprawl/project/e1m1-inputandmovementruntime-20eb28dd3db4) |

## 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 (NEO-6 left `sequence` at `0`; this story advances semantics).
- **Client:** Build on [NEO-5](NEO-5-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 Linear)**

- Prediction / reconciliation, PostgreSQL, speed anti-cheat.

**Dependencies**

- [NEO-6](NEO-6-implementation-plan.md): in-memory `PositionState` **read** API and store.
- [NEO-5](NEO-5-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 NEO-6 `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 NEO-5 `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, bool)`** (NEO-11: second arg = boot snap vs nav goal) so **`main`** snaps on boot and path-follows after move verify.  
   - **`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 NEO-5 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 NEO-7 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–NEO-6). |
| `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` | NEO-7 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 NEO-7).

## Pull request description (Linear 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 NEO-6; **`sequence`** increments after each successful move)

- Same as NEO-6 **`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.
