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
+155
View File
@@ -60,6 +60,8 @@ const MAX_CONTACTS_REPORTED := 32
const DEFAULT_WOBBLE_DURATION: float = 0.48
const DEFAULT_WOBBLE_ANGLE_DEGREES: float = 7.0
const DEFAULT_WOBBLE_OFFSET_LOCAL: float = 4.0
const OWNER_COLLISION_GRACE_CLEAR_MARGIN: float = 24.0
const OWNER_COLLISION_GRACE_CLEAR_FRAMES: int = 2
# ---------------------------------------------------------------------------
# Exported properties
@@ -129,6 +131,15 @@ var _wobble_center: Vector2 = Vector2.ZERO
var _wobble_baselines: Dictionary = {}
var _last_physics_velocity: Vector2 = Vector2.ZERO
var _has_physics_velocity_sample: bool = false
var _owner_collision_grace_actor_id: int = 0
var _owner_collision_grace_actor_ref: WeakRef
var _owner_collision_grace_locomotion_ref: WeakRef
var _owner_collision_grace_body_refs: Array[WeakRef] = []
var _owner_collision_grace_role: RuntimeRole = RuntimeRole.EDIT
var _owner_collision_grace_while_reserved: bool = false
var _owner_collision_grace_clear_frames: int = 0
var _owner_collision_grace_started_frame: int = -1
var _owner_collision_grace_last_sample_frame: int = -1
# ---------------------------------------------------------------------------
# Signals
@@ -178,12 +189,14 @@ func _process(delta: float) -> void:
func _physics_process(_delta: float) -> void:
_update_owner_collision_grace()
if not freeze and runtime_role in [RuntimeRole.START_PHYSICS, RuntimeRole.RELEASED]:
_last_physics_velocity = linear_velocity
_has_physics_velocity_sample = _last_physics_velocity.is_finite()
func _exit_tree() -> void:
clear_owner_collision_grace()
cancel_wobble()
@@ -320,6 +333,8 @@ func apply_stage_depth() -> void:
func apply_runtime_role(role: RuntimeRole) -> void:
runtime_role = role
if _owner_collision_grace_actor_id != 0 and (role != _owner_collision_grace_role or not is_actor_blocking()):
clear_owner_collision_grace()
match role:
RuntimeRole.EDIT:
freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
@@ -369,6 +384,146 @@ func is_actor_blocking() -> bool:
return bool(stage_roles.get("blocks_stickmen", false))
## Temporarily ignores contacts with one stickman. Pickup reservations can keep
## protection active until the reservation ends; Set Down uses exit clearance.
## The prop's authored role and contacts with supports or other stickmen remain.
func begin_owner_collision_grace(actor_rig: Node2D, while_reserved: bool = false) -> void:
clear_owner_collision_grace()
if actor_rig == null or not is_instance_valid(actor_rig) or not is_actor_blocking():
return
_owner_collision_grace_actor_id = actor_rig.get_instance_id()
_owner_collision_grace_actor_ref = weakref(actor_rig)
_owner_collision_grace_role = runtime_role
_owner_collision_grace_while_reserved = while_reserved
_owner_collision_grace_clear_frames = 0
_owner_collision_grace_started_frame = Engine.get_physics_frames()
_owner_collision_grace_last_sample_frame = -1
for body: PhysicsBody2D in _actor_collision_bodies(actor_rig):
body.add_collision_exception_with(self)
add_collision_exception_with(body)
_owner_collision_grace_body_refs.append(weakref(body))
if actor_rig.has_method("get_locomotion"):
var locomotion: Node = actor_rig.call("get_locomotion") as Node
if locomotion != null and is_instance_valid(locomotion) and locomotion.has_method("register_owner_collision_grace_prop"):
locomotion.call("register_owner_collision_grace_prop", self)
_owner_collision_grace_locomotion_ref = weakref(locomotion)
func release_owner_collision_grace(actor_rig: Node2D) -> void:
if not is_owner_collision_grace_active_for(actor_rig):
return
_owner_collision_grace_while_reserved = false
_owner_collision_grace_clear_frames = 0
_owner_collision_grace_started_frame = Engine.get_physics_frames()
_owner_collision_grace_last_sample_frame = -1
func is_owner_collision_grace_active_for(actor: Node) -> bool:
return (
actor != null
and is_instance_valid(actor)
and _owner_collision_grace_actor_id != 0
and actor.get_instance_id() == _owner_collision_grace_actor_id
)
func clear_owner_collision_grace() -> void:
for body_ref: WeakRef in _owner_collision_grace_body_refs:
var body: PhysicsBody2D = body_ref.get_ref() as PhysicsBody2D
if body != null and is_instance_valid(body):
body.remove_collision_exception_with(self)
remove_collision_exception_with(body)
_owner_collision_grace_body_refs.clear()
var locomotion: Node = _owner_collision_grace_locomotion_ref.get_ref() as Node if _owner_collision_grace_locomotion_ref != null else null
if locomotion != null and is_instance_valid(locomotion) and locomotion.has_method("unregister_owner_collision_grace_prop"):
locomotion.call("unregister_owner_collision_grace_prop", self)
_owner_collision_grace_locomotion_ref = null
_owner_collision_grace_actor_ref = null
_owner_collision_grace_actor_id = 0
_owner_collision_grace_role = RuntimeRole.EDIT
_owner_collision_grace_while_reserved = false
_owner_collision_grace_clear_frames = 0
_owner_collision_grace_started_frame = -1
_owner_collision_grace_last_sample_frame = -1
func _update_owner_collision_grace() -> void:
if _owner_collision_grace_actor_id == 0:
return
var actor: Node2D = _owner_collision_grace_actor_ref.get_ref() as Node2D if _owner_collision_grace_actor_ref != null else null
if actor == null or not is_instance_valid(actor) or runtime_role != _owner_collision_grace_role or not is_actor_blocking():
clear_owner_collision_grace()
return
if _owner_collision_grace_while_reserved:
return
var physics_frame: int = Engine.get_physics_frames()
if physics_frame <= _owner_collision_grace_started_frame or physics_frame == _owner_collision_grace_last_sample_frame:
return
_owner_collision_grace_last_sample_frame = physics_frame
if _actor_collision_shapes_overlap(actor):
_owner_collision_grace_clear_frames = 0
return
_owner_collision_grace_clear_frames += 1
if _owner_collision_grace_clear_frames >= OWNER_COLLISION_GRACE_CLEAR_FRAMES:
clear_owner_collision_grace()
func _actor_collision_shapes_overlap(actor_rig: Node2D) -> bool:
if not is_inside_tree() or get_world_2d() == null or collision_layer == 0:
return false
var space_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
if space_state == null:
return true
var active_shape_count: int = 0
for body: PhysicsBody2D in _actor_collision_bodies(actor_rig):
if body.collision_layer == 0:
continue
for child: Node in body.get_children():
var collision_shape: CollisionShape2D = child as CollisionShape2D
if collision_shape == null or collision_shape.disabled or collision_shape.shape == null:
continue
active_shape_count += 1
var query := PhysicsShapeQueryParameters2D.new()
query.shape = collision_shape.shape
query.transform = collision_shape.global_transform
query.collision_mask = collision_layer
query.margin = OWNER_COLLISION_GRACE_CLEAR_MARGIN
query.exclude = [body.get_rid()]
for hit: Dictionary in space_state.intersect_shape(query, 16):
if hit.get("collider") == self:
return true
return active_shape_count == 0
func _actor_collision_bodies(actor_rig: Node2D) -> Array[PhysicsBody2D]:
var result: Array[PhysicsBody2D] = []
var seen: Dictionary = {}
if actor_rig == null or not is_instance_valid(actor_rig):
return result
if actor_rig.has_method("get_collision_bodies"):
var collision_bodies: Variant = actor_rig.call("get_collision_bodies")
if collision_bodies is Dictionary:
for raw_body: Variant in (collision_bodies as Dictionary).values():
_append_actor_collision_body(raw_body, result, seen)
if actor_rig.has_method("get_ragdoll_bodies"):
var ragdoll_bodies: Variant = actor_rig.call("get_ragdoll_bodies")
if ragdoll_bodies is Array:
for raw_body: Variant in ragdoll_bodies as Array:
_append_actor_collision_body(raw_body, result, seen)
return result
func _append_actor_collision_body(raw_body: Variant, bodies: Array[PhysicsBody2D], seen: Dictionary) -> void:
var body: PhysicsBody2D = raw_body as PhysicsBody2D
if body == null or not is_instance_valid(body):
return
var body_id: int = body.get_instance_id()
if seen.has(body_id):
return
seen[body_id] = true
bodies.append(body)
func can_receive_prop_contact() -> bool:
if runtime_role == RuntimeRole.HELD or runtime_role == RuntimeRole.EDIT:
return false