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:
2026-10-09 23:11:34 -04:00
parent 3f66cc3195
commit 1d4a71525a
52 changed files with 20051 additions and 109 deletions
+6 -3
View File
@@ -113,6 +113,7 @@ func _run() -> void:
prop.polygon_points = PackedVector2Array([Vector2(-10, -10), Vector2(10, -10), Vector2(10, 10), Vector2(-10, 10)])
prop.freeze = true
root.add_child(prop)
prop.apply_runtime_role(PROP_BLOCK.RuntimeRole.START_STILL)
await process_frame
var gizmos: StageGizmos = STAGE_GIZMOS.new()
root.add_child(gizmos)
@@ -128,6 +129,7 @@ func _run() -> void:
gizmos.drag_to(prop.global_position + (corner - prop.global_position) * 2.0)
gizmos.end_drag()
_check(is_equal_approx(prop.scale.x / initial_scale.x, 2.0) and is_equal_approx(prop.scale.y / initial_scale.y, 2.0), "corner drag applies uniform scale")
prop.begin_play_instance_scale()
await physics_frame
var space: PhysicsDirectSpaceState2D = root.get_world_2d().direct_space_state
var polygon_inside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new()
@@ -138,8 +140,7 @@ func _run() -> void:
var polygon_outside_hits: Array[Dictionary] = space.intersect_point(polygon_outside)
_check(not polygon_inside_hits.is_empty() and polygon_outside_hits.is_empty(), "scaled polygon collision matches its visual extent")
prop.global_rotation = deg_to_rad(20.0)
prop.begin_play_instance_scale()
prop.freeze = false
prop.apply_runtime_role(PROP_BLOCK.RuntimeRole.START_PHYSICS)
for frame: int in 3:
await physics_frame
var rotated_local_inside: Vector2 = Vector2(19.0, 0.0).rotated(prop.global_rotation)
@@ -154,6 +155,7 @@ func _run() -> void:
circle.scale = Vector2(2.0, 2.0)
circle.freeze = true
root.add_child(circle)
circle.apply_runtime_role(PROP_BLOCK.RuntimeRole.START_STILL)
await physics_frame
var circle_inside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new()
circle_inside.position = circle.global_position + Vector2(19.0, 0.0)
@@ -161,7 +163,7 @@ func _run() -> void:
circle_outside.position = circle.global_position + Vector2(21.0, 0.0)
_check(not space.intersect_point(circle_inside).is_empty() and space.intersect_point(circle_outside).is_empty(), "scaled circle collision matches its visual extent")
circle.begin_play_instance_scale()
circle.freeze = false
circle.apply_runtime_role(PROP_BLOCK.RuntimeRole.START_PHYSICS)
for frame: int in 3:
await physics_frame
circle_inside.position = circle.global_position + Vector2(19.0, 0.0)
@@ -171,6 +173,7 @@ func _run() -> void:
circle.end_play_instance_scale()
circle.scale = Vector2(2.0, 1.0)
circle.begin_play_instance_scale()
circle.apply_runtime_role(PROP_BLOCK.RuntimeRole.START_PHYSICS)
await physics_frame
var ellipse_inside: PhysicsPointQueryParameters2D = PhysicsPointQueryParameters2D.new()
ellipse_inside.position = circle.global_position + Vector2(0.0, 9.0)