diff --git a/content/README.md b/content/README.md index f7b10ae..843f4d8 100644 --- a/content/README.md +++ b/content/README.md @@ -10,6 +10,7 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** | [`items/`](items/) | Item catalogs; each row matches [`schemas/item-def.schema.json`](schemas/item-def.schema.json) — **stable `id`**, **`stackMax`**, **`inventorySlotKind`** for [E3.M3](../docs/decomposition/modules/E3_M3_ItemizationAndInventorySchema.md) | | [`recipes/`](recipes/) | Recipe catalogs; each row matches [`schemas/recipe-def.schema.json`](schemas/recipe-def.schema.json) — **stable `id`**, **`recipeKind`**, **`requiredSkillId`**, inputs/outputs for [E3.M2](../docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md) | | [`abilities/`](abilities/) | Combat ability catalogs; each row matches [`schemas/ability-def.schema.json`](schemas/ability-def.schema.json) — **stable `id`**, **`baseDamage`**, **`cooldownSeconds`** for [E5.M1](../docs/decomposition/modules/E5_M1_CombatRulesEngine.md) | +| [`npc-behaviors/`](npc-behaviors/) | NPC behavior archetype catalogs; each row matches [`schemas/npc-behavior-def.schema.json`](schemas/npc-behavior-def.schema.json) — **stable `id`**, **`archetypeKind`**, aggro/leash radii, telegraph + attack timings for [E5.M2](../docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) | | [`resource-nodes/`](resource-nodes/) | Resource node + yield catalogs; node rows match [`schemas/resource-node-def.schema.json`](schemas/resource-node-def.schema.json), yield rows match [`schemas/resource-yield-row.schema.json`](schemas/resource-yield-row.schema.json) — **stable `nodeDefId`**, **`gatherLens`**, **`maxGathers`** for [E3.M1](../docs/decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) | **Prototype Slice 1 (NEO-33):** CI expects **exactly three** skill rows with ids **`salvage`**, **`refine`**, **`intrusion`** (gather + process + tech). Each row must declare **`allowedXpSourceKinds`** (non-empty); see [E2.M1 — Designer note: `allowedXpSourceKinds`](../docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md#designer-note-allowedxpsourcekinds-and-grant-call-sites) for what each kind means for grant wiring. @@ -22,6 +23,8 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** **Prototype E5 Slice 1 — abilities (NEO-76):** CI expects **exactly four** ability ids aligned to [E5.M1 ability freeze](../docs/decomposition/modules/E5_M1_CombatRulesEngine.md#prototype-slice-1-freeze-e5m1-01) — **`prototype_pulse`**, **`prototype_guard`**, **`prototype_dash`**, **`prototype_burst`**. **Do not rename** ability `id` after ship—change **`displayName`** only. See [E5M1-prototype-backlog.md](../docs/plans/E5M1-prototype-backlog.md) and [NEO-76 plan](../docs/plans/NEO-76-implementation-plan.md). +**Prototype E5 Slice 2 — NPC behaviors (NEO-87):** CI expects **exactly three** npc behavior ids aligned to [E5.M2 behavior freeze](../docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md#prototype-slice-2-freeze-e5m2-01) — **`prototype_melee_pressure`**, **`prototype_ranged_control`**, **`prototype_elite_mini_boss`**. Each row requires **`leashRadius` > `aggroRadius`**. **Do not rename** behavior `id` after ship—change **`displayName`** only. See [E5M2-prototype-backlog.md](../docs/plans/E5M2-prototype-backlog.md) and [NEO-87 plan](../docs/plans/NEO-87-implementation-plan.md). + **Prototype Slice 4 (NEO-45):** CI expects **exactly one** `MasteryTrack` for **`salvage`** only (`refine` / `intrusion` have no mastery rows yet). Tier 1 @ skill level **2** is a **mutually exclusive** branch pick (`scrap_efficiency` vs `bulk_haul`) with **no** perks; tier 2 @ level **4** unlocks one perk per branch path. **Do not rename** `branchId` / `perkId` after ship—change `displayName` only. See [E2.M3 — Designer note](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#designer-note-tiers-branches-and-level-gates) and [freeze table](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.md#prototype-slice-4-freeze--salvage-flagship-neo-45). Server load path and hot-reload policy are TBD; keep files versioned here as the source of truth. diff --git a/content/npc-behaviors/prototype_npc_behaviors.json b/content/npc-behaviors/prototype_npc_behaviors.json new file mode 100644 index 0000000..aa2b1df --- /dev/null +++ b/content/npc-behaviors/prototype_npc_behaviors.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "npcBehaviors": [ + { + "id": "prototype_melee_pressure", + "displayName": "Melee Pressure", + "archetypeKind": "melee_pressure", + "maxHp": 100, + "aggroRadius": 8.0, + "leashRadius": 16.0, + "telegraphWindupSeconds": 1.5, + "attackDamage": 15, + "attackCooldownSeconds": 3.0 + }, + { + "id": "prototype_ranged_control", + "displayName": "Ranged Control", + "archetypeKind": "ranged_control", + "maxHp": 80, + "aggroRadius": 10.0, + "leashRadius": 20.0, + "telegraphWindupSeconds": 2.0, + "attackDamage": 12, + "attackCooldownSeconds": 4.0 + }, + { + "id": "prototype_elite_mini_boss", + "displayName": "Elite Mini-Boss", + "archetypeKind": "elite_mini_boss", + "maxHp": 200, + "aggroRadius": 8.0, + "leashRadius": 18.0, + "telegraphWindupSeconds": 2.5, + "attackDamage": 25, + "attackCooldownSeconds": 5.0 + } + ] +} diff --git a/content/schemas/npc-behavior-def.schema.json b/content/schemas/npc-behavior-def.schema.json new file mode 100644 index 0000000..f03d55a --- /dev/null +++ b/content/schemas/npc-behavior-def.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/npc-behavior-def.json", + "title": "NpcBehaviorDef", + "description": "Single NPC behavior archetype row for catalogs (e.g. content/npc-behaviors/*_npc_behaviors.json). IDs are stable forever—rename display in displayName only. See docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "displayName", + "archetypeKind", + "maxHp", + "aggroRadius", + "leashRadius", + "telegraphWindupSeconds", + "attackDamage", + "attackCooldownSeconds" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Immutable behavior key for NPC instance binding, aggro, telegraph scheduling, and combat resolution. Never change after content ships; add a new id if the archetype splits." + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Player-facing label; safe to change without migrating NPC data." + }, + "archetypeKind": { + "type": "string", + "description": "Prototype archetype grouping for HUD, telemetry, and state-machine routing.", + "enum": ["melee_pressure", "ranged_control", "elite_mini_boss"] + }, + "maxHp": { + "type": "integer", + "minimum": 1, + "description": "Maximum HP for NPC instances bound to this behavior def." + }, + "aggroRadius": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Horizontal proximity radius for re-aggro after leash clear (XZ-only at runtime)." + }, + "leashRadius": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Horizontal radius beyond which aggro holder clears when the player leaves (must exceed aggroRadius)." + }, + "telegraphWindupSeconds": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Windup duration before NPC attack resolves after telegraph starts." + }, + "attackDamage": { + "type": "integer", + "minimum": 1, + "description": "Deterministic damage dealt to players after telegraph resolves in prototype Slice 2." + }, + "attackCooldownSeconds": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Seconds after last attack before the NPC may enter telegraph again." + } + } +} diff --git a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md index 2c72f11..bb76187 100644 --- a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md +++ b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md @@ -53,6 +53,8 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard **Ability catalogs ([NEO-76](https://linear.app/neon-sprawl/issue/NEO-76)):** the same script validates each row in `content/abilities/*_abilities.json` against [`content/schemas/ability-def.schema.json`](../../../content/schemas/ability-def.schema.json), rejects **duplicate `id`** across files, and (E5 Slice 1) enforces the **frozen four-ability** id set (`prototype_pulse`, `prototype_guard`, `prototype_dash`, `prototype_burst`). Extend the script when additional catalogs and schemas ship. +**NPC behavior catalogs ([NEO-87](https://linear.app/neon-sprawl/issue/NEO-87)):** the same script validates each row in `content/npc-behaviors/*_npc_behaviors.json` against [`content/schemas/npc-behavior-def.schema.json`](../../../content/schemas/npc-behavior-def.schema.json), rejects **duplicate `id`** across files, requires **`leashRadius` > `aggroRadius`** per row, and (E5 Slice 2) enforces the **frozen three-behavior** id set (`prototype_melee_pressure`, `prototype_ranged_control`, `prototype_elite_mini_boss`). + **Decomposition vocabulary:** the same workflow runs [`scripts/check_decomposition_language.py`](../../../scripts/check_decomposition_language.py) (stdlib only) over `docs/decomposition/**/*.md` to catch wording that treats **combat** as a leveled **`SkillDef`** line instead of **gig** progression (see script docstring for patterns). Adjust the script if a future doc needs a documented exception. ## Source anchors diff --git a/docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md b/docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md index b7a31bf..f7e165d 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) → **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) (decomposed; implementation not started) | +| **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-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-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) | ## Purpose @@ -67,15 +67,15 @@ Full story tables, kickoff defaults, and **`blockedBy` graph:** [E5M2-prototype- The **first shipped three-archetype NPC spine** under `content/npc-behaviors/*.json` is **frozen** for behavior binding, aggro tuning, and telegraph timings until a deliberate migration issue expands the roster. -| `NpcBehaviorDef.id` | `displayName` | `archetypeKind` | `maxHp` | `telegraphWindupSeconds` | `attackDamage` | -|---------------------|---------------|-----------------|---------|--------------------------|----------------| -| **`prototype_melee_pressure`** | Melee Pressure | `melee_pressure` | 100 | 1.5 | 15 | -| **`prototype_ranged_control`** | Ranged Control | `ranged_control` | 80 | 2.0 | 12 | -| **`prototype_elite_mini_boss`** | Elite Mini-Boss | `elite_mini_boss` | 200 | 2.5 | 25 | +| `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 | **NPC instance ids (E5M2-05):** **`prototype_npc_melee`**, **`prototype_npc_ranged`**, **`prototype_npc_elite`** — each binds one behavior def + world anchor (replaces **`prototype_target_alpha` / `beta`**). -**Rules:** Do **not** rename frozen behavior **`id`** values without a migration issue. CI enforces **exactly** these three behavior ids. Tune combat feel via numeric fields or new ids in a follow-up issue. +**Rules:** Do **not** rename frozen behavior **`id`** values without a migration issue. CI ([`scripts/validate_content.py`](../../../scripts/validate_content.py)) enforces **exactly** these three behavior ids, duplicate-`id` rejection, and **`leashRadius` > `aggroRadius`** per row. Relaxing or changing that gate belongs in a new Linear issue once the catalog intentionally grows. Plan: [NEO-87 implementation plan](../../plans/NEO-87-implementation-plan.md). ## Risks and telemetry diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 87ac9e0..5d722b3 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 | **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**. No implementation landed yet. | [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2](E5_M2_NpcAiAndBehaviorProfiles.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)). **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), label **`E5.M2`** on NEO-87–NEO-98 | --- diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 4aedf87..85bc6ca 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -78,7 +78,7 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen **E5.M1 note:** Epic 5 **Slice 1** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) → [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); label **`E5.M1`**. Gig XP on defeat: **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1_CombatRulesEngine.md](E5_M1_CombatRulesEngine.md). Builds on E1.M4 cast funnel (NEO-28/31/32); extends **`POST …/ability-cast`** with **`CombatResolution`**. **NEO-76 landed:** frozen four-ability catalog + CI ([NEO-76 plan](../../plans/NEO-76-implementation-plan.md)). **NEO-77 landed:** fail-fast server load of `content/abilities/*_abilities.json` ([NEO-77 plan](../../plans/NEO-77-implementation-plan.md)). **NEO-79 landed:** injectable **`IAbilityDefinitionRegistry`** + DI; hotbar/cast use **`TryNormalizeKnown`** ([NEO-79 plan](../../plans/NEO-79-implementation-plan.md)). **NEO-78 landed:** **`GET /game/world/ability-definitions`** — `AbilityDefinitionsWorldApi` + DTOs in `Game/Combat/` ([NEO-78 plan](../../plans/NEO-78-implementation-plan.md)); 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 plan](../../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 vocabulary (`target_defeated`, `unknown_ability`, `unknown_target`) ([NEO-81 plan](../../plans/NEO-81-implementation-plan.md)); [server README — Combat engine (NEO-81)](../../../server/README.md#combat-engine-neo-81). **NEO-82 landed:** **`AbilityCastApi`** invokes **`CombatOperations.TryResolve`** after E1.M4 gates; nested wire **`combatResolution`** on accept; per-ability catalog **`cooldownSeconds`** on successful resolve; **`target_defeated`** JSON cast deny without cooldown ([NEO-82 plan](../../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`** for client HUD ([NEO-83 plan](../../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 plan](../../plans/NEO-44-implementation-plan.md)); [server README — Gig progression (NEO-44)](../../../server/README.md#gig-progression-snapshot-neo-44). **NEO-84 landed:** comment-only **`ability_used`** / **`enemy_defeat`** telemetry hook sites in **`CombatOperations.TryResolve`**; reserved **`encounter_start`** / **`player_death`** ([NEO-84 plan](../../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 — **`combat_targets_client.gd`**, **`CombatTargetHpLabel`**, **`CastFeedbackLabel`** resolution copy; event-driven **`GET /game/world/combat-targets`** refresh ([NEO-85 plan](../../plans/NEO-85-implementation-plan.md), [`NEO-85` manual QA](../../manual-qa/NEO-85.md)); [client README — Combat feedback + target HP HUD (NEO-85)](../../../client/README.md#combat-feedback--target-hp-hud-neo-85). **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) ([NEO-86 plan](../../plans/NEO-86-implementation-plan.md)); [client README — End-to-end combat loop (NEO-86)](../../../client/README.md#end-to-end-combat-loop-neo-86). **Epic 5 Slice 1 complete — register row Ready.** -**E5.M2 note:** Epic 5 **Slice 2** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); label **`E5.M2`**. Client capstone **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98). See [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2_NpcAiAndBehaviorProfiles.md](E5_M2_NpcAiAndBehaviorProfiles.md). Upstream **E5.M1 Ready**. Replaces **`prototype_target_alpha` / `beta`** with three NPC archetype instances; owns **`ThreatState`**, **`TelegraphEvent`**, session **`IPlayerCombatHealthStore`**. **Backlog decomposed** — implementation not started. +**E5.M2 note:** Epic 5 **Slice 2** backlog in Linear ([Epic 5 — PvE Combat and Encounter Content](https://linear.app/neon-sprawl/project/epic-5-pve-combat-and-encounter-content-cf3c94c3-5346-40e0-8aaf-281e846f2846)): [NEO-87](https://linear.app/neon-sprawl/issue/NEO-87) → [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98); label **`E5.M2`**. Client capstone **E5M2-12** [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98). See [E5M2-prototype-backlog.md](../../plans/E5M2-prototype-backlog.md), [E5_M2_NpcAiAndBehaviorProfiles.md](E5_M2_NpcAiAndBehaviorProfiles.md). Upstream **E5.M1 Ready**. **NEO-87 landed:** frozen three-behavior catalog + CI ([NEO-87 plan](../../plans/NEO-87-implementation-plan.md)). Replaces **`prototype_target_alpha` / `beta`** with three NPC archetype instances; owns **`ThreatState`**, **`TelegraphEvent`**, session **`IPlayerCombatHealthStore`**. ### Epic 6 — PvP Security diff --git a/docs/plans/E5M2-prototype-backlog.md b/docs/plans/E5M2-prototype-backlog.md index aaaa8a6..4eb64f7 100644 --- a/docs/plans/E5M2-prototype-backlog.md +++ b/docs/plans/E5M2-prototype-backlog.md @@ -94,9 +94,11 @@ Working backlog for **Epic 5 — Slice 2** ([NPC archetypes and telegraphs](../d **Acceptance criteria** -- [ ] PR gate validates NPC behavior JSON against schema. -- [ ] Exactly three prototype behavior ids; duplicate `id` fails CI. -- [ ] Stable id list documented in module doc freeze box. +- [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. + +**Landed ([NEO-87](https://linear.app/neon-sprawl/issue/NEO-87)):** [`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), CI gates in [`validate_content.py`](../../scripts/validate_content.py); plan [NEO-87-implementation-plan.md](NEO-87-implementation-plan.md). --- diff --git a/docs/plans/NEO-87-implementation-plan.md b/docs/plans/NEO-87-implementation-plan.md new file mode 100644 index 0000000..e505eef --- /dev/null +++ b/docs/plans/NEO-87-implementation-plan.md @@ -0,0 +1,128 @@ +# 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. diff --git a/docs/reviews/2026-05-25-NEO-87.md b/docs/reviews/2026-05-25-NEO-87.md new file mode 100644 index 0000000..12c4add --- /dev/null +++ b/docs/reviews/2026-05-25-NEO-87.md @@ -0,0 +1,58 @@ +# Code review — NEO-87 prototype NpcBehaviorDef catalog + CI + +**Date:** 2026-05-25 +**Scope:** Branch `NEO-87-e5m2-npc-behavior-catalog-schemas-ci` · commits `846da90`–`6fd9694` vs `origin/main` +**Base:** `origin/main` +**Follow-up:** Restored missing `prototype_elite_mini_boss` row; `validate_content.py` passes with 3 npc behavior ids. + +## Verdict + +**Approve with nits** + +## Summary + +NEO-87 lands E5M2-01 content-only work: `npc-behavior-def.schema.json`, NPC behavior validation in `scripts/validate_content.py` (mirroring NEO-76 ability pattern), and thorough doc updates (E5.M2 freeze table, `content/README.md`, CT.M1, alignment register, backlog checkboxes). Schema shape, CI gate constants, and Python validation helpers align with the kickoff plan. **Follow-up:** the initial commit shipped a two-row catalog (elite row dropped before commit); restored per freeze table — CI now passes with all three ids. + +## Documentation checked + +| Document | Result | +|----------|--------| +| [`docs/plans/NEO-87-implementation-plan.md`](../plans/NEO-87-implementation-plan.md) | **Matches** — reconciliation and AC align with three-row catalog after follow-up fix. | +| [`docs/plans/E5M2-prototype-backlog.md`](../plans/E5M2-prototype-backlog.md) · **E5M2-01** | **Matches** — AC and “Landed” note accurate once catalog is complete. | +| [`docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md`](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) | **Matches** — freeze table, catalog file, and CI rules line aligned. | +| [`docs/decomposition/modules/CT_M1_ContentValidationPipeline.md`](../decomposition/modules/CT_M1_ContentValidationPipeline.md) | **Matches** — NPC behavior PR gate paragraph added. | +| [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) · **E5.M2** | **Matches** — **NEO-87 landed** accurate after catalog fix. | +| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Matches** — E5.M2 note cites **NEO-87 landed** with passing CI. | +| [`content/README.md`](../../content/README.md) | **Matches** — `npc-behaviors/` row + E5 Slice 2 freeze paragraph (ids and rules). | +| [`docs/decomposition/modules/client_server_authority.md`](../decomposition/modules/client_server_authority.md) | **N/A** — no runtime authority changes; content-only. | +| Full-stack epic decomposition | **Matches** — E5M2-01 is **server/content + CI** only; kickoff explicitly defers Godot to E5M2-04+ / NEO-97 / NEO-98. No `docs/manual-qa/NEO-87.md` required. | + +## Blocking issues + +1. ~~**Missing third catalog row — CI fails.** [`content/npc-behaviors/prototype_npc_behaviors.json`](../../content/npc-behaviors/prototype_npc_behaviors.json) contains only `prototype_melee_pressure` and `prototype_ranged_control`. The E5M2 three-id gate requires `prototype_elite_mini_boss` as well. Running validation:~~ **Done.** Restored `prototype_elite_mini_boss` row per kickoff freeze table; `validate_content.py` exits 0 with `3 unique npc behavior id(s)` (`24cbd9b`). + +## Suggestions + +1. ~~**Defer “landed” doc claims until CI is green** — After fixing the catalog, re-run validation and confirm plan reconciliation, E5M2-01 backlog “Landed” note, alignment register, and module register accurately reflect a passing gate (same pattern as [NEO-76 review follow-up](../reviews/2026-05-24-NEO-76.md)).~~ **Done.** Validation green; existing doc claims now accurate — no further doc edits required. + +2. **Optional: one-row-per-`archetypeKind` gate** — The three-id frozenset already implies one row per archetype for E5M2-01. If the roster grows in a follow-up issue, consider an explicit archetype coverage check; not required for this slice. + +## Nits + +- ~~Nit: Commit subject “ship prototype … catalog” reads complete while the catalog file is incomplete — minor messaging; resolved when elite row lands.~~ **Done.** Elite row restored in follow-up commit. + +## Verification + +```bash +# From repo root +python3 -m venv .venv-content && .venv-content/bin/pip install -r scripts/requirements-content.txt +.venv-content/bin/python scripts/validate_content.py +# Expect: content OK … 1 npc behavior catalog file(s) … 3 unique npc behavior id(s) + +# Negative smokes (optional, after fix) +# — duplicate id across rows → non-zero exit +# — remove one frozen id → gate error +# — leashRadius <= aggroRadius → numeric gate error +``` + +PR gate (`.github/workflows/pr-gate.yml`) runs the same script; no additional workflow changes needed for this story. diff --git a/scripts/validate_content.py b/scripts/validate_content.py index 5ff1161..b56254b 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -11,6 +11,7 @@ Validates: - resource yield catalogs: content/resource-nodes/*_resource_yields.json vs resource-yield-row.schema.json (NEO-57) - recipe catalogs: content/recipes/*_recipes.json rows vs content/schemas/recipe-def.schema.json (NEO-65) - ability catalogs: content/abilities/*_abilities.json rows vs content/schemas/ability-def.schema.json (NEO-76) +- npc behavior catalogs: content/npc-behaviors/*_npc_behaviors.json rows vs content/schemas/npc-behavior-def.schema.json (NEO-87) """ from __future__ import annotations @@ -32,12 +33,14 @@ RESOURCE_YIELD_ROW_SCHEMA = REPO_ROOT / "content/schemas/resource-yield-row.sche RECIPE_IO_ROW_SCHEMA = REPO_ROOT / "content/schemas/recipe-io-row.schema.json" RECIPE_SCHEMA = REPO_ROOT / "content/schemas/recipe-def.schema.json" ABILITY_SCHEMA = REPO_ROOT / "content/schemas/ability-def.schema.json" +NPC_BEHAVIOR_SCHEMA = REPO_ROOT / "content/schemas/npc-behavior-def.schema.json" SKILLS_DIR = REPO_ROOT / "content/skills" MASTERY_DIR = REPO_ROOT / "content/mastery" ITEMS_DIR = REPO_ROOT / "content/items" RESOURCE_NODES_DIR = REPO_ROOT / "content/resource-nodes" RECIPES_DIR = REPO_ROOT / "content/recipes" ABILITIES_DIR = REPO_ROOT / "content/abilities" +NPC_BEHAVIORS_DIR = REPO_ROOT / "content/npc-behaviors" # Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes. PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"}) @@ -102,6 +105,16 @@ PROTOTYPE_E5M1_ABILITY_IDS = frozenset( } ) +# Epic 5 Slice 2 prototype lock (NEO-87): exact npc behavior ids after schema passes. +# Keep in sync with E5.M2 freeze table and future NEO-88 server loader. +PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS = frozenset( + { + "prototype_melee_pressure", + "prototype_ranged_control", + "prototype_elite_mini_boss", + } +) + def _prototype_slice1_gate(seen_ids: dict[str, str], id_to_category: dict[str, str]) -> str | None: """Return a human-readable error if Slice 1 contract fails, else None.""" @@ -469,6 +482,79 @@ def _prototype_e5m1_ability_gate(seen_ids: dict[str, str]) -> str | None: return None +def _validate_npc_behavior_catalogs( + *, + npc_behavior_files: list[Path], + npc_behavior_validator: Draft202012Validator, +) -> tuple[int, dict[str, str], dict[str, dict]]: + """Validate NPC behavior JSON files. Returns (error_count, seen_ids, id_to_row).""" + errors = 0 + seen_ids: dict[str, str] = {} + id_to_row: dict[str, dict] = {} + + for path in npc_behavior_files: + rel = str(path.relative_to(REPO_ROOT)) + data = json.loads(path.read_text(encoding="utf-8")) + schema_version = data.get("schemaVersion") + if schema_version != 1: + print(f"error: {rel}: expected schemaVersion 1, got {schema_version!r}", file=sys.stderr) + errors += 1 + continue + + npc_behaviors = data.get("npcBehaviors") + if not isinstance(npc_behaviors, list): + print(f"error: {rel}: expected top-level 'npcBehaviors' array", file=sys.stderr) + errors += 1 + continue + + for i, row in enumerate(npc_behaviors): + if not isinstance(row, dict): + print(f"error: {rel}: npcBehaviors[{i}] must be an object", file=sys.stderr) + errors += 1 + continue + row_schema_errors = 0 + for err in sorted(npc_behavior_validator.iter_errors(row), key=lambda e: e.path): + loc = ".".join(str(p) for p in err.path) or "(root)" + print(f"error: {rel} npcBehaviors[{i}] {loc}: {err.message}", file=sys.stderr) + row_schema_errors += 1 + errors += 1 + bid = row.get("id") + if isinstance(bid, str) and row_schema_errors == 0: + prev = seen_ids.get(bid) + if prev: + print(f"error: duplicate npc behavior id {bid!r} in {prev} and {rel}", file=sys.stderr) + errors += 1 + else: + seen_ids[bid] = rel + id_to_row[bid] = row + + return errors, seen_ids, id_to_row + + +def _prototype_e5m2_npc_behavior_gate(seen_ids: dict[str, str]) -> str | None: + """Return a human-readable error if E5M2 npc behavior contract fails, else None.""" + ids = frozenset(seen_ids.keys()) + if ids != PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS: + return ( + "error: prototype E5M2 expects exactly npc behavior ids " + f"{sorted(PROTOTYPE_E5M2_NPC_BEHAVIOR_IDS)!r}, got {sorted(ids)!r}" + ) + return None + + +def _prototype_e5m2_npc_behavior_numeric_gate(id_to_row: dict[str, dict]) -> str | None: + """Return a human-readable error if leash/aggro invariants fail, else None.""" + for bid, row in id_to_row.items(): + aggro = row.get("aggroRadius") + leash = row.get("leashRadius") + if isinstance(aggro, (int, float)) and isinstance(leash, (int, float)): + if not leash > aggro: + return ( + f"error: npc behavior {bid!r}: leashRadius {leash} must be > aggroRadius {aggro}" + ) + return None + + def _prototype_slice4_gate(track_skill_ids: list[str]) -> str | None: """Return a human-readable error if Slice 4 contract fails, else None.""" if len(track_skill_ids) != 1: @@ -792,6 +878,9 @@ def main() -> int: if not ABILITY_SCHEMA.is_file(): print(f"error: missing schema {ABILITY_SCHEMA}", file=sys.stderr) return 1 + if not NPC_BEHAVIOR_SCHEMA.is_file(): + print(f"error: missing schema {NPC_BEHAVIOR_SCHEMA}", file=sys.stderr) + return 1 skill_schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8")) skill_validator = Draft202012Validator(skill_schema) @@ -808,6 +897,8 @@ def main() -> int: recipe_validator = _recipe_def_validator() ability_schema = json.loads(ABILITY_SCHEMA.read_text(encoding="utf-8")) ability_validator = Draft202012Validator(ability_schema) + npc_behavior_schema = json.loads(NPC_BEHAVIOR_SCHEMA.read_text(encoding="utf-8")) + npc_behavior_validator = Draft202012Validator(npc_behavior_schema) if not SKILLS_DIR.is_dir(): print(f"error: missing directory {SKILLS_DIR}", file=sys.stderr) @@ -873,6 +964,15 @@ def main() -> int: print(f"error: no *_abilities.json files under {ABILITIES_DIR}", file=sys.stderr) return 1 + if not NPC_BEHAVIORS_DIR.is_dir(): + print(f"error: missing directory {NPC_BEHAVIORS_DIR}", file=sys.stderr) + return 1 + + npc_behavior_files = sorted(NPC_BEHAVIORS_DIR.glob("*_npc_behaviors.json")) + if not npc_behavior_files: + print(f"error: no *_npc_behaviors.json files under {NPC_BEHAVIORS_DIR}", file=sys.stderr) + return 1 + seen_ids: dict[str, str] = {} id_to_category: dict[str, str] = {} errors = 0 @@ -1074,6 +1174,24 @@ def main() -> int: print(e5m1_ability_err, file=sys.stderr) return 1 + npc_behavior_errors, npc_behavior_seen_ids, npc_behavior_id_to_row = _validate_npc_behavior_catalogs( + npc_behavior_files=npc_behavior_files, + npc_behavior_validator=npc_behavior_validator, + ) + if npc_behavior_errors: + print(f"content validation failed with {npc_behavior_errors} error(s)", file=sys.stderr) + return 1 + + e5m2_npc_behavior_err = _prototype_e5m2_npc_behavior_gate(npc_behavior_seen_ids) + if e5m2_npc_behavior_err: + print(e5m2_npc_behavior_err, file=sys.stderr) + return 1 + + e5m2_npc_behavior_numeric_err = _prototype_e5m2_npc_behavior_numeric_gate(npc_behavior_id_to_row) + if e5m2_npc_behavior_numeric_err: + print(e5m2_npc_behavior_numeric_err, file=sys.stderr) + return 1 + print( "content OK: " f"{len(skill_files)} skill catalog file(s), " @@ -1084,11 +1202,13 @@ def main() -> int: f"{len(yield_files)} resource yield catalog file(s), " f"{len(recipe_files)} recipe catalog file(s), " f"{len(ability_files)} ability catalog file(s), " + f"{len(npc_behavior_files)} npc behavior catalog file(s), " f"{len(seen_ids)} unique skill id(s), " f"{len(item_seen_ids)} unique item id(s), " f"{len(node_seen_ids)} unique nodeDefId(s), " f"{len(recipe_seen_ids)} unique recipe id(s), " f"{len(ability_seen_ids)} unique ability id(s), " + f"{len(npc_behavior_seen_ids)} unique npc behavior id(s), " f"{len(track_skill_ids)} mastery track(s)" ) return 0