Implement Phase 17a Prop Roles UI/UX Specification and associated functionality
- Added UI/UX specification for prop roles and set down mechanics in phase 17a. - Created `PropStageRoles` script to manage normalized behavior for props, including depth, motion, and collision settings. - Introduced `StagePhysicsPolicy` for shared physics configurations related to props and actors. - Developed comprehensive tests for prop roles, including default settings, legacy migration, serialization, and runtime behavior. - Ensured proper handling of set down actions, including clearance and orientation preservation.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const STAGE_SCENE := preload("res://scenes/stage/sandbox_stage.tscn")
|
||||
const STAGE_PHYSICS := preload("res://scripts/stage/stage_physics_policy.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
@@ -23,7 +24,7 @@ func _run() -> void:
|
||||
_test_collision_proxy_geometry(rig)
|
||||
await _test_collision_proxy_physics_alignment(rig)
|
||||
_test_reactions(rig)
|
||||
await _test_direct_restores_animated_pose(stage, rig)
|
||||
await _test_direct_pause_and_edit_restore(stage, rig)
|
||||
_test_ghost_collision(stage)
|
||||
await _test_stage_contact_routing(stage, rig)
|
||||
_test_ragdoll_transitions(rig)
|
||||
@@ -54,7 +55,7 @@ func _test_collision_proxy_geometry(rig: StickmanRig) -> void:
|
||||
_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.collision_layer == STAGE_PHYSICS.ACTOR_LAYER and body.collision_mask == STAGE_PHYSICS.actor_mask(), "%s proxy starts with the actor policy" % 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)
|
||||
|
||||
@@ -90,9 +91,9 @@ func _test_collision_proxy_geometry(rig: StickmanRig) -> void:
|
||||
empty_visual.free()
|
||||
|
||||
rig.set_collision_bodies_enabled(false)
|
||||
_check(_all_proxy_layers_equal(rig, 0), "public collision hook disables every proxy")
|
||||
_check(_all_proxy_layers_equal(rig, 0, 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")
|
||||
_check(_all_proxy_layers_equal(rig, STAGE_PHYSICS.ACTOR_LAYER, STAGE_PHYSICS.actor_mask()), "public collision hook re-enables every proxy with the actor policy")
|
||||
|
||||
var sizes_before := _proxy_shape_sizes(rig)
|
||||
rig.set_facing_profile(StickmanRig.FacingProfile.LEFT)
|
||||
@@ -181,7 +182,7 @@ func _test_reactions(rig: StickmanRig) -> void:
|
||||
_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:
|
||||
func _test_direct_pause_and_edit_restore(stage: Node2D, rig: StickmanRig) -> void:
|
||||
var authored: Dictionary = stage._authored[rig.get_instance_id()]
|
||||
var ragdoll := rig.get_node("RigComponents/Ragdoll") as StickmanRagdoll
|
||||
var authored_markers := ragdoll.get_spawn_pose()
|
||||
@@ -194,18 +195,37 @@ func _test_direct_restores_animated_pose(stage: Node2D, rig: StickmanRig) -> voi
|
||||
torso_marker.rotation = PI
|
||||
var head_marker := rig.get_node("IK_Targets/Head") as Marker2D
|
||||
head_marker.position += Vector2(120.0, 80.0)
|
||||
var direct_position: Vector2 = rig.global_position
|
||||
var direct_rotation: float = rig.global_rotation
|
||||
var direct_facing: int = rig.get_facing_profile()
|
||||
var direct_markers: Dictionary = {}
|
||||
for marker_name: String in StickmanRagdoll.STAND_POSE:
|
||||
var marker := rig.get_node("IK_Targets/" + marker_name) as Node2D
|
||||
direct_markers[marker_name] = {"pos": marker.position, "rot": marker.rotation}
|
||||
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")
|
||||
_check(rig.global_position.distance_to(direct_position) < 0.01, "PLAY to DIRECT preserves the live stickman position while paused")
|
||||
_check(absf(angle_difference(direct_rotation, rig.global_rotation)) < 0.001, "PLAY to DIRECT preserves the live stickman rotation while paused")
|
||||
_check(rig.get_facing_profile() == direct_facing, "PLAY to DIRECT preserves the live facing profile while paused")
|
||||
for marker_name: String in StickmanRagdoll.STAND_POSE:
|
||||
var marker := rig.get_node("IK_Targets/" + marker_name) as Node2D
|
||||
var paused_pose: Dictionary = direct_markers[marker_name]
|
||||
_check(marker.position.distance_to(paused_pose["pos"] as Vector2) < 0.01, "%s marker stays in its live pose while DIRECT is paused" % marker_name)
|
||||
_check(absf(angle_difference(float(paused_pose["rot"]), marker.rotation)) < 0.001, "%s marker keeps its live rotation while DIRECT is paused" % marker_name)
|
||||
stage.set_mode(stage.StageMode.EDIT)
|
||||
for _frame in 4:
|
||||
await physics_frame
|
||||
await process_frame
|
||||
_check(rig.global_position.distance_to(authored["position"] as Vector2) < 0.01, "EDIT/STOP reset restores the authored stickman position")
|
||||
_check(absf(angle_difference(float(authored["rotation"]), rig.global_rotation)) < 0.001, "EDIT/STOP reset restores the authored stickman rotation")
|
||||
_check(rig.get_facing_profile() == int(authored["facing_profile"]), "EDIT/STOP reset 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 = authored_markers[marker_name]
|
||||
_check(marker.position.distance_to(expected["pos"] as Vector2) < 0.01, "%s marker returns to its authored spawn pose after PLAY" % marker_name)
|
||||
_check(absf(angle_difference(torso_marker.rotation, float((authored_markers["Torso"] as Dictionary)["rot"]))) < 0.001, "torso marker restores its authored rotation after PLAY")
|
||||
_check(marker.position.distance_to(expected["pos"] as Vector2) < 0.01, "%s marker returns to its authored pose after EDIT/STOP reset" % marker_name)
|
||||
_check(absf(angle_difference(torso_marker.rotation, float((authored_markers["Torso"] as Dictionary)["rot"]))) < 0.001, "EDIT/STOP reset restores the authored torso marker rotation")
|
||||
|
||||
|
||||
func _test_ghost_collision(stage: Node2D) -> void:
|
||||
@@ -213,12 +233,14 @@ func _test_ghost_collision(stage: Node2D) -> void:
|
||||
if ghost == null:
|
||||
return
|
||||
stage._configure_ghost_collision(ghost)
|
||||
_check(_all_proxy_layers_equal(ghost, 0), "stickman placement ghosts have no collision")
|
||||
_check(_all_proxy_layers_equal(ghost, 0, 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)
|
||||
for _frame: int in 3:
|
||||
await physics_frame
|
||||
var prop := PropBlock.new()
|
||||
prop.position = Vector2(50.0, 25.0)
|
||||
prop.linear_velocity = Vector2(400.0, 0.0)
|
||||
@@ -332,26 +354,26 @@ func _test_ragdoll_transitions(rig: StickmanRig) -> void:
|
||||
_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(_all_proxy_layers_equal(rig, 0, 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")
|
||||
_check(_all_proxy_layers_equal(rig, STAGE_PHYSICS.ACTOR_LAYER, STAGE_PHYSICS.actor_mask()), "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")
|
||||
_check(_all_proxy_layers_equal(rig, 0, 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")
|
||||
_check(_all_proxy_layers_equal(rig, STAGE_PHYSICS.ACTOR_LAYER, STAGE_PHYSICS.actor_mask()), "snap restores animated collision proxies")
|
||||
|
||||
|
||||
func _all_proxy_layers_equal(rig: StickmanRig, expected: int) -> bool:
|
||||
func _all_proxy_layers_equal(rig: StickmanRig, expected_layer: int, expected_mask: int) -> bool:
|
||||
for body: AnimatableBody2D in rig.get_locomotion().collision_bodies.values():
|
||||
if body.collision_layer != expected or body.collision_mask != expected:
|
||||
if body.collision_layer != expected_layer or body.collision_mask != expected_mask:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user