13 KiB
NEO-91 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-91 |
| Title | E5M2-05: Prototype NPC instance registry + combat-target migration |
| Linear | https://linear.app/neon-sprawl/issue/NEO-91/e5m2-05-prototype-npc-instance-registry-combat-target-migration |
| Module | E5.M2 — NpcAiAndBehaviorProfiles · Epic 5 Slice 2 · backlog E5M2-05 |
| Branch | NEO-91-npc-instance-registry-combat-target-migration |
| Precursor | NEO-89 — INpcBehaviorDefinitionRegistry + DI (Done on main) |
| Pattern | NEO-83 — combat-targets GET + PrototypeTargetRegistry; NEO-89 — injectable behavior registry |
| Client counterpart | NEO-97 — Godot archetype markers + tab cycle (out of scope here; breaking id change must land in NEO-97 before playable Godot verification) |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Registry shape | New PrototypeNpcRegistry vs extend PrototypeTargetRegistry? |
New PrototypeNpcRegistry in Game/Npc/ — targeting/combat consume it; remove alpha/beta ids. |
User: new PrototypeNpcRegistry. |
| World anchors | Where to place three NPC instances? | Melee at alpha (-3, 0.5, -3), ranged at beta (3, 0, 3), elite at origin (0, 0.5, 0); SharedLockRadius 6.0. |
User: preserve alpha/beta + elite at origin. |
| Behavior binding | Which behavior def per instance? | 1:1: melee→prototype_melee_pressure, ranged→prototype_ranged_control, elite→prototype_elite_mini_boss. |
Adopted (frozen naming in E5.M2). |
| Bruno defeat spine | Which NPC for defeat AC? | prototype_npc_melee (100 HP, 4× pulse — alpha replacement). |
User: melee. |
Goal, scope, and out-of-scope
Goal: Replace passive prototype_target_alpha / prototype_target_beta dummies with three NPC combat instances wired into targeting, cast resolution, and the combat-target HP store — each instance bound to a frozen behaviorDefId with max HP from the behavior catalog.
In scope (from Linear + E5M2-05):
PrototypeNpcRegistryinGame/Npc/: instance id,behaviorDefId, world anchor, shared lock radius.- Migrate
ICombatEntityHealthStoregates and max HP from flat 100 → per-instancemaxHpviaINpcBehaviorDefinitionRegistry. - Replace
PrototypeTargetRegistryalpha/beta usage across targeting, cast, combat-targets GET, dev fixture reset. - Update server unit/integration tests and all Bruno smokes that reference alpha/beta.
- Document breaking id change + required client constant migration (NEO-97).
Out of scope (from Linear + backlog):
- Behavior tick, aggro, telegraphs (NEO-92+).
- Godot markers / tab cycle (NEO-97).
docs/manual-qa/NEO-91.md— server-only; Bruno + automated tests (NEO-83/90 pattern).
Acceptance criteria checklist
- Cast + combat-targets GET operate on three NPC ids only (alpha/beta removed).
- Each instance references a valid frozen
behaviorDefIdresolvable viaINpcBehaviorDefinitionRegistry. - Bruno defeat spine passes for
prototype_npc_melee(4× pulse → defeated).
Technical approach
-
PrototypeNpcRegistry(Game/Npc/):-
Stable id constants:
prototype_npc_melee,prototype_npc_ranged,prototype_npc_elite. -
SharedLockRadius = 6.0(same convention as E1.M3/E5.M1 dummies). -
PrototypeNpcInstanceEntry:BehaviorDefId, anchorX/Y/Z,LockRadius. -
Frozen bindings (kickoff):
Instance id behaviorDefIdAnchor (X, Y, Z) prototype_npc_meleeprototype_melee_pressure(-3, 0.5, -3) prototype_npc_rangedprototype_ranged_control(3, 0, 3) prototype_npc_eliteprototype_elite_mini_boss(0, 0.5, 0) -
TryGet(instanceIdLowercase, out entry),GetInstanceIdsInOrder()(ordinal sort — tab cycle order for future NEO-97).
-
-
Retire
PrototypeTargetRegistry: Remove alpha/beta ids and the class (or leave a one-release obsolete shim only if grep shows external consumers — prefer delete and update all server references). Targeting/combat importPrototypeNpcRegistryinstead. -
InMemoryCombatEntityHealthStore: Constructor-injectINpcBehaviorDefinitionRegistry. Gate rows onPrototypeNpcRegistry.TryGet. Lazy-initcurrentHpto behaviormaxHp;CreateSnapshot/TryResetToFulluse per-instance max from catalog. Remove dependence onPrototypeCombatConstants.MaxPrototypeTargetHp(delete constant class if unused). -
Targeting + cast paths:
PlayerTargetStateReader,TargetingApi,AbilityCastApi— swapPrototypeTargetRegistry.TryGet→PrototypeNpcRegistry.TryGet(range math unchanged). -
Combat-targets GET + fixture:
CombatTargetsWorldApi,CombatTargetFixtureApi— enumeratePrototypeNpcRegistry.GetInstanceIdsInOrder()(expect 3 rows). -
Bruno migration: Replace every
prototype_target_alpha/prototype_target_betawith NPC ids. Key updates:combat-targets/— assert 3 targets; defeat spine onprototype_npc_melee(same 4× pulse / 75→50→25→0 stepping).ability-cast/— happy cast onprototype_npc_ranged(beta slot/spine replacement); lock/defeat spines on melee.targeting/— select melee/ranged ids.gig-progression/Get gig progression after defeat spine.bru— melee defeat.scripts/combat-targets-reset-helper.js— unchanged route; resets all registry ids server-side.
-
Docs:
server/README.md— combat targets section lists three NPC ids + per-archetype max HP; notePrototypeNpcRegistry+ breaking change forclient/scripts/prototype_target_constants.gd(NEO-97). Update E5_M2 instance registry bullet and E5M2-prototype-backlog.md E5M2-05 checkboxes when landed.
Expected snapshot values (fresh server, prototype_pulse = 25 damage)
| Instance | maxHp (from behavior def) |
After 4× pulse on melee only |
|---|---|---|
prototype_npc_elite |
200 | 200 / 200 |
prototype_npc_melee |
100 | 0 / 100, defeated: true |
prototype_npc_ranged |
80 | 80 / 80 |
(GET row order: ascending targetId — elite, melee, ranged.)
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Npc/PrototypeNpcRegistry.cs |
Frozen three-instance registry: ids, behavior bindings, anchors, lock radius, ordered enumeration. |
server/NeonSprawl.Server.Tests/Game/Npc/PrototypeNpcRegistryTests.cs |
AAA unit tests: three ids in order, valid behavior bindings, anchor/lock radius parity, unknown id fails. |
docs/plans/NEO-91-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Combat/InMemoryCombatEntityHealthStore.cs |
Gate on PrototypeNpcRegistry; per-instance max HP from INpcBehaviorDefinitionRegistry. |
server/NeonSprawl.Server/Game/Combat/CombatEntityHealthServiceCollectionExtensions.cs |
Ensure health store resolves with behavior registry dependency (singleton ctor injection). |
server/NeonSprawl.Server/Game/Combat/CombatTargetsWorldApi.cs |
Enumerate NPC instance ids from PrototypeNpcRegistry. |
server/NeonSprawl.Server/Game/Combat/CombatTargetFixtureApi.cs |
Reset all three NPC instance rows. |
server/NeonSprawl.Server/Game/Combat/ICombatEntityHealthStore.cs |
XML remarks: NPC instance ids + catalog max HP (not alpha/beta / flat 100). |
server/NeonSprawl.Server/Game/Combat/PrototypeCombatConstants.cs |
Remove or narrow — flat 100 no longer authoritative. |
server/NeonSprawl.Server/Game/Targeting/PlayerTargetStateReader.cs |
Use PrototypeNpcRegistry.TryGet for validity/range. |
server/NeonSprawl.Server/Game/Targeting/TargetingApi.cs |
Target select gate on NPC registry. |
server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs |
Cast target gate on NPC registry. |
server/NeonSprawl.Server/Game/Targeting/PrototypeTargetRegistry.cs |
Delete (after references migrated). |
server/README.md |
Document three NPC combat targets, registry, breaking id change, per-archetype HP. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatEntityHealthStoreTests.cs |
AAA tests for per-archetype max HP, melee/ranged/elite independence. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetsWorldApiTests.cs |
Three targets, ordinal order, melee defeat spine, per-archetype maxHp assertions. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatOperationsTests.cs |
Replace alpha/beta ids with NPC ids. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetFixtureApiTests.cs |
Three-instance reset. |
server/NeonSprawl.Server.Tests/Game/Targeting/TargetingApiTests.cs |
NPC id select/range cases. |
server/NeonSprawl.Server.Tests/Game/Targeting/PlayerTargetStateReaderTests.cs |
NPC anchor range checks. |
server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs |
Cast against NPC ids. |
server/NeonSprawl.Server.Tests/Game/AbilityInput/CooldownSnapshotApiTests.cs |
Update target id literals if referenced. |
server/NeonSprawl.Server.Tests/Game/Gigs/GigProgressionGrantPersistenceIntegrationTests.cs |
Melee defeat id in integration spine. |
bruno/neon-sprawl-server/combat-targets/*.bru |
Three-target assertions; melee defeat spine. |
bruno/neon-sprawl-server/ability-cast/*.bru |
NPC target ids in lock/cast spines. |
bruno/neon-sprawl-server/targeting/*.bru |
NPC target ids in select requests. |
bruno/neon-sprawl-server/gig-progression/Get gig progression after defeat spine.bru |
Melee defeat spine. |
docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md |
Instance registry landed note (when complete). |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E5.M2 row — NEO-91 instance registry. |
docs/plans/E5M2-prototype-backlog.md |
E5M2-05 acceptance checkboxes + landed note when complete. |
Explicitly not modified (client — NEO-97): client/scripts/prototype_target_constants.gd, Godot tests, manual QA. Plan documents the required client migration.
Tests
| File | Coverage |
|---|---|
server/NeonSprawl.Server.Tests/Game/Npc/PrototypeNpcRegistryTests.cs |
Three ids ordinal order; each behaviorDefId resolves via host registry; anchors + SharedLockRadius; unknown id fails TryGet. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatEntityHealthStoreTests.cs |
Lazy init to catalog maxHp (100/80/200); damage/defeat/reset per instance; independence across three rows. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetsWorldApiTests.cs |
GET returns 3 targets in id order with correct maxHp; one pulse on melee → 75/100; four pulses → melee defeated, others full. |
server/NeonSprawl.Server.Tests/Game/Combat/CombatOperationsTests.cs |
Resolve against NPC ids (spot-check melee + elite maxHp paths). |
server/NeonSprawl.Server.Tests/Game/Targeting/TargetingApiTests.cs |
Select melee in range; unknown legacy alpha id → unknown_target. |
server/NeonSprawl.Server.Tests/Game/AbilityInput/AbilityCastApiTests.cs |
Cast accept/deny with NPC target ids. |
Bruno combat-targets/Get combat targets after cast spine.bru is the AC defeat spine (melee, 4× pulse). No docs/manual-qa/NEO-91.md (server-only).
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| Linear blockedBy NEO-89 | Proceed — NEO-89 Done on main. |
adopted |
| Client breakage until NEO-97 | Document in plan + README; cross-link NEO-97; Bruno-only verification is not prototype-complete per full-stack epic decomposition. | adopted |
Flat MaxPrototypeTargetHp removal |
Delete constant; tests assert catalog values per instance. | adopted |
| Gig XP defeat grant | No change — grant fires on any target defeat; melee spine still satisfies NEO-44 chain. | adopted |
| Elite HP in future spines | Defer 8× pulse Bruno until needed; melee spine satisfies AC. | deferred |
Decisions (kickoff)
PrototypeNpcRegistryinGame/Npc/— new source of truth; retire alpha/beta (user confirmed).- Anchors: melee
(-3, 0.5, -3), ranged(3, 0, 3), elite(0, 0.5, 0); lock radius 6.0 (user confirmed). - Behavior binding: 1:1 melee/ranged/elite → frozen behavior ids (adopted).
- Bruno defeat spine:
prototype_npc_melee(user confirmed).