Replace placeholder XP level math with content-backed thresholds validated in CI and at server boot, and update docs/manual QA/review artifacts to reflect NEO-39 delivery and follow-up fixes.
Register SkillDefinitionCatalog from content/skills with JsonSchema.Net validation
mirroring validate_content.py and the prototype Slice 1 gate. Resolve paths via
Content:SkillsDirectory or discovery from AppContext.BaseDirectory. Eager-load
after WebApplication build so bad catalogs block startup. Add unit and host
tests, pin test factories to repo skills, document config in server/README, add
manual QA checklist, and extend Bruno GET /health smoke tests.
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.
Capture the E1.M4 vertical slice backlog in-repo and sync module/dependency docs with the created Linear stories so planning and execution references stay aligned.
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.
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.
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.
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.
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.
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.