# NEO-83 — Implementation plan

## Story reference

| Field | Value |
|--------|--------|
| **Key** | NEO-83 |
| **Title** | E5M1-08: GET /game/world/combat-targets snapshot |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-83/e5m1-08-get-gameworldcombat-targets-snapshot |
| **Module** | [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) · Epic 5 Slice 1 · backlog **E5M1-08** |
| **Branch** | `NEO-83-combat-targets-snapshot` |
| **Precursor** | [NEO-80](https://linear.app/neon-sprawl/issue/NEO-80) — `ICombatEntityHealthStore` (**Done** on `main`); [NEO-82](https://linear.app/neon-sprawl/issue/NEO-82) — cast POST + `combatResolution` (**Done** on `main`) |
| **Pattern** | [NEO-78](https://linear.app/neon-sprawl/issue/NEO-78) — `GET /game/world/ability-definitions`; read-only world snapshot backed by injectable store |
| **Blocks** | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) client HUD (polls this route) |
| **Client counterpart** | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) — Godot combat targets client + HP HUD (**out of scope** here; no client-facing change in this story) |

## Kickoff clarifications

| Topic | Question | Agent recommendation | Answer |
|--------|----------|----------------------|--------|
| **Manual QA doc** | Add `docs/manual-qa/NEO-83.md`? | **Skip** — server-only; no Godot change; Bruno + API tests satisfy AC (NEO-78/NEO-82 pattern). | **User:** skip. |

**No other blocking decisions** — route path (`GET /game/world/combat-targets`), envelope (`schemaVersion` **1**, **`targets`** array), row fields (`targetId`, `maxHp`, `currentHp`, `defeated`), lazy init for both registry dummies on read, ascending id order (alpha → beta), and store injection are specified in [E5M1-08](E5M1-prototype-backlog.md#e5m1-08--get-gameworldcombat-targets-snapshot) and [E5.M1 kickoff defaults](E5M1-prototype-backlog.md#kickoff-decisions-decomposition-defaults).

## Goal, scope, and out-of-scope

**Goal:** Expose a stable, read-only JSON snapshot of prototype combat dummy HP so clients and tooling can display authoritative target health without local damage math — backed by **`ICombatEntityHealthStore`** (same source of truth as cast resolution).

**In scope (from Linear + [E5M1-08](E5M1-prototype-backlog.md#e5m1-08--get-gameworldcombat-targets-snapshot)):**

- **`GET /game/world/combat-targets`** → versioned list (`schemaVersion` **1**, **`targets`** array).
- Each row: **`targetId`**, **`maxHp`**, **`currentHp`**, **`defeated`** — mapped from **`CombatEntityHealthSnapshot`**.
- Lazy-init **`prototype_target_alpha`** and **`prototype_target_beta`** on first read via **`TryGet`** (same policy as NEO-80).
- Targets ordered by **`targetId`** ascending (alpha before beta).
- API integration tests (AAA): fresh snapshot, post-cast damage reflection, defeated after lethal sequence.
- Bruno folder `bruno/neon-sprawl-server/combat-targets/` — happy GET + cast→GET defeat spine.
- **`server/README.md`** combat-targets section; cross-link NEO-80 health store + NEO-82 cast route.

**Out of scope (from Linear + backlog):**

- Godot wiring — **[NEO-85](https://linear.app/neon-sprawl/issue/NEO-85)**.
- Per-target query filters, player HP, persistence, NPC spawn (E5.M2).
- **`TryResetToFull`** over HTTP (tests/fixtures only).
- **`docs/manual-qa/NEO-83.md`** (kickoff decision).

## Acceptance criteria checklist

- [x] GET reflects damage applied via cast route without client-side HP math.
- [x] Defeated flag true after lethal cast sequence in Bruno.

## Technical approach

1. **Route:** **`GET /game/world/combat-targets`** — parameterless read; inject **`ICombatEntityHealthStore`** only.

2. **Target enumeration:** Add **`PrototypeTargetRegistry.GetPrototypeTargetIdsInOrder()`** (or equivalent static ordered list) returning **`prototype_target_alpha`**, **`prototype_target_beta`** in ordinal id order — mirrors **`GetDefinitionsInIdOrder()`** pattern and avoids hardcoding ids in the API handler.

3. **Handler flow (`CombatTargetsWorldApi`):**
   - For each registry id in order, call **`healthStore.TryGet(id, out snapshot)`** (lazy-init on first access).
   - Map each snapshot to **`CombatTargetJson`** (`targetId`, `maxHp`, `currentHp`, `defeated`).
   - Return **`Results.Json(new CombatTargetsListResponse { SchemaVersion = 1, Targets = … })`**.

4. **Wire DTOs (`CombatTargetsListDtos.cs`):**
   - **`CombatTargetsListResponse`** — `schemaVersion` (const **1**), `targets` array.
   - **`CombatTargetJson`** — camelCase JSON property names matching backlog + existing cast/combat field vocabulary.

5. **DI / routing:** Wire **`app.MapCombatTargetsWorldApi()`** from **`Program.cs`** next to **`MapAbilityDefinitionsWorldApi()`** (after ability definitions world GET).

6. **Integration tests (`CombatTargetsWorldApiTests.cs`):**
   - **Fresh GET:** both targets at **`maxHp` 100**, **`currentHp` 100**, **`defeated` false**; ascending id order.
   - **After one pulse cast on alpha:** GET shows alpha **`currentHp` 75**, beta still 100; alpha **`defeated` false**.
   - **After four pulse casts on alpha:** GET shows alpha **`currentHp` 0**, **`defeated` true**; beta unchanged.
   - Reuse cast setup helpers from **`AbilityCastApiTests`** pattern (bind slot 0 pulse, lock alpha, teleport in range).

7. **Bruno (`bruno/neon-sprawl-server/combat-targets/`):**
   - **`Get combat targets.bru`** — assert 200, **`schemaVersion` 1**, **`targets.length === 2`**, ascending ids, both at full HP on fresh server.
   - **`Get combat targets after one cast.bru`** — pre-request: move + hotbar + lock alpha + one pulse cast; GET asserts alpha **`currentHp` 75**.
   - **`Get combat targets after cast spine.bru`** — pre-request: four pulse casts + defeat; GET asserts alpha **`defeated` true** (AC spine).
   - **`folder.bru`** — run order note; cross-link ability-cast folder.

8. **Docs:** Add **`server/README.md`** **`GET /game/world/combat-targets`** section with field table + curl example; update NEO-80 health store row (“HTTP read” no longer pending). Reconcile [E5M1-prototype-backlog.md](E5M1-prototype-backlog.md) E5M1-08 checkboxes and [E5_M1_CombatRulesEngine.md](../decomposition/modules/E5_M1_CombatRulesEngine.md) when landed.

### Expected snapshot values (prototype_pulse → alpha, fresh server)

| Step | `prototype_target_alpha` | `prototype_target_beta` |
|------|--------------------------|-------------------------|
| Fresh GET (no casts) | 100 / 100, `defeated: false` | 100 / 100, `defeated: false` |
| After 1× pulse on alpha | 75 / 100, `defeated: false` | 100 / 100, `defeated: false` |
| After 4× pulse on alpha | 0 / 100, `defeated: true` | 100 / 100, `defeated: false` |

(`currentHp` / `maxHp`)

## Files to add

| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Combat/CombatTargetsWorldApi.cs` | Maps **`GET /game/world/combat-targets`**. |
| `server/NeonSprawl.Server/Game/Combat/CombatTargetsListDtos.cs` | Response envelope + row JSON types. |
| `server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetsWorldApiTests.cs` | AAA integration tests: fresh, post-cast, defeat. |
| `bruno/neon-sprawl-server/combat-targets/Get combat targets.bru` | Bruno happy path. |
| `bruno/neon-sprawl-server/combat-targets/Get combat targets after one cast.bru` | Fast one-pulse cast→GET smoke (code review follow-up). |
| `bruno/neon-sprawl-server/combat-targets/Get combat targets after cast spine.bru` | Bruno cast → GET damage + defeat AC spine. |
| `bruno/neon-sprawl-server/combat-targets/folder.bru` | Folder docs + run order. |
| `docs/plans/NEO-83-implementation-plan.md` | This plan. |

## Files to modify

| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Game/Targeting/PrototypeTargetRegistry.cs` | Add **`GetPrototypeTargetIdsInOrder()`** for stable enumeration in GET handler + tests. |
| `server/NeonSprawl.Server/Program.cs` | Register **`MapCombatTargetsWorldApi()`** after ability definitions world GET. |
| `server/README.md` | Document combat-targets GET, field table, curl; update NEO-80 “HTTP read” row. |
| `docs/plans/E5M1-prototype-backlog.md` | Mark E5M1-08 landed when complete. |
| `docs/decomposition/modules/E5_M1_CombatRulesEngine.md` | Update status + related implementation slice for NEO-83. |
| `docs/decomposition/modules/module_dependency_register.md` | Extend E5.M1 note with NEO-83 landed summary. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M1 row cites combat-targets GET. |

## Tests

| File | Coverage |
|------|----------|
| `server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetsWorldApiTests.cs` | **Fresh GET:** schema v1, two targets, alpha/beta ids ascending, full HP, not defeated. **Post-cast:** bind pulse, lock alpha, one cast → GET alpha 75 HP. **Defeat chain:** four casts → GET alpha `currentHp` 0, `defeated` true; beta still full. **AAA** per [csharp-style](../../.cursor/rules/csharp-style.md). |

Bruno spine covers manual HTTP verification; no **`docs/manual-qa/NEO-83.md`** (server-only, no Godot).

## Open questions / risks

| Question / risk | Agent recommendation | Status |
|-----------------|---------------------|--------|
| **Manual QA doc** | **Skip** (user kickoff). | **adopted** |
| **Array property name (`targets` vs `combatTargets`)** | **`targets`** — matches `abilities` / `recipes` world GET naming. | **adopted** |
| **Always return both registry dummies** | **Yes** — lazy-init both on GET per E5M1-08 backlog. | **adopted** |
| **Shared factory with cast tests** | **Same `InMemoryWebApplicationFactory` instance** for cast→GET integration tests — verifies store singleton coherence. | **adopted** |
| **Bruno cooldown waits on defeat spine** | **Reuse NEO-82 pattern** (~3.2s between pulse casts in pre-request when asserting four-pulse defeat). | **adopted** |

## Decisions (kickoff)

- No manual QA doc; Bruno + README + API tests sufficient (server-only — NEO-85 owns Godot HUD).
- Wire envelope: **`schemaVersion` 1**, **`targets`** array with **`targetId`**, **`maxHp`**, **`currentHp`**, **`defeated`**.
- Always return both prototype registry targets in ascending id order; lazy-init via **`TryGet`** on read.

## Reconciliation (implementation)

- **`CombatTargetsWorldApi`** + **`CombatTargetsListDtos`** in **`Game/Combat/`**; **`PrototypeTargetRegistry.GetPrototypeTargetIdsInOrder()`**.
- **3** AAA integration tests in **`CombatTargetsWorldApiTests`** (fresh GET, post-cast damage, four-pulse defeat).
- Bruno happy GET + one-pulse cast→GET smoke + four-pulse defeat spine; **`server/README.md`**, E5M1 backlog, E5_M1 module doc, dependency register, alignment table updated.
- Code review follow-up: **`TryGet` miss throws** instead of silent skip; one-pulse test asserts cast **`combatResolution`** coherence.
