Commit Graph

49 Commits (09e160e955a18a59ea91c001ebdf22f5cab3fb9a)

Author SHA1 Message Date
VinPropane 96fe4d11f0 NEO-26: gdformat selection_event_client_test.gd 2026-04-25 19:43:25 -04:00
VinPropane 1882476fd3 NEO-26: address code review — tracking docs, sequence tests, review strikethrough
Align E1.M3 rows with landed SelectionEvent (NEO-26) vs follow-on scope.
Assert selection_event sequence in GdUnit. Mark review suggestions/nits done.
2026-04-25 19:41:46 -04:00
VinPropane 5127fe59a7 NEO-26: emit selection_event from TargetSelectionClient
Add selection_event signal when lockedTargetId changes; attribute tab, clear,
and server_correction from POST/GET paths. Optional log_selection_events print.
GdUnit coverage; README, E1.M3 snapshot, manual QA, and plan checklist updated.
2026-04-25 19:33:53 -04:00
VinPropane ac2916bfee NEO-25: satisfy gdlint/gdformat for pre-push hook 2026-04-24 20:56:04 -04:00
VinPropane c17526e611 NEO-25: close code review follow-up (alignment, warning, tests) 2026-04-24 20:54:26 -04:00
VinPropane 0e72d98177 NEO-25: move resource_node anchor clear of Obstacle at (6,1,5) 2026-04-24 20:42:12 -04:00
VinPropane ce54fcc44f NEO-25: world interactables GET, registry rows, fetch-driven Godot props + glow 2026-04-24 20:36:15 -04:00
VinPropane d6ef4fd811 NEO-24: race-free target/select via optional positionHint
Followup #5: after locking alpha, walking into beta's ring, stopping and
pressing Tab could still return `out_of_range` even when client and server
agreed on position (Δ=0). The freshness-kick move-stream POST and the
target/select POST race on separate HTTP connections so the select can be
validated against a stale stored snap.

Extend TargetSelectRequest v1 with an optional positionHint {x,y,z}.
Server uses the hint for the radius check and for the echoed
targetState.validity; the hint is advisory and does not write to the
position store (move-stream stays the sole write path). Client attaches
the hint automatically whenever set_freshness_kick(...) is wired, so
headless unit tests that skip the wiring fall through to the no-hint
behavior. Freshness kick is kept in place to refresh the stored snap for
later validity GETs but is no longer load-bearing for denial correctness.

Server: 3 new tests (hint accepts when stored is stale, hint-far denies,
hint never writes to position store). Client: 1 new test asserting the
hint is in the POST body when wired. All 62 server + 96 client tests
pass.

Plan Decision 8, review follow-up #5, manual QA section 6, and NEO-23
contract notes updated. New bruno request exercises the hint path.
2026-04-21 23:52:20 -04:00
VinPropane a7453eb82f NEO-24: unify prototype target radius at 6 m, overlap rings at origin
All prototype targets now share `PrototypeTargetRegistry.SharedLockRadius`
(= 6 m). Anchors moved to alpha `(-3, 0.5, -3)` and beta `(3, 0, 3)` so
the two 6 m rings overlap ~3.5 m wide centered at origin, letting Tab
flip between targets without locomotion. Spawn `(-5, -5)` keeps alpha
in range (~2.83 m) and beta out (~11.31 m) so the existing soft-lock
denial QA path at spawn still holds.

`prototype_target_constants.gd` mirrors the shared radius + new anchors.
The range-aware Tab picker now **skips the currently-locked id** so Tab
always expresses a swap intent — if the only in-range target is already
locked, the fallback picks the next cycle id and the server denies
(preserves NEO-23's "Tab = visible denial when no swap possible" rule).

Tests:
- Server: new `PlayerTargetStateReaderTests.ComputeValidity_AtOrigin_…`
  asserts both targets report `Ok` at origin. Existing boundary test
  rewritten for the new alpha anchor.
- Client: new `test_tab_from_locked_alpha_at_origin_swaps_to_beta_when_both_in_range`
  and `test_tab_when_only_current_lock_is_in_range_falls_back_to_cycle_for_denial`.
  Existing in-range/out-of-range positions updated for the new layout.

Server 59/59 and client 95/95 suites pass.

Plan Decision 7, review follow-up #4, manual QA section 5 (new "Tab-flip
at origin" overlap sanity), and README updated. Per-target radii will
return with real combat design (E5.M1) — this is a prototype stub.
2026-04-21 23:37:42 -04:00
VinPropane 63fecb1099 NEO-24: range-aware Tab cycle skips out-of-range anchors before POST
Previously Tab from no-lock always picked `ORDERED_IDS[0]` (alpha), so
standing next to beta and pressing Tab was denied with `out_of_range`
even though the client HUD clearly showed beta in range.

Added `PrototypeTargetConstants.next_in_range_id_after(current, world)`
which walks the cycle and returns the first anchor whose client-side
horizontal distance is within the anchor radius.
`TargetSelectionClient.request_tab_next()` prefers that pick when the
player reference is wired (same path `set_freshness_kick` uses) and
falls back to the plain cycle order when nothing is in range so the
server still owns the denial reason.

Server contract (NEO-23) untouched. Tests cover the beta pick, the
nothing-in-range fallback, and the no-player-ref regression. Plan
Decision 6 + review follow-up #3 + README + manual QA updated.
2026-04-21 23:26:56 -04:00
VinPropane e31cd2df68 NEO-24: freshen server position before target-select + surface divergence on HUD
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.
2026-04-21 23:21:16 -04:00
VinPropane 71ae829482 NEO-24: add visible target-anchor markers + HUD distance lines
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.
2026-04-21 23:04:03 -04:00
VinPropane 435a32b3b3 NEO-24: fix movement-driven target refresh via authoritative_ack signal
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.
2026-04-21 22:35:41 -04:00
VinPropane ee572e3661 NEO-24: client tab-target + lock HUD synced to server
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`.
2026-04-21 22:14:16 -04:00
VinPropane abb6e6b81b NEO-22: Add Godot .uid sidecars for WASD locomotion scripts 2026-04-17 20:51:16 -04:00
VinPropane 735f569496 NEO-22: Fix locomotion test static calls for GDScript + LSP
Type preload as GDScript so static helpers resolve on the script
resource; pass Array[float] literals into median_feet_y_from_samples to
match typed parameters. Document FLOOR_RAY_FEET_INVALID sync in tests.
2026-04-17 20:24:16 -04:00
VinPropane c79c58c298 NEO-22: Unit-test WASD locomotion pure helpers
Extract static helpers (floor-ray continuation, median feet Y, micro-slip
XZ projection, wish-aligned horizontal velocity) from the tick path so
GdUnit can cover seam math without physics doubles. Add
player_locomotion_wasd_test.gd, document in README, and update NEO-22
plan/review.
2026-04-17 20:16:25 -04:00
VinPropane 90a17bc43d NEO-22: Extract WASD locomotion for gdlint max-file-lines
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.
2026-04-17 20:13:36 -04:00
VinPropane a7e6a5af59 NEO-22: Fix GdUnit warnings in position_authority_client_test
Rename mock variables to avoid shadowing GdUnitTestSuite.mock, drop
redundant await on synchronous assert_signal chains, and ignore
unused request_completed on HangingHttpTransport (API parity only).
2026-04-17 20:07:27 -04:00
VinPropane a4095cb9bf NEO-22: Remove click-to-move client and legacy move POST/verify
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).
2026-04-17 20:06:46 -04:00
VinPropane 6cc2ac60d4 NEO-22: stop applying move-stream HTTP body as client teleport
Successful move-stream responses echoed server position roughly one RTT behind
integrated move_and_slide, so apply_as_snap still fired whenever dh exceeded
the locomotion skip threshold. Treat stream 200 as ack-only: do not emit
authoritative_position_received. Boot GET and click-move verify unchanged.
Update README + GdUnit expectation.
2026-04-17 19:34:45 -04:00
VinPropane a2c6c9f93e NEO-22: add move-stream API and WASD client locomotion
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.
2026-04-17 19:21:33 -04:00
VinPropane a442f02dbd NEON-29: Fix CI tests (horizontal step flag, idle_support vectors) 2026-04-15 23:57:13 -04:00
VinPropane 9bdd928c80 NEON-29: Fix gdlint (player returns/lines, split idle tests, gdlintrc cap) 2026-04-15 23:48:15 -04:00
VinPropane 4a51e64f25 Merge branch 'main' into NEON-29-expand-prototype-district 2026-04-13 13:22:51 -04:00
VinPropane 7e17ff563b chore: point docs and rules at Linear NEO issue ids
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.
2026-04-13 13:15:32 -04:00
VinPropane 865eed2e53 NEON-29: Ease flat-floor idle latch (HUD XZ drift)
Raise stable-idle floor up-dot entry to 0.998 so Jolt normals on a flat
box can latch the idle anchor; 0.999 rarely tripped and corrective
move_and_slide + rim/bump nudges kept moving XZ.

Add STABLE_IDLE_ENTER_STREAK_FRAMES (2) before first latch; arrival
path still latches immediately. Unlatch budget 8→10 ticks.
2026-04-12 17:33:49 -04:00
VinPropane 591db65b4a NEON-29: Fix idle_support_is_stable test normal vs 0.999 dot
(0.02, 0.995).normalized() dots ~0.9998 with UP, so default
STABLE_IDLE_FLOOR_MIN_UP_DOT (0.999) still treats it as flat.
Use the same tilted normal as min_flat_up_dot_parameter test.
2026-04-12 17:27:37 -04:00
VinPropane edec9158e4 NEON-29: Hysteresis for walk vertical routing (tread ping-pong)
feet_y wobble from move_and_slide on approach treads crossed
DESCEND_GOAL_Y_MARGIN each frame, toggling path vs direct steer.
Schmitt latch (WALK_VERT_ROUTE_LATCH_ON_SEP/OFF_SEP) with reset on
nav lifecycle. Extend debug_idle_trace with feet_y, vert_sep, vlat,
ncol. Add player_test for latch reset.
2026-04-12 17:17:36 -04:00
VinPropane 46f8851a1a NEON-29: Hold-threshold stable idle + physics HUD coords
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.
2026-04-12 17:13:37 -04:00
VinPropane 5f6058b0ce NEON-29: Stable idle on flat tread next to riser (loose+ridged)
loose_ticks+ridged no longer forces ~0.8s corrective idle when floor normal
is still level (tread corner with vertical riser). Gate that branch on
shallow floor (dot < IDLE_RIM_MIN_FLOOR_UP_DOT). Bump unlatch streak to 5.
Update GdUnit + NEON-29 plan.
2026-04-12 17:09:55 -04:00
VinPropane 8d9ad0ecd3 NEON-29: Stable idle latch + Y anchor for approach treads
Hold latched zero-vel idle through STABLE_IDLE_UNLATCH_TICKS consecutive
unstable readings so thin tread lips do not drop into corrective move_and_slide
every other frame. Idle anchor now freezes Y as well as XZ. Reset latch on
goal/snap/arrival; seed latch on arrival when support is stable after idle tick.
Update hold_idle_anchor GdUnit expectation.
2026-04-12 17:06:30 -04:00
VinPropane 0f4c23b815 NEON-29: Reduce flat-floor idle XZ jitter after walks
Stop forcing moving floor_max_angle for the full post-stop window on open
flat support; tighten ridged wallish threshold; relax stable floor dot;
on arrival, take stable-idle path when support is already stable after
one idle tick. Add GdUnit case for shallow seam normals.
2026-04-12 16:56:40 -04:00
VinPropane 1867e3e45c NEON-29: Fix idle jitter on flat treads after stop
idle_support_is_stable no longer treats every post-stop loose-tick frame as
unstable; only slide normals showing floor+wall (lip) keep corrective idle.
Deduplicate ridged-contact check via idle_slide_contacts_are_ridged.
Relax STABLE_IDLE_FLOOR_MIN_UP_DOT slightly for Jolt normal flicker.
Update GdUnit cases and NEON-29 plan note.
2026-04-12 16:50:21 -04:00
VinPropane dbd9d67f45 NEON-29: Cap step assist to navigation agent_max_climb (0.35 m).
TerracePlatformB top is ~0.6 m above the floor; agent_max_climb only
limited nav links, while chained step assist could still climb in one
click. Refuse assist when goal surface is more than 0.35 m above feet
so the gold TerraceStepB path (or a second click) is required.
2026-04-12 01:12:10 -04:00
VinPropane ef211ab4ac NEON-29: fix arrival check using actual capsule bottom for vert_err
The vertical arrival error was computed with CAPSULE_HALF_HEIGHT (0.5),
placing code-feet at body_y - 0.5 = 0.4 on a floor-level player.
Step surfaces at Y=0.3 are only 0.1 m away from that reference, and
Jolt can nudge the body down to ~0.8 when the bottom hemisphere contacts
the step edge, making code-feet = 0.3 and vert_err = 0 — triggering
immediate arrival while the player is still at floor level.

Fix: pass CAPSULE_HALF_HEIGHT + PLAYER_CAPSULE_RADIUS (= 0.9) to
vertical_arrival_error so actual feet = body_y - 0.9 = 0.0 on the
floor. Step goals (Y=0.3) now give vert_err=0.3 >> VERT_ARRIVE_EPS,
preventing premature arrival. Arrival fires correctly once the step
assist lifts the player onto the surface (body_y=1.2, err=0).
Add two tests pinning the new pre/post-climb behaviour.
2026-04-11 19:35:39 -04:00
VinPropane 15d304fa2c NEON-28: camera contracts, E6.M2 adjacency, occluder purge 2026-04-10 22:11:40 -04:00
VinPropane a66bf885f2 NEON-16: apply repo-local gdformat
Apply the repo-local gdformat output to the touched GDScript files so the pre-push formatting hook passes. This is a formatting-only follow-up to the lint-hook cleanup.
2026-04-10 00:50:29 -04:00
VinPropane 4b861a69df NEON-16: satisfy GDScript lint hooks
Refactor the bump escape helper and shared test script loading so the branch passes the repo's pre-push GDScript lint checks. This keeps the runtime behavior intact while unblocking push and PR creation.
2026-04-10 00:48:11 -04:00
VinPropane 1f5c1e7619 NEON-16: harden idle jitter follow-up
Harden idle settling so flat ground, obstacle contacts, and rare bump-edge cases stop drifting instead of looping corrective nudges. Also resolves the saved review follow-up items and syncs the runtime docs to the final feet-height and idle-anchor behavior.
2026-04-10 00:46:53 -04:00
VinPropane 0fa2c0f93b NEON-30: skip occluder bodies in ground-pick raycast loop
ground_pick.gd now checks _collider_is_occluder before the walkable
break: when the ray hits a body in the "occluder" group it advances
OCCLUDER_PICK_THROUGH past the hit point and continues, unconditionally
and regardless of OcclusionPolicy fade state.  Non-occluder click
targeting is unchanged.

Three unit tests added to ground_pick_test.gd covering occluder ancestry
detection.  Plan open questions and module doc updated.
2026-04-09 22:51:22 -04:00
VinPropane f1b4e50729 NEON-27: finalise review (Approved) + Godot uid files
Review verdict updated to Approved; all blocking issues and suggestions
resolved and documented. Add auto-generated Godot .uid sidecar files for
occlusion_policy.gd and occlusion_policy_test.gd.
2026-04-08 23:55:29 -04:00
VinPropane 15d405261f NEON-27: implement OcclusionPolicy — RayCast-based material fade
Adds OcclusionPolicy resource and wires it into IsometricFollowCamera.
Each _process frame, iterative rays from the camera eye to the player
focus detect bodies tagged "occluder"; their MeshInstance3D surfaces are
overridden with a transparent StandardMaterial3D (fade_alpha=0.25, instant).
Materials restore when the body clears the ray. Null-material surfaces
(e.g. the prototype Obstacle with no material assigned) receive a plain
transparent StandardMaterial3D so they fade correctly.

- client/scripts/occlusion_policy.gd — new Resource (enabled, fade_alpha,
  occluder_group, occluder_collision_mask, max_occluder_cast_depth,
  occluder_count_log_threshold, is_valid)
- client/resources/isometric_occlusion_policy.tres — default instance
- client/scripts/isometric_follow_camera.gd — occlusion_policy export,
  _update_occlusion pipeline, _apply_occluder_fade/_restore_occluder helpers,
  static occlusion_policy_is_valid guard, _exit_tree cleanup
- client/scenes/main.tscn — Obstacle tagged "occluder", policy assigned
- client/test/occlusion_policy_test.gd — new GdUnit4 unit tests
- client/test/isometric_follow_camera_test.gd — policy guard static tests
- docs/decomposition/modules/E1_M2_IsometricCameraController.md — NEON-27
  snapshot + readability demo gate note
- docs/plans/NEON-27-implementation-plan.md — null-material decision recorded
2026-04-08 23:34:00 -04:00
VinPropane ff830813c7 NEON-26: address code review follow-ups (zoom validation, docs) 2026-04-08 22:58:28 -04:00
VinPropane 753ce84861 NEON-26: ZoomBandConfig, discrete zoom input, CameraState sync 2026-04-08 22:32:45 -04:00
VinPropane 4930fd89a6 NEON-25: isometric follow camera, CameraState, tests, and docs 2026-04-07 23:46:04 -04:00
VinPropane 4f46fa580b NS-21: fix ground_pick_test const — preload not load (Godot 4.6)
load() is not a constant expression; suite failed parse and was skipped in CI
while gdlint passed. Restores 11 tests across 3 suites.
2026-04-06 22:32:05 -04:00
VinPropane f09517af6a NS-21: fix gdlint in test suites (load-once, line length, const name) 2026-04-06 22:25:23 -04:00
VinPropane 8113e48cc8 NS-21: add GdUnit4, client tests, and CI headless run
- Vendor gdUnit4 v6.1.2 under client/addons; enable plugin in project.godot
- Tests: player.gd API, position authority HTTP flow (Node transport double),
  ground_pick walkable ancestry; test double script for injection
- position_authority_client: _create_http_request factory, Node-typed _http,
  emit move_rejected unknown for empty/missing reasonCode strings
- gdscript workflow: lint scripts+test, format check, cache Godot 4.6, GdUnit CLI
- README testing section; gitignore client/reports; testing-expectations Godot harness
2026-04-06 22:08:37 -04:00