NEO-87: ship prototype NpcBehaviorDef catalog, schema, and CI gates.
Add three frozen archetype rows under content/npc-behaviors with E5M2 allowlist validation, leash>aggro guard, and module doc freeze updates.pull/124/head
parent
846da90219
commit
6fd9694ff4
|
|
@ -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) |
|
| [`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) |
|
| [`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) |
|
| [`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) |
|
| [`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.
|
**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 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).
|
**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.
|
Server load path and hot-reload policy are TBD; keep files versioned here as the source of truth.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
**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.
|
**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
|
## Source anchors
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
| **Module ID** | E5.M2 |
|
| **Module ID** | E5.M2 |
|
||||||
| **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) |
|
| **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) |
|
||||||
| **Stage target** | Prototype |
|
| **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) |
|
| **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
|
## 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.
|
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` |
|
| `NpcBehaviorDef.id` | `displayName` | `archetypeKind` | `maxHp` | `aggroRadius` | `leashRadius` | `telegraphWindupSeconds` | `attackDamage` | `attackCooldownSeconds` |
|
||||||
|---------------------|---------------|-----------------|---------|--------------------------|----------------|
|
|---------------------|---------------|-----------------|---------|---------------|---------------|--------------------------|----------------|-------------------------|
|
||||||
| **`prototype_melee_pressure`** | Melee Pressure | `melee_pressure` | 100 | 1.5 | 15 |
|
| **`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 | 2.0 | 12 |
|
| **`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 | 2.5 | 25 |
|
| **`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`**).
|
**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
|
## Risks and telemetry
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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.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
|
### Epic 6 — PvP Security
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,11 @@ Working backlog for **Epic 5 — Slice 2** ([NPC archetypes and telegraphs](../d
|
||||||
|
|
||||||
**Acceptance criteria**
|
**Acceptance criteria**
|
||||||
|
|
||||||
- [ ] PR gate validates NPC behavior JSON against schema.
|
- [x] PR gate validates NPC behavior JSON against schema.
|
||||||
- [ ] Exactly three prototype behavior ids; duplicate `id` fails CI.
|
- [x] Exactly three prototype behavior ids; duplicate `id` fails CI.
|
||||||
- [ ] Stable id list documented in module doc freeze box.
|
- [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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,16 @@
|
||||||
|
|
||||||
## Acceptance criteria checklist
|
## Acceptance criteria checklist
|
||||||
|
|
||||||
- [ ] PR gate validates NPC behavior JSON against schema.
|
- [x] PR gate validates NPC behavior JSON against schema.
|
||||||
- [ ] Exactly three prototype behavior ids; duplicate `id` fails CI.
|
- [x] Exactly three prototype behavior ids; duplicate `id` fails CI.
|
||||||
- [ ] Stable id list documented in module doc freeze box.
|
- [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
|
## Technical approach
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Validates:
|
||||||
- resource yield catalogs: content/resource-nodes/*_resource_yields.json vs resource-yield-row.schema.json (NEO-57)
|
- 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)
|
- 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)
|
- 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
|
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_IO_ROW_SCHEMA = REPO_ROOT / "content/schemas/recipe-io-row.schema.json"
|
||||||
RECIPE_SCHEMA = REPO_ROOT / "content/schemas/recipe-def.schema.json"
|
RECIPE_SCHEMA = REPO_ROOT / "content/schemas/recipe-def.schema.json"
|
||||||
ABILITY_SCHEMA = REPO_ROOT / "content/schemas/ability-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"
|
SKILLS_DIR = REPO_ROOT / "content/skills"
|
||||||
MASTERY_DIR = REPO_ROOT / "content/mastery"
|
MASTERY_DIR = REPO_ROOT / "content/mastery"
|
||||||
ITEMS_DIR = REPO_ROOT / "content/items"
|
ITEMS_DIR = REPO_ROOT / "content/items"
|
||||||
RESOURCE_NODES_DIR = REPO_ROOT / "content/resource-nodes"
|
RESOURCE_NODES_DIR = REPO_ROOT / "content/resource-nodes"
|
||||||
RECIPES_DIR = REPO_ROOT / "content/recipes"
|
RECIPES_DIR = REPO_ROOT / "content/recipes"
|
||||||
ABILITIES_DIR = REPO_ROOT / "content/abilities"
|
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.
|
# Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes.
|
||||||
PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"})
|
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:
|
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."""
|
"""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
|
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:
|
def _prototype_slice4_gate(track_skill_ids: list[str]) -> str | None:
|
||||||
"""Return a human-readable error if Slice 4 contract fails, else None."""
|
"""Return a human-readable error if Slice 4 contract fails, else None."""
|
||||||
if len(track_skill_ids) != 1:
|
if len(track_skill_ids) != 1:
|
||||||
|
|
@ -792,6 +878,9 @@ def main() -> int:
|
||||||
if not ABILITY_SCHEMA.is_file():
|
if not ABILITY_SCHEMA.is_file():
|
||||||
print(f"error: missing schema {ABILITY_SCHEMA}", file=sys.stderr)
|
print(f"error: missing schema {ABILITY_SCHEMA}", file=sys.stderr)
|
||||||
return 1
|
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_schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8"))
|
||||||
skill_validator = Draft202012Validator(skill_schema)
|
skill_validator = Draft202012Validator(skill_schema)
|
||||||
|
|
@ -808,6 +897,8 @@ def main() -> int:
|
||||||
recipe_validator = _recipe_def_validator()
|
recipe_validator = _recipe_def_validator()
|
||||||
ability_schema = json.loads(ABILITY_SCHEMA.read_text(encoding="utf-8"))
|
ability_schema = json.loads(ABILITY_SCHEMA.read_text(encoding="utf-8"))
|
||||||
ability_validator = Draft202012Validator(ability_schema)
|
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():
|
if not SKILLS_DIR.is_dir():
|
||||||
print(f"error: missing directory {SKILLS_DIR}", file=sys.stderr)
|
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)
|
print(f"error: no *_abilities.json files under {ABILITIES_DIR}", file=sys.stderr)
|
||||||
return 1
|
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] = {}
|
seen_ids: dict[str, str] = {}
|
||||||
id_to_category: dict[str, str] = {}
|
id_to_category: dict[str, str] = {}
|
||||||
errors = 0
|
errors = 0
|
||||||
|
|
@ -1074,6 +1174,24 @@ def main() -> int:
|
||||||
print(e5m1_ability_err, file=sys.stderr)
|
print(e5m1_ability_err, file=sys.stderr)
|
||||||
return 1
|
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(
|
print(
|
||||||
"content OK: "
|
"content OK: "
|
||||||
f"{len(skill_files)} skill catalog file(s), "
|
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(yield_files)} resource yield catalog file(s), "
|
||||||
f"{len(recipe_files)} recipe catalog file(s), "
|
f"{len(recipe_files)} recipe catalog file(s), "
|
||||||
f"{len(ability_files)} ability 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(seen_ids)} unique skill id(s), "
|
||||||
f"{len(item_seen_ids)} unique item id(s), "
|
f"{len(item_seen_ids)} unique item id(s), "
|
||||||
f"{len(node_seen_ids)} unique nodeDefId(s), "
|
f"{len(node_seen_ids)} unique nodeDefId(s), "
|
||||||
f"{len(recipe_seen_ids)} unique recipe id(s), "
|
f"{len(recipe_seen_ids)} unique recipe id(s), "
|
||||||
f"{len(ability_seen_ids)} unique ability 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)"
|
f"{len(track_skill_ids)} mastery track(s)"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue