12 KiB
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-progresswith versioned envelope (schemaVersion1,encountersarray).- Per-encounter row:
encounterId,state(inactive|active|completed),defeatedTargetIds, optionalcompletedAt, optionalrewardGrantSummary(itemId+quantitygrants). - Projection from
IEncounterDefinitionRegistry,IEncounterProgressStore,IEncounterCompletionStore,IEncounterCompleteEventStore(no second source of truth). EncounterProgressApi+ wire DTOs inGame/Encounters/.- Integration tests (AAA): unknown player 404; inactive default; partial progress after 1–2 defeats; completed once with grant summary.
- Bruno
bruno/neon-sprawl-server/encounter-progress/— self-contained three-NPC defeat pre-request → GETcompletedonce. server/README.mdroute 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
completedstate and grant summary. - Bruno smoke: defeat chain → GET shows
completedonce.
Implementation reconciliation (shipped)
- Route:
GET /game/players/{id}/encounter-progress—EncounterProgressApi.BuildSnapshotprojects progress, completion, and event stores; 404 viaIPositionStateStore. - DTOs:
EncounterProgressListResponse(schema v1); rowstateinactive/active/completed;rewardGrantSummaryfromIEncounterCompleteEventStore.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
-
Route:
GET /game/players/{id}/encounter-progress— injectIPositionStateStore,IEncounterDefinitionRegistry,IEncounterProgressStore,IEncounterCompletionStore,IEncounterCompleteEventStore. -
Player gate: Trim
id; return 404 when empty or position missing (mirrorGigProgressionSnapshotApi). -
State derivation (per catalog encounter id, normalized lowercase keys in stores):
Condition stateIEncounterCompletionStore.IsCompletedcompletedProgress snapshot StartedactiveOtherwise inactive -
Row fields:
encounterId: catalogid(lowercase).defeatedTargetIds: fromEncounterProgressSnapshot.DefeatedNpcInstanceIdswhen progress exists; else[](sorted ordinal for stable JSON).completedAt: ISO-8601 UTC fromIEncounterCompletionStore.TryGetCompletedAtwhencompleted; omitted otherwise.rewardGrantSummary: whencompleted, mapEncounterCompleteEvent.GrantedItemsfromIEncounterCompleteEventStore.TryGet→{ itemId, quantity }[]. If completion flag set but event missing (invariant violation), throwInvalidOperationException(fail-fast; same spirit as NEO-103 missing reward table). Omitted when notcompleted.
-
Ordering: Build rows from
encounterRegistry.GetDefinitionsInIdOrder()(prototype: singleprototype_combat_pocket). -
Implementation:
EncounterProgressApi+EncounterProgressListDtos.cswith internalBuildSnapshot(playerId, …)testable likeGigProgressionSnapshotApi.BuildSnapshot. Registerapp.MapEncounterProgressApi()inProgram.csafterMapGigProgressionSnapshotApi(). -
Bruno:
bruno/neon-sprawl-server/encounter-progress/folder.bruGet 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 assertsactivewith growingdefeatedTargetIdsafter 1–2 defeats (optional intermediate GETs in pre-request) and finalcompletedwithrewardGrantSummarymatchingscrap_metal_bulk×10,contract_handoff_token×1; second GET stillcompleted(idempotent read).
-
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 |
0–3 ids | omitted |
| 1–2 lethal defeats | active |
1–2 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 |