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

168 lines
14 KiB
Markdown

# NEO-118 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-118 |
| **Title** | E7M1-07: Quest objective wiring (gather, craft, encounter) |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-118/e7m1-07-quest-objective-wiring-gather-craft-encounter |
| **Module** | [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-07** |
| **Branch** | `NEO-118-quest-objective-wiring` |
| **Precursor** | [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) — `QuestStateOperations` accept/advance/complete (**Done** on `main`) |
| **Pattern** | [NEO-106](NEO-106-implementation-plan.md) — `EncounterCombatWiring` best-effort side effect on success paths; [NEO-117](NEO-117-implementation-plan.md) — `QuestStateOperations` for advance/complete |
| **Blocks** | [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) — GET quest-progress; [NEO-121](https://linear.app/neon-sprawl/issue/NEO-121) — telemetry hook sites |
| **Client counterpart** | [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) — quest progress HUD reads GET snapshot after gather/craft/encounter mutations (E7M1-11). Bruno-only progress verification is **not** prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md). |
## Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **`inventory_has_item` triggers** | When to evaluate beyond gather/craft/encounter grant paths? | Inventory mutations **+ quest accept + after step advance** — backlog “post-grant or on polling hook from accept”; chain step 4 token often pre-held from combat intro | **Adopted** — user confirmed |
| **Accept / step-advance re-eval scope** | Which objective kinds to reconcile on accept/step entry? | **`inventory_has_item` only**; `gather_item` / `craft_recipe` / `encounter_complete` stay event-driven | **Adopted** — user confirmed |
**Additional defaults (no kickoff question — settled by backlog / NEO-116):**
- Counter accumulation uses **event quantity** (gather yield qty, craft batch qty); cap stored counter at objective `quantity` for satisfaction.
- **All active quests** matching the event are credited (e.g. gather intro + chain step 1 on same gather).
- Wiring is **best-effort** — quest evaluation failures must not fail gather/craft/encounter (mirror `EncounterCombatWiring`).
- `encounter_complete` objectives treat satisfaction as counter **≥ 1** (no quantity field in catalog).
- Step completion: when **all** objectives on the current step are satisfied → `TryAdvanceStep` to next index, or `TryMarkComplete` on terminal step; after advance, run **`inventory_has_item`** snapshot pass for the new step.
## Goal, scope, and out-of-scope
**Goal:** Advance active quest steps when server-side gather, craft, and encounter events succeed; evaluate **`inventory_has_item`** from inventory snapshots on grant paths, quest accept, and step advance.
**In scope (from Linear + [E7M1-07](E7M1-prototype-backlog.md#e7m1-07--quest-objective-wiring-gather-craft-encounter)):**
- **`QuestObjectiveWiring`** (static) — evaluate active quests against incoming events; update counters via store; auto advance/complete via **`QuestStateOperations`**.
- Hook from **`GatherOperations.TryGather`** success — **`gather_item`** objectives (item id + quantity delta).
- Hook from **`CraftOperations.TryCraft`** success — **`craft_recipe`** objectives (recipe id; quantity = craft batch size).
- Hook from **`EncounterCompletionOperations.TryCompleteAndGrant`** success — **`encounter_complete`** objectives (encounter id).
- **`inventory_has_item`** — inventory snapshot read after successful inventory mutations; **`TryProcessInventoryHasItem`** on quest accept and after step advance (current/new step only).
- Counter accumulation for quantity objectives; auto **`TryAdvanceStep`** / **`TryMarkComplete`** when step objectives satisfied.
- Integration-style tests (AAA): accept quest → simulate gather/craft/encounter → progress row advances through chain.
- `server/README.md` — objective wiring subsection + hook table.
- `docs/manual-qa/NEO-118.md` — Bruno/server-side verification checklist (no HTTP quest-progress yet).
**Out of scope (from Linear + backlog):**
- Client-side objective tracking; item consume on turn-in; E7.M2 reward bundles.
- HTTP **`GET …/quest-progress`** ([NEO-119](https://linear.app/neon-sprawl/issue/NEO-119)) and accept POST ([NEO-120](https://linear.app/neon-sprawl/issue/NEO-120)).
- Telemetry hook comments ([NEO-121](https://linear.app/neon-sprawl/issue/NEO-121) / E7M1-10).
- Retroactive **`encounter_complete`** credit on accept (encounter already done before quest accept).
- GET polling for **`inventory_has_item`** (deferred to NEO-119 if needed).
## Acceptance criteria checklist
- [x] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds.
- [x] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success.
- [x] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes.
- [x] Chain quest steps advance in order across mixed objective kinds.
## Implementation reconciliation (shipped)
- **Types:** `QuestObjectiveKinds`, `QuestObjectiveWiring` (`TryProcessGatherSuccess`, `TryProcessCraftSuccess`, `TryProcessEncounterCompleteSuccess`, `TryProcessInventoryHasItemForQuest`, `TryCompleteStepIfSatisfied`).
- **Hooks:** `GatherOperations`, `CraftOperations`, `EncounterCompletionOperations`, `QuestStateOperations.TryAccept` / `TryAdvanceStep`.
- **Tests:** `QuestObjectiveWiringTests` (6 AAA cases); existing gather/craft/encounter/state tests updated for new parameters.
- **Docs:** `server/README.md` objective wiring section; `docs/manual-qa/NEO-118.md`.
## Technical approach
1. **`QuestObjectiveKinds`** — string constants: `gather_item`, `craft_recipe`, `encounter_complete`, `inventory_has_item` (mirror schema enum).
2. **`QuestObjectiveWiring`** (`Game/Quests/`)
- **`TryProcessGatherSuccess(playerId, itemId, quantityGranted, …)`** — scan all catalog quest ids; for each **active** row, match current-step **`gather_item`** objectives by `itemId`; bump counter by `quantityGranted` (capped at objective `quantity`).
- **`TryProcessCraftSuccess(playerId, recipeId, batchQuantity, …)`** — match **`craft_recipe`** by `recipeId`; increment counter by `batchQuantity` (capped).
- **`TryProcessEncounterCompleteSuccess(playerId, encounterId, …)`** — match **`encounter_complete`**; set counter to **1** when encounter id matches.
- **`TryProcessInventoryHasItemForPlayer(playerId, …)`** / **`TryProcessInventoryHasItemForQuest(playerId, questId, …)`** — read **`IPlayerInventoryStore.TryGetSnapshot`**; for current-step **`inventory_has_item`** objectives, set counter to `min(bagQuantity, objective.quantity)`; used after grants, on accept, after step advance.
- Shared private **`TryEvaluateActiveQuests`** / **`TryCompleteStepIfSatisfied`**: when every objective on the current step meets threshold → if last step → **`QuestStateOperations.TryMarkComplete`**; else → **`TryAdvanceStep(currentIndex + 1)`** then **`TryProcessInventoryHasItemForQuest`** for the advanced quest (chain terminal token case).
- Best-effort: swallow/log at debug; never throw to callers.
3. **Inventory quantity helper**
- Private static on wiring (or small internal helper): sum **`PlayerInventorySnapshot`** bag slot quantities for an `itemId` (same semantics as craft/encounter tests).
4. **Call sites**
- **`GatherOperations.TryGather`** — before successful return, call **`TryProcessGatherSuccess`** with `yield.ItemId` / `yield.Quantity` (add quest + inventory deps to method signature).
- **`CraftOperations.TryCraft`** — before successful return, call **`TryProcessCraftSuccess`** with normalized recipe id and batch `quantity`.
- **`EncounterCompletionOperations.TryCompleteAndGrant`** — after successful grant + completion mark, call **`TryProcessEncounterCompleteSuccess`** with normalized encounter id.
- **`QuestStateOperations.TryAccept`** — after successful activation, call **`TryProcessInventoryHasItemForQuest`** (add `IPlayerInventoryStore` + `TimeProvider` params).
- **`QuestStateOperations.TryAdvanceStep`** — after successful advance, call **`TryProcessInventoryHasItemForQuest`** for the same quest id.
- Propagate new dependencies through **`InteractionApi`**, **`PlayerCraftApi`**, **`EncounterCombatWiring`**, and **`AbilityCastApi`** DI resolves (factory tests already resolve full host).
5. **Tests** (`QuestObjectiveWiringTests.cs`)
- Use **`InMemoryWebApplicationFactory`** + real catalog/registry/store.
- **Gather intro:** accept → N gathers until counter ≥ 3 → quest **completed** (single-step).
- **Refine intro:** prerequisite setup → accept → craft **`refine_scrap_standard`** → **completed**.
- **Combat intro:** accept → **`TryProcessEncounterCompleteSuccess`** for **`prototype_combat_pocket`** → **completed**.
- **Chain:** accept chain → gather 5 scrap → advance to step 1 → craft refine → step 2 → craft stim → step 3 → seed **`contract_handoff_token`** in inventory → advance triggers step 4 **`inventory_has_item`** → **completed**.
- **Parallel credit:** gather intro + chain step 0 both active; one gather credits both counters appropriately.
- Update **`QuestStateOperationsTests`** signatures if accept/advance gain inventory/time params (pass real store from factory).
6. **Docs**
- `server/README.md` — replace “Objective wiring is E7M1-07” stub with wiring summary, hook table, **`inventory_has_item`** trigger list.
- Alignment register / module doc / E7M1 backlog checkboxes on story land.
### Wiring flow (gather success)
```mermaid
flowchart TD
G[GatherOperations.TryGather success] --> W[QuestObjectiveWiring.TryProcessGatherSuccess]
W --> A{For each active quest}
A --> M{Current step has matching gather_item?}
M -->|yes| C[TryUpdateObjectiveCounter capped]
M -->|no| A
C --> S{All objectives satisfied?}
S -->|terminal step| MC[QuestStateOperations.TryMarkComplete]
S -->|more steps| AS[QuestStateOperations.TryAdvanceStep]
AS --> I[TryProcessInventoryHasItemForQuest]
S -->|no| A
```
## Files to add
| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Quests/QuestObjectiveKinds.cs` | Stable objective kind string constants. |
| `server/NeonSprawl.Server/Game/Quests/QuestObjectiveWiring.cs` | Event + inventory snapshot evaluation; step auto-advance/complete. |
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestObjectiveWiringTests.cs` | AAA integration-style tests for all four prototype quests + chain. |
| `docs/manual-qa/NEO-118.md` | Server-side Bruno/unit verification checklist until NEO-119 HTTP. |
| `docs/plans/NEO-118-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/Gathering/GatherOperations.cs` | Call quest wiring on successful gather; add quest/inventory/time deps. |
| `server/NeonSprawl.Server/Game/Crafting/CraftOperations.cs` | Call quest wiring on successful craft. |
| `server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs` | Call quest wiring after successful completion + grants. |
| `server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs` | Invoke `inventory_has_item` pass after accept/advance; add inventory + time deps. |
| `server/NeonSprawl.Server/Game/Interaction/InteractionApi.cs` | Pass quest wiring deps into `GatherOperations.TryGather`. |
| `server/NeonSprawl.Server/Game/Crafting/PlayerCraftApi.cs` | Pass quest wiring deps into `CraftOperations.TryCraft`. |
| `server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs` | Pass quest wiring deps through to `EncounterCompletionOperations`. |
| `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` | Pass quest wiring deps into `EncounterCombatWiring` (if signature changes). |
| `server/NeonSprawl.Server.Tests/Game/Quests/QuestStateOperationsTests.cs` | Update `TryAccept` / `TryAdvanceStep` call sites for new parameters. |
| `server/README.md` | Document objective wiring hooks and `inventory_has_item` triggers. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M1 row — NEO-118 wiring landed (on story complete). |
| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | NEO-118 wiring bullet + README link (on story complete). |
| `docs/decomposition/modules/module_dependency_register.md` | E7.M1 note — NEO-118 (on story complete). |
| `docs/plans/E7M1-prototype-backlog.md` | E7M1-07 acceptance checkboxes (on story complete). |
## Tests
| Test file | What it covers |
|-----------|----------------|
| `QuestObjectiveWiringTests.cs` | **Gather intro:** accept → gather **`scrap_metal_bulk`** until complete. **Refine intro:** prerequisites met → accept → craft **`refine_scrap_standard`** → complete. **Combat intro:** accept → encounter complete event → complete. **Chain:** four-step mixed kinds including **`inventory_has_item`** terminal after token present. **Parallel:** gather intro + chain step 0 simultaneous credit. **Inventory on advance:** token pre-seeded → step advance to terminal → auto-complete without new grant. |
| `QuestStateOperationsTests.cs` | Regression — existing accept/advance/complete cases with updated method signatures (inventory store + time provider from factory). |
| Existing gather/craft/encounter tests | Smoke regression — no behavior change when no active quests (full suite in CI). |
## Open questions / risks
| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **`QuestStateOperations` signature growth** | Add `IPlayerInventoryStore` + `TimeProvider` to accept/advance for wiring; keep complete unchanged | **adopted** |
| **Terminal step auto-complete without explicit advance call** | Wiring calls `TryMarkComplete` when last step objectives satisfied (operations layer idempotent) | **adopted** |
| **Multi-objective steps** | Implement generic all-objectives-satisfied check even though prototype steps are single-objective | **adopted** |
| **Encounter done before combat quest accept** | Out of scope; manual QA accepts before combat | **deferred** |
| **NEO-117 review nits reused ids** | Use `PrototypeE7M1QuestCatalogRules` constants in wiring tests | **adopted** |