NEO-129: add implementation plan for quest-progress completionRewardSummary.
parent
c1849740b2
commit
2b06eb2a1e
|
|
@ -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** |
|
||||
Loading…
Reference in New Issue