# NEO-129 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-129 | | **Title** | E7M2-06: Extend GET quest-progress with completionRewardSummary | | **Linear** | https://linear.app/neon-sprawl/issue/NEO-129/e7m2-06-extend-get-quest-progress-with-completionrewardsummary | | **Module** | [E7.M2 — RewardAndUnlockRouter](../decomposition/modules/E7_M2_RewardAndUnlockRouter.md) · Epic 7 Slice 2 · backlog **E7M2-06** | | **Branch** | `NEO-129-quest-progress-completion-reward-summary` | | **Precursor** | [NEO-128](https://linear.app/neon-sprawl/issue/NEO-128) **`Done`** — deliver-then-mark wiring in `QuestStateOperations.TryMarkComplete` | | **Pattern** | [NEO-108](NEO-108-implementation-plan.md) — optional grant summary on completed rows from event/delivery store; [NEO-119](NEO-119-implementation-plan.md) — quest-progress GET envelope | | **Blocks** | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) — client quest completion reward HUD (E7M2-08) | | **Client counterpart** | [NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) — parse **`completionRewardSummary`** in Godot; **not in this story** | ## Kickoff clarifications | Topic | Question | Agent recommendation | Answer | |--------|----------|----------------------|--------| | **`completionRewardSummary` shape** | How should item + skill XP lines appear in JSON? | **Nested object** `{ itemGrants: [{ itemId, quantity }], skillXpGrants: [{ skillId, amount }] }` — mirrors content **`completionRewardBundle`** and **`RewardDeliveryEvent`** grant snapshots; NEO-131 needs both kinds. | **Adopted** — nested bundle shape | | **Missing delivery event** | When **`status: completed`** but **`IRewardDeliveryStore.TryGet`** misses? | **Omit** **`completionRewardSummary`** — matches Linear AC (“when delivery recorded”); softer than NEO-108 encounter fail-fast. | **Adopted** — omit when no event | **Additional defaults (no kickoff question — settled by backlog / landed code):** - **Source of truth:** **`IRewardDeliveryStore.TryGet(playerId, questId)`** → map **`GrantedItems`** / **`GrantedSkillXp`** only; no second read from quest catalog bundle at HTTP layer. - **`schemaVersion`:** stay **1** — additive optional field (NEO-108 precedent for **`rewardGrantSummary`**). - **Row coverage:** **`not_started`** / **`active`** omit **`completionRewardSummary`**; empty grant arrays included when summary is present (e.g. gather intro: **`itemGrants: []`**, **`skillXpGrants: [{ skillId: salvage, amount: 25 }]`**). - **Mapper consistency:** thread **`IRewardDeliveryStore`** through **`MapQuestProgressRow`** so **`QuestAcceptApi`** embed rows match GET when **`completed`**. - **Manual QA doc:** skip **`docs/manual-qa/NEO-129.md`** — server-only; Bruno + API integration tests (NEO-108 / NEO-119 precedent). ## Goal, scope, and out-of-scope **Goal:** Client-readable grant summary on completed quest rows over existing **`GET /game/players/{id}/quest-progress`**. **In scope (from Linear + [E7M2-06](E7M2-prototype-backlog.md#e7m2-06--extend-get-quest-progress-with-completionrewardsummary)):** - Extend **`QuestProgressApi`** DTOs + OpenAPI-stable JSON: per completed quest optional **`completionRewardSummary`** (nested **`itemGrants`** + **`skillXpGrants`**). - Populate from **`IRewardDeliveryStore`** delivery event snapshot. - Integration tests (AAA): omit on **`not_started`** / **`active`**; present on gather-intro complete + operator-chain complete; idempotent GET unchanged. - Bruno assertion on gather-intro complete path. - `server/README.md` quest-progress section update; E7.M2 module alignment anchor. **Out of scope (from Linear):** - Godot HUD ([NEO-131](https://linear.app/neon-sprawl/issue/NEO-131) E7M2-08). - New top-level route; **`QuestStateOperationResult`** shape changes; telemetry hooks ([NEO-130](https://linear.app/neon-sprawl/issue/NEO-130)). - Postgres delivery-store persistence. ## Acceptance criteria checklist - [x] Completed quest rows include **`completionRewardSummary`** when delivery recorded. - [x] **`not_started`** / **`active`** rows omit summary. ## Implementation reconciliation (shipped) - **DTOs:** **`QuestCompletionRewardSummaryJson`** with nested **`itemGrants`** + **`skillXpGrants`**; optional **`completionRewardSummary`** on **`QuestProgressRowJson`**. - **Projection:** **`QuestProgressApi.BuildSnapshot`** / **`MapQuestProgressRow`** read **`IRewardDeliveryStore.TryGet`**; omit summary when no delivery event (kickoff). - **Accept:** **`QuestAcceptApi.MapResponse`** threads **`playerId`** + **`deliveryStore`** into row mapper. - **Tests:** eleven AAA tests in **`QuestProgressApiTests`** — omit on default/active; gather complete salvage **25**; operator chain item + XP; GET-refresh chain terminal summary; omit when completed without delivery record; full-summary idempotent GET replay (gather + operator chain). **`QuestAcceptApiTests`** — null summary on active accept; summary on already-completed deny row. - **Bruno:** **`Get quest progress after gather intro complete.bru`**; default GET asserts summary omitted on **`not_started`**. - **Docs:** **`server/README.md`**; E7.M2 module anchor; alignment register; E7M2-06 backlog AC. ## Technical approach ### 1. Wire DTOs (`QuestProgressListDtos.cs`) Add grant line types and summary wrapper (camelCase JSON, stable with content schemas): | Type | JSON fields | |------|-------------| | **`QuestItemGrantJson`** | **`itemId`**, **`quantity`** | | **`QuestSkillXpGrantJson`** | **`skillId`**, **`amount`** | | **`QuestCompletionRewardSummaryJson`** | **`itemGrants`**, **`skillXpGrants`** | Add optional **`completionRewardSummary`** on **`QuestProgressRowJson`** with **`JsonIgnore(WhenWritingNull)`**. Reuse item line shape from **`EncounterRewardGrantJson`** field names; skill lines match **`quest-skill-xp-grant.schema.json`**. ### 2. Extend projection (`QuestProgressApi.cs`) - **`BuildSnapshot`**: accept **`IRewardDeliveryStore`**; pass into per-row mapper. - **`MapQuestProgressRow(playerId, questId, progressStore, deliveryStore)`** and **`MapQuestProgressRow(playerId, snapshot, deliveryStore)`**: - On **`completed`**: **`deliveryStore.TryGet`** → when hit, map grant snapshots preserving commit-time order (mirror **`EncounterProgressApi.MapGrantSummary`**). - When miss: leave **`completionRewardSummary`** null (omitted in JSON). - On **`active`** / **`not_started`**: no store lookup; summary omitted. ```csharp private static QuestCompletionRewardSummaryJson? MapCompletionRewardSummary( string playerId, string questId, IRewardDeliveryStore deliveryStore) { if (!deliveryStore.TryGet(playerId, questId, out var deliveryEvent)) { return null; } return new QuestCompletionRewardSummaryJson { ItemGrants = MapItemGrants(deliveryEvent.GrantedItems), SkillXpGrants = MapSkillXpGrants(deliveryEvent.GrantedSkillXp), }; } ``` ### 3. Accept response alignment (`QuestAcceptApi.cs`) **`MapResponse`**: when embedding **`QuestProgressRowJson`**, call **`MapQuestProgressRow(result.PlayerId, snapshot, deliveryStore)`** — requires threading **`playerId`** + **`deliveryStore`** into **`MapResponse`** from the route handler (already injected). ### 4. Prototype row expectations (frozen E7M2-01) | Quest id | **`completionRewardSummary`** when completed | |----------|-----------------------------------------------| | **`prototype_quest_gather_intro`** | **`itemGrants: []`**, **`skillXpGrants: [{ skillId: salvage, amount: 25 }]`** | | **`prototype_quest_refine_intro`** | **`itemGrants: []`**, **`skillXpGrants: [{ skillId: refine, amount: 25 }]`** | | **`prototype_quest_combat_intro`** | **`itemGrants: []`**, **`skillXpGrants: [{ skillId: salvage, amount: 25 }]`** | | **`prototype_quest_operator_chain`** | **`itemGrants: [{ itemId: survey_drone_kit, quantity: 1 }]`**, **`skillXpGrants: [{ skillId: salvage, amount: 50 }]`** | ### 5. Bruno (`bruno/neon-sprawl-server/quest-progress/`) **`Get quest progress after gather intro complete.bru`** (new): - Pre-request: accept gather intro (idempotent) → move near **`prototype_resource_node_alpha`** → interact/gather until objective completes (3× scrap on alpha, same spine as **`QuestProgressApiTests`**). - GET **`/game/players/{{playerId}}/quest-progress`** → assert gather row **`status: completed`**, **`completionRewardSummary.skillXpGrants`** matches salvage **25**, **`itemGrants`** empty array; second GET unchanged. ### 6. Docs - **`server/README.md`** — extend quest-progress table with **`completionRewardSummary`** projection source (**`IRewardDeliveryStore`**), sample JSON snippet for gather intro. - **`E7_M2_RewardAndUnlockRouter.md`** — NEO-129 HTTP anchor. - **`documentation_and_implementation_alignment.md`** — E7.M2 row note when landed. - **`E7M2-prototype-backlog.md`** — check E7M2-06 AC when complete. ### Read flow ```mermaid sequenceDiagram participant Client participant API as QuestProgressApi participant Wiring as QuestObjectiveWiring participant Store as IPlayerQuestStateStore participant Delivery as IRewardDeliveryStore Client->>API: GET /game/players/{id}/quest-progress API->>Wiring: TryRefreshInventoryProgressForPlayer API->>Store: per catalog quest TryGetProgress alt status completed API->>Delivery: TryGet(playerId, questId) Delivery-->>API: RewardDeliveryEvent or miss end API-->>Client: schemaVersion 1 + quests[] (+ optional completionRewardSummary) ``` ## Files to add | Path | Purpose | |------|---------| | `bruno/neon-sprawl-server/quest-progress/Get quest progress after gather intro complete.bru` | Gather-intro complete spine → GET asserts **`completionRewardSummary`**. | | `docs/plans/NEO-129-implementation-plan.md` | This plan. | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/Quests/QuestProgressListDtos.cs` | Add grant line + **`completionRewardSummary`** wrapper types; optional field on row DTO. | | `server/NeonSprawl.Server/Game/Quests/QuestProgressApi.cs` | Thread **`IRewardDeliveryStore`** through **`BuildSnapshot`** / **`MapQuestProgressRow`**; map delivery event → summary. | | `server/NeonSprawl.Server/Game/Quests/QuestAcceptApi.cs` | Pass **`playerId`** + **`deliveryStore`** into row mapper for embedded quest rows. | | `server/NeonSprawl.Server.Tests/Game/Quests/QuestProgressApiTests.cs` | Assert summary omitted on default/active; present on gather complete + operator chain; idempotent replay. | | `server/README.md` | Document **`completionRewardSummary`** on quest-progress GET. | | `docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md` | NEO-129 implementation anchor. | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M2 alignment register when landed. | | `docs/plans/E7M2-prototype-backlog.md` | Mark E7M2-06 acceptance criteria when complete. | ## Tests | Test file | What it covers | |-----------|----------------| | `QuestProgressApiTests.cs` | **Omit:** existing **`not_started`** / **`active`** tests assert **`CompletionRewardSummary`** null. **Gather complete:** extend **`GetQuestProgress_ShouldReturnCompletedGatherIntro_WhenObjectivesSatisfied`** — **`skillXpGrants`** salvage **25**, empty **`itemGrants`**. **Operator chain:** extend chain complete test — **`survey_drone_kit` ×1** + salvage **50**. **Idempotent read:** second GET same summary (new test or assert in gather test). **AAA** per [csharp-style](../../.cursor/rules/csharp-style.md). | ## Open questions / risks | Question / risk | Agent recommendation | Status | |-----------------|----------------------|--------| | **Completed without delivery event** | Omit summary per kickoff; no throw. | **adopted** | | **Empty `itemGrants` in JSON** | Include **`[]`** when summary present — explicit for client parsers (NEO-131). | **adopted** | | **Bruno shared player state** | Pre-request idempotent accept + gather; tolerate prior collection seq (NEO-119 folder pattern). | **adopted** | | **NEO-131 display copy** | Defer formatting / HUD timing to client story. | **deferred** |