NEO-57: merge E3.M1 Slice 2 decomposition docs

Bring E3M1-prototype-backlog, E3.M1 freeze table, and register notes
from chore/e3m1-slice2-decomposition onto the story branch.
pull/92/head
VinPropane 2026-05-24 13:47:55 -04:00
commit 17daf9a9ca
5 changed files with 331 additions and 15 deletions

View File

@ -8,9 +8,12 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with **
| [`skills/`](skills/) | Skill catalogs; each row matches [`schemas/skill-def.schema.json`](schemas/skill-def.schema.json) — **stable `id`**, **`allowedXpSourceKinds`** for [E2.M1](../docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md) / [E2.M2](../docs/decomposition/modules/E2_M2_XpAwardAndLevelEngine.md) |
| [`mastery/`](mastery/) | Mastery catalogs; each file matches [`schemas/mastery-catalog.schema.json`](schemas/mastery-catalog.schema.json) — **stable `branchId` / `perkId`**, tier gates via skill level ([E2.M3](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md)) |
| [`items/`](items/) | Item catalogs; each row matches [`schemas/item-def.schema.json`](schemas/item-def.schema.json) — **stable `id`**, **`stackMax`**, **`inventorySlotKind`** for [E3.M3](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) |
| [`resource-nodes/`](resource-nodes/) | Resource node catalogs (planned E3M1-01); each row matches JSON Schema under [`schemas/`](schemas/) — **stable `nodeDefId`**, **`gatherLens`**, yields for [E3.M1](../docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) |
**Prototype Slice 1 (NEO-33):** CI expects **exactly three** skill rows with ids **`salvage`**, **`refine`**, **`intrusion`** (gather + process + tech). Each row must declare **`allowedXpSourceKinds`** (non-empty); see [E2.M1 — Designer note: `allowedXpSourceKinds`](../docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md#designer-note-allowedxpsourcekinds-and-grant-call-sites) for what each kind means for grant wiring.
**Prototype Slice 2 — resource nodes (NEO-57, planned):** CI will expect **exactly four** node defs aligned to [E3.M1 gather lens freeze](../docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md#prototype-slice-2-freeze-e3m1-01); yields reference frozen item ids only. See [E3M1-prototype-backlog.md](../docs/plans/E3M1-prototype-backlog.md).
**Prototype Slice 1 — items (NEO-50):** CI expects **exactly six** item rows with ids **`scrap_metal_bulk`**, **`refined_plate_stock`**, **`field_stim_mk0`**, **`survey_drone_kit`**, **`contract_handoff_token`**, **`prototype_armor_shell`** — one row per **`prototypeRole`** (`material` through `equip_stub`). **Do not rename** ids after ship—change **`displayName`** only. See [E3.M3 — Designer note](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md#designer-note-stack-slot-and-v1-scope) and [freeze table](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md#prototype-slice-1-freeze-neo-50).
**Prototype Slice 4 (NEO-45):** CI expects **exactly one** `MasteryTrack` for **`salvage`** only (`refine` / `intrusion` have no mastery rows yet). Tier 1 @ skill level **2** is a **mutually exclusive** branch pick (`scrap_efficiency` vs `bulk_haul`) with **no** perks; tier 2 @ level **4** unlocks one perk per branch path. **Do not rename** `branchId` / `perkId` after ship—change `displayName` only. See [E2.M3 — Designer note](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#designer-note-tiers-branches-and-level-gates) and [freeze table](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#prototype-slice-4-freeze--salvage-flagship-neo-45).

View File

@ -7,41 +7,97 @@
| **Module ID** | E3.M1 |
| **Epic** | [Epic 3 — Crafting Economy](../epics/epic_03_crafting_economy.md) |
| **Stage target** | Prototype |
| **Status** | In Progress (see [dependency register](module_dependency_register.md); prototype gather XP anchor [NEO-41](../../plans/NEO-41-implementation-plan.md)) |
| **Status** | In Progress — Slice 2 backlog [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)[NEO-64](https://linear.app/neon-sprawl/issue/NEO-64) ([E3M1-prototype-backlog](../../plans/E3M1-prototype-backlog.md); NEO-41 anchor below; see [dependency register](module_dependency_register.md)) |
| **Linear** | Label **`E3.M1`** per [decomposition README — Linear alignment](../README.md#linear-alignment) |
## Purpose
World resource nodes, gather interactions, and yield outputs feeding inventory ([E3.M3](E3_M3_ItemizationAndInventorySchema.md)) and XP ([E2.M2](E2_M2_XpAwardAndLevelEngine.md)).
**Authority:** [Client vs server authority](client_server_authority.md) — the server resolves **whether** a gather succeeds, **what** items are granted, **remaining node capacity**, and **skill XP** from gather; the client sends **interact intent** via [E1.M3 — InteractionAndTargetingLayer](E1_M3_InteractionAndTargetingLayer.md) (`POST …/interact`) and displays outcomes.
World resource nodes, gather interactions, and yield outputs feeding inventory ([E3.M3](E3_M3_ItemizationAndInventorySchema.md)) and skill XP ([E2.M2](E2_M2_XpAwardAndLevelEngine.md)). Ecology / respawn pacing per zone defers to [E4.M2 — SpawnEcologyController](E4_M2_SpawnEcologyController.md).
## Boundary: E1.M3 vs E3.M1
| Topic | E1.M3 (today) | E3.M1 (this module) |
|--------|----------------|---------------------|
| **Primary job** | Interactable eligibility (range, known id, kind) | **Gather resolution**: yields, depletion, inventory + XP side effects |
| **Wire / HTTP** | `POST …/interact` + `InteractionResponse` | No separate gather route in prototype — E3.M1 hooks **after** E1.M3 allows interact |
| **Registry** | `PrototypeInteractableRegistry` / `GET …/interactables` | **`ResourceNodeDef`** + **`ResourceYieldTable`** content; instance capacity store |
| **Denials** | `out_of_range`, `unknown_interactable` | Adds **`node_depleted`**, `inventory_full`, etc. via gather engine |
**Shared policy:** horizontal reach on **X/Z** matches [NEO-9](../../plans/NEO-9-implementation-plan.md) / E1.M3 — gather does not redefine range math.
## Responsibilities
- Spawn/manage nodes per `ResourceNodeDef`; resolve `GatherResult` via `ResourceYieldTable`; integrate interaction range via [E1.M3](E1_M3_InteractionAndTargetingLayer.md).
- Author **`ResourceNodeDef`** and **`ResourceYieldTable`** content; load and expose defs to server subsystems.
- Track per-instance **remaining gathers** (prototype capacity/depletion); deny when exhausted.
- Resolve **`GatherResult`**: item grants into [E3.M3](E3_M3_ItemizationAndInventorySchema.md) inventory, **`salvage`** skill XP via [E2.M2](E2_M2_XpAwardAndLevelEngine.md) grant operations.
- Integrate with **`resource_node`** interactables in `PrototypeInteractableRegistry` (world anchors until [E4.M1](E4_M1_ZoneGraphAndTravelRules.md)).
**Deferred:** zone spawn profiles, regen timers, probabilistic yields, cross-skill unlock nodes — [gathering.md](../../game-design/gathering.md) open questions.
## Key contracts
| Contract | Role |
|----------|------|
| `ResourceNodeDef` | Node type, capacity, regen. |
| `GatherResult` | Items and depletion state. |
| `ResourceYieldTable` | Probabilities or fixed yields. |
| Contract | Kind | Role |
|----------|------|------|
| `ResourceNodeDef` | Content | Node type, max gathers, gather lens, link to yield table. |
| `ResourceYieldTable` | Content | Fixed item outputs per gather (`itemId`, quantity). |
| `GatherResult` | Server-internal (prototype) | Success/deny, granted items, depletion snapshot, reason codes. |
### Contract notes ([contracts.md](contracts.md))
- **Content** defs live under `content/resource-nodes/` with JSON Schema in CI ([E3M1-01](../../plans/E3M1-prototype-backlog.md#e3m1-01--prototype-resourcenodedef--yield-tables--schemas--ci)).
- **`GatherResult`** may promote to **wire** JSON or Protobuf when the client needs explicit gather payloads; until then, prototype uses **`InteractionResponse`** + inventory GET for verification.
- **Denials:** stable **`reasonCode`** strings on interact deny (`node_depleted`, `inventory_full`, …) consistent with [E1.M3](E1_M3_InteractionAndTargetingLayer.md) patterns.
**Logical fields (illustrative):**
| Contract | Illustrative fields |
|----------|---------------------|
| `ResourceNodeDef` | `nodeDefId`; `gatherLens` (`consumer_salvage`, `subsurface`, `bio`, `urban_bulk`); `maxGathers`; `yieldTableId` or embedded yields |
| `ResourceYieldTable` | `nodeDefId`; `grants[]` with `itemId`, `quantity` |
| `GatherResult` | `success`; `reasonCode`; `grantsApplied[]`; `remainingGathers`; optional `xpGrantSummary` |
## Module dependencies
- **E1.M3** — InteractionAndTargetingLayer.
- **E2.M2** — XpAwardAndLevelEngine.
- **E1.M3** — InteractionAndTargetingLayer (`POST …/interact`, interactable descriptors).
- **E2.M2** — XpAwardAndLevelEngine (skill XP grant operations).
- **E3.M3** — ItemizationAndInventorySchema (inventory grants) — logical dependency for Slice 2 yields; register lists E1.M3 + E2.M2 only so gather XP anchor ([NEO-41](../../plans/NEO-41-implementation-plan.md)) could land before inventory.
## Dependents (by design)
- **E3.M2** — RefinementAndRecipeExecution (inputs).
- **E4.M2** — SpawnEcologyController.
- **E3.M2** — RefinementAndRecipeExecution (crafted inputs from gathered materials).
- **E4.M2** — SpawnEcologyController (respawn / ecology).
## Designer note: gather lenses and yields
Prototype Slice 2 uses **four** nodes — one per [gather lens](../../game-design/gathering.md#node-lenses-design-facing). All prototype yields grant **`scrap_metal_bulk`** (different quantities per node are allowed) so the gather→refine loop can start without expanding the item catalog. **Skill XP** stays **`salvage`** + **`activity`** (10 XP) on every node until per-lens skill mapping is a deliberate content pass.
**Depletion:** each interactable instance has **`maxGathers`** successful collects; at zero, interact returns **`node_depleted`**. Regen on timer is **out of scope** for Slice 2 ([E4.M2](E4_M2_SpawnEcologyController.md)).
## Prototype Slice 2 freeze (E3M1-01)
The **first shipped four-node spine** is **frozen** for downstream references (recipes, ecology, quests). **`nodeDefId`** must match **`interactableId`** in `PrototypeInteractableRegistry` for prototype wiring.
| `nodeDefId` / `interactableId` | `gatherLens` | Notes |
|-------------------------------|--------------|--------|
| **`prototype_resource_node_alpha`** | `consumer_salvage` | Existing NEO-9/NEO-41 anchor (12, 6) |
| **`prototype_subsurface_vein_beta`** | `subsurface` | New anchor — plan PR sets X/Z |
| **`prototype_bio_mat_gamma`** | `bio` | New anchor — plan PR sets X/Z |
| **`prototype_urban_bulk_delta`** | `urban_bulk` | New anchor — plan PR sets X/Z |
**Rules:** Do **not** rename these ids after ship. CI enforces **exactly four** node defs and valid yield item references. Relaxing the gate belongs in a new Linear issue when the roster grows.
## Related implementation slices
Epic 3 **Slice 2** — 46 node types; `resource_gathered`, `gather_node_depleted`.
Epic 3 **Slice 2**[gather nodes and outputs](../epics/epic_03_crafting_economy.md#epic-3-slice-2): 46 node types, yield tables, `resource_gathered`, `gather_node_depleted`.
**Prototype implementation anchor (NEO-41):** until **`GatherResult`** / yield tables exist, the repo treats a **successful** **`POST …/interact`** on an entry with **`kind: resource_node`** (`prototype_resource_node_alpha`) as **gather completion** for **skill XP** only — **`salvage`** + **`activity`** via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) (same rules as NEO-38). Migrate this hook when a dedicated gather pipeline lands.
**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)).
**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
- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Epic 3.
- [gathering.md](../../game-design/gathering.md)
- [Module dependency register](module_dependency_register.md)

View File

@ -48,6 +48,8 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen
| E3.M4 | SinkAndDurabilityLifecycle | E3.M3, E8.M3 | DurabilityState, ItemSinkEvent, RepairCostRule | Pre-production | Planned |
| E3.M5 | EconomyBalancePolicy | E3.M4, E9.M2 | EconomyPolicy, PriceBandRule, FaucetSinkRatio | Pre-production | Planned |
**E3.M1 note:** Epic 3 **Slice 2** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) → [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64); label **`E3.M1`**. See [E3M1-prototype-backlog.md](../../plans/E3M1-prototype-backlog.md), [E3_M1_ResourceNodeAndGatherLoop.md](E3_M1_ResourceNodeAndGatherLoop.md). **NEO-41** (prototype gather XP on `resource_node` interact) remains until **E3M1-07** migrates into the gather engine. Slice 2 adds **`ResourceNodeDef`**, **`ResourceYieldTable`**, **`GatherResult`**, depletion store, inventory grants, and telemetry hooks **`resource_gathered`** / **`gather_node_depleted`**.
**E3.M3 note:** Epic 3 **Slice 1** backlog in Linear ([Epic 3 — Crafting, Gathering, and Itemization Economy](https://linear.app/neon-sprawl/project/epic-3-crafting-gathering-and-itemization-economy-65785ed05bc2)): [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) → [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56); label **`E3.M3`**. See [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md), [E3_M3_ItemizationAndInventorySchema.md](E3_M3_ItemizationAndInventorySchema.md). **NEO-50** (content + CI), **NEO-51** (server fail-fast load), **NEO-52** (`IItemDefinitionRegistry` + DI), **NEO-53** (`GET /game/world/item-definitions`), **NEO-54** (inventory store + stack/slot rules engine), **NEO-55** (`GET`/`POST /game/players/{id}/inventory`), and **NEO-56** (comment-only `item_created` / `inventory_transfer_denied` telemetry hook sites in `PlayerInventoryOperations`) — Epic 3 Slice 1 backlog **E3M3-01****E3M3-07** complete; register row **In Progress** until Slice 2+; later slices update the alignment table as they land.
### Epic 4 — World Topology

View File

@ -0,0 +1,255 @@
# E3.M1 — Prototype story backlog (ResourceNodeAndGatherLoop)
Working backlog for **Epic 3 — Slice 2** ([gather nodes and outputs](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-2)). Decomposition and contracts: [E3.M1 — ResourceNodeAndGatherLoop](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md).
**Labels (Linear):** every issue **`E3.M1`**; add **`server`** / **`Story`** as listed per issue.
**Precursor (do not re-scope):** [NEO-41](https://linear.app/neon-sprawl/issue/NEO-41) grants **`salvage`** skill XP on successful **`POST …/interact`** when interactable **`kind`** is **`resource_node`** — **no** `GatherResult`, yields, inventory, or depletion yet. Slice 2 **migrates** XP into the gather engine and adds item grants + node capacity.
**Upstream (must be landed):** [E3.M3](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) Slice 1 — frozen item catalog, **`IPlayerInventoryStore`**, inventory operations ([NEO-50](https://linear.app/neon-sprawl/issue/NEO-50)[NEO-56](https://linear.app/neon-sprawl/issue/NEO-56)). **E1.M3** interact + range ([NEO-9](https://linear.app/neon-sprawl/issue/NEO-9), [NEO-25](https://linear.app/neon-sprawl/issue/NEO-25)). **E2.M2** grant path ([NEO-38](https://linear.app/neon-sprawl/issue/NEO-38)).
**Prototype gather spine (frozen in E3M1-01):** **four** `ResourceNodeDef` rows — one per [gather lens](../game-design/gathering.md#node-lenses-design-facing) — each with a fixed **`ResourceYieldTable`** entry awarding **`scrap_metal_bulk`** (primary material from [E3.M3 freeze](../decomposition/modules/E3_M3_ItemizationAndInventorySchema.md#prototype-slice-1-freeze-neo-50)). World placement stays on **`PrototypeInteractableRegistry`** until zone graph ([E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md)) ships.
**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the same branch as implementation work.
| Slug | Linear |
|------|--------|
| E3M1-01 | [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) |
| E3M1-02 | [NEO-58](https://linear.app/neon-sprawl/issue/NEO-58) |
| E3M1-03 | [NEO-59](https://linear.app/neon-sprawl/issue/NEO-59) |
| E3M1-04 | [NEO-60](https://linear.app/neon-sprawl/issue/NEO-60) |
| E3M1-05 | [NEO-61](https://linear.app/neon-sprawl/issue/NEO-61) |
| E3M1-06 | [NEO-62](https://linear.app/neon-sprawl/issue/NEO-62) |
| E3M1-07 | [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63) |
| E3M1-08 | [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64) |
**Dependency graph in Linear:** E3M1-02 blocked by E3M1-01. E3M1-03 blocked by E3M1-02. E3M1-04 blocked by E3M1-03. E3M1-05 blocked by E3M1-03. E3M1-06 blocked by E3M1-05 and E3M1-03 (inventory from E3.M3). E3M1-07 blocked by E3M1-06. E3M1-08 blocked by E3M1-06 (may parallel E3M1-07).
---
## Story order (recommended)
| Order | Slug | Depends on |
|-------|------|------------|
| 1 | **E3M1-01** | E3.M3 item catalog (NEO-50) |
| 2 | **E3M1-02** | E3M1-01 |
| 3 | **E3M1-03** | E3M1-02 |
| 4 | **E3M1-04** | E3M1-03 |
| 5 | **E3M1-05** | E3M1-03 |
| 6 | **E3M1-06** | E3M1-05, E3.M3 inventory store (NEO-54+) |
| 7 | **E3M1-07** | E3M1-06 |
| 8 | **E3M1-08** | E3M1-06 |
**Downstream (separate modules):** [E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) recipes consume gathered materials; [NEO-42](https://linear.app/neon-sprawl/issue/NEO-42) refine XP hook runs after craft success; [E4.M2](../decomposition/modules/E4_M2_SpawnEcologyController.md) owns respawn/ecology pacing later.
---
## Kickoff decisions (decomposition defaults)
| Topic | Decision | Rationale |
|-------|----------|-----------|
| Node count | **4** frozen defs (epic allows 46) | One per gather lens; enough variety without bloating prototype registry |
| Gather wire surface | Keep **`POST …/interact`** ([E1.M3](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md)) | No separate gather HTTP in prototype; E3.M1 owns resolution **after** interact eligibility |
| Yields | **Fixed** quantities per gather (no RNG) | Deterministic tests; probability tables are pre-production |
| Depleted node | **`Allowed: false`** + **`reasonCode: node_depleted`** | Prevents “free” taps; clearer than silent no-op |
| XP | **Migrate** NEO-41 inline grant into gather engine on successful yield | Single call site for gather completion side effects |
| Skill per node | All prototype nodes award **`salvage`** + **`activity`** (10 XP) | Matches NEO-41; per-lens skill mapping is content follow-up |
---
### E3M1-01 — Prototype ResourceNodeDef + yield tables + schemas + CI
**Goal:** Lock content shape and CI validation for **four** frozen resource nodes and their **fixed** yield tables before server load.
**In scope**
- `content/schemas/resource-node-def.schema.json` and `content/schemas/resource-yield-table.schema.json` (or a single catalog schema if simpler — pick one in the plan PR and document).
- `content/resource-nodes/prototype_resource_nodes.json` with stable **`nodeDefId`** values aligned to interactable ids (see module doc freeze table).
- `content/resource-nodes/prototype_resource_yields.json` (or embedded yields — plan decides) referencing only **`scrap_metal_bulk`** from the E3.M3 freeze.
- `scripts/validate_content.py`: schema validation, duplicate ids, exact four-node allowlist, **`gatherLens`** coverage, cross-check that every yield **`itemId`** exists in `content/items/`.
- Designer note in [E3_M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) + `content/README.md`.
**Out of scope**
- Server loader, depletion store, inventory grants, HTTP.
**Acceptance criteria**
- [ ] PR gate validates resource-node JSON against schema.
- [ ] Exactly four prototype node ids; duplicate `nodeDefId` fails CI.
- [ ] Every yield references a valid frozen item id.
- [ ] Stable id list documented in module doc freeze box.
---
### E3M1-02 — Server resource-node catalog load (fail-fast)
**Goal:** Disk → host: startup load of `content/resource-nodes/*.json` with CI-parity validation.
**In scope**
- Loader + catalog types under `server/NeonSprawl.Server/Game/Gathering/` (path TBD in plan).
- Fail-fast on malformed files, duplicate ids, unknown yield item ids (cross-check **`IItemDefinitionRegistry`** at startup).
- Unit tests (AAA) for loader happy path and failure modes.
**Out of scope**
- Per-instance depletion, HTTP, interact wiring.
**Acceptance criteria**
- [ ] Server refuses boot when resource-node catalog invalid (mirror E3.M3 / E2.M1 catalog behavior).
- [ ] Catalog resolves `ResourceNodeDef` and yield row by `nodeDefId`.
---
### E3M1-03 — Resource node definition registry + DI
**Goal:** Injectable `IResourceNodeDefinitionRegistry` consumed by gather engine and interact integration.
**In scope**
- `ResourceNodeDefinitionRegistry` implementation + DI registration.
- Unit tests (AAA): lookup, unknown id, yield metadata, capacity fields.
**Out of scope**
- HTTP, persistence of depletion state.
**Acceptance criteria**
- [ ] Host resolves registry from DI; unknown `nodeDefId` distinguishable from valid ids in tests.
---
### E3M1-04 — GET world resource-node-definitions
**Goal:** Versioned read-only catalog for Bruno, client preview, and tooling — mirror [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) item-definitions pattern.
**In scope**
- `GET /game/world/resource-node-definitions` + DTOs + API tests (AAA).
- Bruno folder `bruno/neon-sprawl-server/resource-node-definitions/`.
- `server/README.md` section.
**Out of scope**
- Per-instance remaining capacity (instance state is E3M1-05).
**Acceptance criteria**
- [ ] GET returns all four prototype defs with schema version field and yield summary fields needed for QA.
- [ ] Bruno happy path documented.
---
### E3M1-05 — World node instance depletion store
**Goal:** Server-authoritative **remaining gathers** per node instance (`interactableId`), with structured deny when depleted.
**In scope**
- `IResourceNodeInstanceStore` (or equivalent) keyed by **`interactableId`** (matches [NEO-25](https://linear.app/neon-sprawl/issue/NEO-25) descriptor ids).
- Initialize capacity from `ResourceNodeDef.maxGathers` (or equivalent field name frozen in E3M1-01).
- Decrement on successful gather commit; **`node_depleted`** when zero.
- Persistence: in-memory + Postgres + migration (mirror NEO-54 / NEO-8 policy).
- Unit + integration tests (AAA).
**Out of scope**
- Item grants, skill XP, HTTP beyond store tests.
**Acceptance criteria**
- [ ] First gather on a fresh node succeeds; repeated gathers until capacity returns **`node_depleted`** without going negative.
- [ ] Postgres + in-memory parity tests.
---
### E3M1-06 — GatherResult engine (yields + inventory + skill XP)
**Goal:** Single server operation that resolves **`GatherResult`**: validate node + capacity, grant items via **`PlayerInventoryOperations`**, award **`salvage`** XP via **`SkillProgressionGrantOperations`**, commit depletion.
**In scope**
- `GatherOperations` / `GatherResult` types in `server/NeonSprawl.Server/Game/Gathering/`.
- Stable deny reason codes: `node_depleted`, `inventory_full`, `unknown_node`, etc.
- Unit tests (AAA) with stub inventory + progression stores.
- Document **`GatherResult`** shape in module doc (server-internal until promoted to wire).
**Out of scope**
- `InteractionApi` wiring (E3M1-07).
- Client HUD.
- Regen / respawn timers ([E4.M2](../decomposition/modules/E4_M2_SpawnEcologyController.md)).
**Acceptance criteria**
- [ ] Successful gather adds configured **`scrap_metal_bulk`** quantity to player bag.
- [ ] Full bag returns stable `reasonCode` without partial silent loss.
- [ ] Skill XP grant uses same rules as NEO-38 / NEO-41 (`salvage`, `activity`, 10 XP).
- [ ] Depletion store decrements atomically with successful grant.
---
### E3M1-07 — Interact path integration + registry expansion + Bruno
**Goal:** Replace NEO-41 inline XP block with gather engine; register **four** prototype resource nodes in **`PrototypeInteractableRegistry`**; extend Bruno/manual QA for gather → inventory.
**In scope**
- `InteractionApi`: on `resource_node` kind, call gather engine; map engine deny → `InteractionResponse` with **`Allowed: false`** + `reasonCode` (including **`node_depleted`**).
- Remove duplicate XP grant path once gather engine covers it.
- Add three new interactable anchors (positions TBD in plan; keep **`prototype_resource_node_alpha`** anchor).
- Bruno: move near node → interact → GET inventory shows **`scrap_metal_bulk`**; depleted node deny.
- `server/README.md`; integration tests (AAA).
**Out of scope**
- Godot gather HUD (optional follow-up).
- New gather-specific HTTP route.
**Acceptance criteria**
- [ ] In-range interact on fresh node increases inventory quantity and **`salvage`** XP.
- [ ] Depleted node returns **`node_depleted`** without inventory or XP change.
- [ ] Terminal interact unchanged (no gather side effects).
- [ ] Bruno documents happy path + depletion deny for at least one node.
---
### E3M1-08 — `resource_gathered` / `gather_node_depleted` telemetry hook sites
**Goal:** Comment-only hooks on gather success and depletion for future E9.M1 catalog events.
**In scope**
- Hook placement in gather engine (and interact deny branch if applicable).
- `TODO(E9.M1)` comments; no production logging.
- README + module doc pointer.
**Out of scope**
- Telemetry ingest, dashboards.
**Acceptance criteria**
- [ ] Hook sites documented in code and `server/README.md`.
- [ ] Matches Epic 3 Slice 2 telemetry vocabulary in [epic_03](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-2).
---
## After this backlog
- Decompose **[E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md)** Slice 3 (recipes + craft HTTP) once E3M1-06+ land and inventory holds gathered materials.
- Track delivery in Linear; keep `blockedBy` links synchronized if scope changes.
- For each issue kickoff, add `docs/plans/{NEO-XX}-implementation-plan.md` per [story-kickoff](../../.cursor/rules/story-kickoff.md).
- Add `docs/manual-qa/{NEO-XX}.md` when interact + inventory surfaces are testable (E3M1-07+).
## Related docs
- [E3_M1_ResourceNodeAndGatherLoop.md](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md)
- [E3M3-prototype-backlog.md](E3M3-prototype-backlog.md)
- [epic_03_crafting_economy.md](../decomposition/epics/epic_03_crafting_economy.md)
- [gathering.md](../game-design/gathering.md)
- [NEO-41 implementation plan](NEO-41-implementation-plan.md)

View File

@ -196,7 +196,7 @@ Working backlog for **Epic 3 — Slice 1** ([items and inventory MVP](../decompo
## After this backlog
- Decompose **[E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md)** Slice 2 (gather yields → inventory) once E3M3-05+ land.
- **[E3.M1](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md)** Slice 2 decomposed — [E3M1-prototype-backlog.md](E3M1-prototype-backlog.md) ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)[NEO-64](https://linear.app/neon-sprawl/issue/NEO-64)).
- Track delivery in Linear; keep `blockedBy` links synchronized if scope changes.
- For each issue kickoff, add `docs/plans/{NEO-XX}-implementation-plan.md` per [story-kickoff](../../.cursor/rules/story-kickoff.md).
- Add `docs/manual-qa/{NEO-XX}.md` when HTTP surfaces land (E3M3-06+).