extends SceneTree const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: var stage := STAGE_SCENE.instantiate() as Node2D root.add_child(stage) await process_frame var rig := _spawn_rig(stage, Vector2.ZERO) if rig == null: _finish() return _test_collision_proxy_geometry(rig) await _test_collision_proxy_physics_alignment(rig) _test_reactions(rig) await _test_direct_restores_animated_pose(stage, rig) _test_ghost_collision(stage) await _test_stage_contact_routing(stage, rig) _test_ragdoll_transitions(rig) stage.queue_free() await process_frame _finish() func _spawn_rig(stage: Node2D, position: Vector2) -> StickmanRig: var rig := stage._spawner.spawn("stickman", position) as StickmanRig _check(rig != null, "stage spawns a stickman rig") if rig != null: stage._finalize_placed_node(rig) return rig func _test_collision_proxy_geometry(rig: StickmanRig) -> void: var locomotion: StickmanLocomotion = rig.get_locomotion() _check(locomotion.collision_bodies.size() == 10, "locomotion builds ten collision proxies") _check(locomotion.collision_lengths.size() == 9, "locomotion records nine fitted capsule lengths") for part_name: String in locomotion.COLLISION_PARTS: var definition: Dictionary = locomotion.COLLISION_PARTS[part_name] var body := locomotion.collision_bodies.get(part_name) as AnimatableBody2D _check(body != null, "%s proxy exists" % part_name) if body == null: continue _check(body.name == "CollisionBody", "%s proxy uses the standard name" % part_name) _check(not body.sync_to_physics, "%s proxy uses explicit transform synchronization" % part_name) _check(body.top_level, "%s proxy is isolated from RemoteTransform2D parent updates" % part_name) _check(body.collision_layer == 1 and body.collision_mask == 1, "%s proxy starts enabled" % part_name) _check(body.get_meta("stickman_rig", null) == rig, "%s proxy stores its rig" % part_name) _check(String(body.get_meta("part_name", "")) == part_name, "%s proxy stores its part name" % part_name) var shape_node := body.get_node_or_null(NodePath("CollisionShape2D")) as CollisionShape2D _check(shape_node != null, "%s proxy owns a collision shape" % part_name) if shape_node == null: continue if part_name == "head": var circle := shape_node.shape as CircleShape2D _check(circle != null, "head uses a circle") if circle != null: _check(circle.radius >= locomotion.HEAD_COLLISION_RADIUS, "head radius covers at least the fallback radius") var head_visual := rig.get_node_or_null(NodePath("Body/Head")) as Node2D var bounds: Dictionary = locomotion.mounted_visual_bounds_local(head_visual) _check(bool(bounds.get("valid", false)), "mounted head has measurable geometry") if bool(bounds.get("valid", false)): _check(shape_node.position.is_equal_approx((bounds["rect"] as Rect2).get_center()), "head collider follows the mounted bbox center") _check(shape_node.position.y < 0.0, "head collider is above its neck attachment") else: var capsule := shape_node.shape as CapsuleShape2D _check(capsule != null, "%s uses a capsule" % part_name) if capsule == null: continue var length := float(locomotion.collision_lengths.get(part_name, -1.0)) _check(is_equal_approx(capsule.height, length), "%s capsule uses its fitted length" % part_name) _check(is_equal_approx(capsule.radius, float(definition["radius"])), "%s capsule uses its configured radius" % part_name) _check(shape_node.position.is_equal_approx(Vector2(0.0, length * 0.5)), "%s capsule is centered along +Y" % part_name) _check(is_zero_approx(shape_node.rotation), "%s capsule needs no hanging-frame rotation" % part_name) var empty_visual := Node2D.new() var empty_bounds: Dictionary = locomotion.mounted_visual_bounds_local(empty_visual) _check(not bool(empty_bounds.get("valid", true)), "empty visual bounds use the fallback path") empty_visual.free() rig.set_collision_bodies_enabled(false) _check(_all_proxy_layers_equal(rig, 0), "public collision hook disables every proxy") rig.set_collision_bodies_enabled(true) _check(_all_proxy_layers_equal(rig, 1), "public collision hook re-enables every proxy") var sizes_before := _proxy_shape_sizes(rig) rig.set_facing_profile(StickmanRig.FacingProfile.LEFT) _check(_proxy_shape_sizes(rig) == sizes_before, "LEFT mirroring preserves symmetric proxy sizes") rig.set_facing_profile(StickmanRig.FacingProfile.FORWARD) func _test_collision_proxy_physics_alignment(rig: StickmanRig) -> void: var locomotion: StickmanLocomotion = rig.get_locomotion() rig.walk_to(rig.global_position + Vector2(-400.0, 0.0), 300.0) for _frame in 8: await physics_frame await process_frame locomotion.sync_collision_bodies() await physics_frame await process_frame _check(rig.facing_profile == StickmanRig.FacingProfile.LEFT, "left walk uses the mirrored collision-proxy path") for part_name: String in locomotion.COLLISION_PARTS: var definition: Dictionary = locomotion.COLLISION_PARTS[part_name] var visual := rig.get_node_or_null(NodePath("Body/" + String(definition["visual"]))) as Node2D var body := locomotion.collision_bodies.get(part_name) as AnimatableBody2D if visual == null or body == null: _check(false, "%s has nodes for PhysicsServer alignment" % part_name) continue var server_transform := PhysicsServer2D.body_get_state(body.get_rid(), PhysicsServer2D.BODY_STATE_TRANSFORM) as Transform2D _check(server_transform.origin.distance_to(visual.global_position) < 0.01, "%s PhysicsServer origin follows its walking visual" % part_name) _check(server_transform.x.distance_to(visual.global_transform.x) < 0.001, "%s PhysicsServer X basis follows its walking visual" % part_name) _check(server_transform.y.distance_to(visual.global_transform.y) < 0.001, "%s PhysicsServer Y basis follows its walking visual" % part_name) rig.get_locomotion().cancel_walk() rig.set_facing_profile(StickmanRig.FacingProfile.FORWARD) func _test_reactions(rig: StickmanRig) -> void: var locomotion: StickmanLocomotion = rig.get_locomotion() locomotion.impact_reactions.clear() rig.apply_collision_reaction("unknown", Vector2.RIGHT, 400.0) rig.apply_collision_reaction("head", Vector2.ZERO, 400.0) rig.apply_collision_reaction("head", Vector2.RIGHT, 0.0) _check(locomotion.impact_reactions.is_empty(), "invalid reactions are ignored") var head_proxy := locomotion.collision_bodies["head"] as AnimatableBody2D var head_shape := head_proxy.get_node("CollisionShape2D") as CollisionShape2D var head_circle := head_shape.shape as CircleShape2D var upper_left := head_proxy.to_global(head_shape.position + Vector2(-head_circle.radius * 0.7, -head_circle.radius * 0.7)) rig.apply_collision_reaction("head", Vector2(0.5, 0.5).normalized(), 400.0, upper_left) _check(float((locomotion.impact_reactions["Head"] as Dictionary)["angle"]) < 0.0, "upper-left head contact lowers the struck side") locomotion.impact_reactions.clear() var upper_right := head_proxy.to_global(head_shape.position + Vector2(head_circle.radius * 0.7, -head_circle.radius * 0.7)) rig.apply_collision_reaction("head", Vector2(-0.5, 0.5).normalized(), 400.0, upper_right) _check(float((locomotion.impact_reactions["Head"] as Dictionary)["angle"]) > 0.0, "upper-right head contact lowers the struck side") locomotion.impact_reactions.clear() var lower_left := head_proxy.to_global(head_shape.position + Vector2(-head_circle.radius * 0.7, head_circle.radius * 0.7)) rig.apply_collision_reaction("head", Vector2.UP, 400.0, lower_left) _check(float((locomotion.impact_reactions["Head"] as Dictionary)["angle"]) > 0.0, "lower-left upward contact inverts the upper-left response") locomotion.impact_reactions.clear() var lower_right := head_proxy.to_global(head_shape.position + Vector2(head_circle.radius * 0.7, head_circle.radius * 0.7)) rig.apply_collision_reaction("head", Vector2.UP, 400.0, lower_right) _check(float((locomotion.impact_reactions["Head"] as Dictionary)["angle"]) < 0.0, "lower-right upward contact inverts the upper-right response") locomotion.impact_reactions.clear() rig.apply_collision_reaction("left_lower_arm", Vector2.RIGHT, 10000.0) var lower: Dictionary = locomotion.impact_reactions.get("LeftLowerArm", {}) var upper: Dictionary = locomotion.impact_reactions.get("LeftUpperArm", {}) _check(is_equal_approx(absf(float(lower.get("angle", 0.0))), 0.6), "primary reaction caps at 0.6 radians") _check(is_equal_approx(absf(float(upper.get("angle", 0.0))), 0.24), "secondary reaction applies 0.4 weight") var strong_angle := float(lower.get("angle", 0.0)) (locomotion.impact_reactions["LeftLowerArm"] as Dictionary)["strength"] = 0.25 rig.apply_collision_reaction("left_lower_arm", Vector2.LEFT, 40.0) lower = locomotion.impact_reactions["LeftLowerArm"] _check(is_equal_approx(float(lower["angle"]), strong_angle), "weaker opposite hit retains the stronger signed angle") _check(is_equal_approx(float(lower["strength"]), 1.0), "weaker hit refreshes reaction strength") rig.apply_collision_reaction("left_lower_arm", Vector2.LEFT, 10000.0) lower = locomotion.impact_reactions["LeftLowerArm"] _check(signf(float(lower["angle"])) != signf(strong_angle), "equal capped hit may replace the reaction direction") var lower_bone := locomotion.reaction_bones["LeftLowerArm"] as Bone2D var base_rotation := lower_bone.rotation var max_offset := 0.0 for frame in 30: locomotion.apply_impact_reactions(1.0 / 60.0) max_offset = maxf(max_offset, absf(angle_difference(base_rotation, lower_bone.rotation))) _check(max_offset <= locomotion.REACTION_MAX_ANGLE + 0.001, "repeated reaction frames never accumulate beyond the angular cap") locomotion.apply_impact_reactions(1.0) _check(locomotion.impact_reactions.is_empty(), "expired reactions are erased") _check(is_zero_approx(angle_difference(base_rotation, lower_bone.rotation)), "expired reaction restores its captured base rotation") func _test_direct_restores_animated_pose(stage: Node2D, rig: StickmanRig) -> void: var authored: Dictionary = stage._authored[rig.get_instance_id()] stage.set_mode(stage.StageMode.PLAY) rig.set_facing_profile(StickmanRig.FacingProfile.LEFT) rig.global_position += Vector2(220.0, -90.0) rig.global_rotation = PI var torso_marker := rig.get_node("IK_Targets/Torso") as Marker2D torso_marker.position = Vector2(75.0, -130.0) torso_marker.rotation = PI var head_marker := rig.get_node("IK_Targets/Head") as Marker2D head_marker.position += Vector2(120.0, 80.0) stage.set_mode(stage.StageMode.DIRECT) for _frame in 4: await physics_frame await process_frame _check(rig.global_position.distance_to(authored["position"] as Vector2) < 0.01, "PLAY to DIRECT restores the authored stickman position") _check(absf(angle_difference(float(authored["rotation"]), rig.global_rotation)) < 0.001, "PLAY to DIRECT restores the authored stickman rotation") _check(rig.get_facing_profile() == int(authored["facing_profile"]), "PLAY to DIRECT restores the authored facing profile") for marker_name: String in StickmanRagdoll.STAND_POSE: var marker := rig.get_node("IK_Targets/" + marker_name) as Node2D var expected: Dictionary = StickmanRagdoll.STAND_POSE[marker_name] _check(marker.position.distance_to(expected["pos"] as Vector2) < 0.01, "%s marker returns to standing after PLAY" % marker_name) _check(absf(angle_difference(torso_marker.rotation, float(StickmanRagdoll.STAND_POSE["Torso"]["rot"]))) < 0.001, "torso marker cannot retain an upside-down PLAY frame") func _test_ghost_collision(stage: Node2D) -> void: var ghost := _spawn_rig(stage, Vector2(1000.0, 0.0)) if ghost == null: return stage._configure_ghost_collision(ghost) _check(_all_proxy_layers_equal(ghost, 0), "stickman placement ghosts have no collision") ghost.queue_free() func _test_stage_contact_routing(stage: Node2D, rig: StickmanRig) -> void: stage.set_mode(stage.StageMode.PLAY) var prop := PropBlock.new() prop.position = Vector2(50.0, 25.0) prop.linear_velocity = Vector2(400.0, 0.0) stage._world.add_child(prop) await process_frame _check(prop.continuous_cd == RigidBody2D.CCD_MODE_CAST_SHAPE, "moving props use shape-cast CCD for thin limbs") var emitted_collisions: Array[Dictionary] = [] var emitted_separations: Array[Node] = [] prop.collided.connect(func(other: Node, normal: Vector2, position: Vector2) -> void: emitted_collisions.append({"other": other, "normal": normal, "position": position}) ) prop.separated.connect(func(other: Node) -> void: emitted_separations.append(other) ) var fallback_body := Node2D.new() fallback_body.position = prop.position + Vector2.RIGHT * 20.0 stage._world.add_child(fallback_body) prop._on_body_entered(fallback_body) prop._on_body_exited(fallback_body) _check(emitted_collisions.size() == 1, "PropBlock emits its extended collided signal") if emitted_collisions.size() == 1: _check((emitted_collisions[0]["normal"] as Vector2).is_equal_approx(Vector2.RIGHT), "PropBlock fallback normal points prop-to-other") _check((emitted_collisions[0]["position"] as Vector2).is_equal_approx(prop.global_position), "PropBlock fallback position is world-space") _check(emitted_separations == [fallback_body], "PropBlock mirrors body_exited with separated") var locomotion: StickmanLocomotion = rig.get_locomotion() var lower := locomotion.collision_bodies["left_lower_arm"] as AnimatableBody2D var upper := locomotion.collision_bodies["left_upper_arm"] as AnimatableBody2D var pair_key := "%d:%d" % [rig.get_instance_id(), prop.get_instance_id()] var rule := { "id": 4500, "trigger": { "type": "collided", "source": rig.get_instance_id(), "target": prop.get_instance_id(), "params": {}, }, "actions": [{"type": "wait", "target": rig.get_instance_id(), "params": {"duration": 10.0}}], } stage._event_rules.clear() stage._event_rules.append(rule) var runner: StickmanActionRunner = rig.get_node("RigComponents/ActionRunner") as StickmanActionRunner var initial_runtime_size: int = runner._run_queue.size() stage._on_prop_collided(lower, Vector2.RIGHT, Vector2(10.0, 20.0), prop) _check(stage._collision_pairs.has(pair_key), "first part contact creates a rig/prop pair") _check(runner._run_queue.size() == initial_runtime_size + 1, "first pair contact dispatches a rig-to-prop rule") stage._on_prop_collided(upper, Vector2.RIGHT, Vector2(11.0, 20.0), prop) var pair: Dictionary = stage._collision_pairs[pair_key] _check((pair.get("bodies", {}) as Dictionary).size() == 2, "second part contact joins the active body set") _check(runner._run_queue.size() == initial_runtime_size + 1, "second active part does not repeat the pair rule") _check(locomotion.impact_reactions.has("LeftLowerArm") and locomotion.impact_reactions.has("LeftUpperArm"), "each newly contacted part can react") stage._on_prop_separated(lower, prop) _check(stage._collision_pairs.has(pair_key), "first part exit retains the pair") stage._on_prop_separated(upper, prop) _check(not stage._collision_pairs.has(pair_key), "last part exit clears the pair") stage._on_prop_collided(lower, Vector2.RIGHT, Vector2(12.0, 20.0), prop) _check(runner._run_queue.size() == initial_runtime_size + 2, "full separation permits a new collision rule") var prop_ids: Array[int] = [prop.get_instance_id()] stage._purge_collision_pairs_for_ids(prop_ids) _check(not stage._collision_pairs.has(pair_key), "prop deletion purge removes active pairs") var forward_event := {"type": "collided", "source": rig, "target": prop, "position": Vector2.ZERO, "action": {}} var reverse_event := {"type": "collided", "source": prop, "target": rig, "position": Vector2.ZERO, "action": {}} _check(stage._rule_matches(rule, forward_event), "collided rules preserve rig-to-prop ordering") _check(not stage._rule_matches(rule, reverse_event), "collided rule matching is not bidirectional") var generic_rule := { "id": 4501, "trigger": { "type": "collided", "source": prop.get_instance_id(), "target": fallback_body.get_instance_id(), "params": {}, }, "actions": [{"type": "wait", "target": rig.get_instance_id(), "params": {"duration": 10.0}}], } stage._event_rules.clear() stage._event_rules.append(generic_rule) var before_generic: int = runner._run_queue.size() stage._on_prop_collided(fallback_body, Vector2.RIGHT, Vector2(13.0, 20.0), prop) _check(runner._run_queue.size() == before_generic + 1, "non-stickman contacts preserve prop-to-other event ordering") var invalid_proxy := Node2D.new() invalid_proxy.set_meta("stickman_rig", rig) stage._world.add_child(invalid_proxy) var before_invalid: int = runner._run_queue.size() stage._on_prop_collided(invalid_proxy, Vector2.RIGHT, Vector2.ZERO, prop) _check(runner._run_queue.size() == before_invalid, "incomplete stickman metadata dispatches no generic rule") var ragdoll_proxy := Node2D.new() ragdoll_proxy.set_meta("stickman_ragdoll_body", true) stage._world.add_child(ragdoll_proxy) var before_suppressed := rig.queue_size() stage._on_prop_collided(ragdoll_proxy, Vector2.RIGHT, Vector2.ZERO, prop) _check(rig.queue_size() == before_suppressed, "ragdoll contacts dispatch no rule") fallback_body.queue_free() invalid_proxy.queue_free() ragdoll_proxy.queue_free() prop.queue_free() stage._event_rules.clear() rig.stop_queue() rig.clear_queue() func _test_ragdoll_transitions(rig: StickmanRig) -> void: var locomotion: StickmanLocomotion = rig.get_locomotion() rig.apply_collision_reaction("head", Vector2.RIGHT, 400.0) _check(not locomotion.impact_reactions.is_empty(), "animated rig accepts a reaction before ragdoll") rig.set_ragdoll(true) _check(rig.state == StickmanRig.RigState.RAGDOLL, "rig enters ragdoll") _check(_all_proxy_layers_equal(rig, 0), "ragdoll disables animated collision proxies") _check(locomotion.impact_reactions.is_empty(), "ragdoll entry clears reactions") rig.request_recovery() _check(rig.state == StickmanRig.RigState.RECOVERING, "recovery request starts the stand-up state") _check(_all_proxy_layers_equal(rig, 1), "collision proxies stay enabled throughout recovery") rig.apply_collision_reaction("head", Vector2.RIGHT, 400.0) _check(locomotion.impact_reactions.is_empty(), "recovering rig ignores visual reactions") rig.set_ragdoll(true) _check(rig.state == StickmanRig.RigState.RAGDOLL, "recovery can be interrupted by ragdoll") _check(_all_proxy_layers_equal(rig, 0), "recovery interruption disables proxies again") rig.snap_to_standing() _check(rig.state == StickmanRig.RigState.ANIMATED, "snap returns the rig to animated") _check(_all_proxy_layers_equal(rig, 1), "snap restores animated collision proxies") func _all_proxy_layers_equal(rig: StickmanRig, expected: int) -> bool: for body: AnimatableBody2D in rig.get_locomotion().collision_bodies.values(): if body.collision_layer != expected or body.collision_mask != expected: return false return true func _proxy_shape_sizes(rig: StickmanRig) -> Dictionary: var sizes := {} var locomotion: StickmanLocomotion = rig.get_locomotion() for part_name: String in locomotion.collision_bodies: var body := locomotion.collision_bodies[part_name] as AnimatableBody2D var shape_node := body.get_node(NodePath("CollisionShape2D")) as CollisionShape2D if shape_node.shape is CircleShape2D: sizes[part_name] = (shape_node.shape as CircleShape2D).radius else: var capsule := shape_node.shape as CapsuleShape2D sizes[part_name] = Vector2(capsule.radius, capsule.height) return sizes func _check(condition: bool, message: String) -> void: _checks += 1 if not condition: _failures += 1 push_error("FAIL: " + message) func _finish() -> void: if _failures == 0: print("RESULT: ALL PASSED (%d assertions)" % _checks) quit(0) else: print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks]) quit(1)