From b0d0c4262e30c9de6f318eb5c22c9526ee8a34ed Mon Sep 17 00:00:00 2001 From: VinPropane Date: Fri, 29 May 2026 20:56:34 -0400 Subject: [PATCH] NEO-97: add E5M2-11 client telegraph HUD implementation plan Kickoff plan for Godot poll clients, combat-active HUD labels, and manual QA; records kickoff decisions on poll gating, HUD scope, and windup interpolation. --- docs/plans/NEO-97-implementation-plan.md | 176 +++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 docs/plans/NEO-97-implementation-plan.md diff --git a/docs/plans/NEO-97-implementation-plan.md b/docs/plans/NEO-97-implementation-plan.md new file mode 100644 index 0000000..9083eca --- /dev/null +++ b/docs/plans/NEO-97-implementation-plan.md @@ -0,0 +1,176 @@ +# NEO-97 — Implementation plan + +## Story reference + +| Field | Value | +|--------|--------| +| **Key** | NEO-97 | +| **Title** | E5M2-11: Client telegraph HUD + NPC archetype markers | +| **Linear** | https://linear.app/neon-sprawl/issue/NEO-97/e5m2-11-client-telegraph-hud-npc-archetype-markers | +| **Module** | [E5.M2 — NpcAiAndBehaviorProfiles](../decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md) · Epic 5 Slice 2 · backlog **E5M2-11** | +| **Branch** | `NEO-97-e5m2-11-client-telegraph-hud` | +| **Server deps** | [NEO-94](https://linear.app/neon-sprawl/issue/NEO-94) — **`GET /game/world/npc-runtime-snapshot`** (**Done**); [NEO-95](https://linear.app/neon-sprawl/issue/NEO-95) — **`GET /game/players/{id}/combat-health`** (**Done**) | +| **Pattern** | [NEO-85](https://linear.app/neon-sprawl/issue/NEO-85) / [NEO-32](https://linear.app/neon-sprawl/issue/NEO-32) — thin HTTP clients, `main.gd` HUD wiring, GdUnit mock transport, optional local tick-down between polls | +| **Downstream** | [NEO-98](https://linear.app/neon-sprawl/issue/NEO-98) — playable NPC telegraph combat capstone (manual QA script) | +| **Server counterpart** | [NEO-94](https://linear.app/neon-sprawl/issue/NEO-94) + [NEO-95](https://linear.app/neon-sprawl/issue/NEO-95) — 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 | +|--------|----------|----------------------|--------| +| **Poll gating** | When does ~1 Hz polling run? | **When lock held OR player is `aggroHolderPlayerId` on any NPC** — combat HUD visible without requiring lock on the attacking NPC ([NEO-95](https://linear.app/neon-sprawl/issue/NEO-95) incoming telegraph/HP feedback). | **User:** combat-active gate. | +| **HUD scope** | Which NPC row(s) for `TelegraphLabel` + `NpcStateLabel`? | **Locked NPC + incoming threat** — primary row for locked target; when player is aggro holder on a different NPC, show that NPC’s state/telegraph as incoming threat. | **User:** locked + incoming. | +| **Windup display** | Raw poll value vs local tick-down? | **Server value at poll + local tick-down between polls** — `cooldown_state.gd` pattern; AC “within one poll frame” satisfied at poll boundaries. | **User:** interpolate. | +| **Markers script name** | Backlog `npc_archetype_markers.gd` vs existing `prototype_target_markers.gd`? | **Keep `prototype_target_markers.gd`** — already wired in `main.tscn` as `World/PrototypeTargetMarkers`; NEO-24/NEO-91 precedent. | **Adopted** (repo precedent; not asked). | +| **Constants / tab cycle** | Migrate `prototype_target_constants.gd` to three NPC ids? | **Already landed on `main`** (NEO-91) — verify parity with `PrototypeNpcRegistry.GetInstanceIdsInOrder()`; no duplicate migration. | **Adopted** (already on `main`). | +| **Manual QA doc** | `docs/manual-qa/NEO-97.md`? | **Yes** — player-visible client story; E5M2-11 AC + NEO-85 precedent. | **Adopted** (Linear AC + backlog). | + +## Goal, scope, and out-of-scope + +**Goal:** Godot shows **server-owned** telegraph timing, NPC runtime state, player combat HP, and three archetype world markers — driven by **`npc-runtime-snapshot`** + **`combat-health`** poll, completing the client half of Epic 5 Slice 2 combat feedback. + +**In scope (from Linear + [E5M2-11](E5M2-prototype-backlog.md#e5m2-11--client-telegraph-hud--npc-archetype-markers)):** + +- **`npc_runtime_client.gd`** — poll **`GET /game/world/npc-runtime-snapshot`** (~1 Hz when combat-active per kickoff). +- **`player_combat_health_client.gd`** — poll **`GET /game/players/{id}/combat-health`** on the same timer gate. +- **`npc_runtime_hud_state.gd`** (RefCounted) — cache snapshot + local windup tick-down between polls (kickoff interpolate decision). +- HUD labels under **`UICanvas`**: **`TelegraphLabel`**, **`NpcStateLabel`**, **`PlayerCombatHpLabel`**. +- **`main.gd` wiring:** combat poll timer, render helpers, combat-active gate (lock held OR player is aggro holder). +- **`prototype_target_markers.gd`** — already spawns three archetype markers; verify highlight + labels match migrated constants (no rename to `npc_archetype_markers.gd`). +- **`client/README.md`** — NPC / telegraph HUD section; fix stale alpha/beta copy. +- GdUnit tests with HTTP doubles ([testing-expectations.md](../../.cursor/rules/testing-expectations.md)). +- **`docs/manual-qa/NEO-97.md`** — Godot steps (server + client running). + +**Out of scope (from Linear + backlog):** + +- Final VFX art; dodge mechanics (prototype shows telegraph only). +- Server API / DTO changes ([NEO-94](https://linear.app/neon-sprawl/issue/NEO-94), [NEO-95](https://linear.app/neon-sprawl/issue/NEO-95) already landed). +- Playable capstone script ([NEO-98](https://linear.app/neon-sprawl/issue/NEO-98)). +- Telemetry ingest ([NEO-96](https://linear.app/neon-sprawl/issue/NEO-96) server comment hooks only). + +## Acceptance criteria checklist + +- [ ] Player-visible telegraph countdown matches server snapshot within one poll frame (local tick-down between polls; reconciles on each GET). +- [ ] Tab cycle visits three NPC instances in documented order (`prototype_npc_elite` → `prototype_npc_melee` → `prototype_npc_ranged`). +- [ ] Manual QA checklist exercisable without Bruno. + +## Technical approach + +### Already on `main` (verify, do not re-migrate) + +- **`prototype_target_constants.gd`** — three NPC ids in tab order + anchors/colors/display names. +- **`prototype_target_markers.gd`** + **`prototype_district_visual_catalog.gd`** — three world markers with Elite/Melee/Ranged labels. +- **`target_selection_client.gd`** — tab cycle already uses `PrototypeTargetConstants.ORDERED_IDS`. + +### HTTP clients + +1. **`npc_runtime_client.gd`** + - Mirror **`combat_targets_client.gd`**: injectable HTTP, `_busy` guard. + - **`GET {base}/game/world/npc-runtime-snapshot`** (world route — no player id). + - **`parse_runtime_json(text)`** static: require **`schemaVersion` 1**, **`npcInstances`** array. + - Signals: **`runtime_received(snapshot: Dictionary)`**, **`runtime_sync_failed(reason: String)`**. + - Helpers: **`npc_row(instance_id, snapshot)`**, **`row_for_aggro_holder(player_id, snapshot)`** (first row where **`aggroHolderPlayerId`** matches). + +2. **`player_combat_health_client.gd`** + - Mirror **`cooldown_snapshot_client.gd`**: player-scoped GET. + - **`GET {base}/game/players/{id}/combat-health`**. + - **`parse_combat_health_json(text)`** static: **`schemaVersion` 1**, **`currentHp`**, **`maxHp`**, **`defeated`**. + - Signals: **`health_received(snapshot: Dictionary)`**, **`health_sync_failed(reason: String)`**. + +### Local windup mirror (`npc_runtime_hud_state.gd`) + +- On **`runtime_received`**: store **`serverTimeUtc`** anchor + receive tick (same pattern as **`cooldown_state.gd`**). +- For each row with nested **`activeTelegraph`**: record **`windupRemainingSeconds`** at receive time. +- **`display_windup_remaining(instance_id) -> float`**: anchor value minus elapsed local seconds since receive (floor at 0). +- Reconcile on every poll — no client windup *scheduling*; only display smoothing between polls. + +### Combat-active poll gate + +Start **`Timer`** (~**1.0 s**, one-shot loop) when **any** of: + +- **`TargetSelectionClient.cached_state().lockedTargetId`** is a known NPC id, **or** +- Last **`npc-runtime-snapshot`** has **`aggroHolderPlayerId == dev_player_id`** on any row. + +Stop timer when neither condition holds. On timeout: request both GETs (sequential or parallel — parallel preferred if `_busy` guards are independent). + +Also trigger an immediate sync when lock changes to an NPC or first cast accept (optional fast-path; timer owns steady state). + +### HUD rendering (`main.gd`) + +**`PlayerCombatHpLabel`** — format **`Player HP: {current}/{max}`** + **` (defeated)`**; **`—`** on sync failure / before first poll. + +**`NpcStateLabel`** — two-line priority (kickoff locked + incoming): + +1. **Locked row:** **`NPC state: {shortName} → {state}`** when lock is a known NPC id. +2. **Incoming row:** when player is aggro holder on a *different* NPC (or no lock), append **`Incoming: {shortName} → {state}`**. + +Use server snake_case **`state`** as-is for prototype debug readability (`idle`, `aggro`, `telegraph_windup`, `recover`). + +**`TelegraphLabel`** — when locked or incoming row has **`activeTelegraph`**: + +- **`Telegraph: {shortName} {telegraphId} · {remaining:0.1}s ({archetypeKind})`** +- **`remaining`** from **`npc_runtime_hud_state.display_windup_remaining`** (interpolated). +- **`—`** when no active telegraph on displayed rows. + +**Scene layout (`main.tscn`):** insert three labels after **`CombatTargetHpLabel`** (~offset_top 400 / 426 / 452); shift **`GatherFeedbackLabel`** and economy block down ~78 px if needed. + +### Cross-references + +1. **`client/README.md`** — new **NPC runtime + telegraph HUD (NEO-97)** section; update targeting/combat sections (remove alpha/beta; document three NPC tab order + poll gate). +2. **`docs/plans/E5M2-prototype-backlog.md`** — E5M2-11 checkboxes + landed note when complete. +3. **`docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md`** — NEO-97 landed line when complete. +4. **`docs/decomposition/modules/documentation_and_implementation_alignment.md`** — E5.M2 row bump when E5M2-11 lands. + +### Wire vocabulary (from server DTOs) + +**`npc-runtime-snapshot` row:** `npcInstanceId`, `behaviorDefId`, `state`, `aggroHolderPlayerId`, optional `activeTelegraph` { `telegraphId`, `windupRemainingSeconds`, `archetypeKind` }. + +**`combat-health`:** `playerId`, `maxHp`, `currentHp`, `defeated`. + +## Files to add + +| Path | Purpose | +|------|---------| +| `docs/plans/NEO-97-implementation-plan.md` | This plan. | +| `client/scripts/npc_runtime_client.gd` | GET world npc-runtime-snapshot; parse + row helpers. | +| `client/scripts/npc_runtime_client.gd.uid` | Godot uid companion (same commit as `.gd`). | +| `client/scripts/player_combat_health_client.gd` | GET player combat-health. | +| `client/scripts/player_combat_health_client.gd.uid` | Godot uid companion. | +| `client/scripts/npc_runtime_hud_state.gd` | Snapshot cache + windup tick-down between polls. | +| `client/scripts/npc_runtime_hud_state.gd.uid` | Godot uid companion. | +| `client/test/npc_runtime_client_test.gd` | HTTP double: URL, parse, row lookup, aggro-holder helper. | +| `client/test/player_combat_health_client_test.gd` | HTTP double: URL, parse, schema guard. | +| `client/test/npc_runtime_hud_state_test.gd` | Windup interpolation + reconcile on new snapshot. | +| `client/test/npc_combat_hud_refresh_test.gd` | Combat-active gate + label render harness (mock clients + snapshot fixtures). | +| `docs/manual-qa/NEO-97.md` | Godot manual QA — lock elite, cast, observe telegraph countdown + player HP drop. | + +## Files to modify + +| Path | Rationale | +|------|-----------| +| `client/scripts/main.gd` | Wire npc-runtime + combat-health clients, combat poll timer, HUD render helpers, combat-active gate. | +| `client/scenes/main.tscn` | Add `TelegraphLabel`, `NpcStateLabel`, `PlayerCombatHpLabel`; adjust downstream label offsets. | +| `client/README.md` | NEO-97 NPC/telegraph HUD section; fix stale alpha/beta targeting/combat copy. | +| `docs/plans/E5M2-prototype-backlog.md` | E5M2-11 acceptance checkboxes + landed note when complete. | +| `docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md` | NEO-97 client landed note under Related implementation slices. | +| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E5.M2 alignment register — E5M2-11 complete when shipped. | + +## Tests + +| File | Coverage | +|------|----------| +| `client/test/npc_runtime_client_test.gd` | GET URL; **`parse_runtime_json`** schema v1; **`npc_row`**; **`row_for_aggro_holder`**. | +| `client/test/player_combat_health_client_test.gd` | GET URL with player id; parse success/fail; **`defeated`** flag. | +| `client/test/npc_runtime_hud_state_test.gd` | Apply snapshot → interpolated windup decreases; new poll reconciles to server value. | +| `client/test/npc_combat_hud_refresh_test.gd` | Combat-active gate starts/stops timer; locked + incoming label copy from fixture snapshots. | +| *(existing unchanged)* | **`prototype_target_constants_test.gd`**, **`target_selection_client_test.gd`**, **`prototype_target_markers_test.gd`** — three-NPC tab/marker behavior already covered. | + +## Open questions / risks + +| Question / risk | Agent recommendation | Status | +|-----------------|---------------------|--------| +| **NEO-94/NEO-95 blockers** | Proceed — both **Done** on `main`. | **adopted** | +| **Partial constants/markers migration** | Verify only; do not duplicate NEO-91 work. | **adopted** | +| **README still references alpha/beta** | Update in NEO-97 README pass. | **adopted** | +| **Poll advances server sim** | Accept — GET invokes **`AdvanceAll`**; ~1 Hz during combat is intended prototype behavior ([NEO-94 plan](NEO-94-implementation-plan.md)). | **adopted** | +| **Elite telegraph timeline ~7.5 s** | Manual QA must multi-poll (5 s aggro + 2.5 s windup); document waits in **`NEO-97.md`** (mirror NEO-95 Bruno notes). | **adopted** |