110 lines
8.4 KiB
Markdown
110 lines
8.4 KiB
Markdown
# 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
|
||
|
||
- [x] GET returns all **four** prototype defs with **`schemaVersion`** **1** and yield summary fields needed for QA.
|
||
- [x] Bruno happy path documented and runnable against dev server.
|
||
- [x] 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`.
|