# 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 4–6) | 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-row.schema.json` (two row schemas; catalogs in `prototype_resource_nodes.json` + `prototype_resource_yields.json` — [NEO-57](NEO-57-implementation-plan.md)).
- `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` — one yield row per `nodeDefId`, 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**

- [x] PR gate validates resource-node JSON against schema.
- [x] Exactly four prototype node ids; duplicate `nodeDefId` fails CI.
- [x] Every yield references a valid frozen item id.
- [x] Stable id list documented in module doc freeze box.

**Landed:** [NEO-57](https://linear.app/neon-sprawl/issue/NEO-57) — content + CI only; server load is E3M1-02.

---

### 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**

- [x] Server refuses boot when resource-node catalog invalid (mirror E3.M3 / E2.M1 catalog behavior).
- [x] Catalog resolves `ResourceNodeDef` and yield row by `nodeDefId`.

**Landed:** [NEO-59](https://linear.app/neon-sprawl/issue/NEO-59) — injectable registry + DI; server loader was [NEO-58](https://linear.app/neon-sprawl/issue/NEO-58).

---

### 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**

- [x] 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**

- [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.

---

### 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**

- [x] First gather on a fresh node succeeds; repeated gathers until capacity returns **`node_depleted`** without going negative.
- [x] Postgres + in-memory parity tests.

**Landed:** [NEO-61](https://linear.app/neon-sprawl/issue/NEO-61) — instance depletion store; gather engine is E3M1-06.

---

### 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**

- [x] Successful gather adds configured **`scrap_metal_bulk`** quantity to player bag.
- [x] Full bag returns stable `reasonCode` without partial silent loss.
- [x] Skill XP grant uses same rules as NEO-38 / NEO-41 (`salvage`, `activity`, 10 XP).
- [x] Depletion store decrements atomically with successful grant.

**Landed:** [NEO-62](https://linear.app/neon-sprawl/issue/NEO-62) — gather engine; interact wiring is E3M1-07.

### 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 — **[Epic 3 Slice 5](E3S5-client-prototype-backlog.md#e3s5-02--client-gather-feedback-on-interact-e3m1)** ([NEO-73](https://linear.app/neon-sprawl/issue/NEO-73)).
- New gather-specific HTTP route.

**Acceptance criteria**

- [x] In-range interact on fresh node increases inventory quantity and **`salvage`** XP.
- [x] Depleted node returns **`node_depleted`** without inventory or XP change.
- [x] Terminal interact unchanged (no gather side effects).
- [x] Bruno documents happy path + depletion deny for at least one node.

**Landed:** [NEO-63](https://linear.app/neon-sprawl/issue/NEO-63) — interact gather wiring; telemetry is E3M1-08.

---

### 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 — **out of scope per NEO-64 kickoff:** engine-only; no **`InteractionApi`** hooks).
- `TODO(E9.M1)` comments; no production logging.
- README + module doc pointer.

**Out of scope**

- Telemetry ingest, dashboards.
- Interact deny-branch hook sites (**NEO-64 kickoff** — pre-gather **`node_depleted`** deny uses interact **`reasonCode`** only).

**Acceptance criteria**

- [x] Hook sites documented in code and `server/README.md`.
- [x] Matches Epic 3 Slice 2 telemetry vocabulary in [epic_03](../decomposition/epics/epic_03_crafting_economy.md#epic-3-slice-2).

**Landed:** [NEO-64](https://linear.app/neon-sprawl/issue/NEO-64) — comment-only **`resource_gathered`** / **`gather_node_depleted`** hooks in **`GatherOperations.TryGather`**; Epic 3 Slice 2 / E3.M1 backlog complete.

---

## After this backlog

- **[E3.M2](../decomposition/modules/E3_M2_RefinementAndRecipeExecution.md)** Slice 3 decomposed — [E3M2-prototype-backlog.md](E3M2-prototype-backlog.md) ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)–[NEO-71](https://linear.app/neon-sprawl/issue/NEO-71)).
- **Epic 3 Slice 5 (client)** — [E3S5-client-prototype-backlog.md](E3S5-client-prototype-backlog.md) ([NEO-72](https://linear.app/neon-sprawl/issue/NEO-72)–[NEO-75](https://linear.app/neon-sprawl/issue/NEO-75)); gather HUD is **E3S5-02** [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73).
- 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)
