diff --git a/docs/plans/NEO-102-implementation-plan.md b/docs/plans/NEO-102-implementation-plan.md new file mode 100644 index 0000000..0fa4804 --- /dev/null +++ b/docs/plans/NEO-102-implementation-plan.md @@ -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(sp => new EncounterDefinitionRegistry(sp.GetRequiredService()));` + - `services.AddSingleton(sp => new RewardTableDefinitionRegistry(sp.GetRequiredService()));` + - 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** |