244 lines
7.7 KiB
GDScript
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()
|