9.0 KiB
NEO-7 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-7 |
| Title | E1.M1: Submit MoveCommand → server updates PositionState |
| Linear | NEO-7 |
| Parent context | E1.M1 InputAndMovementRuntime |
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:DevPlayerIdis 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
sequenceon each successful apply so consumers can detect updates (NEO-6 leftsequenceat0; this story advances semantics). - Client: Build on NEO-5 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.mduntil protobuf).
Out of scope (per Linear)
- Prediction / reconciliation, PostgreSQL, speed anti-cheat.
Dependencies
Acceptance criteria checklist
- After sending a command, subsequent
GET /game/players/{id}/positionreflects 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
MoveCommandandPositionState(XML on DTOs +server/README.md/ PR blurb; reuse existingPositionStateResponseshape for successful command responses where practical so one authoritative snapshot type stays consistent).
Technical approach
-
MoveCommand (v1 JSON)
- New request DTO (e.g.
MoveCommandRequest) with top-levelschemaVersion(1) andtarget: object withx,y,z(same numeric style asPositionVector). - Endpoint:
POST /game/players/{id}/movewith JSON body;{id}identifies the player (trimmed, case-insensitive match to store, consistent with NEO-6GET). - Responses: 200 + body same shape as
PositionStateResponseafter apply; 404 if player unknown; 400 ifschemaVersion≠ 1 or payload invalid / missingtarget.
- New request DTO (e.g.
-
Store mutation
- Extend
IPositionStateStorewith an apply method (e.g.TryApplyMoveTarget) that updates position and incrementssequence, usingConcurrentDictionary-safe update (e.g.TryUpdateloop) consistent withInMemoryPositionStateStore.
- Extend
-
Routing
- Add the new route inside existing
PositionStateApi.MapPositionStateApi()(or equivalent dedicated class); keepProgram.cslimited to extension calls only.
- Add the new route inside existing
-
Tests (
NeonSprawl.Server.Tests)- Integration:
POSTmove for dev player → 200, body position matches target andsequenceincreased; thenGETmatches. - Unknown player → 404; bad
schemaVersion→ 400.
- Integration:
-
Godot client — Split scripts by concern (required repo policy: godot-client-script-organization.md); do not grow a monolithic
main.gdwith pick + HTTP + wiring.ground_pick.gdon a newNode3Dchild (e.g.GroundPick): owns walkable ray pick (logic moved out of NEO-5main.gd). Exposestarget_chosen(Vector3)when the user clicks valid ground.main.gdsetsfallback_camera(or equivalent) in_ready.position_authority_client.gdon a newNodechild:@exportbase URL (defaulthttp://127.0.0.1:5253, matchlaunchSettings.json) and dev player id (dev-local-1). CreatesHTTPRequestin_readyviaadd_child(not as a node hand-placed inmain.tscn). Flow:POSTMoveCommand, thenGETposition (tests must show GET reflects apply). Emitsauthoritative_position_received(Vector3, bool)(NEO-11: second arg = boot snap vs nav goal) somainsnaps on boot and path-follows after move verify.main.gd: Thin composer only—connect signals, callsync_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.
-
Documentation
server/README.md: new subsection for move submission: method, path, samplecurl, 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>—POSTthenGETequality,404/400as 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):
{
"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
GETresponse body; successfulPOSTreturns 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
yfrom the command; client should send the sameyas the ray hit (or document if server flattens — plan is no flatten unless playtest demands). - WebSocket: explicitly deferred; HTTP only for this story.