diff --git a/content/README.md b/content/README.md index eafc903..0b92ab2 100644 --- a/content/README.md +++ b/content/README.md @@ -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). diff --git a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md index 380fd6e..f11ba0d 100644 --- a/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md +++ b/docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md @@ -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** — 4–6 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): 4–6 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) diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 6b59763..831445a 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -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), and **NEO-55** (`GET`/`POST /game/players/{id}/inventory`) moved the register row to **In Progress**; later slices update the alignment table as they land. ### Epic 4 — World Topology diff --git a/docs/plans/E3M1-prototype-backlog.md b/docs/plans/E3M1-prototype-backlog.md new file mode 100644 index 0000000..ed7afad --- /dev/null +++ b/docs/plans/E3M1-prototype-backlog.md @@ -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 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-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) diff --git a/docs/plans/E3M3-prototype-backlog.md b/docs/plans/E3M3-prototype-backlog.md index 2146690..68b21d2 100644 --- a/docs/plans/E3M3-prototype-backlog.md +++ b/docs/plans/E3M3-prototype-backlog.md @@ -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+).