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

123 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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
- [x] Registries resolve frozen encounter + reward table ids (`prototype_combat_pocket`, `prototype_combat_pocket_clear`).
- [x] Unknown id normalization fails closed (`TryNormalizeKnown` returns false; no throw).
## Implementation reconciliation (shipped)
- **Interfaces:** `IEncounterDefinitionRegistry`, `IRewardTableDefinitionRegistry` with `TryGetDefinition`, `TryNormalizeKnown`, `GetDefinitionsInIdOrder`.
- **Adapters:** `EncounterDefinitionRegistry`, `RewardTableDefinitionRegistry` over NEO-101 catalogs (cached id-order lists).
- **DI:** `AddEncounterAndRewardCatalogs` registers both registries after catalog singletons; no `Program.cs` change.
- **Tests:** 18 AAA tests in `EncounterDefinitionRegistryTests` + `RewardTableDefinitionRegistryTests` (unit + host DI).
- **Docs:** `server/README.md` registry sections; alignment register E5.M3 row updated.
## 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** |