Avatar loader & customization system: merge onto main + fix regressions, add smoke coverage

Merges the pre-existing avatar system implementation (catalog, slot
registry, loadout, glTF-based loader, customization UI, visualizer --
~2300 lines across scripts/avatar/) onto current main, which had
diverged significantly since this branch was last touched (PC1 UI
fixes, PC2's playable level/player/multiplayer work).

Fixes found while integrating:
- project.godot had wholesale-replaced the [autoload] section again
  (same recurring pattern as the multiplayer server/client branches),
  dropping the ApiClient autoload entirely. Restored it additively
  alongside AvatarSystem.
- All other real conflicts (main_menu.tscn/.gd, main.tscn, .godot
  cache) were pure "this branch re-created a stale duplicate of
  something main already has" -- resolved by taking main's version,
  confirmed via `git diff` that this task's own changes never touched
  those files' actual logic.

New: scripts/smoke_test_avatar_system.gd -- confirms AvatarSystem is a
real autoload, the committed catalog.json parses, loadout equip/
validate/JSON-round-trip works against real catalog entries, and --
since assets/avatars/*.glb model files don't exist yet -- that
AvatarLoader.load_base_body() fails gracefully (null + load_error
signal) rather than crashing when given a real catalog entry with no
backing file. 10/10 passing.

Verified no regressions: all 10 pre-existing Godot smoke suites still
pass against a live backend, and all 61 backend pytest tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN
This commit is contained in:
2026-08-21 01:34:51 +00:00
parent 59ca5a4c6e
commit b6e146e4b2
3 changed files with 109 additions and 4 deletions
+10 -4
View File
@@ -7,14 +7,17 @@ to know what "run the tests" actually checked.
Run everything below in one shot with `scripts/run_checkpoint_tests.py`.
Last verified against `main`: 2026-08-21 — migrations + pytest + all 10
Last verified against `main`: 2026-08-21 — migrations + pytest + all 11
Godot smoke tests, all clean. PC1's real playtest-found bugs (login form
unreachable, 1px world cards, Play → Back losing the session) are covered
by the smoke tests below so they can't silently regress. PC2 (basic
playable world + character + movement) landed 2026-08-20/21 — see
`docs/checkpoints/` once a PC2 record exists, and the three new smoke
tests below (`smoke_test_test_level.gd`, `smoke_test_player.gd`,
`smoke_test_enter_world.gd`) for what's actually verified.
`smoke_test_enter_world.gd`) for what's actually verified. The avatar
loader/customization system (`smoke_test_avatar_system.gd`) also landed
2026-08-21 — real glTF-based loading with graceful failure on missing
assets, verified since `assets/avatars/*.glb` model files don't exist yet.
## Backend — `backend/tests/` (pytest)
@@ -53,8 +56,10 @@ these yet):
## Godot client — `scripts/smoke_test_*.gd`
**11 smoke tests merged into `main`.** Ten need a live backend, one
(`smoke_test_network_client.gd`) needs a live dedicated server instead —
**12 smoke tests merged into `main`.** Ten need a live backend, one
(`smoke_test_network_client.gd`) needs a live dedicated server instead,
and one (`smoke_test_avatar_system.gd`) needs neither — it only exercises
local catalog/loadout/loader logic. Backend-dependent tests read
`AGP_API_BASE_URL` env var, falling back to the
`application/agp/api_base_url` project setting (default
`http://127.0.0.1:8000`) if unset.
@@ -72,6 +77,7 @@ these yet):
| `smoke_test_player.gd` | Loads `scenes/game.tscn` and confirms the player exists as a real `CharacterBody3D` with a `CollisionShape3D`+shape, a `CapsuleMesh`, and spawns at the level's `PlayerSpawn` marker position. Also asserts `JUMP_VELOCITY` is positive and a `Camera3D` exists and is `current` — both were real bugs found 2026-08-20/21 (jump velocity was negative, pushing the player down instead of up; no camera existed at all, so the game was unplayable even though the character could technically move). Structural checks only — real movement/jump *direction* correctness was verified separately via a throwaway headless physics simulation during development, not by an automated test that's part of this suite. |
| `smoke_test_enter_world.gd` | The full PC2 path end to end: register → seed a real world → load `world_list.tscn` → click the real "Enter World" button on a real card (found by walking the card's children, not by index, so it doesn't silently pass if the card layout changes) → confirms `scenes/game.tscn` actually becomes `current_scene` with the player spawned at the level's `PlayerSpawn` marker → sends a real `KEY_ESCAPE` input event (not a direct method call) and confirms it returns to `world_list.tscn`. This is the test that proves "browse → enter → walk around → leave" actually works as one continuous flow, not just that each piece works in isolation. |
| `smoke_test_network_client.gd` | **Real two-process integration test** for `scripts/network_client.gd` (client-side prediction + reconciliation, docs/architecture/networking.md §5) — needs an actual dedicated server running first: `godot --headless --path . scenes/server.tscn &`, *not* the usual HTTP backend. Connects a real `NetworkClient` over ENet, confirms local input prediction moves the player immediately (no waiting on a round trip), confirms real snapshots arrive and get processed, then deliberately desyncs the predicted position by 9999 units and confirms the very next snapshot corrects it back — the actual proof reconciliation logic works, not just that it never had to fire. Known simplification, stated in the script's own docstring: the server's snapshot doesn't currently echo back a per-peer "last input seq processed," so reconciliation corrects against the *most recent* prediction rather than true tick-exact rollback-and-replay (docs/architecture/networking.md §5.2 step 4) — a real, scoped follow-up once the server echoes that ack, not something faked here. |
| `smoke_test_avatar_system.gd` | Confirms `AvatarSystem` is a real autoload, the committed `assets/avatars/catalog.json` actually parses, `get_assets_for_slot("base_body")` returns real entries, an `AvatarLoadout` can set `base_body` and equip a real catalog accessory, the loadout round-trips through `to_json()`/`load_loadout_from_json()`, and — since `assets/avatars/*.glb` model files don't exist yet — that `AvatarLoader.load_base_body()` against a real catalog entry with a missing file fails *gracefully*: returns `null` and emits `load_error` rather than crashing. Visual rendering itself can't be verified until real assets exist. |
Run (once, to build the class cache): `godot --headless --path . --import`,
then for each smoke test: `godot --headless --path . --script