13 KiB
13 KiB
NEO-104 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-104 |
| Title | E5M3-05: Encounter progress + completion stores |
| Linear | https://linear.app/neon-sprawl/issue/NEO-104/e5m3-05-encounter-progress-completion-stores |
| Module | E5.M3 — EncounterAndRewardTables · Epic 5 Slice 3 · backlog E5M3-05 |
| Branch | NEO-104-encounter-progress-completion-stores |
| Precursor | NEO-103 — GET /game/world/encounter-definitions (landed on main) |
| Pattern | NEO-61 / InMemoryResourceNodeInstanceStore — keyed in-memory store + static operations; NEO-44 — gig progression store DI |
| Blocks | NEO-105 — EncounterCompletionOperations + inventory reward apply |
| Client counterpart | None this story — player-visible encounter HUD starts at NEO-110 via NEO-108 GET /game/players/{id}/encounter-progress |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Storage backend | In-memory only vs Postgres when configured? | In-memory only — E5M3 kickoff default; session prototype. | Adopted — in-memory only |
| Progress vs completion | Single store vs split? | Split stores — progress: started + defeatedNpcInstanceIds; completion: idempotent completedAt; operations expose IsAllRequiredTargetsDefeated. |
Adopted — split stores |
| Encounter binding | Explicit encounterId vs auto-resolve from NPC? |
Auto-resolve — scan IEncounterDefinitionRegistry for encounter whose requiredNpcInstanceIds contains the NPC. |
Adopted — auto-resolve from npcInstanceId |
| Combat / HTTP wiring | Wire AbilityCastApi or HTTP on this story? |
Defer — E5M3-07 / E5M3-08; NEO-104 ships stores + operations + unit tests only. | Adopted (implicit — not asked; backlog out-of-scope) |
| Activation before defeat | Auto-activate on TryMarkTargetDefeated vs require prior TryActivateOnFirstEngagement? |
Require activation — TryMarkTargetDefeated no-ops when not started; activation is separate (NEO-106 wires first damaging cast). |
Adopted (plan default; backlog activation hook) |
Goal, scope, and out-of-scope
Goal: Server-owned per-player encounter activation, defeat progress (order-independent subset of requiredNpcInstanceIds), and an idempotent completion flag at the store layer — plus EncounterProgressOperations that resolve encounters from NPC ids via IEncounterDefinitionRegistry.
In scope (from Linear + E5M3-05):
IEncounterProgressStore— keyed by(playerId, encounterId); fields:started(bool),defeatedNpcInstanceIds(set, normalized lowercase).IEncounterCompletionStore—TryMarkCompleted,IsCompleted,TryGetCompletedAt; secondTryMarkCompletedis no-op (false).InMemoryEncounterProgressStore,InMemoryEncounterCompletionStore— thread-safeConcurrentDictionary+ per-key locks (mirror gathering/NPC stores).EncounterProgressOperations:TryActivateOnFirstEngagement(playerId, npcInstanceId, …)— resolve encounter; setstartedif not started and not completed.TryMarkTargetDefeated(playerId, npcInstanceId, …)— requires started + not completed; add NPC to defeated set if in encounter’s required list; idempotent re-mark of same NPC returns success without duplicate progress.IsAllRequiredTargetsDefeated(playerId, encounterId, …)— compares defeated set to registryrequiredNpcInstanceIds(set equality).
- DI registration in
EncounterCatalogServiceCollectionExtensionsor newEncounterProgressServiceCollectionExtensions. InMemoryWebApplicationFactory— register in-memory encounter stores (remove any Postgres placeholder if added later).- Unit tests (AAA) with injected fake stores / registry.
server/README.mdencounter progress section (brief).
Out of scope (from Linear + backlog):
- Inventory grants,
EncounterCompleteEvent,EncounterCompletionOperations(NEO-105). - Combat defeat / ability-cast wiring (NEO-106).
- HTTP
GET /game/players/{id}/encounter-progress(NEO-108). - Postgres persistence, Godot, Bruno,
docs/manual-qa/NEO-104.md.
Acceptance criteria checklist
- Defeating required NPCs accumulates progress in any order (via
TryMarkTargetDefeatedafter activation). - Completion predicate true only when all required ids defeated (
IsAllRequiredTargetsDefeated). - Second completion attempt on same player+encounter is idempotent no-op at store layer (
TryMarkCompletedreturnsfalse).
Implementation reconciliation (shipped)
- Stores:
IEncounterProgressStore/InMemoryEncounterProgressStore;IEncounterCompletionStore/InMemoryEncounterCompletionStore;EncounterProgressIdsnormalization. - Operations:
EncounterProgressOperations—TryResolveEncounterForNpc,TryActivateOnFirstEngagement,TryMarkTargetDefeated,IsAllRequiredTargetsDefeated. - DI: registered in
AddEncounterAndRewardCatalogs. - Tests:
InMemoryEncounterProgressStoreTests,InMemoryEncounterCompletionStoreTests,EncounterProgressOperationsTests; host DI resolve inEncounterDefinitionRegistryTests. - Docs:
server/README.md; E5.M3 module snapshot + alignment register + backlog updated. - Plan file list:
Program.csandInMemoryWebApplicationFactory.csunchanged — stores register in existingAddEncounterAndRewardCatalogs(already invoked at startup); always in-memory (no Postgres test-factory swap).
Technical approach
-
Snapshots / keys
EncounterProgressSnapshot:PlayerId,EncounterId,Started,DefeatedNpcInstanceIds(read-only set).EncounterPlayerEncounterKey: normalizeplayerId+encounterIdwith trim + lowercase (match registry / position-state id normalization).- NPC ids normalized the same way when stored and compared.
-
IEncounterProgressStoreTryGetProgress(playerId, encounterId, out snapshot)— missing row ⇒ not started, empty defeated set.TryActivate(playerId, encounterId)— creates row withstarted: true, empty defeated set; returnstrueon first activation,falseif already started (idempotent) or if completion store says completed.TryAddDefeatedTarget(playerId, encounterId, npcInstanceId)— requires existing started row; add to set; returntrueif newly added,falseif duplicate NPC or not started.
-
IEncounterCompletionStorebool IsCompleted(playerId, encounterId)bool TryMarkCompleted(playerId, encounterId, DateTimeOffset completedAt)—truefirst time,falseif already completed.bool TryGetCompletedAt(…, out DateTimeOffset completedAt)
-
EncounterProgressOperations(static, mirrorGatherOperations/AggroOperations)TryResolveEncounterForNpc(npcInstanceId, registry, out encounterId, out definition)— scanGetDefinitionsInIdOrder()for first encounter whererequiredNpcInstanceIdscontains normalized NPC (prototype: singleprototype_combat_pocket).TryActivateOnFirstEngagement: resolve encounter → check completion store →TryActivateon progress store.TryMarkTargetDefeated: resolve encounter → deny if completed → deny if not started → validate NPC in required list →TryAddDefeatedTarget.IsAllRequiredTargetsDefeated: load progress + definition; compare required set to defeated set (order-independent).- Inject
TimeProvideronly where completion store needs timestamps (completion store acceptscompletedAtfrom caller; NEO-105 passesTimeProvider.GetUtcNow()).
-
DI
services.AddSingleton<IEncounterProgressStore, InMemoryEncounterProgressStore>();services.AddSingleton<IEncounterCompletionStore, InMemoryEncounterCompletionStore>();- Extend
AddEncounterAndRewardCatalogsor addAddEncounterProgressStorescalled fromProgram.csafter encounter catalogs.
-
Reserved hook
- XML comment on
TryActivateOnFirstEngagement:// TODO(E9.M1): encounter_start telemetry(activates E5M3-07 hook site).
- XML comment on
-
Docs
server/README.md— stores + operations, note combat/HTTP deferred.- Update E5_M3 implementation snapshot when landed.
Prototype encounter behavior (frozen)
| Step | Expected |
|---|---|
| Activate | TryActivateOnFirstEngagement(devPlayer, prototype_npc_melee) → started, 0/3 defeated |
| Defeat 2 | Mark melee + ranged → IsAllRequiredTargetsDefeated false |
| Defeat 3 | Mark elite → IsAllRequiredTargetsDefeated true |
| Complete store | TryMarkCompleted once → true; second → false |
| Order | elite → melee → ranged also reaches all-defeated |
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Encounters/EncounterProgressSnapshot.cs |
Immutable progress row for store read model. |
server/NeonSprawl.Server/Game/Encounters/IEncounterProgressStore.cs |
Progress store contract. |
server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterProgressStore.cs |
Thread-safe in-memory implementation. |
server/NeonSprawl.Server/Game/Encounters/IEncounterCompletionStore.cs |
Completion store contract. |
server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompletionStore.cs |
Thread-safe in-memory implementation. |
server/NeonSprawl.Server/Game/Encounters/EncounterProgressOperations.cs |
Static orchestration: resolve encounter, activate, mark defeat, all-defeated predicate. |
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterProgressOperationsTests.cs |
AAA unit tests: activation, any-order defeats, all-defeated predicate, completion idempotency. |
server/NeonSprawl.Server.Tests/Game/Encounters/InMemoryEncounterProgressStoreTests.cs |
AAA store tests: activate idempotency, duplicate defeat, not-started deny. |
server/NeonSprawl.Server.Tests/Game/Encounters/InMemoryEncounterCompletionStoreTests.cs |
AAA completion idempotency tests. |
docs/plans/NEO-104-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Encounters/EncounterCatalogServiceCollectionExtensions.cs |
Register IEncounterProgressStore + IEncounterCompletionStore singletons (or call new extension from here). |
server/NeonSprawl.Server/Program.cs |
Ensure encounter progress stores registered at startup (if split extension, invoke it). |
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs |
Swap in test in-memory encounter stores; strip production registrations in test host. |
server/README.md |
Document encounter progress/completion stores and operations; defer HTTP/combat to NEO-106/108. |
docs/decomposition/modules/E5_M3_EncounterAndRewardTables.md |
Implementation snapshot — progress/completion stores (NEO-104). |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E5.M3 row — note NEO-104 stores when landed. |
docs/plans/E5M3-prototype-backlog.md |
Mark E5M3-05 AC when complete. |
Tests
| Test file | What it covers |
|---|---|
InMemoryEncounterProgressStoreTests.cs |
Arrange: store. Act/Assert: first TryActivate true; second false; TryAddDefeatedTarget without activate false; duplicate NPC add false; two distinct NPCs in set. |
InMemoryEncounterCompletionStoreTests.cs |
Arrange: store + fixed completedAt. Act: TryMarkCompleted twice. Assert: first true, second false; IsCompleted true; TryGetCompletedAt matches. |
EncounterProgressOperationsTests.cs |
Arrange: real EncounterDefinitionRegistry from test factory catalog + in-memory stores. Act/Assert: activate on prototype_npc_melee; defeat three NPCs in permuted order → IsAllRequiredTargetsDefeated true only after third; partial progress false; TryMarkTargetDefeated before activate false; TryMarkCompleted idempotent; progress blocked when already completed. |
EncounterDefinitionRegistryTests.cs (optional host test) |
Arrange: InMemoryWebApplicationFactory. Assert: resolves IEncounterProgressStore + IEncounterCompletionStore from DI. |
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| Multiple encounters sharing one NPC | First match in GetDefinitionsInIdOrder() wins — acceptable for prototype (single encounter). Document in operations XML. |
adopted |
| Mark defeat after encounter completed | TryMarkTargetDefeated returns false — fail closed. |
adopted |
| Postgres follow-on | Defer — new migration + store pair in a future issue when session persistence is required. | deferred |