WIP checkpoint: Implement avatar loader and customization UI in Godot
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
63f7b34db8d8cdea90c76aacccf841ec
|
63f7b34db8d8cdea90c76aacccf841ec
|
||||||
::res://::1787270429
|
::res://::1787270484
|
||||||
README.md::TextFile::-1::1787013015::0::1::::<><><>0<>0<><>::
|
README.md::TextFile::-1::1787013015::0::1::::<><><>0<>0<><>::
|
||||||
::res://assets/::1787270429
|
::res://assets/::1787270429
|
||||||
::res://assets/avatars/::1787270429
|
::res://assets/avatars/::1787270429
|
||||||
@@ -20,7 +20,7 @@ main_menu.gd::GDScript::6526915941780468089::1787270429::0::1::::<>Control<><>0<
|
|||||||
avatar_catalog.gd::GDScript::5104367973301127782::1787270429::0::1::::AvatarCatalog<>RefCounted<><>0<>0<><>::
|
avatar_catalog.gd::GDScript::5104367973301127782::1787270429::0::1::::AvatarCatalog<>RefCounted<><>0<>0<><>::
|
||||||
avatar_customization_ui.gd::GDScript::6000242829080071607::1787270429::0::1::::AvatarCustomizationUI<>Control<><>0<>0<><>::
|
avatar_customization_ui.gd::GDScript::6000242829080071607::1787270429::0::1::::AvatarCustomizationUI<>Control<><>0<>0<><>::
|
||||||
avatar_loader.gd::GDScript::3645080687026000779::1787270429::0::1::::AvatarLoader<>Node<><>0<>0<><>::
|
avatar_loader.gd::GDScript::3645080687026000779::1787270429::0::1::::AvatarLoader<>Node<><>0<>0<><>::
|
||||||
avatar_loadout.gd::GDScript::8780959424915557114::1787270429::0::1::::AvatarLoadout<>RefCounted<><>0<>0<><>::
|
avatar_loadout.gd::GDScript::8780959424915557114::1787271124::0::1::::AvatarLoadout<>RefCounted<><>0<>0<><>::
|
||||||
avatar_slot_registry.gd::GDScript::8212023476238218904::1787270429::0::1::::AvatarSlotRegistry<>RefCounted<><>0<>0<><>::
|
avatar_slot_registry.gd::GDScript::8212023476238218904::1787270429::0::1::::AvatarSlotRegistry<>RefCounted<><>0<>0<><>::
|
||||||
avatar_system.gd::GDScript::150798117962859248::1787270429::0::1::::<>Node<><>0<>0<><>::
|
avatar_system.gd::GDScript::150798117962859248::1787270429::0::1::::<>Node<><>0<>0<><>::
|
||||||
avatar_visualizer.gd::GDScript::6283745077729863623::1787270429::0::1::::AvatarVisualizer<>Object<><>0<>0<><>::
|
avatar_visualizer.gd::GDScript::6283745077729863623::1787270429::0::1::::AvatarVisualizer<>Object<><>0<>0<><>::
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var skin_tone: Color = Color.TRANSPARENT
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
## Represents a single equipped item in a slot.
|
## Represents a single equipped item in a slot.
|
||||||
## Used by AvatarVisualizer to iterate over equipped items.
|
## Used by consumers to iterate over equipped items.
|
||||||
class SlotItem:
|
class SlotItem:
|
||||||
## The slot this item belongs to.
|
## The slot this item belongs to.
|
||||||
var slot_id: String
|
var slot_id: String
|
||||||
@@ -60,13 +60,13 @@ func _init() -> void:
|
|||||||
static func from_json(json_string: String) -> AvatarLoadout:
|
static func from_json(json_string: String) -> AvatarLoadout:
|
||||||
var loadout := AvatarLoadout.new()
|
var loadout := AvatarLoadout.new()
|
||||||
var data: Variant = JSON.parse_string(json_string)
|
var data: Variant = JSON.parse_string(json_string)
|
||||||
if data is not Dictionary:
|
if not (data is Dictionary):
|
||||||
loadout.validation_errors.append("Loadout JSON root is not an object")
|
loadout.validation_errors.append("Loadout JSON root is not an object")
|
||||||
return loadout
|
return loadout
|
||||||
|
|
||||||
var root: Dictionary = data as Dictionary
|
var root: Dictionary = data as Dictionary
|
||||||
|
|
||||||
# Parse base_body
|
# Parse base_body.
|
||||||
if root.has("base_body"):
|
if root.has("base_body"):
|
||||||
var bb = root["base_body"]
|
var bb = root["base_body"]
|
||||||
if typeof(bb) == TYPE_STRING:
|
if typeof(bb) == TYPE_STRING:
|
||||||
@@ -74,25 +74,29 @@ static func from_json(json_string: String) -> AvatarLoadout:
|
|||||||
else:
|
else:
|
||||||
loadout.validation_errors.append("base_body must be a string")
|
loadout.validation_errors.append("base_body must be a string")
|
||||||
|
|
||||||
# Parse slots
|
# Parse slots.
|
||||||
if root.has("slots"):
|
if root.has("slots"):
|
||||||
var slots_data = root["slots"]
|
var slots_data = root["slots"]
|
||||||
if typeof(slots_data) == TYPE_DICTIONARY:
|
if typeof(slots_data) == TYPE_DICTIONARY:
|
||||||
loadout.slots = slots_data as Dictionary
|
var parsed_slots: Dictionary = slots_data as Dictionary
|
||||||
|
for slot_id in parsed_slots:
|
||||||
|
if slot_id is String:
|
||||||
|
loadout.slots[slot_id] = parsed_slots[slot_id]
|
||||||
else:
|
else:
|
||||||
loadout.validation_errors.append("slots must be an object")
|
loadout.validation_errors.append("slots must be an object")
|
||||||
else:
|
else:
|
||||||
loadout.validation_errors.append("Missing 'slots' in loadout JSON")
|
loadout.validation_errors.append("Missing 'slots' in loadout JSON")
|
||||||
|
|
||||||
# Parse skin_tone
|
# Parse skin_tone.
|
||||||
if root.has("skin_tone"):
|
if root.has("skin_tone"):
|
||||||
var st = root["skin_tone"]
|
var st = root["skin_tone"]
|
||||||
if typeof(st) == TYPE_DICTIONARY:
|
if typeof(st) == TYPE_DICTIONARY:
|
||||||
|
var tone: Dictionary = st as Dictionary
|
||||||
loadout.skin_tone = Color(
|
loadout.skin_tone = Color(
|
||||||
st.get("r", 0.0),
|
float(tone.get("r", 0.0)),
|
||||||
st.get("g", 0.0),
|
float(tone.get("g", 0.0)),
|
||||||
st.get("b", 0.0),
|
float(tone.get("b", 0.0)),
|
||||||
st.get("a", 1.0)
|
float(tone.get("a", 1.0))
|
||||||
)
|
)
|
||||||
|
|
||||||
return loadout
|
return loadout
|
||||||
@@ -107,11 +111,11 @@ func to_json() -> String:
|
|||||||
|
|
||||||
for slot_id in AvatarSlotRegistry.SLOT_IDS:
|
for slot_id in AvatarSlotRegistry.SLOT_IDS:
|
||||||
if slot_id == "accessory":
|
if slot_id == "accessory":
|
||||||
data.slots[slot_id] = slots.get(slot_id, [])
|
data["slots"][slot_id] = slots.get(slot_id, [])
|
||||||
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
|
# Include skin_tone if set.
|
||||||
if skin_tone != Color.TRANSPARENT:
|
if skin_tone != Color.TRANSPARENT:
|
||||||
data["skin_tone"] = {
|
data["skin_tone"] = {
|
||||||
"r": skin_tone.r,
|
"r": skin_tone.r,
|
||||||
@@ -132,11 +136,11 @@ func to_json() -> String:
|
|||||||
func validate() -> bool:
|
func validate() -> bool:
|
||||||
validation_errors.clear()
|
validation_errors.clear()
|
||||||
|
|
||||||
# Check base_body
|
# Check base_body.
|
||||||
if base_body.is_empty():
|
if base_body.is_empty():
|
||||||
validation_errors.append("base_body is required")
|
validation_errors.append("base_body is required")
|
||||||
|
|
||||||
# Check each slot
|
# Check each known slot.
|
||||||
for slot_id in AvatarSlotRegistry.SLOT_IDS:
|
for slot_id in AvatarSlotRegistry.SLOT_IDS:
|
||||||
var slot_def = AvatarSlotRegistry.get_slot(slot_id)
|
var slot_def = AvatarSlotRegistry.get_slot(slot_id)
|
||||||
if slot_def == null:
|
if slot_def == null:
|
||||||
@@ -146,7 +150,7 @@ func validate() -> bool:
|
|||||||
var value = slots.get(slot_id)
|
var value = slots.get(slot_id)
|
||||||
|
|
||||||
if slot_def.exclusive:
|
if slot_def.exclusive:
|
||||||
# Exclusive slots: must be null or a string
|
# Exclusive slots: must be null or a string.
|
||||||
if value != null and typeof(value) != TYPE_STRING:
|
if value != null and typeof(value) != TYPE_STRING:
|
||||||
validation_errors.append(
|
validation_errors.append(
|
||||||
"Slot '%s' must be null or a string, got %s" % [
|
"Slot '%s' must be null or a string, got %s" % [
|
||||||
@@ -154,32 +158,29 @@ func validate() -> bool:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Non-exclusive (accessory): must be null or an array
|
# Non-exclusive (accessory): must be null or an array.
|
||||||
if value != null and typeof(value) != TYPE_ARRAY:
|
if value != null and typeof(value) != TYPE_ARRAY:
|
||||||
validation_errors.append(
|
validation_errors.append(
|
||||||
"Slot '%s' must be null or an array, got %s" % [
|
"Slot '%s' must be null or an array, got %s" % [
|
||||||
slot_id, typeof(value)
|
slot_id, typeof(value)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
elif value is Array:
|
elif value is Array:
|
||||||
if slot_id == "accessory":
|
if slot_id == "accessory" and (value as Array).size() > MAX_ACCESSORIES:
|
||||||
if value.size() > MAX_ACCESSORIES:
|
validation_errors.append(
|
||||||
validation_errors.append(
|
"Accessory count (%d) exceeds maximum (%d)" % [
|
||||||
"Accessory count (%d) exceeds maximum (%d)" % [
|
(value as Array).size(), MAX_ACCESSORIES
|
||||||
value.size(), MAX_ACCESSORIES
|
]
|
||||||
]
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# Check for duplicate accessories
|
# Check for duplicate accessories.
|
||||||
if slots.has("accessory") and slots["accessory"] is Array:
|
if slots.has("accessory") and slots["accessory"] is Array:
|
||||||
var accessories = slots["accessory"] as Array
|
var accessories := slots["accessory"] as Array
|
||||||
var seen := {}
|
var seen := {}
|
||||||
for item in accessories:
|
for item in accessories:
|
||||||
if item is String:
|
if item is String:
|
||||||
if seen.has(item):
|
if seen.has(item):
|
||||||
validation_errors.append(
|
validation_errors.append("Duplicate accessory: %s" % item)
|
||||||
"Duplicate accessory: %s" % item
|
|
||||||
)
|
|
||||||
seen[item] = true
|
seen[item] = true
|
||||||
|
|
||||||
return validation_errors.is_empty()
|
return validation_errors.is_empty()
|
||||||
@@ -231,20 +232,12 @@ func set_equipment(slot_id: String, asset_id: String) -> void:
|
|||||||
push_error("Slot '%s' is not exclusive" % slot_id)
|
push_error("Slot '%s' is not exclusive" % slot_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
if slots.has(slot_id):
|
slots[slot_id] = null if asset_id.is_empty() else asset_id
|
||||||
var current = slots[slot_id]
|
|
||||||
if current is Array:
|
|
||||||
# Convert array to single value
|
|
||||||
slots[slot_id] = asset_id if asset_id.is_empty() else null
|
|
||||||
else:
|
|
||||||
slots[slot_id] = asset_id if not asset_id.is_empty() else null
|
|
||||||
else:
|
|
||||||
slots[slot_id] = asset_id if not asset_id.is_empty() else null
|
|
||||||
|
|
||||||
|
|
||||||
## Add an accessory to the accessory slot.
|
## Add an accessory to the accessory slot.
|
||||||
func add_accessory(asset_id: String) -> bool:
|
func add_accessory(asset_id: String) -> bool:
|
||||||
var accessories = slots.get("accessory", []) as Array
|
var accessories := slots.get("accessory", []) as Array
|
||||||
if accessories.size() >= MAX_ACCESSORIES:
|
if accessories.size() >= MAX_ACCESSORIES:
|
||||||
return false
|
return false
|
||||||
if asset_id.is_empty():
|
if asset_id.is_empty():
|
||||||
@@ -258,7 +251,7 @@ func add_accessory(asset_id: String) -> bool:
|
|||||||
|
|
||||||
## Remove an accessory from the accessory slot.
|
## Remove an accessory from the accessory slot.
|
||||||
func remove_accessory(asset_id: String) -> bool:
|
func remove_accessory(asset_id: String) -> bool:
|
||||||
var accessories = slots.get("accessory", []) as Array
|
var accessories := slots.get("accessory", []) as Array
|
||||||
if accessories.has(asset_id):
|
if accessories.has(asset_id):
|
||||||
accessories.erase(asset_id)
|
accessories.erase(asset_id)
|
||||||
slots["accessory"] = accessories
|
slots["accessory"] = accessories
|
||||||
@@ -277,12 +270,12 @@ func get_equipped_items() -> Array[SlotItem]:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if typeof(value) == TYPE_STRING:
|
if typeof(value) == TYPE_STRING:
|
||||||
var asset_id = value as String
|
var asset_id := value as String
|
||||||
if not asset_id.is_empty():
|
if not asset_id.is_empty():
|
||||||
items.append(SlotItem.new(slot_id, asset_id))
|
items.append(SlotItem.new(slot_id, asset_id))
|
||||||
elif typeof(value) == TYPE_ARRAY:
|
elif typeof(value) == TYPE_ARRAY:
|
||||||
for item_id in value:
|
for item_id in value:
|
||||||
if item_id is String and not item_id.is_empty():
|
if item_id is String and not (item_id as String).is_empty():
|
||||||
items.append(SlotItem.new(slot_id, item_id))
|
items.append(SlotItem.new(slot_id, item_id))
|
||||||
|
|
||||||
return items
|
return items
|
||||||
@@ -302,12 +295,12 @@ func compute_hash() -> int:
|
|||||||
hasher.update(slot_id.to_utf8_buffer())
|
hasher.update(slot_id.to_utf8_buffer())
|
||||||
var value = slots.get(slot_id)
|
var value = slots.get(slot_id)
|
||||||
if value is String:
|
if value is String:
|
||||||
hasher.update(value.to_utf8_buffer())
|
hasher.update((value as String).to_utf8_buffer())
|
||||||
elif value is Array:
|
elif value is Array:
|
||||||
var arr_str = JSON.stringify(value)
|
var arr_str = JSON.stringify(value)
|
||||||
hasher.update(arr_str.to_utf8_buffer())
|
hasher.update(arr_str.to_utf8_buffer())
|
||||||
else:
|
else:
|
||||||
hasher.update("null".to_utf8_buffer())
|
hasher.update("null".to_utf8_buffer())
|
||||||
var digest = hasher.finish()
|
var digest = hasher.finish()
|
||||||
# Return first 4 bytes as int for compact storage
|
# Return first 4 bytes as int for compact storage.
|
||||||
return digest[0] | (digest[1] << 8) | (digest[2] << 16) | (digest[3] << 24)
|
return digest[0] | (digest[1] << 8) | (digest[2] << 16) | (digest[3] << 24)
|
||||||
|
|||||||
Reference in New Issue
Block a user