# 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](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) · Epic 5 Slice 2 · backlog **E5M2-05** |
| **Branch** | `NEO-91-npc-instance-registry-combat-target-migration` |
| **Precursor** | [NEO-89](https://linear.app/neon-sprawl/issue/NEO-89) — `INpcBehaviorDefinitionRegistry` + DI (**Done** on `main`) |
| **Pattern** | [NEO-83](https://linear.app/neon-sprawl/issue/NEO-83) — combat-targets GET + `PrototypeTargetRegistry`; [NEO-89](https://linear.app/neon-sprawl/issue/NEO-89) — injectable behavior registry |
| **Client counterpart** | [NEO-97](https://linear.app/neon-sprawl/issue/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](E5M2-prototype-backlog.md#e5m2-05--prototype-npc-instance-registry--combat-target-migration)):**

- **`PrototypeNpcRegistry`** in `Game/Npc/`: instance id, `behaviorDefId`, world anchor, shared lock radius.
- Migrate **`ICombatEntityHealthStore`** gates and max HP from flat 100 → per-instance **`maxHp`** via **`INpcBehaviorDefinitionRegistry`**.
- Replace **`PrototypeTargetRegistry`** alpha/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](https://linear.app/neon-sprawl/issue/NEO-97)).

**Out of scope (from Linear + backlog):**

- Behavior tick, aggro, telegraphs ([NEO-92+](E5M2-prototype-backlog.md)).
- Godot markers / tab cycle ([NEO-97](https://linear.app/neon-sprawl/issue/NEO-97)).
- `docs/manual-qa/NEO-91.md` — server-only; Bruno + automated tests (NEO-83/90 pattern).

## Acceptance criteria checklist

- [x] Cast + combat-targets GET operate on **three NPC ids only** (alpha/beta removed).
- [x] Each instance references a valid frozen **`behaviorDefId`** resolvable via **`INpcBehaviorDefinitionRegistry`**.
- [x] Bruno defeat spine passes for **`prototype_npc_melee`** (4× pulse → defeated).

## Technical approach

1. **`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`, anchor `X/Y/Z`, `LockRadius`.
   - Frozen bindings (kickoff):

     | Instance id | `behaviorDefId` | Anchor (X, Y, Z) |
     |-------------|-----------------|------------------|
     | `prototype_npc_melee` | `prototype_melee_pressure` | (-3, 0.5, -3) |
     | `prototype_npc_ranged` | `prototype_ranged_control` | (3, 0, 3) |
     | `prototype_npc_elite` | `prototype_elite_mini_boss` | (0, 0.5, 0) |

   - **`TryGet(instanceIdLowercase, out entry)`**, **`GetInstanceIdsInOrder()`** (ordinal sort — tab cycle order for future NEO-97).

2. **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 import **`PrototypeNpcRegistry`** instead.

3. **`InMemoryCombatEntityHealthStore`:** Constructor-inject **`INpcBehaviorDefinitionRegistry`**. Gate rows on **`PrototypeNpcRegistry.TryGet`**. Lazy-init **`currentHp`** to behavior **`maxHp`**; **`CreateSnapshot`** / **`TryResetToFull`** use per-instance max from catalog. Remove dependence on **`PrototypeCombatConstants.MaxPrototypeTargetHp`** (delete constant class if unused).

4. **Targeting + cast paths:** **`PlayerTargetStateReader`**, **`TargetingApi`**, **`AbilityCastApi`** — swap **`PrototypeTargetRegistry.TryGet`** → **`PrototypeNpcRegistry.TryGet`** (range math unchanged).

5. **Combat-targets GET + fixture:** **`CombatTargetsWorldApi`**, **`CombatTargetFixtureApi`** — enumerate **`PrototypeNpcRegistry.GetInstanceIdsInOrder()`** (expect **3** rows).

6. **Bruno migration:** Replace every **`prototype_target_alpha`** / **`prototype_target_beta`** with NPC ids. Key updates:
   - **`combat-targets/`** — assert **3** targets; defeat spine on **`prototype_npc_melee`** (same 4× pulse / 75→50→25→0 stepping).
   - **`ability-cast/`** — happy cast on **`prototype_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.

7. **Docs:** **`server/README.md`** — combat targets section lists three NPC ids + per-archetype max HP; note **`PrototypeNpcRegistry`** + breaking change for **`client/scripts/prototype_target_constants.gd`** ([NEO-97](https://linear.app/neon-sprawl/issue/NEO-97)). Update [E5_M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) instance registry bullet and [E5M2-prototype-backlog.md](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](../../.cursor/rules/full-stack-epic-decomposition.md). | **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** |

## Reconciliation (implementation)

- **`PrototypeNpcRegistry`** landed in `Game/Npc/`; **`PrototypeTargetRegistry`** and **`PrototypeCombatConstants`** removed.
- **`InMemoryCombatEntityHealthStore`** injects **`INpcBehaviorDefinitionRegistry`** for per-instance catalog max HP.
- Server tests + Bruno migrated to three NPC ids; melee 4× pulse defeat spine is AC verification.
- **Client not updated** — [NEO-97](https://linear.app/neon-sprawl/issue/NEO-97) owns `prototype_target_constants.gd` migration.
