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.pull/120/head
parent
b65bff2a36
commit
f03e689ce9
|
|
@ -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** |
|
||||
Loading…
Reference in New Issue