From 4c0af0357347b436d9ae1a6f9c78f8f24b92b88b Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 13:01:14 +0000 Subject: [PATCH] WIP checkpoint: Implement avatar loader and customization UI in Godot --- scripts/avatar/avatar_loadout.gd | 30 +++++++++++++++- scripts/avatar/avatar_slot_registry.gd | 50 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/scripts/avatar/avatar_loadout.gd b/scripts/avatar/avatar_loadout.gd index 2ae4fa1..1594c6f 100644 --- a/scripts/avatar/avatar_loadout.gd +++ b/scripts/avatar/avatar_loadout.gd @@ -22,6 +22,9 @@ var slots: Dictionary = {} ## Validation errors found during the last validate() call. var validation_errors: Array[String] = [] +## Skin tone color override (default: transparent = use asset colors). +var skin_tone: Color = Color.TRANSPARENT + func _init() -> void: validation_errors = [] @@ -55,6 +58,17 @@ static func from_json(json_string: String) -> AvatarLoadout: else: 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 @@ -62,7 +76,7 @@ static func from_json(json_string: String) -> AvatarLoadout: func to_json() -> String: var data := { "base_body": base_body, - "slots": {} + "slots": {}, } for slot_id in AvatarSlotRegistry.SLOT_IDS: @@ -71,6 +85,15 @@ func to_json() -> String: else: 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, " ") @@ -132,6 +155,11 @@ func validate() -> bool: 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. func is_equipped(slot_id: String) -> bool: if not AvatarSlotRegistry.has_slot(slot_id): diff --git a/scripts/avatar/avatar_slot_registry.gd b/scripts/avatar/avatar_slot_registry.gd index b7b9a2a..e466ba2 100644 --- a/scripts/avatar/avatar_slot_registry.gd +++ b/scripts/avatar/avatar_slot_registry.gd @@ -47,9 +47,32 @@ const ATTACHMENT_SLOTS: PackedStringArray = [ 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: @@ -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 @@ -165,6 +200,21 @@ 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):