# NEO-87 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-87 |
| **Title** | E5M2-01: Prototype NpcBehaviorDef catalog + schemas + CI |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-87/e5m2-01-prototype-npcbehaviordef-catalog-schemas-ci |
| **Module** | [E5.M2 — NpcAiAndBehaviorProfiles](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) · Epic 5 Slice 2 · backlog **E5M2-01** |
| **Branch** | `NEO-87-e5m2-npc-behavior-catalog-schemas-ci` |
| **Precursor** | [E5.M1](../decomposition/modules/E5_M1_CombatRulesEngine.md) **Ready** — passive dummies remain until E5M2-05 |
| **Pattern** | [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) — row JSON Schema + catalog envelope + `scripts/validate_content.py` gate |
| **Blocks** | [NEO-88](https://linear.app/neon-sprawl/issue/NEO-88) — server NPC behavior catalog load |

## Kickoff clarifications

| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Radius / cooldown stat freeze** | `aggroRadius`, `leashRadius`, `attackCooldownSeconds` per archetype? | **Archetype-specific table** — melee 8/16/3.0 · ranged 10/20/4.0 · elite 8/18/5.0; radii sit above 6 m tab-lock; leash > aggro for anti-kite. | **User:** archetype-specific table (adopted below). |
| **Catalog envelope** | File shape? | `{ "schemaVersion": 1, "npcBehaviors": [ … ] }` in `content/npc-behaviors/prototype_npc_behaviors.json` (mirror abilities / items). | **Adopted** (NEO-76 precedent). |
| **CI constant name** | Slice gate identifier? | **`PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS`** — Epic 5 Slice 2; avoids collision with E5M1 ability gate. | **Adopted**. |
| **Client counterpart** | Godot issue for this story? | **None** — server-only content + CI; player-visible work starts at E5M2-04+ / [NEO-97](https://linear.app/neon-sprawl/issue/NEO-97) / [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98). | **Adopted** (E5M2-01 out of scope). |

## Goal, scope, and out-of-scope

**Goal:** Lock content shape and CI validation for **three** frozen `NpcBehaviorDef` rows before server load (NEO-88+).

**In scope (from Linear + [E5M2-01](E5M2-prototype-backlog.md#e5m2-01--prototype-npcbehaviordef-catalog--schemas--ci)):**

- `content/schemas/npc-behavior-def.schema.json`.
- `content/npc-behaviors/prototype_npc_behaviors.json` — three frozen ids (full freeze table below).
- `scripts/validate_content.py` — schema validation, duplicate `id`, exact three-id allowlist, positive numeric guards (`leashRadius` > `aggroRadius`).
- Expand **Prototype Slice 2 freeze** table in [E5_M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) + designer note in `content/README.md`.
- **CT.M1** PR gate paragraph for NPC behavior catalogs.
- `documentation_and_implementation_alignment.md` E5.M2 row → note NEO-87 catalog in progress / landed.

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

- Server loader, runtime engine, HTTP, Godot (NEO-88+).
- NPC instance registry / dummy migration (E5M2-05).

## Acceptance criteria checklist

- [x] PR gate validates NPC behavior JSON against schema.
- [x] Exactly three prototype behavior ids; duplicate `id` fails CI.
- [x] Stable id list documented in module doc freeze box.

## Implementation reconciliation (shipped)

- **Schema:** [`npc-behavior-def.schema.json`](../../content/schemas/npc-behavior-def.schema.json) — required `id`, `displayName`, `archetypeKind`, `maxHp`, `aggroRadius`, `leashRadius`, `telegraphWindupSeconds`, `attackDamage`, `attackCooldownSeconds`.
- **Catalog:** [`prototype_npc_behaviors.json`](../../content/npc-behaviors/prototype_npc_behaviors.json) — three frozen rows per kickoff stat table.
- **CI:** `PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS` gate + `leashRadius > aggroRadius` guard in [`validate_content.py`](../../scripts/validate_content.py).
- **Docs:** E5.M2 full freeze table, `content/README.md`, CT.M1, alignment register, E5M2-01 backlog checkboxes.

## Technical approach

1. **Row schema (`npc-behavior-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `id`, `displayName`, `archetypeKind`, `maxHp`, `aggroRadius`, `leashRadius`, `telegraphWindupSeconds`, `attackDamage`, `attackCooldownSeconds`. `id` pattern `^[a-z][a-z0-9_]*$`. `displayName` `minLength: 1`. `archetypeKind` enum `melee_pressure` \| `ranged_control` \| `elite_mini_boss`. `maxHp` integer ≥ 1. `attackDamage` integer ≥ 1. `aggroRadius`, `leashRadius`, `telegraphWindupSeconds`, `attackCooldownSeconds` number with `exclusiveMinimum: 0` (strictly positive).

2. **Catalog file:** `content/npc-behaviors/prototype_npc_behaviors.json` with `schemaVersion: 1` and three rows from the kickoff-adopted freeze table.

3. **Frozen content values (E5M2-01 — kickoff adopted):**

   | `NpcBehaviorDef.id` | `displayName` | `archetypeKind` | `maxHp` | `aggroRadius` | `leashRadius` | `telegraphWindupSeconds` | `attackDamage` | `attackCooldownSeconds` |
   |---------------------|---------------|-----------------|---------|---------------|---------------|--------------------------|----------------|-------------------------|
   | `prototype_melee_pressure` | Melee Pressure | `melee_pressure` | 100 | 8.0 | 16.0 | 1.5 | 15 | 3.0 |
   | `prototype_ranged_control` | Ranged Control | `ranged_control` | 80 | 10.0 | 20.0 | 2.0 | 12 | 4.0 |
   | `prototype_elite_mini_boss` | Elite Mini-Boss | `elite_mini_boss` | 200 | 8.0 | 18.0 | 2.5 | 25 | 5.0 |

   **Rules:** Do **not** rename frozen behavior **`id`** values after ship — change **`displayName`** only, or add a new `id` in a follow-up issue. CI enforces **exactly** these three ids. Tune combat feel via numeric fields or new ids in a follow-up issue.

4. **`validate_content.py`:**
   - Add `NPC_BEHAVIOR_SCHEMA`, `NPC_BEHAVIORS_DIR`; discover `content/npc-behaviors/*_npc_behaviors.json`.
   - `_validate_npc_behavior_catalogs`: envelope `schemaVersion: 1`, top-level `npcBehaviors` array; validate each row against `npc-behavior-def.schema.json`; track `seen_ids`; reject duplicates across files.
   - **`_prototype_e5m2_npc_behavior_gate`:** ids must equal `PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS` (frozenset of three ids above).
   - **`_prototype_e5m2_npc_behavior_numeric_gate`:** after schema pass, require `leashRadius > aggroRadius` per row (backlog anti-kite invariant).
   - Run NPC behavior validation after ability catalogs succeed (no cross-catalog refs in E5M2-01).
   - Require NPC behavior schema file in `main()` startup checks; extend module docstring + success summary with npc-behavior file + id counts.
   - Keep `PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS` in sync with [E5.M2 freeze table](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md#prototype-slice-2-freeze-e5m2-01) and future NEO-88 server loader.

5. **Docs:**
   - `content/README.md` — add `npc-behaviors/` table row + E5 Slice 2 NPC behavior freeze paragraph.
   - `E5_M2_NpcAiAndBehaviorProfiles.md` — expand **Prototype Slice 2 freeze (E5M2-01)** table with full column set + CI rules line.
   - `CT_M1_ContentValidationPipeline.md` — NPC behavior catalog PR gate paragraph.
   - `documentation_and_implementation_alignment.md` — E5.M2 row notes NEO-87 catalog when complete.
   - `E5M2-prototype-backlog.md` — E5M2-01 checkboxes when implementation completes.

6. **No server/C#/client changes** in this story.

## Files to add

| Path | Purpose |
|------|---------|
| `content/schemas/npc-behavior-def.schema.json` | JSON Schema for a single `NpcBehaviorDef` row. |
| `content/npc-behaviors/prototype_npc_behaviors.json` | Prototype three-behavior catalog (`schemaVersion` + `npcBehaviors` array). |
| `docs/plans/NEO-87-implementation-plan.md` | This plan. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `scripts/validate_content.py` | Load/validate NPC behavior catalogs; duplicate `id`; E5M2 three-id gate; leash > aggro guard; success summary. |
| `content/README.md` | Document `content/npc-behaviors/`, schema path, E5 Slice 2 freeze (active catalog). |
| `docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md` | Full freeze table + CI enforcement note; link to plan when complete. |
| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for NPC behavior catalog validation. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M2 / NEO-87 status after catalog lands. |
| `docs/plans/E5M2-prototype-backlog.md` | E5M2-01 checkboxes + landed note when complete. |

## Tests

| Item | Coverage |
|------|----------|
| **CI / PR gate** | `.github/workflows/pr-gate.yml` already runs `python scripts/validate_content.py` — extended script is the regression signal. |
| **Manual negative cases** | From repo root after implementation: (1) duplicate behavior `id` → non-zero exit; (2) remove one frozen id → gate error; (3) break schema (zero `aggroRadius` or `leashRadius <= aggroRadius`) → schema or numeric gate error; (4) empty `displayName` → schema error. |
| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports npc-behavior catalog + existing catalogs OK. |

No dedicated pytest module in-repo today (same as NEO-76 / NEO-50).

## Open questions / risks

| Question / risk | Agent recommendation | Status |
|-----------------|----------------------|--------|
| **Catalog growth** | E5M2 gate requires **exactly** the three frozen ids; expanding the roster needs a follow-up issue to change `PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS`. | **adopted** |
| **C# vs Python drift** | None until NEO-88; document “keep in sync” constant names in both places when server load lands (mirror NEO-77 / `PROTOTYPE_E5M1_ABILITY_IDS`). | **adopted** |
| **Radii vs tab-lock 6 m** | `aggroRadius` 8–10 m keeps proximity re-aggro usable in the combat pocket without matching tab-lock exactly; E5M2-06 distance checks use behavior-def radii, not `PrototypeTargetRegistry.SharedLockRadius`. | **adopted** |
| **Runtime still uses alpha/beta dummies** | Expected — content lands before E5M2-05 migrates combat targets to NPC instance ids. | **adopted** |

None blocking.
