# E7.M2 — Prototype story backlog (RewardAndUnlockRouter) Working backlog for **Epic 7 — Slice 2** ([reward and unlock routing](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing)). Decomposition and contracts: [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md). **Full-stack policy:** Epics deliver **playable game features**. Every **player-visible** story has a **`client`** Linear issue created in **this same decomposition pass** — not an undocumented follow-up. Reference: [E7M1 (paired server+client)](../plans/E7M1-prototype-backlog.md), [E5M3 (encounter loot capstone)](../plans/E5M3-prototype-backlog.md). **Labels (Linear):** every issue **`E7.M2`**; add **`server`** or **`client`** + **`Story`** as listed. **Precursor (do not re-scope):** [E7.M1](../decomposition/modules/E7_M1_QuestStateMachine.md) Slice 1 **Ready** — quest state + objectives only; no duplicate completion grants on replay ([NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)). [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) encounter loot stays on **`EncounterCompletionOperations`** + **`rewardGrantSummary`** on encounter-progress GET — **not** migrated through E7.M2 in Slice 2. **`EncounterCompleteEvent`** ([NEO-107](https://linear.app/neon-sprawl/issue/NEO-107)) remains a future consumer hook. **NEO-43** [`MissionRewardSkillXpGrant`](../../server/NeonSprawl.Server/Game/Skills/MissionRewardSkillXpGrant.cs) is the skill-XP apply path for bundle rows with **`sourceKind: mission_reward`**. **Upstream (must be landed):** [E7.M1](E7_M1_QuestStateMachine.md) **Ready** ([NEO-112](https://linear.app/neon-sprawl/issue/NEO-112)–[NEO-123](https://linear.app/neon-sprawl/issue/NEO-123)); [E2.M2](E2_M2_XpAwardAndLevelEngine.md) grant stack ([NEO-38](https://linear.app/neon-sprawl/issue/NEO-38), [NEO-43](https://linear.app/neon-sprawl/issue/NEO-43)); [E3.M3](E3_M3_ItemizationAndInventorySchema.md) inventory ([NEO-54](https://linear.app/neon-sprawl/issue/NEO-54)–[NEO-55](https://linear.app/neon-sprawl/issue/NEO-55)). [NEO-122](https://linear.app/neon-sprawl/issue/NEO-122) quest HUD for client reward surfacing. **Prototype reward spine (frozen in E7M2-01):** **`completionRewardBundle`** on each of the **four** frozen [E7.M1 quests](E7_M1_QuestStateMachine.md#prototype-slice-1-freeze-e7m1-01) — **quest completion only** (not per-step). Grant kinds v1: **`itemGrants`** + **`skillXpGrants`** only. **Solo only** — per-character idempotent keys ([quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md)). **Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the story branch when work starts. | Slug | Layer | Linear | |------|-------|--------| | E7M2-01 | server | [NEO-124](https://linear.app/neon-sprawl/issue/NEO-124) | | E7M2-02 | server | [NEO-125](https://linear.app/neon-sprawl/issue/NEO-125) | | E7M2-03 | server | [NEO-126](https://linear.app/neon-sprawl/issue/NEO-126) | | E7M2-04 | server | [NEO-127](https://linear.app/neon-sprawl/issue/NEO-127) | | E7M2-05 | server | [NEO-128](https://linear.app/neon-sprawl/issue/NEO-128) | | E7M2-06 | server | [NEO-129](https://linear.app/neon-sprawl/issue/NEO-129) | | E7M2-07 | server | [NEO-130](https://linear.app/neon-sprawl/issue/NEO-130) | | E7M2-08 | client | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) | | E7M2-09 | client | [NEO-132](https://linear.app/neon-sprawl/issue/NEO-132) | **Dependency graph in Linear:** E7M2-02 blocked by E7M2-01. E7M2-03 blocked by E7M2-01. E7M2-04 blocked by E7M2-02 and E7M2-03. E7M2-05 blocked by E7M2-04. E7M2-06 blocked by E7M2-05. E7M2-07 blocked by E7M2-05 (may parallel E7M2-06). E7M2-08 blocked by E7M2-06. E7M2-09 blocked by E7M2-08. **Board order:** estimates **1–9** matching slug order (E7M2-01 = 1 … E7M2-09 = 9). On the Epic 7 project board, sort by **Estimate** (ascending). --- ## Story order (recommended) | Order | Slug | Layer | Depends on | |-------|------|-------|------------| | 1 | **E7M2-01** | server | E7.M1 quest catalog frozen | | 2 | **E7M2-02** | server | E7M2-01 | | 3 | **E7M2-03** | server | E7M2-01 | | 4 | **E7M2-04** | server | E7M2-02, E7M2-03 | | 5 | **E7M2-05** | server | E7M2-04 | | 6 | **E7M2-06** | server | E7M2-05 | | 7 | **E7M2-07** | server | E7M2-05 | | 8 | **E7M2-08** | client | E7M2-06 | | 9 | **E7M2-09** | client | E7M2-08 | **Downstream (separate modules):** [E6.M4](../decomposition/modules/E6_M4_RewardParityEnforcer.md) lint against bundles; [E7.M3](../decomposition/modules/E7_M3_FactionReputationLedger.md) reputation grants (pre-production); E5.M3 **`EncounterCompleteEvent`** full router consumer (optional follow-on). --- ## Kickoff decisions (decomposition defaults) | Topic | Decision | Rationale | |-------|----------|-----------| | Reward trigger | **Quest completion only** (not per-step) | Slice 2 AC is idempotent **completion** delivery; step bundles add scope without prototype need | | Grant kinds (v1) | **`itemGrants`** + **`skillXpGrants`** (`mission_reward` at apply) | [NEO-43](NEO-43-implementation-plan.md) prep; unlock/flag grants deferred | | Unlock / world gates | **Deferred** to pre-production | Epic mentions unlocks; no prototype gate content yet — comment-only **`unlock_granted`** hook | | Encounter loot | **Unchanged** — E5.M3 direct grants | Avoid breaking [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) / [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); `EncounterCompleteEvent` consumer is follow-on | | Bundle attachment | **`completionRewardBundle`** on each frozen **four** quests | Readable capstone; CI enforces shape on all E7.M1 ids | | Idempotency key | **`{playerId}:quest_complete:{questId}`** | [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md); once per character per quest | | Delivery store | **In-memory** **`IRewardDeliveryStore`** (+ optional Postgres migration) | Matches encounter/quest prototype stores | | Fail-closed | **Quest complete rolls back** if bundle apply fails after mark (inventory full deny) | Mirror [NEO-105](NEO-105-implementation-plan.md) encounter completion compensating rollback | | HTTP read | **Extend** **`GET …/quest-progress`** with **`completionRewardSummary`** per completed quest | Mirror encounter **`rewardGrantSummary`** ([NEO-108](NEO-108-implementation-plan.md)); no new top-level route in Slice 2 | | Party mode | **Solo only** | E8.M1 not landed | | HUD fidelity | Prototype labels / debug panels | NEO-122 / NEO-110 precedent | ### Prototype completion bundles (E7M2-01 freeze — tunable display only) | Quest id | Item grants | Skill XP grants | |----------|-------------|-----------------| | `prototype_quest_gather_intro` | — | **`salvage`** **25** (`mission_reward`) | | `prototype_quest_refine_intro` | — | **`refine`** **25** (`mission_reward`) | | `prototype_quest_combat_intro` | — | **`salvage`** **25** (`mission_reward`) | | `prototype_quest_operator_chain` | **`survey_drone_kit` ×1** | **`salvage`** **50** (`mission_reward`) | Combat intro bundle is **skill XP only** — encounter loot already granted **`contract_handoff_token`** via E5.M3 (no double item payout on same milestone). --- ### E7M2-01 — QuestRewardBundle schema + quest catalog extension + CI **Goal:** Lock **`completionRewardBundle`** content shape and CI validation on the four frozen quests before server load. **In scope** - `content/schemas/quest-reward-bundle.schema.json`, `content/schemas/quest-skill-xp-grant.schema.json` (reuse or align with existing grant row patterns). - Extend `content/schemas/quest-def.schema.json` with optional **`completionRewardBundle`**. - Update `content/quests/prototype_quests.json` — bundles per freeze table above. - `scripts/validate_content.py`: bundle schema validation, item id cross-refs, skill id cross-refs, `mission_reward` allowlist alignment with `prototype_skills.json`. - Designer note in [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) + `content/README.md`. **Out of scope** - Server loader, delivery store, router, HTTP, Godot. **Acceptance criteria** - [x] PR gate validates quest JSON including optional completion bundles. - [x] All four prototype quests include a **`completionRewardBundle`** row matching freeze table. - [x] Invalid item/skill refs fail CI. **Client counterpart:** none (infrastructure-only). --- ### E7M2-02 — Server quest catalog load: reward bundle validation **Goal:** Fail-fast startup validation of **`completionRewardBundle`** with CI parity. **In scope** - Extend `server/NeonSprawl.Server/Game/Quests/` catalog loader / registry types. - Cross-check bundle grants against item + skill registries at startup. - Unit tests (AAA) for happy path and broken bundle refs. - `server/README.md` section. **Out of scope** - Delivery apply, HTTP projection changes. **Acceptance criteria** - [x] Host exits on invalid bundle refs at startup. - [x] `dotnet test` covers loader gates. **Client counterpart:** none (infrastructure-only). --- ### E7M2-03 — Reward delivery store (`IRewardDeliveryStore`) **Goal:** Durable idempotency record for **`RewardDeliveryEvent`** per player + quest completion. **In scope** - `IRewardDeliveryStore`, in-memory implementation, optional Postgres migration (NEO-29-style split). - Idempotency key **`{playerId}:quest_complete:{questId}`**; store granted item + skill XP summary for audit/read projection. - Unit tests: first delivery applies; replay returns existing event without double-apply. - `server/README.md` store section. **Out of scope** - Router apply logic (E7M2-04), quest-state wiring (E7M2-05). **Acceptance criteria** - [x] `TryRecord` idempotent on same key. - [x] Store readable summary for HTTP projection. **Client counterpart:** none (infrastructure-only). --- ### E7M2-04 — `RewardRouterOperations` (apply `QuestRewardBundle`) **Goal:** Server-internal apply for item + skill XP completion bundles with structured outcomes. **In scope** - `RewardRouterOperations.TryDeliverQuestCompletion` in `server/NeonSprawl.Server/Game/Quests/` (or `Game/Rewards/`). - Item grants via **`PlayerInventoryOperations`**; skill XP via **`SkillProgressionGrantOperations.TryApplyGrant`** with fixed **`mission_reward`** source kind (deny envelopes; not the fire-and-forget **`MissionRewardSkillXpGrant`** wrapper). - Deny reason codes (inventory full, unknown item/skill, xp deny). - Unit + integration tests (AAA): happy path, inventory deny, idempotent replay via store. **Out of scope** - Quest state transition hooks (E7M2-05), HTTP DTOs (E7M2-06). **Acceptance criteria** - [x] Bundle rows apply atomically where possible; compensating rollback on partial failure before store record. - [x] Skill XP uses **`mission_reward`** source kind only. **Client counterpart:** none (server engine). --- ### E7M2-05 — Wire router into `QuestStateOperations.TryMarkComplete` **Goal:** First-time quest completion triggers bundle delivery exactly once. **In scope** - Call **`RewardRouterOperations`** from **`TryMarkComplete`** success path (after state commit or transactional equivalent per fail-closed decision). - Skip delivery on idempotent complete replay (already `Completed` status). - Integration tests: complete gather intro → inventory/skills reflect bundle; replay → no duplicate grants. - Bruno smoke in `bruno/neon-sprawl-server/quest-progress/` (optional subfolder). **Out of scope** - Encounter completion path; step-complete bundles. **Acceptance criteria** - [x] Completing a quest via objective wiring delivers bundle once. - [x] Idempotent complete does not double-grant items or XP. **Client counterpart:** [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) — HUD surfacing (blocked by E7M2-06). --- ### E7M2-06 — Extend `GET …/quest-progress` with `completionRewardSummary` **Goal:** Client-readable grant summary on completed quest rows (mirror encounter progress). **In scope** - Extend `QuestProgressApi` DTOs + OpenAPI-stable JSON: per completed quest optional **`completionRewardSummary`** (item lines + skill XP lines). - Populate from **`IRewardDeliveryStore`** / delivery event snapshot. - Integration tests + Bruno assertions on gather intro complete path. - `server/README.md` quest-progress section update. **Out of scope** - Godot HUD (E7M2-08). **Acceptance criteria** - [x] Completed quest rows include summary when delivery recorded. - [x] `not_started` / `active` rows omit summary. **Client counterpart:** [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131). --- ### E7M2-07 — Reward telemetry hook sites (comment-only) **Goal:** Anchor future E9.M1 catalog events without ingest. **In scope** - Comment-only **`reward_delivery`** in **`RewardRouterOperations`** (first-time success). - Comment-only **`unlock_granted`** stub (no runtime unlock apply in Slice 2). - `server/README.md` hook list. **Out of scope** - E9.M1 ingest, unlock engine. **Acceptance criteria** - [x] Hook sites documented in README; no behavior change beyond comments. **Landed ([NEO-130](https://linear.app/neon-sprawl/issue/NEO-130)):** comment-only **`reward_delivery`** + **`unlock_granted`** stub in **`RewardRouterOperations`**; [server README — Reward telemetry hooks (NEO-130)](../../server/README.md#reward-telemetry-hooks-neo-130); plan [NEO-130](../../plans/NEO-130-implementation-plan.md). **Client counterpart:** none (infrastructure-only). --- ### E7M2-08 — Client quest completion reward HUD (Godot) **Goal:** Player sees quest completion rewards in Godot without Bruno. **In scope** - Extend **`quest_progress_client.gd`** parse for **`completionRewardSummary`**. - **`QuestRewardDeliveryLabel`** (or extend **`QuestProgressLabel`** / **`quest_hud_controller.gd`**) — compact grant lines when quest transitions to **`completed`**. - Refresh on quest-progress GET after objective events (reuse NEO-122 triggers). - GdUnit parse tests; `client/README.md` section. - `docs/manual-qa/NEO-131.md` component checklist. **Out of scope** - Final quest journal UI; reward VFX. **Acceptance criteria** - [ ] Completing a quest shows readable reward copy on HUD. - [ ] Failed GET surfaces visible error (NEO-122 pattern). **Server counterpart:** [NEO-129](https://linear.app/neon-sprawl/issue/NEO-129). --- ### E7M2-09 — Playable quest reward delivery capstone (Godot) **Goal:** Prove Epic 7 Slice 2 acceptance **in Godot**: quest completion grants items + skill XP **once**; idempotent on replay. **In scope** - **`docs/manual-qa/NEO-132.md`**: single-session capstone — extend [NEO-123](NEO-123.md) flow or shorter reward-focused path; verify **`completionRewardSummary`** + inventory/skills; Godot restart + duplicate complete attempt unchanged. - **`client/README.md`**: **End-to-end quest reward loop** section; cross-links NEO-124–NEO-131. - Module alignment on completion: [E7_M2](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md), [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md), [module_dependency_register.md](../decomposition/modules/module_dependency_register.md). **Out of scope** - Encounter loot migration; faction reputation; Bruno-only proof. **Acceptance criteria** - [ ] Human completes **`docs/manual-qa/NEO-132.md`** with server + client. - [ ] Epic 7 Slice 2 AC: idempotent reward delivery; replays cannot double-claim. - [ ] Re-read [epic_07 Slice 2 AC](../decomposition/epics/epic_07_quest_faction.md#slice-2---reward-and-unlock-routing). **Client counterpart:** this story **is** the Slice 2 client capstone. --- ## Decomposition complete checklist - [x] Every player-visible AC has a **`client`** Linear issue (NEO-131, NEO-132) - [x] No forbidden deferral language in backlog - [x] Capstone client story exists (E7M2-09 / NEO-132) - [x] Epic DoD updated for Godot verification (see epic_07 update) - [x] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E7.M2 row updated (decomposition pass) - [x] [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E7.M2 note updated ## Related docs - [epic_07_quest_faction.md](../decomposition/epics/epic_07_quest_faction.md) - [E7M1-prototype-backlog.md](E7M1-prototype-backlog.md) - [NEO-43 implementation plan](NEO-43-implementation-plan.md) - [quest_scope_and_party.md](../decomposition/modules/quest_scope_and_party.md) - [neon_sprawl_vision.plan.md — Prototype Scope](../../neon_sprawl_vision.plan.md)