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

191 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 NPCs 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
- [x] Player-visible telegraph countdown matches server snapshot within one poll frame (local tick-down between polls; reconciles on each GET).
- [x] Tab cycle visits three NPC instances in documented order (`prototype_npc_elite` → `prototype_npc_melee``prototype_npc_ranged`).
- [x] Manual QA checklist exercisable without Bruno.
## Implementation reconciliation (shipped)
- **`npc_runtime_client.gd`**, **`player_combat_health_client.gd`**, **`npc_runtime_hud_state.gd`**, **`npc_combat_hud_helpers.gd`** — poll + parse + windup interpolation + shared HUD formatters.
- **`main.gd`** — combat-active ~1 Hz poll timer; **`TelegraphLabel`**, **`NpcStateLabel`**, **`PlayerCombatHpLabel`** render helpers (locked + incoming threat rows).
- **`main.tscn`** — three HUD labels; downstream label offsets shifted.
- GdUnit: **`npc_runtime_client_test.gd`**, **`player_combat_health_client_test.gd`**, **`npc_runtime_hud_state_test.gd`**, **`npc_combat_hud_refresh_test.gd`**; shared formatters in **`npc_combat_hud_helpers.gd`**.
- **`docs/manual-qa/NEO-97.md`**, **`client/README.md`**, E5M2 backlog + module + alignment register updated.
- **`prototype_target_constants.gd`** / **`prototype_target_markers.gd`** — verified on `main` (NEO-91); no rename to `npc_archetype_markers.gd`.
## 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 per-row **`windupRemainingSeconds`** at receive tick (no `serverTimeUtc` anchor — display smoothing only).
- 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/scripts/npc_combat_hud_helpers.gd` | Shared combat-active gate + NPC/telegraph label formatters. |
| `client/scripts/npc_combat_hud_helpers.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 + timer mirror; telegraph fallback/expiry; sync error copy. |
| `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 via helpers. |
| `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. |
| `client/scripts/npc_runtime_client.gd` | Instance **`npc_row`** falls back to **`_last_snapshot`**. |
| `client/scripts/npc_combat_hud_helpers.gd` | Code-review follow-up: shared HUD helpers extracted from `main.gd`. |
| `docs/decomposition/modules/E5_M2_NpcAiAndBehaviorProfiles.md` | Risks + NPC instance row aligned with NEO-97 kickoff. |
| `docs/reviews/2026-05-29-NEO-97.md` | Strike-through review suggestions when fixed. |
| `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; **`row_for_aggro_holder`**; instance **`npc_row`** **`_last_snapshot`** fallback. |
| `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 + timer mirror; telegraph fallback/expiry; sync error on labels. |
| *(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** |