8.1 KiB
Review — NEO-24
Date: 2026-04-21
Scope: NEO-24-e1m3-client-tab-target-lock-ui-synced-to-server (origin/main...HEAD)
Base: origin/main (merge-base 37f44a494400f4fdd369077c52dfc012df73a5d5)
Follow-up: blocking issue + suggestions below are done (strikethrough + Done.). See the NEO-24 plan Decision 3 for the signal change.
Post-merge UX follow-up (2026-04-21): user reported "targeting result feels very inconsistent and doesn't seem to matter whether i'm within range of the console or not." Root cause was not a correctness bug — the PrototypeTargetRegistry anchors (alpha (-5,-5) r=8, beta (8,8) r=4) have no visible scene meshes, so players were reasoning about targeting against the PrototypeTerminal box at origin (which belongs to NEO-9 interaction, not NEO-23/24 targeting). Added:
client/scripts/prototype_target_markers.gd— spawns an unshaded colored mast + flat radius ring per anchor (mirrors the server registry viaprototype_target_constants.gd).- Per-anchor distance readout on
UICanvas/TargetLockLabel:<id>: <d> m / <radius> (in|out)computed client-side from the live capsule position. Labeled explicitly as a lag diagnostic — ifValidity: okshows while the distance line reads(out), the server's position snapshot is trailing the visible capsule and the nextauthoritative_ackrefresh reconciles it. - Updated
client/README.mdanddocs/manual-qa/NEO-24.md(Section 1 boot state and Section 4 locomotion check) to describe the markers and distance lines.
Verdict
Request changes — Addressed. Blocking issue fixed; integration test added; doc-alignment row updated.
Summary
This branch adds the client-side target selection node, HUD label, input bindings, tests, and story docs for NEO-24. The overall direction matches the E1.M3 plan well: selection intent is POSTed to the server, the HUD paints only server-acknowledged target state, and denial handling is intentionally authoritative.
Follow-up verification confirms the earlier review findings were addressed. The movement-triggered soft-lock refresh is now reachable during normal locomotion via PositionAuthorityClient.authoritative_ack, the new integration test covers the real authority-to-target wiring, and the E1.M3 implementation-alignment row was updated to reflect NEO-24 landing.
Documentation checked
docs/plans/NEO-24-implementation-plan.md— matches. Decision 2 now usesPositionAuthorityClient.authoritative_ack, and Decision 3 records why the new signal was added.docs/manual-qa/NEO-24.md— matches. Section 4’s locomotion-driven refresh path is now implemented by the new authority ack signal and target refresh wiring.docs/decomposition/modules/E1_M3_InteractionAndTargetingLayer.md— matches for server-authoritative target state and denial reconciliation.docs/decomposition/modules/client_server_authority.md— matches for client-as-intent / server-as-truth behavior.docs/decomposition/modules/module_dependency_register.md— matches.E1.M3remainsIn Progress.docs/decomposition/modules/documentation_and_implementation_alignment.md— matches. TheE1.M3tracking row now reflects NEO-24 landed (HUD +authoritative_ackrefresh) and points back to the plan/manual QA docs.
Blocking issues
-
The movement-driven refresh path described by the plan, README, and manual QA is not reachable during normal locomotion.Done. Added a separatemain.gdconnectsTargetSelectionClient.on_authoritative_position_snap()toPositionAuthorityClient.authoritative_position_received, butposition_authority_client.gdonly emits that signal from_emit_position_from_response()duringsync_from_server()and the move-rejection recovery path. Successfulmove-streamPOSTs explicitly do not emit the signal, so walking around with a held lock never triggers the throttledGET /targetrefresh that NEO-24 depends on. As a result, the HUD can stay stuck atValidity: okafter the player walks out of range, which breaks the plan's Decision 2, the acceptance criterion for soft-lock refresh, and manual QA section 4.authoritative_ack(world)signal onPositionAuthorityClientthat fires on bothBOOT_GET200 and successfulmove-stream200 (extracted a shared_parse_world_from_responsehelper).authoritative_position_receivedkeeps its current snap-only semantics so rubber-band suppression is untouched.TargetSelectionClient.on_authoritative_ack(world)replaceson_authoritative_position_snap; cooldown + lock gate unchanged.main.gdconnects to the new signal. Seeclient/scripts/position_authority_client.gd,target_selection_client.gd,main.gd, and plan Decision 3.Relevant behavior in
client/scripts/position_authority_client.gd:match _phase: Phase.BOOT_GET: _busy = false if response_code == 200: _emit_position_from_response(text, true) Phase.STREAM_POST: ... if response_code == 200: # Do not emit `authoritative_position_received` here. passMeanwhile
client/scripts/target_selection_client.gdonly refreshes from that signal:func on_authoritative_position_snap(_world: Vector3, apply_as_snap: bool) -> void: if not apply_as_snap: return if not _has_lock(): return ... request_sync_from_server()The fix needs to either emit a suitable movement/reconciliation signal during normal locomotion or hook the target refresh to another authoritative movement event that actually occurs while moving.
Suggestions
Add an integration-level test for the real movement/targeting wiring. The currentDone. Addedclient/test/target_selection_client_test.gdsuite callson_authoritative_position_snap()directly, which is why it misses the broken connection toPositionAuthorityClient. A focused test aroundmain.gdwiring orPositionAuthorityClientsignal behavior would prevent this regression.client/test/target_refresh_on_locomotion_test.gd: wires a realPositionAuthorityClientandTargetSelectionClientwith mock transports and the sameauthoritative_ack→on_authoritative_ackconnectionmain.gduses. Verifies (a) amove-stream200 with a held lock produces exactly one throttledGET /targetand the cached state flips toout_of_range, and (b) the same path is a no-op when no lock is held. Also addedtest_sync_boot_get_200_emits_authoritative_ackandtest_move_stream_post_200_emits_authoritative_acktoposition_authority_client_test.gdso renames on either side will break the per-script suites too.UpdateDone. E1.M3 row now reflects NEO-24 landed (HUD +docs/decomposition/modules/documentation_and_implementation_alignment.mdif this story lands. ItsE1.M3snapshot still says client tab-target is open, which will be stale after merge.authoritative_ackrefresh) with links to the plan and manual QA file.
Nits
None.
Verification
- Ran
godot --headless --import --path . --quit-after 10fromclient/. - Ran
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://test/position_authority_client_test.gdfromclient/— suite passed, including the newauthoritative_ackassertions. - Ran
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://test/target_selection_client_test.gdfromclient/— suite passed, including the renamed ack-based refresh tests. - Ran
godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://test/target_refresh_on_locomotion_test.gdfromclient/— integration suite passed, confirmingmove-stream200 triggers exactly one throttled target refresh while locked and none while unlocked. - Read diagnostics for the touched client/scripts, client/test, plan, and review files — no linter errors found.
- I did not run the full manual client/server checklist;
docs/manual-qa/NEO-24.mdsection 4 remains the highest-value end-to-end sanity check if you want one more runtime pass before merge.