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.pull/136/head
parent
1ce2b9893c
commit
b0d0c4262e
|
|
@ -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** |
|
||||
Loading…
Reference in New Issue