# NEO-24 — Manual QA checklist | Field | Value | |--------|--------| | **Key** | NEO-24 | | **Title** | E1.M3: Client tab-target + lock UI synced to server | | **Linear** | [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24/e1m3-client-tab-target-lock-ui-synced-to-server) | | **Plan** | [`docs/plans/NEO-24-implementation-plan.md`](../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` - [ ] 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: `POST` → `200` 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 authority snap, 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 snap. - [ ] 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. - [ ] Stand still with no lock changes. Server log shows **no** targeting GETs at idle (no timer polling). ## 5. Swap to beta when in range - [ ] WASD toward beta at **(8, 8)** until you are within 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. ## 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 `authoritative_position_received`. - [ ] HUD's `MoveRejectLabel` shows the rejection message as before (NEO-19). - [ ] `TargetSelectionClient` treats that snap 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 1–2 minute session. Benign warnings from other systems are fine; nothing from `TargetSelectionClient` during normal play. --- When sections 1–10 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](../plans/NEO-24-implementation-plan.md).