Add new scene files and implement stage impact resolver

- Created three new scene files: `wobble.scene`, `wobble2.scene`, and `wobble3.scene` with various terrain and prop configurations.
- Introduced `stage_impact_resolver.gd` script to manage impact detection and response between props and stickmen.
- Added unit tests for actor blocking overlap to ensure proper interaction with stationary props.
This commit is contained in:
2026-10-14 13:08:34 -04:00
parent 628c07e70e
commit 5c3b056a13
21 changed files with 1891 additions and 22 deletions
+154
View File
@@ -38,6 +38,7 @@ var use_spots: Array[Dictionary] = []
var next_use_spot_number: int = 1
var stage_roles: Dictionary = {}
var runtime_role: RuntimeRole = RuntimeRole.EDIT
var wobble_on_impact: bool = false
# ---------------------------------------------------------------------------
# Child node names
@@ -55,6 +56,9 @@ const DEFAULT_FILL_COLOR := Color(0.55, 0.35, 0.15, 1.0)
const DEFAULT_OUTLINE_COLOR := Color(0.15, 0.08, 0.02, 1.0)
const DEFAULT_OUTLINE_WIDTH: float = 2.0
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
# ---------------------------------------------------------------------------
# Exported properties
@@ -114,6 +118,16 @@ var _visual_local_bounds := Rect2()
var _custom_collision_hull: PackedVector2Array = PackedVector2Array()
var _play_instance_scale: Vector2 = Vector2.ONE
var _play_scale_active: bool = false
var _wobble_duration: float = DEFAULT_WOBBLE_DURATION
var _wobble_angle_degrees: float = DEFAULT_WOBBLE_ANGLE_DEGREES
var _wobble_offset_local: float = DEFAULT_WOBBLE_OFFSET_LOCAL
var _wobble_elapsed: float = 0.0
var _wobble_active: bool = false
var _wobble_paused: bool = false
var _wobble_center: Vector2 = Vector2.ZERO
var _wobble_baselines: Dictionary = {}
var _last_physics_velocity: Vector2 = Vector2.ZERO
var _has_physics_velocity_sample: bool = false
# ---------------------------------------------------------------------------
# Signals
@@ -144,6 +158,144 @@ func _ready() -> void:
body_entered.connect(_on_body_entered)
if not body_exited.is_connected(_on_body_exited):
body_exited.connect(_on_body_exited)
reset_impact_velocity_baseline()
func _process(delta: float) -> void:
if Engine.is_editor_hint() or not _wobble_active or _wobble_paused:
return
_wobble_elapsed = minf(_wobble_elapsed + maxf(delta, 0.0), _wobble_duration)
var progress: float = clampf(_wobble_elapsed / _wobble_duration, 0.0, 1.0)
if progress >= 1.0:
cancel_wobble()
return
var envelope: float = (1.0 - progress) * (1.0 - progress)
var phase: float = TAU * 2.5 * progress
var angle: float = deg_to_rad(_wobble_angle_degrees) * sin(phase) * envelope
var offset: float = _wobble_offset_local * cos(phase) * envelope
_apply_wobble_pose(angle, offset)
func _physics_process(_delta: float) -> void:
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:
cancel_wobble()
func set_wobble_on_impact(enabled: bool) -> void:
wobble_on_impact = enabled
if not enabled:
cancel_wobble()
func set_wobble_style(duration: float, angle_degrees: float, offset_local: float) -> void:
if not is_finite(duration) or not is_finite(angle_degrees) or not is_finite(offset_local):
return
_wobble_duration = maxf(0.05, duration)
_wobble_angle_degrees = maxf(0.0, angle_degrees)
_wobble_offset_local = maxf(0.0, offset_local)
func preview_wobble() -> void:
_start_wobble()
func play_impact_wobble() -> void:
if wobble_on_impact:
_start_wobble()
func cancel_wobble() -> void:
for raw_node: Variant in _wobble_baselines.keys():
var node: Node2D = raw_node as Node2D
if node == null or not is_instance_valid(node):
continue
var baseline: Dictionary = _wobble_baselines[raw_node]
node.position = baseline["position"] as Vector2
node.rotation = float(baseline["rotation"])
node.scale = baseline["scale"] as Vector2
_wobble_baselines.clear()
_wobble_active = false
_wobble_paused = false
_wobble_elapsed = 0.0
func set_wobble_paused(paused: bool) -> void:
_wobble_paused = paused and _wobble_active
func get_impact_velocity() -> Vector2:
if runtime_role in [RuntimeRole.EDIT, RuntimeRole.START_STILL, RuntimeRole.HELD] or freeze:
return Vector2.ZERO
if _has_physics_velocity_sample and _last_physics_velocity.is_finite():
return _last_physics_velocity
return linear_velocity if linear_velocity.is_finite() else Vector2.ZERO
func reset_impact_velocity_baseline() -> void:
_last_physics_velocity = linear_velocity if linear_velocity.is_finite() else Vector2.ZERO
_has_physics_velocity_sample = true
func _start_wobble() -> void:
cancel_wobble()
_capture_wobble_baselines()
if _wobble_baselines.is_empty():
return
_wobble_active = true
_wobble_elapsed = 0.0
_apply_wobble_pose(0.0, _wobble_offset_local)
func _capture_wobble_baselines() -> void:
var bounds: Dictionary = {"min_x": INF, "min_y": INF, "max_x": -INF, "max_y": -INF}
for child_name: String in [POLYGON_NODE_NAME, OUTLINE_NODE_NAME, "Visuals"]:
var node: Node2D = get_node_or_null(NodePath(child_name)) as Node2D
if node == null or not node.visible:
continue
_wobble_baselines[node] = {"position": node.position, "rotation": node.rotation, "scale": node.scale}
_collect_artwork_bounds(node, bounds)
if float(bounds["min_x"]) <= float(bounds["max_x"]):
_wobble_center = Vector2(
(float(bounds["min_x"]) + float(bounds["max_x"])) * 0.5,
(float(bounds["min_y"]) + float(bounds["max_y"])) * 0.5
)
else:
_wobble_center = get_use_spot_local_bounds().get_center()
func _collect_artwork_bounds(node: Node, bounds: Dictionary) -> void:
if node is Polygon2D:
for point: Vector2 in (node as Polygon2D).polygon:
_accumulate_artwork_point(to_local((node as Polygon2D).to_global(point)), bounds)
elif node is Line2D:
for point: Vector2 in (node as Line2D).points:
_accumulate_artwork_point(to_local((node as Line2D).to_global(point)), bounds)
for child: Node in node.get_children():
_collect_artwork_bounds(child, bounds)
func _accumulate_artwork_point(point: Vector2, bounds: Dictionary) -> void:
bounds["min_x"] = minf(float(bounds["min_x"]), point.x)
bounds["min_y"] = minf(float(bounds["min_y"]), point.y)
bounds["max_x"] = maxf(float(bounds["max_x"]), point.x)
bounds["max_y"] = maxf(float(bounds["max_y"]), point.y)
func _apply_wobble_pose(angle: float, offset: float) -> void:
for raw_node: Variant in _wobble_baselines:
var node: Node2D = raw_node as Node2D
if node == null or not is_instance_valid(node):
continue
var baseline: Dictionary = _wobble_baselines[raw_node]
var base_position: Vector2 = baseline["position"] as Vector2
node.position = _wobble_center + (base_position - _wobble_center).rotated(angle) + Vector2(offset, 0.0)
node.rotation = float(baseline["rotation"]) + angle
node.scale = baseline["scale"] as Vector2
func set_stage_roles(roles: Dictionary) -> void:
@@ -507,6 +659,7 @@ func get_interaction_scale() -> Vector2:
## the authored uniform scale into child visuals and collision geometry for
## PLAY, then restore the root scale on return to EDIT for scene persistence.
func begin_play_instance_scale() -> void:
cancel_wobble()
if _play_scale_active:
return
_play_instance_scale = scale
@@ -518,6 +671,7 @@ func begin_play_instance_scale() -> void:
_sync_transform_to_physics_server()
func end_play_instance_scale() -> void:
cancel_wobble()
if not _play_scale_active:
return
_play_scale_active = false