13 KiB
E3.M1 — Prototype story backlog (ResourceNodeAndGatherLoop)
Working backlog for Epic 3 — Slice 2 (gather nodes and outputs). Decomposition and contracts: E3.M1 — ResourceNodeAndGatherLoop.
Labels (Linear): every issue E3.M1; add server / Story as listed per issue.
Precursor (do not re-scope): 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 Slice 1 — frozen item catalog, IPlayerInventoryStore, inventory operations (NEO-50–NEO-56). E1.M3 interact + range (NEO-9, NEO-25). E2.M2 grant path (NEO-38).
Prototype gather spine (frozen in E3M1-01): four ResourceNodeDef rows — one per gather lens — each with a fixed ResourceYieldTable entry awarding scrap_metal_bulk (primary material from E3.M3 freeze). World placement stays on PrototypeInteractableRegistry until zone graph (E4.M1) ships.
Linear issues (created): attach docs/plans/NEO-*-implementation-plan.md on the same branch as implementation work.
| Slug | Linear |
|---|---|
| E3M1-01 | NEO-57 |
| E3M1-02 | NEO-58 |
| E3M1-03 | NEO-59 |
| E3M1-04 | NEO-60 |
| E3M1-05 | NEO-61 |
| E3M1-06 | NEO-62 |
| E3M1-07 | NEO-63 |
| E3M1-08 | 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 recipes consume gathered materials; NEO-42 refine XP hook runs after craft success; E4.M2 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) |
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.jsonandcontent/schemas/resource-yield-row.schema.json(two row schemas; catalogs inprototype_resource_nodes.json+prototype_resource_yields.json— NEO-57).content/resource-nodes/prototype_resource_nodes.jsonwith stablenodeDefIdvalues aligned to interactable ids (see module doc freeze table).content/resource-nodes/prototype_resource_yields.json— one yield row pernodeDefId, referencing onlyscrap_metal_bulkfrom the E3.M3 freeze.scripts/validate_content.py: schema validation, duplicate ids, exact four-node allowlist,gatherLenscoverage, cross-check that every yielditemIdexists incontent/items/.- Designer note in E3_M1 +
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
nodeDefIdfails CI. - Every yield references a valid frozen item id.
- Stable id list documented in module doc freeze box.
Landed: 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
IItemDefinitionRegistryat 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
ResourceNodeDefand yield row bynodeDefId.
Landed: NEO-59 — injectable registry + DI; server loader was NEO-58.
E3M1-03 — Resource node definition registry + DI
Goal: Injectable IResourceNodeDefinitionRegistry consumed by gather engine and interact integration.
In scope
ResourceNodeDefinitionRegistryimplementation + 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
nodeDefIddistinguishable 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 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.mdsection.
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 byinteractableId(matches NEO-25 descriptor ids).- Initialize capacity from
ResourceNodeDef.maxGathers(or equivalent field name frozen in E3M1-01). - Decrement on successful gather commit;
node_depletedwhen 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_depletedwithout 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/GatherResulttypes inserver/NeonSprawl.Server/Game/Gathering/.- Stable deny reason codes:
node_depleted,inventory_full,unknown_node, etc. - Unit tests (AAA) with stub inventory + progression stores.
- Document
GatherResultshape in module doc (server-internal until promoted to wire).
Out of scope
InteractionApiwiring (E3M1-07).- Client HUD.
- Regen / respawn timers (E4.M2).
Acceptance criteria
- Successful gather adds configured
scrap_metal_bulkquantity to player bag. - Full bag returns stable
reasonCodewithout 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: onresource_nodekind, call gather engine; map engine deny →InteractionResponsewithAllowed: false+reasonCode(includingnode_depleted).- Remove duplicate XP grant path once gather engine covers it.
- Add three new interactable anchors (positions TBD in plan; keep
prototype_resource_node_alphaanchor). - 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
salvageXP. - Depleted node returns
node_depletedwithout 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.
After this backlog
- Decompose E3.M2 Slice 3 (recipes + craft HTTP) once E3M1-06+ land and inventory holds gathered materials.
- Track delivery in Linear; keep
blockedBylinks synchronized if scope changes. - For each issue kickoff, add
docs/plans/{NEO-XX}-implementation-plan.mdper story-kickoff. - Add
docs/manual-qa/{NEO-XX}.mdwhen interact + inventory surfaces are testable (E3M1-07+).