neon-sprawl/docs/plans/NEO-55-implementation-plan.md

9.3 KiB

NEO-55 — Implementation plan

Story reference

Field Value
Key NEO-55
Title E3.M3: Player inventory GET/POST + Bruno
Linear https://linear.app/neon-sprawl/issue/NEO-55/e3m3-player-inventory-getpost-bruno
Module E3.M3 — ItemizationAndInventorySchema · Epic 3 Slice 1 (E3M3-06)
Branch NEO-55-player-inventory-getpost-bruno
Precursor NEO-54IPlayerInventoryStore + PlayerInventoryOperations (Done on main)
Pattern NEO-37 / NEO-38GET/POST /game/players/{id}/skill-progression snapshot + mutating apply with structured deny

Kickoff clarifications

Topic Question Agent recommendation Answer
POST shape Single POST vs separate add/remove routes? Single POST on same path as GET with mutationKind add | remove, itemId, quantity, schemaVersion — mirrors NEO-37/38 one-route read/write pattern and maps 1:1 to PlayerInventoryOperations. User: single POST + mutationKind.
GET slots Full fixed arrays vs sparse occupied-only? Full fixed arraysbagSlots length 24, equipmentSlots length 1; empty slots { slotIndex, quantity: 0 } with itemId omitted — matches NEO-54 snapshot model and supports client grid rendering. User: full fixed arrays.
POST success field applied vs granted? applied — inventory mutations are add/remove, not XP grants; aligns with hotbar updated naming. User: applied.
Manual QA doc Add docs/manual-qa/NEO-55.md? Yes — user-visible HTTP surface; same pattern as NEO-53. User: No — Bruno + README only.

Goal, scope, and out-of-scope

Goal: Versioned GET /game/players/{id}/inventory snapshot and mutating POST for manual QA and future client, delegating rules to PlayerInventoryOperations (NEO-54).

In scope (from Linear + E3M3-06):

  • GET /game/players/{id}/inventory — instances + fixed slot arrays for known player.
  • POST /game/players/{id}/inventorymutationKind add / remove with engine-backed deny reason codes.
  • Known-player gate via IPositionStateStore (NEO-37 pattern).
  • Bruno bruno/neon-sprawl-server/inventory/ — happy path + at least one deny case.
  • server/README.md inventory HTTP subsection.
  • API integration tests (AAA).

Out of scope (from Linear):

  • Godot HUD.
  • Gather node depletion (E3.M1).
  • Telemetry hook sites (NEO-56).
  • docs/manual-qa/NEO-55.md (kickoff decision — Bruno + README cover manual verification).

Acceptance criteria checklist

  • GET returns instances + slots for known player (dev-local-1 seeded empty inventory).
  • POST add/remove matches E3M3-05 engine rules (stackMax, all-or-nothing add, stable reasonCode denies).
  • Bruno exercises happy path + at least one deny (inventory_full or invalid_item).

Technical approach

  1. Routes: GET and POST /game/players/{id}/inventory — trim id; 404 when empty/unknown position (NEO-37 gate).

  2. GET response (PlayerInventorySnapshotResponse, schemaVersion 1):

    • playerId, schemaVersion, bagSlots (length 24), equipmentSlots (length 1).
    • Each slot row: slotIndex, optional itemId (omitted when empty), quantity (0 when empty).
    • Map from PlayerInventorySnapshot via internal BuildSnapshot(playerId, store) helper (mirror SkillProgressionSnapshotApi.BuildSnapshot).
  3. POST request (PlayerInventoryMutationRequest, schemaVersion 1):

    • mutationKind: add | remove (ordinal ignore-case).
    • itemId, quantity (positive int for engine; non-positive handled by engine → invalid_quantity deny).
  4. POST response (PlayerInventoryMutationResponse, schemaVersion 1):

    • applied: true on success, false on rule deny.
    • reasonCode: null on success; engine codes on deny (inventory_full, invalid_item, insufficient_quantity, invalid_quantity).
    • inventory: full snapshot echo (unchanged on deny, updated on success) — mirror NEO-38 progression echo.
  5. HTTP status mapping (NEO-38 precedent):

    • 400 — null body or schemaVersion mismatch; unknown mutationKind string.
    • 404 — unknown player (position gate) or PlayerInventoryMutationKind.StoreMissing.
    • 200 + JSON — rule Denied with applied: false + reasonCode + snapshot echo.
    • 200 + JSON — Applied with applied: true, reasonCode: null, updated snapshot.
  6. Implementation files: PlayerInventoryApi.cs + PlayerInventoryDtos.cs in Game/Items/; wire app.MapPlayerInventoryApi() from Program.cs after item catalog maps (inventory store already registered via AddItemDefinitionCatalog chain).

  7. Bruno (bruno/neon-sprawl-server/inventory/):

    • Get inventory.bru — 200, schemaVersion 1, bagSlots.length === 24, equipmentSlots.length === 1, all empty on fresh dev player.
    • Post inventory add.bru — add scrap_metal_bulk qty 5; assert applied true, slot shows item + quantity.
    • Post inventory add deny invalid item.bru (or fill-bag deny) — assert applied false + stable reasonCode.
    • folder.bru — match sibling folders.
  8. Docs (on land): Expand server/README.md NEO-54 inventory section with GET/POST curl examples and reason codes. Update E3_M3 Related implementation slices and documentation_and_implementation_alignment.md E3.M3 row.

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Items/PlayerInventoryDtos.cs Versioned GET snapshot + POST request/response DTOs (schemaVersion 1).
server/NeonSprawl.Server/Game/Items/PlayerInventoryApi.cs MapPlayerInventoryApi — GET/POST routes, position gate, engine dispatch, JSON mapping.
server/NeonSprawl.Server.Tests/Game/Items/PlayerInventoryApiTests.cs AAA HTTP integration: GET 404/200 empty grid; POST add happy; POST deny (invalid_item, insufficient_quantity); schema mismatch 400; unknown player 404.
bruno/neon-sprawl-server/inventory/folder.bru Bruno folder metadata.
bruno/neon-sprawl-server/inventory/Get inventory.bru GET happy path against dev-local-1.
bruno/neon-sprawl-server/inventory/Post inventory add.bru POST add happy path (scrap_metal_bulk).
bruno/neon-sprawl-server/inventory/Post inventory add deny invalid item.bru POST deny with invalid_item.

Files to modify

Path Rationale
server/NeonSprawl.Server/Program.cs Register MapPlayerInventoryApi() alongside other game APIs.
server/README.md Document GET/POST inventory routes, request/response shapes, curl examples, reason codes; remove “no HTTP in NEO-54” deferral note.
docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md Related implementation slices — inventory HTTP bullet (NEO-55).
docs/decomposition/modules/documentation_and_implementation_alignment.md E3.M3 row — note NEO-55 GET/POST when landed.

Tests

Test file What it covers
PlayerInventoryApiTests.cs GET unknown player → 404. GET dev-local-1200, schemaVersion 1, 24 bag + 1 equipment slots, all empty. POST add scrap_metal_bulk qty 10applied true, quantity in bag slot(s), reasonCode null. POST add unknown item → 200, applied false, invalid_item, inventory unchanged. POST remove without prior add → insufficient_quantity. POST bad schemaVersion400. POST unknown mutationKind400. AAA per csharp-style.

Bruno scripts complement automated tests for dev-server manual verification (no docs/manual-qa/NEO-55.md per kickoff).

Open questions / risks

Question / risk Agent recommendation Status
Unknown mutationKind 400 BadRequest (client contract error), not engine deny — same class as schema mismatch. adopted
Empty slot JSON Omit itemId property when quantity 0 (System.Text.Json default for null optional). adopted
NEO-56 telemetry hooks No hook comments in this story — NEO-56 owns engine/POST hook placement. deferred to NEO-56
Concurrent POSTs Last-write-wins at store layer (NEO-54 review note); acceptable for prototype QA. adopted

None blocking.