WIP checkpoint: Implement avatar loader and customization UI in Godot
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user