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

8.9 KiB
Raw Blame History

NEO-26 — Implementation plan

Story reference

Field Value
Key NEO-26
Title E1.M3: SelectionEvent surface + debug or HUD consumer
Linear NEO-26
Slug E1M3-04
Git branch NEO-26-selectionevent-surface-debug-consumer
Parent context Epic 1 — Core Player Runtime · E1M3 prototype backlog
Depends on NEO-23 (TargetState v1), NEO-24 (tab/clear + TargetSelectionClient) — code paths exist on main; Linear may still list relations until those issues are closed.
Decomposition E1.M3 — InteractionAndTargetingLayer (SelectionEvent contract); E1.M4 — AbilityInputScaffold (future subscriber).

Goal, scope, and out-of-scope

Goal: Every material change to the clients server-acknowledged combat target lock id (lockedTargetId in the cached TargetState mirror) is observable as a SelectionEvent carrying previous id, next id, and cause (tab, clear, server_correction, …). Ship one built-in consumer (debug overlay, print, or thin HUD) so the bus is exercised in dev runs.

In scope

  • Minimal GDScript event shape (Dictionary or small typed helper) aligned with the illustrative table in E1.M3: previous (Variant String or null), next (same), cause (String enum-like literals).
  • Cause attribution at the authoritative update boundary inside the existing selection client (see Technical approach) so we do not guess causes from state diffs alone.
  • Consumer: append-only ring buffer of last N events (e.g. 816) shown in UI or dev-only print gated by a @export var — pick one primary path in implementation; document the subscription point for E1.M4 (signal name + emitter node path pattern).
  • Tests: tab vs clear vs movement-driven GET produce distinct cause strings when they each produce a lock-id transition (or documented no-op when id unchanged).

Out of scope (per Linear / backlog)

  • Full prompt / party UI; wire protobuf events.
  • Interactable primary selection (NEO-25 catalog) — this story is target lock only unless a trivial extension falls out naturally (if not, say “none” interactable events in module doc follow-up).
  • Changing server TargetState contracts (NEO-23); new HTTP routes.

Acceptance criteria checklist

  • Tab path: after a successful selection cycle that changes lockedTargetId, a SelectionEvent is emitted with cause tab (and correct previous/next ids).
  • Clear path: clearing the lock emits clear when lockedTargetId transitions from non-null to null.
  • Server correction: a GET-driven authoritative update (e.g. movement-triggered refresh from on_authoritative_ackrequest_sync_from_server) that changes lockedTargetId emits server_correction. If the prototype server never changes id on GET (only validity), document that id-unchanged refreshes do not emit SelectionEvent for v1, and add a test or comment proving GET is still classified when id does change (or a focused mock scenario).
  • E1.M4 hook: docs/plans/NEO-26-implementation-plan.md (and optionally client/README.md under targeting) states which node exposes selection_event (or agreed name) and that E1.M4 may connect without reshaping the payload.

Technical approach

  1. Single emitter: Extend TargetSelectionClient (client/scripts/target_selection_client.gd) rather than diffing target_state_changed in a second node — only this class knows whether the in-flight HTTP phase was GET (refresh) vs POST from tab, clear, or direct request_select_target_id (if used).

  2. Intent / phase tags: Before starting HTTP, set a private enum or String tag, e.g. _pending_selection_cause, cleared after handling the response:

    • request_tab_nexttab (even though it calls request_select_target_id internally, set cause at the tab entrypoint before POST).
    • request_clear_targetclear.
    • request_select_target_id when not entered from request_tab_nextdirect_select (reserved for tests / future UI; document).
    • request_sync_from_server when invoked from on_authoritative_ack → mark refresh as server_correction for the next GET completion.
    • Boot / one-shot sync from main.gd → use server_correction or initial_sync consistently; prefer initial_sync only if we want tests to ignore boot noise — Decision (default): use server_correction for any GET that is not tied to a tab/clear POST, including first sync, only emit if normalized lockedTargetId differs from prior snapshot (avoids spam on identical boot GET).
  3. Emit rule: After _state is updated in _update_state_from_get / _update_state_from_post, compare previous normalized lockedTargetId to new; if equal, no SelectionEvent (validity-only changes stay on target_state_changed only unless product later expands the contract).

  4. Signal: Add e.g. selection_event(event: Dictionary) with keys previous, next, cause (and optional sequence int passthrough for debugging). Keep target_state_changed unchanged for existing HUD.

  5. Consumer: New small script selection_event_debug_panel.gd (type Control or Node driving a RichTextLabel) under the HUD canvas, connected in main.gd only via connect — keeps godot-client-script-organization satisfied.

  6. Docs: Update client/README.md “Target lock + tab cycle” area with a short “SelectionEvent (NEO-26)” subsection: signal name, payload keys, E1.M4 subscription note.

Files to add

Path Purpose
client/scripts/selection_event_debug_panel.gd Thin HUD / debug list: listens for selection_event, shows last N events (timestamp optional).
client/test/selection_event_client_test.gd GdUnit4: assert causes for tab POST, clear POST, and GET-with-mock-body changing lock id (TargetSelectionTestDouble pattern).

Files to modify

Path Rationale
client/scripts/target_selection_client.gd Track pending cause through GET/POST; compare old/new lockedTargetId; emit selection_event; document next to target_state_changed.
client/scenes/main.tscn Add debug panel node (sibling under UICanvas or next to existing target HUD) and script attachment.
client/scripts/main.gd Connect TargetSelectionClient.selection_event → debug panel; minimal wiring only.
client/README.md Document subscription point + payload for E1.M4.
docs/decomposition/modules/E1_M3_InteractionAndTargetingLayer.md Implementation snapshot bullet: replace “Still open — SelectionEvent” with pointer to NEO-26 + signal name (keeps decomposition aligned with shipped code).

Tests

Test file What it covers
client/test/selection_event_client_test.gd Add: Tab mock POST changes lock → event cause == "tab". Clear mock → "clear". Simulated GET completion after flagging movement refresh → "server_correction" when body changes lockedTargetId. Assert no event when id unchanged.
client/test/target_selection_client_test.gd Change (optional): If new behavior duplicates setup, keep NEO-24 tests focused on HTTP/state; only touch this file when sharing helpers avoids copy-paste.

Manual verification: Run server + client; tab and clear; walk until soft-lock story changes validity — confirm debug line classification matches plan; screenshot or note in story if server_correction only appears when GET changes id.

Open questions / risks

  • Validity-only updates: v1 emits only on lockedTargetId change; if design later needs validity transitions as events, extend shape with kind: "lock_id" \| "validity" in a follow-up.
  • Linear blocked-by: Relations may still show NEO-23/NEO-24 until closed; implementation assumes their client/server surfaces on main are present.

Decisions

Topic Decision
D1 — Emitter location TargetSelectionClient owns selection_event (intent-aware).
D2 — Boot GET Same emit rules as other GETs; use cause server_correction for GET path, suppress if id unchanged from empty initial snapshot.
D3 — direct_select Expose cause for non-tab request_select_target_id callers (tests); not required for acceptance but avoids mis-labeling as tab.