Files
security-game/docs/adr/0011-aim-free-selection-and-precision-walk.md
zwitschi afa1d6beec
Full build / Import + headless tests (push) Successful in 36s
Full build / Full desktop build + optional release (push) Successful in 4m17s
v0.3.0: rendering pass, generated props, selection model and Web build
Rendering and presentation
- Shared surface-material module (src/game/materials.gd) with per-surface
  roughness/metallic/uv profiles; ACES tonemapping, glow, fog and phase-driven sky
  energy. WorldBuilder rebuilt on those profiles with facade windows that light up,
  door trim/handles, curb, lane dashes, bollards, bench, bin and planter foliage.
- NPC silhouettes: per-role clothing palettes, caps/hoods/backpacks/sunglasses,
  hands and shoes, arm/leg walk swing.
- Six generated low-poly GLB props (bollards, bench, bin, planter, newspaper box,
  hydrant) built by tools/model_pipeline.py, loaded by WorldBuilder with a
  procedural fallback; ADR-0012.

Selection and movement
- Focus ranking by view angle then distance (NpcFocus.rank_indices); Tab and the
  mouse wheel cycle nearby people initially, right click clears focus with a
  re-acquire grace, HUD shows the candidate count, Shift walks at half speed.
  ADR-0011. Windowed smoke extended to cover wheel cycling and clearing.

QA tooling
- screen_designs.py catalogue grown to 19 screens (8 scene, 11 UI) plus a `list`
  subcommand; draft generation keeps existing AI references unless --force.

Web export
- New Web preset using the non-threaded Godot template (static hosting, no
  COOP/COEP). tools/build_releases.sh exports build/web and packages
  build/step-line-web-<ver>.zip; publish_release.sh uploads it with the desktop
  artifacts. test_web_export.gd covers the preset and release wiring.

Verification
- Headless suite 177 -> 181, Python tooling 53 tests, all eight smokes pass
  (including the windowed focus/wheel smoke). Web bundle serves index.html,
  index.wasm (35 MB) and index.pck (30 MB) with HTTP 200 from a static server.
- Version 0.3.0; README, PLAN, RELEASING, TODO/DONE and MEMORY updated.
2026-10-31 23:07:41 +01:00

3.7 KiB

ADR-0011: Aim-free target selection, precision walk, and focus clearing

  • Status: Accepted
  • Date: 2026-10-31

Context

ADR-0007 and ADR-0008 split input into locomotion, look, and a focus cone. A pass over the resulting controls found four problems in play:

  1. The cone ignored what the player sees. NpcFocus.pick_index returned the first head inside the cone in group order, not the one nearest the crosshair. Two people in the cone gave an arbitrary pick.
  2. Tab cycled in spawn order. GameManager._cycle_nearby_target walked the npcs array, so "next" meant "next spawned", not "next nearest".
  3. A cycled target was dropped within a few frames. The cone re-evaluated every physics frame and cleared a target that was not under the crosshair after the 8-frame miss grace. Aim-free selection therefore only worked while the player also aimed.
  4. There was no way to dismiss someone. Esc pauses the shift and right click only cancelled modals.

The movement pass also left one gap at the door: the only walking speed is 3.2 m/s, which overshoots when the player is trying to stand at a register or read an artifact at conversation distance.

Decision

Rank the cone by angle, then distance. NpcFocus.rank_indices scores every head by its angle from the camera forward vector and breaks ties by distance; pick_index is the first ranked entry. The cone therefore picks the person closest to the crosshair, which is what the player is looking at.

Make aim-free selection authoritative. GuardController.cycle_target(direction) walks nearby_candidates() — everyone within 12 m, nearest first — starting from the current target and wrapping in both directions. The chosen avatar is pinned: while a pinned target is alive the cone does not re-evaluate, so Tab and the mouse wheel produce a stable selection the player can then act on.

Bind the mouse wheel to the same ring. cycle_target_next (wheel down) and cycle_target_prev (wheel up) are new input actions handled in GameManager._handle_mouse_button. Tab remains the keyboard path. The HUD prints how many people are in the ring (hud.nearby), so the player knows the control exists before they need it.

Right click clears focus, with a short grace. With no modal open, right click clears the target and suppresses cone acquisition for FOCUS_SUPPRESS_S = 0.75 s. Without the grace the cone re-acquired the same person on the next frame, which made the control feel broken. Any explicit pick (cone or cycle) cancels the suppression.

Add a precision walk. Holding Shift (walk_slow) multiplies the walk speed by SLOW_WALK_FACTOR = 0.45. This is a fine-positioning aid, not a sprint: PLAN.md deliberately excludes sprint and crouch, and this does not add a faster move.

Escape keeps its current meaning (close the top modal, otherwise pause). Selection is not overloaded onto the same key.

Consequences

Positive

  • The focused person is predictable: the one nearest the crosshair, or the one the player explicitly cycled to.
  • Aim-free play works: cycle, act, cycle again, without fighting the cone.
  • Right click has an unambiguous "never mind" meaning while remaining a modal cancel.
  • Standing on a mark while reading an artifact is possible.

Negative

  • Two more input actions and one more piece of guard state (target_pinned, focus_suppress_s).
  • A cycled target stays selected until the player clears it, acts, or the avatar is freed; it does not expire on its own.
  • The 0.75 s suppression is a feel constant that may need tuning against play testing.

Follow-up

  • The action wheel (ADR-0009) and inspection panels still take A/D; the wheel owns those keys while it is open, and the guard is frozen then, so no conflict is observable.