Implement collision bodies, physics events, and hit reactions for StickmanRig
- Added a new specification for collision bodies, detailing the implementation requirements and corrections needed in the StickmanRig, PropBlock, and SandboxStage scripts. - Created a comprehensive test suite for collision bodies, including tests for proxy geometry, physics alignment, reaction handling, ghost collision, and stage contact routing. - Ensured that the collision system correctly handles ragdoll transitions and maintains the integrity of existing event rules. - Introduced new methods in StickmanRig for managing collision bodies and applying reactions based on impact direction and speed. - Updated PropBlock to emit signals for collisions and separations, preserving the order of events for stickman and prop interactions.
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
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:
|
||||
_check(rig._collision_bodies.size() == 10, "rig builds ten collision proxies")
|
||||
_check(rig._collision_lengths.size() == 9, "rig records nine fitted capsule lengths")
|
||||
for part_name: String in rig.COLLISION_PARTS:
|
||||
var definition: Dictionary = rig.COLLISION_PARTS[part_name]
|
||||
var body := rig._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 >= rig.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 = rig._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(rig._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 = rig._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:
|
||||
rig.walk_to(rig.global_position + Vector2(-400.0, 0.0), 300.0)
|
||||
for _frame in 8:
|
||||
await physics_frame
|
||||
await process_frame
|
||||
rig._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 rig.COLLISION_PARTS:
|
||||
var definition: Dictionary = rig.COLLISION_PARTS[part_name]
|
||||
var visual := rig.get_node_or_null(NodePath("Body/" + String(definition["visual"]))) as Node2D
|
||||
var body := rig._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._cancel_walking()
|
||||
rig.set_facing_profile(StickmanRig.FacingProfile.FORWARD)
|
||||
|
||||
|
||||
func _test_reactions(rig: StickmanRig) -> void:
|
||||
rig._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(rig._impact_reactions.is_empty(), "invalid reactions are ignored")
|
||||
|
||||
var head_proxy := rig._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((rig._impact_reactions["Head"] as Dictionary)["angle"]) < 0.0, "upper-left head contact lowers the struck side")
|
||||
rig._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((rig._impact_reactions["Head"] as Dictionary)["angle"]) > 0.0, "upper-right head contact lowers the struck side")
|
||||
rig._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((rig._impact_reactions["Head"] as Dictionary)["angle"]) > 0.0, "lower-left upward contact inverts the upper-left response")
|
||||
rig._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((rig._impact_reactions["Head"] as Dictionary)["angle"]) < 0.0, "lower-right upward contact inverts the upper-right response")
|
||||
rig._impact_reactions.clear()
|
||||
|
||||
rig.apply_collision_reaction("left_lower_arm", Vector2.RIGHT, 10000.0)
|
||||
var lower: Dictionary = rig._impact_reactions.get("LeftLowerArm", {})
|
||||
var upper: Dictionary = rig._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))
|
||||
(rig._impact_reactions["LeftLowerArm"] as Dictionary)["strength"] = 0.25
|
||||
rig.apply_collision_reaction("left_lower_arm", Vector2.LEFT, 40.0)
|
||||
lower = rig._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 = rig._impact_reactions["LeftLowerArm"]
|
||||
_check(signf(float(lower["angle"])) != signf(strong_angle), "equal capped hit may replace the reaction direction")
|
||||
var lower_bone := rig._reaction_bones["LeftLowerArm"] as Bone2D
|
||||
var base_rotation := lower_bone.rotation
|
||||
var max_offset := 0.0
|
||||
for frame in 30:
|
||||
rig._apply_impact_reactions(1.0 / 60.0)
|
||||
max_offset = maxf(max_offset, absf(angle_difference(base_rotation, lower_bone.rotation)))
|
||||
_check(max_offset <= rig.REACTION_MAX_ANGLE + 0.001, "repeated reaction frames never accumulate beyond the angular cap")
|
||||
rig._apply_impact_reactions(1.0)
|
||||
_check(rig._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 rig.STAND_POSE:
|
||||
var marker := rig.get_node("IK_Targets/" + marker_name) as Node2D
|
||||
var expected: Dictionary = rig.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(rig.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:
|
||||
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 lower := rig._collision_bodies["left_lower_arm"] as AnimatableBody2D
|
||||
var upper := rig._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 initial_queue_size := rig.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(rig.queue_size() == initial_queue_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(rig.queue_size() == initial_queue_size + 1, "second active part does not repeat the pair rule")
|
||||
_check(rig._impact_reactions.has("LeftLowerArm") and rig._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(rig.queue_size() == initial_queue_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 := rig.queue_size()
|
||||
stage._on_prop_collided(fallback_body, Vector2.RIGHT, Vector2(13.0, 20.0), prop)
|
||||
_check(rig.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 := rig.queue_size()
|
||||
stage._on_prop_collided(invalid_proxy, Vector2.RIGHT, Vector2.ZERO, prop)
|
||||
_check(rig.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:
|
||||
rig.apply_collision_reaction("head", Vector2.RIGHT, 400.0)
|
||||
_check(not rig._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(rig._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(rig._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._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 := {}
|
||||
for part_name: String in rig._collision_bodies:
|
||||
var body := rig._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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://btt85kp10h5mk
|
||||
Reference in New Issue
Block a user