12 KiB
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 · Epic 7 Slice 2 · backlog E7M2-06 |
| Branch | NEO-129-quest-progress-completion-reward-summary |
| Precursor | NEO-128 Done — deliver-then-mark wiring in QuestStateOperations.TryMarkComplete |
| Pattern | NEO-108 — optional grant summary on completed rows from event/delivery store; NEO-119 — quest-progress GET envelope |
| Blocks | NEO-131 — client quest completion reward HUD (E7M2-08) |
| Client counterpart | 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)→ mapGrantedItems/GrantedSkillXponly; no second read from quest catalog bundle at HTTP layer. schemaVersion: stay 1 — additive optional field (NEO-108 precedent forrewardGrantSummary).- Row coverage:
not_started/activeomitcompletionRewardSummary; empty grant arrays included when summary is present (e.g. gather intro:itemGrants: [],skillXpGrants: [{ skillId: salvage, amount: 25 }]). - Mapper consistency: thread
IRewardDeliveryStorethroughMapQuestProgressRowsoQuestAcceptApiembed rows match GET whencompleted. - 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):
- Extend
QuestProgressApiDTOs + OpenAPI-stable JSON: per completed quest optionalcompletionRewardSummary(nesteditemGrants+skillXpGrants). - Populate from
IRewardDeliveryStoredelivery 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.mdquest-progress section update; E7.M2 module alignment anchor.
Out of scope (from Linear):
- Godot HUD (NEO-131 E7M2-08).
- New top-level route;
QuestStateOperationResultshape changes; telemetry hooks (NEO-130). - Postgres delivery-store persistence.
Acceptance criteria checklist
- Completed quest rows include
completionRewardSummarywhen delivery recorded. not_started/activerows omit summary.
Implementation reconciliation (shipped)
- DTOs:
QuestCompletionRewardSummaryJsonwith nesteditemGrants+skillXpGrants; optionalcompletionRewardSummaryonQuestProgressRowJson. - Projection:
QuestProgressApi.BuildSnapshot/MapQuestProgressRowreadIRewardDeliveryStore.TryGet; omit summary when no delivery event (kickoff). - Accept:
QuestAcceptApi.MapResponsethreadsplayerId+deliveryStoreinto 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 onnot_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: acceptIRewardDeliveryStore; pass into per-row mapper.MapQuestProgressRow(playerId, questId, progressStore, deliveryStore)andMapQuestProgressRow(playerId, snapshot, deliveryStore):- On
completed:deliveryStore.TryGet→ when hit, map grant snapshots preserving commit-time order (mirrorEncounterProgressApi.MapGrantSummary). - When miss: leave
completionRewardSummarynull (omitted in JSON). - On
active/not_started: no store lookup; summary omitted.
- On
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 asQuestProgressApiTests). - GET
/game/players/{{playerId}}/quest-progress→ assert gather rowstatus: completed,completionRewardSummary.skillXpGrantsmatches salvage 25,itemGrantsempty array; second GET unchanged.
6. Docs
server/README.md— extend quest-progress table withcompletionRewardSummaryprojection 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
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. |
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 |