From 902e03848ce73ff2afa9d3fa0c044ef06ee22dc4 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Mon, 25 May 2026 11:18:41 -0400 Subject: [PATCH] NEO-83: Add implementation plan for combat-targets GET snapshot. Kickoff clarifications recorded; server-only scope with Bruno and API test coverage for authoritative HP reads. --- docs/plans/NEO-83-implementation-plan.md | 137 +++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/plans/NEO-83-implementation-plan.md diff --git a/docs/plans/NEO-83-implementation-plan.md b/docs/plans/NEO-83-implementation-plan.md new file mode 100644 index 0000000..17cd6e3 --- /dev/null +++ b/docs/plans/NEO-83-implementation-plan.md @@ -0,0 +1,137 @@ +# 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 + +- [ ] GET reflects damage applied via cast route without client-side HP math. +- [ ] 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 cast spine.bru`** — pre-request: move + hotbar + lock alpha + one pulse cast; GET asserts alpha **`currentHp` 75**; optional extended pre-request for four casts + **`defeated` true** on alpha (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 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.