188 lines
12 KiB
Markdown
188 lines
12 KiB
Markdown
# 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** |
|