NEO-102: add implementation plan for encounter/reward registries + DI

pull/141/head
VinPropane 2026-05-31 00:33:45 -04:00
parent d7c2684488
commit 93eca821d7
1 changed files with 114 additions and 0 deletions

View File

@ -0,0 +1,114 @@
# NEO-102 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-102 |
| **Title** | E5M3-03: Injectable encounter/reward registries + DI |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-102/e5m3-03-injectable-encounterreward-registries-di |
| **Module** | [E5.M3 — EncounterAndRewardTables](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) · Epic 5 Slice 3 · backlog **E5M3-03** |
| **Branch** | `NEO-102-injectable-encounter-reward-registries-di` |
| **Precursor** | [NEO-101](https://linear.app/neon-sprawl/issue/NEO-101) — fail-fast encounter + reward-table catalog load (**landed on `main`**) |
| **Pattern** | [NEO-52](NEO-52-implementation-plan.md) / [NEO-79](NEO-79-implementation-plan.md) — thin registry adapter over startup catalog + DI; strict split after NEO-101 loader |
| **Blocks** | [NEO-103](https://linear.app/neon-sprawl/issue/NEO-103) — GET `/game/world/encounter-definitions`; E5M3-05+ progress/completion engines |
| **Client counterpart** | None — server-only registry surface; player-visible work starts at [NEO-110](https://linear.app/neon-sprawl/issue/NEO-110) / [NEO-111](https://linear.app/neon-sprawl/issue/NEO-111) |
## Kickoff clarifications
No clarifications needed — E5M3-03 backlog, [NEO-101 plan](NEO-101-implementation-plan.md) strict split, and NEO-52/79 registry precedent settle API surface, test scope, and DI wiring. User confirmed **Proceed — no clarifications needed** on kickoff.
| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Registry API surface** | Include `TryNormalizeKnown` + `GetDefinitionsInIdOrder` on both registries? | **Yes** — E5M3-03 backlog + `IAbilityDefinitionRegistry` (NEO-79) pattern. | **Adopted** |
| **Lookup naming** | `TryGetDefinition` vs domain-specific names? | **`TryGetDefinition`** — item/skill/ability/recipe precedent; catalogs keep `TryGetEncounter` / `TryGetRewardTable`. | **Adopted** |
| **Test scope** | Registry unit tests only, or also host DI resolve? | **Both** — mirror NEO-52/79; NEO-101 already covers catalog boot. | **Adopted** |
| **Program.cs eager resolve** | Eager-resolve registries at boot? | **Omit** — catalog singletons already eager-resolved (NEO-101); registry is thin adapter. | **Adopted** |
| **Call site migration** | Migrate existing consumers off catalog types? | **None this story** — no game code injects encounter/reward catalogs yet; HTTP is NEO-103. | **Adopted** |
## Goal, scope, and out-of-scope
**Goal:** Provide **`IEncounterDefinitionRegistry`** and **`IRewardTableDefinitionRegistry`** backed by the startup-loaded **`EncounterDefinitionCatalog`** and **`RewardTableDefinitionCatalog`**: resolve by stable id, enumerate definitions in id order, and expose **`TryNormalizeKnown`** for fail-closed id validation. Register both in DI so **NEO-103+** (HTTP, progress store, completion engine) depend on interfaces instead of catalog types.
**In scope (from Linear + [E5M3-03](E5M3-prototype-backlog.md#e5m3-03--injectable-encounterreward-registries--di)):**
- `EncounterDefinitionRegistry` + `RewardTableDefinitionRegistry` thin adapters over NEO-101 catalogs.
- DI registration in `AddEncounterAndRewardCatalogs`.
- Unit tests (AAA): known prototype id lookup + metadata; unknown id / null miss without throwing; `TryNormalizeKnown` trim/case/whitespace behavior; enumeration order; host resolves both registries from DI.
**Out of scope (from Linear + backlog):**
- HTTP routes ([NEO-103](https://linear.app/neon-sprawl/issue/NEO-103)).
- Progress store, completion grants, combat hooks (E5M3-05+).
- Changing loader, E5M3 gate, or catalog load semantics (NEO-101).
- Godot / client changes.
## Acceptance criteria checklist
- [ ] Registries resolve frozen encounter + reward table ids (`prototype_combat_pocket`, `prototype_combat_pocket_clear`).
- [ ] Unknown id normalization fails closed (`TryNormalizeKnown` returns false; no throw).
## Technical approach
1. **`IEncounterDefinitionRegistry`** — mirror [`IAbilityDefinitionRegistry`](../../server/NeonSprawl.Server/Game/Combat/IAbilityDefinitionRegistry.cs):
- `TryGetDefinition(string? encounterId, [NotNullWhen(true)] out EncounterDefRow? definition)` — null and unknown ids return false without throwing.
- `TryNormalizeKnown(string? rawEncounterId, [NotNullWhen(true)] out string normalized)` — trim + lowercase + catalog lookup; null/empty/whitespace return false.
- `GetDefinitionsInIdOrder()` — all rows ordered by `EncounterDefRow.Id` (ordinal).
- XML remarks: encounter progress/completion callers (NEO-104+) and HTTP read model (NEO-103) should use this interface, not `EncounterDefinitionCatalog`.
2. **`IRewardTableDefinitionRegistry`** — same surface over `RewardTableDefRow` / `RewardTableDefinitionCatalog`.
3. **`EncounterDefinitionRegistry`** — primary-constructor adapter delegating to `EncounterDefinitionCatalog.TryGetEncounter`; cache `GetDefinitionsInIdOrder` at construction (NEO-79 pattern).
4. **`RewardTableDefinitionRegistry`** — same over `RewardTableDefinitionCatalog.TryGetRewardTable`.
5. **DI** — extend [`AddEncounterAndRewardCatalogs`](../../server/NeonSprawl.Server/Game/Encounters/EncounterCatalogServiceCollectionExtensions.cs):
- `services.AddSingleton<IEncounterDefinitionRegistry>(sp => new EncounterDefinitionRegistry(sp.GetRequiredService<EncounterDefinitionCatalog>()));`
- `services.AddSingleton<IRewardTableDefinitionRegistry>(sp => new RewardTableDefinitionRegistry(sp.GetRequiredService<RewardTableDefinitionCatalog>()));`
- No change to `Program.cs` eager-resolve (catalogs only).
6. **Comments / docs** — tighten catalog summaries (already reference NEO-102); update [server README](../../server/README.md) encounter + reward-table sections to document registries as preferred lookup surface (replace “land in NEO-102” placeholders).
7. **Tests** — new `EncounterDefinitionRegistryTests.cs` and `RewardTableDefinitionRegistryTests.cs` mirroring [`AbilityDefinitionRegistryTests`](../../server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionRegistryTests.cs):
- In-memory catalog helper building registry from row dictionaries.
- Prototype fixture via repo catalogs or inline rows.
- `Host_ShouldResolveRegistriesFromDi_WhenStartupSucceeds` in one file (resolve both interfaces via `InMemoryWebApplicationFactory`, assert frozen ids + grant metadata).
8. **Alignment doc** — update E5.M3 row in `documentation_and_implementation_alignment.md` when complete.
## Files to add
| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Encounters/IEncounterDefinitionRegistry.cs` | Public contract: lookup, normalize, enumerate. |
| `server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionRegistry.cs` | Singleton adapter over `EncounterDefinitionCatalog`. |
| `server/NeonSprawl.Server/Game/Encounters/IRewardTableDefinitionRegistry.cs` | Public contract: lookup, normalize, enumerate. |
| `server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionRegistry.cs` | Singleton adapter over `RewardTableDefinitionCatalog`. |
| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionRegistryTests.cs` | AAA unit + host DI tests for encounter registry. |
| `server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionRegistryTests.cs` | AAA unit + host DI tests for reward-table registry. |
| `docs/plans/NEO-102-implementation-plan.md` | This plan. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/Encounters/EncounterCatalogServiceCollectionExtensions.cs` | Register `IEncounterDefinitionRegistry` + `IRewardTableDefinitionRegistry` after catalog singletons. |
| `server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionCatalog.cs` | Confirm summary points at `IEncounterDefinitionRegistry` (replace NEO-102 placeholder wording if needed). |
| `server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalog.cs` | Confirm summary points at `IRewardTableDefinitionRegistry`. |
| `server/README.md` | Encounter + reward-table catalog sections: document registries as preferred lookup surface. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M3 row — note NEO-102 registries when complete. |
## Tests
| Test file | What it covers |
|-----------|----------------|
| `server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionRegistryTests.cs` | **Unit:** `TryGetDefinition` returns true + expected metadata for `prototype_combat_pocket` (displayName, completionCriteriaKind, requiredNpcInstanceIds, rewardTableId); false for null/unknown. **Unit:** `TryNormalizeKnown` trim/case success; false for null/whitespace/unknown. **Unit:** `GetDefinitionsInIdOrder` ordinal order. **Host:** `InMemoryWebApplicationFactory` resolves `IEncounterDefinitionRegistry` with frozen encounter row. |
| `server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionRegistryTests.cs` | **Unit:** `TryGetDefinition` returns true + fixed grants for `prototype_combat_pocket_clear` (`scrap_metal_bulk` ×10, `contract_handoff_token` ×1); false for null/unknown. **Unit:** `TryNormalizeKnown` trim/case success; false for null/whitespace/unknown. **Unit:** `GetDefinitionsInIdOrder` ordinal order. **Host:** `InMemoryWebApplicationFactory` resolves `IRewardTableDefinitionRegistry` with frozen grant quantities. |
## Open questions / risks
| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Constants drift vs catalog load** | No runtime re-validation — registry delegates to NEO-101 fail-fast catalogs only. | **adopted** |
| **Duplicate host boot tests** | Keep NEO-101 catalog boot tests; add registry-specific DI resolve assertions (do not remove catalog coverage). | **adopted** |
| **NEO-103 HTTP projection** | NEO-103 should inject registries, not catalogs — document in README now. | **adopted** |