NEO-55: add implementation plan for inventory GET/POST + Bruno
Kickoff clarifications: single POST with mutationKind, full fixed slot arrays, applied response field; no manual QA doc per user choice.pull/90/head
parent
658b627903
commit
db3b80d242
|
|
@ -0,0 +1,122 @@
|
|||
# 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](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) · Epic 3 Slice 1 (E3M3-06) |
|
||||
| **Branch** | `NEO-55-player-inventory-getpost-bruno` |
|
||||
| **Precursor** | [NEO-54](https://linear.app/neon-sprawl/issue/NEO-54) — `IPlayerInventoryStore` + `PlayerInventoryOperations` (**Done** on `main`) |
|
||||
| **Pattern** | [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) / [NEO-38](https://linear.app/neon-sprawl/issue/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](E3M3-prototype-backlog.md#e3m3-06--player-inventory-http--bruno)):**
|
||||
|
||||
- **`GET /game/players/{id}/inventory`** — instances + fixed slot arrays for known player.
|
||||
- **`POST /game/players/{id}/inventory`** — **`mutationKind`** **`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](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md)).
|
||||
- Telemetry hook sites ([NEO-56](https://linear.app/neon-sprawl/issue/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](E3_M3_ItemizationAndInventorySchema.md) **Related implementation slices** and [documentation_and_implementation_alignment.md](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](../../.cursor/rules/csharp-style.md). |
|
||||
|
||||
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.
|
||||
Loading…
Reference in New Issue