diff --git a/bruno/neon-sprawl-server/npc-behavior-definitions/Get npc behavior definitions.bru b/bruno/neon-sprawl-server/npc-behavior-definitions/Get npc behavior definitions.bru
new file mode 100644
index 0000000..0358b1e
--- /dev/null
+++ b/bruno/neon-sprawl-server/npc-behavior-definitions/Get npc behavior definitions.bru
@@ -0,0 +1,67 @@
+meta {
+ name: GET npc behavior definitions
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/game/world/npc-behavior-definitions
+ body: none
+ auth: none
+}
+
+tests {
+ test("returns 200 JSON with schema v1 and npcBehaviors array", function () {
+ expect(res.getStatus()).to.equal(200);
+ expect(res.getHeader("content-type")).to.contain("application/json");
+ const body = res.getBody();
+ expect(body.schemaVersion).to.equal(1);
+ expect(body.npcBehaviors).to.be.an("array");
+ expect(body.npcBehaviors.length).to.equal(3);
+ });
+
+ test("npcBehaviors are ascending by id (ordinal)", function () {
+ const body = res.getBody();
+ const ids = body.npcBehaviors.map((x) => x.id);
+ const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
+ expect(ids).to.eql(sorted);
+ });
+
+ test("frozen prototype three matches registry id order", function () {
+ const body = res.getBody();
+ const ids = body.npcBehaviors.map((x) => x.id);
+ expect(ids).to.eql([
+ "prototype_elite_mini_boss",
+ "prototype_melee_pressure",
+ "prototype_ranged_control",
+ ]);
+ });
+
+ test("prototype_melee_pressure row matches catalog", function () {
+ const body = res.getBody();
+ const row = body.npcBehaviors.find((x) => x.id === "prototype_melee_pressure");
+ expect(row).to.be.an("object");
+ expect(row.displayName).to.equal("Melee Pressure");
+ expect(row.archetypeKind).to.equal("melee_pressure");
+ expect(row.maxHp).to.equal(100);
+ expect(row.aggroRadius).to.equal(8);
+ expect(row.leashRadius).to.equal(16);
+ expect(row.telegraphWindupSeconds).to.equal(1.5);
+ expect(row.attackDamage).to.equal(15);
+ expect(row.attackCooldownSeconds).to.equal(3);
+ });
+
+ test("prototype_elite_mini_boss row matches catalog", function () {
+ const body = res.getBody();
+ const row = body.npcBehaviors.find((x) => x.id === "prototype_elite_mini_boss");
+ expect(row).to.be.an("object");
+ expect(row.displayName).to.equal("Elite Mini-Boss");
+ expect(row.archetypeKind).to.equal("elite_mini_boss");
+ expect(row.maxHp).to.equal(200);
+ expect(row.aggroRadius).to.equal(8);
+ expect(row.leashRadius).to.equal(18);
+ expect(row.telegraphWindupSeconds).to.equal(2.5);
+ expect(row.attackDamage).to.equal(25);
+ expect(row.attackCooldownSeconds).to.equal(5);
+ });
+}
diff --git a/bruno/neon-sprawl-server/npc-behavior-definitions/folder.bru b/bruno/neon-sprawl-server/npc-behavior-definitions/folder.bru
new file mode 100644
index 0000000..27f770f
--- /dev/null
+++ b/bruno/neon-sprawl-server/npc-behavior-definitions/folder.bru
@@ -0,0 +1,3 @@
+meta {
+ name: npc-behavior-definitions
+}
diff --git a/docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md b/docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md
index 5f37af3..012a8b4 100644
--- a/docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md
+++ b/docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md
@@ -7,7 +7,7 @@
| **Module ID** | E5.M2 |
| **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) |
| **Stage target** | Prototype |
-| **Status** | In Progress — Slice 2 backlog [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md): **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) catalog + CI landed · **E5M2-02** [NEO-88](https://linear.app/neon-sprawl/issue/NEO-88) server load landed · **E5M2-03** [NEO-89](https://linear.app/neon-sprawl/issue/NEO-89) registry + DI landed → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) |
+| **Status** | In Progress — Slice 2 backlog [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md): **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) catalog + CI landed · **E5M2-02** [NEO-88](https://linear.app/neon-sprawl/issue/NEO-88) server load landed · **E5M2-03** [NEO-89](https://linear.app/neon-sprawl/issue/NEO-89) registry + DI landed · **E5M2-04** [NEO-90](https://linear.app/neon-sprawl/issue/NEO-90) behavior-definitions GET landed → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) |
| **Linear** | Label **`E5.M2`** · [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md) **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) · **E5M2-02** [NEO-88](https://linear.app/neon-sprawl/issue/NEO-88) · **E5M2-03** [NEO-89](https://linear.app/neon-sprawl/issue/NEO-89) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) |
## Purpose
@@ -42,7 +42,7 @@ Enemy archetype behavior loops, aggro logic, and telegraph scheduling on top of
## Related implementation slices
-Epic 5 **Slice 2 — NPC archetypes and telegraphs**: three archetypes (melee pressure, ranged control, elite mini-boss); telemetry `telegraph_fired`, `npc_state_transition`.
+Epic 5 **Slice 2 — NPC archetypes and telegraphs**: three archetypes (melee pressure, ranged control, elite mini-boss); telemetry `telegraph_fired`, `npc_state_transition`. **HTTP read model (NEO-90):** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/`; Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`.
## Linear backlog (decomposed)
@@ -79,7 +79,9 @@ The **first shipped three-archetype NPC spine** under `content/npc-behaviors/*.j
**Server load (NEO-88):** On host startup, `server/NeonSprawl.Server/Game/Npc/` loads `content/npc-behaviors/*_npc_behaviors.json` with the same validation gates as CI and **refuses to listen** when the catalog is invalid. Config and discovery: [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). Plan: [NEO-88 implementation plan](../../plans/NEO-88-implementation-plan.md).
-**NPC behavior definition registry (NEO-89):** **`INpcBehaviorDefinitionRegistry`** in `server/NeonSprawl.Server/Game/Npc/` wraps the startup-loaded catalog for read-only lookup by stable behavior `id` (`TryGetDefinition`), trim/lowercase validation (`TryNormalizeKnown`), and ordered enumeration (`GetDefinitionsInIdOrder`). Stable prototype id constants: **`PrototypeNpcBehaviorRegistry`**. **NEO-90+** (HTTP definitions, instance registry, runtime tick) should inject the interface rather than `NpcBehaviorDefinitionCatalog`. Plan: [NEO-89 implementation plan](../../plans/NEO-89-implementation-plan.md).
+**NPC behavior definition registry (NEO-89):** **`INpcBehaviorDefinitionRegistry`** in `server/NeonSprawl.Server/Game/Npc/` wraps the startup-loaded catalog for read-only lookup by stable behavior `id` (`TryGetDefinition`), trim/lowercase validation (`TryNormalizeKnown`), and ordered enumeration (`GetDefinitionsInIdOrder`). Stable prototype id constants: **`PrototypeNpcBehaviorRegistry`**. **NEO-91+** (instance registry, runtime tick) should inject the interface rather than `NpcBehaviorDefinitionCatalog`. Plan: [NEO-89 implementation plan](../../plans/NEO-89-implementation-plan.md).
+
+**NPC behavior definitions HTTP (NEO-90):** **`GET /game/world/npc-behavior-definitions`** — versioned read-only projection (`schemaVersion` **1**, **`npcBehaviors`**) backed by **`INpcBehaviorDefinitionRegistry`**. Plan: [NEO-90 implementation plan](../../plans/NEO-90-implementation-plan.md); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`.
## Risks and telemetry
diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md
index 8972f16..954fc6f 100644
--- a/docs/decomposition/modules/documentation_and_implementation_alignment.md
+++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md
@@ -57,7 +57,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. **NEO-72 landed:** client inventory HUD — **`inventory_client.gd`**, **`item_definitions_client.gd`**, **`InventoryLabel`**, boot hydrate + **`I`** refresh ([NEO-72](../../plans/NEO-72-implementation-plan.md), [`NEO-72` manual QA](../../manual-qa/NEO-72.md)); `client/README.md` inventory HUD section. **NEO-75 landed:** **`prototype_economy_hud_section.gd`** collapse toggle + capstone manual QA ([NEO-75](../../plans/NEO-75-implementation-plan.md), [`NEO-75` manual QA](../../manual-qa/NEO-75.md)); Epic 3 Slice 5 client complete. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [NEO-72](../../plans/NEO-72-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
| E3.M2 | Ready | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-67 landed:** injectable **`IRecipeDefinitionRegistry`** + lookup tests ([NEO-67](../../plans/NEO-67-implementation-plan.md)). **NEO-68 landed:** **`GET /game/world/recipe-definitions`** — `RecipeDefinitionsWorldApi` + DTOs in `Game/Crafting/` ([NEO-68](../../plans/NEO-68-implementation-plan.md), [`NEO-68` manual QA](../../manual-qa/NEO-68.md)); [server README — Recipe definitions (NEO-68)](../../../server/README.md#recipe-definitions-neo-68); Bruno `bruno/neon-sprawl-server/recipe-definitions/`. **NEO-69 landed:** **`CraftOperations.TryCraft`** + **`CraftResult`** — pre-flight inputs/outputs, inventory mutation, refine XP ([NEO-69](../../plans/NEO-69-implementation-plan.md)); [server README — Craft engine (NEO-69)](../../../server/README.md#craft-engine-neo-69-and-craft-http-neo-70). **NEO-70 landed:** **`POST /game/players/{id}/craft`** — `PlayerCraftApi` + wire **`CraftResponse`**; Bruno gather→refine→make spine ([NEO-70](../../plans/NEO-70-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/craft/`. **NEO-71 landed:** comment-only **`item_crafted`** / **`craft_failed`** telemetry hook sites in **`CraftOperations.TryCraft`** ([NEO-71](../../plans/NEO-71-implementation-plan.md)); [server README — Craft telemetry hooks (NEO-71)](../../../server/README.md#craft-telemetry-hooks-neo-71). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** — wired from craft engine success path. Epic 3 Slice 3 server backlog **E3M2-01**–**E3M2-07** complete. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **NEO-74 landed:** client craft UI — **`recipe_definitions_client.gd`**, **`craft_client.gd`**, **`craft_recipe_panel.gd`**, **`CraftFeedbackLabel`**, **refine** skill row ([NEO-74](../../plans/NEO-74-implementation-plan.md), [`NEO-74` manual QA](../../manual-qa/NEO-74.md)); `client/README.md` craft section. **NEO-75 landed:** capstone playable gather→refine→make loop — [`NEO-75` manual QA](../../manual-qa/NEO-75.md), **`prototype_economy_hud_section.gd`**; Epic 3 Slice 5 client complete. | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](../../plans/NEO-67-implementation-plan.md), [NEO-68](../../plans/NEO-68-implementation-plan.md), [NEO-69](../../plans/NEO-69-implementation-plan.md), [NEO-70](../../plans/NEO-70-implementation-plan.md), [NEO-71](../../plans/NEO-71-implementation-plan.md), [NEO-74](../../plans/NEO-74-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) |
| E5.M1 | Ready | **NEO-76 landed:** frozen prototype four-ability catalog in [`content/abilities/prototype_abilities.json`](../../../content/abilities/prototype_abilities.json); [`ability-def.schema.json`](../../../content/schemas/ability-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M1 four-id gate ([NEO-76](../../plans/NEO-76-implementation-plan.md)). **NEO-77 landed:** fail-fast server load of `content/abilities/*_abilities.json` at startup — `server/NeonSprawl.Server/Game/Combat/` ([NEO-77](../../plans/NEO-77-implementation-plan.md)); [server README — Ability catalog](../../../server/README.md#ability-catalog-contentabilities-neo-77). **NEO-79 landed:** injectable **`IAbilityDefinitionRegistry`** + DI; hotbar/cast use **`TryNormalizeKnown`** ([NEO-79](../../plans/NEO-79-implementation-plan.md)); Bruno `bruno/neon-sprawl-server/hotbar-loadout/` + `ability-cast/` unknown-ability deny smokes. **NEO-78 landed:** **`GET /game/world/ability-definitions`** — `AbilityDefinitionsWorldApi` + DTOs in `Game/Combat/` ([NEO-78](../../plans/NEO-78-implementation-plan.md)); [server README — Ability definitions (NEO-78)](../../../server/README.md#ability-definitions-neo-78); Bruno `bruno/neon-sprawl-server/ability-definitions/`. **NEO-80 landed:** **`ICombatEntityHealthStore`** / **`InMemoryCombatEntityHealthStore`** — lazy prototype dummy HP at **`PrototypeCombatConstants.MaxPrototypeTargetHp` (100)**; DI via **`AddCombatEntityHealthStore()`** ([NEO-80](../../plans/NEO-80-implementation-plan.md)); [server README — Combat entity health (NEO-80)](../../../server/README.md#combat-entity-health-neo-80). **NEO-81 landed:** **`CombatOperations.TryResolve`** + **`CombatResult`** + **`CombatReasonCodes`** — defeated pre-check deny, catalog damage ([NEO-81](../../plans/NEO-81-implementation-plan.md)); [server README — Combat engine (NEO-81)](../../../server/README.md#combat-engine-neo-81). **NEO-82 landed:** **`AbilityCastApi`** → **`CombatOperations.TryResolve`**; nested wire **`combatResolution`** on accept; per-ability catalog cooldown; **`target_defeated`** cast deny ([NEO-82](../../plans/NEO-82-implementation-plan.md)); [server README — Ability cast (NEO-82)](../../../server/README.md#ability-cast-neo-31-neo-82); Bruno `bruno/neon-sprawl-server/ability-cast/` defeat spine. **NEO-83 landed:** **`GET /game/world/combat-targets`** — `CombatTargetsWorldApi` + DTOs; authoritative HP snapshot from **`ICombatEntityHealthStore`** ([NEO-83](../../plans/NEO-83-implementation-plan.md)); [server README — Combat targets snapshot (NEO-83)](../../../server/README.md#combat-targets-snapshot-neo-83); Bruno `bruno/neon-sprawl-server/combat-targets/`. **NEO-44 landed:** **`CombatDefeatGigXpGrant`** on cast **`targetDefeated`** — **25** gig XP to **`breach`** via **`IPlayerGigProgressionStore`** (V007); **`GET …/gig-progression`** ([NEO-44](../../plans/NEO-44-implementation-plan.md), [`NEO-44` manual QA](../../manual-qa/NEO-44.md)); [server README — Gig progression (NEO-44)](../../../server/README.md#gig-progression-snapshot-neo-44); Bruno `bruno/neon-sprawl-server/gig-progression/`. **NEO-84 landed:** comment-only **`ability_used`** / **`enemy_defeat`** telemetry hook sites in **`CombatOperations.TryResolve`**; reserved **`encounter_start`** / **`player_death`** ([NEO-84](../../plans/NEO-84-implementation-plan.md)); [server README — Combat telemetry hooks (NEO-84)](../../../server/README.md#combat-telemetry-hooks-neo-84). **NEO-85 landed:** client combat HUD — **`ability_cast_client.gd`** parses **`combatResolution`**; **`combat_targets_client.gd`**, **`CastFeedbackLabel`** resolution copy, **`CombatTargetHpLabel`**; event-driven GET refresh ([NEO-85](../../plans/NEO-85-implementation-plan.md), [`NEO-85` manual QA](../../manual-qa/NEO-85.md)); `client/README.md` combat HUD section. **NEO-86 landed:** playable combat capstone — **`gig_progression_client.gd`**, **`GigXpLabel`**, defeat-triggered gig GET refresh; capstone manual QA [`NEO-86`](../../manual-qa/NEO-86.md); `client/README.md` end-to-end combat loop section ([NEO-86](../../plans/NEO-86-implementation-plan.md)). **Epic 5 Slice 1 client capstone complete.** **Backlog decomposed:** Epic 5 Slice 1 — **E5M1-01**–**E5M1-12** landed. **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32). | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [NEO-77](../../plans/NEO-77-implementation-plan.md), [NEO-79](../../plans/NEO-79-implementation-plan.md), [NEO-78](../../plans/NEO-78-implementation-plan.md), [NEO-80](../../plans/NEO-80-implementation-plan.md), [NEO-81](../../plans/NEO-81-implementation-plan.md), [NEO-82](../../plans/NEO-82-implementation-plan.md), [NEO-83](../../plans/NEO-83-implementation-plan.md), [NEO-44](../../plans/NEO-44-implementation-plan.md), [NEO-84](../../plans/NEO-84-implementation-plan.md), [NEO-85](../../plans/NEO-85-implementation-plan.md), [NEO-86](../../plans/NEO-86-implementation-plan.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) |
-| E5.M2 | In Progress | **NEO-87 landed:** frozen prototype three-behavior catalog in [`content/npc-behaviors/prototype_npc_behaviors.json`](../../../content/npc-behaviors/prototype_npc_behaviors.json); [`npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M2 three-id gate ([NEO-87](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` at startup — `server/NeonSprawl.Server/Game/Npc/` ([NEO-88](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI; **`PrototypeNpcBehaviorRegistry`** id constants ([NEO-89](../../plans/NEO-89-implementation-plan.md)). **Backlog decomposed:** Epic 5 Slice 2 — [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md) **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); client capstone **NEO-98**. Three archetypes (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`); replaces alpha/beta dummies with **`prototype_npc_melee` / `ranged` / `elite`**. Owns **`ThreatState`**, **`TelegraphEvent`**, **`GET …/npc-runtime-snapshot`**, session **`IPlayerCombatHealthStore`**. Upstream **E5.M1 Ready**. | [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2](E5_M2_NpcAiAndBehaviorProfiles.md), [NEO-87](../../plans/NEO-87-implementation-plan.md), [NEO-88](../../plans/NEO-88-implementation-plan.md), [NEO-89](../../plans/NEO-89-implementation-plan.md), label **`E5.M2`** on NEO-87–NEO-98 |
+| E5.M2 | In Progress | **NEO-87 landed:** frozen prototype three-behavior catalog in [`content/npc-behaviors/prototype_npc_behaviors.json`](../../../content/npc-behaviors/prototype_npc_behaviors.json); [`npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) E5M2 three-id gate ([NEO-87](../../plans/NEO-87-implementation-plan.md)). **NEO-88 landed:** fail-fast server load of `content/npc-behaviors/*_npc_behaviors.json` at startup — `server/NeonSprawl.Server/Game/Npc/` ([NEO-88](../../plans/NEO-88-implementation-plan.md)); [server README — NPC behavior catalog](../../../server/README.md#npc-behavior-catalog-contentnpc-behaviors-neo-88). **NEO-89 landed:** injectable **`INpcBehaviorDefinitionRegistry`** + DI; **`PrototypeNpcBehaviorRegistry`** id constants ([NEO-89](../../plans/NEO-89-implementation-plan.md)). **NEO-90 landed:** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/` ([NEO-90](../../plans/NEO-90-implementation-plan.md)); [server README — NPC behavior definitions (NEO-90)](../../../server/README.md#npc-behavior-definitions-neo-90); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`. **Backlog decomposed:** Epic 5 Slice 2 — [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md) **E5M2-01** [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); client capstone **NEO-98**. Three archetypes (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`); replaces alpha/beta dummies with **`prototype_npc_melee` / `ranged` / `elite`**. Owns **`ThreatState`**, **`TelegraphEvent`**, **`GET …/npc-runtime-snapshot`**, session **`IPlayerCombatHealthStore`**. Upstream **E5.M1 Ready**. | [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2](E5_M2_NpcAiAndBehaviorProfiles.md), [NEO-87](../../plans/NEO-87-implementation-plan.md), [NEO-88](../../plans/NEO-88-implementation-plan.md), [NEO-89](../../plans/NEO-89-implementation-plan.md), [NEO-90](../../plans/NEO-90-implementation-plan.md), label **`E5.M2`** on NEO-87–NEO-98 |
---
diff --git a/docs/plans/E5M2-prototype-backlog.md b/docs/plans/E5M2-prototype-backlog.md
index fd354a2..7ec6022 100644
--- a/docs/plans/E5M2-prototype-backlog.md
+++ b/docs/plans/E5M2-prototype-backlog.md
@@ -165,8 +165,10 @@ Working backlog for **Epic 5 — Slice 2** ([NPC archetypes and telegraphs](../d
**Acceptance criteria**
-- [ ] GET returns all three defs in ascending `id` order with `schemaVersion` **1**.
-- [ ] Bruno happy GET passes in CI Bruno step.
+- [x] GET returns all three defs in ascending `id` order with `schemaVersion` **1**.
+- [x] Bruno happy GET passes in CI Bruno step.
+
+**Landed ([NEO-90](https://linear.app/neon-sprawl/issue/NEO-90)):** **`GET /game/world/npc-behavior-definitions`** — `NpcBehaviorDefinitionsWorldApi` + DTOs in `Game/Npc/`; plan [NEO-90-implementation-plan.md](NEO-90-implementation-plan.md); Bruno `bruno/neon-sprawl-server/npc-behavior-definitions/`.
---
diff --git a/docs/plans/NEO-90-implementation-plan.md b/docs/plans/NEO-90-implementation-plan.md
index bafdc82..4f159a3 100644
--- a/docs/plans/NEO-90-implementation-plan.md
+++ b/docs/plans/NEO-90-implementation-plan.md
@@ -43,9 +43,9 @@ No other blocking decisions — route path, `schemaVersion` **1**, registry inje
## Acceptance criteria checklist
-- [ ] GET returns all **three** prototype behavior defs with **`schemaVersion`** **1** and stable field names.
-- [ ] Rows appear in ascending **`id`** order (ordinal).
-- [ ] Bruno happy GET passes in CI Bruno step.
+- [x] GET returns all **three** prototype behavior defs with **`schemaVersion`** **1** and stable field names.
+- [x] Rows appear in ascending **`id`** order (ordinal).
+- [x] Bruno happy GET passes in CI Bruno step.
## Technical approach
@@ -116,3 +116,10 @@ Bruno scripts complement automated tests for manual dev-server verification. No
- **`npcBehaviors`** top-level array key — matches content catalog envelope (user confirmed).
- **No manual QA doc** — server-only HTTP; Bruno + automated tests (user confirmed).
- **Registry-only injection** — HTTP layer depends on **`INpcBehaviorDefinitionRegistry`**, not **`NpcBehaviorDefinitionCatalog`**.
+
+## Reconciliation (implementation)
+
+- **`NpcBehaviorDefinitionsWorldApi`** + **`NpcBehaviorDefinitionsListDtos`** registered via **`MapNpcBehaviorDefinitionsWorldApi()`** in **`Program.cs`**; injects **`INpcBehaviorDefinitionRegistry`** only.
+- **`NpcBehaviorDefinitionsWorldApiTests`** — integration test for schema v1, frozen-three id order, **`prototype_melee_pressure`** / **`prototype_elite_mini_boss`** spot-checks.
+- **Bruno:** `bruno/neon-sprawl-server/npc-behavior-definitions/Get npc behavior definitions.bru` with response-shape tests.
+- **`server/README.md`**, **E5.M2** module doc, **alignment register**, and **E5M2 backlog** updated.
diff --git a/server/NeonSprawl.Server.Tests/Game/Npc/NpcBehaviorDefinitionsWorldApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Npc/NpcBehaviorDefinitionsWorldApiTests.cs
new file mode 100644
index 0000000..1b8b01c
--- /dev/null
+++ b/server/NeonSprawl.Server.Tests/Game/Npc/NpcBehaviorDefinitionsWorldApiTests.cs
@@ -0,0 +1,57 @@
+using System.Linq;
+using System.Net;
+using System.Net.Http.Json;
+using NeonSprawl.Server.Game.Npc;
+using Xunit;
+
+namespace NeonSprawl.Server.Tests.Game.Npc;
+
+public class NpcBehaviorDefinitionsWorldApiTests
+{
+ /// Frozen prototype three in registry id order (ordinal). Keep in sync with Bruno.
+ public static readonly string[] FrozenThreeInIdOrder =
+ [
+ PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss,
+ PrototypeNpcBehaviorRegistry.PrototypeMeleePressure,
+ PrototypeNpcBehaviorRegistry.PrototypeRangedControl,
+ ];
+
+ [Fact]
+ public async Task GetNpcBehaviorDefinitions_ShouldReturnSchemaV1_WithFrozenThreeInIdOrder()
+ {
+ // Arrange
+ await using var factory = new InMemoryWebApplicationFactory();
+ var client = factory.CreateClient();
+ // Act
+ var response = await client.GetAsync("/game/world/npc-behavior-definitions");
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ var body = await response.Content.ReadFromJsonAsync();
+ Assert.NotNull(body);
+ Assert.Equal(NpcBehaviorDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
+ Assert.NotNull(body.NpcBehaviors);
+ Assert.Equal(3, body.NpcBehaviors.Count);
+ var ids = body.NpcBehaviors.Select(static b => b.Id).ToList();
+ Assert.Equal(FrozenThreeInIdOrder, ids);
+
+ var melee = body.NpcBehaviors.Single(b => b.Id == PrototypeNpcBehaviorRegistry.PrototypeMeleePressure);
+ Assert.Equal("Melee Pressure", melee.DisplayName);
+ Assert.Equal("melee_pressure", melee.ArchetypeKind);
+ Assert.Equal(100, melee.MaxHp);
+ Assert.Equal(8.0, melee.AggroRadius);
+ Assert.Equal(16.0, melee.LeashRadius);
+ Assert.Equal(1.5, melee.TelegraphWindupSeconds);
+ Assert.Equal(15, melee.AttackDamage);
+ Assert.Equal(3.0, melee.AttackCooldownSeconds);
+
+ var elite = body.NpcBehaviors.Single(b => b.Id == PrototypeNpcBehaviorRegistry.PrototypeEliteMiniBoss);
+ Assert.Equal("Elite Mini-Boss", elite.DisplayName);
+ Assert.Equal("elite_mini_boss", elite.ArchetypeKind);
+ Assert.Equal(200, elite.MaxHp);
+ Assert.Equal(8.0, elite.AggroRadius);
+ Assert.Equal(18.0, elite.LeashRadius);
+ Assert.Equal(2.5, elite.TelegraphWindupSeconds);
+ Assert.Equal(25, elite.AttackDamage);
+ Assert.Equal(5.0, elite.AttackCooldownSeconds);
+ }
+}
diff --git a/server/NeonSprawl.Server/Game/Npc/NpcBehaviorDefinitionsListDtos.cs b/server/NeonSprawl.Server/Game/Npc/NpcBehaviorDefinitionsListDtos.cs
new file mode 100644
index 0000000..e6c7e1e
--- /dev/null
+++ b/server/NeonSprawl.Server/Game/Npc/NpcBehaviorDefinitionsListDtos.cs
@@ -0,0 +1,47 @@
+using System.Text.Json.Serialization;
+
+namespace NeonSprawl.Server.Game.Npc;
+
+/// JSON body for GET /game/world/npc-behavior-definitions (NEO-90).
+public sealed class NpcBehaviorDefinitionsListResponse
+{
+ public const int CurrentSchemaVersion = 1;
+
+ [JsonPropertyName("schemaVersion")]
+ public int SchemaVersion { get; init; } = CurrentSchemaVersion;
+
+ /// Loaded behavior defs ordered by stable id (ordinal), matching .
+ [JsonPropertyName("npcBehaviors")]
+ public required IReadOnlyList NpcBehaviors { get; init; }
+}
+
+/// One row in the read-only NPC behavior definition projection.
+public sealed class NpcBehaviorDefinitionJson
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; init; }
+
+ [JsonPropertyName("displayName")]
+ public required string DisplayName { get; init; }
+
+ [JsonPropertyName("archetypeKind")]
+ public required string ArchetypeKind { get; init; }
+
+ [JsonPropertyName("maxHp")]
+ public required int MaxHp { get; init; }
+
+ [JsonPropertyName("aggroRadius")]
+ public required double AggroRadius { get; init; }
+
+ [JsonPropertyName("leashRadius")]
+ public required double LeashRadius { get; init; }
+
+ [JsonPropertyName("telegraphWindupSeconds")]
+ public required double TelegraphWindupSeconds { get; init; }
+
+ [JsonPropertyName("attackDamage")]
+ public required int AttackDamage { get; init; }
+
+ [JsonPropertyName("attackCooldownSeconds")]
+ public required double AttackCooldownSeconds { get; init; }
+}
diff --git a/server/NeonSprawl.Server/Game/Npc/NpcBehaviorDefinitionsWorldApi.cs b/server/NeonSprawl.Server/Game/Npc/NpcBehaviorDefinitionsWorldApi.cs
new file mode 100644
index 0000000..66eb048
--- /dev/null
+++ b/server/NeonSprawl.Server/Game/Npc/NpcBehaviorDefinitionsWorldApi.cs
@@ -0,0 +1,41 @@
+namespace NeonSprawl.Server.Game.Npc;
+
+/// Maps GET /game/world/npc-behavior-definitions (NEO-90).
+public static class NpcBehaviorDefinitionsWorldApi
+{
+ public static WebApplication MapNpcBehaviorDefinitionsWorldApi(this WebApplication app)
+ {
+ app.MapGet(
+ "/game/world/npc-behavior-definitions",
+ (INpcBehaviorDefinitionRegistry registry) =>
+ {
+ var defs = registry.GetDefinitionsInIdOrder();
+ var npcBehaviors = new List(defs.Count);
+ foreach (var d in defs)
+ {
+ npcBehaviors.Add(
+ new NpcBehaviorDefinitionJson
+ {
+ Id = d.Id,
+ DisplayName = d.DisplayName,
+ ArchetypeKind = d.ArchetypeKind,
+ MaxHp = d.MaxHp,
+ AggroRadius = d.AggroRadius,
+ LeashRadius = d.LeashRadius,
+ TelegraphWindupSeconds = d.TelegraphWindupSeconds,
+ AttackDamage = d.AttackDamage,
+ AttackCooldownSeconds = d.AttackCooldownSeconds,
+ });
+ }
+
+ return Results.Json(
+ new NpcBehaviorDefinitionsListResponse
+ {
+ SchemaVersion = NpcBehaviorDefinitionsListResponse.CurrentSchemaVersion,
+ NpcBehaviors = npcBehaviors,
+ });
+ });
+
+ return app;
+ }
+}
diff --git a/server/NeonSprawl.Server/Program.cs b/server/NeonSprawl.Server/Program.cs
index d3b7020..9178f58 100644
--- a/server/NeonSprawl.Server/Program.cs
+++ b/server/NeonSprawl.Server/Program.cs
@@ -54,6 +54,7 @@ app.MapSkillDefinitionsWorldApi();
app.MapItemDefinitionsWorldApi();
app.MapRecipeDefinitionsWorldApi();
app.MapAbilityDefinitionsWorldApi();
+app.MapNpcBehaviorDefinitionsWorldApi();
app.MapCombatTargetsWorldApi();
app.MapResourceNodeDefinitionsWorldApi();
app.MapPlayerInventoryApi();
diff --git a/server/README.md b/server/README.md
index d660636..96e8eaa 100644
--- a/server/README.md
+++ b/server/README.md
@@ -106,6 +106,14 @@ On startup the host loads every **`*_npc_behaviors.json`** under the npc-behavio
On success, **Information** logs include the resolved npc-behaviors directory path, distinct behavior count, and catalog file count. Game code should use **`INpcBehaviorDefinitionRegistry`** for lookups (`TryGetDefinition`, `TryNormalizeKnown`, `GetDefinitionsInIdOrder`; NEO-89). The catalog singleton remains for fail-fast startup only; do not inject **`NpcBehaviorDefinitionCatalog`** in new game code. Stable prototype behavior id constants: **`PrototypeNpcBehaviorRegistry`**.
+## NPC behavior definitions (NEO-90)
+
+**`GET /game/world/npc-behavior-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`npcBehaviors`**) backed by **`INpcBehaviorDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`archetypeKind`**, **`maxHp`**, **`aggroRadius`**, **`leashRadius`**, **`telegraphWindupSeconds`**, **`attackDamage`**, and **`attackCooldownSeconds`**. Plan: [NEO-90 implementation plan](../../docs/plans/NEO-90-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/npc-behavior-definitions/`.
+
+```bash
+curl -sS -i "http://localhost:5253/game/world/npc-behavior-definitions"
+```
+
## Ability definitions (NEO-78)
**`GET /game/world/ability-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`abilities`**) backed by **`IAbilityDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Each row includes **`id`**, **`displayName`**, **`baseDamage`**, **`cooldownSeconds`**, and **`abilityKind`** when present in content. Plan: [NEO-78 implementation plan](../../docs/plans/NEO-78-implementation-plan.md); Bruno: `bruno/neon-sprawl-server/ability-definitions/`.