- Introduced a new action type `move_to` allowing children to direct a stickman to a destination using either Walk or Run styles. - Updated the ActionEditor UI to include Walk/Run buttons for selecting movement styles. - Ensured compatibility with existing `walk_to` actions by converting them to the new `move_to` format during loading. - Defined clear success criteria for the implementation, including distinct movement animations and behavior for both Walk and Run. - Established a comprehensive verification contract to ensure functionality across various terrains and scenarios.
1108 lines
46 KiB
GDScript
1108 lines
46 KiB
GDScript
class_name StickmanPropInteractor
|
|
extends Node
|
|
## Owns one-stickman/one-hand prop interaction state during Sandbox PLAY.
|
|
|
|
const ANCHOR_DATA := preload("res://scripts/props/prop_anchor_data.gd")
|
|
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
|
|
|
enum Hand { LEFT, RIGHT }
|
|
enum Phase { IDLE, APPROACHING, REACHING, PICKUP_RECOVERING, HELD, THROWING }
|
|
|
|
const MAX_REACH: float = 390.0
|
|
const APPROACH_GAP: float = 90.0
|
|
const APPROACH_REPLAN_LIMIT: int = 1
|
|
const REACH_DURATION: float = 0.34
|
|
const PICKUP_RECOVER_DURATION: float = 0.32
|
|
const LOW_PICKUP_THRESHOLD: float = 110.0
|
|
const LOW_PICKUP_MAX_DROP: float = 110.0
|
|
const LOW_PICKUP_LEAN: float = 28.0
|
|
const THROW_LIFT_DURATION: float = 0.30
|
|
const THROW_SWING_DURATION: float = 0.16
|
|
const THROW_FOLLOW_THROUGH: float = 0.24
|
|
const THROW_PREVIEW_HORIZON: float = 2.0
|
|
const MIN_THROW_FLIGHT_TIME: float = 0.35
|
|
const MAX_THROW_RANGE: float = 3600.0
|
|
const MAX_THROW_SPEED: float = 2100.0
|
|
const ZERO_THROW_DISTANCE: float = 4.0
|
|
const THROW_DESCENT_EPSILON: float = 0.001
|
|
const THROW_SPIN_SPEED: float = 2.4
|
|
const THROW_TRAJECTORY_SEGMENTS: int = 24
|
|
const THROW_HEAD_CLEARANCE: float = 36.0
|
|
const THROW_REACH_MARGIN: float = 8.0
|
|
|
|
signal action_completed
|
|
signal prop_event(event_type: String, prop: PropBlock, reason: String)
|
|
|
|
var _rig: Node2D
|
|
var _locomotion: Node
|
|
var _resolver: Callable
|
|
var _phase: Phase = Phase.IDLE
|
|
var _held_prop: PropBlock
|
|
var _held_anchor: Dictionary = {}
|
|
var _hand: Hand = Hand.RIGHT
|
|
var _active_action: Dictionary = {}
|
|
var _reach_start: Vector2 = Vector2.ZERO
|
|
var _reach_target: Vector2 = Vector2.ZERO
|
|
var _reach_elapsed: float = 0.0
|
|
var _pickup_recover_elapsed: float = 0.0
|
|
var _pickup_torso_start: Vector2 = Vector2.ZERO
|
|
var _pickup_torso_start_rotation: float = 0.0
|
|
var _pickup_torso_crouched: Vector2 = Vector2.ZERO
|
|
var _pickup_torso_crouched_rotation: float = 0.0
|
|
var _pickup_uses_crouch: bool = false
|
|
var _throw_elapsed: float = 0.0
|
|
var _throw_released: bool = false
|
|
var _throw_base_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_windup_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_overhead_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_swing_control_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_release_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_follow_control_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_follow_marker_local: Vector2 = Vector2.ZERO
|
|
var _throw_release_attachment_local: Transform2D = Transform2D.IDENTITY
|
|
var _throw_facing_left: bool = false
|
|
var _throw_effective_target: Vector2 = Vector2.ZERO
|
|
var _throw_torso_start: Vector2 = Vector2.ZERO
|
|
var _throw_torso_windup: Vector2 = Vector2.ZERO
|
|
var _throw_torso_start_rotation: float = 0.0
|
|
var _throw_torso_windup_rotation: float = 0.0
|
|
var _approach_replans: int = 0
|
|
var _marker_start_position: Vector2 = Vector2.ZERO
|
|
var _saved_body_state: Dictionary = {}
|
|
var _last_transform: Transform2D = Transform2D.IDENTITY
|
|
var _sampled_linear_velocity: Vector2 = Vector2.ZERO
|
|
var _sampled_angular_velocity: float = 0.0
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
if _held_prop != null and is_instance_valid(_held_prop):
|
|
_release_held("scene_reset", false)
|
|
else:
|
|
_clear_reservation()
|
|
|
|
|
|
func configure(rig: Node2D, locomotion: Node, resolver: Callable) -> void:
|
|
_rig = rig
|
|
_locomotion = locomotion
|
|
_resolver = resolver
|
|
|
|
|
|
## Returns true when the queue runner must wait for action_completed.
|
|
## Immediate success and safe no-op failures both return false.
|
|
func start_action(action: Dictionary) -> bool:
|
|
var type := String(action.get("type", ""))
|
|
match type:
|
|
"pick_up_prop":
|
|
return _begin_pickup(String(action.get("target_prop_id", "")), String(action.get("hand", "auto")), action)
|
|
"drop_prop":
|
|
if _held_prop != null and is_instance_valid(_held_prop):
|
|
_release_held("manual", false)
|
|
return false
|
|
"throw_prop":
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
return false
|
|
var target: Variant = action.get("target_position", Vector2.INF)
|
|
if not target is Vector2 or not (target as Vector2).is_finite():
|
|
push_warning("StickmanPropInteractor: invalid throw target; skipped.")
|
|
return false
|
|
var raw_target := target as Vector2
|
|
if _rig == null or raw_target.distance_to(_rig.global_position) <= ZERO_THROW_DISTANCE:
|
|
return false
|
|
var effective_target := _clamp_throw_target(raw_target)
|
|
if absf(effective_target.x - _rig.global_position.x) > 0.5:
|
|
_rig.call("set_facing_profile", 0 if effective_target.x < _rig.global_position.x else 1)
|
|
var marker := _hand_marker()
|
|
if marker == null:
|
|
return false
|
|
var pose := _build_throw_pose(effective_target, marker, _held_prop, _hand, Vector2.ZERO)
|
|
if pose.is_empty():
|
|
return false
|
|
var throw_facing_left := bool(pose["facing_left"])
|
|
var pose_transform := _rig_transform_for_facing(throw_facing_left)
|
|
var release_marker_world: Vector2 = pose_transform * (pose["release"] as Vector2)
|
|
var release_attachment := _predicted_attachment_transform(release_marker_world, throw_facing_left, _held_prop, _held_anchor, _hand)
|
|
var release_attachment_local: Transform2D = pose_transform.affine_inverse() * release_attachment
|
|
var spin_sign := -1.0 if throw_facing_left else 1.0
|
|
var predicted_release := pose_transform * release_attachment_local
|
|
if _solve_throw(predicted_release, effective_target, _held_prop, spin_sign * THROW_SPIN_SPEED).is_empty():
|
|
return false
|
|
_store_throw_pose(pose, marker)
|
|
_throw_facing_left = throw_facing_left
|
|
_throw_release_attachment_local = release_attachment_local
|
|
_throw_effective_target = effective_target
|
|
_active_action = action.duplicate(true)
|
|
_throw_elapsed = 0.0
|
|
_throw_released = false
|
|
_phase = Phase.THROWING
|
|
return true
|
|
return false
|
|
|
|
|
|
func tick(delta: float) -> void:
|
|
if _rig == null or not is_instance_valid(_rig):
|
|
return
|
|
match _phase:
|
|
Phase.APPROACHING:
|
|
if _locomotion != null and bool(_locomotion.call("consume_walk_completed")):
|
|
_on_approach_arrived()
|
|
Phase.REACHING:
|
|
_update_reach(delta)
|
|
Phase.PICKUP_RECOVERING:
|
|
_update_pickup_recovery(delta)
|
|
Phase.HELD:
|
|
_update_held_transform(delta)
|
|
Phase.THROWING:
|
|
_update_throw(delta)
|
|
|
|
|
|
func cancel_action() -> void:
|
|
if _phase == Phase.APPROACHING and _locomotion != null:
|
|
_locomotion.call("cancel_walk")
|
|
if _phase == Phase.APPROACHING or _phase == Phase.REACHING:
|
|
_clear_reservation()
|
|
_restore_hand_target()
|
|
_restore_pickup_torso()
|
|
elif _phase == Phase.PICKUP_RECOVERING:
|
|
_restore_pickup_torso()
|
|
_phase = Phase.HELD
|
|
_active_action.clear()
|
|
return
|
|
elif _phase == Phase.THROWING and not _throw_released:
|
|
var marker := _hand_marker()
|
|
if marker != null:
|
|
marker.position = _throw_base_marker_local
|
|
_restore_throw_torso()
|
|
_phase = Phase.HELD if _held_prop != null else Phase.IDLE
|
|
_active_action.clear()
|
|
return
|
|
elif _phase == Phase.THROWING and _throw_released:
|
|
_throw_released = false
|
|
_throw_elapsed = 0.0
|
|
_restore_hand_target()
|
|
_restore_throw_torso()
|
|
if _phase != Phase.HELD:
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
|
|
|
|
func preview_throw(target: Vector2, queue_prefix: Array[Dictionary] = []) -> Dictionary:
|
|
if not target.is_finite():
|
|
return {}
|
|
var context := _build_throw_preview_context(queue_prefix)
|
|
if not bool(context.get("available", false)):
|
|
return {
|
|
"preview_available": false,
|
|
"preview_reason": String(context.get("reason", "No prop is held or planned before this throw.")),
|
|
}
|
|
var prop: PropBlock = context["prop"]
|
|
var anchor: Dictionary = context["anchor"]
|
|
var hand_value := int(context["hand"])
|
|
var root_offset: Vector2 = context["root_offset"]
|
|
var effective_target := _clamp_throw_target(target, context["root_position"])
|
|
if _rig == null or target.distance_to(context["root_position"]) <= ZERO_THROW_DISTANCE:
|
|
var invalid := _invalid_throw_preview(effective_target, _rig.global_position if _rig != null else target)
|
|
invalid.merge(_throw_preview_metadata(context), true)
|
|
return invalid
|
|
var marker := _hand_marker_for(hand_value)
|
|
if marker == null:
|
|
return {"preview_available": false, "preview_reason": "The planned pickup hand is unavailable."}
|
|
var pose := _build_throw_pose(effective_target, marker, prop, hand_value, root_offset)
|
|
if pose.is_empty():
|
|
return {"preview_available": false, "preview_reason": "The throw pose cannot be estimated from the current rig."}
|
|
var facing_left := bool(pose["facing_left"])
|
|
var release_marker_world := _rig_transform_for_facing(facing_left, root_offset) * (pose["release"] as Vector2)
|
|
var release_transform := _predicted_attachment_transform(release_marker_world, facing_left, prop, anchor, hand_value, root_offset)
|
|
var spin_speed := -THROW_SPIN_SPEED if facing_left else THROW_SPIN_SPEED
|
|
var solution := _solve_throw(release_transform, effective_target, prop, spin_speed)
|
|
if solution.is_empty():
|
|
var invalid := _invalid_throw_preview(effective_target, release_transform.origin)
|
|
invalid.merge(_throw_preview_metadata(context), true)
|
|
return invalid
|
|
solution.merge(_throw_preview_metadata(context), true)
|
|
solution["effective_target"] = effective_target
|
|
solution["target_reachable"] = true
|
|
return _clip_preview_to_collision(solution, prop)
|
|
|
|
|
|
func _build_throw_preview_context(queue_prefix: Array[Dictionary]) -> Dictionary:
|
|
if _rig == null or not is_instance_valid(_rig):
|
|
return {"available": false, "reason": "The acting stickman is unavailable."}
|
|
var prop: PropBlock = _held_prop if _held_prop != null and is_instance_valid(_held_prop) else null
|
|
var anchor: Dictionary = _held_anchor.duplicate(true) if prop != null else {}
|
|
var hand_value := int(_hand)
|
|
var has_prop := prop != null
|
|
var root_position := _rig.global_position
|
|
var root_offset := Vector2.ZERO
|
|
var predicted_facing_left := _is_facing_left()
|
|
var planned := false
|
|
var uncertain := _phase != Phase.IDLE and _phase != Phase.HELD
|
|
var plan_step := 0
|
|
for entry: Dictionary in queue_prefix:
|
|
var action: Dictionary = entry.get("action", {})
|
|
if action.is_empty():
|
|
continue
|
|
if bool(entry.get("active", false)):
|
|
uncertain = true
|
|
var action_type := String(action.get("type", ""))
|
|
if ACTION_REGISTRY.is_move_action(action):
|
|
action_type = ACTION_REGISTRY.MOVE_TO
|
|
match action_type:
|
|
"move_to":
|
|
var feet_target: Variant = action.get("target", Vector2.INF)
|
|
if feet_target is Vector2 and (feet_target as Vector2).is_finite() and _locomotion != null:
|
|
var previous_root := root_position
|
|
var foot_offset: Vector2 = _locomotion.call("get_foot_offset")
|
|
root_position = (feet_target as Vector2) + foot_offset
|
|
root_offset = root_position - _rig.global_position
|
|
if not is_zero_approx(root_position.x - previous_root.x):
|
|
predicted_facing_left = root_position.x < previous_root.x
|
|
uncertain = true
|
|
"pick_up_prop":
|
|
if has_prop:
|
|
continue
|
|
var prop_id := String(action.get("target_prop_id", ""))
|
|
if prop_id.is_empty() or not _resolver.is_valid():
|
|
return {"available": false, "reason": "A pickup before this throw has no resolvable prop."}
|
|
var resolved: Variant = _resolver.call(prop_id)
|
|
if not resolved is PropBlock or not is_instance_valid(resolved) or (resolved as PropBlock).is_queued_for_deletion():
|
|
return {"available": false, "reason": "A pickup before this throw refers to a missing prop."}
|
|
var candidate_prop := resolved as PropBlock
|
|
var owner_id := int(candidate_prop.interaction_owner_id)
|
|
var reserved_by := int(candidate_prop.interaction_reserved_by)
|
|
if (owner_id != 0 and owner_id != int(_rig.get_instance_id())) or (reserved_by != 0 and reserved_by != int(_rig.get_instance_id())):
|
|
return {"available": false, "reason": "The planned pickup prop is held or reserved by another stickman."}
|
|
var requested_hand := String(action.get("hand", "auto"))
|
|
var picked := _choose_preview_grip_and_hand(candidate_prop, requested_hand, root_offset, predicted_facing_left)
|
|
if picked.is_empty():
|
|
return {"available": false, "reason": "The planned prop has no compatible grip for this pickup."}
|
|
var candidate_anchor: Dictionary = picked["anchor"]
|
|
var candidate_hand := int(picked["hand"])
|
|
var grip_position := _grip_world_position_for_facing(candidate_prop, candidate_anchor, predicted_facing_left)
|
|
var wrist_position := _preview_wrist_position(candidate_hand, root_offset, predicted_facing_left)
|
|
if wrist_position.distance_to(grip_position) > MAX_REACH:
|
|
uncertain = true
|
|
var side := -1.0 if root_position.x <= grip_position.x else 1.0
|
|
var approach_root := Vector2(grip_position.x + side * APPROACH_GAP, root_position.y)
|
|
if not is_equal_approx(approach_root.x, root_position.x):
|
|
predicted_facing_left = approach_root.x < root_position.x
|
|
root_position = approach_root
|
|
root_offset = root_position - _rig.global_position
|
|
grip_position = _grip_world_position_for_facing(candidate_prop, candidate_anchor, predicted_facing_left)
|
|
wrist_position = _preview_wrist_position(candidate_hand, root_offset, predicted_facing_left)
|
|
if wrist_position.distance_to(grip_position) > MAX_REACH:
|
|
uncertain = true
|
|
prop = candidate_prop
|
|
anchor = candidate_anchor.duplicate(true)
|
|
hand_value = candidate_hand
|
|
has_prop = true
|
|
planned = true
|
|
plan_step = int(entry.get("step", 0))
|
|
"drop_prop", "throw_prop":
|
|
if has_prop:
|
|
prop = null
|
|
anchor.clear()
|
|
has_prop = false
|
|
planned = false
|
|
uncertain = true
|
|
"ragdoll":
|
|
uncertain = true
|
|
if not has_prop or prop == null or not is_instance_valid(prop):
|
|
return {"available": false, "reason": "No prop is held or picked up earlier in this queue."}
|
|
return {
|
|
"available": true,
|
|
"prop": prop,
|
|
"anchor": anchor,
|
|
"hand": hand_value,
|
|
"root_position": root_position,
|
|
"root_offset": root_offset,
|
|
"planned": planned,
|
|
"uncertain": uncertain,
|
|
"plan_step": plan_step,
|
|
}
|
|
|
|
|
|
func _choose_preview_grip_and_hand(prop: PropBlock, requested_hand: String, root_offset: Vector2, facing_left: bool) -> Dictionary:
|
|
var anchors := prop.get_interaction_anchors()
|
|
var candidates: Array[Dictionary] = []
|
|
for hand_name: String in ["left", "right"]:
|
|
if requested_hand != "auto" and requested_hand != hand_name:
|
|
continue
|
|
var hand_value: Hand = Hand.LEFT if hand_name == "left" else Hand.RIGHT
|
|
var compatible: Array[String] = ANCHOR_DATA.compatible_grip_ids(anchors, hand_value)
|
|
for anchor_id: String in compatible:
|
|
var candidate_anchor: Dictionary = anchors.get(anchor_id, {})
|
|
var wrist_position := _preview_wrist_position(hand_value, root_offset, facing_left)
|
|
var grip_position := _grip_world_position_for_facing(prop, candidate_anchor, facing_left)
|
|
candidates.append({"anchor_id": anchor_id, "anchor": candidate_anchor, "hand": hand_value, "distance": wrist_position.distance_to(grip_position)})
|
|
if candidates.is_empty():
|
|
return {}
|
|
candidates.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
var a_primary := String(a.get("anchor_id", "")) == "primary_grip"
|
|
var b_primary := String(b.get("anchor_id", "")) == "primary_grip"
|
|
if a_primary != b_primary:
|
|
return a_primary
|
|
return float(a.get("distance", INF)) < float(b.get("distance", INF))
|
|
)
|
|
return candidates[0]
|
|
|
|
|
|
func _preview_wrist_position(hand_value: int, root_offset: Vector2, facing_left: bool) -> Vector2:
|
|
var wrist := _solved_wrist_transform_for(hand_value)
|
|
var rig_transform := _rig_transform_for_facing(facing_left, root_offset)
|
|
return rig_transform * _rig.to_local(wrist.origin)
|
|
|
|
|
|
func _throw_preview_metadata(context: Dictionary) -> Dictionary:
|
|
var planned := bool(context.get("planned", false))
|
|
var uncertain := bool(context.get("uncertain", false))
|
|
var hint := ""
|
|
var step := int(context.get("plan_step", 0))
|
|
if planned and step > 0:
|
|
hint = "Assumes Step %d pickup succeeds." % step
|
|
elif planned:
|
|
hint = "Assumes an earlier pickup succeeds."
|
|
if uncertain:
|
|
hint = "Estimated preview. " + hint if not hint.is_empty() else "Estimated preview from earlier actions."
|
|
return {"preview_available": true, "preview_planned": planned, "preview_uncertain": uncertain, "preview_hint": hint}
|
|
|
|
|
|
func _build_throw_pose(target: Vector2, marker: Marker2D, prop: PropBlock, hand_value: int, root_offset: Vector2) -> Dictionary:
|
|
var prethrow_delta := target - prop.global_position
|
|
if not prethrow_delta.is_finite():
|
|
return {}
|
|
var root_position := _rig.global_position + root_offset
|
|
var facing_left := target.x < root_position.x if absf(target.x - root_position.x) > 0.5 else _is_facing_left()
|
|
var suffix := "Left" if hand_value == Hand.LEFT else "Right"
|
|
var shoulder_bone := _rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm" % suffix)) as Bone2D
|
|
var lower_bone := _rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm/%sLowerArm" % [suffix, suffix])) as Bone2D
|
|
var head_node := _rig.get_node_or_null(NodePath("Body/Head")) as Node2D
|
|
if shoulder_bone == null or lower_bone == null or head_node == null:
|
|
return {}
|
|
var pose_transform := _rig_transform_for_facing(facing_left, root_offset)
|
|
var head_center := pose_transform * _rig.to_local(head_node.global_position)
|
|
var shoulder := pose_transform * _rig.to_local(shoulder_bone.global_position)
|
|
var head_radius := 100.0
|
|
var radius_value: Variant = head_node.get("radius")
|
|
if radius_value is float or radius_value is int:
|
|
head_radius = maxf(float(radius_value), 20.0)
|
|
var aim_sign := signf(target.x - root_position.x)
|
|
if is_zero_approx(aim_sign):
|
|
aim_sign = -1.0 if facing_left else 1.0
|
|
var max_reach := maxf(1.0, shoulder_bone.length + lower_bone.length - THROW_REACH_MARGIN)
|
|
var behind_control := head_center + Vector2(-aim_sign * head_radius * 0.78, -head_radius * 0.20)
|
|
var overhead := head_center + Vector2(-aim_sign * 8.0, -head_radius - THROW_HEAD_CLEARANCE)
|
|
var swing_control := head_center + Vector2(aim_sign * (head_radius * 0.62), -head_radius - THROW_HEAD_CLEARANCE * 0.45)
|
|
var release := head_center + Vector2(aim_sign * (head_radius + 44.0), -head_radius * 0.26)
|
|
var follow_control := release + Vector2(aim_sign * 44.0, -12.0)
|
|
var follow := release + Vector2(aim_sign * 88.0, 46.0)
|
|
behind_control = _clamp_to_arm_reach(behind_control, shoulder, max_reach)
|
|
overhead = _clamp_to_arm_reach(overhead, shoulder, max_reach)
|
|
swing_control = _clamp_to_arm_reach(swing_control, shoulder, max_reach)
|
|
release = _clamp_to_arm_reach(release, shoulder, max_reach)
|
|
follow_control = _clamp_to_arm_reach(follow_control, shoulder, max_reach)
|
|
follow = _clamp_to_arm_reach(follow, shoulder, max_reach)
|
|
var torso_marker := _torso_marker()
|
|
var torso_start := torso_marker.position if torso_marker != null else Vector2.ZERO
|
|
var torso_start_rotation := torso_marker.rotation if torso_marker != null else 0.0
|
|
var local_aim_sign := -aim_sign if facing_left else aim_sign
|
|
return {
|
|
"facing_left": facing_left,
|
|
"base": marker.position,
|
|
"windup": pose_transform.affine_inverse() * behind_control,
|
|
"overhead": pose_transform.affine_inverse() * overhead,
|
|
"swing_control": pose_transform.affine_inverse() * swing_control,
|
|
"release": pose_transform.affine_inverse() * release,
|
|
"follow_control": pose_transform.affine_inverse() * follow_control,
|
|
"follow": pose_transform.affine_inverse() * follow,
|
|
"torso_start": torso_start,
|
|
"torso_start_rotation": torso_start_rotation,
|
|
"torso_windup": torso_start + Vector2(-local_aim_sign * 24.0, -14.0),
|
|
"torso_windup_rotation": torso_start_rotation - local_aim_sign * 0.12,
|
|
}
|
|
|
|
|
|
func _store_throw_pose(pose: Dictionary, marker: Marker2D) -> void:
|
|
_throw_base_marker_local = marker.position
|
|
_throw_windup_marker_local = pose["windup"]
|
|
_throw_overhead_marker_local = pose["overhead"]
|
|
_throw_swing_control_marker_local = pose["swing_control"]
|
|
_throw_release_marker_local = pose["release"]
|
|
_throw_follow_control_marker_local = pose["follow_control"]
|
|
_throw_follow_marker_local = pose["follow"]
|
|
_throw_torso_start = pose["torso_start"]
|
|
_throw_torso_start_rotation = float(pose["torso_start_rotation"])
|
|
_throw_torso_windup = pose["torso_windup"]
|
|
_throw_torso_windup_rotation = float(pose["torso_windup_rotation"])
|
|
|
|
|
|
func _rig_transform_for_facing(facing_left: bool, root_offset: Vector2 = Vector2.ZERO) -> Transform2D:
|
|
var result := _rig.global_transform
|
|
result.origin += root_offset
|
|
var x_axis := result.x
|
|
if _is_facing_left():
|
|
x_axis = -x_axis
|
|
if facing_left:
|
|
x_axis = -x_axis
|
|
result.x = x_axis
|
|
return result
|
|
|
|
|
|
func _clamp_to_arm_reach(point: Vector2, shoulder: Vector2, max_reach: float) -> Vector2:
|
|
var delta := point - shoulder
|
|
if delta.length() <= max_reach or delta.is_zero_approx():
|
|
return point
|
|
return shoulder + delta.normalized() * max_reach
|
|
|
|
|
|
func _clamp_throw_target(target: Vector2, root_position: Vector2 = Vector2.INF) -> Vector2:
|
|
if _rig == null:
|
|
return target
|
|
var center := root_position if root_position.is_finite() else _rig.global_position
|
|
var offset := target - center
|
|
if offset.length() <= MAX_THROW_RANGE or offset.is_zero_approx():
|
|
return target
|
|
return center + offset.normalized() * MAX_THROW_RANGE
|
|
|
|
|
|
func _invalid_throw_preview(effective_target: Vector2, start_position: Vector2) -> Dictionary:
|
|
return {
|
|
"start_position": start_position,
|
|
"effective_target": effective_target,
|
|
"landing_position": effective_target,
|
|
"collision_position": Vector2.INF,
|
|
"collision_before_target": false,
|
|
"target_reachable": false,
|
|
"flight_time": 0.0,
|
|
"trajectory_points": PackedVector2Array(),
|
|
}
|
|
|
|
|
|
func disarm_for_ragdoll() -> void:
|
|
var interrupted_pickup := _phase == Phase.APPROACHING or _phase == Phase.REACHING or _phase == Phase.PICKUP_RECOVERING
|
|
var interrupted_windup := _phase == Phase.THROWING and not _throw_released
|
|
if _phase == Phase.APPROACHING or _phase == Phase.REACHING or _phase == Phase.PICKUP_RECOVERING:
|
|
cancel_action()
|
|
elif interrupted_windup:
|
|
cancel_action()
|
|
if _held_prop != null and is_instance_valid(_held_prop):
|
|
_release_held("ragdoll", false)
|
|
if interrupted_pickup or interrupted_windup:
|
|
action_completed.emit()
|
|
|
|
|
|
func reset_for_scene_exit() -> void:
|
|
if _phase == Phase.APPROACHING or _phase == Phase.REACHING or _phase == Phase.PICKUP_RECOVERING or (_phase == Phase.THROWING and not _throw_released):
|
|
cancel_action()
|
|
if _held_prop != null and is_instance_valid(_held_prop):
|
|
_release_held("scene_reset", false)
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
_restore_hand_target()
|
|
|
|
|
|
func _begin_pickup(prop_id: String, requested_hand: String, action: Dictionary) -> bool:
|
|
if _phase != Phase.IDLE or _held_prop != null or _rig == null:
|
|
return false
|
|
if int(_rig.get("state")) != 0:
|
|
return false
|
|
if prop_id.is_empty() or not _resolver.is_valid():
|
|
return false
|
|
var resolved: Variant = _resolver.call(prop_id)
|
|
if not resolved is PropBlock:
|
|
return false
|
|
var prop := resolved as PropBlock
|
|
if not is_instance_valid(prop) or prop.is_queued_for_deletion():
|
|
return false
|
|
if prop.interaction_owner_id != 0 or prop.interaction_reserved_by != 0:
|
|
return false
|
|
var picked := _choose_grip_and_hand(prop, requested_hand)
|
|
if picked.is_empty():
|
|
push_warning("StickmanPropInteractor: no compatible one-hand grip; pickup skipped.")
|
|
return false
|
|
_held_anchor = (picked["anchor"] as Dictionary).duplicate(true)
|
|
_hand = int(picked["hand"])
|
|
_held_prop = prop
|
|
_held_prop.interaction_reserved_by = _rig.get_instance_id()
|
|
_active_action = action.duplicate(true)
|
|
_approach_replans = 0
|
|
var marker := _hand_marker()
|
|
if marker == null:
|
|
_clear_reservation()
|
|
_phase = Phase.IDLE
|
|
return false
|
|
_marker_start_position = marker.position
|
|
var grip_position := _grip_world_position(prop, _held_anchor)
|
|
if _solved_wrist_transform().origin.distance_to(grip_position) > MAX_REACH:
|
|
if not _start_approach(grip_position):
|
|
_clear_reservation()
|
|
_phase = Phase.IDLE
|
|
return false
|
|
_phase = Phase.APPROACHING
|
|
return true
|
|
_start_reach(grip_position)
|
|
return true
|
|
|
|
|
|
func _choose_grip_and_hand(prop: PropBlock, requested_hand: String) -> Dictionary:
|
|
var anchors := prop.get_interaction_anchors()
|
|
var candidates: Array[Dictionary] = []
|
|
for hand_name: String in ["left", "right"]:
|
|
if requested_hand != "auto" and requested_hand != hand_name:
|
|
continue
|
|
var hand_value: int = Hand.LEFT if hand_name == "left" else Hand.RIGHT
|
|
var compatible: Array[String] = ANCHOR_DATA.compatible_grip_ids(anchors, hand_value)
|
|
for anchor_id: String in compatible:
|
|
var anchor: Dictionary = anchors.get(anchor_id, {})
|
|
var distance: float = _solved_wrist_transform_for(hand_value).origin.distance_to(_grip_world_position(prop, anchor))
|
|
candidates.append({"anchor_id": anchor_id, "anchor": anchor, "hand": hand_value, "distance": distance})
|
|
if candidates.is_empty():
|
|
return {}
|
|
candidates.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
var a_primary := String(a.get("anchor_id", "")) == "primary_grip"
|
|
var b_primary := String(b.get("anchor_id", "")) == "primary_grip"
|
|
if a_primary != b_primary:
|
|
return a_primary
|
|
return float(a.get("distance", INF)) < float(b.get("distance", INF))
|
|
)
|
|
return candidates[0]
|
|
|
|
|
|
func _start_approach(grip_position: Vector2) -> bool:
|
|
if _locomotion == null:
|
|
return false
|
|
var side := -1.0 if _rig.global_position.x <= grip_position.x else 1.0
|
|
var foot_offset: Vector2 = _locomotion.call("get_foot_offset")
|
|
var target_root := Vector2(grip_position.x + side * APPROACH_GAP, _rig.global_position.y)
|
|
var target_feet := target_root - foot_offset
|
|
return bool(_locomotion.call("walk_to", target_feet, -1.0))
|
|
|
|
|
|
func _on_approach_arrived() -> void:
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
_clear_reservation()
|
|
_restore_hand_target()
|
|
_restore_pickup_torso()
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
return
|
|
var grip_position := _grip_world_position(_held_prop, _held_anchor)
|
|
var marker := _hand_marker()
|
|
if marker == null:
|
|
_clear_reservation()
|
|
_restore_hand_target()
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
return
|
|
if _solved_wrist_transform().origin.distance_to(grip_position) > MAX_REACH:
|
|
if _approach_replans < APPROACH_REPLAN_LIMIT:
|
|
_approach_replans += 1
|
|
if _start_approach(grip_position):
|
|
return
|
|
_clear_reservation()
|
|
_restore_hand_target()
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
return
|
|
_start_reach(grip_position)
|
|
|
|
|
|
func _start_reach(grip_position: Vector2) -> void:
|
|
var marker := _hand_marker()
|
|
if marker == null:
|
|
_clear_reservation()
|
|
_restore_hand_target()
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
return
|
|
_reach_start = marker.global_position
|
|
_reach_target = grip_position
|
|
_reach_elapsed = 0.0
|
|
var torso_marker := _torso_marker()
|
|
_pickup_uses_crouch = false
|
|
if torso_marker != null:
|
|
_pickup_torso_start = torso_marker.position
|
|
_pickup_torso_start_rotation = torso_marker.rotation
|
|
_pickup_torso_crouched = _pickup_torso_start
|
|
_pickup_torso_crouched_rotation = _pickup_torso_start_rotation
|
|
var grip_local := _rig.to_local(grip_position)
|
|
var low_amount := clampf((grip_local.y - _pickup_torso_start.y - LOW_PICKUP_THRESHOLD) / LOW_PICKUP_MAX_DROP, 0.0, 1.0)
|
|
if low_amount > 0.0:
|
|
var toward_prop := signf(grip_local.x - _pickup_torso_start.x)
|
|
if is_zero_approx(toward_prop):
|
|
toward_prop = -1.0 if _is_facing_left() else 1.0
|
|
_pickup_torso_crouched += Vector2(toward_prop * LOW_PICKUP_LEAN * low_amount, LOW_PICKUP_MAX_DROP * low_amount)
|
|
_pickup_torso_crouched_rotation -= toward_prop * 0.10 * low_amount
|
|
_pickup_uses_crouch = true
|
|
_rig.call("_set_limb_mods_enabled", true)
|
|
_phase = Phase.REACHING
|
|
|
|
|
|
func _update_reach(delta: float) -> void:
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
_clear_reservation()
|
|
_restore_hand_target()
|
|
_phase = Phase.IDLE
|
|
action_completed.emit()
|
|
return
|
|
var marker := _hand_marker()
|
|
if marker == null:
|
|
cancel_action()
|
|
action_completed.emit()
|
|
return
|
|
_reach_elapsed += delta
|
|
var weight := clampf(_reach_elapsed / REACH_DURATION, 0.0, 1.0)
|
|
var eased_weight := smoothstep(0.0, 1.0, weight)
|
|
_reach_target = _grip_world_position(_held_prop, _held_anchor)
|
|
marker.global_position = _reach_start.lerp(_reach_target, eased_weight)
|
|
_update_pickup_crouch(eased_weight)
|
|
if weight < 1.0:
|
|
return
|
|
if _solved_wrist_transform().origin.distance_to(_reach_target) > MAX_REACH:
|
|
cancel_action()
|
|
action_completed.emit()
|
|
return
|
|
_commit_pickup(marker)
|
|
|
|
|
|
func _commit_pickup(marker: Marker2D) -> void:
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
cancel_action()
|
|
action_completed.emit()
|
|
return
|
|
_saved_body_state = {
|
|
"freeze": _held_prop.freeze,
|
|
"freeze_mode": _held_prop.freeze_mode,
|
|
"collision_layer": _held_prop.collision_layer,
|
|
"collision_mask": _held_prop.collision_mask,
|
|
"z_index": _held_prop.z_index,
|
|
"z_as_relative": _held_prop.z_as_relative,
|
|
"linear_velocity": _held_prop.linear_velocity,
|
|
"angular_velocity": _held_prop.angular_velocity,
|
|
}
|
|
_held_prop.interaction_reserved_by = 0
|
|
_held_prop.interaction_owner_id = _rig.get_instance_id()
|
|
_held_prop.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
|
|
_held_prop.freeze = true
|
|
_held_prop.sleeping = false
|
|
_held_prop.collision_layer = 0
|
|
_held_prop.collision_mask = 0
|
|
if _pickup_uses_crouch:
|
|
_pickup_recover_elapsed = 0.0
|
|
_phase = Phase.PICKUP_RECOVERING
|
|
else:
|
|
_phase = Phase.HELD
|
|
_active_action.clear()
|
|
_update_held_transform(0.0)
|
|
prop_event.emit("prop_picked_up", _held_prop, "")
|
|
if not _pickup_uses_crouch:
|
|
action_completed.emit()
|
|
|
|
|
|
func _update_pickup_recovery(delta: float) -> void:
|
|
_pickup_recover_elapsed += delta
|
|
var weight := smoothstep(0.0, 1.0, clampf(_pickup_recover_elapsed / PICKUP_RECOVER_DURATION, 0.0, 1.0))
|
|
_update_pickup_crouch(1.0 - weight)
|
|
_update_held_transform(delta)
|
|
if weight < 1.0:
|
|
return
|
|
_restore_pickup_torso()
|
|
_phase = Phase.HELD
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
|
|
|
|
func _update_pickup_crouch(weight: float) -> void:
|
|
var torso_marker := _torso_marker()
|
|
if torso_marker == null or not _pickup_uses_crouch:
|
|
return
|
|
torso_marker.position = _pickup_torso_start.lerp(_pickup_torso_crouched, weight)
|
|
torso_marker.rotation = lerpf(_pickup_torso_start_rotation, _pickup_torso_crouched_rotation, weight)
|
|
|
|
|
|
func _restore_pickup_torso() -> void:
|
|
var torso_marker := _torso_marker()
|
|
if torso_marker != null and _pickup_uses_crouch:
|
|
torso_marker.position = _pickup_torso_start
|
|
torso_marker.rotation = _pickup_torso_start_rotation
|
|
_pickup_uses_crouch = false
|
|
|
|
|
|
func _update_held_transform(delta: float, follow_hand_marker: bool = false) -> void:
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
_clear_held_state()
|
|
return
|
|
var marker := _hand_marker()
|
|
if follow_hand_marker and marker != null:
|
|
_held_prop.global_transform = _predicted_attachment_transform(marker.global_position, _is_facing_left())
|
|
else:
|
|
var wrist := _solved_wrist_transform()
|
|
var anchor := _scaled_anchor(_held_anchor, _held_prop)
|
|
_held_prop.global_transform = ANCHOR_DATA.solve_attachment_transform(wrist, anchor, Vector2.ONE, _is_facing_left())
|
|
_held_prop.z_as_relative = true
|
|
_held_prop.z_index = _rig.z_index + int(_held_anchor.get("z_index_offset", 0))
|
|
if delta > 0.0:
|
|
var current := _held_prop.global_transform
|
|
_sampled_linear_velocity = (current.origin - _last_transform.origin) / delta
|
|
_sampled_angular_velocity = wrapf(current.get_rotation() - _last_transform.get_rotation(), -PI, PI) / delta
|
|
_last_transform = _held_prop.global_transform
|
|
|
|
|
|
func _update_throw(delta: float) -> void:
|
|
_throw_elapsed += delta
|
|
var marker := _hand_marker()
|
|
var release_time := THROW_LIFT_DURATION + THROW_SWING_DURATION
|
|
if not _throw_released:
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
return
|
|
if marker != null:
|
|
if _throw_elapsed < THROW_LIFT_DURATION:
|
|
var lift_weight := smoothstep(0.0, 1.0, clampf(_throw_elapsed / THROW_LIFT_DURATION, 0.0, 1.0))
|
|
marker.position = _quadratic_bezier(_throw_base_marker_local, _throw_windup_marker_local, _throw_overhead_marker_local, lift_weight)
|
|
_update_throw_torso(lift_weight)
|
|
else:
|
|
var swing_weight := smoothstep(0.0, 1.0, clampf((_throw_elapsed - THROW_LIFT_DURATION) / THROW_SWING_DURATION, 0.0, 1.0))
|
|
marker.position = _quadratic_bezier(_throw_overhead_marker_local, _throw_swing_control_marker_local, _throw_release_marker_local, swing_weight)
|
|
_update_throw_torso(1.0 - swing_weight * 0.65)
|
|
_update_held_transform(delta, true)
|
|
if not _throw_released and _throw_elapsed >= release_time:
|
|
if marker != null:
|
|
marker.position = _throw_release_marker_local
|
|
_update_throw_torso(0.35)
|
|
if _held_prop != null and is_instance_valid(_held_prop):
|
|
_held_prop.global_transform = _rig_transform_for_facing(_throw_facing_left) * _throw_release_attachment_local
|
|
var target := _throw_effective_target
|
|
var spin_sign := -1.0 if _throw_facing_left else 1.0
|
|
var solution := _solve_throw(_held_prop.global_transform, target, _held_prop, spin_sign * THROW_SPIN_SPEED)
|
|
if solution.is_empty():
|
|
if marker != null:
|
|
marker.position = _throw_base_marker_local
|
|
_restore_throw_torso()
|
|
_phase = Phase.HELD
|
|
_active_action.clear()
|
|
action_completed.emit()
|
|
return
|
|
_release_held("throw", true, solution["velocity"], spin_sign * THROW_SPIN_SPEED)
|
|
_throw_released = true
|
|
if _throw_released and marker != null:
|
|
var follow_weight := smoothstep(0.0, 1.0, clampf((_throw_elapsed - release_time) / THROW_FOLLOW_THROUGH, 0.0, 1.0))
|
|
marker.position = _quadratic_bezier(_throw_release_marker_local, _throw_follow_control_marker_local, _throw_follow_marker_local, follow_weight)
|
|
_update_throw_torso(0.35 * (1.0 - follow_weight))
|
|
if _throw_released and _throw_elapsed >= release_time + THROW_FOLLOW_THROUGH:
|
|
_phase = Phase.IDLE
|
|
_throw_released = false
|
|
_active_action.clear()
|
|
_restore_hand_target()
|
|
_restore_throw_torso()
|
|
action_completed.emit()
|
|
|
|
|
|
func _update_throw_torso(weight: float) -> void:
|
|
var torso_marker := _torso_marker()
|
|
if torso_marker == null:
|
|
return
|
|
torso_marker.position = _throw_torso_start.lerp(_throw_torso_windup, weight)
|
|
torso_marker.rotation = lerpf(_throw_torso_start_rotation, _throw_torso_windup_rotation, weight)
|
|
|
|
|
|
func _restore_throw_torso() -> void:
|
|
var torso_marker := _torso_marker()
|
|
if torso_marker != null:
|
|
torso_marker.position = _throw_torso_start
|
|
torso_marker.rotation = _throw_torso_start_rotation
|
|
|
|
|
|
func _solve_throw(release_transform: Transform2D, target: Vector2, prop: PropBlock, spin_speed: float) -> Dictionary:
|
|
var start := release_transform.origin
|
|
var delta_position := target - start
|
|
if not start.is_finite() or not target.is_finite() or not delta_position.is_finite():
|
|
return {}
|
|
if delta_position.is_zero_approx():
|
|
return {}
|
|
var gravity_direction: Vector2 = ProjectSettings.get_setting("physics/2d/default_gravity_vector", Vector2.DOWN)
|
|
var gravity_magnitude := float(ProjectSettings.get_setting("physics/2d/default_gravity", 980.0)) * prop.gravity_scale
|
|
if not gravity_direction.is_finite() or not is_finite(gravity_magnitude) or not is_finite(spin_speed):
|
|
return {}
|
|
var gravity := gravity_direction.normalized() * gravity_magnitude
|
|
if not gravity.is_finite():
|
|
return {}
|
|
var flight_time := _shortest_valid_throw_time(delta_position, gravity)
|
|
if flight_time <= 0.0:
|
|
return {}
|
|
var velocity := (delta_position - 0.5 * gravity * flight_time * flight_time) / flight_time
|
|
if not velocity.is_finite() or velocity.length() > MAX_THROW_SPEED + 0.01:
|
|
return {}
|
|
return _make_throw_solution(start, target, velocity, gravity, release_transform.get_rotation(), spin_speed, flight_time)
|
|
|
|
|
|
func _shortest_valid_throw_time(displacement: Vector2, gravity: Vector2) -> float:
|
|
var gravity_squared := gravity.length_squared()
|
|
var lower_time := MIN_THROW_FLIGHT_TIME
|
|
var upper_time := THROW_PREVIEW_HORIZON
|
|
if gravity_squared > 0.000001:
|
|
var a := 0.25 * gravity_squared
|
|
var b := -(displacement.dot(gravity) + MAX_THROW_SPEED * MAX_THROW_SPEED)
|
|
var c := displacement.length_squared()
|
|
var discriminant := b * b - 4.0 * a * c
|
|
if discriminant < 0.0:
|
|
return 0.0
|
|
var root := sqrt(maxf(discriminant, 0.0))
|
|
var lower_time_squared := (-b - root) / (2.0 * a)
|
|
var upper_time_squared := (-b + root) / (2.0 * a)
|
|
if upper_time_squared <= 0.0:
|
|
return 0.0
|
|
lower_time = maxf(lower_time, sqrt(maxf(lower_time_squared, 0.0)))
|
|
upper_time = minf(upper_time, sqrt(upper_time_squared))
|
|
var down_direction := gravity / sqrt(gravity_squared)
|
|
var down_displacement := displacement.dot(down_direction)
|
|
if down_displacement < 0.0:
|
|
var descending_time := sqrt(-2.0 * down_displacement / sqrt(gravity_squared)) + THROW_DESCENT_EPSILON
|
|
lower_time = maxf(lower_time, descending_time)
|
|
else:
|
|
lower_time = maxf(lower_time, displacement.length() / MAX_THROW_SPEED)
|
|
if lower_time > upper_time or lower_time > THROW_PREVIEW_HORIZON:
|
|
return 0.0
|
|
return lower_time
|
|
|
|
|
|
func _make_throw_solution(start: Vector2, target: Vector2, velocity: Vector2, gravity: Vector2, release_rotation: float, spin_speed: float, flight_time: float) -> Dictionary:
|
|
var points := PackedVector2Array()
|
|
for index: int in THROW_TRAJECTORY_SEGMENTS + 1:
|
|
var t := flight_time * float(index) / float(THROW_TRAJECTORY_SEGMENTS)
|
|
points.append(start + velocity * t + gravity * (0.5 * t * t))
|
|
return {
|
|
"velocity": velocity,
|
|
"landing_position": target,
|
|
"start_position": start,
|
|
"effective_target": target,
|
|
"release_rotation": release_rotation,
|
|
"spin_speed": spin_speed,
|
|
"flight_time": flight_time,
|
|
"trajectory_points": points,
|
|
}
|
|
|
|
|
|
func _predicted_attachment_transform(wrist_position: Vector2, facing_left: bool, prop_override: PropBlock = null, anchor_override: Dictionary = {}, hand_override: int = -1, root_offset: Vector2 = Vector2.ZERO) -> Transform2D:
|
|
var prop := _held_prop if prop_override == null else prop_override
|
|
var anchor := _held_anchor if anchor_override.is_empty() else anchor_override
|
|
var hand_value := int(_hand) if hand_override < 0 else hand_override
|
|
if _rig == null or prop == null or not is_instance_valid(prop):
|
|
return Transform2D(0.0, wrist_position)
|
|
var suffix := "Left" if hand_value == Hand.LEFT else "Right"
|
|
var lower_visual := _rig.get_node_or_null(NodePath("Body/%sLowerArm" % suffix)) as Node2D
|
|
var elbow_world := _solved_wrist_transform_for(hand_value).origin
|
|
if lower_visual != null:
|
|
elbow_world = lower_visual.global_position
|
|
var rig_transform := _rig_transform_for_facing(facing_left, root_offset)
|
|
var elbow_position := rig_transform * _rig.to_local(elbow_world)
|
|
var arm_direction := wrist_position - elbow_position
|
|
var arm_angle := arm_direction.angle() if not arm_direction.is_zero_approx() else _solved_wrist_transform_for(hand_value).get_rotation()
|
|
var wrist_transform := Transform2D(arm_angle, wrist_position)
|
|
return ANCHOR_DATA.solve_attachment_transform(wrist_transform, _scaled_anchor(anchor, prop), Vector2.ONE, facing_left)
|
|
|
|
|
|
func _clip_preview_to_collision(solution: Dictionary, prop: PropBlock) -> Dictionary:
|
|
if prop == null or not is_instance_valid(prop):
|
|
return solution
|
|
var collision_shape: Shape2D
|
|
var collision_local_transform := Transform2D.IDENTITY
|
|
var polygon_node := prop.get_node_or_null(NodePath("CollisionPolygon2D")) as CollisionPolygon2D
|
|
if polygon_node != null and polygon_node.polygon.size() >= 3:
|
|
var convex_points := Geometry2D.convex_hull(polygon_node.polygon)
|
|
if convex_points.size() >= 3:
|
|
var convex_shape := ConvexPolygonShape2D.new()
|
|
convex_shape.points = convex_points
|
|
collision_shape = convex_shape
|
|
collision_local_transform = prop.global_transform.affine_inverse() * polygon_node.global_transform
|
|
if polygon_node.disabled:
|
|
collision_shape = null
|
|
var circle_node := prop.get_node_or_null(NodePath("CollisionShape2D")) as CollisionShape2D
|
|
if collision_shape == null and circle_node != null and circle_node.shape != null and not circle_node.disabled:
|
|
collision_shape = circle_node.shape
|
|
collision_local_transform = prop.global_transform.affine_inverse() * circle_node.global_transform
|
|
if collision_shape == null:
|
|
return solution
|
|
var collision_mask := int(_saved_body_state.get("collision_mask", prop.collision_mask))
|
|
if collision_mask == 0:
|
|
return solution
|
|
var points: PackedVector2Array = solution.get("trajectory_points", PackedVector2Array())
|
|
if points.size() < 2:
|
|
return solution
|
|
var space_state := prop.get_world_2d().direct_space_state
|
|
if space_state == null:
|
|
return solution
|
|
var excluded: Array[RID] = [prop.get_rid()]
|
|
if _rig != null and _rig.has_method("get_collision_body_rids"):
|
|
for body_rid: RID in _rig.call("get_collision_body_rids"):
|
|
if body_rid.is_valid():
|
|
excluded.append(body_rid)
|
|
var clipped_points := PackedVector2Array([points[0]])
|
|
var impact_position := points[-1]
|
|
var impact_time := float(solution.get("flight_time", THROW_PREVIEW_HORIZON))
|
|
var collision_position := Vector2.INF
|
|
var collision_before_target := false
|
|
var segment_count := points.size() - 1
|
|
var horizon := float(solution.get("flight_time", THROW_PREVIEW_HORIZON))
|
|
var start_rotation := float(solution.get("release_rotation", 0.0))
|
|
for index: int in segment_count:
|
|
var from_position := points[index]
|
|
var to_position := points[index + 1]
|
|
var t0 := horizon * float(index) / float(segment_count)
|
|
var dt := horizon / float(segment_count)
|
|
var query := PhysicsShapeQueryParameters2D.new()
|
|
query.shape = collision_shape
|
|
query.transform = Transform2D(start_rotation + float(solution.get("spin_speed", THROW_SPIN_SPEED)) * t0, from_position) * collision_local_transform
|
|
query.motion = to_position - from_position
|
|
query.collision_mask = collision_mask
|
|
query.exclude = excluded
|
|
var cast := space_state.cast_motion(query)
|
|
if cast.size() >= 2 and cast[0] < 0.9999:
|
|
var safe_fraction := clampf(cast[0], 0.0, 1.0)
|
|
impact_position = from_position.lerp(to_position, safe_fraction)
|
|
impact_time = t0 + dt * safe_fraction
|
|
collision_position = impact_position
|
|
collision_before_target = true
|
|
clipped_points.append(impact_position)
|
|
break
|
|
clipped_points.append(to_position)
|
|
var clipped := solution.duplicate(true)
|
|
clipped["landing_position"] = impact_position
|
|
clipped["flight_time"] = impact_time
|
|
clipped["trajectory_points"] = clipped_points
|
|
clipped["collision_position"] = collision_position
|
|
clipped["collision_before_target"] = collision_before_target
|
|
return clipped
|
|
|
|
|
|
func _quadratic_bezier(from: Vector2, control: Vector2, to: Vector2, weight: float) -> Vector2:
|
|
var t := clampf(weight, 0.0, 1.0)
|
|
var inverse := 1.0 - t
|
|
return from * inverse * inverse + control * (2.0 * inverse * t) + to * t * t
|
|
|
|
|
|
func _release_held(reason: String, throwing: bool, throw_velocity: Vector2 = Vector2.ZERO, throw_spin: float = 0.0) -> void:
|
|
if _held_prop == null or not is_instance_valid(_held_prop):
|
|
_clear_held_state()
|
|
return
|
|
var prop := _held_prop
|
|
var release_velocity := throw_velocity if throwing else _sampled_linear_velocity
|
|
var release_angular := _sampled_angular_velocity
|
|
if not release_velocity.is_finite():
|
|
release_velocity = Vector2.ZERO
|
|
if not is_finite(release_angular):
|
|
release_angular = 0.0
|
|
prop.interaction_owner_id = 0
|
|
prop.interaction_reserved_by = 0
|
|
prop.collision_layer = int(_saved_body_state.get("collision_layer", 1))
|
|
prop.collision_mask = int(_saved_body_state.get("collision_mask", 1))
|
|
prop.z_index = int(_saved_body_state.get("z_index", prop.z_index))
|
|
prop.z_as_relative = bool(_saved_body_state.get("z_as_relative", prop.z_as_relative))
|
|
prop.freeze_mode = int(_saved_body_state.get("freeze_mode", RigidBody2D.FREEZE_MODE_KINEMATIC))
|
|
prop.freeze = false if throwing else bool(_saved_body_state.get("freeze", false))
|
|
if throwing:
|
|
prop.sleeping = false
|
|
prop.linear_velocity = release_velocity
|
|
prop.angular_velocity = throw_spin
|
|
elif not prop.freeze:
|
|
prop.sleeping = false
|
|
prop.linear_velocity = release_velocity
|
|
prop.angular_velocity = release_angular
|
|
_held_prop = null
|
|
_held_anchor.clear()
|
|
_saved_body_state.clear()
|
|
_sampled_linear_velocity = Vector2.ZERO
|
|
_sampled_angular_velocity = 0.0
|
|
if not throwing:
|
|
_restore_hand_target()
|
|
_phase = Phase.IDLE if not throwing else Phase.THROWING
|
|
if reason == "manual" or reason == "ragdoll":
|
|
prop_event.emit("prop_dropped", prop, reason)
|
|
elif reason == "throw":
|
|
prop_event.emit("prop_thrown", prop, "")
|
|
|
|
|
|
func _clear_reservation() -> void:
|
|
if _held_prop != null and is_instance_valid(_held_prop):
|
|
if _held_prop.interaction_reserved_by == _rig.get_instance_id():
|
|
_held_prop.interaction_reserved_by = 0
|
|
_held_prop = null
|
|
_held_anchor.clear()
|
|
_active_action.clear()
|
|
|
|
|
|
func _clear_held_state() -> void:
|
|
_restore_pickup_torso()
|
|
_held_prop = null
|
|
_held_anchor.clear()
|
|
_saved_body_state.clear()
|
|
_phase = Phase.IDLE
|
|
_active_action.clear()
|
|
|
|
|
|
func _restore_hand_target() -> void:
|
|
if _rig == null:
|
|
return
|
|
var marker := _hand_marker()
|
|
if marker != null:
|
|
marker.position = _marker_start_position
|
|
|
|
|
|
func _hand_marker() -> Marker2D:
|
|
return _hand_marker_for(_hand)
|
|
|
|
|
|
func _torso_marker() -> Marker2D:
|
|
return _rig.get_node_or_null(NodePath("IK_Targets/Torso")) as Marker2D if _rig != null else null
|
|
|
|
|
|
func _hand_marker_for(hand: int) -> Marker2D:
|
|
if _rig == null:
|
|
return null
|
|
var path := "IK_Targets/Left_Hand" if hand == Hand.LEFT else "IK_Targets/Right_Hand"
|
|
return _rig.get_node_or_null(NodePath(path)) as Marker2D
|
|
|
|
|
|
func _solved_wrist_transform() -> Transform2D:
|
|
return _solved_wrist_transform_for(_hand)
|
|
|
|
|
|
func _solved_wrist_transform_for(hand: Hand) -> Transform2D:
|
|
var suffix := "Left" if hand == Hand.LEFT else "Right"
|
|
var lower_visual := _rig.get_node_or_null(NodePath("Body/%sLowerArm" % suffix)) as Node2D
|
|
var lower_bone := _rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm/%sLowerArm" % [suffix, suffix])) as Bone2D
|
|
if lower_visual == null or lower_bone == null:
|
|
var marker := _hand_marker_for(hand)
|
|
return Transform2D(0.0, marker.global_position if marker != null else _rig.global_position)
|
|
var elbow := lower_visual.to_global(Vector2.ZERO)
|
|
var wrist := lower_visual.to_global(Vector2(0.0, lower_bone.length))
|
|
var direction := (wrist - elbow).normalized()
|
|
return Transform2D(direction.angle(), wrist)
|
|
|
|
|
|
func _scaled_anchor(anchor: Dictionary, prop: PropBlock) -> Dictionary:
|
|
var result := anchor.duplicate(true)
|
|
result["position"] = (anchor.get("position", Vector2.ZERO) as Vector2) * prop.get_interaction_scale()
|
|
return result
|
|
|
|
|
|
func _grip_world_position(prop: PropBlock, anchor: Dictionary) -> Vector2:
|
|
return _grip_world_position_for_facing(prop, anchor, _is_facing_left())
|
|
|
|
|
|
func _grip_world_position_for_facing(prop: PropBlock, anchor: Dictionary, facing_left: bool) -> Vector2:
|
|
var scaled := _scaled_anchor(anchor, prop)
|
|
var local_position: Vector2 = scaled.get("position", Vector2.ZERO)
|
|
if facing_left:
|
|
local_position.x = -local_position.x
|
|
return prop.global_transform * local_position
|
|
|
|
|
|
func _is_facing_left() -> bool:
|
|
return _rig != null and int(_rig.call("get_facing_profile")) == 0
|