Avatar loader & customization system: merge onto main + fix regressions, add smoke coverage #53

Merged
agent-bot merged 27 commits from agent/896fbb97de1d into main 2026-08-21 01:35:29 +00:00
2 changed files with 79 additions and 1 deletions
Showing only changes of commit 4c0af03573 - Show all commits
+29 -1
View File
@@ -22,6 +22,9 @@ var slots: Dictionary = {}
## Validation errors found during the last validate() call. ## Validation errors found during the last validate() call.
var validation_errors: Array[String] = [] var validation_errors: Array[String] = []
## Skin tone color override (default: transparent = use asset colors).
var skin_tone: Color = Color.TRANSPARENT
func _init() -> void: func _init() -> void:
validation_errors = [] validation_errors = []
@@ -55,6 +58,17 @@ static func from_json(json_string: String) -> AvatarLoadout:
else: else:
loadout.validation_errors.append("Missing 'slots' in loadout JSON") loadout.validation_errors.append("Missing 'slots' in loadout JSON")
# Parse skin_tone
if root.has("skin_tone"):
var st = root["skin_tone"]
if typeof(st) == TYPE_DICTIONARY:
loadout.skin_tone = Color(
st.get("r", 0.0),
st.get("g", 0.0),
st.get("b", 0.0),
st.get("a", 1.0)
)
return loadout return loadout
@@ -62,7 +76,7 @@ static func from_json(json_string: String) -> AvatarLoadout:
func to_json() -> String: func to_json() -> String:
var data := { var data := {
"base_body": base_body, "base_body": base_body,
"slots": {} "slots": {},
} }
for slot_id in AvatarSlotRegistry.SLOT_IDS: for slot_id in AvatarSlotRegistry.SLOT_IDS:
@@ -71,6 +85,15 @@ func to_json() -> String:
else: else:
data.slots[slot_id] = slots.get(slot_id, null) data.slots[slot_id] = slots.get(slot_id, null)
# Include skin_tone if set
if skin_tone != Color.TRANSPARENT:
data["skin_tone"] = {
"r": skin_tone.r,
"g": skin_tone.g,
"b": skin_tone.b,
"a": skin_tone.a,
}
return JSON.stringify(data, " ") return JSON.stringify(data, " ")
@@ -132,6 +155,11 @@ func validate() -> bool:
return validation_errors.is_empty() return validation_errors.is_empty()
## Convenience alias for validate().
func is_valid() -> bool:
return validate()
## Check if a specific slot is equipped with a non-null item. ## Check if a specific slot is equipped with a non-null item.
func is_equipped(slot_id: String) -> bool: func is_equipped(slot_id: String) -> bool:
if not AvatarSlotRegistry.has_slot(slot_id): if not AvatarSlotRegistry.has_slot(slot_id):
+50
View File
@@ -47,9 +47,32 @@ const ATTACHMENT_SLOTS: PackedStringArray = [
const MULTI_SLOT_IDS: PackedStringArray = ["accessory"] 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). # Slot metadata lookup tables (indexed by SLOT_IDS position).
# Each entry: { category, mount_bones, exclusive, is_body_part } # Each entry: { category, mount_bones, exclusive, is_body_part }
var _slot_data: Dictionary = {} var _slot_data: Dictionary = {}
var _slot_definitions: Array[SlotDefinition] = []
func _init() -> void: func _init() -> void:
@@ -150,6 +173,18 @@ func _build_registry() -> void:
}, },
} }
# 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 # Public API
@@ -165,6 +200,21 @@ static func has_slot(slot_id: String) -> bool:
return slot_id in SLOT_IDS 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"). ## Returns the category for a slot ("body_part" or "attachment").
static func get_category(slot_id: String) -> String: static func get_category(slot_id: String) -> String:
if not has_slot(slot_id): if not has_slot(slot_id):