neon-sprawl/docs/plans/NEO-129-implementation-plan.md

188 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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** |