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

12 KiB
Raw Permalink Blame History

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) → 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):

  • 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 E7M2-08).
  • New top-level route; QuestStateOperationResult shape changes; telemetry hooks (NEO-130).
  • Postgres delivery-store persistence.

Acceptance criteria checklist

  • Completed quest rows include completionRewardSummary when delivery recorded.
  • 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.
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

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_WhenObjectivesSatisfiedskillXpGrants 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