11 KiB
NEO-62 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-62 |
| Title | E3.M1: GatherResult engine (yields + inventory + skill XP) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-62/e3m1-gatherresult-engine-yields-inventory-skill-xp |
| Module | E3.M1 — ResourceNodeAndGatherLoop · Epic 3 Slice 2 · backlog E3M1-06 |
| Branch | NEO-62-e3m1-gatherresult-engine-yields-inventory-skill-xp |
| Precursors | NEO-59 — IResourceNodeDefinitionRegistry + DI (Done); NEO-61 — IResourceNodeInstanceStore + depletion ops (Done); NEO-54 — PlayerInventoryOperations (Done); NEO-38 / NEO-41 — skill XP grant path (Done) |
| Pattern | NEO-61 — static *Operations orchestrator over injectable stores; NEO-54 — subsystem reason codes surfaced on gather deny |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Side-effect order | Inventory / XP / depletion commit sequence? | Inventory add → XP grant → depletion decrement last — depletion only after successful grant; avoids consuming capacity on inventory_full (NEO-54 all-or-nothing). |
User: inventory → XP → depletion last. |
| XP failure after inventory | Roll back items or partial success? | Deny gather + compensating TryRemoveStack — gather success must include items and XP per AC; NEO-41 silent XP no-op stays on interact until NEO-63. |
User: rollback + deny. |
Goal, scope, and out-of-scope
Goal: Single server operation resolving GatherResult: validate node + capacity, grant configured yield via PlayerInventoryOperations, award salvage XP via SkillProgressionGrantOperations, commit depletion via ResourceNodeInstanceOperations — engine-only, ready for interact wiring (NEO-63).
In scope (from Linear + E3M1-06):
GatherOperations/GatherResulttypes inserver/NeonSprawl.Server/Game/Gathering/.- Stable deny
reasonCodepassthrough:node_depleted,inventory_full,unknown_node, plus gather-specificprogression_store_missingwhen XP rollback path runs. - Unit / integration tests (AAA) via
InMemoryWebApplicationFactorywith real catalogs and stores. - Document
GatherResultshape in module doc (server-internal until promoted to wire).
Out of scope (from Linear + backlog):
InteractionApiwiring and duplicate NEO-41 XP removal (NEO-63).- Client HUD, Bruno, HTTP gather route.
- Regen / respawn timers (E4.M2).
- Telemetry hook sites (NEO-64).
- Cross-store transactions (prototype relies on ordered commits + compensating remove on XP failure).
Acceptance criteria checklist
- Successful gather adds configured
scrap_metal_bulkquantity to player bag. - Full bag returns stable
inventory_fullwithout partial silent loss or depletion change. - Skill XP grant uses same rules as NEO-38 / NEO-41 (
salvage,activity, 10 XP viaGatherSkillXpConstants). - Depletion store decrements atomically with successful grant (decrement last, only when inventory + XP both succeed).
Technical approach
-
Entry point:
GatherOperations.TryGather(...)— static orchestrator acceptingplayerId,interactableId, and dependencies:IResourceNodeDefinitionRegistry(def + yield),IItemDefinitionRegistry+IPlayerInventoryStore,ISkillDefinitionRegistry,IPlayerSkillProgressionStore,ISkillLevelCurve,PerkUnlockEngine,IResourceNodeInstanceStore.
-
Normalize key:
ResourceNodeInstanceIds.Normalize(interactableId)(trim + lowercase); empty →unknown_node. -
Definition + yield resolve:
TryGetDefinitionandTryGetYieldon normalized id. Either miss →Denied+GatherReasonCodes.UnknownNode(no writes). -
Capacity pre-check (read-only, no decrement):
store.TryEnsureInitialized(key, definition.MaxGathers)(lazy init mirror of NEO-61).store.TryGetRemainingGathers(key, out snapshot)— ifremainingGathers <= 0→Denied+GatherReasonCodes.NodeDepletedwith snapshot (no inventory / XP / decrement).
-
Inventory grant (first mutation):
PlayerInventoryOperations.TryAddStack(playerId, yield.ItemId, yield.Quantity, itemRegistry, inventoryStore).Denied+inventory_full(passthrough) → return immediately; depletion unchanged.StoreMissing→GatherReasonCodes.InventoryStoreMissing(gather surface; unlikely in tests).Applied→ continue.
-
Skill XP (second mutation):
SkillProgressionGrantOperations.TryApplyGrantwithGatherSkillXpConstants.SalvageSkillId,ActivityXpPerResourceNodeGather,ActivitySourceKind.Granted→ continue.ProgressionStoreMissingorDenied→ compensating rollback:PlayerInventoryOperations.TryRemoveStacksameitemId/quantity; returnDenied+GatherReasonCodes.ProgressionStoreMissing(or passthrough skill denyreasonCodefrom response whenDenied). Depletion unchanged.
-
Depletion commit (last mutation):
ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(key, nodeRegistry, store).- Expect
Appliedgiven step 4 pre-check; on unexpectedDenied, attempt compensating inventory remove + surfacenode_depleted(defensive; document as prototype race risk — no XP rollback in this edge case).
- Expect
-
Success envelope —
GatherResult:Success = true,ReasonCode = nullGrantsApplied: single entry{ itemId, quantity }from yield row (prototype: one yield per node).RemainingGathers: from depletion outcome snapshot.XpGrantSummary: optional compact summary from grant response (e.g.skillId,amount,sourceKind) for tests / future wire.
-
Reason codes (
GatherReasonCodes): Re-export stable subsystem strings where applicable:unknown_node,node_depleted,inventory_full— same literals as NEO-61 / NEO-54.progression_store_missing,inventory_store_missing— gather-layer codes for store-missing paths.
-
Yield quantities (content, fixed): Alpha 1, beta 2, gamma 3, delta 5
scrap_metal_bulkperprototype_resource_yields.json. Tests spot-check alpha (1) and optionally delta (5). -
Docs (on land): Update E3_M1 Related implementation slices +
GatherResultcontract note; documentation_and_implementation_alignment.md E3.M1 row; E3M1-06 checkboxes in E3M1-prototype-backlog.md; briefserver/README.mdgather-engine subsection (engine-only; interact in NEO-63).
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Gathering/GatherReasonCodes.cs |
Stable gather deny codes (passthrough + store-missing). |
server/NeonSprawl.Server/Game/Gathering/GatherGrantApplied.cs |
One applied yield row in success envelope. |
server/NeonSprawl.Server/Game/Gathering/GatherXpGrantSummary.cs |
Compact XP summary on successful gather. |
server/NeonSprawl.Server/Game/Gathering/GatherResult.cs |
Success/deny envelope: grants, remainingGathers, reasonCode, xp summary. |
server/NeonSprawl.Server/Game/Gathering/GatherOperations.cs |
Orchestrator: pre-check → inventory → XP → depletion; rollback on XP failure. |
server/NeonSprawl.Server.Tests/Game/Gathering/GatherOperationsTests.cs |
AAA: success path, inventory_full, node_depleted, unknown_node, XP rollback, quantity per yield. |
Files to modify
| Path | Rationale |
|---|---|
server/README.md |
Gather engine subsection: GatherOperations, commit order, reason codes; note interact wiring in NEO-63. |
docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md |
Related implementation slices — gather engine bullet (NEO-62); GatherResult field table alignment. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E3.M1 row — note NEO-62 gather engine when landed. |
docs/plans/E3M1-prototype-backlog.md |
E3M1-06 acceptance checkboxes when landed. |
Tests
| Test file | What it covers |
|---|---|
server/NeonSprawl.Server.Tests/Game/Gathering/GatherOperationsTests.cs |
Integration-style (DI factory): TryGather on prototype_resource_node_alpha — Success, bag quantity +1 scrap_metal_bulk, salvage XP +10, RemainingGathers == 9. Full bag: pre-fill 24 bag slots (survey_drone_kit ×1 each) → Denied, inventory_full, depletion row unchanged (still lazy-uninitialized or full capacity). Depletion loop: exhaust alpha (10 gathers) → next node_depleted, no inventory/XP change. unknown_node for bogus id. XP failure: custom test factory or stub progression store that returns ProgressionStoreMissing after inventory add → gather denied, inventory restored to pre-gather snapshot. Optional: delta node grants quantity 5. AAA per csharp-style. |
No Bruno or HTTP tests — out of scope; manual gather flow waits for NEO-63.
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
NEO-61 on main |
Branch from main where NEO-59/60/61 are merged (confirmed at kickoff). |
adopted |
Duplicate XP via InteractionApi |
Leave NEO-41 interact hook until NEO-63 removes it; engine-only scope for NEO-62. | adopted |
| Depletion fails after inventory+XP | Extremely unlikely after pre-check; defensive inventory rollback, no XP undo — document; no cross-store transaction in prototype. | deferred |
| Per-lens yield quantities in tests | Spot-check alpha (1) + delta (5); full four-node matrix optional. | adopted |
None blocking.
Decisions (kickoff)
- Commit order: inventory add → XP grant → depletion decrement last.
- XP failure: deny gather + compensating
TryRemoveStack; depletion not committed. - Reason codes: passthrough subsystem literals on gather deny surface; gather-specific codes for store-missing only.