11 KiB
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 · Epic 5 Slice 1 · backlog E5M1-08 |
| Branch | NEO-83-combat-targets-snapshot |
| Precursor | NEO-80 — ICombatEntityHealthStore (Done on main); NEO-82 — cast POST + combatResolution (Done on main) |
| Pattern | NEO-78 — GET /game/world/ability-definitions; read-only world snapshot backed by injectable store |
| Blocks | NEO-85 client HUD (polls this route) |
| Client counterpart | 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 and E5.M1 kickoff 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):
GET /game/world/combat-targets→ versioned list (schemaVersion1,targetsarray).- Each row:
targetId,maxHp,currentHp,defeated— mapped fromCombatEntityHealthSnapshot. - Lazy-init
prototype_target_alphaandprototype_target_betaon first read viaTryGet(same policy as NEO-80). - Targets ordered by
targetIdascending (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.mdcombat-targets section; cross-link NEO-80 health store + NEO-82 cast route.
Out of scope (from Linear + backlog):
- Godot wiring — NEO-85.
- Per-target query filters, player HP, persistence, NPC spawn (E5.M2).
TryResetToFullover 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
-
Route:
GET /game/world/combat-targets— parameterless read; injectICombatEntityHealthStoreonly. -
Target enumeration: Add
PrototypeTargetRegistry.GetPrototypeTargetIdsInOrder()(or equivalent static ordered list) returningprototype_target_alpha,prototype_target_betain ordinal id order — mirrorsGetDefinitionsInIdOrder()pattern and avoids hardcoding ids in the API handler. -
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 = … }).
- For each registry id in order, call
-
Wire DTOs (
CombatTargetsListDtos.cs):CombatTargetsListResponse—schemaVersion(const 1),targetsarray.CombatTargetJson— camelCase JSON property names matching backlog + existing cast/combat field vocabulary.
-
DI / routing: Wire
app.MapCombatTargetsWorldApi()fromProgram.csnext toMapAbilityDefinitionsWorldApi()(after ability definitions world GET). -
Integration tests (
CombatTargetsWorldApiTests.cs):- Fresh GET: both targets at
maxHp100,currentHp100,defeatedfalse; ascending id order. - After one pulse cast on alpha: GET shows alpha
currentHp75, beta still 100; alphadefeatedfalse. - After four pulse casts on alpha: GET shows alpha
currentHp0,defeatedtrue; beta unchanged. - Reuse cast setup helpers from
AbilityCastApiTestspattern (bind slot 0 pulse, lock alpha, teleport in range).
- Fresh GET: both targets at
-
Bruno (
bruno/neon-sprawl-server/combat-targets/):Get combat targets.bru— assert 200,schemaVersion1,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 alphacurrentHp75.Get combat targets after cast spine.bru— pre-request: four pulse casts + defeat; GET asserts alphadefeatedtrue (AC spine).folder.bru— run order note; cross-link ability-cast folder.
-
Docs: Add
server/README.mdGET /game/world/combat-targetssection with field table + curl example; update NEO-80 health store row (“HTTP read” no longer pending). Reconcile E5M1-prototype-backlog.md E5M1-08 checkboxes and 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. |
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:
schemaVersion1,targetsarray withtargetId,maxHp,currentHp,defeated. - Always return both prototype registry targets in ascending id order; lazy-init via
TryGeton read.
Reconciliation (implementation)
CombatTargetsWorldApi+CombatTargetsListDtosinGame/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:
TryGetmiss throws instead of silent skip; one-pulse test asserts castcombatResolutioncoherence.