Add prop interaction system and throw mechanics specification

- Introduced `stickman_prop_interactor.gd` to manage prop interactions.
- Created `phase_11_prop_interactions.md` detailing the design and implementation of prop pickup, drop, and throw mechanics.
- Defined interaction states, action lifecycles, and event triggers for prop handling.
- Specified constraints for throw actions, including target positioning and velocity calculations.
- Established acceptance criteria for prop interactions and lifecycle management.
This commit is contained in:
2026-09-25 23:42:26 -04:00
parent 730a9a966b
commit 6a1359f244
22 changed files with 3433 additions and 34 deletions
+42 -1
View File
@@ -179,6 +179,7 @@ signal action_finished(action: Dictionary, index: int)
signal queue_finished # queue ran to completion (not on stop)
signal queue_changed # any queue mutation
signal speech_finished # bubble auto-hid after speak()
signal prop_interaction_event(event_type: String, prop: PropBlock, reason: String)
# ---------------------------------------------------------------------------
# Internal state
@@ -225,6 +226,7 @@ var source_stk_path: String = ""
var _locomotion_component: StickmanLocomotion = null
var _ragdoll_component: StickmanRagdoll = null
var _action_runner_component: StickmanActionRunner = null
var _prop_interactor_component: Node = null
var _simulation_paused: bool = false
var _phase12_paused_animation: bool = false
var _phase12_paused_animation_name: StringName = &""
@@ -238,12 +240,15 @@ func _ready() -> void:
_locomotion_component = get_node_or_null(NodePath("RigComponents/Locomotion")) as StickmanLocomotion
_ragdoll_component = get_node_or_null(NodePath("RigComponents/Ragdoll")) as StickmanRagdoll
_action_runner_component = get_node_or_null(NodePath("RigComponents/ActionRunner")) as StickmanActionRunner
_prop_interactor_component = get_node_or_null(NodePath("RigComponents/PropInteractor"))
if _locomotion_component == null:
push_warning("StickmanRig: missing RigComponents/Locomotion; locomotion boundary unavailable.")
if _ragdoll_component == null:
push_warning("StickmanRig: missing RigComponents/Ragdoll; ragdoll boundary unavailable.")
if _action_runner_component == null:
push_warning("StickmanRig: missing RigComponents/ActionRunner; action boundary unavailable.")
if _prop_interactor_component == null:
push_warning("StickmanRig: missing RigComponents/PropInteractor; prop interactions unavailable.")
if _locomotion_component != null:
_locomotion_component.configure(self)
_locomotion_component.arrived.connect(func(target: Vector2) -> void: arrived.emit(target))
@@ -251,8 +256,11 @@ func _ready() -> void:
_ragdoll_component.configure(self, _locomotion_component)
_ragdoll_component.recovery_requested.connect(func() -> void: request_recovery())
_ragdoll_component.state_transition_requested.connect(_set_state)
if _prop_interactor_component != null:
_prop_interactor_component.call("configure", self, _locomotion_component, Callable())
_prop_interactor_component.connect("prop_event", _on_prop_interaction_event)
if _action_runner_component != null:
_action_runner_component.configure(self, _locomotion_component)
_action_runner_component.configure(self, _locomotion_component, _prop_interactor_component)
_action_runner_component.action_queue = _pending_action_queue
_action_runner_component.action_started.connect(func(action: Dictionary, index: int) -> void: action_started.emit(action, index))
_action_runner_component.action_finished.connect(func(action: Dictionary, index: int) -> void: action_finished.emit(action, index))
@@ -315,6 +323,8 @@ func _physics_process(delta: float) -> void:
_ragdoll_component.tick_pre_movement(delta, int(state), rest_timeout, auto_recover)
if _locomotion_component != null:
_locomotion_component.tick_movement(delta, int(state))
if _prop_interactor_component != null:
_prop_interactor_component.call("tick", delta)
if _action_runner_component != null:
_action_runner_component.tick(delta, int(state), is_ragdoll_at_rest())
_pin_mirrored_head_rotation()
@@ -457,6 +467,8 @@ func _set_state(next_state: int) -> void:
func set_ragdoll(enabled: bool) -> void:
if enabled:
if _prop_interactor_component != null:
_prop_interactor_component.call("disarm_for_ragdoll")
if _locomotion_component != null:
_locomotion_component.reset_for_ragdoll_handoff()
match state:
@@ -480,6 +492,35 @@ func toggle_ragdoll() -> void:
set_ragdoll(not is_in_ragdoll())
func set_prop_resolver(resolver: Callable) -> void:
if _prop_interactor_component != null:
_prop_interactor_component.call("configure", self, _locomotion_component, resolver)
func start_prop_action(action: Dictionary) -> bool:
return bool(_prop_interactor_component.call("start_action", action)) if _prop_interactor_component != null else false
func cancel_prop_action() -> void:
if _prop_interactor_component != null:
_prop_interactor_component.call("cancel_action")
func preview_prop_throw(target: Vector2, queue_prefix: Array[Dictionary] = []) -> Dictionary:
if _prop_interactor_component == null:
return {}
return _prop_interactor_component.call("preview_throw", target, queue_prefix)
func reset_prop_interactions_for_scene_exit() -> void:
if _prop_interactor_component != null:
_prop_interactor_component.call("reset_for_scene_exit")
func _on_prop_interaction_event(event_type: String, prop: PropBlock, reason: String) -> void:
prop_interaction_event.emit(event_type, prop, reason)
## Public stand-up request. No-op unless the rig is in RAGDOLL.
func request_recovery() -> void:
if state == RigState.RAGDOLL and _ragdoll_component != null: