neon-sprawl/docs/plans/NEO-83-implementation-plan.md

11 KiB
Raw Permalink Blame History

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-80ICombatEntityHealthStore (Done on main); NEO-82 — cast POST + combatResolution (Done on main)
Pattern NEO-78GET /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 (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.
  • 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):

    • CombatTargetsListResponseschemaVersion (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-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: 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.