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

8.9 KiB
Raw Blame History

NS-16 — Implementation plan

Story reference

Field Value
Key NS-16
Title E1.M1: Submit MoveCommand → server updates PositionState
Jira NS-16
Parent context NS-10 — 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: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 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 until protobuf).

Out of scope (per Jira)

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

Dependencies

  • NS-15: in-memory PositionState read API and store.
  • NS-14: 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 schemaVersion400.
  5. Godot clientSplit scripts by concern (required repo policy: 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):
{
  "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.