Files
modbison/scripts/smoke_test_avatar_system.gd
T
daddy d6cd3293ae 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
2026-09-05 21:38:00 +00:00

122 lines
4.5 KiB
GDScript

extends SceneTree
## 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).
##
## 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
# Preloaded rather than referenced by global class_name so this test also
# runs from a cold cache (a bare `--script` run with no prior --import).
const AvatarLoadout := preload("res://scripts/avatar/avatar_loadout.gd")
var passed := 0
var failed := 0
func _initialize() -> void:
_run()
func _run() -> void:
await process_frame
# 1. AvatarSystem is a real autoload, not just a loose script.
var system := root.get_node_or_null("AvatarSystem")
if not _check("AvatarSystem autoload exists", system != null):
_finish()
return
# 2. The real catalog.json (committed to assets/avatars/) actually parses.
var err: Error = system.load_catalog()
_check("catalog loads without error", err == OK)
_check("AvatarSystem reports catalog loaded", system.is_catalog_loaded)
var base_bodies: Array[String] = system.get_assets_for_slot("base_body")
_check("catalog has at least one base_body entry", base_bodies.size() > 0)
# 3. Loadout: create, equip, validate against the real catalog.
# base_body is its own dedicated field on AvatarLoadout, not something
# set via the generic per-slot set_equipment() -- AvatarSlotRegistry's
# SLOT_IDS deliberately excludes "base_body" (it's the foundational
# model you start from, not a layered attachment slot).
var loadout := AvatarLoadout.new()
if base_bodies.size() > 0:
loadout.base_body = base_bodies[0]
_check("loadout has base_body set",
base_bodies.size() == 0 or not loadout.base_body.is_empty())
var hair_options: Array[String] = system.get_assets_for_slot("hair")
if hair_options.size() > 0:
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()
_check("loadout serializes to non-empty JSON", json_str.length() > 0)
var restored: AvatarLoadout = system.load_loadout_from_json(json_str)
_check("loadout round-trips through JSON", restored != null and restored.is_valid())
# 5. Full assembly against the placeholder GLB pack.
if base_bodies.size() > 0:
var errors := [] as Array[String]
var error_handler := func(msg): errors.append(msg)
system.loader.load_error.connect(error_handler)
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_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")
_finish()
func _check(description: String, condition: bool) -> bool:
if condition:
print(" PASS: ", description)
passed += 1
else:
printerr(" FAIL: ", description)
failed += 1
return condition
func _finish() -> void:
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
quit(0 if failed == 0 else 1)