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

9.1 KiB
Raw Blame History

NEO-24 — Implementation plan

Story reference

Field Value
Key NEO-24
Title E1.M3: Client tab-target + lock UI synced to server
Linear NEO-24
Slug E1M3-02
Git branch NEO-24-e1m3-client-tab-target-lock-ui-synced-to-server
Parent context Epic 1 — Core Player Runtime · E1M3 prototype backlog
Depends on NEO-23 (targeting HTTP v1)
Decomposition E1.M3 — InteractionAndTargetingLayer; authority notes: client_server_authority — E1.M3

Goal, scope, and out-of-scope

Goal: Tab (or agreed input action) cycles server-eligible stub targets in a documented deterministic order; HUD / debug overlay reflects the last server-acknowledged TargetState (lockedTargetId, validity, sequence). Client sends selection intent via existing POST …/target/select; optional optimistic presentation must reconcile to the authoritative targetState in each 200 response (including denials), per E1.M3 authority doc.

In scope

  • Godot: input → TargetSelectRequest v1 → parse TargetSelectResponse / PlayerTargetStateResponse; maintain last acknowledged target state for UI.
  • Tab cycle order: match server stub contract — lexicographic ascending id over the prototype registry ids (prototype_target_alpha, then prototype_target_beta), wrapping from last → first; same ordering as server README — Targeting / PrototypeTargetRegistry comments.
  • Clear lock: POST with targetId omitted or JSON null (NEO-23 rule); binding documented in client/project.godot (proposed: Esc via target_clear).
  • UI: readable label (or small overlay) showing server lock id (or none / empty), validity, and optionally sequence for debugging.
  • Soft lock / movement: when the player moves, validity can become out_of_range without clearing lockedTargetId. Client must refresh authoritative state periodically or on hooks so the overlay stays truthful (see Technical approach).
  • Shared constants: duplicate stub target id list + order in a small GDScript constants module aligned with PrototypeTargetRegistry (NEO-23 plan expectation for NEO-24 preview/cycle).

Out of scope (per Linear / backlog)

  • Nearest-in-cone auto-target; sticky soft-target under latency.
  • AbilityCastRequest / hotbar — E1.M4.
  • Server route or DTO changes (covered by NEO-23); no new ASP.NET endpoints unless a client bug reveals a contract gap (then narrow fix + plan update).

Acceptance criteria checklist

  • With ≥2 stub targets, tab advances selection intent in ascending id order and wraps at the end of the ordered list.
  • After each successful round-trip, UI shows lockedTargetId (or equivalent none) and validity from the response body (targetState on POST, body on GET).
  • On selectionApplied: false, UI shows authoritative targetState from the same response (no “stuck optimistic” lock id that the server denied).
  • Clear action clears lock server-side and UI updates from the response.
  • Input bindings for tab cycle and clear are documented in client/project.godot (and briefly in client/README.md if other prototype keys are documented there).

Technical approach

  1. prototype_target_constants.gd (or rename if clearer): export ordered Array[String] of stub ids matching PrototypeTargetRegistry (alpha, beta) for tab math only; single source for “eligible cycle list” on the client.

  2. target_selection_client.gd: Node analogous to interaction_request_client.gd / position_authority_client.gd: owns injectable HTTP transport, base_url, dev_player_id, _busy guard. Responsibilities:

    • request_sync_from_server()GET …/target → parse PlayerTargetStateResponse v1 → cache + emit signal.
    • request_select_target_id(target_id: String)POST …/target/select with body {"schemaVersion":1,"targetId":…}.
    • request_clear_target()POST with no targetId / null per NEO-23.
    • request_tab_next() — read cached lockedTargetId + validity; compute next id in the ordered list (if no lock, choose first id in order); POST select; on deny, signal carries server targetState only (no client-only lock).
    • Signal e.g. target_state_changed(state: Dictionary) or typed fields for consumers (lockedTargetId Variant string-or-null, validity string, sequence int, selection_applied bool optional for last POST).
    • Polling: while last known lockedTargetId is non-null, issue GET …/target on a throttled timer (e.g. every 0.250.5 s) so out_of_range updates after locomotion without requiring new tab presses. Stop or slow polling when no lock if desired for noise (document choice in plan Decisions during implementation).
  3. Optimistic highlight (optional slice): If implemented, keep pending selection separate from acknowledged state; on 200 denial, drop pending and paint from targetState. If timeboxed, v1 may ship ack-only UI (still satisfies “optional optimistic” — none used).

  4. main.tscn / main.gd: Add child node with target_selection_client.gd; connect signal to a Label (new TargetLockLabel under UICanvas). Call request_sync_from_server() once after boot position sync (or same frame as first authority ready) so HUD matches server on start.

  5. project.godot: target_tabTab; target_clearEscape (document; user may request alternatives before implementation).

  6. Manual QA: Run server + client; tab between stubs; walk out of radius and confirm validity flips to out_of_range on overlay while id remains; clear with Esc.

Files to add

Path Purpose
client/scripts/prototype_target_constants.gd Ordered stub targetId list + helpers for “next in cycle” matching server registry.
client/scripts/target_selection_client.gd HTTP GET/POST targeting APIs; tab/clear entrypoints; throttled refresh; signals for UI.
client/test/target_selection_test_double.gd Test subclass injecting mock HTTP (pattern from position_authority_test_double.gd).
client/test/target_selection_client_test.gd GdUnit4: tab order wrap, POST success path, denial keeps server lockedTargetId, clear POST, GET parse.

Files to modify

Path Rationale
client/scenes/main.tscn Add TargetSelectionClient node + TargetLockLabel (or reuse a debug panel) bound to scripts.
client/scripts/main.gd Wire @onready target client + label; connect target_state_changed; trigger initial request_sync_from_server() after authority boot path.
client/project.godot Register target_tab and target_clear input actions with defaults above.
client/README.md One short subsection: tab-target prototype, bindings, and how to read the lock overlay (parity with interaction/move docs).

Tests

Test file What to cover
client/test/target_selection_client_test.gd Mock transport enqueues JSON: GET returns a v1 envelope; POST apply returns selectionApplied: true + targetState; POST deny returns selectionApplied: false + reasonCode + authoritative targetState; tab from no lock selects first id in order; tab from alpha requests beta; wrap from beta to alpha; clear issues POST without targetId. Use MockHttpTransport pattern from position_authority_client_test.gd.

No new C# tests (client-only story). Bruno: NEO-23 targeting requests already live under bruno/neon-sprawl-server/targeting/; no new .bru required unless implementation discovers a server contract change (then add per repo testing expectations for HTTP contract changes).

Open questions / risks

  • Input bindings: Plan assumes Tab + Esc. Confirm or specify alternatives (e.g. mouse thumb buttons) before locking project.godot.
  • Polling cost: Throttled GET while locked is simple but adds HTTP traffic; acceptable for prototype. If objection, switch to polling only when HUD visible or tie to move-stream cadence with plan update.
  • Scene visuals: This story does not require 3D reticles on stub meshes; overlay text satisfies AC. World-space highlights can be a follow-up.

Decisions

(None locked yet — update this table during implementation chat / PR.)