diff --git a/docs/plans/NEO-60-implementation-plan.md b/docs/plans/NEO-60-implementation-plan.md new file mode 100644 index 0000000..343bc52 --- /dev/null +++ b/docs/plans/NEO-60-implementation-plan.md @@ -0,0 +1,109 @@ +# NEO-60 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-60 | +| **Title** | E3.M1: GET world resource-node-definitions + Bruno | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-60/e3m1-get-world-resource-node-definitions-bruno | +| **Module** | [E3.M1 — ResourceNodeAndGatherLoop](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) · Epic 3 Slice 2 · backlog **E3M1-04** | +| **Branch** | `NEO-60-e3m1-get-world-resource-node-definitions-bruno` | +| **Precursor** | [NEO-59](https://linear.app/neon-sprawl/issue/NEO-59) — `IResourceNodeDefinitionRegistry` + DI (**Done** on `main`) | +| **Pattern** | [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) — `GET /game/world/item-definitions` | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Yield projection** | Nested `yield` object vs flat `itemId`/`quantity` on the row? | **Nested `yield: { itemId, quantity }`** — mirrors catalog split (node def + yield table) and matches NEO-59 registry shape; room for multi-yield later. | **User:** nested yield object. | +| **Row id JSON name** | `id` vs `nodeDefId`? | **`id`** — same HTTP convention as item/skill definition endpoints (NEO-53 / NEO-36). | **User:** `id`. | +| **Manual QA doc** | Add `docs/manual-qa/NEO-60.md`? | **Yes** — user-visible HTTP surface; mirror NEO-53. | **User:** yes. | + +## Goal, scope, and out-of-scope + +**Goal:** Expose a stable, read-only JSON endpoint that returns all loaded prototype resource-node definitions (with yield summary) for dev tooling, Bruno, manual QA, and future client/codegen, backed by **`IResourceNodeDefinitionRegistry`** without duplicating catalog truth outside the server. + +**In scope (from Linear + [E3M1-04](E3M1-prototype-backlog.md#e3m1-04--get-world-resource-node-definitions)):** + +- **`GET /game/world/resource-node-definitions`** with versioned envelope (`schemaVersion` **1**, **`nodes`** array). +- Each row: **`id`**, **`displayName`**, **`gatherLens`**, **`maxGathers`**, nested **`yield`** with **`itemId`** and **`quantity`** (camelCase JSON). +- Nodes ordered by **`id`** ordinal, matching **`GetDefinitionsInIdOrder()`**. +- Bruno folder `bruno/neon-sprawl-server/resource-node-definitions/`. +- `docs/manual-qa/NEO-60.md`. +- `server/README.md` section. +- API integration tests (AAA). + +**Out of scope (from Linear + backlog):** + +- Per-instance remaining capacity / depletion state ([NEO-61](https://linear.app/neon-sprawl/issue/NEO-61)). +- Gather engine, inventory mutation, interact wiring ([NEO-62+](E3M1-prototype-backlog.md)). +- Client HUD. + +## Acceptance criteria checklist + +- [ ] GET returns all **four** prototype defs with **`schemaVersion`** **1** and yield summary fields needed for QA. +- [ ] Bruno happy path documented and runnable against dev server. +- [ ] Automated API tests (AAA) assert four ids in order and spot-check node + yield metadata. + +## Technical approach + +1. **Route:** **`GET /game/world/resource-node-definitions`** — parameterless read; inject **`IResourceNodeDefinitionRegistry`** only (not `ResourceNodeCatalog`). + +2. **Response shape:** Top-level **`schemaVersion`** (`ResourceNodeDefinitionsListResponse.CurrentSchemaVersion` = **1**) plus **`nodes`** array. Each row maps from **`ResourceNodeDefRow`** + joined **`ResourceYieldRow`** via **`TryGetYield`**: **`id`** ← `NodeDefId`, **`displayName`**, **`gatherLens`**, **`maxGathers`**, **`yield`** ← `{ itemId, quantity }`. Prototype catalog guarantees one yield per node at load time; if **`TryGetYield`** fails for an enumerated def, treat as programmer error (should not occur post-NEO-58 validation). + +3. **Ordering:** Build list from **`registry.GetDefinitionsInIdOrder()`**. Tests assert exact id sequence (ordinal): + + `prototype_bio_mat_gamma`, `prototype_resource_node_alpha`, `prototype_subsurface_vein_beta`, `prototype_urban_bulk_delta`. + +4. **Implementation:** New **`ResourceNodeDefinitionsWorldApi`** + **`ResourceNodeDefinitionsListDtos.cs`** in `Game/Gathering/` (mirror [`ItemDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Items/ItemDefinitionsWorldApi.cs)). Wire **`app.MapResourceNodeDefinitionsWorldApi()`** from **`Program.cs`** next to **`MapItemDefinitionsWorldApi()`**. + +5. **Bruno:** `bruno/neon-sprawl-server/resource-node-definitions/` with **`Get resource node definitions.bru`** + **`folder.bru`**. Tests: 200, JSON, **`schemaVersion` 1**, **`nodes.length === 4`**, ascending id order, frozen four ids in registry order, spot-check **`prototype_resource_node_alpha`** (`gatherLens` `consumer_salvage`, `maxGathers` 10, `yield.itemId` `scrap_metal_bulk`, `yield.quantity` 1) and **`prototype_urban_bulk_delta`** (`yield.quantity` 5). + +6. **Docs:** `docs/manual-qa/NEO-60.md` (mirror NEO-53). Update **`server/README.md`** resource-node section with GET + curl. Update [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md) **Related implementation slices** and [documentation_and_implementation_alignment.md](documentation_and_implementation_alignment.md) E3.M1 row when landed. Flip E3M1-04 acceptance checkboxes in [E3M1-prototype-backlog.md](E3M1-prototype-backlog.md). + +## Files to add + +| Path | Purpose | +|------|---------| +| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs` | `Map*` extension registering GET route; maps registry → JSON (node + nested yield). | +| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsListDtos.cs` | Versioned response + row/yield DTOs for JSON serialization. | +| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema v1, four ids in order, spot-check rows + yields. | +| `bruno/neon-sprawl-server/resource-node-definitions/Get resource node definitions.bru` | Manual / Bruno verification against dev server. | +| `bruno/neon-sprawl-server/resource-node-definitions/folder.bru` | Bruno folder metadata (match `item-definitions`). | +| `docs/manual-qa/NEO-60.md` | Checklist: start server, curl GET, confirm four ids + spot-check yield. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Program.cs` | Register `MapResourceNodeDefinitionsWorldApi()` alongside other game world APIs. | +| `server/README.md` | Document `GET /game/world/resource-node-definitions`, curl example, links to plan/manual QA/Bruno. | +| `docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md` | **Related implementation slices** — HTTP read model bullet (NEO-60). | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M1 row — note NEO-60 HTTP projection when landed. | +| `docs/plans/E3M1-prototype-backlog.md` | E3M1-04 acceptance checkboxes when landed. | + +## Tests + +| Test file | What it covers | +|-----------|----------------| +| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs` | **Integration:** `GET /game/world/resource-node-definitions` via **`InMemoryWebApplicationFactory`**; **`ReadFromJsonAsync`**; assert **`schemaVersion` 1**; **`nodes`** count **4**; ordered **`id`** list equals frozen four (ordinal); spot-check **`prototype_resource_node_alpha`** (`displayName` “Prototype Salvage Heap”, `gatherLens` `consumer_salvage`, `maxGathers` 10, `yield.itemId` `scrap_metal_bulk`, `yield.quantity` 1) and **`prototype_urban_bulk_delta`** (`gatherLens` `urban_bulk`, `yield.quantity` 5). **AAA** per [csharp-style](../../.cursor/rules/csharp-style.md). | + +Bruno scripts complement automated tests for manual dev-server verification. + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Linear blockedBy NEO-59** | Branch from **`main`** where NEO-59 is merged (confirmed at kickoff). | **adopted** | +| **Missing yield at map time** | Rely on NEO-58 load parity (every node has exactly one yield row); no silent omission in HTTP mapper. | **adopted** | +| **Instance depletion in response** | Omit remaining gathers — instance state is NEO-61; this endpoint is definition catalog only. | **adopted** | + +None blocking. + +## Decisions (kickoff) + +- **Nested `yield` object** — `{ itemId, quantity }` per row; joined from registry’s separate TryGet methods. +- **Row `id` JSON name** — consistent with item/skill world definition endpoints. +- **`nodes` top-level array** — parallel to `items` / `skills` naming on sibling routes. +- **Manual QA doc included** — `docs/manual-qa/NEO-60.md`.