feat(stage): complete phase 20b action plans

This commit is contained in:
2026-11-04 01:19:57 -05:00
parent 87a33b0abc
commit a9c0886348
70 changed files with 14234 additions and 469 deletions
+129
View File
@@ -6,10 +6,12 @@ const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
const PROP_BLOCK_SCRIPT := preload("res://scripts/props/prop_block.gd")
const PROP_INTERACTOR_SCRIPT := preload("res://scripts/characters/stickman/stickman_prop_interactor.gd")
const PROP_ANCHOR_DATA := preload("res://scripts/props/prop_anchor_data.gd")
const STICKMAN_RIG_SCENE := preload("res://scenes/characters/stickman/stickman_rig.tscn")
var _checks: int = 0
var _failures: int = 0
var _pickup_test_prop: PropBlock = null
func _initialize() -> void:
@@ -22,6 +24,8 @@ func _run() -> void:
_test_set_down_serialization()
_test_unknown_preview()
await _test_runtime_profiles()
await _test_set_down_owner_collision_grace()
await _test_reserved_pickup_collision_protection()
await _test_start_still_support_contact()
await _test_set_down_clearance_and_release()
await _test_set_down_preserves_pickup_orientation()
@@ -140,6 +144,131 @@ func _test_runtime_profiles() -> void:
await process_frame
func _test_set_down_owner_collision_grace() -> void:
var world := Node2D.new()
root.add_child(world)
var owner_rig := STICKMAN_RIG_SCENE.instantiate() as Node2D
world.add_child(owner_rig)
var other_rig := STICKMAN_RIG_SCENE.instantiate() as Node2D
other_rig.position = Vector2(1000.0, 0.0)
world.add_child(other_rig)
await physics_frame
var owner_bodies: Dictionary = owner_rig.call("get_collision_bodies")
var torso: AnimatableBody2D = owner_bodies.get("torso") as AnimatableBody2D
_expect(torso != null, "Set Down grace fixture has an animated torso collider")
if torso == null:
world.queue_free()
await process_frame
return
var prop: PropBlock = PROP_BLOCK_SCRIPT.new()
prop.polygon_points = PackedVector2Array([Vector2(-20.0, -20.0), Vector2(20.0, -20.0), Vector2(20.0, 20.0), Vector2(-20.0, 20.0)])
prop.gravity_scale = 0.0
world.add_child(prop)
await physics_frame
prop.global_position = torso.global_position
prop.set_stage_roles(PROP_STAGE_ROLES.new_placement(true))
prop.apply_runtime_role(PropBlock.RuntimeRole.HELD)
prop.interaction_owner_id = owner_rig.get_instance_id()
var interactor: Node = owner_rig.get_node(NodePath("RigComponents/PropInteractor"))
interactor.set("_held_prop", prop)
interactor.call("_release_held", "set_down", false, Vector2.ZERO, 0.0, true, false)
for _frame: int in 4:
await physics_frame
_expect(prop.runtime_role == PropBlock.RuntimeRole.RELEASED and prop.is_actor_blocking(), "Set Down grace preserves the prop's authored released collision role")
_expect(prop.is_owner_collision_grace_active_for(owner_rig), "Set Down release starts grace for the releasing stickman")
_expect(not prop.is_owner_collision_grace_active_for(other_rig), "Set Down grace does not apply to a different stickman")
var owner_exceptions: Array[PhysicsBody2D] = torso.get_collision_exceptions()
_expect(owner_exceptions.has(prop) and prop.get_collision_exceptions().has(torso), "Set Down grace installs a symmetric exception for the releasing stickman's collider")
var other_bodies: Dictionary = other_rig.call("get_collision_bodies")
var other_torso: AnimatableBody2D = other_bodies.get("torso") as AnimatableBody2D
_expect(other_torso != null and not other_torso.get_collision_exceptions().has(prop), "other stickmen keep ordinary prop collision")
var locomotion: Node = owner_rig.call("get_locomotion") as Node
var owner_sweep: Dictionary = locomotion.call("_sweep_actor_blocking_props", Vector2(40.0, 0.0))
_expect(float(owner_sweep.get("fraction", 0.0)) >= 0.999, "the releasing stickman's movement sweep skips its grace prop")
owner_rig.global_position += Vector2(1000.0, 0.0)
await physics_frame
_expect(prop.is_owner_collision_grace_active_for(owner_rig), "Set Down grace waits for a second clear physics frame")
await physics_frame
_expect(not prop.is_owner_collision_grace_active_for(owner_rig), "Set Down grace ends after the releasing stickman's collision shapes clear the prop")
_expect(not torso.get_collision_exceptions().has(prop) and not prop.get_collision_exceptions().has(torso), "clearance removes both sides of the temporary collision exception")
prop.apply_runtime_role(PropBlock.RuntimeRole.HELD)
interactor.set("_held_prop", prop)
interactor.call("_release_held", "manual", false, Vector2.ZERO, 0.0, true, false)
_expect(not prop.is_owner_collision_grace_active_for(owner_rig), "Drop does not receive Set Down collision grace")
world.queue_free()
await process_frame
func _test_reserved_pickup_collision_protection() -> void:
var world := Node2D.new()
root.add_child(world)
var rig := STICKMAN_RIG_SCENE.instantiate() as Node2D
world.add_child(rig)
await physics_frame
var collision_bodies: Dictionary = rig.call("get_collision_bodies")
var torso: AnimatableBody2D = collision_bodies.get("torso") as AnimatableBody2D
_expect(torso != null, "pickup protection fixture has an animated torso collider")
if torso == null:
world.queue_free()
await process_frame
return
var prop: PropBlock = PROP_BLOCK_SCRIPT.new()
prop.stage_object_id = "pickup-protection-test-prop"
prop.polygon_points = PackedVector2Array([Vector2(-24.0, -20.0), Vector2(24.0, -20.0), Vector2(24.0, 20.0), Vector2(-24.0, 20.0)])
prop.gravity_scale = 0.0
world.add_child(prop)
await physics_frame
prop.global_position = torso.global_position + Vector2(800.0, 0.0)
prop.set_interaction_anchors(PROP_ANCHOR_DATA.preset_layout("one_handed", Rect2(Vector2(-24.0, -20.0), Vector2(48.0, 40.0))))
var roles: Dictionary = PROP_STAGE_ROLES.new_placement(true)
roles["blocks_stickmen"] = true
roles["released_blocks_stickmen"] = true
prop.set_stage_roles(roles)
prop.apply_runtime_role(PropBlock.RuntimeRole.RELEASED)
_pickup_test_prop = prop
var interactor: Node = rig.get_node(NodePath("RigComponents/PropInteractor"))
interactor.set("_resolver", Callable(self, "_resolve_pickup_test_prop"))
var action := {"type": "pick_up_prop", "target_prop_id": prop.stage_object_id, "hand": "auto"}
var started: bool = bool(interactor.call("start_action", action))
_expect(started and int(interactor.get("_phase")) == PROP_INTERACTOR_SCRIPT.Phase.APPROACHING, "pickup reserves a distant prop and enters the approach phase")
_expect(prop.interaction_reserved_by == rig.get_instance_id() and prop.is_owner_collision_grace_active_for(rig), "pickup reservation starts protection for its target")
for _frame: int in 5:
await physics_frame
_expect(prop.is_owner_collision_grace_active_for(rig), "reserved pickup protection stays active while the stickman is far away")
var locomotion: Node = rig.call("get_locomotion") as Node
var approach_sweep: Dictionary = locomotion.call("_sweep_actor_blocking_props", Vector2(1000.0, 0.0))
_expect(float(approach_sweep.get("fraction", 0.0)) >= 0.999, "the owner's approach sweep skips its reserved pickup target")
var hand_marker: Marker2D = interactor.call("_hand_marker") as Marker2D
interactor.call("_commit_pickup", hand_marker)
_expect(prop.runtime_role == PropBlock.RuntimeRole.HELD and not prop.is_owner_collision_grace_active_for(rig), "successful pickup clears target protection through the held profile")
interactor.call("reset_for_scene_exit")
await physics_frame
var torso_local: Vector2 = rig.to_local(torso.global_position)
locomotion.call("cancel_walk")
rig.global_position = prop.global_position - torso_local
await physics_frame
var retry_started: bool = bool(interactor.call("start_action", action))
_expect(retry_started and prop.is_owner_collision_grace_active_for(rig), "a retry protects its target before approach begins")
locomotion.call("cancel_walk")
interactor.call("cancel_action")
_expect(prop.interaction_reserved_by == 0 and prop.is_owner_collision_grace_active_for(rig), "cancel releases the reservation but keeps exit protection")
for _frame: int in 4:
await physics_frame
_expect(prop.is_owner_collision_grace_active_for(rig), "cancel protection remains while the stickman still overlaps the target")
rig.global_position += Vector2(1000.0, 0.0)
await physics_frame
_expect(prop.is_owner_collision_grace_active_for(rig), "cancel protection waits for a second clear frame")
await physics_frame
_expect(not prop.is_owner_collision_grace_active_for(rig), "cancel protection clears after the stickman leaves the target")
_pickup_test_prop = null
world.queue_free()
await process_frame
func _resolve_pickup_test_prop(prop_id: String) -> PropBlock:
return _pickup_test_prop if _pickup_test_prop != null and _pickup_test_prop.stage_object_id == prop_id else null
func _test_start_still_support_contact() -> void:
var world := Node2D.new()
root.add_child(world)