diff --git a/docs/plans/NEO-78-implementation-plan.md b/docs/plans/NEO-78-implementation-plan.md new file mode 100644 index 0000000..1d5fd85 --- /dev/null +++ b/docs/plans/NEO-78-implementation-plan.md @@ -0,0 +1,119 @@ +# NEO-78 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-78 | +| **Title** | E5M1-04: GET /game/world/ability-definitions | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-78/e5m1-04-get-gameworldability-definitions | +| **Module** | [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) · Epic 5 Slice 1 · backlog **E5M1-04** | +| **Branch** | `NEO-78-get-world-ability-definitions` | +| **Precursor** | [NEO-79](https://linear.app/neon-sprawl/issue/NEO-79) — `IAbilityDefinitionRegistry` + DI (**Done** on `main`) | +| **Pattern** | [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68) — `GET /game/world/recipe-definitions`; [NEO-53](https://linear.app/neon-sprawl/issue/NEO-53) — item definitions world GET | +| **Client counterpart** | None (server-only); optional client enrichment in [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) via this HTTP route | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **Manual QA doc** | Add `docs/manual-qa/NEO-78.md`? | **Yes** — NEO-68 pattern for world GET HTTP. | **User:** skip — manual QA only when there are client-facing changes; this story is server-only (Bruno + automated tests suffice). | +| **Optional `abilityKind`** | Emit when row has no kind? | **Omit property when null** — NEO-68 “loaded fields only”; all four prototype rows have a kind today. | **User:** omit when null. | + +No other blocking decisions — route path, envelope (`schemaVersion` **1**, **`abilities`** array), row fields, registry injection, and frozen four-id order are specified in [E5M1-04](E5M1-prototype-backlog.md#e5m1-04--get-gameworldability-definitions) and [E5.M1 freeze box](../decomposition/modules/E5_M1_CombatRulesEngine.md#prototype-slice-1-freeze-e5m1-01). + +## Goal, scope, and out-of-scope + +**Goal:** Expose a stable, read-only JSON endpoint that returns all four loaded prototype ability definitions for Bruno, tooling, and optional client preview ([NEO-85](https://linear.app/neon-sprawl/issue/NEO-85)) — backed by **`IAbilityDefinitionRegistry`** without duplicating catalog truth. + +**In scope (from Linear + [E5M1-04](E5M1-prototype-backlog.md#e5m1-04--get-gameworldability-definitions)):** + +- **`GET /game/world/ability-definitions`** with versioned envelope (`schemaVersion` **1**, **`abilities`** array). +- Row fields: **`id`**, **`displayName`**, **`baseDamage`**, **`cooldownSeconds`**, **`abilityKind`** (omit when null). +- Abilities ordered by **`id`** ordinal, matching **`GetDefinitionsInIdOrder()`**. +- Bruno folder `bruno/neon-sprawl-server/ability-definitions/`. +- `server/README.md` section. +- API integration tests (AAA). + +**Out of scope (from Linear + backlog):** + +- Godot HUD / client wiring ([NEO-85](https://linear.app/neon-sprawl/issue/NEO-85)). +- Combat resolution, damage application, cooldown commit ([NEO-80+](E5M1-prototype-backlog.md)). +- Per-player ability state or loadout mutation. +- `docs/manual-qa/NEO-78.md` (server-only; no client-facing changes per kickoff). + +## Acceptance criteria checklist + +- [ ] GET returns all **four** prototype abilities with **`schemaVersion`** **1** and stable field names (`id`, `displayName`, `baseDamage`, `cooldownSeconds`, `abilityKind` when present). +- [ ] Bruno happy path documents response shape and runs against dev server. +- [ ] Automated API tests (AAA) assert four ids in registry order and spot-check metadata. + +## Technical approach + +1. **Route:** **`GET /game/world/ability-definitions`** — parameterless read; inject **`IAbilityDefinitionRegistry`** only (not **`AbilityDefinitionCatalog`**). + +2. **Response shape:** Top-level **`schemaVersion`** (`AbilityDefinitionsListResponse.CurrentSchemaVersion` = **1**) plus **`abilities`** array. Each row maps from **`AbilityDefRow`**: **`id`**, **`displayName`**, **`baseDamage`**, **`cooldownSeconds`**, **`abilityKind`**. Use **`JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)`** on **`abilityKind`** so absent kinds are omitted from JSON. + +3. **Ordering:** Build list from **`registry.GetDefinitionsInIdOrder()`** (already ordinal by id). Tests assert exact id sequence (frozen four from [E5.M1 freeze box](../decomposition/modules/E5_M1_CombatRulesEngine.md#prototype-slice-1-freeze-e5m1-01)): + + `prototype_burst`, `prototype_dash`, `prototype_guard`, `prototype_pulse` + + (ordinal sort of the four ids — same order as **`AbilityDefinitionRegistryTests`** / catalog keys). + +4. **Implementation:** New **`AbilityDefinitionsWorldApi`** + **`AbilityDefinitionsListDtos.cs`** in `Game/Combat/` (mirror [`RecipeDefinitionsWorldApi`](../../server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionsWorldApi.cs)). Wire **`app.MapAbilityDefinitionsWorldApi()`** from **`Program.cs`** next to other world definition APIs (after **`MapRecipeDefinitionsWorldApi()`**). + +5. **Bruno:** `bruno/neon-sprawl-server/ability-definitions/` with **`Get ability definitions.bru`** + **`folder.bru`**. Tests: 200, JSON, **`schemaVersion` 1**, **`abilities.length === 4`**, ascending id order, **exact frozen-four registry sequence**, spot-check **`prototype_pulse`** (`baseDamage` 25, `cooldownSeconds` 3.0, `abilityKind` `attack`) and **`prototype_burst`** (`baseDamage` 40, `cooldownSeconds` 5.0, `abilityKind` `attack`). + +6. **Docs:** Update **`server/README.md`** ability-catalog section with GET + curl example. Update [E5_M1](E5_M1_CombatRulesEngine.md) **Related implementation slices** and [documentation_and_implementation_alignment.md](documentation_and_implementation_alignment.md) E5.M1 row when landed. + +### Expected prototype row values (from content) + +| `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` | + +## Files to add + +| Path | Purpose | +|------|---------| +| `server/NeonSprawl.Server/Game/Combat/AbilityDefinitionsWorldApi.cs` | `Map*` extension registering GET route; maps registry → JSON. | +| `server/NeonSprawl.Server/Game/Combat/AbilityDefinitionsListDtos.cs` | Versioned response + row DTOs for JSON serialization. | +| `server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema v1, four ids in order, spot-check rows. | +| `bruno/neon-sprawl-server/ability-definitions/Get ability definitions.bru` | Manual / Bruno verification against dev server. | +| `bruno/neon-sprawl-server/ability-definitions/folder.bru` | Bruno folder metadata (match `recipe-definitions`). | +| `docs/plans/NEO-78-implementation-plan.md` | This plan. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `server/NeonSprawl.Server/Program.cs` | Register `MapAbilityDefinitionsWorldApi()` alongside other game world definition APIs. | +| `server/README.md` | Document `GET /game/world/ability-definitions`, curl example, links to plan/Bruno. | +| `docs/decomposition/modules/E5_M1_CombatRulesEngine.md` | **Related implementation slices** — HTTP read model bullet (NEO-78). | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M1 row — note NEO-78 HTTP projection when landed. | + +## Tests + +| File | Coverage | +|------|----------| +| `server/NeonSprawl.Server.Tests/Game/Combat/AbilityDefinitionsWorldApiTests.cs` | **Integration:** `GET /game/world/ability-definitions` via **`InMemoryWebApplicationFactory`**; **`ReadFromJsonAsync`**; assert **`schemaVersion` 1**; **`abilities`** count **4**; ordered **`id`** list equals frozen four in ordinal id order; spot-check **`prototype_pulse`** and **`prototype_burst`** damage/cooldown/kind. **AAA** per [csharp-style](../../.cursor/rules/csharp-style.md). | + +Bruno scripts complement automated tests for manual dev-server verification. No `docs/manual-qa/NEO-78.md` (server-only per kickoff). + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|---------------------|--------| +| **Linear blockedBy NEO-79** | Branch from **`main`** where NEO-79 is merged (confirmed at kickoff; branch fast-forwarded). | **adopted** | +| **Optional fields later** | When content adds new optional ability-def fields, bump **`schemaVersion`** or extend v1 in a follow-up issue. | **deferred** | +| **Client not in this story** | Cross-link **NEO-85** in plan; Bruno is not prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md). | **adopted** | +| **`cooldownSeconds` JSON type** | Emit as JSON **number** (double) matching content schema; tests assert numeric equality. | **adopted** | + +## Decisions (kickoff) + +- **No manual QA doc** — server-only HTTP; Bruno + automated tests (user confirmed). +- **`abilityKind` omitted when null** — v1 contract emits loaded fields only (user confirmed). +- **Registry-only injection** — HTTP layer depends on **`IAbilityDefinitionRegistry`**, not **`AbilityDefinitionCatalog`**.