PC3 Phase 1.4-1.6: avatars wired into the game end to end
- 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
This commit is contained in:
+45
-3
@@ -40,6 +40,7 @@ var _auth_button: Button
|
||||
var _status_label: Label
|
||||
var _welcome_label: Label
|
||||
var _play_button: Button
|
||||
var _customize_button: Button
|
||||
var _quit_button: Button
|
||||
|
||||
var _is_register_mode: bool = false
|
||||
@@ -63,6 +64,7 @@ func _ready() -> void:
|
||||
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
|
||||
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
|
||||
_play_button = $CenterContainer/VBoxContainer/PlayButton
|
||||
_customize_button = $CenterContainer/VBoxContainer/CustomizeButton
|
||||
_quit_button = $CenterContainer/VBoxContainer/QuitButton
|
||||
|
||||
# Default to Play being gated. When a token exists, the async session
|
||||
@@ -70,6 +72,7 @@ func _ready() -> void:
|
||||
# keeping the button disabled until validation succeeds prevents a fast
|
||||
# click from navigating to world_list with an expired/invalid token.
|
||||
_play_button.disabled = true
|
||||
_customize_button.disabled = true
|
||||
|
||||
# Check whether a valid session already exists (e.g. user logged in,
|
||||
# navigated to world_list, then clicked Back). If so, restore the
|
||||
@@ -80,6 +83,7 @@ func _ready() -> void:
|
||||
var username: String = str((me_result.data as Dictionary).get("username", ""))
|
||||
if username != "":
|
||||
_show_authenticated(username)
|
||||
await _prime_avatar()
|
||||
auth_state_ready.emit()
|
||||
return
|
||||
|
||||
@@ -115,6 +119,7 @@ func _show_authenticated(username: String) -> void:
|
||||
_auth_button.visible = false
|
||||
_auth_mode_label.visible = false
|
||||
_play_button.disabled = false
|
||||
_customize_button.disabled = false
|
||||
_auth_buttons_row.visible = false
|
||||
print("Restored authenticated session for: ", username)
|
||||
|
||||
@@ -129,6 +134,7 @@ func _reset_to_logged_out() -> void:
|
||||
_over_18_check.visible = false
|
||||
_over_18_check.button_pressed = false
|
||||
_play_button.disabled = true # Gate play behind successful auth.
|
||||
_customize_button.disabled = true
|
||||
_update_auth_mode_ui()
|
||||
|
||||
|
||||
@@ -142,6 +148,12 @@ func _on_play_button_pressed() -> void:
|
||||
get_tree().change_scene_to_file("res://scenes/world_list.tscn")
|
||||
|
||||
|
||||
func _on_customize_button_pressed() -> void:
|
||||
if _customize_button.disabled:
|
||||
return
|
||||
get_tree().change_scene_to_file("res://scenes/avatar_customization.tscn")
|
||||
|
||||
|
||||
func _on_quit_button_pressed() -> void:
|
||||
get_tree().quit()
|
||||
|
||||
@@ -214,7 +226,7 @@ func _on_auth_button_pressed() -> void:
|
||||
_clear_status()
|
||||
|
||||
var result := await ApiClient.register(username, email, password, true)
|
||||
_on_auth_complete(result)
|
||||
await _on_auth_complete(result)
|
||||
else:
|
||||
_auth_button.disabled = true
|
||||
_auth_button.text = "Logging in..."
|
||||
@@ -222,7 +234,7 @@ func _on_auth_button_pressed() -> void:
|
||||
_clear_status()
|
||||
|
||||
var result := await ApiClient.login(username, password)
|
||||
_on_auth_complete(result)
|
||||
await _on_auth_complete(result)
|
||||
|
||||
|
||||
func _on_auth_complete(result: Dictionary) -> void:
|
||||
@@ -232,7 +244,10 @@ func _on_auth_complete(result: Dictionary) -> void:
|
||||
_is_authenticating = false
|
||||
|
||||
if result.ok:
|
||||
# Success — hide the auth form fields, show only the welcome label.
|
||||
# Update the UI to the authenticated state first, synchronously, so
|
||||
# the player sees "Welcome" + an enabled Play immediately -- then
|
||||
# prime the avatar (a catalog load + a loadout fetch) in the
|
||||
# background. A slow or failing prime must never gate getting in.
|
||||
_username_edit.visible = false
|
||||
_password_edit.visible = false
|
||||
_email_edit.visible = false
|
||||
@@ -243,13 +258,40 @@ func _on_auth_complete(result: Dictionary) -> void:
|
||||
_welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges()
|
||||
_welcome_label.visible = true
|
||||
_play_button.disabled = false
|
||||
_customize_button.disabled = false
|
||||
_auth_buttons_row.visible = false
|
||||
print("Auth successful. Token stored.")
|
||||
await _prime_avatar()
|
||||
else:
|
||||
# Failure — show backend error detail.
|
||||
_show_status(result.detail)
|
||||
|
||||
|
||||
# ─── Avatar priming ───────────────────────────────────────────────────────────
|
||||
|
||||
## After a successful auth, load the asset catalog and the player's saved
|
||||
## loadout so the in-world player renders as their avatar. Best-effort: a
|
||||
## failure here just means the player falls back to the default capsule, it
|
||||
## never blocks getting into the game.
|
||||
func _prime_avatar() -> void:
|
||||
if not Engine.has_singleton("AvatarSystem") and get_node_or_null("/root/AvatarSystem") == null:
|
||||
return
|
||||
var avatars = get_node_or_null("/root/AvatarSystem")
|
||||
if avatars == null:
|
||||
return
|
||||
if not await avatars.ensure_catalog():
|
||||
push_warning("main_menu: avatar catalog failed to load; using fallback look")
|
||||
return
|
||||
var res: Dictionary = await ApiClient.get_avatar_loadout()
|
||||
if res.ok and res.data is Dictionary and (res.data as Dictionary).has("data"):
|
||||
avatars.set_loadout_from_dict((res.data as Dictionary)["data"])
|
||||
else:
|
||||
# No saved loadout reachable -- fall back to the catalog's first base body.
|
||||
var bases: Array = avatars.get_assets_for_slot("base_body")
|
||||
if bases.size() > 0:
|
||||
avatars.set_loadout_from_dict({"base_body": bases[0], "slots": {}})
|
||||
|
||||
|
||||
# ─── Status helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
func _show_status(message: String) -> void:
|
||||
|
||||
Reference in New Issue
Block a user