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

11 KiB
Raw Permalink 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: 2.83 m / 6.0 (in) (spawn (-5,-5) is ~2.83 m from alpha anchor (-3,-3))
    • prototype_target_beta: 11.31 m / 6.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 with a shared 6 m radius: an orange mast + flat ring at (-3, 0.5, -3) (alpha) and a violet mast + ring at (3, 0, 3) (beta). Both rings lie flat on the floor and visibly overlap in a band of ~3.5 m centered at origin. The player capsule should sit inside the alpha ring (only) 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 at spawn, press Tab again. The server denies the swap to beta (spawn is ~11.3 m from (3, 3); shared radius is 6 m). Because the range-aware picker skips the currently-locked id, Tab falls back to the plain cycle (→ beta), the server rejects, and the soft lock is preserved.
  • 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 ~6 m horizontally from (-3, -3) (easiest route: head toward the -x/-z side of the map away from both rings). 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 alpha ring. 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 / 6.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. Tab-flip at origin (overlap zone) — shared-radius sanity

  • WASD toward the origin (0, 0). Both rings overlap there; the HUD should show both distance lines with (in) (each ~4.24 m / 6.0).
  • Press Tab from no-lock (if Esc'd first): HUD flips to Target: prototype_target_alpha, Validity: ok, Seq +1. Cycle-first.
  • Press Tab again: HUD flips to Target: prototype_target_beta, Validity: ok, Seq +1. The range-aware picker returns beta (next in cycle, still in range).
  • Press Tab a third time: HUD flips back to Target: prototype_target_alpha. This is the core "flip between targets without moving" sanity — if it does not flip cleanly, check the server log; you should see a successful POST /target/select per press.
  • Walk slightly off-center so only one ring covers you (e.g. toward alpha's anchor): distance lines flip (in) / (out) accordingly, and Tab swaps behave like Section 3 (denial on the out-of-range direction, preserved soft lock).

6. Swap to beta when in range (via walking, not the overlap)

  • WASD from spawn straight toward beta at (3, 3) until the capsule is inside the violet ring (≤6 m). Press Tab. HUD swaps cleanly:
    • Target: prototype_target_beta
    • Validity: ok
    • Seq increments by 1
  • Press Tab once more from deep inside beta (alpha out of range). Server denies — HUD stays at beta / ok with Denied: out_of_range appended (same soft-lock UX as Section 3, other direction).
  • Range-aware cycle sanity: press Esc to clear the lock (Target: —), then — while still inside beta's ring but outside alpha's — press Tab. The client skips alpha (out of range) and locks beta directly (Target: prototype_target_beta, Validity: ok). A plain cycle would pick alpha first and get denied with out_of_range; if that happens, the range-aware pick in PrototypeTargetConstants.next_in_range_id_after is not running (e.g. set_freshness_kick not wired).
  • positionHint sanity (NEO-24 follow-up #5): walk to the edge of beta's ring, stop, and immediately press Tab. The select POST should succeed (Validity: ok). The client now attaches positionHint: {x,y,z} to the target/select body, so the server runs the radius check against the live capsule position instead of the stored move-stream snap — even when PlayerPositionLabel's age=… is several seconds old or Δ>0. Denials here would mean the hint is not being attached (confirm set_freshness_kick is wired in main.gd) or the client's sense of "in range" genuinely disagrees with the server's radius math (check the per-target distance lines).
  • Soft-lock roam regression (follow-up #5 repro): lock alpha at spawn, walk out of alpha's ring and into beta's ring (HUD shows Validity: out_of_range, beta distance < 6 m (in)), stop, wait until age=… climbs past ~3 s, and press Tab. Select swaps to beta successfully (Target: prototype_target_beta, Validity: ok). Regression before follow-up #5: this path denied with out_of_range because the select POST raced the freshness kick's move-stream POST on separate HTTP connections.

7. 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).

8. 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.

9. 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.

10. 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.

11. 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.

12. 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 111 are ticked and 12 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.