145 lines
10 KiB
Markdown
145 lines
10 KiB
Markdown
# 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 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 + cast→GET defeat spine; **`server/README.md`**, E5M1 backlog, E5_M1 module doc, dependency register, alignment table updated.
|