neon-sprawl/docs/plans/NEO-62-implementation-plan.md

11 KiB
Raw Blame History

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-59IResourceNodeDefinitionRegistry + DI (Done); NEO-61IResourceNodeInstanceStore + depletion ops (Done); NEO-54PlayerInventoryOperations (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 / GatherResult types in server/NeonSprawl.Server/Game/Gathering/.
  • Stable deny reasonCode passthrough: node_depleted, inventory_full, unknown_node, plus gather-specific progression_store_missing when XP rollback path runs.
  • Unit / integration tests (AAA) via InMemoryWebApplicationFactory with real catalogs and stores.
  • Document GatherResult shape in module doc (server-internal until promoted to wire).

Out of scope (from Linear + backlog):

  • InteractionApi wiring 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_bulk quantity to player bag.
  • Full bag returns stable inventory_full without partial silent loss or depletion change.
  • Skill XP grant uses same rules as NEO-38 / NEO-41 (salvage, activity, 10 XP via GatherSkillXpConstants).
  • Depletion store decrements atomically with successful grant (decrement last, only when inventory + XP both succeed).

Technical approach

  1. Entry point: GatherOperations.TryGather(...) — static orchestrator accepting playerId, interactableId, and dependencies:

    • IResourceNodeDefinitionRegistry (def + yield),
    • IItemDefinitionRegistry + IPlayerInventoryStore,
    • ISkillDefinitionRegistry, IPlayerSkillProgressionStore, ISkillLevelCurve, PerkUnlockEngine,
    • IResourceNodeInstanceStore.
  2. Normalize key: ResourceNodeInstanceIds.Normalize(interactableId) (trim + lowercase); empty → unknown_node.

  3. Definition + yield resolve: TryGetDefinition and TryGetYield on normalized id. Either miss → Denied + GatherReasonCodes.UnknownNode (no writes).

  4. Capacity pre-check (read-only, no decrement):

    • store.TryEnsureInitialized(key, definition.MaxGathers) (lazy init mirror of NEO-61).
    • store.TryGetRemainingGathers(key, out snapshot) — if remainingGathers <= 0Denied + GatherReasonCodes.NodeDepleted with snapshot (no inventory / XP / decrement).
  5. Inventory grant (first mutation): PlayerInventoryOperations.TryAddStack(playerId, yield.ItemId, yield.Quantity, itemRegistry, inventoryStore).

    • Denied + inventory_full (passthrough) → return immediately; depletion unchanged.
    • StoreMissingGatherReasonCodes.InventoryStoreMissing (gather surface; unlikely in tests).
    • Applied → continue.
  6. Skill XP (second mutation): SkillProgressionGrantOperations.TryApplyGrant with GatherSkillXpConstants.SalvageSkillId, ActivityXpPerResourceNodeGather, ActivitySourceKind.

    • Granted → continue.
    • ProgressionStoreMissing or Deniedcompensating rollback: PlayerInventoryOperations.TryRemoveStack same itemId / quantity; return Denied + GatherReasonCodes.ProgressionStoreMissing (or passthrough skill deny reasonCode from response when Denied). Depletion unchanged.
  7. Depletion commit (last mutation): ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(key, nodeRegistry, store).

    • Expect Applied given step 4 pre-check; on unexpected Denied, attempt compensating inventory remove + surface node_depleted (defensive; document as prototype race risk — no XP rollback in this edge case).
  8. Success envelope — GatherResult:

    • Success = true, ReasonCode = null
    • GrantsApplied: 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.
  9. 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.
  10. Yield quantities (content, fixed): Alpha 1, beta 2, gamma 3, delta 5 scrap_metal_bulk per prototype_resource_yields.json. Tests spot-check alpha (1) and optionally delta (5).

  11. Docs (on land): Update E3_M1 Related implementation slices + GatherResult contract note; documentation_and_implementation_alignment.md E3.M1 row; E3M1-06 checkboxes in E3M1-prototype-backlog.md; brief server/README.md gather-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_alphaSuccess, 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.