PC3 Phase 1.2: placeholder avatar asset pack + make the loader actually assemble
assets/avatars/catalog.json referenced 14 GLB files that were never in the repo, so the avatar system could not load a single avatar. And the loader's assembly path had never run against a real skeleton -- it called Skeleton3D.has_bone() (no such method in Godot 4) and hand-copied bones in a way that dropped the skin binding. - scripts/tools/gen_placeholder_avatars.gd: generates the pack headlessly via GLTFDocument -- one rigged/skinned base body (the full 24-bone canonical rig from a T-pose) + 13 primitive attachments. Crude on purpose; real art swaps in as a pure content change (catalog paths unchanged). - assets/avatars/*.glb: the generated pack (checked in). - avatar_loader.gd: rewrote _parse_glb_as_avatar to lift the skin-bound Skeleton3D that generate_scene() already produces, instead of rebuilding it. Attachments now mount via BoneAttachment3D to the catalog's mount_bone and are actually added to the avatar tree (they were parsed then dropped). Fixed the has_bone() crash, the null ARRAY_BONES cast in _retarget_skin, and the Array[String] type mismatch on the accessory list. - smoke_test_avatar_system.gd: replaced the "asserts it fails gracefully because the files don't exist" section with a real assembly check -- Skeleton3D present, full 24-bone rig, canonical bones resolve by name, visible body mesh, equipped items mounted to bones. 14/14 from a cold cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
extends SceneTree
|
||||
## Smoke test for the avatar catalog/slot/loadout/system layers.
|
||||
## Smoke test for the avatar catalog/slot/loadout/system layers AND the full
|
||||
## assembly path against the placeholder GLB asset pack
|
||||
## (assets/avatars/*.glb, generated by scripts/tools/gen_placeholder_avatars.gd).
|
||||
##
|
||||
## Real .glb model assets don't exist yet (assets/avatars/catalog.json
|
||||
## references them, the files themselves are a tracked user to-do -- see
|
||||
## the "AGP MVP status" project memory). This test covers everything that
|
||||
## doesn't require them (catalog parsing, slot registry, loadout equip/
|
||||
## validate, the AvatarSystem orchestrator's autoload wiring) and confirms
|
||||
## the one thing that DOES touch missing files -- AvatarLoader.load_base_body()
|
||||
## -- fails gracefully (emits load_error, returns null) instead of crashing.
|
||||
## Visual rendering itself can't be verified until real assets exist.
|
||||
## Covers: catalog parsing, slot registry, loadout equip/validate/round-trip,
|
||||
## the AvatarSystem autoload wiring, and load_avatar() actually producing a
|
||||
## Skeleton3D with the canonical rig, a visible body mesh, and equipped
|
||||
## attachments mounted to the right bones.
|
||||
##
|
||||
## Run with:
|
||||
## godot --headless --path . --script res://scripts/smoke_test_avatar_system.gd
|
||||
@@ -55,8 +53,9 @@ func _run() -> void:
|
||||
|
||||
var hair_options: Array[String] = system.get_assets_for_slot("hair")
|
||||
if hair_options.size() > 0:
|
||||
var equip_ok: bool = loadout.add_accessory(hair_options[0])
|
||||
_check("adding a real catalog accessory succeeds", equip_ok)
|
||||
loadout.set_equipment("hair", hair_options[0])
|
||||
_check("equipping a real catalog hair item succeeds",
|
||||
loadout.slots.get("hair") == hair_options[0])
|
||||
|
||||
# 4. Loadout serializes and survives a round trip.
|
||||
var json_str: String = loadout.to_json()
|
||||
@@ -64,25 +63,45 @@ func _run() -> void:
|
||||
var restored: AvatarLoadout = system.load_loadout_from_json(json_str)
|
||||
_check("loadout round-trips through JSON", restored != null and restored.is_valid())
|
||||
|
||||
# 5. The part that DOES need real files: confirm it fails gracefully
|
||||
# rather than crashing, since assets/avatars/*.glb genuinely don't
|
||||
# exist yet. A crash here would be a real bug; a clean load_error
|
||||
# signal + null return is the correct, already-designed-for behavior.
|
||||
# 5. Full assembly against the placeholder GLB pack.
|
||||
if base_bodies.size() > 0:
|
||||
# A plain bool captured by a lambda is captured BY VALUE in
|
||||
# GDScript -- mutating it inside the closure doesn't propagate to
|
||||
# this outer scope. A single-element Array is the standard
|
||||
# workaround (confirmed the hard way earlier in this same session,
|
||||
# see scripts/smoke_test_network_client.gd's own comment on this).
|
||||
var error_state := [false]
|
||||
var error_handler := func(_msg): error_state[0] = true
|
||||
var errors := [] as Array[String]
|
||||
var error_handler := func(msg): errors.append(msg)
|
||||
system.loader.load_error.connect(error_handler)
|
||||
var result: Node3D = system.loader.load_base_body(base_bodies[0])
|
||||
|
||||
var chest_options: Array[String] = system.get_assets_for_slot("chest")
|
||||
if chest_options.size() > 0:
|
||||
loadout.set_equipment("chest", chest_options[0])
|
||||
var acc_options: Array[String] = system.get_assets_for_slot("accessory")
|
||||
if acc_options.size() > 0:
|
||||
loadout.add_accessory(acc_options[0])
|
||||
|
||||
var avatar: Node3D = system.load_avatar(loadout)
|
||||
system.loader.load_error.disconnect(error_handler)
|
||||
_check("load_base_body against a missing .glb returns null (not a crash)", result == null)
|
||||
_check("load_base_body against a missing .glb emits load_error (not silent)", error_state[0])
|
||||
|
||||
_check("load_avatar returns a node (no load_error: %s)" % str(errors),
|
||||
avatar != null and errors.is_empty())
|
||||
if avatar:
|
||||
var skel := avatar.get_node_or_null("Skeleton3D") as Skeleton3D
|
||||
_check("assembled avatar has a Skeleton3D", skel != null)
|
||||
if skel:
|
||||
_check("skeleton carries the full canonical rig (24 bones)",
|
||||
skel.get_bone_count() == 24)
|
||||
_check("canonical bones resolve by name (Head, LeftHand, RightFoot)",
|
||||
skel.find_bone("Head") != -1
|
||||
and skel.find_bone("LeftHand") != -1
|
||||
and skel.find_bone("RightFoot") != -1)
|
||||
var body_meshes := 0
|
||||
var bone_mounts := 0
|
||||
for c in skel.get_children():
|
||||
if c is MeshInstance3D and c.visible:
|
||||
body_meshes += 1
|
||||
if c is BoneAttachment3D:
|
||||
bone_mounts += 1
|
||||
_check("a visible body mesh is present", body_meshes >= 1)
|
||||
_check("equipped hair + chest are mounted to bones", bone_mounts >= 2)
|
||||
else:
|
||||
print(" SKIP: no base_body entries in catalog to test the failure path against")
|
||||
print(" SKIP: no base_body entries in catalog")
|
||||
|
||||
_finish()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user