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-54 — IPlayerInventoryStore + PlayerInventoryOperations (Done on main) |
| Pattern | NEO-37 / NEO-38 — GET/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 arrays — bagSlots 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}/inventory—mutationKindadd/removewith 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.mdinventory 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-1seeded empty inventory). - POST add/remove matches E3M3-05 engine rules (
stackMax, all-or-nothing add, stablereasonCodedenies). - Bruno exercises happy path + at least one deny (
inventory_fullorinvalid_item).
Technical approach
-
Routes:
GETandPOST/game/players/{id}/inventory— trimid; 404 when empty/unknown position (NEO-37 gate). -
GET response (
PlayerInventorySnapshotResponse,schemaVersion1):playerId,schemaVersion,bagSlots(length 24),equipmentSlots(length 1).- Each slot row:
slotIndex, optionalitemId(omitted when empty),quantity(0 when empty). - Map from
PlayerInventorySnapshotvia internalBuildSnapshot(playerId, store)helper (mirrorSkillProgressionSnapshotApi.BuildSnapshot).
-
POST request (
PlayerInventoryMutationRequest,schemaVersion1):mutationKind:add|remove(ordinal ignore-case).itemId,quantity(positive int for engine; non-positive handled by engine →invalid_quantitydeny).
-
POST response (
PlayerInventoryMutationResponse,schemaVersion1):applied:trueon success,falseon rule deny.reasonCode:nullon 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.
-
HTTP status mapping (NEO-38 precedent):
- 400 — null body or
schemaVersionmismatch; unknownmutationKindstring. - 404 — unknown player (position gate) or
PlayerInventoryMutationKind.StoreMissing. - 200 + JSON — rule
Deniedwithapplied: false+reasonCode+ snapshot echo. - 200 + JSON —
Appliedwithapplied: true,reasonCode: null, updated snapshot.
- 400 — null body or
-
Implementation files:
PlayerInventoryApi.cs+PlayerInventoryDtos.csinGame/Items/; wireapp.MapPlayerInventoryApi()fromProgram.csafter item catalog maps (inventory store already registered viaAddItemDefinitionCatalogchain). -
Bruno (
bruno/neon-sprawl-server/inventory/):Get inventory.bru— 200,schemaVersion1,bagSlots.length === 24,equipmentSlots.length === 1, all empty on fresh dev player.Post inventory add.bru— addscrap_metal_bulkqty 5; assertappliedtrue, slot shows item + quantity.Post inventory add deny invalid item.bru(or fill-bag deny) — assertappliedfalse + stablereasonCode.folder.bru— match sibling folders.
-
Docs (on land): Expand
server/README.mdNEO-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-1 → 200, schemaVersion 1, 24 bag + 1 equipment slots, all empty. POST add scrap_metal_bulk qty 10 → applied 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 schemaVersion → 400. POST unknown mutationKind → 400. 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.