Files
Papay-Shooter/debug/spawn_smoke_test.gd
T
Nicholas ButzkeandClaude Fable 5 1ad3563e5a feat: animations follow the mechanics — upper/lower split + real grapple pose
Two fidelity gaps closed:

- Reload/throw/shoot/hit used to REPLACE the whole-body clip, so reloading
  mid-slide snapped the character upright on standing legs. The model now
  runs a runtime AnimationTree (clips -> Transition -> TimeScale -> OneShot)
  whose one-shot layer is FILTERED to upper-body bones: the arms play the
  action while the legs keep sliding/running/falling. Land stays full-body.
  The rifle hold releases exactly while the one-shot node reports active.
- Grapple no longer plays the library's horizontal-swim clip. The zip pose
  is procedural: Fall as the airborne base, the spine pivots to fly along
  the line to the actual anchor point (capped so overhead shots don't fold
  the body), the head sights the anchor, legs trail behind, the FREE left
  hand reaches up the rope — and the right hand keeps holding the rifle.
  The anchor point feeds in from the controller for local and remote
  players (synced_grapple_point).
- anim_capture: deterministic open-ground teleport + two new regression
  shots (reload-while-sliding, grapple zip toward a real anchor).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-20 22:51:04 -04:00

141 lines
4.8 KiB
GDScript

extends SceneTree
## Headless smoke test for the player spawn + skin + animation pipeline.
## Boots the test level as a singleplayer match, spawns the local player,
## ticks frames, and verifies the visual model came up for both the
## procedural skin and a GLB skin.
##
## Run: godot --headless --path . -s res://debug/spawn_smoke_test.gd
var _failures: Array = []
func _init() -> void:
call_deferred("_run")
func _run() -> void:
await process_frame
await process_frame
var skin_mgr = root.get_node_or_null("/root/SkinManager")
_check(skin_mgr != null, "SkinManager autoload exists")
var audio_mgr = root.get_node_or_null("/root/AudioManager")
_check(audio_mgr != null, "AudioManager autoload exists")
if audio_mgr:
_check(audio_mgr.has_sound("ak47_fire"), "AudioManager auto-registered ak47_fire")
# Pick the GLB skin if its model file is present, else default.
var glb_skin_id := ""
if skin_mgr:
for id in skin_mgr.get_skin_ids():
var s = skin_mgr.get_skin(id)
if s.model_path != "" and ResourceLoader.exists(s.model_path):
glb_skin_id = id
break
await _test_spawn_with_skin("default", false)
if glb_skin_id != "":
await _test_spawn_with_skin(glb_skin_id, true)
else:
print("NOTE: no GLB skin available, skipped skinned model test")
print("\n=== SPAWN SMOKE SUMMARY ===")
print("Failures: %d" % _failures.size())
for f in _failures:
print("FAIL: ", f)
quit(0 if _failures.is_empty() else 1)
func _test_spawn_with_skin(skin_id: String, expect_skinned: bool) -> void:
print("\n--- Spawn test with skin '%s' ---" % skin_id)
var skin_mgr = root.get_node_or_null("/root/SkinManager")
if skin_mgr:
skin_mgr.set_active_skin(skin_id)
var nm = root.get_node_or_null("/root/NetworkManager")
if not _check(nm != null, "NetworkManager autoload exists"):
return
nm.start_singleplayer_match("Deathmatch")
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
# Let the level build and the player spawn + settle.
for i in 30:
await process_frame
var level = current_scene
if not _check(level != null, "level scene loaded"):
return
var player = _find_player(level)
if not _check(player != null, "player '1' spawned"):
return
_check(player.get_node_or_null("MovementStateMachine") != null, "state machine present")
var visual = player.get_visual_model() if player.has_method("get_visual_model") else null
_check(visual != null, "visual model present")
if expect_skinned:
var skinned = player.get_node_or_null("SkinnedModel")
if _check(skinned != null, "SkinnedModel created for GLB skin"):
_check(skinned.loaded, "GLB model loaded")
_check(skinned.skeleton != null, "skeleton found in GLB")
# The mesh must be bound to the skeleton, else it renders its bind
# pose (T-pose) while the skeleton animates invisibly.
var bound := false
if skinned.skeleton:
for mi in skinned.find_children("*", "MeshInstance3D", true, false):
if mi.skin != null and mi.get_node_or_null(mi.skeleton) == skinned.skeleton:
bound = true
_check(bound, "skinned mesh is bound to the skeleton (won't T-pose)")
if skinned.animation_player:
# The AnimationTree drives playback now; the raw player is idle.
_check(skinned._anim_tree != null and skinned._anim_tree.active,
"animation tree active")
print(" clips resolved: ", skinned._resolved_clips)
# Drive some movement states through the same API the game uses.
for state in ["ground", "air", "slide", "wall_run", "dash"]:
skinned.update_state(state, 9.0, false)
await process_frame
_check(true, "state cycling did not crash")
# Third-person toggle: camera swap + owner model visibility.
if player.has_method("set_third_person"):
var tp_cam = player.get_node_or_null(
"HeadPivot/OrbitPivot/ShoulderOffset/ThirdPersonBoom/ThirdPersonCamera")
_check(tp_cam != null, "third-person camera boom created")
player.set_third_person(true)
await process_frame
_check(player.third_person, "toggled into third person")
if tp_cam:
_check(tp_cam.current, "third-person camera is current when toggled on")
player.set_third_person(false)
await process_frame
_check(not player.third_person, "toggled back to first person")
else:
var humanoid = player.get_node_or_null("HumanoidModel")
_check(humanoid != null and humanoid.visible, "procedural model visible for color skin")
# Simulate a few physics frames of idle play.
for i in 30:
await physics_frame
_check(is_instance_valid(player), "player survived 30 physics frames")
func _find_player(node: Node) -> Node:
if node is CharacterBody3D and node.name == "1" and node.has_method("get_visual_model"):
return node
for child in node.get_children():
var found := _find_player(child)
if found:
return found
return null
func _check(ok: bool, msg: String) -> bool:
if ok:
print(" OK: ", msg)
else:
print(" FAIL: ", msg)
_failures.append(msg)
return ok