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
+205 -1
View File
@@ -44,6 +44,7 @@ const STAGE_RULE_AUTHORING_SCRIPT := preload("res://scripts/editors/stage/stage_
const STAGE_ACTION_QUEUE_SCRIPT := preload("res://scripts/editors/stage/stage_action_queue_controller.gd")
const STAGE_DIRECTOR_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_director_controller.gd")
const STAGE_RUN_SESSION_SCRIPT := preload("res://scripts/editors/stage/stage_run_session.gd")
const STAGE_IMPACT_RESOLVER_SCRIPT := preload("res://scripts/stage/stage_impact_resolver.gd")
const STAGE_ASSET_BROWSER_SCRIPT := preload("res://scripts/editors/stage/stage_asset_browser.gd")
const STAGE_MODAL_VIEWS_SCRIPT := preload("res://scripts/editors/stage/stage_modal_views.gd")
const STAGE_THEME_CONTROLLER_SCRIPT := preload("res://scripts/editors/stage/stage_theme_controller.gd")
@@ -241,6 +242,7 @@ var _grid: StageGrid
var _spatial_index: StageSpatialIndex
var _terrain_painter: StageTerrainPainter
var _rule_engine: StageRuleEngine
var _impact_resolver: StageImpactResolver
var _rule_authoring: StageRuleAuthoringController
var _action_queue_controller: StageActionQueueController
var _speech_session: StageSpeechSession
@@ -248,6 +250,7 @@ var _asset_browser: StageAssetBrowser
var _modal_views: StageModalViews
var _chrome: StageChrome
var _prop_settings_view: StagePropSettingsView
var _edit_wobble_preview_prop: PropBlock
var _use_spot_overlay: StageUseSpotOverlay
var _use_target_capture_bar: StageUseTargetCaptureBar
var _placement_overlay = null # StagePlacementOverlay (preloaded)
@@ -1181,6 +1184,7 @@ func _ready() -> void:
get_tree().auto_accept_quit = false
_camera.make_current()
_run_session = STAGE_RUN_SESSION_SCRIPT.new() as StageRunSession
_impact_resolver = STAGE_IMPACT_RESOLVER_SCRIPT.new() as StageImpactResolver
_document = STAGE_DOCUMENT_CONTROLLER_SCRIPT.new() as StageDocumentController
_document.open_path_requested.connect(_on_document_open_path_requested)
_document.save_path_selected.connect(_on_scene_save_path_selected)
@@ -1311,6 +1315,8 @@ func _process(delta: float) -> void:
func _physics_process(delta: float) -> void:
if _impact_resolver != null:
_impact_resolver.finish_sweep_tick(Engine.get_physics_frames())
_update_throw_preview_cache()
_update_use_target_preview_cache()
var elapsed_timestamp: float = _run_session.advance_elapsed(delta) if _run_session != null else -1.0
@@ -1676,6 +1682,7 @@ func set_mode(mode: StageMode) -> void:
_sync_mode_buttons()
return
var previous_mode: StageMode = current_mode
_cancel_edit_wobble_preview()
if mode == StageMode.PLAY and _run_state == RunState.STOPPED and not _cli_startup:
if not _save_scene_to_path(AUTOSAVE_PATH, true):
push_warning("SandboxStage: autosave before Play failed.")
@@ -1781,6 +1788,9 @@ func _enter_play_mode() -> void:
func _start_run() -> void:
_cancel_edit_wobble_preview()
if _impact_resolver != null:
_impact_resolver.reset()
# STOP reasserts the authored transforms for a few physics frames. If the
# child immediately presses PLAY, those delayed writes must not put an
# authored prop scale back on the root after begin_play_instance_scale()
@@ -1795,6 +1805,9 @@ func _start_run() -> void:
func _pause_run() -> void:
if not _run_session.pause_run(_world_children_selectable()):
return
if _impact_resolver != null:
_impact_resolver.reset()
_seed_impact_contact_baseline()
if _use_spot_overlay != null:
_use_spot_overlay.set_paused(true)
_refresh_event_edge_baseline()
@@ -1813,6 +1826,9 @@ func _resume_run() -> void:
_clear_director_pending()
if not _run_session.resume_run(_world_children_selectable(), _rule_engine):
return
if _impact_resolver != null:
_impact_resolver.reset()
_seed_impact_contact_baseline()
if _use_spot_overlay != null:
_use_spot_overlay.set_paused(false)
_sync_run_controls()
@@ -1820,6 +1836,9 @@ func _resume_run() -> void:
func _reset_run() -> void:
_cancel_edit_wobble_preview()
if _impact_resolver != null:
_impact_resolver.reset()
if PROP_TRACE_ENABLED:
print("[PropTrace] stage reset_run mode=%s run_state=%s" % [StageMode.keys()[int(current_mode)], RunState.keys()[int(_run_state)]])
_run_session.begin_reset()
@@ -1856,6 +1875,7 @@ func _sync_run_controls() -> void:
func _finish_resume_baseline() -> void:
if _run_state == RunState.RUNNING and current_mode == StageMode.PLAY:
_seed_impact_contact_baseline()
_event_dispatch_ready = true
@@ -1876,6 +1896,8 @@ func _refresh_event_edge_baseline() -> void:
_rule_engine.refresh_event_edge_baseline(_world_children_selectable())
func _save_object_state(node: Node2D) -> void:
if node is StickmanRig:
(node as StickmanRig).reset_collision_proxy_velocity_baselines()
_run_session.save_object_state(node)
@@ -1957,12 +1979,19 @@ func _finalize_placed_node(node: Node2D) -> void:
rig.action_finished.connect(_on_rig_action_finished.bind(rig))
rig.speech_finished.connect(_on_rig_speech_finished.bind(rig))
rig.state_changed.connect(_on_rig_state_changed.bind(rig))
rig.blocking_prop_sweep_hit.connect(_on_blocking_prop_sweep_hit)
rig.prop_interaction_event.connect(_on_rig_prop_interaction_event.bind(rig))
rig.use_completed.connect(_on_rig_use_completed.bind(rig))
rig.use_failed.connect(_on_rig_use_failed.bind(rig))
_director_visuals.mark_dirty()
if node is PropBlock:
var prop := node as PropBlock
if _sandbox_theme != null:
prop.set_wobble_style(
_sandbox_theme.number("effects.impact_wobble.duration", PropBlock.DEFAULT_WOBBLE_DURATION, 0.05, 2.0),
_sandbox_theme.number("effects.impact_wobble.maximum_angle_degrees", PropBlock.DEFAULT_WOBBLE_ANGLE_DEGREES, 0.0, 30.0),
_sandbox_theme.number("effects.impact_wobble.sideways_offset", PropBlock.DEFAULT_WOBBLE_OFFSET_LOCAL, 0.0, 24.0)
)
prop.collided.connect(_on_prop_collided.bind(prop))
if not prop.physics_profile_changed.is_connected(_on_prop_physics_profile_changed):
prop.physics_profile_changed.connect(_on_prop_physics_profile_changed)
@@ -2864,6 +2893,7 @@ func _build_prop_settings_view(ui: CanvasLayer) -> void:
_prop_settings_view.configure(_sandbox_theme as SandboxTheme, TOP_BAR_HEIGHT, STATUS_BAR_HEIGHT)
_prop_settings_view.role_choice_requested.connect(_on_prop_role_choice)
_prop_settings_view.role_toggle_requested.connect(_on_prop_role_toggle)
_prop_settings_view.wobble_toggled.connect(_on_prop_wobble_toggled)
_prop_settings_view.spot_add_requested.connect(_on_spot_add_requested)
_prop_settings_view.spot_move_requested.connect(_on_spot_move_requested)
_prop_settings_view.spot_rename_requested.connect(_on_spot_rename_requested)
@@ -3002,6 +3032,29 @@ func _on_prop_role_toggle(enabled: bool, key: String) -> void:
_refresh_prop_settings_card()
func _on_prop_wobble_toggled(enabled: bool) -> void:
var prop: PropBlock = _selection.get_primary() as PropBlock if _selection != null else null
if prop == null or not is_instance_valid(prop) or current_mode != StageMode.EDIT:
return
prop.set_wobble_on_impact(enabled)
if enabled:
_cancel_edit_wobble_preview()
prop.preview_wobble()
_edit_wobble_preview_prop = prop
else:
_edit_wobble_preview_prop = null
_save_object_state(prop)
_mark_scene_dirty()
_refresh_prop_settings_card()
func _cancel_edit_wobble_preview() -> void:
var prop: PropBlock = _edit_wobble_preview_prop
_edit_wobble_preview_prop = null
if prop != null and is_instance_valid(prop):
prop.cancel_wobble()
func _on_spot_add_requested() -> void:
var prop: PropBlock = _selection.get_primary() as PropBlock if _selection != null else null
if current_mode == StageMode.EDIT and prop != null and is_instance_valid(prop):
@@ -3846,6 +3899,7 @@ func _on_grid_size_changed(value: float) -> void:
func _on_selection_changed(nodes: Array[Node2D]) -> void:
_cancel_edit_wobble_preview()
if _spot_edit_active and (_spot_edit_prop == null or not is_instance_valid(_spot_edit_prop) or not nodes.has(_spot_edit_prop)):
_cancel_spot_work()
_gizmos.set_targets(nodes)
@@ -4654,7 +4708,10 @@ func _on_prop_collided(other: Node, normal: Vector2, position: Vector2, prop: Pr
# authoritative snap_to_standing() cleanup has run.
if current_mode != StageMode.PLAY or _run_state != RunState.RUNNING or not _event_dispatch_ready:
return
if other == null or not is_instance_valid(other) or _rule_engine == null:
if other == null or not is_instance_valid(other):
return
_route_impact_contact(prop, other, normal, position)
if _rule_engine == null:
return
if not _rule_engine.register_contact(prop, other):
return
@@ -4679,6 +4736,8 @@ func _on_prop_collided(other: Node, normal: Vector2, position: Vector2, prop: Pr
func _on_prop_separated(other: Node, prop: PropBlock) -> void:
if _impact_resolver != null and other != null and is_instance_valid(other):
_impact_resolver.end_contact(prop, other)
if current_mode != StageMode.PLAY or _run_state != RunState.RUNNING or not _event_dispatch_ready:
return
if other == null or not is_instance_valid(other) or _rule_engine == null:
@@ -4692,12 +4751,157 @@ func _on_prop_separated(other: Node, prop: PropBlock) -> void:
_rule_engine.unregister_rig_body_contact(rig, prop, other)
func _route_impact_contact(prop: PropBlock, other: Node, normal: Vector2, position: Vector2) -> void:
if _impact_resolver == null or prop == null or other == null:
return
var receiver_velocity: Vector2 = prop.get_impact_velocity()
var other_velocity: Vector2 = _impact_other_velocity(other)
var sample: Dictionary = {
"receiver": prop,
"other": other,
"normal": normal,
"position": position,
"receiver_velocity": receiver_velocity,
"other_velocity": other_velocity,
"source": &"body_contact",
}
if other.has_meta("stickman_rig"):
sample["rig"] = other.get_meta("stickman_rig")
if _impact_resolver.begin_contact(sample, Engine.get_physics_frames()):
prop.play_impact_wobble()
if other is PropBlock:
(other as PropBlock).play_impact_wobble()
func _on_blocking_prop_sweep_hit(
rig: StickmanRig,
prop: PropBlock,
normal: Vector2,
position: Vector2,
requested_velocity: Vector2
) -> void:
if current_mode != StageMode.PLAY or _run_state != RunState.RUNNING or not _event_dispatch_ready:
return
if rig == null or prop == null or not is_instance_valid(rig) or not is_instance_valid(prop):
return
var sample: Dictionary = {
"receiver": prop,
"other": rig,
"rig": rig,
"normal": normal,
"position": position,
"receiver_velocity": prop.get_impact_velocity(),
"other_velocity": requested_velocity,
"source": &"blocking_sweep",
}
if _impact_resolver != null and _impact_resolver.note_sweep(sample, Engine.get_physics_frames()):
prop.play_impact_wobble()
func _impact_other_velocity(other: Node) -> Vector2:
if other is PropBlock:
return (other as PropBlock).get_impact_velocity()
var rig: StickmanRig = other.get_meta("stickman_rig") as StickmanRig if other.has_meta("stickman_rig") else null
if other.has_meta("stickman_ragdoll_body") and bool(other.get_meta("stickman_ragdoll_body")):
return rig.ragdoll_body_velocity(other as RigidBody2D) if rig != null and other is RigidBody2D else Vector2.ZERO
if rig != null and other is PhysicsBody2D:
return rig.collision_proxy_velocity(other as PhysicsBody2D)
if other is RigidBody2D and not (other as RigidBody2D).freeze:
var velocity: Vector2 = (other as RigidBody2D).linear_velocity
return velocity if velocity.is_finite() else Vector2.ZERO
return Vector2.ZERO
func _seed_impact_contact_baseline() -> void:
if _impact_resolver == null or _world == null:
return
var tick: int = Engine.get_physics_frames()
for node: Node in _world.get_children():
if not node is PropBlock or not is_instance_valid(node):
continue
var prop: PropBlock = node as PropBlock
var direct_state: PhysicsDirectBodyState2D = PhysicsServer2D.body_get_direct_state(prop.get_rid())
if direct_state == null:
continue
for contact_index: int in direct_state.get_contact_count():
var collider_id: int = direct_state.get_contact_collider_id(contact_index)
var collider_object: Object = instance_from_id(collider_id)
if not collider_object is Node:
continue
var other: Node = collider_object as Node
var contact_normal: Vector2 = direct_state.get_contact_local_normal(contact_index)
if contact_normal.is_zero_approx():
contact_normal = other.global_position - prop.global_position if other is Node2D else Vector2.RIGHT
var sample: Dictionary = {
"receiver": prop,
"other": other,
"normal": contact_normal,
"position": direct_state.get_contact_local_position(contact_index),
"receiver_velocity": Vector2.ZERO,
"other_velocity": Vector2.ZERO,
"source": &"baseline",
}
if other.has_meta("stickman_rig"):
sample["rig"] = other.get_meta("stickman_rig")
_impact_resolver.begin_contact(sample, tick)
func _on_rig_state_changed(new_state: int, rig: StickmanRig) -> void:
if _impact_resolver != null:
_impact_resolver.end_rig_contacts(rig)
if new_state == StickmanRig.RigState.RAGDOLL:
_connect_ragdoll_impact_contacts(rig)
var rig_ids: Array[int] = [rig.get_instance_id()]
_purge_collision_pairs_for_ids(rig_ids)
func _connect_ragdoll_impact_contacts(rig: StickmanRig) -> void:
if rig == null or not is_instance_valid(rig):
return
for body: RigidBody2D in rig.get_ragdoll_bodies():
if body == null or not is_instance_valid(body):
continue
body.contact_monitor = true
body.max_contacts_reported = maxi(body.max_contacts_reported, 8)
var entered_callback: Callable = Callable(self, "_on_ragdoll_body_entered").bind(rig, body)
if not body.body_entered.is_connected(entered_callback):
body.body_entered.connect(entered_callback)
var exited_callback: Callable = Callable(self, "_on_ragdoll_body_exited").bind(rig, body)
if not body.body_exited.is_connected(exited_callback):
body.body_exited.connect(exited_callback)
func _on_ragdoll_body_entered(other: Node, rig: StickmanRig, body: RigidBody2D) -> void:
if current_mode != StageMode.PLAY or _run_state != RunState.RUNNING or not _event_dispatch_ready:
return
if rig == null or body == null or other == null:
return
if not is_instance_valid(rig) or not is_instance_valid(body) or not is_instance_valid(other):
return
if not other is PropBlock:
return
var prop: PropBlock = other as PropBlock
var normal: Vector2 = body.global_position - prop.global_position
if normal.is_zero_approx():
normal = Vector2.UP
var position: Vector2 = (body.global_position + prop.global_position) * 0.5
var direct_state: PhysicsDirectBodyState2D = PhysicsServer2D.body_get_direct_state(body.get_rid())
if direct_state != null:
for contact_index: int in direct_state.get_contact_count():
if direct_state.get_contact_collider_id(contact_index) != prop.get_instance_id():
continue
position = body.to_global(direct_state.get_contact_local_position(contact_index))
break
_route_impact_contact(prop, body, normal.normalized(), position)
func _on_ragdoll_body_exited(other: Node, _rig: StickmanRig, body: RigidBody2D) -> void:
if _impact_resolver == null or other == null or not is_instance_valid(other):
return
if other is PropBlock:
_impact_resolver.end_contact(other as PropBlock, body)
func _purge_collision_pairs_for_ids(ids: Array[int]) -> void:
if _rule_engine != null:
_rule_engine.purge_collision_pairs_for_ids(ids)
@@ -10,6 +10,7 @@ const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
signal role_choice_requested(key: String, value: String)
signal role_toggle_requested(enabled: bool, key: String)
signal wobble_toggled(enabled: bool)
signal spot_add_requested
signal spot_move_requested(spot_id: String)
signal spot_rename_requested(spot_id: String, name: String)
@@ -26,6 +27,7 @@ var _fields: VBoxContainer
var _notice: Label
var _depth_group: StagePanelChoiceGroup
var _motion_group: StagePanelChoiceGroup
var _wobble_row: StagePanelToggleRow
var _controls: Dictionary = {}
var _spot_rows: VBoxContainer
var _spot_empty_label: Label
@@ -193,6 +195,7 @@ func _build() -> void:
_controls["blocks_stickmen"] = _make_toggle_row(_fields, "blocks_stickmen", "Blocks stickmen")
_controls["can_pick_up"] = _make_toggle_row(_fields, "can_pick_up", "Stickmen can pick it up")
_add_release_options()
_add_wobble_section()
_add_use_spots_section()
_reopen_button = Button.new()
@@ -239,6 +242,19 @@ func _add_use_spots_section() -> void:
section.add_child(_spot_rows)
func _add_wobble_section() -> void:
_add_heading(_fields, "When bumped")
_wobble_row = STAGE_PANEL_TOGGLE_ROW.new() as StagePanelToggleRow
_wobble_row.name = "WobbleWhenBumped"
_wobble_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_wobble_row.configure("Wobble when bumped")
_wobble_row.set_help_text("A hard bump makes this prop wobble once in PLAY.")
_wobble_row.apply_theme(_stage_theme)
_fields.add_child(_wobble_row)
_wobble_row.preferred_height_changed.connect(_on_row_height_changed)
_wobble_row.toggled.connect(_on_wobble_toggled)
func _sync_spots(spots: Array[Dictionary], selected_spot_id: String) -> void:
if _spot_rows == null:
return
@@ -390,6 +406,9 @@ func _sync_roles(prop: PropBlock, roles: Dictionary) -> void:
else:
_depth_group.select_no_signal(StringName(String(roles.get("depth", PROP_STAGE_ROLES.DEPTH_BEHIND))))
_motion_group.select_no_signal(StringName(String(roles.get("start_motion", PROP_STAGE_ROLES.START_STILL))))
if _wobble_row != null:
_wobble_row.set_checked_no_signal(prop.wobble_on_impact)
_wobble_row.set_help_text("A hard bump makes this prop wobble once in PLAY.")
for key: String in ["holds_other_props", "blocks_stickmen", "can_pick_up", "released_blocks_stickmen", "released_bumps_other_props"]:
var control := _controls.get(key) as StagePanelToggleRow
if control == null:
@@ -453,6 +472,10 @@ func _on_role_toggled(enabled: bool, key: String) -> void:
role_toggle_requested.emit(enabled, key)
func _on_wobble_toggled(enabled: bool) -> void:
wobble_toggled.emit(enabled)
func _on_row_height_changed() -> void:
if _card != null:
_card.request_fit()
+65 -4
View File
@@ -10,6 +10,8 @@ const STATE_RUNNING: int = 1
const STATE_PAUSED: int = 2
## Set true temporarily when diagnosing authored/play prop scale transfer.
const PROP_SCALE_TRACE_ENABLED: bool = false
## Temporary, low-volume diagnostics for props that should block stickmen.
const ACTOR_PROP_DEBUG_ENABLED: bool = true
var run_state: int = STATE_STOPPED
var elapsed_seconds: float = 0.0
@@ -44,14 +46,18 @@ func start_run(
for node: Node2D in world_nodes:
if node is PropBlock:
var prop: PropBlock = node as PropBlock
prop.cancel_wobble()
trace_prop_scale(prop, "before_play_scale_transfer", stage_mode_name, "RUNNING", camera_zoom)
prop.begin_play_instance_scale()
trace_prop_scale(prop, "after_play_scale_transfer", stage_mode_name, "RUNNING", camera_zoom)
var start_role: PropBlock.RuntimeRole = PropBlock.RuntimeRole.START_STILL if String(prop.stage_roles.get("start_motion", PROP_STAGE_ROLES.START_STILL)) == PROP_STAGE_ROLES.START_STILL else PropBlock.RuntimeRole.START_PHYSICS
prop.apply_runtime_role(start_role)
if ACTOR_PROP_DEBUG_ENABLED:
_debug_actor_blocking_prop(prop)
trace_prop_scale(prop, "after_play_role_applied", stage_mode_name, "RUNNING", camera_zoom)
elif node is STICKMAN_RIG_SCRIPT:
var rig: StickmanRig = node as StickmanRig
rig.reset_collision_proxy_velocity_baselines()
rig.reset_simulation_pause()
rig.stop_queue()
rig.clear_reactive_actions()
@@ -60,6 +66,44 @@ func start_run(
rig.start_queue()
elif node is RigidBody2D:
(node as RigidBody2D).freeze = false
if node is PropBlock:
(node as PropBlock).reset_impact_velocity_baseline()
func _debug_actor_blocking_prop(prop: PropBlock) -> void:
if prop == null or not is_instance_valid(prop):
return
var role_index: int = clampi(int(prop.runtime_role), 0, PropBlock.RuntimeRole.keys().size() - 1)
var collision_nodes := PackedStringArray()
for child: Node in prop.get_children():
if child is CollisionPolygon2D:
var polygon: CollisionPolygon2D = child as CollisionPolygon2D
collision_nodes.append(
"CollisionPolygon2D(disabled=%s,points=%d,global=%s)"
% [str(polygon.disabled), polygon.polygon.size(), str(polygon.global_transform)]
)
elif child is CollisionShape2D:
var shape: CollisionShape2D = child as CollisionShape2D
collision_nodes.append(
"CollisionShape2D(disabled=%s,type=%s,global=%s)"
% [str(shape.disabled), shape.shape.get_class() if shape.shape != null else "null", str(shape.global_transform)]
)
print(
"[PropCollisionDebug][PlayProp] id=%s asset=%s role=%s blocking=%s roles=%s layer=%d mask=%d freeze=%s pos=%s scale=%s collision=%s"
% [
prop.stage_object_id,
prop.spawn_id,
PropBlock.RuntimeRole.keys()[role_index],
str(prop.is_actor_blocking()),
str(prop.stage_roles),
prop.collision_layer,
prop.collision_mask,
str(prop.freeze),
str(prop.global_position),
str(prop.global_scale),
"; ".join(collision_nodes),
]
)
func pause_run(world_nodes: Array[Node2D]) -> bool:
@@ -69,7 +113,11 @@ func pause_run(world_nodes: Array[Node2D]) -> bool:
run_state = STATE_PAUSED
resume_baseline_frames = 0
for node: Node2D in world_nodes:
if node is STICKMAN_RIG_SCRIPT:
if node is PropBlock:
var prop: PropBlock = node as PropBlock
prop.set_wobble_paused(true)
capture_prop_pause_state(prop)
elif node is STICKMAN_RIG_SCRIPT:
(node as StickmanRig).pause_simulation()
elif node is RigidBody2D:
capture_prop_pause_state(node as RigidBody2D)
@@ -91,8 +139,13 @@ func resume_run(world_nodes: Array[Node2D], rule_engine: StageRuleEngine) -> boo
body.sleeping = bool(snapshot.get("sleeping", false))
prop_pause_states.clear()
for node: Node2D in world_nodes:
if node is STICKMAN_RIG_SCRIPT:
(node as StickmanRig).resume_simulation()
if node is PropBlock:
(node as PropBlock).set_wobble_paused(false)
(node as PropBlock).reset_impact_velocity_baseline()
elif node is STICKMAN_RIG_SCRIPT:
var rig: StickmanRig = node as StickmanRig
rig.reset_collision_proxy_velocity_baselines()
rig.resume_simulation()
run_state = STATE_RUNNING
event_dispatch_ready = false
resume_baseline_frames = 2
@@ -127,6 +180,7 @@ func reset_world(
var body: RigidBody2D = node as RigidBody2D
if node is PropBlock:
var prop: PropBlock = node as PropBlock
prop.cancel_wobble()
var authored: Dictionary = authored_state.get(prop.get_instance_id(), {})
trace_prop_scale(prop, "before_stop_scale_restore", stage_mode_name, "STOPPED", camera_zoom)
prop.set_stage_roles(authored.get("roles", prop.stage_roles))
@@ -185,7 +239,9 @@ func save_object_state(node: Node2D) -> void:
if not node is STICKMAN_RIG_SCRIPT:
entry["scale"] = node.scale
if node is PropBlock:
entry["roles"] = (node as PropBlock).stage_roles.duplicate(true)
var prop: PropBlock = node as PropBlock
entry["roles"] = prop.stage_roles.duplicate(true)
entry["wobble_on_impact"] = prop.wobble_on_impact
authored_state[node.get_instance_id()] = entry
@@ -210,13 +266,18 @@ func restore_authored_state(stage_mode_name: String, run_state_name: String, cam
trace_prop_scale(node as PropBlock, "after_authored_scale_write", stage_mode_name, run_state_name, camera_zoom)
if node is STICKMAN_RIG_SCRIPT and entry.has("facing_profile"):
(node as STICKMAN_RIG_SCRIPT).set_facing_profile(int(entry["facing_profile"]))
if node is STICKMAN_RIG_SCRIPT:
(node as StickmanRig).reset_collision_proxy_velocity_baselines()
if node is RigidBody2D:
var body: RigidBody2D = node as RigidBody2D
body.linear_velocity = Vector2.ZERO
body.angular_velocity = 0.0
if node is PropBlock:
var prop: PropBlock = node as PropBlock
prop.cancel_wobble()
prop.set_stage_roles(entry.get("roles", prop.stage_roles))
prop.set_wobble_on_impact(bool(entry.get("wobble_on_impact", prop.wobble_on_impact)))
prop.reset_impact_velocity_baseline()
prop.apply_runtime_role(PropBlock.RuntimeRole.EDIT)
@@ -60,6 +60,7 @@ func collect(
entry["roles"] = prop.stage_roles.duplicate(true)
entry["use_spots"] = PROP_USE_SPOT_DATA.encode(prop.get_use_spots())
entry["next_use_spot_number"] = prop.next_use_spot_number
entry["wobble_on_impact"] = prop.wobble_on_impact
var prop_scale: Vector2 = authored_state.get("scale", prop.scale)
entry["scale"] = STAGE_SCENE_IO.vector_to_data(prop_scale)
entry["material_preset"] = prop.material_preset
@@ -175,6 +176,7 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
if node is PropBlock:
var prop: PropBlock = node as PropBlock
prop.stage_object_id = String(entry.get("stage_object_id", ""))
prop.set_wobble_on_impact(bool(entry.get("wobble_on_impact", false)))
var decoded_spots: Array[Dictionary] = PROP_USE_SPOT_DATA.decode(entry.get("use_spots", []) as Array)
prop.set_use_spots(decoded_spots, int(entry.get("next_use_spot_number", 1)))
var default_roles: Dictionary = PROP_STAGE_ROLES.new_placement(prop.has_usable_grip())
@@ -212,6 +214,7 @@ func apply(data: Dictionary, world: Node2D, spawner: StageSpawner) -> Dictionary
(node as StickmanRig).queue_action(action)
else:
warnings.append("%s: invalid action queue. Open this character's queue and review its actions." % file_id)
(node as StickmanRig).reset_collision_proxy_velocity_baselines()
loaded_nodes.append(node)
file_to_runtime[file_id] = node.get_instance_id()
var decoded_rules: Dictionary = STAGE_SCENE_IO.decode_rules(data.get("event_rules", []) as Array, file_to_runtime, unavailable)