feat(studio,stacking,animations,collections): M16 ModBison Studio, object stacking, model animations, and poly collections for MVP 2
checkpoint / checkpoint (push) Successful in 6m32s

This commit is contained in:
2026-09-22 18:30:57 +00:00
parent 654c1ec6bf
commit 721dbe5ae7
37 changed files with 5202 additions and 17 deletions
+204
View File
@@ -0,0 +1,204 @@
extends SceneTree
## Smoke test for animated .glb model instantiation and clip playback (Task 38d23c9dc0cb).
var _failures: int = 0
var _checks: int = 0
const StaticObjectNodeClass = preload("res://scripts/static_object_node.gd")
const WorldObjectClass = preload("res://scripts/world_object.gd")
func _init() -> void:
print("--- Running Smoke Test: Model Animation Playback ---")
test_animation_clip_detection()
test_animation_playback_and_looping()
test_animation_speed_and_autoplay()
test_inspector_animation_properties()
if _failures == 0:
print("--- PASS: All %d model animation checks passed! ---" % _checks)
quit(0)
else:
printerr("--- FAIL: %d failures out of %d checks ---" % [_failures, _checks])
quit(1)
func _check(desc: String, cond: bool) -> void:
_checks += 1
if cond:
print(" ✓ %s" % desc)
else:
_failures += 1
printerr(" ✗ FAIL: %s" % desc)
func _create_test_animated_node(asset_id: String, properties: Dictionary = {}) -> StaticObjectNode:
var wo := WorldObjectClass.new()
wo.id = "test_anim_obj"
wo.type = "prop"
wo.asset_id = asset_id
wo.position = Vector3(0, 0, 0)
wo.properties = properties.duplicate(true)
var node = StaticObjectNodeClass.new()
node.world_object = wo
root.add_child(node)
return node
func test_animation_clip_detection() -> void:
print("Testing animation clip discovery...")
# Create a dummy node with an AnimationPlayer and animation library
var wo := WorldObjectClass.new()
wo.id = "mock_anim_obj"
wo.type = "prop"
wo.asset_id = "props/crate_small.glb"
wo.properties = {}
var node = StaticObjectNodeClass.new()
node.world_object = wo
root.add_child(node)
# Manually attach an AnimationPlayer with 2 mock clips to test clip discovery
var ap := AnimationPlayer.new()
ap.name = "AnimationPlayer"
var lib := AnimationLibrary.new()
var anim_walk := Animation.new()
anim_walk.length = 1.0
var anim_idle := Animation.new()
anim_idle.length = 2.0
lib.add_animation("walk", anim_walk)
lib.add_animation("idle", anim_idle)
ap.add_animation_library("", lib)
node.add_child(ap)
# Call _setup_animation_and_motion
node.call("_load_mesh")
node.call("_setup_animation_and_motion")
var anim_list: Array[String] = node.get_available_animations()
_check("Found AnimationPlayer", node.get_animation_player() != null)
_check("Discovered 2 animation clips", anim_list.size() >= 2)
_check("Contains 'walk' clip", anim_list.has("walk"))
_check("Contains 'idle' clip", anim_list.has("idle"))
_check("Recorded available_animations in world_object properties",
wo.properties.has("available_animations") and wo.properties["available_animations"].has("walk"))
node.queue_free()
func test_animation_playback_and_looping() -> void:
print("Testing animation clip playback & loop modes...")
var wo := WorldObjectClass.new()
wo.id = "playback_obj"
wo.type = "prop"
wo.properties = {
"animation": {
"clip": "spin",
"autoplay": true,
"loop": true,
"speed": 1.5
}
}
var node = StaticObjectNodeClass.new()
node.world_object = wo
root.add_child(node)
var ap := AnimationPlayer.new()
var lib := AnimationLibrary.new()
var anim_spin := Animation.new()
anim_spin.length = 1.0
lib.add_animation("spin", anim_spin)
ap.add_animation_library("", lib)
node.add_child(ap)
node.call("_load_mesh")
node.call("_setup_animation_and_motion")
_check("Animation 'spin' is playing automatically", ap.is_playing() and ap.current_animation == "spin")
_check("Animation speed scale set to 1.5", is_equal_approx(ap.speed_scale, 1.5))
_check("Animation loop mode set to LOOP_LINEAR", anim_spin.loop_mode == Animation.LOOP_LINEAR)
# Test stopping and manual play
node.stop_animation()
_check("Animation stopped", not ap.is_playing())
node.play_animation("spin", 2.0, false)
_check("Animation restarted with speed 2.0", ap.is_playing() and is_equal_approx(ap.speed_scale, 2.0))
_check("Loop mode changed to LOOP_NONE", anim_spin.loop_mode == Animation.LOOP_NONE)
node.queue_free()
func test_animation_speed_and_autoplay() -> void:
print("Testing autoplay disable and speed settings...")
var wo := WorldObjectClass.new()
wo.id = "no_autoplay_obj"
wo.type = "prop"
wo.properties = {
"animation": {
"clip": "dance",
"autoplay": false,
"loop": false,
"speed": 0.5
}
}
var node = StaticObjectNodeClass.new()
node.world_object = wo
root.add_child(node)
var ap := AnimationPlayer.new()
var lib := AnimationLibrary.new()
var anim_dance := Animation.new()
anim_dance.length = 2.5
lib.add_animation("dance", anim_dance)
ap.add_animation_library("", lib)
node.add_child(ap)
node.call("_load_mesh")
node.call("_setup_animation_and_motion")
_check("Animation does not autoplay when autoplay=false", not ap.is_playing())
_check("Speed scale configured properly", is_equal_approx(ap.speed_scale, 0.5))
node.queue_free()
func test_inspector_animation_properties() -> void:
print("Testing dynamic property updates on StaticObjectNode...")
var wo := WorldObjectClass.new()
wo.id = "dynamic_anim_obj"
wo.type = "prop"
wo.properties = {}
var node = StaticObjectNodeClass.new()
node.world_object = wo
root.add_child(node)
var ap := AnimationPlayer.new()
var lib := AnimationLibrary.new()
var a1 := Animation.new()
a1.length = 1.0
var a2 := Animation.new()
a2.length = 1.0
lib.add_animation("run", a1)
lib.add_animation("jump", a2)
ap.add_animation_library("", lib)
node.add_child(ap)
node.call("_load_mesh")
node.call("_setup_animation_and_motion")
# Update animation config dynamically as InspectorPanel would
node.update_animation({
"clip": "jump",
"autoplay": true,
"loop": true,
"speed": 1.25
})
_check("Updated animation is 'jump'", ap.is_playing() and ap.current_animation == "jump")
_check("Updated speed is 1.25", is_equal_approx(ap.speed_scale, 1.25))
node.queue_free()