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()