11 KiB
11 KiB
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:5253on boot;GET /healthreturns JSON. - In Godot, select
TargetSelectionClientin the scene tree:base_urlmatches the server URL anddev_player_idmatchesGame:DevPlayerId(defaultdev-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, underPlayerPositionLabel) shows:Target: —Validity: noneSeq: 0prototype_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/PlayerPositionLabelshows ansrv: (x, y, z) Δ=<d>m age=<ms>line within ~1 s of boot (bootGET /positionpopulates 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 singleGET /game/players/dev-local-1/targetfrom the boot sync.
2. Tab cycle — happy path
- Press Tab. HUD flips to:
Target: prototype_target_alphaValidity: okSeq: 1
- Server log shows
POST /game/players/dev-local-1/target/selectwith{"schemaVersion":1,"targetId":"prototype_target_alpha"}. - No
Denied:line is appended to the HUD (success ⇒ noreasonCode).
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: 1withDenied: out_of_rangeappended. 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:
POST→200withselectionApplied: 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 nextmove-stream200 (each successful POST emitsauthoritative_ack→ throttled targetGET), HUD flips to:Target: prototype_target_alpha(unchanged — soft lock)Validity: out_of_rangeSeq: 1(unchanged — no lock id change)
- Walk back inside the alpha ring. HUD returns to
Validity: okwithin ~250 ms of the nextmove-streamack. - While walking, Godot Output is not spammed with targeting GETs. Server log shows no more than roughly one targeting
GETper 250 ms during sustained motion, even thoughmove-streamPOSTs fire at ~20 Hz. - Stand still with no lock changes. Server log shows no targeting GETs at idle (no
move-streamis 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 flipsin/outas you cross the visible orange ring edge. Any time the HUD showsValidity: okbut the distance line reads(out), the server position snapshot is lagging the visible capsule — the nextauthoritative_ackrefresh 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 successfulPOST /target/selectper 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_betaValidity: okSeqincrements by 1
- Press Tab once more from deep inside beta (alpha out of range). Server denies — HUD stays at
beta / okwithDenied: out_of_rangeappended (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 without_of_range; if that happens, the range-aware pick inPrototypeTargetConstants.next_in_range_id_afteris not running (e.g.set_freshness_kicknot wired). positionHintsanity (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 attachespositionHint: {x,y,z}to thetarget/selectbody, so the server runs the radius check against the live capsule position instead of the storedmove-streamsnap — even whenPlayerPositionLabel'sage=…is several seconds old orΔ>0. Denials here would mean the hint is not being attached (confirmset_freshness_kickis wired inmain.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 untilage=…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 without_of_rangebecause the select POST raced the freshness kick'smove-streamPOST on separate HTTP connections.
7. Clear
- Press Esc at any point with a lock held. HUD flips to:
Target: —Validity: noneSeqincrements by 1
- Any
Denied: …line from the previous state is gone. - Server log shows
POST /target/selectwith body{"schemaVersion":1}(notargetIdkey). - 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 showsTarget: —,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
_inputbefore 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. Whenmove-streamreturns 400 (vertical_step_exceeded),PositionAuthorityClientruns a boot-style re-sync which emits bothauthoritative_position_received(for the snap) andauthoritative_ack(for cooldown-throttled consumers). - HUD's
MoveRejectLabelshows the rejection message as before (NEO-19). TargetSelectionClienttreats 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-streamstill works (NEO-22);PlayerPositionLabelupdates every physics tick. Einteract with the prototype terminal still printsallowed=true/falsein Output (NEO-9).- Camera zoom / occluder obstacle toggle (Ctrl+Shift+K) still work.
- No new red lines in Godot Output during a typical 1–2 minute session. Benign warnings from other systems are fine; nothing from
TargetSelectionClientduring normal play.
When sections 1–11 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.