From f03e689ce980fb357449e33d90cd463060c467c6 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Mon, 25 May 2026 13:13:53 -0400 Subject: [PATCH] NEO-85: Add kickoff implementation plan for client combat HUD. Documents event-driven combat-target refresh, CastFeedbackLabel resolution copy, and dedicated CombatTargetHpLabel per kickoff clarifications. --- docs/plans/NEO-85-implementation-plan.md | 142 +++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 docs/plans/NEO-85-implementation-plan.md diff --git a/docs/plans/NEO-85-implementation-plan.md b/docs/plans/NEO-85-implementation-plan.md new file mode 100644 index 0000000..7b0170d --- /dev/null +++ b/docs/plans/NEO-85-implementation-plan.md @@ -0,0 +1,142 @@ +# NEO-85 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-85 | +| **Title** | E5M1-11: Client combat feedback + target HP HUD | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-85/e5m1-11-client-combat-feedback-target-hp-hud | +| **Module** | [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) · Epic 5 Slice 1 · backlog **E5M1-11** | +| **Branch** | `NEO-85-client-combat-feedback-hp-hud` | +| **Server deps** | [NEO-82](https://linear.app/neon-sprawl/issue/NEO-82) — cast POST + nested **`combatResolution`** (**Done** on `main`); [NEO-83](https://linear.app/neon-sprawl/issue/NEO-83) — **`GET /game/world/combat-targets`** (**Done** on `main`) | +| **Pattern** | [NEO-28](https://linear.app/neon-sprawl/issue/NEO-28) / [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — thin HTTP clients, `main.gd` HUD wiring, GdUnit mock transport | +| **Downstream** | [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86) capstone playable tab-target combat loop (manual QA script) | +| **Server counterpart** | [NEO-82](https://linear.app/neon-sprawl/issue/NEO-82) + [NEO-83](https://linear.app/neon-sprawl/issue/NEO-83) — this issue is the **client** half; Bruno-only verification is not prototype-complete per [full-stack epic decomposition](../../.cursor/rules/full-stack-epic-decomposition.md) | + +## Kickoff clarifications + +| Topic | Question | Agent recommendation | Answer | +|--------|----------|----------------------|--------| +| **HP refresh** | Event-driven vs periodic poll for **`GET /game/world/combat-targets`**? | **Event-driven only** — refresh after successful cast accept + on target lock change; matches cooldown sync pattern and minimizes HTTP chatter ([E5M1-11 backlog](E5M1-prototype-backlog.md#e5m1-11--client-combat-feedback--target-hp-hud)). | **User:** event-driven only. | +| **Cast feedback surface** | Extend **`CastFeedbackLabel`** vs dedicated combat line? | **Extend `CastFeedbackLabel`** with resolution copy on accept — NEO-28 precedent; keep HUD compact. | **User:** extend `CastFeedbackLabel`. | +| **HP label placement** | Dedicated label vs append to **`TargetLockLabel`**? | **New `CombatTargetHpLabel`** below **`CastFeedbackLabel`** — AC requires readable HP readout; **`TargetLockLabel`** is already dense lock/range debug. | **User:** dedicated `CombatTargetHpLabel`. | + +No other blocking decisions — wire field names follow NEO-82 **`combatResolution`** / NEO-83 **`targets`** row vocabulary; zero-damage accepts show **`0 dmg`** from server fields (no local combat math). + +## Goal, scope, and out-of-scope + +**Goal:** Godot displays authoritative cast outcomes and locked dummy HP without local combat math — player-visible damage/defeat feedback and an HP label that matches the server snapshot after casts. + +**In scope (from Linear + [E5M1-11](E5M1-prototype-backlog.md#e5m1-11--client-combat-feedback--target-hp-hud)):** + +- Extend **`ability_cast_client.gd`** to parse nested **`combatResolution`** on accept and surface it on **`cast_result_received`**. +- Update **`CastFeedbackLabel`** with damage / defeat messaging on accept (e.g. **`Cast: 25 dmg → 75 HP`**, **`Cast: target defeated!`** when **`targetDefeated`**). +- **`combat_targets_client.gd`** — **`GET /game/world/combat-targets`**; cache snapshot; **`target_row(target_id)`** helper. +- **`CombatTargetHpLabel`** — shows locked target **`currentHp/maxHp`** (+ **`defeated`** suffix); **`—`** when no lock. +- **`main.gd` wiring:** event-driven combat-target refresh on successful cast + **`target_state_changed`**; boot hydrate when first lock arrives. +- GdUnit tests with HTTP doubles ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)). +- **`docs/manual-qa/NEO-85.md`** — Godot steps (server + client running). +- **`client/README.md`** combat HUD section. + +**Out of scope (from Linear + backlog):** + +- Final combat VFX, floating damage numbers art pass, player HP bar. +- Gig XP HUD row — optional in [NEO-86](https://linear.app/neon-sprawl/issue/NEO-86) capstone. +- Periodic HP polling, local damage prediction, server changes. + +## Acceptance criteria checklist + +- [ ] Player sees damage/defeat feedback in HUD after digit-key cast (not Output-only). +- [ ] Target HP label matches server snapshot after cast sequence. +- [ ] Manual QA exercisable without Bruno. + +## Technical approach + +1. **`ability_cast_client.gd` (NEO-85)** + - Parse optional **`combatResolution`** dict when **`accepted: true`** (`abilityId`, `targetId`, `damageDealt`, `targetRemainingHp`, `targetDefeated`). + - Extend signal: **`cast_result_received(accepted, reason_code, resolution)`** — **`resolution`** empty **`Dictionary`** on deny or missing block. + - Add static **`parse_cast_response_json(text) -> Dictionary`** for tests (returns `{accepted, reasonCode, resolution}`). + +2. **`combat_targets_client.gd` (new)** + - Mirror **`interactables_catalog_client.gd`** / **`skill_progression_client.gd`**: injectable HTTP, **`_busy`** guard. + - **`GET {base}/game/world/combat-targets`** — no player id segment (world route). + - **`parse_targets_json(text)`** static: require **`schemaVersion` 1**, **`targets`** array. + - Signals: **`targets_received(snapshot: Dictionary)`**, **`targets_sync_failed(reason: String)`**. + - **`request_sync_from_server()`** public entry. + - **`target_row(target_id: String, snapshot: Dictionary = {}) -> Dictionary`** — find row by **`targetId`**; uses cached last snapshot when second arg omitted. + +3. **Cast feedback copy (`main.gd` → `_on_cast_result_received`)** + - **Deny:** unchanged NEO-28 lines (`ability_cast_denied: {reasonCode}`). + - **Accept + resolution present:** + - If **`targetDefeated`:** **`Cast: {damageDealt} dmg → 0 HP — defeated!`** + - Else: **`Cast: {damageDealt} dmg → {targetRemainingHp} HP`** + - **Accept + empty resolution:** fallback **`Cast: accepted`** (defensive; should not occur post-NEO-82). + - Keep cooldown sync on accept (existing NEO-32 path). + +4. **Combat target HP HUD (`main.gd`)** + - Cache **`_combat_targets_snapshot: Dictionary`** on **`targets_received`**. + - **`_render_combat_target_hp_label()`** — read **`_last_target_state.lockedTargetId`**; lookup row; format **`Target HP: {id} {current}/{max}`** + **` (defeated)`** when **`defeated`**; **`Target HP: — (no lock)`** when unlocked. + - **Refresh triggers (event-driven, kickoff decision):** + - **`target_state_changed`** → **`combat_targets_client.request_sync_from_server()`** when lock id changes or validity transitions to a locked id. + - **`cast_result_received`** when **`accepted`** → request combat-target sync (authoritative HP after cast; resolution fields give immediate cast feedback, GET reconciles label). + - Re-render HP label after snapshot received and when target state changes (even if GET still in flight, show last cached row for current lock). + +5. **Scene layout (`main.tscn`)** + - Add **`CombatTargetHpLabel`** between **`CastFeedbackLabel`** and **`GatherFeedbackLabel`** (~offset_top 374, shift **`GatherFeedbackLabel`** / **`CraftFeedbackLabel`** / economy block down ~22px if needed for non-overlap). + - Style: match existing left HUD labels (outline, font_size 15, warm tint distinct from cast line). + +6. **Client boot wiring (`main.gd`)** + - Instantiate **`CombatTargetsClient`** in hotbar/cast setup block; apply authority **`base_url`** (world GET — no **`dev_player_id`**). + - Connect **`targets_received`** → cache + **`_render_combat_target_hp_label`**. + - Update **`cast_result_received`** connection signature for third **`resolution`** arg. + +7. **Tests (GdUnit, AAA)** + - **`ability_cast_client_test.gd`:** extend — accept body with **`combatResolution`** → signal carries resolution dict; deny → empty resolution. + - **`combat_targets_client_test.gd`:** mock GET 200 two-target snapshot → **`target_row("prototype_target_alpha")`** fields; schema mismatch → failure signal. + - **`combat_feedback_refresh_test.gd`:** harness — cast accept signal triggers **`combat_targets_client.request_sync_from_server`** (spy/mock); target lock change triggers sync (mirrors **`craft_feedback_refresh_test.gd`** / **`gather_feedback_refresh_test.gd`**). + +8. **Docs on land** + - **`client/README.md`:** combat HUD subsection — cast resolution copy, HP label, event-driven GET refresh, dev fixture reset note (`POST /game/__dev/combat-targets-fixture` when enabled). + - **`docs/manual-qa/NEO-85.md`:** Tab lock alpha → bind slot 0 pulse → cast until defeat; verify **`CastFeedbackLabel`** + **`CombatTargetHpLabel`**; optional fixture reset between runs. + - Reconcile [E5M1-prototype-backlog.md](E5M1-prototype-backlog.md) E5M1-11 checkboxes, [E5_M1_CombatRulesEngine.md](../decomposition/modules/E5_M1_CombatRulesEngine.md), [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md) when implementation completes. + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/plans/NEO-85-implementation-plan.md` | This plan. | +| `client/scripts/combat_targets_client.gd` | GET world combat-targets snapshot; parse v1; **`targets_received`** / **`targets_sync_failed`**; **`target_row`**. | +| `client/scripts/combat_targets_client.gd.uid` | Godot uid companion (tracked). | +| `client/test/combat_targets_client_test.gd` | GdUnit: parse two-target snapshot; **`target_row`** lookup; HTTP error path. AAA layout. | +| `client/test/combat_feedback_refresh_test.gd` | GdUnit: cast accept + target lock change invoke combat-target sync (spy harness). AAA layout. | +| `docs/manual-qa/NEO-85.md` | Manual QA checklist — Godot combat feedback + HP HUD without Bruno. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scripts/ability_cast_client.gd` | Parse **`combatResolution`**; extend **`cast_result_received`** with resolution dict; static parse helper for tests. | +| `client/test/ability_cast_client_test.gd` | Cover resolution parsing + extended signal payload on accept/deny. | +| `client/scripts/main.gd` | Wire **`CombatTargetsClient`**; combat HP label render; cast feedback copy with resolution; event-driven refresh hooks. | +| `client/scenes/main.tscn` | Add **`CombatTargetHpLabel`** node; adjust downstream label offsets if needed. | +| `client/README.md` | Document combat HUD (cast resolution feedback + target HP label + refresh triggers). | + +## Tests + +| Path | Change | +|------|--------| +| `client/test/ability_cast_client_test.gd` | **Add/extend:** accept JSON with **`combatResolution`** → signal third arg has **`damageDealt`**, **`targetRemainingHp`**, **`targetDefeated`**; deny → empty resolution. | +| `client/test/combat_targets_client_test.gd` | **Add:** GET parse v1; alpha/beta rows; 404/invalid schema failure signal. | +| `client/test/combat_feedback_refresh_test.gd` | **Add:** successful cast accept triggers combat-target sync; target lock change triggers sync. | + +**Manual verification:** [`docs/manual-qa/NEO-85.md`](manual-qa/NEO-85.md) — server + Godot; Tab lock **`prototype_target_alpha`**, digit cast **`prototype_pulse`**, observe stepping HP on label and defeat copy on 4th hit; **`target_defeated`** deny on 5th preserves HUD state. + +## Open questions / risks + +| Question or risk | Agent recommendation | Status | +|------------------|----------------------|--------| +| **Signal signature break** | Extend **`cast_result_received`** with third arg (empty dict on deny) — update **`main.gd`** + tests in same commit. | **adopted** | +| **HP label stale during in-flight GET** | Show last cached snapshot for locked id immediately after cast feedback; refresh when GET completes. | **adopted** | +| **Layout shift in `main.tscn`** | Insert HP label between cast and gather lines; bump gather/craft offsets minimally. | **adopted** | +| **Dev fixture reset for QA** | Document optional **`POST /game/__dev/combat-targets-fixture`** in manual QA when Development host enabled (NEO-83 README). | **adopted** |