NEO-129: add implementation plan for quest-progress completionRewardSummary.

pull/168/head
VinPropane 2026-06-07 18:37:35 -04:00
parent c1849740b2
commit 2b06eb2a1e
1 changed files with 178 additions and 0 deletions

View File

@ -0,0 +1,178 @@
# 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
- [ ] Completed quest rows include **`completionRewardSummary`** when delivery recorded.
- [ ] **`not_started`** / **`active`** rows omit summary.
## 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** |