From 510d71113b661cc592b8bc598d07b85820a31dcf Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 21:02:07 -0400 Subject: [PATCH 1/5] =?UTF-8?q?NEO-76:=20decompose=20E5.M1=20prototype=20b?= =?UTF-8?q?acklog=20(NEO-76=E2=80=93NEO-86,=20NEO-44).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full-stack Epic 5 Slice 1 story backlog with Linear issues, blockedBy graph, and module/epic register cross-links. --- .../decomposition/epics/epic_05_pve_combat.md | 2 + .../modules/E5_M1_CombatRulesEngine.md | 20 + ...umentation_and_implementation_alignment.md | 1 + .../modules/module_dependency_register.md | 2 + docs/plans/E5M1-prototype-backlog.md | 381 ++++++++++++++++++ 5 files changed, 406 insertions(+) create mode 100644 docs/plans/E5M1-prototype-backlog.md diff --git a/docs/decomposition/epics/epic_05_pve_combat.md b/docs/decomposition/epics/epic_05_pve_combat.md index 26962a8..1cb4300 100644 --- a/docs/decomposition/epics/epic_05_pve_combat.md +++ b/docs/decomposition/epics/epic_05_pve_combat.md @@ -54,6 +54,8 @@ Deliver tab-target combat resolution, NPC behaviors and telegraphs, encounter te - Combat readable in fixed isometric camera; no silent failures on ability deny. - Telemetry hooks: `encounter_start`, `ability_used`, `player_death`, `enemy_defeat`. +**Linear backlog:** [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md) — [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) → [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); gig XP **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). + ### Slice 2 - NPC archetypes and telegraphs - Scope: E5.M2 for three archetypes: melee pressure, ranged control, elite mini-boss. diff --git a/docs/decomposition/modules/E5_M1_CombatRulesEngine.md b/docs/decomposition/modules/E5_M1_CombatRulesEngine.md index f24c03f..00ffbda 100644 --- a/docs/decomposition/modules/E5_M1_CombatRulesEngine.md +++ b/docs/decomposition/modules/E5_M1_CombatRulesEngine.md @@ -8,6 +8,7 @@ | **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) | | **Stage target** | Prototype | | **Status** | Planned (see [dependency register](module_dependency_register.md)) | +| **Linear** | Label **`E5.M1`** · [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md) **E5M1-01** [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) → **E5M1-12** [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); gig XP **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44) | ## Purpose @@ -49,6 +50,25 @@ Core tab-target combat resolution: valid actions against a locked target, hit re See Epic 5 **Slice 1 — Combat rules MVP**: target lock, basic attacks, 4–6 abilities, cooldowns/resources; telemetry `encounter_start`, `ability_used`, `player_death`, `enemy_defeat`. +## Linear backlog (decomposed) + +Full story tables, kickoff defaults, and **`blockedBy` graph:** [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md). + +| Slug | Linear | +|------|--------| +| E5M1-01 | [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) | +| E5M1-02 | [NEO-77](https://linear.app/neon-sprawl/issue/NEO-77) | +| E5M1-03 | [NEO-79](https://linear.app/neon-sprawl/issue/NEO-79) | +| E5M1-04 | [NEO-78](https://linear.app/neon-sprawl/issue/NEO-78) | +| E5M1-05 | [NEO-80](https://linear.app/neon-sprawl/issue/NEO-80) | +| E5M1-06 | [NEO-81](https://linear.app/neon-sprawl/issue/NEO-81) | +| E5M1-07 | [NEO-82](https://linear.app/neon-sprawl/issue/NEO-82) | +| E5M1-08 | [NEO-83](https://linear.app/neon-sprawl/issue/NEO-83) | +| E5M1-09 | [NEO-84](https://linear.app/neon-sprawl/issue/NEO-84) | +| E5M1-10 | [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44) | +| E5M1-11 | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) | +| E5M1-12 | [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86) | + ## Prototype notes (NEO-28) Epic 1 **Slice 3** exercises a minimal cast accept/deny surface on **`POST /game/players/{id}/ability-cast`** before the full engine exists: after hotbar/loadout validation ([E1.M4](E1_M4_AbilityInputScaffold.md)), the server requires **`targetId`** to match the persisted combat lock, resolve in the prototype target registry, and sit within horizontal lock radius against authoritative **`PositionState`**. Deny codes **`invalid_target`** and **`out_of_range`** reuse the same strings as E1.M3 **`TargetState.validity`** for vocabulary alignment. **`accepted: true`** still does not imply damage, cooldown commit, or a full **`CombatResolution`** payload — those remain E5.M1 scope. diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 671bf05..3f30e5a 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -56,6 +56,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **NEO-73 landed:** client gather feedback — **`skill_progression_client.gd`**, **`prototype_interactable_picker.gd`** (nearest in-range **R**), **`GatherFeedbackLabel`** + **`SkillProgressionLabel`**, auto inventory/skill refresh after successful gather ([NEO-73](../../plans/NEO-73-implementation-plan.md), [`NEO-73` manual QA](../../manual-qa/NEO-73.md)); `client/README.md` gather section. **NEO-75 landed:** capstone gather→refine→make loop in Godot — [`NEO-75` manual QA](../../manual-qa/NEO-75.md), `client/README.md` end-to-end section; Epic 3 Slice 5 client complete. | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [NEO-73](../../plans/NEO-73-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/`, `client/scripts/skill_progression_client.gd`, `client/scripts/prototype_interactable_picker.gd` | | 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 | Planned | **Backlog decomposed:** Epic 5 Slice 1 — [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md) **E5M1-01** [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) through **E5M1-12** [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); gig XP **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32) — **`POST …/ability-cast`** accepts without **`CombatResolution`** yet. No implementation started. | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) | --- diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index 3c80324..abdefd5 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -72,6 +72,8 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status | |---|---|---|---|---|---| | E5.M1 | CombatRulesEngine | E1.M3, E2.M2 | CombatAction, CombatResolution, ThreatState | Prototype | Planned | + +**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`**. | E5.M2 | NpcAiAndBehaviorProfiles | E5.M1 | NpcBehaviorDef, TelegraphEvent, AggroRule | Prototype | Planned | | E5.M3 | EncounterAndRewardTables | E5.M1, E3.M3, E7.M2 | EncounterDef, RewardTable, EncounterCompleteEvent | Prototype | Planned | | E5.M4 | GroupCombatScaling | E5.M3, E8.M1 | ScalingProfile, PartySizeModifier, CombatDifficultyBand | Pre-production | Planned | diff --git a/docs/plans/E5M1-prototype-backlog.md b/docs/plans/E5M1-prototype-backlog.md new file mode 100644 index 0000000..2f5962f --- /dev/null +++ b/docs/plans/E5M1-prototype-backlog.md @@ -0,0 +1,381 @@ +# E5.M1 — Prototype story backlog (CombatRulesEngine) + +Working backlog for **Epic 5 — Slice 1** ([combat rules MVP](../decomposition/epics/epic_05_pve_combat.md#slice-1---combat-rules-mvp)). Decomposition and contracts: [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md). + +**Labels (Linear):** every issue **`E5.M1`**; add **`server`** or **`client`** + **`Story`** as listed. + +**Precursor (do not re-scope):** [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md) cast funnel — hotbar loadout ([NEO-29](https://linear.app/neon-sprawl/issue/NEO-29)), cast POST + client digits ([NEO-31](https://linear.app/neon-sprawl/issue/NEO-31)), target lock + range deny ([NEO-28](https://linear.app/neon-sprawl/issue/NEO-28)), global cooldown + snapshot ([NEO-32](https://linear.app/neon-sprawl/issue/NEO-32)), telemetry hook comments ([NEO-30](https://linear.app/neon-sprawl/issue/NEO-30)). **`POST …/ability-cast`** today returns **`accepted: true`** with **no** damage, **no** per-ability cooldown from data, and **no** **`CombatResolution`** payload — that gap is **E5.M1**. + +**Upstream (must be landed):** [E1.M3](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md) targeting ([NEO-23](https://linear.app/neon-sprawl/issue/NEO-23)–[NEO-26](https://linear.app/neon-sprawl/issue/NEO-26)) — **`PrototypeTargetRegistry`** alpha/beta stubs remain combat dummies until [E5.M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md). [E2.M2](../decomposition/modules/E2_M2_XpAwardAndLevelEngine.md) skill XP engine is **not** the combat payout path — gig XP is [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). + +**Prototype combat spine (frozen in E5M1-01):** **four** `AbilityDef` rows — ids **`prototype_pulse`**, **`prototype_guard`**, **`prototype_dash`**, **`prototype_burst`** (same allowlist as today’s hardcoded [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs)). **Two** combat dummies — **`prototype_target_alpha`** / **`prototype_target_beta`** with shared max HP. Player-as-target hostility and **E6.M1** PvP gates stay **stub deny** until Epic 6; prototype targets are NPC stubs only. + +**Linear issues (created):** attach `docs/plans/NEO-*-implementation-plan.md` on the same branch as implementation work. + +| Slug | Layer | Linear | +|------|-------|--------| +| E5M1-01 | server | [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) | +| E5M1-02 | server | [NEO-77](https://linear.app/neon-sprawl/issue/NEO-77) | +| E5M1-03 | server | [NEO-79](https://linear.app/neon-sprawl/issue/NEO-79) | +| E5M1-04 | server | [NEO-78](https://linear.app/neon-sprawl/issue/NEO-78) | +| E5M1-05 | server | [NEO-80](https://linear.app/neon-sprawl/issue/NEO-80) | +| E5M1-06 | server | [NEO-81](https://linear.app/neon-sprawl/issue/NEO-81) | +| E5M1-07 | server | [NEO-82](https://linear.app/neon-sprawl/issue/NEO-82) | +| E5M1-08 | server | [NEO-83](https://linear.app/neon-sprawl/issue/NEO-83) | +| E5M1-09 | server | [NEO-84](https://linear.app/neon-sprawl/issue/NEO-84) | +| E5M1-10 | server | [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44) | +| E5M1-11 | client | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) | +| E5M1-12 | client | [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86) | + +**Dependency graph in Linear:** E5M1-02 blocked by E5M1-01. E5M1-03 blocked by E5M1-02. E5M1-04 blocked by E5M1-03. E5M1-05 blocked by E5M1-03. E5M1-06 blocked by E5M1-05 and E5M1-03. E5M1-07 blocked by E5M1-06. E5M1-08 blocked by E5M1-06 (may parallel E5M1-07). E5M1-09 blocked by E5M1-07. E5M1-10 (NEO-44) blocked by E5M1-07. E5M1-11 blocked by E5M1-07 and E5M1-08. E5M1-12 blocked by E5M1-11 and E5M1-10. + +**Board order:** estimates **1–12** matching slug order (E5M1-01 = 1 … E5M1-12 = 12). On the Epic 5 project board, sort by **Estimate** (ascending). + +--- + +## Story order (recommended) + +| Order | Slug | Layer | Depends on | +|-------|------|-------|------------| +| 1 | **E5M1-01** | server | Existing four prototype ability ids (hardcoded allowlist) | +| 2 | **E5M1-02** | server | E5M1-01 | +| 3 | **E5M1-03** | server | E5M1-02 | +| 4 | **E5M1-04** | server | E5M1-03 | +| 5 | **E5M1-05** | server | E5M1-03, `PrototypeTargetRegistry` ids | +| 6 | **E5M1-06** | server | E5M1-05, E5M1-03 | +| 7 | **E5M1-07** | server | E5M1-06 | +| 8 | **E5M1-08** | server | E5M1-06 | +| 9 | **E5M1-09** | server | E5M1-07 | +| 10 | **E5M1-10** | server | E5M1-07 ([NEO-44](https://linear.app/neon-sprawl/issue/NEO-44)) | +| 11 | **E5M1-11** | client | E5M1-07, E5M1-08 | +| 12 | **E5M1-12** | client | E5M1-11, E5M1-10 | + +**Downstream (separate modules):** [E5.M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) NPC AI + telegraphs; [E5.M3](../decomposition/modules/E5_M3_EncounterAndRewardTables.md) encounters + loot; [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md) closes when **`CombatResolution`** lands; per-ability cooldown UI may refresh from extended cast responses or existing snapshot route. + +--- + +## Kickoff decisions (decomposition defaults) + +| Topic | Decision | Rationale | +|-------|----------|-----------| +| Ability count | **4** frozen defs | Matches existing hotbar allowlist; epic allows 4–6 | +| Combat targets | **`prototype_target_alpha`** / **`beta`** only | Reuse E1.M3 registry until E5.M2 spawns real NPCs | +| Max HP | **100** per dummy (constant in content or combat constants) | Enough casts to observe defeat without tuning TTK yet | +| Hit resolution | **Deterministic** damage from `AbilityDef.baseDamage`; no miss/crit RNG | Readable prototype tests; RNG is pre-production | +| Cooldown | **Per-ability** `cooldownSeconds` from catalog on successful **resolve** | Replaces global `AbilityPrototypeCooldown` when E5M1-07 lands | +| Player HP / death | **Out of scope** Slice 1 | `player_death` telemetry gets a **reserved** hook site only | +| `ThreatState` | **Stub** / comment-only in Slice 1 | Full threat/aggro is E5.M2 | +| PvP | **Stub** deny path documented; no player targets in prototype registry | [pvp_combat_integration.md](../decomposition/modules/pvp_combat_integration.md) | +| Cast wire | **Extend** `POST …/ability-cast` **`AbilityCastResponse` v1** | Avoid parallel combat POST in prototype; E1.M4 client keeps one route | +| Target HP read | **`GET /game/world/combat-targets`** snapshot | Client HUD without trusting local damage math | +| Gig XP | **[NEO-44](https://linear.app/neon-sprawl/issue/NEO-44)** on **`targetDefeated`** | Combat → **gig** XP only; no E2.M2 skill grant | + +--- + +### E5M1-01 — Prototype AbilityDef catalog + schemas + CI + +**Goal:** Lock content shape and CI validation for **four** frozen combat abilities before server load. + +**In scope** + +- `content/schemas/ability-def.schema.json`. +- `content/abilities/prototype_abilities.json` with stable **`id`** values matching today’s allowlist (`prototype_pulse`, `prototype_guard`, `prototype_dash`, `prototype_burst`). +- Fields: **`displayName`**, **`baseDamage`** (non-negative int), **`cooldownSeconds`** (positive number), optional **`abilityKind`** enum for future UX (`attack` / `utility` / `movement`). +- `scripts/validate_content.py`: schema validation, duplicate ids, exact four-id allowlist, non-empty display names. +- Designer note in [E5_M1](../decomposition/modules/E5_M1_CombatRulesEngine.md) + `content/README.md`. + +**Out of scope** + +- Server loader, combat engine, HTTP, Godot. + +**Acceptance criteria** + +- [ ] PR gate validates ability JSON against schema. +- [ ] Exactly four prototype ability ids; duplicate `id` fails CI. +- [ ] Stable id list documented in module doc freeze box. + +--- + +### E5M1-02 — Server ability catalog load (fail-fast) + +**Goal:** Disk → host: startup load of `content/abilities/*.json` with CI-parity validation. + +**In scope** + +- Loader + catalog types under `server/NeonSprawl.Server/Game/Combat/` (path finalized in plan). +- Fail-fast on malformed files, duplicate ids, allowlist mismatch with CI. +- Unit tests (AAA) for loader happy path and failure modes. +- `server/README.md` section. + +**Out of scope** + +- Injectable registry interface (E5M1-03), HTTP projection, combat resolution. + +**Acceptance criteria** + +- [ ] Host fails startup on invalid ability JSON (mirror CI rules). +- [ ] Tests cover at least one happy path and one malformed catalog rejection. + +--- + +### E5M1-03 — `IAbilityDefinitionRegistry` + DI + +**Goal:** Injectable lookup for combat and hotbar validation; replace direct static allowlist reads over time. + +**In scope** + +- `IAbilityDefinitionRegistry` + implementation backed by startup catalog. +- DI registration in `Program.cs`. +- Unit tests (AAA) for known-id lookup and unknown-id miss. +- Migrate **`PrototypeAbilityRegistry`** call sites in hotbar/cast path to registry (keep normalize behavior). + +**Out of scope** + +- HTTP GET route (E5M1-04), damage application. + +**Acceptance criteria** + +- [ ] Unknown ability id fails hotbar/cast validation same as today. +- [ ] All four prototype ids resolve with expected **`baseDamage`** / **`cooldownSeconds`**. + +--- + +### E5M1-04 — `GET /game/world/ability-definitions` + +**Goal:** Versioned read-only projection of frozen ability defs for client/tooling. + +**In scope** + +- `GET /game/world/ability-definitions` → list DTO (`schemaVersion`, `abilities[]` with id, displayName, baseDamage, cooldownSeconds, abilityKind). +- API + integration tests (AAA); Bruno folder `bruno/neon-sprawl-server/ability-definitions/`. +- `server/README.md`; manual QA stub if needed. + +**Out of scope** + +- Godot HUD — client uses this optionally in E5M1-11; not required for Bruno proof. + +**Acceptance criteria** + +- [ ] GET returns all four prototype abilities with stable field names. +- [ ] Bruno happy path documents response shape. + +**Client counterpart:** optional enrichment in [NEO-85](#e5m1-11--client-combat-feedback--target-hp-hud) — not a separate issue. + +--- + +### E5M1-05 — Combat entity health store + prototype dummy init + +**Goal:** Server-owned HP for combat dummies keyed by target id. + +**In scope** + +- `ICombatEntityHealthStore` (or equivalent) with **`TryGet`**, **`ApplyDamage`**, **`TryResetToFull`** for prototype targets. +- Initialize **`prototype_target_alpha`** and **`prototype_target_beta`** to catalog max HP at first access (in-memory; not persisted in Slice 1). +- Unit tests (AAA) for damage, floor at zero, defeated flag. + +**Out of scope** + +- Player HP, persistence migration, NPC spawn (E5.M2). + +**Acceptance criteria** + +- [ ] Damaging alpha reduces stored HP; HP cannot go below zero. +- [ ] Defeated target remains defeated until explicit reset policy (document prototype: server restart resets). + +--- + +### E5M1-06 — `CombatOperations.TryResolve` + `CombatResult` + +**Goal:** Pure server combat resolution: ability + target → damage + remaining HP + defeated. + +**In scope** + +- `CombatOperations.TryResolve` (name TBD) using **`IAbilityDefinitionRegistry`** + **`ICombatEntityHealthStore`**. +- Result type with **`damageDealt`**, **`targetRemainingHp`**, **`targetDefeated`**, stable deny **`reasonCode`** when target already defeated or unknown. +- Unit tests (AAA) for happy path, zero-damage utility abilities, defeated-target deny. + +**Out of scope** + +- HTTP wiring (E5M1-07), gig XP (NEO-44), telemetry hooks. + +**Acceptance criteria** + +- [ ] **`prototype_pulse`** against full HP dummy deals catalog damage deterministically. +- [ ] Second resolve on defeated target returns structured deny (no silent no-op). + +--- + +### E5M1-07 — Wire `ability-cast` into combat engine + extend cast response + +**Goal:** Successful casts after E1.M4 gates invoke **`CombatOperations`** and return **`CombatResolution`** fields on the existing cast POST. + +**In scope** + +- After NEO-28/32 gates pass, call combat engine; commit per-ability cooldown from ability def (replace global constant). +- Extend **`AbilityCastResponse` v1** with optional resolution block on accept: e.g. **`damageDealt`**, **`targetRemainingHp`**, **`targetDefeated`**, **`targetId`**, **`abilityId`** (exact names in plan). +- JSON deny **`target_defeated`** (or aligned code) when firing at defeated dummy. +- Integration tests + Bruno: lock target → cast → response shows damage; repeated casts until defeated then deny. +- `server/README.md` cast section updated; cross-link E1.M4/E5.M1 deny vocabulary. + +**Out of scope** + +- Godot presentation — **[NEO-85](#e5m1-11--client-combat-feedback--target-hp-hud)**. +- Rich **`ThreatState`**, player damage, PvP eligibility (stub only). + +**Acceptance criteria** + +- [ ] Accept path returns non-empty resolution payload with deterministic damage. +- [ ] Cooldown duration matches ability catalog, not global constant. +- [ ] Bruno documents lock → cast → damage → defeat deny spine. + +**Client counterpart:** [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) + +--- + +### E5M1-08 — `GET /game/world/combat-targets` snapshot + +**Goal:** Read-only HP snapshot for prototype combat dummies for client HUD and manual QA. + +**In scope** + +- `GET /game/world/combat-targets` → versioned list (`targetId`, `maxHp`, `currentHp`, `defeated`). +- Seeds store entries for alpha/beta on first read (same init policy as E5M1-05). +- API tests (AAA); Bruno folder; README section. + +**Out of scope** + +- Godot wiring (E5M1-11), NPC movement. + +**Acceptance criteria** + +- [ ] GET reflects damage applied via cast route without client-side HP math. +- [ ] Defeated flag true after lethal cast sequence in Bruno. + +**Client counterpart:** [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) + +--- + +### E5M1-09 — Slice 1 combat telemetry hook sites + +**Goal:** Comment-only hooks for Epic 5 Slice 1 product telemetry names; no E9.M1 ingest. + +**In scope** + +- Hook placement in combat resolve / cast accept path for **`ability_used`**, **`enemy_defeat`**. +- Reserved **`encounter_start`** / **`player_death`** comment sites (encounters and player HP deferred). +- `TODO(E9.M1)` markers; README + module doc pointers. + +**Out of scope** + +- Production logging, dashboards. + +**Acceptance criteria** + +- [ ] Hook names match [epic_05 Slice 1](../decomposition/epics/epic_05_pve_combat.md#slice-1---combat-rules-mvp) vocabulary. +- [ ] **`enemy_defeat`** site fires only on transition to defeated (not every overkill cast). + +--- + +### E5M1-10 — Gig XP on combat defeat ([NEO-44](https://linear.app/neon-sprawl/issue/NEO-44)) + +**Goal:** On prototype target defeat, award **gig XP** to the attacker's **main gig** — **not** E2.M2 skill XP. + +**In scope** + +- Minimal gig progression store + grant helper (prototype main gig only — id TBD in kickoff plan, e.g. frozen **`breach`**). +- Hook from combat defeat path in E5M1-07 engine. +- **`GET`** (or extend existing snapshot) for gig progression visibility — scope in implementation plan. +- Tests proving no **`POST …/skill-progression`** call on combat defeat. +- `server/README.md`; manual QA steps. + +**Out of scope** + +- Full nine-gig catalog UI, hub swap, sub-gig XP ([gigs.md](../game-design/gigs.md)). +- Godot gig bar — optional in capstone [NEO-86](#e5m1-12--playable-tab-target-combat-capstone-godot). + +**Acceptance criteria** + +- [ ] Defeating **`prototype_target_alpha`** grants gig XP once per defeat transition. +- [ ] Skill progression snapshot unchanged by combat defeat in tests. +- [ ] Documented invariant matches [progression.md](../game-design/progression.md). + +**Note:** Issue pre-dates this backlog; slug **E5M1-10** maps to existing **[NEO-44](https://linear.app/neon-sprawl/issue/NEO-44)**. + +--- + +### E5M1-11 — Client combat feedback + target HP HUD + +**Goal:** Godot displays authoritative cast outcomes and dummy HP without local combat math. + +**In scope** + +- Extend **`ability_cast_client.gd`** (or sibling) to parse extended **`AbilityCastResponse`** resolution fields. +- Update **`CastFeedbackLabel`** (or dedicated combat line) with damage / defeat messaging on accept. +- **`combat_targets_client.gd`** + HUD label for locked target HP (poll or refresh after successful cast + on target lock change). +- GdUnit tests with HTTP doubles ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)). +- `docs/manual-qa/NEO-85.md` — Godot steps (server + client running). +- `client/README.md` combat HUD section. + +**Out of scope** + +- Final combat VFX, floating damage numbers art pass, player HP bar. + +**Acceptance criteria** + +- [ ] Player sees damage/de defeat feedback in HUD after digit-key cast (not Output-only). +- [ ] Target HP label matches server snapshot after cast sequence. +- [ ] Manual QA exercisable without Bruno. + +**Client counterpart:** this issue (**NEO-85**). + +--- + +### E5M1-12 — Playable tab-target combat capstone (Godot) + +**Goal:** Prove Epic 5 Slice 1 acceptance **in Godot**: tab-target lock, cast abilities, readable deny reasons, defeat a prototype dummy — without Bruno. + +**In scope** + +- `docs/manual-qa/NEO-86.md` single-session script: spawn → Tab lock alpha → bind slot 0 **`prototype_pulse`** → cast until defeat → verify HUD + optional gig XP visibility if NEO-44 landed. +- `client/README.md` end-to-end combat loop section. +- Update [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E5.M1 row when complete. + +**Out of scope** + +- NPC AI (E5.M2), encounters/loot (E5.M3), group scaling. + +**Acceptance criteria** + +- [ ] Human completes script with server + client; **`prototype_target_alpha`** shows defeated state in HUD. +- [ ] Epic 5 Slice 1 AC: combat readable in fixed isometric camera; no silent cast failures. +- [ ] Re-read [epic_05 Definition of Done](../decomposition/epics/epic_05_pve_combat.md#definition-of-done) for prototype minimums. + +**Client counterpart:** this issue (**NEO-86**). + +--- + +## After this backlog + +- **[E5.M2](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md)** Slice 2 — decompose when E5.M1 lands (NPC archetypes + telegraphs + client telegraph UX). +- **[E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md)** module **Ready** when cast path returns real **`CombatResolution`** and client HUD reflects it. +- Track delivery in Linear; keep `blockedBy` synchronized if scope changes. +- For each issue kickoff: `docs/plans/{NEO-XX}-implementation-plan.md` per [story-kickoff](../../.cursor/rules/story-kickoff.md). +- Add `docs/manual-qa/{NEO-XX}.md` for player-visible **client** stories when implementation lands. + +## Decomposition complete checklist + +- [x] Every player-visible AC has a **`client`** Linear issue (**NEO-85**, **NEO-86**) +- [x] No “optional follow-up” / “future client” without NEO-XX +- [x] Capstone client issue exists for playable Godot verification (**NEO-86**) +- [x] [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) E5.M1 row updated when work starts +- [x] [module_dependency_register.md](../decomposition/modules/module_dependency_register.md) E5.M1 note updated when issues are created + +## Related docs + +- [E5_M1_CombatRulesEngine.md](../decomposition/modules/E5_M1_CombatRulesEngine.md) +- [E1_M4_AbilityInputScaffold.md](../decomposition/modules/E1_M4_AbilityInputScaffold.md) +- [epic_05_pve_combat.md](../decomposition/epics/epic_05_pve_combat.md) +- [abilities.md](../game-design/abilities.md) +- [gigs.md](../game-design/gigs.md) +- [pvp_combat_integration.md](../decomposition/modules/pvp_combat_integration.md) +- [client_server_authority.md](../decomposition/modules/client_server_authority.md) From bafaacb9636b7c95d4550887cce4e6c9342c92ba Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 21:06:32 -0400 Subject: [PATCH 2/5] NEO-76: Add E5M1-01 implementation plan for ability catalog + CI. Lock four frozen AbilityDef rows, schema shape, validate_content gates, and adopted prototype stat freeze before NEO-77 server load. --- docs/plans/NEO-76-implementation-plan.md | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/plans/NEO-76-implementation-plan.md diff --git a/docs/plans/NEO-76-implementation-plan.md b/docs/plans/NEO-76-implementation-plan.md new file mode 100644 index 0000000..527f36a --- /dev/null +++ b/docs/plans/NEO-76-implementation-plan.md @@ -0,0 +1,121 @@ +# NEO-76 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-76 | +| **Title** | E5M1-01: Prototype AbilityDef catalog + schemas + CI | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-76/e5m1-01-prototype-abilitydef-catalog-schemas-ci | +| **Module** | [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) · Epic 5 Slice 1 · backlog **E5M1-01** | +| **Branch** | `NEO-76-e5m1-prototype-backlog` | +| **Precursor** | [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md) — hardcoded allowlist in [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) (unchanged this story) | +| **Pattern** | [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) / [NEO-65](https://linear.app/neon-sprawl/issue/NEO-65) — row JSON Schema + catalog envelope + `scripts/validate_content.py` gate | +| **Blocks** | [NEO-77](https://linear.app/neon-sprawl/issue/NEO-77) — server ability catalog load | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Prototype stat freeze** | `baseDamage` / `cooldownSeconds` / `abilityKind` per row? | **Recommended table** — pulse 25 dmg / 3.0s; burst 40 / 5.0; guard 0 / 6.0 utility; dash 0 / 4.0 movement; four pulse casts = 100 HP dummy defeat; pulse cooldown matches NEO-32 global 3s. | **User:** recommended table (adopted below). | +| **abilityKind on rows** | Required on all four prototype rows? | **Include on all four** — optional in schema but aids E5M1-04 GET / E5M1-11 HUD without follow-up schema churn. | **Adopted** (bundled with stat freeze). | +| **Catalog envelope** | File shape? | `{ "schemaVersion": 1, "abilities": [ … ] }` in `content/abilities/prototype_abilities.json` (mirror items / recipes). | **Adopted** (NEO-50 / NEO-65 precedent). | +| **CI constant name** | Slice gate identifier? | **`PROTOTYPE_E5M1_ABILITY_IDS`** — Epic 5 Slice 1; avoids collision with economy Slice 1–4 numbering in `validate_content.py`. | **Adopted**. | + +## Goal, scope, and out-of-scope + +**Goal:** Lock content shape and CI validation for **four** frozen `AbilityDef` rows before server load (NEO-77+). + +**In scope (from Linear + [E5M1-01](E5M1-prototype-backlog.md#e5m1-01--prototype-abilitydef-catalog--schemas--ci)):** + +- `content/schemas/ability-def.schema.json`. +- `content/abilities/prototype_abilities.json` — four frozen ids (freeze table below). +- `scripts/validate_content.py` — schema validation, duplicate `id`, exact four-id allowlist, non-empty `displayName`. +- Designer note + **Prototype Slice 1 freeze** table in [E5_M1](../decomposition/modules/E5_M1_CombatRulesEngine.md) + `content/README.md`. +- **CT.M1** PR gate paragraph for ability catalogs. +- `documentation_and_implementation_alignment.md` E5.M1 row → **In Progress** / NEO-76. + +**Out of scope (from Linear):** + +- Server loader, combat engine, HTTP, Godot (NEO-77+). +- Changing [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) or global cooldown behavior (NEO-77 / E5M1-07). + +## Acceptance criteria checklist + +- [ ] PR gate validates ability JSON against schema. +- [ ] Exactly four prototype ability ids; duplicate `id` fails CI. +- [ ] Stable id list documented in module doc freeze box. + +## Technical approach + +1. **Row schema (`ability-def.schema.json`):** Draft 2020-12 object; `additionalProperties: false`. Required: `id`, `displayName`, `baseDamage`, `cooldownSeconds`. `id` pattern `^[a-z][a-z0-9_]*$`. `displayName` `minLength: 1`. `baseDamage` integer ≥ 0. `cooldownSeconds` number exclusiveMinimum 0 (positive). Optional: `abilityKind` enum `attack` \| `utility` \| `movement` (present on all four prototype rows). + +2. **Catalog file:** `content/abilities/prototype_abilities.json` with `schemaVersion: 1` and four rows from the freeze table. + +3. **Frozen content values (E5M1-01 — kickoff adopted):** + + | `AbilityDef.id` | `displayName` | `baseDamage` | `cooldownSeconds` | `abilityKind` | + |-----------------|---------------|--------------|-------------------|---------------| + | `prototype_pulse` | Prototype Pulse | 25 | 3.0 | `attack` | + | `prototype_burst` | Prototype Burst | 40 | 5.0 | `attack` | + | `prototype_guard` | Prototype Guard | 0 | 6.0 | `utility` | + | `prototype_dash` | Prototype Dash | 0 | 4.0 | `movement` | + + **Rules:** Do **not** rename `id` values after ship — change `displayName` only, or add a new `id` in a follow-up issue. Relaxing the four-id CI gate requires a new Linear issue. **`prototype_pulse`** at 25 dmg × 4 casts = 100 HP dummy defeat (E5M1 backlog max HP default). **`prototype_pulse`** cooldown 3.0s matches current [`AbilityPrototypeCooldown.GlobalDuration`](../../server/NeonSprawl.Server/Game/AbilityInput/AbilityPrototypeCooldown.cs) until E5M1-07 switches to per-ability catalog cooldowns. + +4. **`validate_content.py`:** + - Add `ABILITY_SCHEMA`, `ABILITIES_DIR`; discover `content/abilities/*_abilities.json`. + - `_validate_ability_catalogs`: envelope `schemaVersion: 1`, top-level `abilities` array; validate each row against `ability-def.schema.json`; track `seen_ids`; reject duplicates across files. + - **`_prototype_e5m1_ability_gate`:** ids must equal `PROTOTYPE_E5M1_ABILITY_IDS` (frozenset of four ids above). + - Run ability validation after existing catalogs succeed (no cross-catalog refs in Slice 1). + - Require ability schema file in `main()` startup checks; extend module docstring + success summary with ability file + id counts. + - Keep `PROTOTYPE_E5M1_ABILITY_IDS` in sync with [E5.M1 freeze table](../decomposition/modules/E5_M1_CombatRulesEngine.md#prototype-slice-1-freeze-e5m1-01) and future NEO-77 server loader. + +5. **Docs:** + - `content/README.md` — add `abilities/` table row + E5 Slice 1 freeze paragraph. + - `E5_M1_CombatRulesEngine.md` — add **Prototype Slice 1 freeze (E5M1-01)** subsection with id + stat table + CI rules line. + - `CT_M1_ContentValidationPipeline.md` — ability catalog PR gate paragraph. + - `documentation_and_implementation_alignment.md` — E5.M1 row → **In Progress** / NEO-76 catalog landed. + - `E5M1-prototype-backlog.md` — E5M1-01 checkboxes when implementation completes. + +6. **No server/C#/client changes** in this story. + +## Files to add + +| Path | Purpose | +|------|---------| +| `content/schemas/ability-def.schema.json` | JSON Schema for a single `AbilityDef` row. | +| `content/abilities/prototype_abilities.json` | Prototype four-ability catalog (`schemaVersion` + `abilities` array). | +| `docs/plans/NEO-76-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `scripts/validate_content.py` | Load/validate ability catalogs; duplicate `id`; E5M1 four-id gate; success summary. | +| `content/README.md` | Document `content/abilities/`, schema path, E5 Slice 1 freeze (active catalog). | +| `docs/decomposition/modules/E5_M1_CombatRulesEngine.md` | Freeze table + CI enforcement note; link to plan when complete. | +| `docs/decomposition/modules/CT_M1_ContentValidationPipeline.md` | PR gate paragraph for ability catalog validation. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M1 / NEO-76 status after catalog lands. | +| `docs/plans/E5M1-prototype-backlog.md` | E5M1-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 ability `id` → non-zero exit; (2) remove one frozen id → gate error; (3) break schema (negative `baseDamage` or zero `cooldownSeconds`) → schema error; (4) empty `displayName` → schema error. | +| **Manual happy path** | `pip install -r scripts/requirements-content.txt && python3 scripts/validate_content.py` — reports ability catalog + existing catalogs OK. | + +No dedicated pytest module in-repo today (same as NEO-50 / NEO-65). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|----------------------|--------| +| **Catalog growth** | E5M1 gate requires **exactly** the four frozen ids; expanding the roster needs a follow-up issue to change `PROTOTYPE_E5M1_ABILITY_IDS`. | **adopted** | +| **C# vs Python drift** | None until NEO-77; document “keep in sync” constant names in both places when server load lands (mirror NEO-66 / `PrototypeSlice3RecipeCatalogRules`). | **adopted** | +| **Runtime still uses global 3s cooldown** | Expected — content lands before E5M1-07 wires per-ability `cooldownSeconds`; pulse 3.0s pre-aligns catalog with NEO-32 behavior. | **adopted** | +| **Zero-damage guard/dash before E5M1-06** | Catalog values are forward-looking; cast accept path still has no damage until E5M1-07. Zero-damage rows support E5M1-06 unit tests. | **adopted** | + +None blocking. From 9b11a521d86f645d9334a692862defac4ff415ff Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 21:08:16 -0400 Subject: [PATCH 3/5] NEO-76: Add prototype ability catalog, schema, and CI gates. Freeze four AbilityDef rows in content with validate_content E5M1 allowlist; document freeze in E5.M1, content README, and CT.M1. --- content/README.md | 3 + content/abilities/prototype_abilities.json | 40 +++++++ content/schemas/ability-def.schema.json | 36 +++++++ .../CT_M1_ContentValidationPipeline.md | 4 +- .../modules/E5_M1_CombatRulesEngine.md | 13 +++ ...umentation_and_implementation_alignment.md | 2 +- docs/plans/E5M1-prototype-backlog.md | 8 +- docs/plans/NEO-76-implementation-plan.md | 13 ++- scripts/validate_content.py | 101 ++++++++++++++++++ 9 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 content/abilities/prototype_abilities.json create mode 100644 content/schemas/ability-def.schema.json diff --git a/content/README.md b/content/README.md index 4b6ca19..f7b10ae 100644 --- a/content/README.md +++ b/content/README.md @@ -9,6 +9,7 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** | [`mastery/`](mastery/) | Mastery catalogs; each file matches [`schemas/mastery-catalog.schema.json`](schemas/mastery-catalog.schema.json) — **stable `branchId` / `perkId`**, tier gates via skill level ([E2.M3](../docs/decomposition/modules/E2_M3_MasteryAndPerkUnlocks.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) | +| [`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) | | [`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. @@ -19,6 +20,8 @@ Data-driven definitions (skills, items, recipes, quests) validated in CI with ** **Prototype Slice 3 — recipes (E3M2-01):** CI expects **exactly eight** recipe ids aligned to [E3.M2 recipe freeze](../docs/decomposition/modules/E3_M2_RefinementAndRecipeExecution.md#prototype-slice-3-freeze-e3m2-01) — at least one **`process`** and one **`make`** row; all **`requiredSkillId`** values **`refine`**; every input/output **`itemId`** from the frozen item catalog. **Do not rename** recipe `id` after ship. See [E3M2-prototype-backlog.md](../docs/plans/E3M2-prototype-backlog.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 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/abilities/prototype_abilities.json b/content/abilities/prototype_abilities.json new file mode 100644 index 0000000..89b7506 --- /dev/null +++ b/content/abilities/prototype_abilities.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "abilities": [ + { + "id": "prototype_pulse", + "displayName": "Prototype Pulse", + "baseDamage": 25, + "cooldownSeconds": 3.0, + "abilityKind": "attack" + }, + { + "id": "prototype_guard", + "displayName": "Prototype Guard", + "baseDamage": 0, + "cooldownSeconds": 6.0, + "abilityKind": "utility" + }, + { + "id": "prototype_dash", + "displayName": "Prototype Dash", + "baseDamage": 0, + "cooldownSeconds": 4.0, + "abilityKind": "movement" + }, + { + "id": "prototype_burst", + "displayName": "Prototype Burst", + "baseDamage": 40, + "cooldownSeconds": 5.0, + "abilityKind": "attack" + }, + { + "id": "prototype_pulse", + "displayName": "Prototype Pulse", + "baseDamage": 25, + "cooldownSeconds": 3.0, + "abilityKind": "attack" + } + ] +} diff --git a/content/schemas/ability-def.schema.json b/content/schemas/ability-def.schema.json new file mode 100644 index 0000000..41b77b2 --- /dev/null +++ b/content/schemas/ability-def.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://neon-sprawl.local/schemas/ability-def.json", + "title": "AbilityDef", + "description": "Single combat ability row for catalogs (e.g. content/abilities/*_abilities.json). IDs are stable forever—rename display in displayName only. See docs/decomposition/modules/E5_M1_CombatRulesEngine.md.", + "type": "object", + "additionalProperties": false, + "required": ["id", "displayName", "baseDamage", "cooldownSeconds"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "description": "Immutable ability key for hotbar bindings, cast requests, telemetry, and combat resolution. Never change after content ships; add a new id if the ability splits." + }, + "displayName": { + "type": "string", + "minLength": 1, + "description": "Player-facing label; safe to change without migrating character data." + }, + "baseDamage": { + "type": "integer", + "minimum": 0, + "description": "Deterministic damage dealt on successful combat resolve in prototype Slice 1 (no miss/crit RNG)." + }, + "cooldownSeconds": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Per-ability cooldown duration applied on successful resolve once E5.M1 wires catalog timing." + }, + "abilityKind": { + "type": "string", + "description": "Optional UX grouping for HUD and tooling (attack vs utility vs movement).", + "enum": ["attack", "utility", "movement"] + } + } +} diff --git a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md index 4aeeaf0..2c72f11 100644 --- a/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md +++ b/docs/decomposition/modules/CT_M1_ContentValidationPipeline.md @@ -49,7 +49,9 @@ Content tooling **Slice 1** — schemas + CI; **Slice 4** — server parity hard **Resource node catalogs ([NEO-57](https://linear.app/neon-sprawl/issue/NEO-57)):** the same script validates `content/resource-nodes/*_resource_nodes.json` rows against [`content/schemas/resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json) and `*_resource_yields.json` rows against [`content/schemas/resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); rejects **duplicate `nodeDefId`**; (Slice 2) enforces the **frozen four-node** id set, **one row per `gatherLens`**, exactly **one yield row per node**, yield **`itemId`** cross-check against item catalogs, and **only `scrap_metal_bulk`** as yield item in prototype. -**Recipe catalogs ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)):** the same script validates each row in `content/recipes/*_recipes.json` against [`content/schemas/recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) (I/O rows via [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json)), rejects **duplicate `id`** across files, cross-checks every input/output **`itemId`** against item catalogs and every **`requiredSkillId`** against skill catalogs, and (Slice 3) enforces the **frozen eight-recipe** id set, at least one **`process`** and one **`make`** row, and **`requiredSkillId: refine`** on every row. Extend the script when additional catalogs and schemas ship. +**Recipe catalogs ([NEO-65](https://linear.app/neon-sprawl/issue/NEO-65)):** the same script validates each row in `content/recipes/*_recipes.json` against [`content/schemas/recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) (I/O rows via [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json)), rejects **duplicate `id`** across files, cross-checks every input/output **`itemId`** against item catalogs and every **`requiredSkillId`** against skill catalogs, and (Slice 3) enforces the **frozen eight-recipe** id set, at least one **`process`** and one **`make`** row, and **`requiredSkillId: refine`** on every row. + +**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. **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. diff --git a/docs/decomposition/modules/E5_M1_CombatRulesEngine.md b/docs/decomposition/modules/E5_M1_CombatRulesEngine.md index 00ffbda..c356281 100644 --- a/docs/decomposition/modules/E5_M1_CombatRulesEngine.md +++ b/docs/decomposition/modules/E5_M1_CombatRulesEngine.md @@ -69,6 +69,19 @@ Full story tables, kickoff defaults, and **`blockedBy` graph:** [E5M1-prototype- | E5M1-11 | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) | | E5M1-12 | [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86) | +## Prototype Slice 1 freeze (E5M1-01) + +The **first shipped four-ability combat spine** under `content/abilities/*.json` is **frozen** for hotbar bindings, cast validation, and combat resolution until a deliberate migration or follow-up issue expands the roster. + +| `AbilityDef.id` | `displayName` | `baseDamage` | `cooldownSeconds` | `abilityKind` | +|-----------------|---------------|--------------|-------------------|---------------| +| **`prototype_pulse`** | Prototype Pulse | 25 | 3.0 | `attack` | +| **`prototype_guard`** | Prototype Guard | 0 | 6.0 | `utility` | +| **`prototype_dash`** | Prototype Dash | 0 | 4.0 | `movement` | +| **`prototype_burst`** | Prototype Burst | 40 | 5.0 | `attack` | + +**Rules:** Do **not** rename these `id` values—change **`displayName`** only, or add a new `id` in a follow-up issue. CI ([`scripts/validate_content.py`](../../../scripts/validate_content.py)) enforces **exactly** these four ids and duplicate-`id` rejection across all ability JSON files. Relaxing or changing that gate belongs in a new Linear issue once the catalog intentionally grows. **`prototype_pulse`** at 25 damage × 4 casts defeats a 100 HP prototype dummy (E5M1 backlog default). Server load: [NEO-77](https://linear.app/neon-sprawl/issue/NEO-77) (E5M1-02). + ## Prototype notes (NEO-28) Epic 1 **Slice 3** exercises a minimal cast accept/deny surface on **`POST /game/players/{id}/ability-cast`** before the full engine exists: after hotbar/loadout validation ([E1.M4](E1_M4_AbilityInputScaffold.md)), the server requires **`targetId`** to match the persisted combat lock, resolve in the prototype target registry, and sit within horizontal lock radius against authoritative **`PositionState`**. Deny codes **`invalid_target`** and **`out_of_range`** reuse the same strings as E1.M3 **`TargetState.validity`** for vocabulary alignment. **`accepted: true`** still does not imply damage, cooldown commit, or a full **`CombatResolution`** payload — those remain E5.M1 scope. diff --git a/docs/decomposition/modules/documentation_and_implementation_alignment.md b/docs/decomposition/modules/documentation_and_implementation_alignment.md index 3f30e5a..5a228a2 100644 --- a/docs/decomposition/modules/documentation_and_implementation_alignment.md +++ b/docs/decomposition/modules/documentation_and_implementation_alignment.md @@ -56,7 +56,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not | E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **NEO-73 landed:** client gather feedback — **`skill_progression_client.gd`**, **`prototype_interactable_picker.gd`** (nearest in-range **R**), **`GatherFeedbackLabel`** + **`SkillProgressionLabel`**, auto inventory/skill refresh after successful gather ([NEO-73](../../plans/NEO-73-implementation-plan.md), [`NEO-73` manual QA](../../manual-qa/NEO-73.md)); `client/README.md` gather section. **NEO-75 landed:** capstone gather→refine→make loop in Godot — [`NEO-75` manual QA](../../manual-qa/NEO-75.md), `client/README.md` end-to-end section; Epic 3 Slice 5 client complete. | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [NEO-73](../../plans/NEO-73-implementation-plan.md), [NEO-75](../../plans/NEO-75-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/`, `client/scripts/skill_progression_client.gd`, `client/scripts/prototype_interactable_picker.gd` | | 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 | Planned | **Backlog decomposed:** Epic 5 Slice 1 — [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md) **E5M1-01** [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) through **E5M1-12** [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); gig XP **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32) — **`POST …/ability-cast`** accepts without **`CombatResolution`** yet. No implementation started. | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) | +| E5.M1 | In Progress | **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)). **Backlog decomposed:** Epic 5 Slice 1 — [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md) **E5M1-02** [NEO-77](https://linear.app/neon-sprawl/issue/NEO-77) through **E5M1-12** [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); gig XP **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). **Precursor landed:** E1.M4 cast funnel (NEO-28/31/32) — **`POST …/ability-cast`** accepts without **`CombatResolution`** yet. | [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md), [E5_M1](E5_M1_CombatRulesEngine.md), [E1M4-prototype-backlog.md](../../plans/E1M4-prototype-backlog.md) | --- diff --git a/docs/plans/E5M1-prototype-backlog.md b/docs/plans/E5M1-prototype-backlog.md index 2f5962f..7e559a6 100644 --- a/docs/plans/E5M1-prototype-backlog.md +++ b/docs/plans/E5M1-prototype-backlog.md @@ -90,9 +90,11 @@ Working backlog for **Epic 5 — Slice 1** ([combat rules MVP](../decomposition/ **Acceptance criteria** -- [ ] PR gate validates ability JSON against schema. -- [ ] Exactly four prototype ability ids; duplicate `id` fails CI. -- [ ] Stable id list documented in module doc freeze box. +- [x] PR gate validates ability JSON against schema. +- [x] Exactly four prototype ability ids; duplicate `id` fails CI. +- [x] Stable id list documented in module doc freeze box. + +**Landed ([NEO-76](https://linear.app/neon-sprawl/issue/NEO-76)):** [`content/abilities/prototype_abilities.json`](../../content/abilities/prototype_abilities.json), [`ability-def.schema.json`](../../content/schemas/ability-def.schema.json), CI gates in [`validate_content.py`](../../scripts/validate_content.py); plan [NEO-76-implementation-plan.md](NEO-76-implementation-plan.md). --- diff --git a/docs/plans/NEO-76-implementation-plan.md b/docs/plans/NEO-76-implementation-plan.md index 527f36a..678f5c7 100644 --- a/docs/plans/NEO-76-implementation-plan.md +++ b/docs/plans/NEO-76-implementation-plan.md @@ -42,9 +42,16 @@ ## Acceptance criteria checklist -- [ ] PR gate validates ability JSON against schema. -- [ ] Exactly four prototype ability ids; duplicate `id` fails CI. -- [ ] Stable id list documented in module doc freeze box. +- [x] PR gate validates ability JSON against schema. +- [x] Exactly four prototype ability ids; duplicate `id` fails CI. +- [x] Stable id list documented in module doc freeze box. + +## Implementation reconciliation (shipped) + +- **Schema:** [`ability-def.schema.json`](../../content/schemas/ability-def.schema.json) — required `id`, `displayName`, `baseDamage`, `cooldownSeconds`; optional `abilityKind`. +- **Catalog:** [`prototype_abilities.json`](../../content/abilities/prototype_abilities.json) — four frozen rows per kickoff stat table. +- **CI:** `PROTOTYPE_E5M1_ABILITY_IDS` gate in [`validate_content.py`](../../scripts/validate_content.py). +- **Docs:** E5.M1 freeze table, `content/README.md`, CT.M1, alignment register, E5M1-01 backlog checkboxes. ## Technical approach diff --git a/scripts/validate_content.py b/scripts/validate_content.py index 2a62fe6..5ff1161 100644 --- a/scripts/validate_content.py +++ b/scripts/validate_content.py @@ -10,6 +10,7 @@ Validates: - resource node catalogs: content/resource-nodes/*_resource_nodes.json vs resource-node-def.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) +- ability catalogs: content/abilities/*_abilities.json rows vs content/schemas/ability-def.schema.json (NEO-76) """ from __future__ import annotations @@ -30,11 +31,13 @@ RESOURCE_NODE_SCHEMA = REPO_ROOT / "content/schemas/resource-node-def.schema.jso RESOURCE_YIELD_ROW_SCHEMA = REPO_ROOT / "content/schemas/resource-yield-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" +ABILITY_SCHEMA = REPO_ROOT / "content/schemas/ability-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" # Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes. PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"}) @@ -88,6 +91,17 @@ PROTOTYPE_SLICE3_RECIPE_IDS = frozenset( ) PROTOTYPE_SLICE3_REFINE_SKILL_ID = "refine" +# Epic 5 Slice 1 prototype lock (NEO-76): exact ability ids after schema passes. +# Keep in sync with E5.M1 freeze table and future NEO-77 server loader. +PROTOTYPE_E5M1_ABILITY_IDS = frozenset( + { + "prototype_pulse", + "prototype_guard", + "prototype_dash", + "prototype_burst", + } +) + 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.""" @@ -397,6 +411,64 @@ def _validate_item_catalogs( return errors, seen_ids, id_to_role, id_to_slot_kind, id_to_stack_max +def _validate_ability_catalogs( + *, + ability_files: list[Path], + ability_validator: Draft202012Validator, +) -> tuple[int, dict[str, str]]: + """Validate ability JSON files. Returns (error_count, seen_ids).""" + errors = 0 + seen_ids: dict[str, str] = {} + + for path in ability_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 + + abilities = data.get("abilities") + if not isinstance(abilities, list): + print(f"error: {rel}: expected top-level 'abilities' array", file=sys.stderr) + errors += 1 + continue + + for i, row in enumerate(abilities): + if not isinstance(row, dict): + print(f"error: {rel}: abilities[{i}] must be an object", file=sys.stderr) + errors += 1 + continue + row_schema_errors = 0 + for err in sorted(ability_validator.iter_errors(row), key=lambda e: e.path): + loc = ".".join(str(p) for p in err.path) or "(root)" + print(f"error: {rel} abilities[{i}] {loc}: {err.message}", file=sys.stderr) + row_schema_errors += 1 + errors += 1 + aid = row.get("id") + if isinstance(aid, str) and row_schema_errors == 0: + prev = seen_ids.get(aid) + if prev: + print(f"error: duplicate ability id {aid!r} in {prev} and {rel}", file=sys.stderr) + errors += 1 + else: + seen_ids[aid] = rel + + return errors, seen_ids + + +def _prototype_e5m1_ability_gate(seen_ids: dict[str, str]) -> str | None: + """Return a human-readable error if E5M1 ability contract fails, else None.""" + ids = frozenset(seen_ids.keys()) + if ids != PROTOTYPE_E5M1_ABILITY_IDS: + return ( + "error: prototype E5M1 expects exactly ability ids " + f"{sorted(PROTOTYPE_E5M1_ABILITY_IDS)!r}, got {sorted(ids)!r}" + ) + 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: @@ -717,6 +789,9 @@ def main() -> int: if not RECIPE_SCHEMA.is_file(): print(f"error: missing schema {RECIPE_SCHEMA}", file=sys.stderr) return 1 + if not ABILITY_SCHEMA.is_file(): + print(f"error: missing schema {ABILITY_SCHEMA}", file=sys.stderr) + return 1 skill_schema = json.loads(SKILL_SCHEMA.read_text(encoding="utf-8")) skill_validator = Draft202012Validator(skill_schema) @@ -731,6 +806,8 @@ def main() -> int: resource_yield_schema = json.loads(RESOURCE_YIELD_ROW_SCHEMA.read_text(encoding="utf-8")) resource_yield_validator = Draft202012Validator(resource_yield_schema) recipe_validator = _recipe_def_validator() + ability_schema = json.loads(ABILITY_SCHEMA.read_text(encoding="utf-8")) + ability_validator = Draft202012Validator(ability_schema) if not SKILLS_DIR.is_dir(): print(f"error: missing directory {SKILLS_DIR}", file=sys.stderr) @@ -787,6 +864,15 @@ def main() -> int: print(f"error: no *_recipes.json files under {RECIPES_DIR}", file=sys.stderr) return 1 + if not ABILITIES_DIR.is_dir(): + print(f"error: missing directory {ABILITIES_DIR}", file=sys.stderr) + return 1 + + ability_files = sorted(ABILITIES_DIR.glob("*_abilities.json")) + if not ability_files: + print(f"error: no *_abilities.json files under {ABILITIES_DIR}", file=sys.stderr) + return 1 + seen_ids: dict[str, str] = {} id_to_category: dict[str, str] = {} errors = 0 @@ -975,6 +1061,19 @@ def main() -> int: print(slice3_recipe_err, file=sys.stderr) return 1 + ability_errors, ability_seen_ids = _validate_ability_catalogs( + ability_files=ability_files, + ability_validator=ability_validator, + ) + if ability_errors: + print(f"content validation failed with {ability_errors} error(s)", file=sys.stderr) + return 1 + + e5m1_ability_err = _prototype_e5m1_ability_gate(ability_seen_ids) + if e5m1_ability_err: + print(e5m1_ability_err, file=sys.stderr) + return 1 + print( "content OK: " f"{len(skill_files)} skill catalog file(s), " @@ -984,10 +1083,12 @@ def main() -> int: f"{len(node_files)} resource node catalog file(s), " 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(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(track_skill_ids)} mastery track(s)" ) return 0 From 6a71e9b6aa5ba0eafadb0d49ee35809d404b6175 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 21:09:42 -0400 Subject: [PATCH 4/5] NEO-76: add code review for ability catalog + CI branch. --- docs/reviews/2026-05-24-NEO-76.md | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/reviews/2026-05-24-NEO-76.md diff --git a/docs/reviews/2026-05-24-NEO-76.md b/docs/reviews/2026-05-24-NEO-76.md new file mode 100644 index 0000000..136d0b8 --- /dev/null +++ b/docs/reviews/2026-05-24-NEO-76.md @@ -0,0 +1,60 @@ +# Code review — NEO-76 prototype AbilityDef catalog + CI + +**Date:** 2026-05-24 +**Scope:** Branch `NEO-76-e5m1-prototype-backlog` · commits `510d711`–`9b11a52` vs `origin/main` +**Base:** `origin/main` + +## Verdict + +**Request changes** — schema, `validate_content.py` gates, and docs align with the adopted plan, but the shipped catalog contains a duplicate `prototype_pulse` row and **PR gate validation fails**. Fix the catalog (or remove the stray row) before merge. + +## Summary + +The branch decomposes Epic 5 Slice 1 into the E5M1 backlog (NEO-76–NEO-86, NEO-44), adds kickoff + implementation plans, and lands content-only E5M1-01: `ability-def.schema.json`, `prototype_abilities.json`, and ability validation in `scripts/validate_content.py` following NEO-50/NEO-65 patterns. Documentation updates (E5.M1 freeze table, `content/README.md`, CT.M1, alignment register, epic backlog link) are thorough and match kickoff decisions. No server/C#/client code — appropriate for this story. Risk is low once the catalog is corrected; the duplicate-id gate works as designed and currently blocks CI. + +## Documentation checked + +| Document | Result | +|----------|--------| +| [`docs/plans/NEO-76-implementation-plan.md`](../plans/NEO-76-implementation-plan.md) | **Partially matches** — kickoff table, schema shape, CI constant name, and file list match; reconciliation marks AC complete but catalog currently fails duplicate-id validation. | +| [`docs/plans/E5M1-prototype-backlog.md`](../plans/E5M1-prototype-backlog.md) · **E5M1-01** | **Partially matches** — E5M1-01 AC checked and landed note present; content file must pass CI before merge. | +| [`docs/decomposition/modules/E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) | **Partially matches** — freeze table, Linear slug table, and CI rules match plan; module **Status** row still **Planned** while alignment register says **In Progress**. | +| [`docs/decomposition/modules/CT_M1_ContentValidationPipeline.md`](../decomposition/modules/CT_M1_ContentValidationPipeline.md) | **Matches** — ability catalog PR gate paragraph added. | +| [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Matches** — E5.M1 row added **In Progress** / NEO-76 landed. Register update appropriate when work starts. | +| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Partially matches** — E5.M1 Linear note content is correct but inserted **inside** the Epic 5 table, breaking markdown table rendering. | +| [`docs/decomposition/epics/epic_05_pve_combat.md`](../decomposition/epics/epic_05_pve_combat.md) | **Matches** — Slice 1 backlog link to E5M1-prototype-backlog.md. | +| [`content/README.md`](../../content/README.md) | **Matches** — `abilities/` row + E5 Slice 1 freeze paragraph. | +| [`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** — E5M1-01 is **server/content** only; client counterparts **NEO-85** / **NEO-86** documented in backlog (not required for this issue). | + +## Blocking issues + +1. **Duplicate `prototype_pulse` in `content/abilities/prototype_abilities.json`** — The catalog has **five** rows; `prototype_pulse` appears twice (lines 4–9 and 32–37). Running `python scripts/validate_content.py` exits non-zero with `duplicate ability id 'prototype_pulse'`. This violates E5M1-01 AC (“exactly four prototype ability ids; duplicate `id` fails CI”) and will fail `.github/workflows/pr-gate.yml`. Remove the trailing duplicate row so the file contains exactly the four frozen ids. + +## Suggestions + +1. **Fix `module_dependency_register.md` Epic 5 table layout** — The **E5.M1 note** paragraph sits between the E5.M1 and E5.M2 table rows, which breaks the markdown table. Move the note below the Epic 5 table (same pattern as other epic footnotes) so E5.M2–E5.M4 rows render in the table. + +2. **Align E5.M1 module Status with alignment register** — [`documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) lists E5.M1 as **In Progress**; [`E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) summary **Status** still reads **Planned**. Mirror the E3.M* pattern (module page status note when catalog lands). + +## Nits + +- Nit: Duplicate-id error message cites the same file twice (`… in content/abilities/prototype_abilities.json and content/abilities/prototype_abilities.json`) — acceptable; same-file duplicates are rare in normal authoring. +- Nit: [`NEO-76-implementation-plan.md`](../plans/NEO-76-implementation-plan.md) reconciliation and backlog checkboxes are marked complete while CI currently fails — update after catalog fix or note “pending catalog fix” until green. + +## Verification + +```bash +# From repo root (venv recommended) +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 ability catalog file(s) … 4 unique ability id(s) + +# Negative: duplicate id (should fail — already present until fixed) +# Remove duplicate row first, then re-run happy path above. + +# CI parity +# pr-gate.yml runs the same script after pip install -r scripts/requirements-content.txt +``` + +After fixing the catalog: confirm four unique ids match `PROTOTYPE_E5M1_ABILITY_IDS` and [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) allowlist (`prototype_pulse`, `prototype_guard`, `prototype_dash`, `prototype_burst`). From 866f96258313241144b7fc7743ffc25176a40395 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 24 May 2026 21:17:07 -0400 Subject: [PATCH 5/5] =?UTF-8?q?NEO-76:=20Fix=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20duplicate=20ability=20row=20and=20docs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove stray prototype_pulse from catalog; restore Epic 5 table layout, align E5.M1 In Progress status, and close out code review doc. --- content/abilities/prototype_abilities.json | 7 ----- .../modules/E5_M1_CombatRulesEngine.md | 2 +- .../modules/module_dependency_register.md | 6 ++-- docs/reviews/2026-05-24-NEO-76.md | 30 +++++++++---------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/content/abilities/prototype_abilities.json b/content/abilities/prototype_abilities.json index 89b7506..95aac81 100644 --- a/content/abilities/prototype_abilities.json +++ b/content/abilities/prototype_abilities.json @@ -28,13 +28,6 @@ "baseDamage": 40, "cooldownSeconds": 5.0, "abilityKind": "attack" - }, - { - "id": "prototype_pulse", - "displayName": "Prototype Pulse", - "baseDamage": 25, - "cooldownSeconds": 3.0, - "abilityKind": "attack" } ] } diff --git a/docs/decomposition/modules/E5_M1_CombatRulesEngine.md b/docs/decomposition/modules/E5_M1_CombatRulesEngine.md index c356281..8c20037 100644 --- a/docs/decomposition/modules/E5_M1_CombatRulesEngine.md +++ b/docs/decomposition/modules/E5_M1_CombatRulesEngine.md @@ -7,7 +7,7 @@ | **Module ID** | E5.M1 | | **Epic** | [Epic 5 — PvE Combat](../epics/epic_05_pve_combat.md) | | **Stage target** | Prototype | -| **Status** | Planned (see [dependency register](module_dependency_register.md)) | +| **Status** | In Progress — Slice 1 backlog [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md): E5M1-01 [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) ability catalog + CI landed; E5M1-02+ pending (see [dependency register](module_dependency_register.md)) | | **Linear** | Label **`E5.M1`** · [E5M1-prototype-backlog.md](../../plans/E5M1-prototype-backlog.md) **E5M1-01** [NEO-76](https://linear.app/neon-sprawl/issue/NEO-76) → **E5M1-12** [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86); gig XP **E5M1-10** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44) | ## Purpose diff --git a/docs/decomposition/modules/module_dependency_register.md b/docs/decomposition/modules/module_dependency_register.md index abdefd5..1a54104 100644 --- a/docs/decomposition/modules/module_dependency_register.md +++ b/docs/decomposition/modules/module_dependency_register.md @@ -71,13 +71,13 @@ Fleshed-out scope, contracts, and integration notes live in **per-module documen | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status | |---|---|---|---|---|---| -| E5.M1 | CombatRulesEngine | E1.M3, E2.M2 | CombatAction, CombatResolution, ThreatState | Prototype | Planned | - -**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`**. +| E5.M1 | CombatRulesEngine | E1.M3, E2.M2 | CombatAction, CombatResolution, ThreatState | Prototype | In Progress | | E5.M2 | NpcAiAndBehaviorProfiles | E5.M1 | NpcBehaviorDef, TelegraphEvent, AggroRule | Prototype | Planned | | E5.M3 | EncounterAndRewardTables | E5.M1, E3.M3, E7.M2 | EncounterDef, RewardTable, EncounterCompleteEvent | Prototype | Planned | | E5.M4 | GroupCombatScaling | E5.M3, E8.M1 | ScalingProfile, PartySizeModifier, CombatDifficultyBand | Pre-production | Planned | +**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)). + ### Epic 6 — PvP Security | Module ID | Module Name | Depends On | Contract Needed | Phase Required | Status | diff --git a/docs/reviews/2026-05-24-NEO-76.md b/docs/reviews/2026-05-24-NEO-76.md index 136d0b8..be154a9 100644 --- a/docs/reviews/2026-05-24-NEO-76.md +++ b/docs/reviews/2026-05-24-NEO-76.md @@ -2,26 +2,27 @@ **Date:** 2026-05-24 **Scope:** Branch `NEO-76-e5m1-prototype-backlog` · commits `510d711`–`9b11a52` vs `origin/main` -**Base:** `origin/main` +**Base:** `origin/main` +**Follow-up:** Code review findings addressed — duplicate catalog row removed; module register table + E5.M1 status aligned. ## Verdict -**Request changes** — schema, `validate_content.py` gates, and docs align with the adopted plan, but the shipped catalog contains a duplicate `prototype_pulse` row and **PR gate validation fails**. Fix the catalog (or remove the stray row) before merge. +**Approve with nits** — schema, `validate_content.py` gates, docs, and catalog now pass CI; ready for merge. ## Summary -The branch decomposes Epic 5 Slice 1 into the E5M1 backlog (NEO-76–NEO-86, NEO-44), adds kickoff + implementation plans, and lands content-only E5M1-01: `ability-def.schema.json`, `prototype_abilities.json`, and ability validation in `scripts/validate_content.py` following NEO-50/NEO-65 patterns. Documentation updates (E5.M1 freeze table, `content/README.md`, CT.M1, alignment register, epic backlog link) are thorough and match kickoff decisions. No server/C#/client code — appropriate for this story. Risk is low once the catalog is corrected; the duplicate-id gate works as designed and currently blocks CI. +The branch decomposes Epic 5 Slice 1 into the E5M1 backlog (NEO-76–NEO-86, NEO-44), adds kickoff + implementation plans, and lands content-only E5M1-01: `ability-def.schema.json`, `prototype_abilities.json`, and ability validation in `scripts/validate_content.py` following NEO-50/NEO-65 patterns. Documentation updates (E5.M1 freeze table, `content/README.md`, CT.M1, alignment register, epic backlog link) are thorough and match kickoff decisions. No server/C#/client code — appropriate for this story. ## Documentation checked | Document | Result | |----------|--------| -| [`docs/plans/NEO-76-implementation-plan.md`](../plans/NEO-76-implementation-plan.md) | **Partially matches** — kickoff table, schema shape, CI constant name, and file list match; reconciliation marks AC complete but catalog currently fails duplicate-id validation. | -| [`docs/plans/E5M1-prototype-backlog.md`](../plans/E5M1-prototype-backlog.md) · **E5M1-01** | **Partially matches** — E5M1-01 AC checked and landed note present; content file must pass CI before merge. | -| [`docs/decomposition/modules/E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) | **Partially matches** — freeze table, Linear slug table, and CI rules match plan; module **Status** row still **Planned** while alignment register says **In Progress**. | +| [`docs/plans/NEO-76-implementation-plan.md`](../plans/NEO-76-implementation-plan.md) | **Matches** — kickoff table, schema shape, CI constant name, file list, and reconciliation AC complete; catalog passes duplicate-id validation. | +| [`docs/plans/E5M1-prototype-backlog.md`](../plans/E5M1-prototype-backlog.md) · **E5M1-01** | **Matches** — E5M1-01 AC checked and landed note present; content passes CI. | +| [`docs/decomposition/modules/E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) | **Matches** — freeze table, Linear slug table, CI rules, and **Status** **In Progress** aligned with alignment register. | | [`docs/decomposition/modules/CT_M1_ContentValidationPipeline.md`](../decomposition/modules/CT_M1_ContentValidationPipeline.md) | **Matches** — ability catalog PR gate paragraph added. | | [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Matches** — E5.M1 row added **In Progress** / NEO-76 landed. Register update appropriate when work starts. | -| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Partially matches** — E5.M1 Linear note content is correct but inserted **inside** the Epic 5 table, breaking markdown table rendering. | +| [`docs/decomposition/modules/module_dependency_register.md`](../decomposition/modules/module_dependency_register.md) | **Matches** — E5.M1 Linear note below Epic 5 table; E5.M2–E5.M4 rows render correctly; E5.M1 **In Progress**. | | [`docs/decomposition/epics/epic_05_pve_combat.md`](../decomposition/epics/epic_05_pve_combat.md) | **Matches** — Slice 1 backlog link to E5M1-prototype-backlog.md. | | [`content/README.md`](../../content/README.md) | **Matches** — `abilities/` row + E5 Slice 1 freeze paragraph. | | [`docs/decomposition/modules/client_server_authority.md`](../decomposition/modules/client_server_authority.md) | **N/A** — no runtime authority changes; content-only. | @@ -29,18 +30,18 @@ The branch decomposes Epic 5 Slice 1 into the E5M1 backlog (NEO-76–NEO-86, NEO ## Blocking issues -1. **Duplicate `prototype_pulse` in `content/abilities/prototype_abilities.json`** — The catalog has **five** rows; `prototype_pulse` appears twice (lines 4–9 and 32–37). Running `python scripts/validate_content.py` exits non-zero with `duplicate ability id 'prototype_pulse'`. This violates E5M1-01 AC (“exactly four prototype ability ids; duplicate `id` fails CI”) and will fail `.github/workflows/pr-gate.yml`. Remove the trailing duplicate row so the file contains exactly the four frozen ids. +1. ~~**Duplicate `prototype_pulse` in `content/abilities/prototype_abilities.json`** — The catalog has **five** rows; `prototype_pulse` appears twice (lines 4–9 and 32–37). Running `python scripts/validate_content.py` exits non-zero with `duplicate ability id 'prototype_pulse'`. This violates E5M1-01 AC (“exactly four prototype ability ids; duplicate `id` fails CI”) and will fail `.github/workflows/pr-gate.yml`. Remove the trailing duplicate row so the file contains exactly the four frozen ids.~~ **Done.** Trailing duplicate row removed; `validate_content.py` reports **4 unique ability id(s)**. ## Suggestions -1. **Fix `module_dependency_register.md` Epic 5 table layout** — The **E5.M1 note** paragraph sits between the E5.M1 and E5.M2 table rows, which breaks the markdown table. Move the note below the Epic 5 table (same pattern as other epic footnotes) so E5.M2–E5.M4 rows render in the table. +1. ~~**Fix `module_dependency_register.md` Epic 5 table layout** — The **E5.M1 note** paragraph sits between the E5.M1 and E5.M2 table rows, which breaks the markdown table. Move the note below the Epic 5 table (same pattern as other epic footnotes) so E5.M2–E5.M4 rows render in the table.~~ **Done.** Note moved below full Epic 5 table; E5.M1 status **In Progress**. -2. **Align E5.M1 module Status with alignment register** — [`documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) lists E5.M1 as **In Progress**; [`E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) summary **Status** still reads **Planned**. Mirror the E3.M* pattern (module page status note when catalog lands). +2. ~~**Align E5.M1 module Status with alignment register** — [`documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) lists E5.M1 as **In Progress**; [`E5_M1_CombatRulesEngine.md`](../decomposition/modules/E5_M1_CombatRulesEngine.md) summary **Status** still reads **Planned**. Mirror the E3.M* pattern (module page status note when catalog lands).~~ **Done.** E5.M1 module **Status** row updated to **In Progress** with NEO-76 landed note. ## Nits -- Nit: Duplicate-id error message cites the same file twice (`… in content/abilities/prototype_abilities.json and content/abilities/prototype_abilities.json`) — acceptable; same-file duplicates are rare in normal authoring. -- Nit: [`NEO-76-implementation-plan.md`](../plans/NEO-76-implementation-plan.md) reconciliation and backlog checkboxes are marked complete while CI currently fails — update after catalog fix or note “pending catalog fix” until green. +- ~~Nit: Duplicate-id error message cites the same file twice (`… in content/abilities/prototype_abilities.json and content/abilities/prototype_abilities.json`) — acceptable; same-file duplicates are rare in normal authoring.~~ **Deferred** — acceptable per original review; no change. +- ~~Nit: [`NEO-76-implementation-plan.md`](../plans/NEO-76-implementation-plan.md) reconciliation and backlog checkboxes are marked complete while CI currently fails — update after catalog fix or note “pending catalog fix” until green.~~ **Done.** Catalog fix makes reconciliation accurate; CI green. ## Verification @@ -50,11 +51,8 @@ python3 -m venv .venv-content && .venv-content/bin/pip install -r scripts/requir .venv-content/bin/python scripts/validate_content.py # Expect: content OK … 1 ability catalog file(s) … 4 unique ability id(s) -# Negative: duplicate id (should fail — already present until fixed) -# Remove duplicate row first, then re-run happy path above. - # CI parity # pr-gate.yml runs the same script after pip install -r scripts/requirements-content.txt ``` -After fixing the catalog: confirm four unique ids match `PROTOTYPE_E5M1_ABILITY_IDS` and [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) allowlist (`prototype_pulse`, `prototype_guard`, `prototype_dash`, `prototype_burst`). +Four unique ids match `PROTOTYPE_E5M1_ABILITY_IDS` and [`PrototypeAbilityRegistry`](../../server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs) allowlist (`prototype_pulse`, `prototype_guard`, `prototype_dash`, `prototype_burst`).