POST /api/auth/register now requires a real date_of_birth (ISO YYYY-MM-DD).
The server computes age (auth.calculate_age) and rejects under-18s with 403;
a future/nonsensical date is 400. The DOB is stored and surfaced only to
that user (/api/auth/me).
Still self-attested (a user can enter a false DOB) -- but a concrete,
stored, legally-meaningful data point and the seam for a future ID-check
pass. Encryption-at-rest for the column and the Tier-2 hook are not done;
the limitation is stated plainly in docs/architecture/content_moderation.md 5.2.
- auth.py: RegisterRequest.date_of_birth (date); MIN_AGE=18; register()
computes + enforces; UserResponse.date_of_birth typed `date | None`
(the old `str | None` would have 500'd /me once a DOB was actually stored).
- api_client.gd / main_menu.gd: the Over18 checkbox is now a
"Date of birth (YYYY-MM-DD)" field with a client-side sanity check.
- tests: test_auth_age_gate.py rewritten around the endpoint + age math
(adult/minor/exactly-18/future-dob/stored); ~25 over_18 references across
6 backend test files + 6 game smoke tests updated. Backend 74 passed;
game smokes green (auth 6/6, menu_full_flow 12/12, e2e 4/4, etc.);
fresh-clone boot PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
- Session autoload: carries {world_id, host, port} from the world browser
to the game scene.
- world_list.gd "Enter World": POST /api/worlds/{id}/join -> Session.begin
-> load game.tscn. On 503 (backend has no orchestration) falls back to
local single-player; other errors surface on the card.
- api_client.gd: join_world().
- game.gd (new, on game.tscn root): local single-player by default; when
Session.active, connects a NetworkClient to the world's dedicated server
and runs networked -- forwards the local Player's input each physics
frame, snaps to the server position past a generous drift threshold
(the Player keeps predicting locally with real collision), renders other
players from snapshots (labelled capsules, lerped -- avatar appearance
sync is a follow-up, the server doesn't replicate loadouts yet), and
returns to the world list on disconnect.
- player.gd: exposes net_input (the input frame it applied) and
apply_server_correction().
- network_client.gd: send_input() + get_own_server_state() for callers that
do their own collision-aware prediction.
- scripts/mp_game_flow_client.gd + run_game_multiplayer_test.sh: two real
clients each register -> join via the API -> game.tscn -> networked;
A sees B and moves. PASS.
Verified end to end against an orchestration-enabled backend. Non-networked
smokes green (enter_world 9/9, world_list 5/5, player 15/15,
avatar_in_world 10/10, main_menu_full_flow 12/12).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
- api_client.gd: get_avatar_loadout() / save_avatar_loadout()
- main_menu.gd: on successful auth (fresh or session-restore), load the
catalog + fetch the saved loadout into AvatarSystem so the in-world
player renders as the player's avatar. Best-effort: a slow/failed prime
never gates getting in (UI goes authenticated first, prime runs after).
New "Customize Avatar" button (enabled only when signed in).
- avatar_system.gd: local-player convenience API -- ensure_catalog(),
set_loadout_from_dict() (from the backend's GET shape), has_loadout(),
build_current_avatar().
- player.gd: swaps the placeholder capsule for the assembled avatar
(feet-aligned to the capsule bottom) when a loadout is primed; silent
fallback to the capsule otherwise. Camera stays first-person for now
(third-person is a noted follow-up).
- avatar_visualizer.gd: rewrote it -- the old version referenced
AvatarLoader.SlotResult / load_slot() which never existed, so it didn't
compile and took avatar_customization_ui.gd down with it. Now a thin
wrapper over AvatarLoader for the preview.
- avatar_customization_ui.gd: seeds from the live loadout, saves through to
AvatarSystem + PUT /api/avatar/loadout (local file kept as fallback),
fixed a Camera3D.look_at() before-in-tree crash.
- scenes/avatar_customization.tscn rebuilt (old one used the Godot-3
`ViewportContainer` node type) + avatar_customization_screen.gd wrapper
that returns to the menu on close.
- smoke_test_avatar_in_world.gd: primed loadout -> game.tscn -> player
shows the assembled avatar, capsule hidden, rig + bone mounts present.
Verified: cold-cache smokes green (player 15/15, avatar_system 14/14,
avatar_in_world 10/10, main_menu_ui 13/13); against a live backend
main_menu_full_flow 12/12, main_menu_back 20/20, world_list 4/4,
enter_world 9/9, e2e 4/4; plus a manual register->default-loadout->
customize->save->re-fetch-persisted round trip.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
The register form collected and validated an actual date of birth just to
throw it away (backend never stored it beyond a nullable column). Per the
roadmap note: don't collect/store DOB right now, just get a client-side
"I am 18 or older" confirmation. RegisterRequest.date_of_birth (str) is now
over_18 (bool); the endpoint rejects over_18=false with 400 same as before.
calculate_age() is kept (unused by /register now) since real DOB collection
is coming back in a future pass and deleting it broke test_auth_age_gate.py's
import for no benefit.
Frontend: DOBEdit LineEdit replaced with an Over18Check CheckBox; api_client.gd's
register() now takes a bool instead of a date string. All Godot callers
(smoke_test_auth/e2e/world_list/main_menu_ui/main_menu_full_flow) and backend
test files updated to match. Full suite verified against a live backend:
61/61 pytest, 6/6 Godot smoke tests (33 individual assertions), all passing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN