# 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

- [x] GET returns all **four** prototype abilities with **`schemaVersion`** **1** and stable field names (`id`, `displayName`, `baseDamage`, `cooldownSeconds`, `abilityKind` when present).
- [x] Bruno happy path documents response shape and runs against dev server.
- [x] 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`**.

## Reconciliation (implementation)

- **`AbilityDefinitionsWorldApi`** + **`AbilityDefinitionsListDtos`** registered via **`MapAbilityDefinitionsWorldApi()`** in **`Program.cs`**; injects **`IAbilityDefinitionRegistry`** only.
- **`AbilityDefinitionsWorldApiTests`** — integration test for schema v1, frozen-four id order, **`prototype_pulse`** / **`prototype_burst`** spot-checks.
- **Bruno:** `bruno/neon-sprawl-server/ability-definitions/Get ability definitions.bru` with response-shape tests.
- **`server/README.md`**, **E5.M1** module doc, **alignment register**, and **E5M1 backlog** updated.
