## AvatarSlotRegistry - Static registry of slot definitions. ## ## Each slot maps to one or more bones and defines what can be equipped there. ## This is the single source of truth for slot IDs, categories, mount bones, ## exclusivity, and body-part vs attachment classification. ## ## See docs/architecture/avatar.md §3.2 for the full slot specification. class_name AvatarSlotRegistry extends RefCounted # --------------------------------------------------------------------------- # Slot definitions (mirrors §3.2 of the avatar spec) # --------------------------------------------------------------------------- ## All known slot IDs, in canonical order. const SLOT_IDS: PackedStringArray = [ "head", "hair", "face", "torso", "chest", "back", "left_arm", "right_arm", "left_hand", "right_hand", "legs", "pants", "left_foot", "right_foot", "accessory", ] ## Body-part slots (exclusive, replace base mesh). const BODY_PART_SLOTS: PackedStringArray = [ "head", "torso", "legs", "left_arm", "right_arm", ] ## Attachment slots (layered on top of base mesh). const ATTACHMENT_SLOTS: PackedStringArray = [ "hair", "face", "chest", "back", "left_hand", "right_hand", "pants", "left_foot", "right_foot", "accessory", ] ## Slots that are multi-slot (multiple accessories can coexist). const MULTI_SLOT_IDS: PackedStringArray = ["accessory"] ## Metadata for a single slot. class SlotDefinition: ## The slot ID string (e.g. "head", "hair"). var slot_key: String ## "body_part" or "attachment". var category: String ## Primary mount bone name(s). var mount_bones: PackedStringArray ## Whether at most one item can occupy this slot. var exclusive: bool ## Whether this slot replaces a base mesh. var is_body_part: bool func _init(p_slot_key: String, p_category: String, p_mount_bones: PackedStringArray, p_exclusive: bool, p_is_body_part: bool) -> void: slot_key = p_slot_key category = p_category mount_bones = p_mount_bones exclusive = p_exclusive is_body_part = p_is_body_part # Slot metadata lookup tables (indexed by SLOT_IDS position). Static: every # caller uses AvatarSlotRegistry.method(...) directly on the class, never on # an instance, so this data has to be built once at class-load time via # _static_init() rather than an instance _init() that's never invoked. # Each entry: { category, mount_bones, exclusive, is_body_part } static var _slot_data: Dictionary = {} static var _slot_definitions: Array[SlotDefinition] = [] static func _static_init() -> void: _build_registry() static func _build_registry() -> void: _slot_data = { "head": { "category": "body_part", "mount_bones": ["Head"], "exclusive": true, "is_body_part": true, }, "hair": { "category": "attachment", "mount_bones": ["HeadTop"], "exclusive": true, "is_body_part": false, }, "face": { "category": "attachment", "mount_bones": ["Jaw"], "exclusive": true, "is_body_part": false, }, "torso": { "category": "body_part", "mount_bones": ["Chest"], "exclusive": true, "is_body_part": true, }, "chest": { "category": "attachment", "mount_bones": ["Chest"], "exclusive": true, "is_body_part": false, }, "back": { "category": "attachment", "mount_bones": ["Chest"], "exclusive": true, "is_body_part": false, }, "left_arm": { "category": "body_part", "mount_bones": ["LeftUpperArm"], "exclusive": true, "is_body_part": true, }, "right_arm": { "category": "body_part", "mount_bones": ["RightUpperArm"], "exclusive": true, "is_body_part": true, }, "left_hand": { "category": "attachment", "mount_bones": ["LeftHand"], "exclusive": true, "is_body_part": false, }, "right_hand": { "category": "attachment", "mount_bones": ["RightHand"], "exclusive": true, "is_body_part": false, }, "legs": { "category": "body_part", "mount_bones": ["Hips"], "exclusive": true, "is_body_part": true, }, "pants": { "category": "attachment", "mount_bones": ["Hips"], "exclusive": true, "is_body_part": false, }, "left_foot": { "category": "attachment", "mount_bones": ["LeftFoot"], "exclusive": true, "is_body_part": false, }, "right_foot": { "category": "attachment", "mount_bones": ["RightFoot"], "exclusive": true, "is_body_part": false, }, "accessory": { "category": "attachment", "mount_bones": ["Root"], "exclusive": false, "is_body_part": false, }, } # Build SlotDefinition objects. for slot_id in SLOT_IDS: var data = _slot_data[slot_id] var def := SlotDefinition.new( slot_id, data["category"], PackedStringArray(data["mount_bones"]), data["exclusive"], data["is_body_part"], ) _slot_definitions.append(def) # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- ## Returns the list of all known slot IDs. static func get_slot_ids() -> PackedStringArray: return SLOT_IDS ## Returns true if *slot_id* is a known slot. static func has_slot(slot_id: String) -> bool: return slot_id in SLOT_IDS ## Returns the SlotDefinition for a slot, or null if unknown. static func get_slot(slot_id: String) -> SlotDefinition: if not has_slot(slot_id): return null for def in _slot_definitions: if def.slot_key == slot_id: return def return null ## Returns all SlotDefinition objects in canonical order. static func get_slot_definitions() -> Array[SlotDefinition]: return _slot_definitions ## Returns the category for a slot ("body_part" or "attachment"). static func get_category(slot_id: String) -> String: if not has_slot(slot_id): push_error("AvatarSlotRegistry: unknown slot '%s'" % slot_id) return "" return _slot_data[slot_id]["category"] ## Returns the primary mount bone(s) for a slot. static func get_mount_bones(slot_id: String) -> PackedStringArray: if not has_slot(slot_id): push_error("AvatarSlotRegistry: unknown slot '%s'" % slot_id) return PackedStringArray() return PackedStringArray(_slot_data[slot_id]["mount_bones"]) ## Returns whether the slot is exclusive (at most one item). static func is_exclusive(slot_id: String) -> bool: if not has_slot(slot_id): push_error("AvatarSlotRegistry: unknown slot '%s'" % slot_id) return true return _slot_data[slot_id]["exclusive"] ## Returns whether the slot is a body-part slot (replaces base mesh). static func is_body_part_slot(slot_id: String) -> bool: if not has_slot(slot_id): push_error("AvatarSlotRegistry: unknown slot '%s'" % slot_id) return false return _slot_data[slot_id]["is_body_part"] ## Returns whether the slot is a multi-slot (e.g. accessory). static func is_multi_slot(slot_id: String) -> bool: return slot_id in MULTI_SLOT_IDS ## Returns the list of body-part slot IDs. static func get_body_part_slots() -> PackedStringArray: return BODY_PART_SLOTS ## Returns the list of attachment slot IDs. static func get_attachment_slots() -> PackedStringArray: return ATTACHMENT_SLOTS ## Validates that an asset's declared slot matches the slot it is placed in. static func validate_slot_assignment(asset_slot: String, slot_id: String) -> bool: return asset_slot == slot_id ## Returns the canonical order index for a slot (for serialization order). static func get_slot_index(slot_id: String) -> int: return SLOT_IDS.find(slot_id)