diff --git a/bruno/neon-sprawl-server/resource-node-definitions/Get resource node definitions.bru b/bruno/neon-sprawl-server/resource-node-definitions/Get resource node definitions.bru new file mode 100644 index 0000000..a59db19 --- /dev/null +++ b/bruno/neon-sprawl-server/resource-node-definitions/Get resource node definitions.bru @@ -0,0 +1,69 @@ +meta { + name: GET resource node definitions + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/game/world/resource-node-definitions + body: none + auth: none +} + +tests { + test("returns 200 JSON with schema v1 and nodes array", function () { + expect(res.getStatus()).to.equal(200); + expect(res.getHeader("content-type")).to.contain("application/json"); + const body = res.getBody(); + expect(body.schemaVersion).to.equal(1); + expect(body.nodes).to.be.an("array"); + expect(body.nodes.length).to.equal(4); + }); + + test("nodes are ascending by id (ordinal)", function () { + const body = res.getBody(); + const ids = body.nodes.map((x) => x.id); + const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + expect(ids).to.eql(sorted); + }); + + test("frozen prototype four matches registry id order", function () { + const body = res.getBody(); + const ids = body.nodes.map((x) => x.id); + expect(ids).to.eql([ + "prototype_bio_mat_gamma", + "prototype_resource_node_alpha", + "prototype_subsurface_vein_beta", + "prototype_urban_bulk_delta", + ]); + }); + + test("frozen prototype four is present", function () { + const body = res.getBody(); + const ids = new Set(body.nodes.map((x) => x.id)); + expect(ids.has("prototype_bio_mat_gamma")).to.equal(true); + expect(ids.has("prototype_resource_node_alpha")).to.equal(true); + expect(ids.has("prototype_subsurface_vein_beta")).to.equal(true); + expect(ids.has("prototype_urban_bulk_delta")).to.equal(true); + }); + + test("prototype_resource_node_alpha row matches catalog", function () { + const body = res.getBody(); + const row = body.nodes.find((x) => x.id === "prototype_resource_node_alpha"); + expect(row).to.be.an("object"); + expect(row.displayName).to.equal("Prototype Salvage Heap"); + expect(row.gatherLens).to.equal("consumer_salvage"); + expect(row.maxGathers).to.equal(10); + expect(row.yield).to.be.an("object"); + expect(row.yield.itemId).to.equal("scrap_metal_bulk"); + expect(row.yield.quantity).to.equal(1); + }); + + test("prototype_urban_bulk_delta yield quantity matches catalog", function () { + const body = res.getBody(); + const row = body.nodes.find((x) => x.id === "prototype_urban_bulk_delta"); + expect(row).to.be.an("object"); + expect(row.gatherLens).to.equal("urban_bulk"); + expect(row.yield.quantity).to.equal(5); + }); +} diff --git a/bruno/neon-sprawl-server/resource-node-definitions/folder.bru b/bruno/neon-sprawl-server/resource-node-definitions/folder.bru new file mode 100644 index 0000000..b159cd2 --- /dev/null +++ b/bruno/neon-sprawl-server/resource-node-definitions/folder.bru @@ -0,0 +1,3 @@ +meta { + name: resource-node-definitions +} diff --git a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md index 184c882..cdbc968 100644 --- a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md +++ b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md @@ -94,6 +94,8 @@ Epic 3 **Slice 2** — [gather nodes and outputs](../epics/epic_03_crafting_econ **Prototype implementation anchor (NEO-41):** until **`GatherResult`** lands, a **successful** **`POST …/interact`** on **`kind: resource_node`** grants **`salvage`** skill XP only (`activity`, 10 XP) via `SkillProgressionGrantOperations` — **no** inventory or depletion. **E3M1-07** migrates XP into the gather engine and adds item + capacity behavior ([E3M1-prototype-backlog](../../plans/E3M1-prototype-backlog.md)). +**Resource node definitions HTTP (NEO-60):** **`GET /game/world/resource-node-definitions`** — versioned read-only projection (`schemaVersion` **1**, **`nodes`**) backed by **`IResourceNodeDefinitionRegistry`**; each row includes nested **`yield`** summary. Bruno `bruno/neon-sprawl-server/resource-node-definitions/`. Plan: [NEO-60 implementation plan](../../plans/NEO-60-implementation-plan.md). + **Linear backlog (decomposed):** [E3M1-prototype-backlog.md](../../plans/E3M1-prototype-backlog.md) — **E3M1-01** [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) through **E3M1-08** [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64). ## Source anchors diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 9a87040..93b8714 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -53,7 +53,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E2.M1 | In Progress | **NEO-33 landed:** frozen prototype trio **`salvage`** / **`refine`** / **`intrusion`** in [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) enforce schema, duplicate `id`, exact trio ids, and category coverage; designer note on **`allowedXpSourceKinds`** in [E2_M1](E2_M1_SkillDefinitionRegistry.md) + [`content/README.md`](../../../content/README.md). **NEO-34 landed:** fail-fast server load of `content/skills/*.json` at startup — `server/NeonSprawl.Server/Game/Skills/` ([NEO-34](../../plans/NEO-34-implementation-plan.md)); config + discovery in [server README — Skill catalog](../../../server/README.md#skill-catalog-contentskills-neo-34). **NEO-35 landed:** `ISkillDefinitionRegistry` / `SkillDefinitionRegistry` + DI ([NEO-35](../../plans/NEO-35-implementation-plan.md)). **NEO-36 landed:** versioned **`GET /game/world/skill-definitions`** ([NEO-36](../../plans/NEO-36-implementation-plan.md)) — `SkillDefinitionsWorldApi` + `SkillDefinitionsListDtos` in `server/NeonSprawl.Server/Game/Skills/`; Bruno `bruno/neon-sprawl-server/skill-definitions/`; manual QA [`NEO-36`](../../manual-qa/NEO-36.md). **E2.M2 consumer (NEO-38 landed):** grant path validates `skillId` + `sourceKind` vs catalog **`allowedXpSourceKinds`** on **`POST …/skill-progression`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); see [server README — Skill progression grant](../../../server/README.md#skill-progression-grant-neo-38) and [E2_M2](E2_M2_XpAwardAndLevelEngine.md). | [NEO-33](../../plans/NEO-33-implementation-plan.md), [NEO-34](../../plans/NEO-34-implementation-plan.md), [NEO-35](../../plans/NEO-35-implementation-plan.md), [NEO-36](../../plans/NEO-36-implementation-plan.md); [E2_M1](E2_M1_SkillDefinitionRegistry.md); [module_dependency_register.md](module_dependency_register.md) **E2.M1 note**; `server/NeonSprawl.Server/Game/Skills/`; [`docs/manual-qa/NEO-36.md`](../../manual-qa/NEO-36.md); [server README — Skill definitions (NEO-36)](../../../server/README.md#skill-definitions-neo-36) | | E2.M3 | In Progress | **NEO-45 landed:** prototype **`salvage`** mastery catalog + CI gates (see [NEO-45 plan](../../plans/NEO-45-implementation-plan.md)). **NEO-46 landed:** fail-fast server load under `server/NeonSprawl.Server/Game/Mastery/` — `MasteryCatalogLoader`, `IMasteryCatalogRegistry`, cross-check vs `ISkillDefinitionRegistry`, Slice 4 + **`tierIndex`** 1..N gate; see [NEO-46 plan](../../plans/NEO-46-implementation-plan.md). **NEO-47 landed:** `PerkUnlockEngine`, `IPlayerPerkStateStore` + **`V004`**, level-up hook in skill XP grants; see [NEO-47 plan](../../plans/NEO-47-implementation-plan.md). **NEO-49 landed:** comment-only **`perk_unlock`** telemetry hook site in [`PerkUnlockEngine.TryUnlockPerks`](../../../server/NeonSprawl.Server/Game/Mastery/PerkUnlockEngine.cs) ([NEO-49 plan](../../plans/NEO-49-implementation-plan.md), [`NEO-49` manual QA](../../manual-qa/NEO-49.md)); [server README — Perk unlock telemetry (NEO-49)](../../../server/README.md#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49). **NEO-48 landed:** **`GET`/`POST /game/players/{id}/perk-state`** — `PerkStateApi` + DTOs in `Game/Mastery/` ([NEO-48](../../plans/NEO-48-implementation-plan.md), [`NEO-48` manual QA](../../manual-qa/NEO-48.md)); [server README — Perk state (NEO-48)](../../../server/README.md#perk-state-neo-48); Bruno `bruno/neon-sprawl-server/perk-state/`. | [NEO-45](../../plans/NEO-45-implementation-plan.md), [NEO-46](../../plans/NEO-46-implementation-plan.md), [NEO-47](../../plans/NEO-47-implementation-plan.md), [NEO-48](../../plans/NEO-48-implementation-plan.md), [NEO-49](../../plans/NEO-49-implementation-plan.md), [E2M3-pre-production-backlog](../../plans/E2M3-pre-production-backlog.md), [E2_M3](E2_M3_MasteryAndPerkUnlocks.md) | | E2.M2 | In Progress | **NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([NEO-37](../../plans/NEO-37-implementation-plan.md)) — read model for every registered skill; known-player gate via `IPositionStateStore`; [server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37); manual QA [`NEO-37`](../../manual-qa/NEO-37.md). **NEO-38 landed:** **`POST`** same path — grant apply, persistence (`IPlayerSkillProgressionStore`, `V003` migration), structured denies + **`levelUps`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); manual QA [`NEO-38`](../../manual-qa/NEO-38.md); Bruno `bruno/neon-sprawl-server/skill-progression/`; [server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38). **NEO-39 landed:** data-driven `LevelCurve` content + schema + CI validation (`*_level_curve.json`) with fail-fast startup schema checks; progression GET/POST level resolution now uses `ISkillLevelCurve` content-backed thresholds ([NEO-39](../../plans/NEO-39-implementation-plan.md)); manual QA [`NEO-39`](../../manual-qa/NEO-39.md). **NEO-40 landed:** comment-only telemetry hook sites in [`SkillProgressionSnapshotApi.cs`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs) on **`POST …/skill-progression`** for future **`xp_grant`** / **`level_up`** ([NEO-40](../../plans/NEO-40-implementation-plan.md), [`NEO-40` manual QA](../../manual-qa/NEO-40.md)). **NEO-41 landed:** gather prototype — **`POST …/interact`** with **`kind: resource_node`** grants **`salvage`** + **`activity`** (10 XP) via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) ([NEO-41](../../plans/NEO-41-implementation-plan.md), [`NEO-41` manual QA](../../manual-qa/NEO-41.md)); [server README — Interaction](../../../server/README.md#interaction-neo-9). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack; **E3.M2** must invoke on craft/refine success ([NEO-42](../../plans/NEO-42-implementation-plan.md), [`NEO-42` manual QA](../../manual-qa/NEO-42.md)); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42). **NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** / **`MissionRewardSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack with fixed **`sourceKind: mission_reward`**; **E7.M2** must invoke from quest hand-in ([NEO-43](../../plans/NEO-43-implementation-plan.md), [`NEO-43` manual QA](../../manual-qa/NEO-43.md)); [server README — Mission / quest reward (NEO-43)](../../../server/README.md#mission--quest-reward--skill-xp-neo-43). **Slice 3 still open:** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [NEO-37](../../plans/NEO-37-implementation-plan.md), [NEO-38](../../plans/NEO-38-implementation-plan.md), [NEO-39](../../plans/NEO-39-implementation-plan.md), [NEO-40](../../plans/NEO-40-implementation-plan.md), [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-43](../../plans/NEO-43-implementation-plan.md), [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37–NEO-41, NEO-42–NEO-44 | -| E3.M1 | In Progress | **NEO-41 landed (prototype):** `POST …/interact` success on **`resource_node`** (`prototype_resource_node_alpha`) applies **`salvage`** skill XP (**`sourceKind: activity`**, 10 XP) via shared NEO-38 grant operations. **NEO-57 landed:** frozen four-node catalog in [`content/resource-nodes/`](../../../content/resource-nodes/); [`resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json), [`resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); CI gates in [`validate_content.py`](../../../scripts/validate_content.py). **NEO-58 landed:** fail-fast server load of `content/resource-nodes/*` at startup — `server/NeonSprawl.Server/Game/Gathering/` ([NEO-58](../../plans/NEO-58-implementation-plan.md)); [server README — Resource-node catalog](../../../server/README.md#resource-node-catalog-contentresource-nodes-neo-58). **NEO-59 landed:** injectable **`IResourceNodeDefinitionRegistry`** + lookup tests ([NEO-59](../../plans/NEO-59-implementation-plan.md)). **Still planned:** `GatherResult`, depletion, inventory grants per [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-57](../../plans/NEO-57-implementation-plan.md), [NEO-58](../../plans/NEO-58-implementation-plan.md), [NEO-59](../../plans/NEO-59-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `content/resource-nodes/`, `server/NeonSprawl.Server/Game/Gathering/`, `server/NeonSprawl.Server/Game/Interaction/` | +| E3.M1 | In Progress | **NEO-41 landed (prototype):** `POST …/interact` success on **`resource_node`** (`prototype_resource_node_alpha`) applies **`salvage`** skill XP (**`sourceKind: activity`**, 10 XP) via shared NEO-38 grant operations. **NEO-57 landed:** frozen four-node catalog in [`content/resource-nodes/`](../../../content/resource-nodes/); [`resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json), [`resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); CI gates in [`validate_content.py`](../../../scripts/validate_content.py). **NEO-58 landed:** fail-fast server load of `content/resource-nodes/*` at startup — `server/NeonSprawl.Server/Game/Gathering/` ([NEO-58](../../plans/NEO-58-implementation-plan.md)); [server README — Resource-node catalog](../../../server/README.md#resource-node-catalog-contentresource-nodes-neo-58). **NEO-59 landed:** injectable **`IResourceNodeDefinitionRegistry`** + lookup tests ([NEO-59](../../plans/NEO-59-implementation-plan.md)). **NEO-60 landed:** **`GET /game/world/resource-node-definitions`** — `ResourceNodeDefinitionsWorldApi` + DTOs in `Game/Gathering/` ([NEO-60](../../plans/NEO-60-implementation-plan.md), [`NEO-60` manual QA](../../manual-qa/NEO-60.md)); [server README — Resource node definitions (NEO-60)](../../../server/README.md#resource-node-definitions-neo-60); Bruno `bruno/neon-sprawl-server/resource-node-definitions/`. **Still planned:** `GatherResult`, depletion, inventory grants per [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-57](../../plans/NEO-57-implementation-plan.md), [NEO-58](../../plans/NEO-58-implementation-plan.md), [NEO-59](../../plans/NEO-59-implementation-plan.md), [NEO-60](../../plans/NEO-60-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `content/resource-nodes/`, `server/NeonSprawl.Server/Game/Gathering/`, `server/NeonSprawl.Server/Game/Interaction/` | | E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) | --- diff --git a/docs/manual-qa/NEO-60.md b/docs/manual-qa/NEO-60.md new file mode 100644 index 0000000..7f3c05c --- /dev/null +++ b/docs/manual-qa/NEO-60.md @@ -0,0 +1,28 @@ +# NEO-60 — Manual QA checklist + +| 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 | +| Plan | `docs/plans/NEO-60-implementation-plan.md` | +| Branch | `NEO-60-e3m1-get-world-resource-node-definitions-bruno` | + +## Preconditions + +- Server built and configured with default `Content:ResourceNodesDirectory` pointing at repo `content/resource-nodes` (local dev / `InMemoryWebApplicationFactory` tests use the same layout). + +## Checklist + +1. Start **`NeonSprawl.Server`** (e.g. `dotnet run` from `server/NeonSprawl.Server`). +2. **`GET /game/world/resource-node-definitions`** — expect **200** and **`Content-Type`** containing **`application/json`**. Example (default dev URL from `Properties/launchSettings.json` and Bruno `environments/Local.bru`; change the host/port if yours differs): + + ```bash + curl -sS -i "http://localhost:5253/game/world/resource-node-definitions" + ``` + +3. Parse JSON — expect **`schemaVersion`** === **1**, **`nodes`** array length **4**. +4. Confirm **`id`** values match the frozen prototype four in ordinal order: **`prototype_bio_mat_gamma`**, **`prototype_resource_node_alpha`**, **`prototype_subsurface_vein_beta`**, **`prototype_urban_bulk_delta`**. +5. Spot-check **`prototype_resource_node_alpha`**: **`displayName`** “Prototype Salvage Heap”, **`gatherLens`** **`consumer_salvage`**, **`maxGathers`** **10**, **`yield.itemId`** **`scrap_metal_bulk`**, **`yield.quantity`** **1**. +6. Spot-check **`prototype_urban_bulk_delta`**: **`gatherLens`** **`urban_bulk`**, **`yield.quantity`** **5**. +7. Optional: run **`bruno/neon-sprawl-server/resource-node-definitions/Get resource node definitions.bru`** against the same **`baseUrl`** (see `environments/Local.bru`). diff --git a/docs/plans/E3M1-prototype-backlog.md b/docs/plans/E3M1-prototype-backlog.md index ae231f7..be8b775 100644 --- a/docs/plans/E3M1-prototype-backlog.md +++ b/docs/plans/E3M1-prototype-backlog.md @@ -142,8 +142,10 @@ Working backlog for **Epic 3 — Slice 2** ([gather nodes and outputs](../decomp **Acceptance criteria** -- [ ] GET returns all four prototype defs with schema version field and yield summary fields needed for QA. -- [ ] Bruno happy path documented. +- [x] GET returns all four prototype defs with schema version field and yield summary fields needed for QA. +- [x] Bruno happy path documented. + +**Landed:** [NEO-60](https://linear.app/neon-sprawl/issue/NEO-60) — HTTP read model + Bruno; per-instance depletion is E3M1-05. --- diff --git a/docs/plans/NEO-60-implementation-plan.md b/docs/plans/NEO-60-implementation-plan.md index 343bc52..9007924 100644 --- a/docs/plans/NEO-60-implementation-plan.md +++ b/docs/plans/NEO-60-implementation-plan.md @@ -42,9 +42,9 @@ ## 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. +- [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 diff --git a/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs new file mode 100644 index 0000000..49b9671 --- /dev/null +++ b/server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionsWorldApiTests.cs @@ -0,0 +1,47 @@ +using System.Linq; +using System.Net; +using System.Net.Http.Json; +using NeonSprawl.Server.Game.Gathering; +using Xunit; + +namespace NeonSprawl.Server.Tests.Game.Gathering; + +public class ResourceNodeDefinitionsWorldApiTests +{ + private static readonly string[] FrozenFourInIdOrder = + [ + "prototype_bio_mat_gamma", + "prototype_resource_node_alpha", + "prototype_subsurface_vein_beta", + "prototype_urban_bulk_delta", + ]; + + [Fact] + public async Task GetResourceNodeDefinitions_ShouldReturnSchemaV1_WithFrozenFourInIdOrder() + { + // Arrange + await using var factory = new InMemoryWebApplicationFactory(); + var client = factory.CreateClient(); + // Act + var response = await client.GetAsync("/game/world/resource-node-definitions"); + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(body); + Assert.Equal(ResourceNodeDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion); + Assert.NotNull(body.Nodes); + var ids = body.Nodes.Select(static n => n.Id).ToList(); + Assert.Equal(FrozenFourInIdOrder, ids); + + var alpha = body.Nodes.Single(n => n.Id == "prototype_resource_node_alpha"); + Assert.Equal("Prototype Salvage Heap", alpha.DisplayName); + Assert.Equal("consumer_salvage", alpha.GatherLens); + Assert.Equal(10, alpha.MaxGathers); + Assert.Equal("scrap_metal_bulk", alpha.Yield.ItemId); + Assert.Equal(1, alpha.Yield.Quantity); + + var delta = body.Nodes.Single(n => n.Id == "prototype_urban_bulk_delta"); + Assert.Equal("urban_bulk", delta.GatherLens); + Assert.Equal(5, delta.Yield.Quantity); + } +} diff --git a/server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsListDtos.cs b/server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsListDtos.cs new file mode 100644 index 0000000..61443ab --- /dev/null +++ b/server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsListDtos.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace NeonSprawl.Server.Game.Gathering; + +/// JSON body for GET /game/world/resource-node-definitions (NEO-60). +public sealed class ResourceNodeDefinitionsListResponse +{ + public const int CurrentSchemaVersion = 1; + + [JsonPropertyName("schemaVersion")] + public int SchemaVersion { get; init; } = CurrentSchemaVersion; + + /// Loaded node defs ordered by stable id (ordinal), matching . + [JsonPropertyName("nodes")] + public required IReadOnlyList Nodes { get; init; } +} + +/// One row in the read-only resource-node definition projection. +public sealed class ResourceNodeDefinitionJson +{ + [JsonPropertyName("id")] + public required string Id { get; init; } + + [JsonPropertyName("displayName")] + public required string DisplayName { get; init; } + + [JsonPropertyName("gatherLens")] + public required string GatherLens { get; init; } + + [JsonPropertyName("maxGathers")] + public required int MaxGathers { get; init; } + + [JsonPropertyName("yield")] + public required ResourceNodeYieldJson Yield { get; init; } +} + +/// Fixed yield summary for one resource node def. +public sealed class ResourceNodeYieldJson +{ + [JsonPropertyName("itemId")] + public required string ItemId { get; init; } + + [JsonPropertyName("quantity")] + public required int Quantity { get; init; } +} diff --git a/server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs b/server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs new file mode 100644 index 0000000..efcb064 --- /dev/null +++ b/server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionsWorldApi.cs @@ -0,0 +1,47 @@ +namespace NeonSprawl.Server.Game.Gathering; + +/// Maps GET /game/world/resource-node-definitions (NEO-60). +public static class ResourceNodeDefinitionsWorldApi +{ + public static WebApplication MapResourceNodeDefinitionsWorldApi(this WebApplication app) + { + app.MapGet( + "/game/world/resource-node-definitions", + (IResourceNodeDefinitionRegistry registry) => + { + var defs = registry.GetDefinitionsInIdOrder(); + var nodes = new List(defs.Count); + foreach (var d in defs) + { + if (!registry.TryGetYield(d.NodeDefId, out var yieldRow) || yieldRow is null) + { + throw new InvalidOperationException( + $"Resource node definition '{d.NodeDefId}' has no yield row in the loaded catalog."); + } + + nodes.Add( + new ResourceNodeDefinitionJson + { + Id = d.NodeDefId, + DisplayName = d.DisplayName, + GatherLens = d.GatherLens, + MaxGathers = d.MaxGathers, + Yield = new ResourceNodeYieldJson + { + ItemId = yieldRow.ItemId, + Quantity = yieldRow.Quantity, + }, + }); + } + + return Results.Json( + new ResourceNodeDefinitionsListResponse + { + SchemaVersion = ResourceNodeDefinitionsListResponse.CurrentSchemaVersion, + Nodes = nodes, + }); + }); + + return app; + } +} diff --git a/server/NeonSprawl.Server/Program.cs b/server/NeonSprawl.Server/Program.cs index b5d83b6..8e7a00a 100644 --- a/server/NeonSprawl.Server/Program.cs +++ b/server/NeonSprawl.Server/Program.cs @@ -41,6 +41,7 @@ app.MapInteractionApi(); app.MapInteractablesWorldApi(); app.MapSkillDefinitionsWorldApi(); app.MapItemDefinitionsWorldApi(); +app.MapResourceNodeDefinitionsWorldApi(); app.MapPlayerInventoryApi(); app.MapSkillProgressionSnapshotApi(); app.MapPerkStateApi(); diff --git a/server/README.md b/server/README.md index bb4fadc..ac6e649 100644 --- a/server/README.md +++ b/server/README.md @@ -66,6 +66,14 @@ On success, **Information** logs include the resolved resource-nodes directory p **`IResourceNodeDefinitionRegistry`** (NEO-59) is the preferred lookup surface for gather engine, depletion, interact, and HTTP callers — resolve node defs and yield rows by **`nodeDefId`**, enumerate defs in id order. The catalog singleton remains for fail-fast startup only; do not inject **`ResourceNodeCatalog`** in new game code. +## Resource node definitions (NEO-60) + +**`GET /game/world/resource-node-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`nodes`**) backed by **`IResourceNodeDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`gatherLens`**, **`maxGathers`**, and nested **`yield`** (`itemId`, `quantity`). Plan: [NEO-60 implementation plan](../../docs/plans/NEO-60-implementation-plan.md); manual QA: [`docs/manual-qa/NEO-60.md`](../../docs/manual-qa/NEO-60.md); Bruno: `bruno/neon-sprawl-server/resource-node-definitions/`. + +```bash +curl -sS -i "http://localhost:5253/game/world/resource-node-definitions" +``` + ## Item definitions (NEO-53) **`GET /game/world/item-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`items`**) backed by **`IItemDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Plan: [NEO-53 implementation plan](../../docs/plans/NEO-53-implementation-plan.md); manual QA: [`docs/manual-qa/NEO-53.md`](../../docs/manual-qa/NEO-53.md); Bruno: `bruno/neon-sprawl-server/item-definitions/`.