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

14 KiB
Raw Blame History

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 · Epic 5 Slice 2 · backlog E5M2-11
Branch NEO-97-e5m2-11-client-telegraph-hud
Server deps NEO-94GET /game/world/npc-runtime-snapshot (Done); NEO-95GET /game/players/{id}/combat-health (Done)
Pattern NEO-85 / NEO-32 — thin HTTP clients, main.gd HUD wiring, GdUnit mock transport, optional local tick-down between polls
Downstream NEO-98 — playable NPC telegraph combat capstone (manual QA script)
Server counterpart NEO-94 + NEO-95 — this issue is the client half; Bruno-only verification is not prototype-complete per full-stack epic decomposition

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 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 pollscooldown_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):

  • 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).
  • 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, NEO-95 already landed).
  • Playable capstone script (NEO-98).
  • Telemetry ingest (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_eliteprototype_npc_meleeprototype_npc_ranged).
  • 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). 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