Move hotbar state/client field declarations above onready definitions for class order compliance, and include a non-behavioral test file restage so client hooks can validate.
Ship a server-owned hotbar loadout API with Postgres-or-memory persistence so slot bindings survive reconnects in configured environments. Wire client boot hydration and add tests/docs/manual QA to keep the prototype contract and policy explicit.
Map SelectionEvent to target_changed, reserve ability_cast_requested and ability_cast_denied hook locations, and align plan/manual QA/decomposition docs with TODO(E9.M1) telemetry follow-up.
Fix "Tab says denied even though I'm in range of beta": root cause is the
race between the 20 Hz move-stream sampler and the target-select POST. A
Tab press right after the capsule stops can be validated against the
server's last sampled position, which trails the visible capsule by
several meters.
Client-side fixes (server contract unchanged):
- TargetSelectionClient.set_freshness_kick(authority, player): before each
POST /target/select, submit a move-stream with the current player
position so the server's stored snapshot is as fresh as possible when
the range check runs. Wired in main.gd; optional so tests without the
wiring still pass (regression-covered).
- PlayerPositionLabel now shows a "srv: (x,y,z) Δ=<d>m age=<ms>" line
populated from PositionAuthorityClient.authoritative_ack, so any
residual client/server divergence is visible in the HUD and future
reports can point at it directly.
- Two new tests: select POST kicks exactly one freshness stream with the
current player position before POSTing, and the kick is a no-op when
the wiring is absent.
Docs: plan Decision 5, review follow-up #2, manual QA section 5 and
section 1, and client README updated to describe the new behaviors.
Players reported targeting "feels inconsistent" because the server
PrototypeTargetRegistry anchors (alpha (-5,-5) r=8, beta (8,8) r=4) have
no visible meshes — the only console visible in the scene is the NEO-9
PrototypeTerminal at origin, which is unrelated to NEO-23/24 targeting.
Adds a display-only mirror of the server registry:
- client/scripts/prototype_target_markers.gd spawns a colored mast +
translucent flat radius ring per anchor so the lock radius is
visible in-world.
- client/scripts/prototype_target_constants.gd gains ANCHORS with
position/radius/color, matching PrototypeTargetRegistry.cs. Marked
display-only; server remains authoritative for validity.
- TargetLockLabel now renders a per-anchor distance line each physics
tick ("<id>: <d> m / <radius> (in|out)") so players can see the
relationship between the visible capsule and the lock radius and
immediately spot client/server position drift.
Also updates README, plan (Decision 4), manual QA (Section 1 boot +
Section 4 locomotion), and the review file's follow-up section to
describe the UX fix.
Review 2026-04-21 caught that the hybrid soft-lock refresh path is
unreachable during normal WASD locomotion. `main.gd` was wiring
`TargetSelectionClient` to `PositionAuthorityClient.authoritative_position_received`,
which only fires on boot sync and move-rejection resync — successful
`move-stream` 200s are intentionally silent to avoid RTT rubber-banding.
So walking out of an alpha/beta radius never triggered the throttled
`GET /target` the HUD depends on, and validity stayed stuck at `ok`.
Add a separate `authoritative_ack(world)` signal on `PositionAuthorityClient`
emitted from both `BOOT_GET` 200 and successful `move-stream` 200, and
switch `TargetSelectionClient` (+ `main.gd`) to it. The snap signal keeps
its current semantics so rubber-band suppression is untouched; the ack
signal is a pure heartbeat that cooldown-throttled consumers can hook.
Also adds an integration test that wires both real clients together (per
the review's suggestion 1) so renames or mis-wires between the two
scripts fail loudly rather than silently skipping the end-to-end path.
- `client/scripts/position_authority_client.gd`: new `authoritative_ack`
signal; extracted `_parse_world_from_response` helper to share JSON
parse across `BOOT_GET` and `STREAM_POST`; emit ack from both.
- `client/scripts/target_selection_client.gd`: rename
`on_authoritative_position_snap` to `on_authoritative_ack`; drop
`apply_as_snap` gate (no longer meaningful).
- `client/scripts/main.gd`: connect the new signal.
- `client/test/position_authority_client_test.gd`: assert ack on boot
200 and stream 200.
- `client/test/target_selection_client_test.gd`: rename tests for new
handler.
- `client/test/target_refresh_on_locomotion_test.gd`: new integration
suite wiring real authority + target clients.
- Plan Decision 3 + Tests row + Files-to-modify row updated.
- `docs/reviews/2026-04-21-NEO-24.md`: committed with blocking issue +
suggestions struck through with `Done.` notes per
planning-implementation-docs rule.
- `docs/decomposition/modules/documentation_and_implementation_alignment.md`:
E1.M3 row now reflects NEO-24 landed.
- `client/README.md` + `docs/manual-qa/NEO-24.md`: refreshed signal names
so the soft-lock refresh description matches the wiring.
Tests: 88/88 GdUnit headless passed locally.
Adds `TargetSelectionClient` (Godot, HTTP to NEO-23 targeting endpoints):
- Tab cycles ids in ascending order matching `PrototypeTargetRegistry`
(`prototype_target_alpha`, `prototype_target_beta`) and wraps.
- Esc clears by POSTing with `targetId` omitted.
- UI always paints from the server's authoritative `targetState`
(success and denials alike); `UICanvas/TargetLockLabel` shows
`lockedTargetId`, `validity`, `sequence`, and (on deny) `reasonCode`.
- Movement-triggered refresh: while a lock is held, subsequent
`authoritative_position_received` snaps fire a throttled GET
(~250 ms cooldown) so `validity` flips to `out_of_range` after
locomotion without duplicating server radius math on the client.
Registers `target_tab` (Tab) / `target_clear` (Esc) in `project.godot`;
documents bindings + manual QA in `client/README.md`.
Tests: `target_selection_client_test.gd` covers GET parse, tab ordering
from empty/alpha/wrap, POST body shape, denial preserves server state,
clear omits targetId, movement refresh fires only while locked, cooldown
collapses bursts to one GET, and `apply_as_snap=false` is ignored.
All 85 client GdUnit cases pass; gdlint and gdformat clean.
See `docs/plans/NEO-24-implementation-plan.md`.
Move the WASD physics tick into player_locomotion_wasd.gd so player.gd
stays under the 1600-line CI cap. Fix gdlint class order (enum before
consts), load-constant naming, and wrap long doc lines. Update NEO-22
review and plan; refresh gdlintrc comment.
Delete ground_pick and GroundPick wiring; PositionAuthorityClient now only
boot GET and move-stream POST. main.gd snaps on authoritative position only.
Update client/server READMEs, cursor rule example, NEO-22 plan note, and
GdUnit authority tests (stream 400 + boot resync).
Stream HTTP responses can lag integrated move_and_slide by roughly one RTT,
so apply_as_snap was pulling the capsule backward each tick while moving.
Skip small XZ corrections when wish or horizontal speed indicates locomotion,
with a cap so large desync still snaps. move_rejected sets a one-shot flag so
resync after move-stream 400 still applies the authoritative pose.
Introduce POST /game/players/{id}/move-stream with ordered targets, full-chain
validation before apply, and PositionStateResponse. Godot client uses camera-
relative WASD, queues samples, and snaps to stream responses; debug builds keep
click-to-move via ground_pick. Update READMEs, E1.M1 snapshot, and tests.
Add prototype_smooth_hill_piece for QA geometry alongside main scene wiring.
Refresh player locomotion and main scene integration from stashed work.
Track docs/plans/NEO-22-implementation-plan.md (WASD replaces click-to-move).
Migrate Jira NEON-* references to Linear NEO-* and linear.app links in
docs, Cursor rules, READMEs, and traceability comments. Renamed
implementation plans and dated reviews to NEO filenames.
Branch is based on origin/main with this commit only (no NEON-29
gameplay chain). client/README follows main’s district/map copy;
player.gd matches main aside from NEO-14 debug trace prefixes.
While _idle_stable_latched, use STABLE_IDLE_FLOOR_HOLD_MIN_UP_DOT (0.992) for
idle_support floor flatness so tread-edge normal flicker does not unlatch as
often. idle_support_is_stable gains optional min_flat_up_dot; unlatch streak 8.
Prototype position label updates in main _physics_process. Add GdUnit for param.
- main.gd: class member order (prvvars before @onready); wrap doc lines to 100 cols.
- isometric_follow_camera.gd: wrap doc lines to 100 cols.
- position_authority_client.gd: gdformat.
- pre-push: resolve .venv-gd/Scripts/gdlint.exe and gdformat.exe (Windows venv).
- Add scripts/install-git-hooks.ps1; README notes hook install and why CI can still fail.
- Input action dev_toggle_occluder_obstacle (Ctrl+Shift+K): toggle obstacle off nav source via reparent to World, rebake NavigationRegion3D, sync NavigationAgent3D; physics/process/collision hardening unchanged intent.
- Occluder dictionary keys by instance id; related client fixes and README/project input note.
- Plan and review: PARSED_GEOMETRY_BOTH / runtime geometry removal lesson for future mechanics.
- Add BoxShape3D to PrototypeTerminal (layer 1, walkable) so the player
cannot pass through; matches terminal mesh size.
- Bake navigation from collision layer 1 only so the player (layer 2) is
not merged into the mesh; reparent Player under NavigationRegion3D so
NavigationAgent3D resolves a valid map.
- Restore nav waypoint following for long horizontal legs; keep horizontal
steering toward next/goal so bump departure behavior stays sane.