## 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)