e59cf158e5
- 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
116 lines
3.3 KiB
GDScript
116 lines
3.3 KiB
GDScript
## AvatarVisualizer
|
|
##
|
|
## Renders an AvatarLoadout into a 3D node tree for previews (the customization
|
|
## screen). A thin wrapper over AvatarLoader -- it owns its own loader instance
|
|
## and catalog reference so building a preview never disturbs AvatarSystem's
|
|
## live in-world avatar state.
|
|
##
|
|
## Usage:
|
|
## var viz := AvatarVisualizer.new()
|
|
## viz.load_loadout(loadout, preview_root_node3d) # preview_root gets the avatar as a child
|
|
## ...
|
|
## viz.clear()
|
|
|
|
class_name AvatarVisualizer
|
|
extends Object
|
|
|
|
# Preloaded rather than referenced by global class_name -- see avatar_loadout.gd.
|
|
const AvatarLoadout := preload("res://scripts/avatar/avatar_loadout.gd")
|
|
const AvatarLoader := preload("res://scripts/avatar/avatar_loader.gd")
|
|
const AvatarCatalog := preload("res://scripts/avatar/avatar_catalog.gd")
|
|
const AvatarSlotRegistry := preload("res://scripts/avatar/avatar_slot_registry.gd")
|
|
|
|
## Emitted when the visualizer has finished building the render tree.
|
|
signal build_complete
|
|
## Emitted when the visualizer has been cleared.
|
|
signal cleared
|
|
|
|
## The node the assembled avatar is parented under.
|
|
var root: Node3D = null
|
|
|
|
## The assembled avatar node (child of `root`).
|
|
var avatar_node: Node3D = null
|
|
|
|
## The loadout currently rendered.
|
|
var current_loadout: AvatarLoadout = null
|
|
|
|
var _loader = null
|
|
var _catalog = null
|
|
|
|
|
|
## Build the avatar for [param loadout] and parent it under [param root_node]
|
|
## (or the previously-set `root`). Replaces any previously-built avatar.
|
|
func load_loadout(loadout, root_node: Node3D = null) -> void:
|
|
if root_node != null:
|
|
root = root_node
|
|
if root == null or loadout == null:
|
|
return
|
|
|
|
clear()
|
|
current_loadout = loadout
|
|
|
|
if _catalog == null:
|
|
var sys = root.get_node_or_null("/root/AvatarSystem")
|
|
if sys != null:
|
|
_catalog = sys.catalog
|
|
if _catalog == null:
|
|
_catalog = AvatarCatalog.new()
|
|
_catalog.load_catalog("res://assets/avatars/catalog.json")
|
|
|
|
_loader = AvatarLoader.new()
|
|
root.add_child(_loader)
|
|
_loader.init_loader(_catalog)
|
|
|
|
var base: Node3D = _loader.load_base_body(loadout.base_body)
|
|
if base == null:
|
|
return
|
|
root.add_child(base)
|
|
avatar_node = base
|
|
|
|
for slot_id in AvatarSlotRegistry.SLOT_IDS:
|
|
var value = loadout.slots.get(slot_id)
|
|
if slot_id == "accessory":
|
|
if value is Array:
|
|
for aid in value:
|
|
if aid is String and not (aid as String).is_empty():
|
|
_loader.load_attachment(slot_id, aid)
|
|
elif value is String and not (value as String).is_empty():
|
|
_loader.load_attachment(slot_id, value)
|
|
|
|
for slot_id in AvatarSlotRegistry.BODY_PART_SLOTS:
|
|
var value = loadout.slots.get(slot_id)
|
|
if value is String and not (value as String).is_empty():
|
|
_loader.set_base_mesh_visible(slot_id, false)
|
|
|
|
build_complete.emit()
|
|
|
|
|
|
## Tear down the current preview avatar.
|
|
func clear() -> void:
|
|
if avatar_node != null and is_instance_valid(avatar_node):
|
|
avatar_node.queue_free()
|
|
avatar_node = null
|
|
if _loader != null and is_instance_valid(_loader):
|
|
_loader.queue_free()
|
|
_loader = null
|
|
current_loadout = null
|
|
cleared.emit()
|
|
|
|
|
|
func get_root() -> Node3D:
|
|
return root
|
|
|
|
|
|
func get_all_mesh_instances() -> Array[MeshInstance3D]:
|
|
var out: Array[MeshInstance3D] = []
|
|
if avatar_node:
|
|
_collect_meshes(avatar_node, out)
|
|
return out
|
|
|
|
|
|
func _collect_meshes(node: Node, out: Array[MeshInstance3D]) -> void:
|
|
if node is MeshInstance3D:
|
|
out.append(node)
|
|
for child in node.get_children():
|
|
_collect_meshes(child, out)
|