# 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 - [ ] Active **`prototype_quest_gather_intro`** advances when **`scrap_metal_bulk`** gather succeeds. - [ ] Active **`prototype_quest_refine_intro`** advances on **`refine_scrap_standard`** craft success. - [ ] Active **`prototype_quest_combat_intro`** advances when **`prototype_combat_pocket`** completes. - [ ] Chain quest steps advance in order across mixed objective kinds. ## 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** |