f51c49fcfa
Root cause: a stale .godot/global_script_class_cache.cfg was force-committed to the repo (last regenerated before the avatar system merged), and Godot does not rebuild the global class registry on a plain headless scene run -- only an editor scan / `--import` does. So every cross-file `class_name` reference in the avatar module failed to resolve from a fresh clone, and the AvatarSystem autoload failed to instantiate entirely. - Untrack .godot/ and sandbox_probe/.godot/ (already in .gitignore; were force-added during old WIP checkpoints) - Reference sibling avatar scripts via preload() constants instead of their global class_name, so the module parses independent of scan order. Keeps the class_name declarations for editor/tooling use. - avatar_loadout.gd's static from_json() factory self-references via a preload const too (a script can't resolve its own class_name mid-compile on a cold cache). - smoke_test_avatar_system.gd: same preload treatment so it runs from a bare `--script` invocation. Verified from a wiped .godot/: main_menu.tscn and game.tscn boot with zero script errors; smoke_test_avatar_system 10/10, smoke_test_player 15/15, smoke_test_main_menu_ui 13/13, smoke_test_test_level PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
103 lines
4.2 KiB
GDScript
103 lines
4.2 KiB
GDScript
extends SceneTree
|
|
## Smoke test for the avatar catalog/slot/loadout/system layers.
|
|
##
|
|
## 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.
|
|
##
|
|
## 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:
|
|
var equip_ok: bool = loadout.add_accessory(hair_options[0])
|
|
_check("adding a real catalog accessory succeeds", equip_ok)
|
|
|
|
# 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. 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.
|
|
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
|
|
system.loader.load_error.connect(error_handler)
|
|
var result: Node3D = system.loader.load_base_body(base_bodies[0])
|
|
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])
|
|
else:
|
|
print(" SKIP: no base_body entries in catalog to test the failure path against")
|
|
|
|
_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)
|