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
- 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
_show_authenticated() (the session-restore path, used when returning to
main_menu.tscn -- e.g. after clicking Back from world_list) only hid
EmailEdit/Over18Check, unlike _on_auth_complete() (the fresh-login path)
which hides the full form: UsernameEdit, PasswordEdit, EmailEdit,
Over18Check, AuthButton, AuthModeLabel. Play was correctly enabled and
the welcome message was correctly visible, so a check that stopped at
those two passed clean -- but the actual login fields were still on
screen, which is what a real user sees as "it's making me log in again."
Confirmed via live PC1 playtest today.
Reported by: real playtest, 2026-08-20 (yesterday's automated regression
test for this exact Play -> Back flow only asserted Play/welcome/auth
buttons row -- strengthened smoke_test_main_menu_back.gd to also check
the individual form fields, and confirmed it fails exactly this way with
the bug reintroduced before restoring the fix).
Verified against a live backend: 61/61 pytest (unaffected), 8/8 Godot
smoke tests (including 20/20 on the strengthened back-nav test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN
Two things, found together while investigating a stuck blocker task:
1. URGENT: main_menu.gd currently fails to parse at all on main
("Identifier '_dob_edit' not declared in the current scope"). PR #44
(hide-all-fields-on-auth-success) and PR #45 (DOBEdit -> Over18Check
checkbox) each merged cleanly on their own -- Gitea's merge reported
mergeable:true for both -- but together they left a reference to the
old _dob_edit var on a line neither PR's diff touched, so no conflict
was ever flagged. Same failure mode in smoke_test_main_menu_full_flow.gd
(dob_edit, would have failed to even parse in the checkpoint suite).
Fixed both to reference _over_18_check / over_18_check.
2. Lands the Play -> Back session-persistence fix (PR #43, task
28357bca192d) that was stuck behind review the whole time main didn't
have it yet: signal auth_state_ready, _show_authenticated() /
_reset_to_logged_out() helpers, and _ready() restoring the
authenticated UI (not resetting it) when ApiClient already holds a
valid token. Plus the real fix a review round caught on top of that
(task 9186405a6373): _show_authenticated() was hiding AuthPanel while
claiming the welcome label inside it was visible -- a no-op UI-wise,
since WelcomeLabel.visible=true does nothing once its parent is
hidden. AuthPanel now stays visible; the regression test
(smoke_test_main_menu_back.gd, new) asserts is_visible_in_tree(), not
just .visible, so this class of bug can't pass silently again.
Verified against a live backend: 61/61 pytest, 7/7 Godot smoke tests
(including the new smoke_test_main_menu_back.gd, 12/12).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN
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
Reported directly by the user playtesting the exported exe: Play was
permanently disabled and there was no way to log in or register. Root
cause: main_menu.tscn wired Play and Quit, but nothing was ever
connected to _on_login_button_pressed()/_on_register_button_pressed() --
the AuthPanel, its form, and all its backend-calling logic were fully
correct, just completely unreachable. The script's own top comment even
said "(the existing Play/Quit buttons)" as if Play/Quit somehow doubled
as login triggers, which they never did -- a sign nobody ever actually
clicked through the running scene, only checked things at the API level.
Fix: added a real AuthButtonsRow (LoginButton, RegisterButton) wired to
the existing handlers, which now hide themselves once auth succeeds.
Also fixed grab_focus() targeting the AuthPanel (which can't accept
focus, just a background warning) to focus the username field instead.
Verified via two new smoke tests (smoke_test_main_menu_ui.gd,
smoke_test_main_menu_full_flow.gd) using a *real signal emission* on
the buttons, not a direct method call -- that distinction is exactly
what would have caught the original bug. Full click-through confirmed
against a real backend: open register form, fill it, submit, Play
actually becomes enabled, welcome message shows, auth buttons hide.
Also documents a real headless-testing gotcha found while verifying
this: a synthetic Button.pressed.emit() on an await-using handler
doesn't reliably get driven forward in --script/SceneTree mode the way
real GUI input dispatch does -- confirmed by comparing direct-call vs
emit() behavior side by side. Not a bug in the actual click-driven flow,
a test-harness quirk to route around (call the handler directly,
awaited, instead of emitting the signal).
Full checkpoint suite re-verified clean on this branch: migrations,
68/68 pytest, all 6 Godot smoke tests (including the 2 new ones, picked
up automatically by run_checkpoint_tests.py's glob).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- main_menu.gd: Play button now navigates to world_list.tscn
- world_list.gd: fix invalid theme_override_constants property-path syntax
(use add_theme_constant_override), fix @onready node paths missing
/HeaderHBox/, cast Variant dict values to String to avoid type-inference
errors when cards actually render
- world_list.tscn: wire up Back/Refresh button signal connections (scene
had zero [connection] blocks, buttons were dead)
- backend: fix real bug where World.visibility enum sent member names
("PUBLIC") to Postgres instead of lowercase values, breaking both GET
and POST /api/worlds/ for any real query
- backend: fix WorldResponse.created_at typed as str instead of datetime,
causing a second 500 on GET once the enum bug was fixed
- add smoke_test_world_list.gd: headless scene-level smoke test that
registers a user, instantiates the real scene, and confirms world cards
render from live backend data (not just an API-level check)
This branch never actually had the Godot scaffold — a workspace-reuse bug
(see agp source repo) let leftover backend/**/__pycache__/*.pyc files from
an unrelated task get swept into this branch's only prior commit, which
was checkpointed under this task's title despite containing zero Godot
files. Removing that garbage and bringing over the real project from
agent/997c429bc6b4 (the original PR's branch) with the review feedback
addressed:
- Play button no longer references the nonexistent scenes/game.tscn.
- Wired PlayButton/QuitButton [connection] entries in main_menu.tscn.
- Removed the invalid uid="uid://main_menu".
- Added .gitignore for .godot/.
- Removed the non-standard [export]/[export_options] sections from
project.godot (Godot 4 uses export_presets.cfg, not this).