From 72b98a627cbf1ed00c93043c74ccc0e3d04dbd7c Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 7 Jun 2026 12:28:11 -0400 Subject: [PATCH 1/4] NEO-121: Add kickoff implementation plan for quest telemetry hooks. --- docs/plans/NEO-121-implementation-plan.md | 116 ++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/plans/NEO-121-implementation-plan.md diff --git a/docs/plans/NEO-121-implementation-plan.md b/docs/plans/NEO-121-implementation-plan.md new file mode 100644 index 0000000..b0d9908 --- /dev/null +++ b/docs/plans/NEO-121-implementation-plan.md @@ -0,0 +1,116 @@ +# NEO-121 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-121 | +| **Title** | E7M1-10: Quest telemetry hook sites (comment-only) | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-121/e7m1-10-quest-telemetry-hook-sites-comment-only | +| **Module** | [E7.M1 — QuestStateMachine](../decomposition/modules/E7_M1_QuestStateMachine.md) · Epic 7 Slice 1 · backlog **E7M1-10** | +| **Branch** | `NEO-121-quest-telemetry-hooks` | +| **Precursor** | [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) — `QuestStateOperations` (**landed on `main`**); [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) — `QuestObjectiveWiring` (**landed on `main`**); [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) / [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) — quest progress GET + accept POST (**landed on `main`**) | +| **Pattern** | [NEO-64](NEO-64-implementation-plan.md) / [NEO-56](NEO-56-implementation-plan.md) / [NEO-49](NEO-49-implementation-plan.md) — comment-only engine anchors + `TODO(E9.M1)`; engine-only (no duplicate API-layer hooks) | +| **Client counterpart** | none (infrastructure-only per [E7M1-10](E7M1-prototype-backlog.md#e7m1-10--quest-telemetry-hook-sites-comment-only)) | + +## Kickoff clarifications + +**No clarifications needed.** [E7M1-10](E7M1-prototype-backlog.md#e7m1-10--quest-telemetry-hook-sites-comment-only) backlog, Linear AC, landed quest operations/wiring (NEO-117/118), and comment-only telemetry precedents (NEO-49/56/64/71) settle scope: + +- **Runtime behavior:** comments-only + `TODO(E9.M1)` — no production ingest, no `ILogger` (NEO-49/56 precedent). +- **Hook anchor:** **`QuestStateOperations`** only for **`quest_start`**, **`quest_step_complete`**, **`quest_complete`** — HTTP accept POST and objective wiring delegate to the engine; no duplicate API-layer or wiring-layer hooks (NEO-56 engine-only precedent). **`QuestObjectiveWiring`** triggers step advance/complete by calling **`QuestStateOperations`**; hooks live at mutation commit sites. +- **`quest_start` timing:** after successful **`TryActivate`** in **`TryAccept`** (first activation only; deny paths excluded). +- **`quest_step_complete` timing:** after successful **`TryAdvanceStep`** store commit (covers wiring-driven multi-step chains — one emit per committed advance). +- **`quest_complete` timing:** after first-time **`TryMarkComplete`** store commit; **not** on idempotent replay when row is already **`completed`** (NEO-117 idempotency + NEO-49 “idempotent reevaluation produces none” precedent). +- **Optional deny hook:** include **`quest_accept_denied`** comment in **`TryAccept`** **`Deny`** returns with stable **`reasonCode`** (NEO-56 **`inventory_transfer_denied`** pattern; backlog lists as optional — adopt for accept funnel parity). +- **Manual QA doc:** **`docs/manual-qa/NEO-121.md`** — NEO-64/49 comment-only verification pattern (no behavior/API change). + +## Goal, scope, and out-of-scope + +**Goal:** Document and place **comment-only hook sites** on the server-authoritative quest state path for future E9.M1 catalog events **`quest_start`**, **`quest_step_complete`**, and **`quest_complete`**. Align with Epic 7 Slice 1 telemetry vocabulary ([E7_M1 module](../decomposition/modules/E7_M1_QuestStateMachine.md#risks-and-telemetry)). + +**In scope (from Linear + [E7M1-10](E7M1-prototype-backlog.md#e7m1-10--quest-telemetry-hook-sites-comment-only)):** + +- Hook sites in **`QuestStateOperations`** at accept, step advance, and complete commit paths. +- Reserved deny hook **`quest_accept_denied`** (comment-only) on **`TryAccept`** deny paths. +- **`server/README.md`** quest telemetry subsection. +- **`E7_M1_QuestStateMachine.md`** implementation snapshot note. +- **`docs/manual-qa/NEO-121.md`** — verify comments + no behavior change. +- E7M1 backlog + alignment register updates when implementation completes. + +**Out of scope (from Linear + backlog):** + +- E9.M1 ingest pipeline; runtime telemetry emit. +- `ILogger` / metrics / dev-only log lines. +- Duplicate hook comments in **`QuestAcceptApi`**, **`QuestProgressApi`**, or **`QuestObjectiveWiring`** (engine-only precedent). +- Client HUD ([NEO-122](https://linear.app/neon-sprawl/issue/NEO-122)). + +## Acceptance criteria checklist + +- [ ] Hook sites exist at accept, step complete, quest complete paths. +- [ ] Epic 7 Slice 1 telemetry vocabulary documented in `server/README.md`. + +## Technical approach + +1. **Authoritative surface:** **`QuestStateOperations`** — all quest lifecycle mutations funnel here. **`QuestObjectiveWiring`** calls **`TryAdvanceStep`** / **`TryMarkComplete`**; **`QuestAcceptApi`** calls **`TryAccept`**. + +2. **Hook site A — `quest_start`:** In **`TryAccept`**, immediately after **`progressStore.TryActivate`** returns **`true`** (before objective wiring refresh): + - Comment block names future E9.M1 event **`quest_start`**. + - **`TODO(E9.M1): catalog emit`** (NEO-49 style). + - Planned payload fields: `playerId`, `questId`, `currentStepIndex` (0), optional `schemaVersion`. + +3. **Hook site B — `quest_step_complete`:** In **`TryAdvanceStep`**, immediately after **`progressStore.TryAdvanceStep`** returns **`true`**: + - Comment block names future E9.M1 event **`quest_step_complete`**. + - **`TODO(E9.M1): catalog emit`** — emit once per committed step advance (chain quests may emit multiple in one wiring pass). + - Planned payload fields: `playerId`, `questId`, `completedStepIndex` (prior index), `newStepIndex`, optional step id from catalog at E9.M1 wiring time. + +4. **Hook site C — `quest_complete`:** In **`TryMarkComplete`**, immediately after **`progressStore.TryMarkComplete`** returns **`true`** on the first-time completion path (after the early idempotent **`completed`** return): + - Comment block names future E9.M1 event **`quest_complete`**. + - **`TODO(E9.M1): catalog emit`** — not on idempotent replay. + - Planned payload fields: `playerId`, `questId`, `completedAt`, optional total step count. + +5. **Hook site D (optional) — `quest_accept_denied`:** On **`TryAccept`** deny returns (central **`Deny`** helper and/or each deny branch before return): + - Comment block names future E9.M1 event **`quest_accept_denied`**. + - **`TODO(E9.M1): catalog emit`** with stable **`reasonCode`** (`unknown_quest`, `prerequisite_incomplete`, `already_active`, `already_completed`, etc.). + - Planned payload fields: `playerId`, `questId`, `reasonCode`. + +6. **Cross-references (minimal):** + - Update **`QuestStateOperations`** class summary → NEO-121 telemetry hook anchor. + - **`server/README.md`:** new **Quest telemetry hooks (NEO-121)** subsection after quest accept POST (NEO-120). + - **`E7_M1_QuestStateMachine.md`:** NEO-121 landed line under Risks and telemetry. + - **`documentation_and_implementation_alignment.md`** + **`module_dependency_register.md`:** E7.M1 row — E7M1-10 when complete. + - **`E7M1-prototype-backlog.md`:** E7M1-10 checkboxes + landed note. + +7. **Accept + instant wiring:** **`TryAccept`** may trigger **`quest_start`**, then wiring may emit **`quest_step_complete`** / **`quest_complete`** in the same call stack — order is preserved by code flow (activate → wiring → advance/complete). + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/manual-qa/NEO-121.md` | Manual QA: verify comment blocks + no behavior/API change. | +| `docs/plans/NEO-121-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs` | **NEO-121:** comment-only **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** hook sites at mutation/deny commit paths. | +| `server/README.md` | Document quest telemetry hook placement and Epic 7 Slice 1 vocabulary (NEO-121). | +| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | Implementation snapshot: NEO-121 telemetry hooks landed. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M1 row: NEO-121 hook sites. | +| `docs/decomposition/modules/module_dependency_register.md` | E7.M1 note — E7M1-10 complete when story lands. | +| `docs/plans/E7M1-prototype-backlog.md` | E7M1-10 checkboxes + landed note. | + +## Tests + +| Test file | What it covers | +|-----------|----------------| +| *(none added or changed)* | Comments-only change; no behavior or wire contract change. Regression: `dotnet test NeonSprawl.sln` (existing **`QuestStateOperationsTests`**, **`QuestObjectiveWiringTests`**, quest HTTP integration tests unchanged). Manual verification: **`docs/manual-qa/NEO-121.md`**. | + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Duplicate emits on accept + wiring same tick** | Expected — **`quest_start`** then zero or more **`quest_step_complete`** / **`quest_complete`**; E9.M1 can correlate by session/time or sample funnels. | **adopted** | +| **`quest_accept_denied` scope** | Include on all **`TryAccept`** deny paths with **`reasonCode`** (NEO-56 deny pattern). | **adopted** | +| **Module Ready vs In Progress** | Leave E7.M1 **In Progress** until client capstones NEO-122/123 land; only note E7M1-10 complete in register. | **adopted** | From 705a7cd028f5b0371a23b875f8d27c12649e8829 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 7 Jun 2026 12:29:52 -0400 Subject: [PATCH 2/4] NEO-121: Add comment-only quest telemetry hook sites in QuestStateOperations. Document quest_start, quest_step_complete, quest_complete, and quest_accept_denied anchors for future E9.M1 ingest with README, manual QA, and module doc updates. --- .../modules/E7_M1_QuestStateMachine.md | 5 +- ...umentation_and_implementation_alignment.md | 2 +- .../modules/module_dependency_register.md | 2 +- docs/manual-qa/NEO-121.md | 35 +++++++++++ docs/plans/E7M1-prototype-backlog.md | 6 +- docs/plans/NEO-121-implementation-plan.md | 14 ++++- .../Game/Quests/QuestStateOperations.cs | 63 ++++++++++++++++--- server/README.md | 13 ++++ 8 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 docs/manual-qa/NEO-121.md diff --git a/docs/decomposition/modules/E7_M1_QuestStateMachine.md b/docs/decomposition/modules/E7_M1_QuestStateMachine.md index 4ecc476..e68087d 100644 --- a/docs/decomposition/modules/E7_M1_QuestStateMachine.md +++ b/docs/decomposition/modules/E7_M1_QuestStateMachine.md @@ -7,7 +7,7 @@ | **Module ID** | E7.M1 | | **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) | | **Stage target** | Prototype | -| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) **`QuestObjectiveWiring`** **landed**; **E7M1-08** [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) per-player GET **landed**; **E7M1-09** [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) accept POST **landed**; client from **E7M1-11** [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) | +| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) **`QuestObjectiveWiring`** **landed**; **E7M1-08** [NEO-119](https://linear.app/neon-sprawl/issue/NEO-119) per-player GET **landed**; **E7M1-09** [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) accept POST **landed**; **E7M1-10** [NEO-121](https://linear.app/neon-sprawl/issue/NEO-121) telemetry hooks **landed**; client from **E7M1-11** [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) | | **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) | ## Purpose @@ -92,12 +92,15 @@ The **first shipped quest spine** is **frozen** for prototype tuning until a del **Quest accept POST (NEO-120):** **`POST /game/players/{id}/quests/{questId}/accept`** — `QuestAcceptApi` + DTOs; wires **`QuestStateOperations.TryAccept`**; structured **`accepted`** / **`reasonCode`** / optional **`quest`** row ([NEO-120](../../plans/NEO-120-implementation-plan.md)); [server README — Quest accept POST (NEO-120)](../../../server/README.md#quest-accept-post-neo-120); Bruno accept spine in `bruno/neon-sprawl-server/quest-progress/`. +**Quest telemetry hooks (NEO-121):** comment-only **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** hook sites in **`QuestStateOperations`** ([NEO-121](../../plans/NEO-121-implementation-plan.md)); [server README — Quest telemetry hooks (NEO-121)](../../../server/README.md#quest-telemetry-hooks-neo-121). + **Reward policy (Slice 1):** Quest completion updates **`QuestStepState` only** — no duplicate item/XP grants. Gather/craft/encounter paths keep existing payouts; [E7.M2](E7_M2_RewardAndUnlockRouter.md) adds **`QuestRewardBundle`** apply in Slice 2. ## Risks and telemetry - Scripting weight: keep `QuestDef` data-first; templates in decomposition/tooling path per epic. - Double-claim: design for idempotency before E7.M2; lint bundles vs E6.M4 / E3.M5. +- Epic 7 Slice 1 catalog events (comment-only until E9.M1): **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** — anchored in **`QuestStateOperations`** (NEO-121). ## Source anchors diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 2e19fc2..a102ed3 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -59,7 +59,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E5.M1 | Ready | **NEO-76 landed:** frozen prototype four-ability catalog in [`content/abilities/prototype_abilities.json`](../../../content/abilities/prototype_abilities.json); [`ability-def.schema.json`](../../../content/schemas/ability-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M1 four-id gate ([NEO-76](../../plans/NEO-76-implementation-plan.md)). **NEO-77 landed:** fail-fast server load of `content/abilities/*_abilities.json` at startup — `server/NeonSprawl.Server/Game/Combat/` ([NEO-77](../../plans/NEO-77-implementation-plan.md)); [server README — Ability catalog](../../../server/README.md#ability-catalog-contentabilities-neo-77). **NEO-79 landed:** injectable **`IAbilityDefinitionRegistry`** + DI; hotbar/cast use **`TryNormalizeKnown`** ([NEO-79](../../plans/NEO-79-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/hotbar-loadout/` + `ability-cast/` unknown-ability deny smokes. **NEO-78 landed:** **`GET /game/world/ability-definitions`** — `AbilityDefinitionsWorldApi` + DTOs in `Game/Combat/` ([NEO-78](../../plans/NEO-78-implementation-plan.md)); [server README — Ability definitions (NEO-78)](../../../server/README.md#ability-definitions-neo-78); Bruno `bruno/neon-sprawl-server/ability-definitions/`. **NEO-80 landed:** **`ICombatEntityHealthStore`** / **`InMemoryCombatEntityHealthStore`** — lazy init (Slice 1 flat 100 HP; **superseded by [NEO-91](../../plans/NEO-91-implementation-plan.md)** per-archetype catalog max HP + three NPC instance ids); DI via **`AddCombatEntityHealthStore()`** ([NEO-80](../../plans/NEO-80-implementation-plan.md)); [server README — Combat entity health (NEO-80, NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-81 landed:** **`CombatOperations.TryResolve`** + **`CombatResult`** + **`CombatReasonCodes`** — defeated pre-check deny, catalog damage ([NEO-81](../../plans/NEO-81-implementation-plan.md)); [server README — Combat engine (NEO-81)](../../../server/README.md#combat-engine-neo-81). **NEO-82 landed:** **`AbilityCastApi`** → **`CombatOperations.TryResolve`**; nested wire **`combatResolution`** on accept; per-ability catalog cooldown; **`target_defeated`** cast deny ([NEO-82](../../plans/NEO-82-implementation-plan.md)); [server README — Ability cast (NEO-82)](../../../server/README.md#ability-cast-neo-31-neo-82); Bruno `bruno/neon-sprawl-server/ability-cast/` defeat spine. **NEO-83 landed:** **`GET /game/world/combat-targets`** — `CombatTargetsWorldApi` + DTOs; authoritative HP snapshot from **`ICombatEntityHealthStore`** ([NEO-83](../../plans/NEO-83-implementation-plan.md)); [server README — Combat targets snapshot (NEO-83)](../../../server/README.md#combat-targets-snapshot-neo-83); Bruno `bruno/neon-sprawl-server/combat-targets/`. **NEO-44 landed:** **`CombatDefeatGigXpGrant`** on cast **`targetDefeated`** — **25** gig XP to **`breach`** via **`IPlayerGigProgressionStore`** (V007); **`GET …/gig-progression`** ([NEO-44](../../plans/NEO-44-implementation-plan.md), [`NEO-44` manual QA](../../manual-qa/NEO-44.md)); [server README — Gig progression (NEO-44)](../../../server/README.md#gig-progression-snapshot-neo-44); Bruno `bruno/neon-sprawl-server/gig-progression/`. **NEO-84 landed:** comment-only **`ability_used`** / **`enemy_defeat`** telemetry hook sites in **`CombatOperations.TryResolve`**; reserved **`encounter_start`** / **`player_death`** ([NEO-84](../../plans/NEO-84-implementation-plan.md)); [server README — Combat telemetry hooks (NEO-84)](../../../server/README.md#combat-telemetry-hooks-neo-84). **NEO-85 landed:** client combat HUD — **`ability_cast_client.gd`** parses **`combatResolution`**; **`combat_targets_client.gd`**, **`CastFeedbackLabel`** resolution copy, **`CombatTargetHpLabel`**; event-driven GET refresh ([NEO-85](../../plans/NEO-85-implementation-plan.md), [`NEO-85` manual QA](../../manual-qa/NEO-85.md)); `client/README.md` combat HUD section. **NEO-86 landed:** playable combat capstone — **`gig_progression_client.gd`**, **`GigXpLabel`**, defeat-triggered gig GET refresh; capstone manual QA [`NEO-86`](../../manual-qa/NEO-86.md); `client/README.md` end-to-end combat loop section ([NEO-86](../../plans/NEO-86-implementation-plan.md)). **Epic 5 Slice 1 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 1 — **E5M1-01**–**E5M1-12** landed. **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32). | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [NEO-77](../../plans/NEO-77-implementation-plan.md), [NEO-79](../../plans/NEO-79-implementation-plan.md), [NEO-78](../../plans/NEO-78-implementation-plan.md), [NEO-80](../../plans/NEO-80-implementation-plan.md), [NEO-81](../../plans/NEO-81-implementation-plan.md), [NEO-82](../../plans/NEO-82-implementation-plan.md), [NEO-83](../../plans/NEO-83-implementation-plan.md), [NEO-44](../../plans/NEO-44-implementation-plan.md), [NEO-84](../../plans/NEO-84-implementation-plan.md), [NEO-85](../../plans/NEO-85-implementation-plan.md), [NEO-86](../../plans/NEO-86-implementation-plan.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) | | E5.M2 | Ready | **NEO-87 landed:** frozen prototype three-behavior catalog in [`content/npc-behaviors/prototype_npc_behaviors.json`](../../../content/npc-behaviors/prototype_npc_behaviors.json); [`npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M2 three-id gate ([NEO-87](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` at startup — `server/NeonSprawl.Server/Game/Npc/` ([NEO-88](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI; **`PrototypeNpcBehaviorRegistry`** id constants ([NEO-89](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **NEO-91 landed:** **`PrototypeNpcRegistry`** + combat-target migration — three NPC instance ids replace alpha/beta; per-archetype max HP from behavior catalog ([NEO-91](../../plans/NEO-91-implementation-plan.md)); [server README — Combat entity health (NEO-91)](../../../server/README.md#combat-entity-health-neo-80-neo-91). **NEO-92 landed:** **`IThreatStateStore`** + **`AggroOperations`** — first-hit acquire on damaging cast, leash clear on move/cast; cast/move/fixture hooks ([NEO-92](../../plans/NEO-92-implementation-plan.md)); [server README — Threat / aggro state (NEO-92)](../../../server/README.md#threat--aggro-state-neo-92). **NEO-93 landed:** **`INpcRuntimeStateStore`** + **`NpcRuntimeOperations.AdvanceAll`** — catalog-driven behavior state machine, lazy tick advance with delta cap; dev fixture reset ([NEO-93](../../plans/NEO-93-implementation-plan.md)); [server README — NPC runtime behavior state (NEO-93)](../../../server/README.md#npc-runtime-behavior-state-neo-93). **NEO-94 landed:** **`GET /game/world/npc-runtime-snapshot`** — `NpcRuntimeSnapshotWorldApi` + DTOs; lazy **`AdvanceAll`** on poll; nested **`activeTelegraph`** with server-computed **`windupRemainingSeconds`** ([NEO-94](../../plans/NEO-94-implementation-plan.md)); [server README — NPC runtime snapshot (NEO-94)](../../../server/README.md#npc-runtime-snapshot-neo-94); Bruno `bruno/neon-sprawl-server/npc-runtime-snapshot/`. **NEO-95 landed:** **`IPlayerCombatHealthStore`** + **`NpcAttackOperations.TryResolveTelegraphComplete`** — telegraph resolve via behavior **`attackAbilityId`** + ability **`maxRange`** / **`baseDamage`** ([NEO-95](../../plans/NEO-95-implementation-plan.md), [NEO-98](../../plans/NEO-98-implementation-plan.md)); [server README — Session player combat HP (NEO-95)](../../../server/README.md#session-player-combat-hp-neo-95); Bruno `bruno/neon-sprawl-server/combat-health/`. **NEO-96 landed:** comment-only **`telegraph_fired`** + **`npc_state_transition`** hook sites in **`NpcRuntimeOperations.AdvanceAll`** ([NEO-96](../../plans/NEO-96-implementation-plan.md)); [server README — NPC runtime telemetry hooks (NEO-96)](../../../server/README.md#npc-runtime-telemetry-hooks-neo-96). **NEO-97 landed:** client telegraph HUD — **`npc_runtime_client.gd`**, **`player_combat_health_client.gd`**, **`npc_runtime_hud_state.gd`**, **`TelegraphLabel`** / **`NpcStateLabel`** / **`PlayerCombatHpLabel`**; combat-active ~1 Hz poll ([NEO-97](../../plans/NEO-97-implementation-plan.md), [`NEO-97` manual QA](../../manual-qa/NEO-97.md)); [client README — NPC runtime + telegraph HUD (NEO-97)](../../../client/README.md#npc-runtime--telegraph-hud-neo-97). **NEO-98 landed:** playable NPC telegraph combat capstone — [`NEO-98` manual QA](../../manual-qa/NEO-98.md); `client/README.md` end-to-end NPC telegraph combat loop section ([NEO-98](../../plans/NEO-98-implementation-plan.md)). **NEO-98 server integration:** defeat clears aggro/runtime (**`TryStopOnTargetDefeat`**); telegraph damage range-gated via [`prototype_npc_abilities.json`](../../../content/abilities/prototype_npc_abilities.json) + behavior **`attackAbilityId`**. **Epic 5 Slice 2 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 2 — [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md) **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) **landed**. Three archetypes (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`); replaces alpha/beta dummies with **`prototype_npc_melee` / `ranged` / `elite`**. Owns **`ThreatState`**, **`TelegraphEvent`**, **`GET …/npc-runtime-snapshot`**, session **`IPlayerCombatHealthStore`**. Upstream **E5.M1 Ready**. | [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2](E5_M2_NpcAiAndBehaviorProfiles.md), [NEO-87](../../plans/NEO-87-implementation-plan.md), [NEO-88](../../plans/NEO-88-implementation-plan.md), [NEO-89](../../plans/NEO-89-implementation-plan.md), [NEO-90](../../plans/NEO-90-implementation-plan.md), [NEO-91](../../plans/NEO-91-implementation-plan.md), [NEO-92](../../plans/NEO-92-implementation-plan.md), [NEO-93](../../plans/NEO-93-implementation-plan.md), [NEO-94](../../plans/NEO-94-implementation-plan.md), [NEO-95](../../plans/NEO-95-implementation-plan.md), [NEO-96](../../plans/NEO-96-implementation-plan.md), [NEO-97](../../plans/NEO-97-implementation-plan.md), [NEO-98](../../plans/NEO-98-implementation-plan.md), label **`E5.M2`** on NEO-87–NEO-98 | | E5.M3 | Ready | **E5M3-01 catalog landed ([NEO-100](https://linear.app/neon-sprawl/issue/NEO-100)):** encounter + reward-table schemas, `prototype_encounters.json`, `prototype_reward_tables.json`, CI gates. **E5M3-02 server load ([NEO-101](https://linear.app/neon-sprawl/issue/NEO-101)):** fail-fast startup loaders for encounter + reward-table catalogs (CI parity). **E5M3-03 registries ([NEO-102](https://linear.app/neon-sprawl/issue/NEO-102)):** `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry` + DI. **E5M3-04 HTTP read ([NEO-103](https://linear.app/neon-sprawl/issue/NEO-103)):** **`GET /game/world/encounter-definitions`** — `EncounterDefinitionsWorldApi` + nested **`rewardTable`** summary ([NEO-103](../../plans/NEO-103-implementation-plan.md)); [server README — Encounter definitions (NEO-103)](../../../server/README.md#encounter-definitions-neo-103); Bruno `bruno/neon-sprawl-server/encounter-definitions/`. **E5M3-05 stores ([NEO-104](https://linear.app/neon-sprawl/issue/NEO-104)):** **`IEncounterProgressStore`** + **`IEncounterCompletionStore`** + **`EncounterProgressOperations`** ([NEO-104](../../plans/NEO-104-implementation-plan.md)); [server README — Encounter progress (NEO-104)](../../../server/README.md#encounter-progress--completion-stores-neo-104). **E5M3-06 completion grants ([NEO-105](https://linear.app/neon-sprawl/issue/NEO-105)):** **`EncounterCompletionOperations`** + **`EncounterCompleteEvent`** result payload ([NEO-105](../../plans/NEO-105-implementation-plan.md)); [server README — Encounter completion (NEO-105)](../../../server/README.md#encounter-completion--inventory-grants-neo-105). **E5M3-07 combat wiring ([NEO-106](https://linear.app/neon-sprawl/issue/NEO-106)):** **`EncounterCombatWiring`** on **`AbilityCastApi`** ([NEO-106](../../plans/NEO-106-implementation-plan.md)); [server README — Encounter combat wiring (NEO-106)](../../../server/README.md#encounter-combat-wiring-neo-106). **E5M3-09 event record ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)):** **`IEncounterCompleteEventStore`** + E7.M2 hook stub ([NEO-107](../../plans/NEO-107-implementation-plan.md)); [server README — Encounter complete event (NEO-107)](../../../server/README.md#encounter-complete-event-record-neo-107). **E5M3-08 per-player GET ([NEO-108](https://linear.app/neon-sprawl/issue/NEO-108)):** **`GET /game/players/{id}/encounter-progress`** — `EncounterProgressApi` + DTOs ([NEO-108](../../plans/NEO-108-implementation-plan.md)); [server README — Per-player encounter progress (NEO-108)](../../../server/README.md#per-player-encounter-progress-neo-108); Bruno `bruno/neon-sprawl-server/encounter-progress/`. **E5M3-10 telemetry ([NEO-109](https://linear.app/neon-sprawl/issue/NEO-109)):** comment-only **`encounter_start`**, **`encounter_complete`**, **`reward_attribution`**, **`encounter_complete_denied`** in **`EncounterProgressOperations`** / **`EncounterCompletionOperations`** ([NEO-109](../../plans/NEO-109-implementation-plan.md)); [server README — Encounter telemetry hooks (NEO-109)](../../../server/README.md#encounter-telemetry-hooks-neo-109). **E5M3-11 client HUD ([NEO-110](https://linear.app/neon-sprawl/issue/NEO-110)):** **`encounter_progress_client.gd`**, **`EncounterProgressLabel`** / **`EncounterCompleteLabel`**; boot + defeat-triggered GET; inventory refresh on **`completed`** ([NEO-110](../../plans/NEO-110-implementation-plan.md), [`NEO-110` manual QA](../../manual-qa/NEO-110.md)); [client README — Encounter progress + loot HUD (NEO-110)](../../../client/README.md#encounter-progress--loot-hud-neo-110). **E5M3-12 client capstone ([NEO-111](https://linear.app/neon-sprawl/issue/NEO-111)):** playable encounter clear loop — [`NEO-111` manual QA](../../manual-qa/NEO-111.md); [client README — End-to-end encounter clear loop (NEO-111)](../../../client/README.md#end-to-end-encounter-clear-loop-neo-111); plan [NEO-111](../../plans/NEO-111-implementation-plan.md). **Epic 5 Slice 3 client capstone complete.** Epic 5 Slice 3 backlog [E5M3-prototype-backlog.md](../../plans/E5M3-prototype-backlog.md) **E5M3-01** [NEO-100](https://linear.app/neon-sprawl/issue/NEO-100) → **E5M3-12** [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) **landed**. Prototype spine: **`prototype_combat_pocket`** → **`prototype_combat_pocket_clear`**; idempotent completion; per-defeat gig XP unchanged ([NEO-44](../../plans/NEO-44-implementation-plan.md)). Upstream **E5.M2 Ready**, **E3.M3** inventory landed. | [NEO-111 plan](../../plans/NEO-111-implementation-plan.md), [NEO-110 plan](../../plans/NEO-110-implementation-plan.md), [NEO-109 plan](../../plans/NEO-109-implementation-plan.md), [NEO-108 plan](../../plans/NEO-108-implementation-plan.md), [NEO-107 plan](../../plans/NEO-107-implementation-plan.md), [NEO-106 plan](../../plans/NEO-106-implementation-plan.md), [NEO-105 plan](../../plans/NEO-105-implementation-plan.md), [NEO-104 plan](../../plans/NEO-104-implementation-plan.md), [NEO-103 plan](../../plans/NEO-103-implementation-plan.md), [NEO-102 plan](../../plans/NEO-102-implementation-plan.md), [E5_M3](E5_M3_EncounterAndRewardTables.md), label **`E5.M3`** on NEO-100–NEO-111 | -| E7.M1 | Planned | **E7M1-01 catalog landed ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)):** quest schemas, `prototype_quests.json`, CI gates (four frozen quest ids, objective cross-refs, acyclic prerequisites, chain terminal token). **E7M1-02 server load ([NEO-113](https://linear.app/neon-sprawl/issue/NEO-113)):** fail-fast startup load of `content/quests/*_quests.json` — `server/NeonSprawl.Server/Game/Quests/` ([NEO-113](../../plans/NEO-113-implementation-plan.md)); [server README — Quest catalog](../../../server/README.md#quest-catalog-contentquests-neo-113). **E7M1-03 registry ([NEO-114](https://linear.app/neon-sprawl/issue/NEO-114)):** **`IQuestDefinitionRegistry`** + DI ([NEO-114](../../plans/NEO-114-implementation-plan.md)). **E7M1-04 HTTP read ([NEO-115](https://linear.app/neon-sprawl/issue/NEO-115)):** **`GET /game/world/quest-definitions`** — `QuestDefinitionsWorldApi` + DTOs ([NEO-115](../../plans/NEO-115-implementation-plan.md)); [server README — Quest definitions (NEO-115)](../../../server/README.md#quest-definitions-neo-115); Bruno `bruno/neon-sprawl-server/quest-definitions/`. **E7M1-05 store ([NEO-116](https://linear.app/neon-sprawl/issue/NEO-116)):** **`IPlayerQuestStateStore`** + in-memory/Postgres persistence ([NEO-116](../../plans/NEO-116-implementation-plan.md)); [server README — Quest progress store (NEO-116)](../../../server/README.md#quest-progress-store-neo-116). **E7M1-06 operations ([NEO-117](https://linear.app/neon-sprawl/issue/NEO-117)):** **`QuestStateOperations`** + reason codes ([NEO-117](../../plans/NEO-117-implementation-plan.md)); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117). **E7M1-07 objective wiring ([NEO-118](https://linear.app/neon-sprawl/issue/NEO-118)):** **`QuestObjectiveWiring`** on gather/craft/encounter + **`inventory_has_item`** snapshot passes ([NEO-118](../../plans/NEO-118-implementation-plan.md)); [server README — Quest objective wiring (NEO-118)](../../../server/README.md#quest-objective-wiring-neo-118). **E7M1-08 per-player GET ([NEO-119](https://linear.app/neon-sprawl/issue/NEO-119)):** **`GET /game/players/{id}/quest-progress`** — `QuestProgressApi` + DTOs; GET-side inventory refresh ([NEO-119](../../plans/NEO-119-implementation-plan.md)); [server README — Per-player quest progress (NEO-119)](../../../server/README.md#per-player-quest-progress-neo-119); Bruno `bruno/neon-sprawl-server/quest-progress/`. **E7M1-09 accept POST ([NEO-120](https://linear.app/neon-sprawl/issue/NEO-120)):** **`POST /game/players/{id}/quests/{questId}/accept`** — `QuestAcceptApi` + DTOs; **`QuestStateOperations.TryAccept`** ([NEO-120](../../plans/NEO-120-implementation-plan.md)); [server README — Quest accept POST (NEO-120)](../../../server/README.md#quest-accept-post-neo-120); Bruno accept spine in `bruno/neon-sprawl-server/quest-progress/`. **Backlog decomposed (client from E7M1-11+):** Epic 7 Slice 1 — [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Four frozen quests (3 onboarding + 1 gather→craft→combat chain); solo only; state-only completion in Slice 1 (rewards deferred to E7.M2). Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **`contract_handoff_token`** + **`EncounterCompleteEvent`** **Ready**. Register row stays **Planned** until client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). | [NEO-120 plan](../../plans/NEO-120-implementation-plan.md), [NEO-119 plan](../../plans/NEO-119-implementation-plan.md), [NEO-118 plan](../../plans/NEO-118-implementation-plan.md), [NEO-117 plan](../../plans/NEO-117-implementation-plan.md), [NEO-116 plan](../../plans/NEO-116-implementation-plan.md), [NEO-115 plan](../../plans/NEO-115-implementation-plan.md), [NEO-114 plan](../../plans/NEO-114-implementation-plan.md), [NEO-113 plan](../../plans/NEO-113-implementation-plan.md), [NEO-112 plan](../../plans/NEO-112-implementation-plan.md), [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1](E7_M1_QuestStateMachine.md), label **`E7.M1`** on NEO-112–NEO-123 | +| E7.M1 | Planned | **E7M1-01 catalog landed ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)):** quest schemas, `prototype_quests.json`, CI gates (four frozen quest ids, objective cross-refs, acyclic prerequisites, chain terminal token). **E7M1-02 server load ([NEO-113](https://linear.app/neon-sprawl/issue/NEO-113)):** fail-fast startup load of `content/quests/*_quests.json` — `server/NeonSprawl.Server/Game/Quests/` ([NEO-113](../../plans/NEO-113-implementation-plan.md)); [server README — Quest catalog](../../../server/README.md#quest-catalog-contentquests-neo-113). **E7M1-03 registry ([NEO-114](https://linear.app/neon-sprawl/issue/NEO-114)):** **`IQuestDefinitionRegistry`** + DI ([NEO-114](../../plans/NEO-114-implementation-plan.md)). **E7M1-04 HTTP read ([NEO-115](https://linear.app/neon-sprawl/issue/NEO-115)):** **`GET /game/world/quest-definitions`** — `QuestDefinitionsWorldApi` + DTOs ([NEO-115](../../plans/NEO-115-implementation-plan.md)); [server README — Quest definitions (NEO-115)](../../../server/README.md#quest-definitions-neo-115); Bruno `bruno/neon-sprawl-server/quest-definitions/`. **E7M1-05 store ([NEO-116](https://linear.app/neon-sprawl/issue/NEO-116)):** **`IPlayerQuestStateStore`** + in-memory/Postgres persistence ([NEO-116](../../plans/NEO-116-implementation-plan.md)); [server README — Quest progress store (NEO-116)](../../../server/README.md#quest-progress-store-neo-116). **E7M1-06 operations ([NEO-117](https://linear.app/neon-sprawl/issue/NEO-117)):** **`QuestStateOperations`** + reason codes ([NEO-117](../../plans/NEO-117-implementation-plan.md)); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117). **E7M1-07 objective wiring ([NEO-118](https://linear.app/neon-sprawl/issue/NEO-118)):** **`QuestObjectiveWiring`** on gather/craft/encounter + **`inventory_has_item`** snapshot passes ([NEO-118](../../plans/NEO-118-implementation-plan.md)); [server README — Quest objective wiring (NEO-118)](../../../server/README.md#quest-objective-wiring-neo-118). **E7M1-08 per-player GET ([NEO-119](https://linear.app/neon-sprawl/issue/NEO-119)):** **`GET /game/players/{id}/quest-progress`** — `QuestProgressApi` + DTOs; GET-side inventory refresh ([NEO-119](../../plans/NEO-119-implementation-plan.md)); [server README — Per-player quest progress (NEO-119)](../../../server/README.md#per-player-quest-progress-neo-119); Bruno `bruno/neon-sprawl-server/quest-progress/`. **E7M1-09 accept POST ([NEO-120](https://linear.app/neon-sprawl/issue/NEO-120)):** **`POST /game/players/{id}/quests/{questId}/accept`** — `QuestAcceptApi` + DTOs; **`QuestStateOperations.TryAccept`** ([NEO-120](../../plans/NEO-120-implementation-plan.md)); [server README — Quest accept POST (NEO-120)](../../../server/README.md#quest-accept-post-neo-120); Bruno accept spine in `bruno/neon-sprawl-server/quest-progress/`. **E7M1-10 telemetry ([NEO-121](https://linear.app/neon-sprawl/issue/NEO-121)):** comment-only **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** hook sites in **`QuestStateOperations`** ([NEO-121](../../plans/NEO-121-implementation-plan.md)); [server README — Quest telemetry hooks (NEO-121)](../../../server/README.md#quest-telemetry-hooks-neo-121). **Backlog decomposed (client from E7M1-11+):** Epic 7 Slice 1 — [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Four frozen quests (3 onboarding + 1 gather→craft→combat chain); solo only; state-only completion in Slice 1 (rewards deferred to E7.M2). Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **`contract_handoff_token`** + **`EncounterCompleteEvent`** **Ready**. Register row stays **Planned** until client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). | [NEO-121 plan](../../plans/NEO-121-implementation-plan.md), [NEO-120 plan](../../plans/NEO-120-implementation-plan.md), [NEO-119 plan](../../plans/NEO-119-implementation-plan.md), [NEO-118 plan](../../plans/NEO-118-implementation-plan.md), [NEO-117 plan](../../plans/NEO-117-implementation-plan.md), [NEO-116 plan](../../plans/NEO-116-implementation-plan.md), [NEO-115 plan](../../plans/NEO-115-implementation-plan.md), [NEO-114 plan](../../plans/NEO-114-implementation-plan.md), [NEO-113 plan](../../plans/NEO-113-implementation-plan.md), [NEO-112 plan](../../plans/NEO-112-implementation-plan.md), [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1](E7_M1_QuestStateMachine.md), label **`E7.M1`** on NEO-112–NEO-123 | --- diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index e0448b9..f459329 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -109,7 +109,7 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl |---|---|---|---|---|---| | E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned | -**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); **E7M1-07 / NEO-118** **`QuestObjectiveWiring`** landed ([NEO-118 plan](../../plans/NEO-118-implementation-plan.md)); **E7M1-08 / NEO-119** per-player GET landed ([NEO-119 plan](../../plans/NEO-119-implementation-plan.md)); **E7M1-09 / NEO-120** accept POST landed ([NEO-120 plan](../../plans/NEO-120-implementation-plan.md)); register row stays **Planned** until client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). +**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); **E7M1-07 / NEO-118** **`QuestObjectiveWiring`** landed ([NEO-118 plan](../../plans/NEO-118-implementation-plan.md)); **E7M1-08 / NEO-119** per-player GET landed ([NEO-119 plan](../../plans/NEO-119-implementation-plan.md)); **E7M1-09 / NEO-120** accept POST landed ([NEO-120 plan](../../plans/NEO-120-implementation-plan.md)); **E7M1-10 / NEO-121** comment-only quest telemetry hook sites landed ([NEO-121 plan](../../plans/NEO-121-implementation-plan.md)); register row stays **Planned** until client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). | E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned | | E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned | | E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned | diff --git a/docs/manual-qa/NEO-121.md b/docs/manual-qa/NEO-121.md new file mode 100644 index 0000000..1995692 --- /dev/null +++ b/docs/manual-qa/NEO-121.md @@ -0,0 +1,35 @@ +# Manual QA — NEO-121 (quest telemetry hook sites) + +Reference: [implementation plan](../plans/NEO-121-implementation-plan.md), [NEO-117 implementation plan](../plans/NEO-117-implementation-plan.md) (quest operations), [NEO-118 implementation plan](../plans/NEO-118-implementation-plan.md) (objective wiring). + +## Preconditions + +- Run `NeonSprawl.Server` (`dotnet run` from `server/NeonSprawl.Server`; default `http://localhost:5253`) if exercising Bruno; not required for code review only. +- This story adds **comments only**; quest behavior matches NEO-117/118/119/120. + +## Code review check + +- [ ] Open `server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs`. +- [ ] Confirm **`quest_start`** comment block immediately after successful **`TryActivate`** in **`TryAccept`** (before objective wiring). +- [ ] Confirm **`quest_step_complete`** comment block after successful **`TryAdvanceStep`** store commit. +- [ ] Confirm **`quest_complete`** comment block after first-time **`TryMarkComplete`** commit (not on idempotent **`completed`** early return). +- [ ] Confirm **`quest_accept_denied`** comment in **`DenyAccept`** (used by all **`TryAccept`** deny paths). +- [ ] Confirm **`TODO(E9.M1)`** markers and planned payload fields; **no** new `ILogger` or metrics calls. +- [ ] Confirm **no** duplicate hook comments in **`QuestAcceptApi`**, **`QuestProgressApi`**, or **`QuestObjectiveWiring`**. +- [ ] Open [server README — Quest telemetry hooks (NEO-121)](../../server/README.md#quest-telemetry-hooks-neo-121) — NEO-121 subsection present with event table. + +## Regression (optional) + +```bash +cd server && dotnet test NeonSprawl.sln +``` + +Expect all tests pass (comments-only change). + +## Bruno (optional sanity) + +From `bruno/neon-sprawl-server/quest-progress/` against a running server: + +- **Get quest progress default** — all quests **`not_started`** unchanged. +- **Post accept gather intro** — **`accepted: true`** unchanged. +- **Post accept duplicate** — **`accepted: false`**, **`reasonCode: already_active`** unchanged. diff --git a/docs/plans/E7M1-prototype-backlog.md b/docs/plans/E7M1-prototype-backlog.md index 2f8348b..3372add 100644 --- a/docs/plans/E7M1-prototype-backlog.md +++ b/docs/plans/E7M1-prototype-backlog.md @@ -315,8 +315,10 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco **Acceptance criteria** -- [ ] Hook sites exist at accept, step complete, quest complete paths. -- [ ] Epic 7 Slice 1 telemetry vocabulary documented. +- [x] Hook sites exist at accept, step complete, quest complete paths. +- [x] Epic 7 Slice 1 telemetry vocabulary documented. + +**Landed (NEO-121):** comment-only **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, **`quest_accept_denied`** in **`QuestStateOperations`**; [server README — Quest telemetry hooks (NEO-121)](../../server/README.md#quest-telemetry-hooks-neo-121); plan [NEO-121](../../plans/NEO-121-implementation-plan.md). **Client counterpart:** none (infrastructure-only). diff --git a/docs/plans/NEO-121-implementation-plan.md b/docs/plans/NEO-121-implementation-plan.md index b0d9908..4d0cddf 100644 --- a/docs/plans/NEO-121-implementation-plan.md +++ b/docs/plans/NEO-121-implementation-plan.md @@ -47,8 +47,18 @@ ## Acceptance criteria checklist -- [ ] Hook sites exist at accept, step complete, quest complete paths. -- [ ] Epic 7 Slice 1 telemetry vocabulary documented in `server/README.md`. +- [x] Hook sites exist at accept, step complete, quest complete paths. +- [x] Epic 7 Slice 1 telemetry vocabulary documented in `server/README.md`. + +## Implementation reconciliation (shipped) + +- **Engine anchor:** **`QuestStateOperations`** — four comment-only hook sites with **`TODO(E9.M1)`** (no runtime emit). +- **`quest_start`:** after successful **`TryActivate`** in **`TryAccept`**. +- **`quest_step_complete`:** after successful **`TryAdvanceStep`** store commit. +- **`quest_complete`:** after first-time **`TryMarkComplete`** commit (not idempotent replay). +- **`quest_accept_denied`:** **`DenyAccept`** helper on all **`TryAccept`** deny paths. +- **Docs:** `server/README.md` quest telemetry subsection; E7.M1 module + alignment register + E7M1 backlog updated. +- **Manual QA:** `docs/manual-qa/NEO-121.md`. ## Technical approach diff --git a/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs b/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs index f21d092..a8ab09d 100644 --- a/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs +++ b/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs @@ -5,6 +5,8 @@ namespace NeonSprawl.Server.Game.Quests; /// /// Server-authoritative quest accept, step advance, and complete with reason codes (NEO-117). /// Objective wiring: E7M1-07 (NEO-118). HTTP: E7M1-08/09 (NEO-119/NEO-120). +/// Telemetry hook sites: E7M1-10 (NEO-121) — comment-only quest_start, quest_step_complete, +/// quest_complete, quest_accept_denied for future E9.M1. /// public static class QuestStateOperations { @@ -23,22 +25,22 @@ public static class QuestStateOperations if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) || !questRegistry.TryGetDefinition(normalizedQuestId, out var definition)) { - return Deny(QuestStateReasonCodes.UnknownQuest); + return DenyAccept(QuestStateReasonCodes.UnknownQuest); } if (!progressStore.CanWritePlayer(playerId)) { - return Deny(QuestStateReasonCodes.UnknownPlayer); + return DenyAccept(QuestStateReasonCodes.UnknownPlayer); } if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing)) { if (existing.Status == QuestProgressStatus.Completed) { - return Deny(QuestStateReasonCodes.AlreadyCompleted, existing); + return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, existing); } - return Deny(QuestStateReasonCodes.AlreadyActive, existing); + return DenyAccept(QuestStateReasonCodes.AlreadyActive, existing); } foreach (var prerequisiteId in definition.PrerequisiteQuestIds) @@ -46,12 +48,16 @@ public static class QuestStateOperations if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) || prerequisite.Status != QuestProgressStatus.Completed) { - return Deny(QuestStateReasonCodes.PrerequisiteIncomplete); + return DenyAccept(QuestStateReasonCodes.PrerequisiteIncomplete); } } if (progressStore.TryActivate(playerId, normalizedQuestId, out var snapshot)) { + // --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_start` --- + // TODO(E9.M1): catalog emit — first activation only (deny paths excluded). + // Planned payload fields: playerId, questId, currentStepIndex (0). No ingest or ILogger here (comments-only). + QuestObjectiveWiring.TryProcessInventoryHasItemForQuest( playerId, normalizedQuestId, @@ -120,6 +126,11 @@ public static class QuestStateOperations if (progressStore.TryAdvanceStep(playerId, normalizedQuestId, newStepIndex, out var snapshot)) { + // --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_step_complete` --- + // TODO(E9.M1): catalog emit — once per committed step advance (chain quests may emit multiple per wiring pass). + // Planned payload fields: playerId, questId, completedStepIndex (prior index), newStepIndex; + // optional step id from catalog at E9.M1 wiring time. No ingest or ILogger here (comments-only). + QuestObjectiveWiring.TryProcessInventoryHasItemForQuest( playerId, normalizedQuestId, @@ -180,6 +191,11 @@ public static class QuestStateOperations if (progressStore.TryMarkComplete(playerId, normalizedQuestId, timeProvider.GetUtcNow(), out var snapshot)) { + // --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_complete` --- + // TODO(E9.M1): catalog emit — first-time completion only; idempotent replay returns above without hook. + // Planned payload fields: playerId, questId, completedAt; optional total step count. + // No ingest or ILogger here (comments-only). + return Success(snapshot); } @@ -208,11 +224,11 @@ public static class QuestStateOperations string playerId, string questId, QuestStepState attemptSnapshot) => - DenyFromProgressSnapshot(attemptSnapshot, QuestStateReasonCodes.AlreadyActive) + DenyFromProgressSnapshotAccept(attemptSnapshot, QuestStateReasonCodes.AlreadyActive) ?? (progressStore.TryGetProgress(playerId, questId, out var reread) - ? DenyFromProgressSnapshot(reread, QuestStateReasonCodes.AlreadyActive) + ? DenyFromProgressSnapshotAccept(reread, QuestStateReasonCodes.AlreadyActive) : null) - ?? Deny(QuestStateReasonCodes.UnknownPlayer); + ?? DenyAccept(QuestStateReasonCodes.UnknownPlayer); private static QuestStateOperationResult DenyAdvanceFailure( IPlayerQuestStateStore progressStore, @@ -248,9 +264,40 @@ public static class QuestStateOperations return null; } + private static QuestStateOperationResult? DenyFromProgressSnapshotAccept( + QuestStepState? snapshot, + string activeOrIntermediateReasonCode) + { + if (snapshot is null) + { + return null; + } + + if (snapshot.Status == QuestProgressStatus.Completed) + { + return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, snapshot); + } + + if (snapshot.Status == QuestProgressStatus.Active) + { + return DenyAccept(activeOrIntermediateReasonCode, snapshot); + } + + return null; + } + private static QuestStateOperationResult Success(QuestStepState snapshot) => new(Success: true, ReasonCode: null, Snapshot: snapshot); + private static QuestStateOperationResult DenyAccept(string reasonCode, QuestStepState? snapshot = null) + { + // --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_accept_denied` --- + // TODO(E9.M1): catalog emit — reasonCode from QuestStateReasonCodes (unknown_quest, prerequisite_incomplete, etc.). + // Planned payload fields: playerId, questId, reasonCode. No ingest or ILogger here (comments-only). + + return Deny(reasonCode, snapshot); + } + private static QuestStateOperationResult Deny(string reasonCode, QuestStepState? snapshot = null) => new(Success: false, ReasonCode: reasonCode, Snapshot: snapshot); } diff --git a/server/README.md b/server/README.md index f9c7784..49c7b56 100644 --- a/server/README.md +++ b/server/README.md @@ -250,6 +250,19 @@ Sample success: {"schemaVersion":1,"accepted":true,"quest":{"questId":"prototype_quest_gather_intro","status":"active","currentStepIndex":0,"objectiveCounters":{}}} ``` +### Quest telemetry hooks (NEO-121) + +Comment-only hook sites in **`QuestStateOperations`** for future E9.M1 catalog events ([Epic 7 Slice 1](../../docs/decomposition/modules/E7_M1_QuestStateMachine.md#risks-and-telemetry)). **`TODO(E9.M1)`** — no production ingest. HTTP **`POST …/accept`** and **`QuestObjectiveWiring`** delegate to the engine only (no duplicate API-layer or wiring-layer hooks). + +| Event | Anchor | When | +|-------|--------|------| +| **`quest_start`** | **`TryAccept`** | After successful **`TryActivate`** (first activation only). | +| **`quest_step_complete`** | **`TryAdvanceStep`** | After successful store commit (one emit per advance; chain quests may emit multiple per wiring pass). | +| **`quest_complete`** | **`TryMarkComplete`** | After first-time completion commit (not idempotent replay when already **`completed`**). | +| **`quest_accept_denied`** | **`TryAccept`** deny paths | Every structured accept deny with stable **`reasonCode`**. | + +Manual QA: [`docs/manual-qa/NEO-121.md`](../../docs/manual-qa/NEO-121.md); plan: [NEO-121 implementation plan](../../docs/plans/NEO-121-implementation-plan.md). + ## Encounter definitions (NEO-103) **`GET /game/world/encounter-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`encounters`**) backed by **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, nested **`completionCriteria`** (`kind`), **`requiredNpcInstanceIds`**, and nested **`rewardTable`** (`id`, `displayName`, **`fixedGrants`** with `itemId` + `quantity`). Plan: [NEO-103 implementation plan](../../docs/plans/NEO-103-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/encounter-definitions/`. From 89affe2b320b3116f5c9bbbf72ac35e99593bb64 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 7 Jun 2026 12:32:35 -0400 Subject: [PATCH 3/4] NEO-121: Add code review for quest telemetry hook sites. --- docs/reviews/2026-06-07-NEO-121.md | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/reviews/2026-06-07-NEO-121.md diff --git a/docs/reviews/2026-06-07-NEO-121.md b/docs/reviews/2026-06-07-NEO-121.md new file mode 100644 index 0000000..0c31cf8 --- /dev/null +++ b/docs/reviews/2026-06-07-NEO-121.md @@ -0,0 +1,57 @@ +# Code review — NEO-121 (E7M1-10) + +**Date:** 2026-06-07 +**Scope:** Branch `NEO-121-quest-telemetry-hooks` vs `origin/main` — commits `72b98a6` … `705a7cd` +**Base:** `origin/main` + +## Verdict + +**Approve with nits** + +## Summary + +NEO-121 adds **comment-only** E9.M1 telemetry hook sites to **`QuestStateOperations`** for Epic 7 Slice 1 events **`quest_start`**, **`quest_step_complete`**, **`quest_complete`**, and **`quest_accept_denied`**. Hooks are placed at mutation commit sites (after successful store writes) and accept-deny paths funnel through a new **`DenyAccept`** helper (mirroring NEO-56 **`inventory_transfer_denied`**). **`DenyFromProgressSnapshotAccept`** ensures activate-failure rereads also hit the deny hook. No runtime behavior, wire contract, or API surface changes — only comments plus accept-path deny routing that still delegates to **`Deny`**. Documentation is thorough: implementation plan reconciliation, **`server/README.md`** event table, E7.M1 module snapshot, alignment register, backlog checkboxes, and **`docs/manual-qa/NEO-121.md`**. Server-only infrastructure slice; Godot HUD correctly deferred to NEO-122. Risk is negligible. + +## Documentation checked + +| Path | Result | +|------|--------| +| `docs/plans/NEO-121-implementation-plan.md` | **Matches** — kickoff decisions adopted (engine-only anchors, deny helper, idempotent complete exclusion); acceptance checklist checked; reconciliation accurate. | +| `docs/plans/E7M1-prototype-backlog.md` (E7M1-10) | **Matches** — acceptance checkboxes checked; landed note + README link. | +| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | **Matches** — NEO-121 implementation snapshot + Risks and telemetry bullet. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7M1-10 / NEO-121 landed note appended. | +| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E7M1-10 note appended; row stays **Planned** until NEO-123 (per plan). | +| `server/README.md` | **Matches** — Quest telemetry hooks subsection with event table and delegation note. | +| `docs/manual-qa/NEO-121.md` | **Matches** — comment-only verification checklist; optional Bruno/regression steps. | +| Full-stack epic decomposition | **N/A** — infrastructure-only; plan explicitly documents no client counterpart. | + +**Register / tracking:** E7.M1 correctly remains **Planned** until client capstone NEO-123; E7M1-10 completion noted in register note only — no further status change required. + +## Blocking issues + +(none) + +## Suggestions + +(none) + +## Nits + +1. **Nit:** Plan technical approach lists optional **`schemaVersion`** in **`quest_start`** planned payload; the inline comment omits it. Harmless for comment-only work; consider adding for parity when touching the file again. + +2. **Nit:** **`DenyFromProgressSnapshotAccept`** duplicates **`DenyFromProgressSnapshot`** except for **`DenyAccept`** vs **`Deny`** — intentional funneling, but E9.M1 wiring may want **`playerId`** / **`questId`** threaded into **`DenyAccept`** (comment already documents those fields; implementer will need caller context). + +## Verification + +```bash +# Full regression (691 tests passed at review time) +dotnet test NeonSprawl.sln + +# Quest-focused subset +dotnet test NeonSprawl.sln --filter "FullyQualifiedName~Quest" +``` + +Manual (per `docs/manual-qa/NEO-121.md`): + +- Walk **`QuestStateOperations.cs`** hook comment blocks and confirm no duplicate hooks in **`QuestAcceptApi`**, **`QuestProgressApi`**, or **`QuestObjectiveWiring`**. +- Optional Bruno sanity in `bruno/neon-sprawl-server/quest-progress/` — accept happy path + duplicate deny unchanged. From 88d01778a164fe82ec54c59efa4764c641b15bec Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 7 Jun 2026 12:33:34 -0400 Subject: [PATCH 4/4] NEO-121: Address review nits for quest telemetry hook sites. Add schemaVersion to quest_start comment, thread playerId/questId into DenyAccept, and merge duplicate deny snapshot helpers. --- docs/plans/NEO-121-implementation-plan.md | 2 +- docs/reviews/2026-06-07-NEO-121.md | 4 +- .../Game/Quests/QuestStateOperations.cs | 62 ++++++++----------- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/docs/plans/NEO-121-implementation-plan.md b/docs/plans/NEO-121-implementation-plan.md index 4d0cddf..33fc418 100644 --- a/docs/plans/NEO-121-implementation-plan.md +++ b/docs/plans/NEO-121-implementation-plan.md @@ -56,7 +56,7 @@ - **`quest_start`:** after successful **`TryActivate`** in **`TryAccept`**. - **`quest_step_complete`:** after successful **`TryAdvanceStep`** store commit. - **`quest_complete`:** after first-time **`TryMarkComplete`** commit (not idempotent replay). -- **`quest_accept_denied`:** **`DenyAccept`** helper on all **`TryAccept`** deny paths. +- **`quest_accept_denied`:** **`DenyAccept(playerId, questId, …)`** on all **`TryAccept`** deny paths; optional accept context on **`DenyFromProgressSnapshot`** for activate-failure rereads. - **Docs:** `server/README.md` quest telemetry subsection; E7.M1 module + alignment register + E7M1 backlog updated. - **Manual QA:** `docs/manual-qa/NEO-121.md`. diff --git a/docs/reviews/2026-06-07-NEO-121.md b/docs/reviews/2026-06-07-NEO-121.md index 0c31cf8..708d1f6 100644 --- a/docs/reviews/2026-06-07-NEO-121.md +++ b/docs/reviews/2026-06-07-NEO-121.md @@ -37,9 +37,9 @@ NEO-121 adds **comment-only** E9.M1 telemetry hook sites to **`QuestStateOperati ## Nits -1. **Nit:** Plan technical approach lists optional **`schemaVersion`** in **`quest_start`** planned payload; the inline comment omits it. Harmless for comment-only work; consider adding for parity when touching the file again. +1. ~~**Nit:** Plan technical approach lists optional **`schemaVersion`** in **`quest_start`** planned payload; the inline comment omits it. Harmless for comment-only work; consider adding for parity when touching the file again.~~ **Done.** Added optional **`schemaVersion`** to **`quest_start`** inline comment. -2. **Nit:** **`DenyFromProgressSnapshotAccept`** duplicates **`DenyFromProgressSnapshot`** except for **`DenyAccept`** vs **`Deny`** — intentional funneling, but E9.M1 wiring may want **`playerId`** / **`questId`** threaded into **`DenyAccept`** (comment already documents those fields; implementer will need caller context). +2. ~~**Nit:** **`DenyFromProgressSnapshotAccept`** duplicates **`DenyFromProgressSnapshot`** except for **`DenyAccept`** vs **`Deny`** — intentional funneling, but E9.M1 wiring may want **`playerId`** / **`questId`** threaded into **`DenyAccept`** (comment already documents those fields; implementer will need caller context).~~ **Done.** Merged into **`DenyFromProgressSnapshot`** with optional accept-deny context; **`DenyAccept`** now takes **`playerId`** and **`questId`** at all call sites. ## Verification diff --git a/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs b/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs index a8ab09d..7dac722 100644 --- a/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs +++ b/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs @@ -25,22 +25,22 @@ public static class QuestStateOperations if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) || !questRegistry.TryGetDefinition(normalizedQuestId, out var definition)) { - return DenyAccept(QuestStateReasonCodes.UnknownQuest); + return DenyAccept(playerId, questId, QuestStateReasonCodes.UnknownQuest); } if (!progressStore.CanWritePlayer(playerId)) { - return DenyAccept(QuestStateReasonCodes.UnknownPlayer); + return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.UnknownPlayer); } if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing)) { if (existing.Status == QuestProgressStatus.Completed) { - return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, existing); + return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.AlreadyCompleted, existing); } - return DenyAccept(QuestStateReasonCodes.AlreadyActive, existing); + return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.AlreadyActive, existing); } foreach (var prerequisiteId in definition.PrerequisiteQuestIds) @@ -48,7 +48,7 @@ public static class QuestStateOperations if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) || prerequisite.Status != QuestProgressStatus.Completed) { - return DenyAccept(QuestStateReasonCodes.PrerequisiteIncomplete); + return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.PrerequisiteIncomplete); } } @@ -56,7 +56,8 @@ public static class QuestStateOperations { // --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_start` --- // TODO(E9.M1): catalog emit — first activation only (deny paths excluded). - // Planned payload fields: playerId, questId, currentStepIndex (0). No ingest or ILogger here (comments-only). + // Planned payload fields: playerId, questId, currentStepIndex (0), optional schemaVersion. + // No ingest or ILogger here (comments-only). QuestObjectiveWiring.TryProcessInventoryHasItemForQuest( playerId, @@ -224,11 +225,11 @@ public static class QuestStateOperations string playerId, string questId, QuestStepState attemptSnapshot) => - DenyFromProgressSnapshotAccept(attemptSnapshot, QuestStateReasonCodes.AlreadyActive) + DenyFromProgressSnapshot(attemptSnapshot, QuestStateReasonCodes.AlreadyActive, playerId, questId) ?? (progressStore.TryGetProgress(playerId, questId, out var reread) - ? DenyFromProgressSnapshotAccept(reread, QuestStateReasonCodes.AlreadyActive) + ? DenyFromProgressSnapshot(reread, QuestStateReasonCodes.AlreadyActive, playerId, questId) : null) - ?? DenyAccept(QuestStateReasonCodes.UnknownPlayer); + ?? DenyAccept(playerId, questId, QuestStateReasonCodes.UnknownPlayer); private static QuestStateOperationResult DenyAdvanceFailure( IPlayerQuestStateStore progressStore, @@ -244,7 +245,9 @@ public static class QuestStateOperations private static QuestStateOperationResult? DenyFromProgressSnapshot( QuestStepState? snapshot, - string activeOrIntermediateReasonCode) + string activeOrIntermediateReasonCode, + string? acceptDenyPlayerId = null, + string? acceptDenyQuestId = null) { if (snapshot is null) { @@ -253,34 +256,16 @@ public static class QuestStateOperations if (snapshot.Status == QuestProgressStatus.Completed) { - return Deny(QuestStateReasonCodes.AlreadyCompleted, snapshot); + return acceptDenyPlayerId is not null + ? DenyAccept(acceptDenyPlayerId, acceptDenyQuestId ?? string.Empty, QuestStateReasonCodes.AlreadyCompleted, snapshot) + : Deny(QuestStateReasonCodes.AlreadyCompleted, snapshot); } if (snapshot.Status == QuestProgressStatus.Active) { - return Deny(activeOrIntermediateReasonCode, snapshot); - } - - return null; - } - - private static QuestStateOperationResult? DenyFromProgressSnapshotAccept( - QuestStepState? snapshot, - string activeOrIntermediateReasonCode) - { - if (snapshot is null) - { - return null; - } - - if (snapshot.Status == QuestProgressStatus.Completed) - { - return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, snapshot); - } - - if (snapshot.Status == QuestProgressStatus.Active) - { - return DenyAccept(activeOrIntermediateReasonCode, snapshot); + return acceptDenyPlayerId is not null + ? DenyAccept(acceptDenyPlayerId, acceptDenyQuestId ?? string.Empty, activeOrIntermediateReasonCode, snapshot) + : Deny(activeOrIntermediateReasonCode, snapshot); } return null; @@ -289,11 +274,16 @@ public static class QuestStateOperations private static QuestStateOperationResult Success(QuestStepState snapshot) => new(Success: true, ReasonCode: null, Snapshot: snapshot); - private static QuestStateOperationResult DenyAccept(string reasonCode, QuestStepState? snapshot = null) + private static QuestStateOperationResult DenyAccept( + string playerId, + string questId, + string reasonCode, + QuestStepState? snapshot = null) { // --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_accept_denied` --- // TODO(E9.M1): catalog emit — reasonCode from QuestStateReasonCodes (unknown_quest, prerequisite_incomplete, etc.). - // Planned payload fields: playerId, questId, reasonCode. No ingest or ILogger here (comments-only). + // Planned payload fields: playerId, questId, reasonCode (caller context threaded for E9.M1 wiring). + // No ingest or ILogger here (comments-only). return Deny(reasonCode, snapshot); }