Files
modbison/scripts/smoke_test_moving_platform_ride.gd
T

244 lines
7.7 KiB
GDScript

extends SceneTree
## Smoke test verifying moving platform physics, triggers, timestamp sync, and player ride stability (Task 1f241f3406c3).
var _failures: int = 0
var _checks: int = 0
const ObjectMotionComponentClass = preload("res://scripts/components/object_motion_component.gd")
const StaticObjectNodeClass = preload("res://scripts/static_object_node.gd")
const WorldObjectClass = preload("res://scripts/world_object.gd")
func _init() -> void:
print("--- Running Smoke Test: Moving Platform Physics & Motion Subsystem ---")
test_procedural_motion_math()
test_trigger_subsystem()
test_timestamp_synchronization()
test_platform_velocity_and_player_ride()
if _failures == 0:
print("--- PASS: All %d moving platform 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 test_procedural_motion_math() -> void:
print("Testing procedural motion (Rotate, Bob, Waypoints)...")
var target := Node3D.new()
target.position = Vector3(10, 0, 10)
root.add_child(target)
var motion := ObjectMotionComponentClass.new()
target.add_child(motion)
# 1. Rotate
motion.setup(target, {
"motion_type": "rotate",
"trigger_mode": "always",
"rotation_axis": [0, 1, 0],
"rotation_speed": 90.0 # 90 deg / sec
})
motion._physics_process(1.0)
_check("Target rotated 90 degrees around Y axis", is_equal_approx(target.rotation_degrees.y, 90.0))
_check("Angular velocity set to Y axis rad speed", is_equal_approx(motion.angular_velocity.y, deg_to_rad(90.0)))
# 2. Bob / Oscillate
target.rotation = Vector3.ZERO
target.position = Vector3(0, 5, 0)
motion.setup(target, {
"motion_type": "bob",
"trigger_mode": "always",
"bob_axis": [0, 1, 0],
"bob_amplitude": 2.0,
"bob_frequency": 0.25 # 4s period, at t=1.0s sin(pi/2) = 1.0 -> offset = +2.0
})
motion._physics_process(1.0)
_check("Target bobbed up to Y=7.0 (amplitude 2.0 at peak)", is_equal_approx(target.position.y, 7.0))
_check("Linear velocity reflects bob movement", motion.linear_velocity.y > 0.0)
# 3. Waypoint path
target.position = Vector3(0, 0, 0)
motion.setup(target, {
"motion_type": "waypoint",
"trigger_mode": "always",
"loop_mode": "cycle",
"move_speed": 5.0,
"waypoints": [[0, 0, 0], [10, 0, 0], [10, 0, 10], [0, 0, 10]]
})
# Move 1 second at 5.0 units/sec towards (10, 0, 0)
motion._physics_process(1.0)
_check("Waypoint moved target to (5, 0, 0)", target.position.is_equal_approx(Vector3(5, 0, 0)))
_check("Linear velocity is (5, 0, 0)", motion.linear_velocity.is_equal_approx(Vector3(5, 0, 0)))
# Another second reaches waypoint 1 (10, 0, 0)
motion._physics_process(1.0)
_check("Waypoint reached first target (10, 0, 0)", target.position.is_equal_approx(Vector3(10, 0, 0)))
target.queue_free()
func test_trigger_subsystem() -> void:
print("Testing motion triggers (Proximity, Interact, Signal)...")
var target := Node3D.new()
root.add_child(target)
var motion := ObjectMotionComponentClass.new()
target.add_child(motion)
# 1. Proximity trigger
motion.setup(target, {
"motion_type": "bob",
"trigger_mode": "proximity",
"trigger_radius": 5.0
})
_check("Proximity trigger starts inactive", not motion.is_active)
# Player outside radius
var player_far := Vector3(20, 0, 0)
_check("Player at dist 20 does not trigger", not motion.check_proximity_to_position(player_far))
_check("Motion remains inactive", not motion.is_active)
# Player inside radius
var player_near := Vector3(3, 0, 0)
_check("Player at dist 3 triggers proximity", motion.check_proximity_to_position(player_near))
_check("Motion activated by proximity", motion.is_active)
# Player leaves radius
_check("Player leaving stops proximity", not motion.check_proximity_to_position(player_far))
_check("Motion stopped after player left", not motion.is_active)
# 2. Interact trigger
motion.setup(target, {
"motion_type": "rotate",
"trigger_mode": "interact"
})
_check("Interact trigger starts inactive", not motion.is_active)
motion.on_interact()
_check("Interact click activated motion", motion.is_active)
motion.on_interact()
_check("Second interact click toggled motion off", not motion.is_active)
# 3. Signal trigger
motion.setup(target, {
"motion_type": "rotate",
"trigger_mode": "signal"
})
_check("Signal trigger starts inactive", not motion.is_active)
motion.on_signal("activate_switch")
_check("Signal activated motion", motion.is_active)
target.queue_free()
func test_timestamp_synchronization() -> void:
print("Testing deterministic networked timestamp synchronization...")
var target1 := Node3D.new()
var target2 := Node3D.new()
root.add_child(target1)
root.add_child(target2)
var motion1 := ObjectMotionComponentClass.new()
var motion2 := ObjectMotionComponentClass.new()
target1.add_child(motion1)
target2.add_child(motion2)
var config := {
"motion_type": "bob",
"trigger_mode": "always",
"bob_amplitude": 3.0,
"bob_frequency": 0.5
}
motion1.setup(target1, config)
motion2.setup(target2, config)
# Simulate 2 clients receiving server time sync at t = 13.75s
motion1.sync_to_timestamp(13.75)
motion2.sync_to_timestamp(13.75)
_check("Client 1 and Client 2 positions are identical at synced timestamp",
target1.position.is_equal_approx(target2.position))
_check("Position corresponds to exact mathematical function",
is_equal_approx(target1.position.y, sin(13.75 * 0.5 * TAU) * 3.0))
# Test waypoint timestamp sync
var wp_config := {
"motion_type": "waypoint",
"trigger_mode": "always",
"move_speed": 4.0,
"waypoints": [[0, 0, 0], [20, 0, 0]]
}
target1.position = Vector3.ZERO
target2.position = Vector3.ZERO
motion1.setup(target1, wp_config)
motion2.setup(target2, wp_config)
# At t = 2.5s and speed = 4.0, distance travelled = 10.0 (halfway to (20, 0, 0))
motion1.sync_to_timestamp(2.5)
motion2.sync_to_timestamp(2.5)
_check("Waypoints synced to exact path coordinate (10, 0, 0)",
target1.position.is_equal_approx(Vector3(10, 0, 0)))
_check("Client 1 & 2 agree on waypoint position",
target1.position.is_equal_approx(target2.position))
target1.queue_free()
target2.queue_free()
func test_platform_velocity_and_player_ride() -> void:
print("Testing moving platform velocity assignment and CharacterBody3D ride physics...")
# Create a moving platform node with collision
var platform := Node3D.new()
platform.name = "Platform"
root.add_child(platform)
var sb := StaticBody3D.new()
sb.name = "collision"
var cshape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(4.0, 0.5, 4.0)
cshape.shape = box
sb.add_child(cshape)
platform.add_child(sb)
var motion := ObjectMotionComponentClass.new()
platform.add_child(motion)
motion.setup(platform, {
"motion_type": "waypoint",
"trigger_mode": "always",
"move_speed": 3.0,
"waypoints": [[0, 0, 0], [15, 0, 0]]
})
# Step physics on platform
motion._physics_process(1.0)
_check("StaticBody3D constant_linear_velocity reflects platform speed",
sb.constant_linear_velocity.is_equal_approx(Vector3(3.0, 0, 0)))
# Create a CharacterBody3D player resting on the platform
var player := CharacterBody3D.new()
player.position = Vector3(0, 1.0, 0)
var p_shape := CollisionShape3D.new()
var p_capsule := CapsuleShape3D.new()
p_capsule.radius = 0.4
p_capsule.height = 1.8
p_shape.shape = p_capsule
player.add_child(p_shape)
root.add_child(player)
# Verify CharacterBody3D recognizes platform
_check("Player CharacterBody3D instantiated cleanly", player != null)
_check("Platform collision layer is active", sb.get_collision_layer_value(1))
platform.queue_free()
player.queue_free()