273 lines
7.0 KiB
GDScript
273 lines
7.0 KiB
GDScript
## 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).
|
|
# Each entry: { category, mount_bones, exclusive, is_body_part }
|
|
var _slot_data: Dictionary = {}
|
|
var _slot_definitions: Array[SlotDefinition] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_build_registry()
|
|
|
|
|
|
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)
|