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

134 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) · Epic 3 Slice 2 · backlog **E3M1-06** |
| **Branch** | `NEO-62-e3m1-gatherresult-engine-yields-inventory-skill-xp` |
| **Precursors** | [NEO-59](https://linear.app/neon-sprawl/issue/NEO-59) — `IResourceNodeDefinitionRegistry` + DI (**Done**); [NEO-61](https://linear.app/neon-sprawl/issue/NEO-61) — `IResourceNodeInstanceStore` + depletion ops (**Done**); [NEO-54](https://linear.app/neon-sprawl/issue/NEO-54) — `PlayerInventoryOperations` (**Done**); [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) / [NEO-41](https://linear.app/neon-sprawl/issue/NEO-41) — skill XP grant path (**Done**) |
| **Pattern** | [NEO-61](https://linear.app/neon-sprawl/issue/NEO-61) — static `*Operations` orchestrator over injectable stores; [NEO-54](https://linear.app/neon-sprawl/issue/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](E3M1-prototype-backlog.md#e3m1-06--gatherresult-engine-yields--inventory--skill-xp)):**
- **`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](https://linear.app/neon-sprawl/issue/NEO-63)).
- Client HUD, Bruno, HTTP gather route.
- Regen / respawn timers ([E4.M2](../decomposition/modules/E4_M2_SpawnEcologyController.md)).
- Telemetry hook sites ([NEO-64](https://linear.app/neon-sprawl/issue/NEO-64)).
- Cross-store transactions (prototype relies on ordered commits + compensating remove on XP failure).
## Acceptance criteria checklist
- [x] Successful gather adds configured **`scrap_metal_bulk`** quantity to player bag.
- [x] Full bag returns stable **`inventory_full`** without partial silent loss or depletion change.
- [x] Skill XP grant uses same rules as NEO-38 / NEO-41 (`salvage`, `activity`, 10 XP via **`GatherSkillXpConstants`**).
- [x] 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 <= 0`** → **`Denied`** + **`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.
- **`StoreMissing`** → **`GatherReasonCodes.InventoryStoreMissing`** (gather surface; unlikely in tests).
- **`Applied`** → continue.
6. **Skill XP (second mutation):** **`SkillProgressionGrantOperations.TryApplyGrant`** with **`GatherSkillXpConstants.SalvageSkillId`**, **`ActivityXpPerResourceNodeGather`**, **`ActivitySourceKind`**.
- **`Granted`** → continue.
- **`ProgressionStoreMissing`** or **`Denied`** → **compensating 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`](../../content/resource-nodes/prototype_resource_yields.json). Tests spot-check alpha (1) and optionally delta (5).
11. **Docs (on land):** Update [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md) **Related implementation slices** + **`GatherResult`** contract note; [documentation_and_implementation_alignment.md](documentation_and_implementation_alignment.md) E3.M1 row; E3M1-06 checkboxes in [E3M1-prototype-backlog.md](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_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](../../.cursor/rules/csharp-style.md). |
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.