128 lines
9.0 KiB
Markdown
128 lines
9.0 KiB
Markdown
# NEON-4 — Implementation plan
|
||
|
||
## Story reference
|
||
|
||
| Field | Value |
|
||
|--------|--------|
|
||
| **Key** | NEON-4 |
|
||
| **Title** | E1.M1: Submit MoveCommand → server updates PositionState |
|
||
| **Jira** | [NEON-4](https://neon-sprawl.atlassian.net/browse/NEON-4) |
|
||
| **Parent context** | [NEON-9 — E1.M1 InputAndMovementRuntime](https://neon-sprawl.atlassian.net/browse/NEON-9) |
|
||
|
||
## 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 (NEON-3 left `sequence` at `0`; this story advances semantics).
|
||
- **Client:** Build on [NEON-2](NEON-2-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**
|
||
|
||
- [NEON-3](NEON-3-implementation-plan.md): in-memory `PositionState` **read** API and store.
|
||
- [NEON-2](NEON-2-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 NEON-3 `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 NEON-2 `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)`** (NEON-8: 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 NEON-2 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 NEON-4 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–NEON-3). |
|
||
| `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` | NEON-4 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 NEON-4).
|
||
|
||
## 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 NEON-3; **`sequence`** increments after each successful move)
|
||
|
||
- Same as NEON-3 **`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.
|