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.pull/117/head
parent
157564e810
commit
902e03848c
|
|
@ -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.
|
||||
Loading…
Reference in New Issue