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.
49 lines
3.7 KiB
Markdown
49 lines
3.7 KiB
Markdown
# 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.
|