neon-sprawl/docs/manual-qa/NEO-24.md

8.3 KiB
Raw Blame History

NEO-24 — Manual QA checklist

Field Value
Key NEO-24
Title E1.M3: Client tab-target + lock UI synced to server
Linear NEO-24
Plan docs/plans/NEO-24-implementation-plan.md
Branch NEO-24-e1m3-client-tab-target-lock-ui-synced-to-server

Pair the client (Godot F5) with the server (cd server/NeonSprawl.Server && dotnet run). Keep the server console visible so you can see route hits, and the Godot Output dock visible for client warnings.

0. Setup sanity

  • Server prints a URL around http://localhost:5253 on boot; GET /health returns JSON.
  • In Godot, select TargetSelectionClient in the scene tree: base_url matches the server URL and dev_player_id matches Game:DevPlayerId (default dev-local-1).
  • (Optional) Bruno collection bruno/neon-sprawl-server/targeting/ is configured for the same port — use it to cross-check server state after client actions.

1. Boot state

  • On run, the player snaps to spawn (-5, 0.9, -5) — existing NEO-7 behavior, unchanged.
  • UICanvas/TargetLockLabel (top-left, under PlayerPositionLabel) shows:
    • Target: —
    • Validity: none
    • Seq: 0
    • prototype_target_alpha: 0.00 m / 8.0 (in) (spawn (-5,-5) is exactly on alpha's XZ anchor)
    • prototype_target_beta: 18.38 m / 4.0 (out)
  • UICanvas/PlayerPositionLabel shows an srv: (x, y, z) Δ=<d>m age=<ms> line within ~1 s of boot (boot GET /position populates it). At rest the Δ should be near 0.
  • Visible anchors are rendered in-world: an orange mast + flat ring at (-5, 0.5, -5) (alpha, 8 m radius) and a violet mast + ring at (8, 0, 8) (beta, 4 m radius). Both rings lie flat on the floor; the player capsule should sit inside the alpha ring at spawn.
  • Godot Output has no TargetSelectionClient: warnings. Server console shows a single GET /game/players/dev-local-1/target from the boot sync.

2. Tab cycle — happy path

  • Press Tab. HUD flips to:
    • Target: prototype_target_alpha
    • Validity: ok
    • Seq: 1
  • Server log shows POST /game/players/dev-local-1/target/select with {"schemaVersion":1,"targetId":"prototype_target_alpha"}.
  • No Denied: line is appended to the HUD (success ⇒ no reasonCode).

3. Tab to out-of-range target — soft lock preserved

  • With alpha locked, press Tab again. The server denies the swap to beta (spawn is ~18 m from (8, 8); beta radius is 4 m).
  • HUD stays at Target: prototype_target_alpha, Validity: ok, Seq: 1 with Denied: out_of_range appended. That is the NEO-23 soft-lock rule: a failed swap does not change the persisted lock or its sequence.
  • Server log shows the denial: POST200 with selectionApplied: false.
  • Press Tab several more times from here. Each press cleanly denies; HUD never drifts or shows a beta / optimistic lock between responses.

4. Soft-lock out-of-range via locomotion (plan Decision 2)

  • Still locked to alpha, WASD away from spawn until you are ~8 m horizontally from (-5, -5). Within ~250 ms of the next move-stream 200 (each successful POST emits authoritative_ack → throttled target GET), HUD flips to:
    • Target: prototype_target_alpha (unchanged — soft lock)
    • Validity: out_of_range
    • Seq: 1 (unchanged — no lock id change)
  • Walk back inside the radius. HUD returns to Validity: ok within ~250 ms of the next move-stream ack.
  • While walking, Godot Output is not spammed with targeting GETs. Server log shows no more than roughly one targeting GET per 250 ms during sustained motion, even though move-stream POSTs fire at ~20 Hz.
  • Stand still with no lock changes. Server log shows no targeting GETs at idle (no move-stream is sent while idle, so there is no ack to trigger a refresh).
  • Distance lines match the ring: while moving, the prototype_target_alpha: <d> m / 8.0 (in|out) line flips in/out as you cross the visible orange ring edge. Any time the HUD shows Validity: ok but the distance line reads (out), the server position snapshot is lagging the visible capsule — the next authoritative_ack refresh should reconcile within 250 ms.

5. Swap to beta when in range

  • WASD toward beta at (8, 8) until you are inside the violet ring (≤4 m). Press Tab. HUD swaps cleanly:
    • Target: prototype_target_beta
    • Validity: ok
    • Seq increments by 1
  • Press Tab once more. Server denies (alpha is far away) — HUD stays at beta / ok with Denied: out_of_range appended.
  • Freshness kick sanity: walk to the edge of beta's ring, stop, and immediately press Tab. The select POST should succeed (Validity: ok) — the client kicks a move-stream with the current capsule position right before POSTing so the server is not validating against a stale 20 Hz sample. If this denies at a position visibly inside the ring, check PlayerPositionLabel's srv: … line — a large Δ means the freshness kick is not landing before the select POST, and the race needs either a higher move-stream rate or a server-side client-position-hint contract.

6. Clear

  • Press Esc at any point with a lock held. HUD flips to:
    • Target: —
    • Validity: none
    • Seq increments by 1
  • Any Denied: … line from the previous state is gone.
  • Server log shows POST /target/select with body {"schemaVersion":1} (no targetId key).
  • Press Esc again with no lock. Server returns the same cleared state; HUD stays / none (server may or may not bump sequence — whatever it returns is what the HUD shows).

7. Denial UI hygiene

  • Press Tab from while far from both targets. If the server denies (possible depending on exact spawn distance), HUD shows Target: —, Validity: none, Denied: out_of_range. The lock id does not get "stuck" at something the server refused.
  • After any successful Tab / Esc, the Denied: … line goes away on the next response.

8. Input behavior

  • Tab does not make focus jump around UI during play (we intercept in _input before UI focus navigation).
  • Esc does not quit or minimize the game.
  • Holding Tab does not auto-repeat into a firehose of POSTs (pressed action only fires on key press, not echo).
  • E (NEO-9 interact) and WASD (NEO-22 move) still work independently of the target lock; having a lock does not block interact or movement.

9. Coexistence with movement rejection (NEO-19 / move-stream 400)

  • Walk at the orange MoveRejectPedestal (~(7.5, 6.5)) while holding a lock. When move-stream returns 400 (vertical_step_exceeded), PositionAuthorityClient runs a boot-style re-sync which emits both authoritative_position_received (for the snap) and authoritative_ack (for cooldown-throttled consumers).
  • HUD's MoveRejectLabel shows the rejection message as before (NEO-19).
  • TargetSelectionClient treats that ack like any other: if a lock is held, it fires at most one refresh GET (subject to cooldown). No duplicate requests, no stuck busy state.

10. Server-down / transport failure

  • Stop the server. Press Tab / Esc. Godot Output prints TargetSelectionClient: HTTP failed … warnings; the client is not wedged — you can press again (busy flag is released on failure).
  • Restart the server and confirm the next Tab succeeds; HUD updates from the response.

11. No regressions

  • Boot snap still works (NEO-7).
  • WASD locomotion + move-stream still works (NEO-22); PlayerPositionLabel updates every physics tick.
  • E interact with the prototype terminal still prints allowed=true/false in Output (NEO-9).
  • Camera zoom / occluder obstacle toggle (Ctrl+Shift+K) still work.
  • No new red lines in Godot Output during a typical 12 minute session. Benign warnings from other systems are fine; nothing from TargetSelectionClient during normal play.

When sections 110 are ticked and 11 shows no regressions, the story is ready for In Test / PR. Record any deviations or follow-ups in the Decisions or Open questions sections of the implementation plan.