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

12 KiB
Raw Permalink Blame History

NEO-108 — Implementation plan

Story reference

Field Value
Key NEO-108
Title E5M3-08: GET encounter-progress + wire DTOs + Bruno
Linear https://linear.app/neon-sprawl/issue/NEO-108/e5m3-08-get-encounter-progress-wire-dtos-bruno
Module E5.M3 — EncounterAndRewardTables · Epic 5 Slice 3 · backlog E5M3-08
Branch NEO-108-get-encounter-progress-wire-dtos-bruno
Precursor NEO-104 progress + completion stores · NEO-105 completion grants · NEO-106 combat wiring · NEO-107 complete event store (landed on main)
Pattern NEO-103 — world/player GET + Bruno; NEO-44 — player snapshot GET + position gate; NEO-106 — defeat-all-three cast helpers
Blocks NEO-110 — client encounter HUD poll/refresh
Client counterpart NEO-110 — Godot labels poll this GET; not in this story

Kickoff clarifications

Topic Question Agent recommendation Answer
rewardGrantSummary source Event store vs reward table vs fallback? IEncounterCompleteEventStore.GrantedItems on completed — commit-time snapshot (NEO-107). Adopted — event store
Encounter rows in GET All catalog encounters vs only non-inactive? All from GetDefinitionsInIdOrder() with derived inactive / active / completed. Adopted (backlog)
Player gate 404 for unknown player? IPositionStateStore.TryGetPosition — gig/skill progression precedent. Adopted
Optional fields completedAt / rewardGrantSummary when not complete? Omit (or JSON null) when not completed; defeatedTargetIds = [] when inactive. Adopted
Integration tests Dedicated API tests vs cast-only? EncounterProgressApiTests + Bruno three-NPC spine; optional GET assertions after existing cast tests deferred unless useful. Adopted
Manual QA doc docs/manual-qa/NEO-108.md? Skip — server-only (NEO-103 precedent); Bruno + automated API tests. Adopted

Goal, scope, and out-of-scope

Goal: Expose authoritative per-player encounter progress over HTTP so clients and Bruno can read server-owned activation, defeat subset, completion timestamp, and grant summary without inferring from combat responses.

In scope (from Linear + E5M3-08):

  • GET /game/players/{id}/encounter-progress with versioned envelope (schemaVersion 1, encounters array).
  • Per-encounter row: encounterId, state (inactive | active | completed), defeatedTargetIds, optional completedAt, optional rewardGrantSummary (itemId + quantity grants).
  • Projection from IEncounterDefinitionRegistry, IEncounterProgressStore, IEncounterCompletionStore, IEncounterCompleteEventStore (no second source of truth).
  • EncounterProgressApi + wire DTOs in Game/Encounters/.
  • Integration tests (AAA): unknown player 404; inactive default; partial progress after 12 defeats; completed once with grant summary.
  • Bruno bruno/neon-sprawl-server/encounter-progress/ — self-contained three-NPC defeat pre-request → GET completed once.
  • server/README.md route section; E5.M3 module snapshot + alignment register when landed.

Out of scope (from Linear + backlog):

  • Godot poll client — client counterpart NEO-110.
  • Telemetry hook sites (NEO-109).
  • Postgres persistence, cast response DTO changes.
  • docs/manual-qa/NEO-108.md (server-only per kickoff).

Acceptance criteria checklist

  • GET reflects progress after each defeat without client-side inference.
  • Completed encounter shows completed state and grant summary.
  • Bruno smoke: defeat chain → GET shows completed once.

Implementation reconciliation (shipped)

  • Route: GET /game/players/{id}/encounter-progressEncounterProgressApi.BuildSnapshot projects progress, completion, and event stores; 404 via IPositionStateStore.
  • DTOs: EncounterProgressListResponse (schema v1); row state inactive/active/completed; rewardGrantSummary from IEncounterCompleteEventStore.GrantedItems; optional fields omitted when not completed.
  • Tests: five AAA tests in EncounterProgressApiTests (404, inactive, 1-defeat active, 2-defeat active, completed + grant summary).
  • Bruno: bruno/neon-sprawl-server/encounter-progress/ — inactive GET + three-NPC defeat spine.
  • Docs: server/README.md; E5.M3 module snapshot; alignment register; module dependency register; backlog E5M3-08 AC.

Technical approach

  1. Route: GET /game/players/{id}/encounter-progress — inject IPositionStateStore, IEncounterDefinitionRegistry, IEncounterProgressStore, IEncounterCompletionStore, IEncounterCompleteEventStore.

  2. Player gate: Trim id; return 404 when empty or position missing (mirror GigProgressionSnapshotApi).

  3. State derivation (per catalog encounter id, normalized lowercase keys in stores):

    Condition state
    IEncounterCompletionStore.IsCompleted completed
    Progress snapshot Started active
    Otherwise inactive
  4. Row fields:

    • encounterId: catalog id (lowercase).
    • defeatedTargetIds: from EncounterProgressSnapshot.DefeatedNpcInstanceIds when progress exists; else [] (sorted ordinal for stable JSON).
    • completedAt: ISO-8601 UTC from IEncounterCompletionStore.TryGetCompletedAt when completed; omitted otherwise.
    • rewardGrantSummary: when completed, map EncounterCompleteEvent.GrantedItems from IEncounterCompleteEventStore.TryGet{ itemId, quantity }[]. If completion flag set but event missing (invariant violation), throw InvalidOperationException (fail-fast; same spirit as NEO-103 missing reward table). Omitted when not completed.
  5. Ordering: Build rows from encounterRegistry.GetDefinitionsInIdOrder() (prototype: single prototype_combat_pocket).

  6. Implementation: EncounterProgressApi + EncounterProgressListDtos.cs with internal BuildSnapshot(playerId, …) testable like GigProgressionSnapshotApi.BuildSnapshot. Register app.MapEncounterProgressApi() in Program.cs after MapGigProgressionSnapshotApi().

  7. Bruno: bruno/neon-sprawl-server/encounter-progress/

    • folder.bru
    • Get encounter progress inactive.bru — GET before engagement → inactive, empty defeats.
    • Get encounter progress after defeat spine.bru — pre-request: resetPrototypeCombatTargets (shared helper pattern from gig-progression), move, bind slot 3 pulse, defeat melee → ranged → elite via cast; GET asserts active with growing defeatedTargetIds after 12 defeats (optional intermediate GETs in pre-request) and final completed with rewardGrantSummary matching scrap_metal_bulk ×10, contract_handoff_token ×1; second GET still completed (idempotent read).
  8. Docs: Update server/README.md (replace NEO-108 deferral under encounter progress section). Update E5_M3, documentation_and_implementation_alignment.md, E5M3-prototype-backlog.md when landed.

Expected prototype row progression (frozen)

After state defeatedTargetIds rewardGrantSummary
No engagement inactive [] omitted
First damaging cast vs required NPC active 03 ids omitted
12 lethal defeats active 12 ids omitted
Third lethal defeat + grants completed 3 ids scrap ×10, token ×1
Subsequent GET / casts completed 3 ids unchanged summary

Prototype completion read flow

sequenceDiagram
    participant Client
    participant API as EncounterProgressApi
    participant Pos as IPositionStateStore
    participant Reg as IEncounterDefinitionRegistry
    participant Prog as IEncounterProgressStore
    participant Comp as IEncounterCompletionStore
    participant Ev as IEncounterCompleteEventStore

    Client->>API: GET /game/players/{id}/encounter-progress
    API->>Pos: TryGetPosition
    alt unknown player
        API-->>Client: 404
    end
    API->>Reg: GetDefinitionsInIdOrder
    loop each encounter
        API->>Comp: IsCompleted
        API->>Prog: TryGetProgress
        API->>Ev: TryGet when completed
    end
    API-->>Client: schemaVersion 1 + encounters[]

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Encounters/EncounterProgressApi.cs Map* extension for GET route; BuildSnapshot projection.
server/NeonSprawl.Server/Game/Encounters/EncounterProgressListDtos.cs Versioned response + per-encounter row + grant summary DTOs.
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterProgressApiTests.cs AAA HTTP: 404, inactive, partial active, completed + grant summary.
bruno/neon-sprawl-server/encounter-progress/folder.bru Bruno folder metadata.
bruno/neon-sprawl-server/encounter-progress/Get encounter progress inactive.bru GET default inactive.
bruno/neon-sprawl-server/encounter-progress/Get encounter progress after defeat spine.bru Three-NPC defeat pre-request → completed once.
docs/plans/NEO-108-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Program.cs Register MapEncounterProgressApi() after gig progression routes.
server/README.md Document GET route, curl, store projection, Bruno folder; remove NEO-108 deferral notes.
docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md Implementation snapshot — E5M3-08 HTTP read model.
docs/decomposition/modules/documentation_and_implementation_alignment.md E5.M3 row — note NEO-108 HTTP when landed.
docs/decomposition/modules/module_dependency_register.md E5.M3 note — NEO-108 per-player GET when landed.
docs/plans/E5M3-prototype-backlog.md Mark E5M3-08 acceptance criteria when complete.

Tests

Test file What it covers
EncounterProgressApiTests.cs 404 for unknown player. Inactive: fresh factory → GET → one encounter prototype_combat_pocket, inactive, empty defeats, no completedAt / rewardGrantSummary. Partial: cast/defeat helpers (reuse AbilityCastApiTests pattern or inline HTTP) — after 1 defeat active + 1 id; after 2 defeats still active + 2 ids, not completed. Completed: defeat all three → completed, completedAt present, rewardGrantSummary quantities 10 + 1; completion store + event store aligned. AAA per csharp-style.

Open questions / risks

Question / risk Agent recommendation Status
Event missing when completed Throw InvalidOperationException at projection time — should not happen after NEO-107; surfaces wiring bugs in tests. adopted
Bruno slot cooldown Use hotbar slot 3 (or next free) in defeat spine — avoid slot 0/1 conflicts with other Bruno folders (NEO-44 precedent). adopted
Case on defeatedTargetIds Return normalized lowercase ids from store (catalog ids are lowercase). adopted
NEO-110 poll cadence Defer — client story chooses poll-after-cast vs interval. deferred