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

10 KiB
Raw Blame History

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 · Epic 5 Slice 3 · backlog E5M3-03
Branch NEO-102-injectable-encounter-reward-registries-di
Precursor NEO-101 — fail-fast encounter + reward-table catalog load (landed on main)
Pattern NEO-52 / NEO-79 — thin registry adapter over startup catalog + DI; strict split after NEO-101 loader
Blocks 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 / NEO-111

Kickoff clarifications

No clarifications needed — E5M3-03 backlog, NEO-101 plan 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):

  • 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).
  • 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).

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:

    • 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:

    • 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 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:

    • 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