Add Use Spot Overlay and Target Capture Bar functionality
- Introduced `StageUseTargetCaptureBar` class for managing user interactions with props and their use spots. - Implemented UI elements including buttons for changing props and canceling actions. - Added `PropUseSpotData` class for validation and serialization of prop use spots. - Created tests for validating use spot data migration, serialization, and rule matching. - Ensured proper handling of edge cases in use spot data, including unique IDs and finite positions.
This commit is contained in:
@@ -399,7 +399,7 @@ func _advance_to_next_action() -> void:
|
||||
action.erase("speed")
|
||||
_current_run_item["action"] = action
|
||||
var action_type := String(action.get("type", ""))
|
||||
var requires_animated := action_type == ACTION_REGISTRY.MOVE_TO or action_type == "pick_up_prop" or action_type == "drop_prop" or action_type == "throw_prop" or action_type == "set_down_prop"
|
||||
var requires_animated := action_type == ACTION_REGISTRY.MOVE_TO or action_type == "pick_up_prop" or action_type == "drop_prop" or action_type == "throw_prop" or action_type == "set_down_prop" or action_type == "use_prop"
|
||||
if requires_animated and _rig != null:
|
||||
if _rig.state == StickmanRig.RigState.RECOVERING:
|
||||
_run_queue.insert(_run_cursor, _current_run_item.duplicate(true))
|
||||
@@ -443,7 +443,7 @@ func _begin_action(action: Dictionary, legacy_speed_override: float = -1.0) -> v
|
||||
"recover":
|
||||
_action_phase = ActionPhase.RECOVERING
|
||||
recovery_requested.emit()
|
||||
"pick_up_prop", "drop_prop", "throw_prop", "set_down_prop":
|
||||
"pick_up_prop", "drop_prop", "throw_prop", "set_down_prop", "use_prop":
|
||||
if _rig == null or not _rig.start_prop_action(action):
|
||||
_finish_action()
|
||||
else:
|
||||
|
||||
@@ -6,9 +6,10 @@ const ANCHOR_DATA := preload("res://scripts/props/prop_anchor_data.gd")
|
||||
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
||||
const PROP_STAGE_ROLES := preload("res://scripts/props/prop_stage_roles.gd")
|
||||
const STAGE_PHYSICS := preload("res://scripts/stage/stage_physics_policy.gd")
|
||||
const USE_REACH_SOLVER := preload("res://scripts/characters/stickman/stickman_use_reach_solver.gd")
|
||||
|
||||
enum Hand { LEFT, RIGHT }
|
||||
enum Phase { IDLE, APPROACHING, REACHING, PICKUP_RECOVERING, HELD, THROWING, SET_DOWN_APPROACHING, SET_DOWN_REACHING, SET_DOWN_RECOVERING }
|
||||
enum Phase { IDLE, APPROACHING, REACHING, PICKUP_RECOVERING, HELD, THROWING, SET_DOWN_APPROACHING, SET_DOWN_REACHING, SET_DOWN_RECOVERING, USE_APPROACHING, USE_REACHING, USE_RECOVERING }
|
||||
|
||||
const MAX_REACH: float = 390.0
|
||||
const APPROACH_GAP: float = 90.0
|
||||
@@ -16,6 +17,12 @@ const APPROACH_REPLAN_LIMIT: int = 1
|
||||
const REACH_DURATION: float = 0.34
|
||||
const SET_DOWN_REACH_DURATION: float = 0.22
|
||||
const SET_DOWN_RECOVER_DURATION: float = 0.24
|
||||
const USE_REACH_DURATION: float = 0.28
|
||||
const USE_RECOVER_DURATION: float = 0.20
|
||||
const USE_MAX_REPLAN_DISPLACEMENT: float = 180.0
|
||||
const USE_REPLAN_LIMIT: int = 1
|
||||
## Temporary diagnostics for the Use reach failure report. Remove after diagnosis.
|
||||
const USE_REACH_TRACE_ENABLED: bool = true
|
||||
const PICKUP_RECOVER_DURATION: float = 0.32
|
||||
const LOW_PICKUP_THRESHOLD: float = 110.0
|
||||
const LOW_PICKUP_MAX_DROP: float = 110.0
|
||||
@@ -38,6 +45,8 @@ const PROP_TRACE_ENABLED: bool = false
|
||||
|
||||
signal action_completed
|
||||
signal prop_event(event_type: String, prop: PropBlock, reason: String)
|
||||
signal use_completed(prop: PropBlock, spot_id: String, world_position: Vector2)
|
||||
signal use_failed(reason_code: String, prop: PropBlock, spot_id: String)
|
||||
|
||||
var _rig: Node2D
|
||||
var _locomotion: Node
|
||||
@@ -91,6 +100,16 @@ var _set_down_recover_hand_start: Vector2 = Vector2.ZERO
|
||||
var _trace_released_prop: PropBlock
|
||||
var _trace_released_elapsed: float = 0.0
|
||||
var _trace_released_watch_remaining: float = 0.0
|
||||
var _use_prop: PropBlock
|
||||
var _use_spot_id: String = ""
|
||||
var _use_planned_spot_world: Vector2 = Vector2.INF
|
||||
var _use_approach_root: Vector2 = Vector2.ZERO
|
||||
var _use_approach_replans: int = 0
|
||||
var _use_reach_elapsed: float = 0.0
|
||||
var _use_recover_elapsed: float = 0.0
|
||||
var _use_marker_start: Vector2 = Vector2.ZERO
|
||||
var _use_marker_recover_start: Vector2 = Vector2.ZERO
|
||||
var _use_hand: Hand = Hand.RIGHT
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -167,6 +186,8 @@ func start_action(action: Dictionary) -> bool:
|
||||
return true
|
||||
"set_down_prop":
|
||||
return _begin_set_down(action)
|
||||
"use_prop":
|
||||
return _begin_use(action)
|
||||
return false
|
||||
|
||||
|
||||
@@ -193,6 +214,25 @@ func tick(delta: float) -> void:
|
||||
_update_set_down_reach(delta)
|
||||
Phase.SET_DOWN_RECOVERING:
|
||||
_update_set_down_recovery(delta)
|
||||
Phase.USE_APPROACHING:
|
||||
if not _validate_use_target():
|
||||
return
|
||||
var live_spot: Vector2 = _use_prop.use_spot_world_position(_use_spot_id)
|
||||
if not live_spot.is_finite():
|
||||
_fail_use("spot_missing")
|
||||
return
|
||||
if live_spot.distance_to(_use_planned_spot_world) > USE_MAX_REPLAN_DISPLACEMENT:
|
||||
_request_use_replan("target_moved")
|
||||
return
|
||||
if _locomotion != null and bool(_locomotion.call("consume_walk_failed")):
|
||||
_fail_use("route_blocked")
|
||||
return
|
||||
if _locomotion != null and bool(_locomotion.call("consume_walk_completed")):
|
||||
_on_use_approach_arrived()
|
||||
Phase.USE_REACHING:
|
||||
_update_use_reach(delta)
|
||||
Phase.USE_RECOVERING:
|
||||
_update_use_recovery(delta)
|
||||
Phase.PICKUP_RECOVERING:
|
||||
_update_pickup_recovery(delta)
|
||||
Phase.HELD:
|
||||
@@ -202,7 +242,7 @@ func tick(delta: float) -> void:
|
||||
|
||||
|
||||
func cancel_action() -> void:
|
||||
if (_phase == Phase.APPROACHING or _phase == Phase.SET_DOWN_APPROACHING) and _locomotion != null:
|
||||
if (_phase == Phase.APPROACHING or _phase == Phase.SET_DOWN_APPROACHING or _phase == Phase.USE_APPROACHING) and _locomotion != null:
|
||||
_locomotion.call("cancel_walk")
|
||||
if _phase == Phase.SET_DOWN_APPROACHING or _phase == Phase.SET_DOWN_REACHING or _phase == Phase.SET_DOWN_RECOVERING:
|
||||
_restore_hand_target()
|
||||
@@ -232,6 +272,10 @@ func cancel_action() -> void:
|
||||
_throw_elapsed = 0.0
|
||||
_restore_hand_target()
|
||||
_restore_throw_torso()
|
||||
if _phase == Phase.USE_APPROACHING or _phase == Phase.USE_REACHING or _phase == Phase.USE_RECOVERING:
|
||||
_restore_hand_target()
|
||||
_restore_pickup_torso()
|
||||
_clear_use_state()
|
||||
if _phase != Phase.HELD:
|
||||
_phase = Phase.IDLE
|
||||
_active_action.clear()
|
||||
@@ -560,27 +604,327 @@ func _invalid_throw_preview(effective_target: Vector2, start_position: Vector2)
|
||||
func disarm_for_ragdoll() -> void:
|
||||
var interrupted_pickup := _phase == Phase.APPROACHING or _phase == Phase.REACHING or _phase == Phase.PICKUP_RECOVERING
|
||||
var interrupted_set_down := _phase == Phase.SET_DOWN_APPROACHING or _phase == Phase.SET_DOWN_REACHING or _phase == Phase.SET_DOWN_RECOVERING
|
||||
var interrupted_use := _phase == Phase.USE_APPROACHING or _phase == Phase.USE_REACHING or _phase == Phase.USE_RECOVERING
|
||||
var interrupted_windup := _phase == Phase.THROWING and not _throw_released
|
||||
if interrupted_pickup or interrupted_set_down:
|
||||
if interrupted_pickup or interrupted_set_down or interrupted_use:
|
||||
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_set_down or interrupted_windup:
|
||||
if interrupted_pickup or interrupted_set_down or interrupted_use or interrupted_windup:
|
||||
action_completed.emit()
|
||||
|
||||
|
||||
func reset_for_scene_exit() -> void:
|
||||
if PROP_TRACE_ENABLED:
|
||||
print("[PropTrace] reset_for_scene_exit actor=%s phase=%s held=%s" % [_trace_actor_name(), Phase.keys()[int(_phase)], _trace_prop_id(_held_prop)])
|
||||
if _phase == Phase.APPROACHING or _phase == Phase.REACHING or _phase == Phase.PICKUP_RECOVERING or _phase == Phase.SET_DOWN_APPROACHING or _phase == Phase.SET_DOWN_REACHING or _phase == Phase.SET_DOWN_RECOVERING or (_phase == Phase.THROWING and not _throw_released):
|
||||
if _phase == Phase.APPROACHING or _phase == Phase.REACHING or _phase == Phase.PICKUP_RECOVERING or _phase == Phase.SET_DOWN_APPROACHING or _phase == Phase.SET_DOWN_REACHING or _phase == Phase.SET_DOWN_RECOVERING or _phase == Phase.USE_APPROACHING or _phase == Phase.USE_REACHING or _phase == Phase.USE_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()
|
||||
_clear_use_state()
|
||||
|
||||
|
||||
func get_held_prop() -> PropBlock:
|
||||
return _held_prop if _held_prop != null and is_instance_valid(_held_prop) else null
|
||||
|
||||
|
||||
func preview_use(prop: PropBlock, spot_id: String, actor_root: Vector2 = Vector2.INF, predicted_facing_left: bool = false) -> Dictionary:
|
||||
if _rig == null or not is_instance_valid(_rig):
|
||||
return {"known": true, "valid": false, "reason_code": "out_of_reach"}
|
||||
var root: Vector2 = _rig.global_position if not actor_root.is_finite() else actor_root
|
||||
return USE_REACH_SOLVER.solve(_rig, prop, spot_id, root, predicted_facing_left)
|
||||
|
||||
|
||||
func _begin_use(action: Dictionary) -> bool:
|
||||
var prop_id: String = String(action.get("target_prop_id", ""))
|
||||
var spot_id: String = String(action.get("target_spot_id", ""))
|
||||
if _phase != Phase.IDLE or _rig == null or int(_rig.get("state")) != StickmanRig.RigState.ANIMATED:
|
||||
use_failed.emit("out_of_reach", null, spot_id)
|
||||
return false
|
||||
if _held_prop != null and is_instance_valid(_held_prop):
|
||||
use_failed.emit("hands_full", null, spot_id)
|
||||
return false
|
||||
if prop_id.is_empty() or not _resolver.is_valid():
|
||||
use_failed.emit("prop_missing", null, spot_id)
|
||||
return false
|
||||
var resolved: Variant = _resolver.call(prop_id)
|
||||
if not resolved is PropBlock or not is_instance_valid(resolved as PropBlock) or (resolved as PropBlock).is_queued_for_deletion():
|
||||
use_failed.emit("prop_missing", null, spot_id)
|
||||
return false
|
||||
var prop: PropBlock = resolved as PropBlock
|
||||
var preview: Dictionary = USE_REACH_SOLVER.solve(_rig, prop, spot_id, _rig.global_position, _is_facing_left())
|
||||
if not bool(preview.get("valid", false)):
|
||||
use_failed.emit(String(preview.get("reason_code", "out_of_reach")), prop, spot_id)
|
||||
return false
|
||||
_use_prop = prop
|
||||
_use_spot_id = spot_id
|
||||
_use_planned_spot_world = preview.get("spot_world", Vector2.INF)
|
||||
_use_approach_root = preview.get("approach_root", _rig.global_position)
|
||||
_use_approach_replans = 0
|
||||
_use_hand = int(preview.get("hand", Hand.RIGHT))
|
||||
_hand = _use_hand
|
||||
_active_action = action.duplicate(true)
|
||||
_trace_use_reach("begin", "", preview)
|
||||
var marker: Marker2D = _hand_marker()
|
||||
if marker == null:
|
||||
_clear_use_state()
|
||||
use_failed.emit("out_of_reach", prop, spot_id)
|
||||
return false
|
||||
_marker_start_position = marker.position
|
||||
if absf(_use_approach_root.x - _rig.global_position.x) > 8.0:
|
||||
return _start_use_approach(preview)
|
||||
_start_use_reach(preview)
|
||||
return _phase == Phase.USE_REACHING
|
||||
|
||||
|
||||
func _start_use_approach(preview: Dictionary) -> bool:
|
||||
if _locomotion == null or _use_prop == null or not is_instance_valid(_use_prop):
|
||||
_fail_use("prop_missing")
|
||||
return false
|
||||
_use_approach_root = preview.get("approach_root", _use_approach_root)
|
||||
_use_planned_spot_world = preview.get("spot_world", _use_planned_spot_world)
|
||||
_use_hand = int(preview.get("hand", _use_hand))
|
||||
_hand = _use_hand
|
||||
var facing_left: bool = bool(preview.get("facing_left", _is_facing_left()))
|
||||
if facing_left != _is_facing_left():
|
||||
_rig.call("set_facing_profile", StickmanRig.FacingProfile.LEFT if facing_left else StickmanRig.FacingProfile.RIGHT)
|
||||
var target_feet: Vector2 = _use_approach_root - _locomotion.call("get_foot_offset")
|
||||
if not bool(_locomotion.call("walk_to", target_feet, -1.0)):
|
||||
_fail_use("route_blocked")
|
||||
return false
|
||||
_phase = Phase.USE_APPROACHING
|
||||
return true
|
||||
|
||||
|
||||
func _on_use_approach_arrived() -> void:
|
||||
if not _validate_use_target():
|
||||
return
|
||||
var current_spot: Vector2 = _use_prop.use_spot_world_position(_use_spot_id)
|
||||
if not current_spot.is_finite():
|
||||
_fail_use("spot_missing")
|
||||
return
|
||||
if current_spot.distance_to(_use_planned_spot_world) > USE_MAX_REPLAN_DISPLACEMENT:
|
||||
_request_use_replan("target_moved")
|
||||
return
|
||||
var preview: Dictionary = USE_REACH_SOLVER.solve(_rig, _use_prop, _use_spot_id, _rig.global_position, _is_facing_left())
|
||||
_trace_use_reach("approach_arrived", String(preview.get("reason_code", "")), preview)
|
||||
if not bool(preview.get("valid", false)):
|
||||
_request_use_replan(String(preview.get("reason_code", "out_of_reach")))
|
||||
return
|
||||
if absf(Vector2(preview.get("approach_root", _rig.global_position)).x - _rig.global_position.x) > 14.0:
|
||||
_request_use_replan("target_moved", preview)
|
||||
return
|
||||
_use_planned_spot_world = current_spot
|
||||
_start_use_reach(preview)
|
||||
|
||||
|
||||
func _request_use_replan(failure_reason: String, preview: Dictionary = {}) -> void:
|
||||
if _phase == Phase.USE_APPROACHING and _locomotion != null:
|
||||
_locomotion.call("cancel_walk")
|
||||
if _use_approach_replans >= USE_REPLAN_LIMIT:
|
||||
_fail_use("target_moved" if failure_reason == "target_moved" else failure_reason)
|
||||
return
|
||||
_use_approach_replans += 1
|
||||
if preview.is_empty() and _use_prop != null and is_instance_valid(_use_prop):
|
||||
preview = USE_REACH_SOLVER.solve(_rig, _use_prop, _use_spot_id, _rig.global_position, _is_facing_left())
|
||||
if preview.is_empty() or not bool(preview.get("valid", false)):
|
||||
_fail_use(failure_reason)
|
||||
return
|
||||
var new_spot: Vector2 = preview.get("spot_world", Vector2.INF)
|
||||
if not new_spot.is_finite() or new_spot.distance_to(_use_planned_spot_world) > USE_MAX_REPLAN_DISPLACEMENT:
|
||||
_fail_use("target_moved")
|
||||
return
|
||||
_use_planned_spot_world = new_spot
|
||||
_use_approach_root = preview.get("approach_root", _rig.global_position)
|
||||
_use_hand = int(preview.get("hand", _use_hand))
|
||||
_hand = _use_hand
|
||||
if absf(_use_approach_root.x - _rig.global_position.x) > 14.0:
|
||||
_start_use_approach(preview)
|
||||
else:
|
||||
_start_use_reach(preview)
|
||||
|
||||
|
||||
func _start_use_reach(preview: Dictionary) -> void:
|
||||
_use_hand = int(preview.get("hand", _use_hand))
|
||||
_hand = _use_hand
|
||||
var marker: Marker2D = _hand_marker()
|
||||
if marker == null:
|
||||
_fail_use("out_of_reach")
|
||||
return
|
||||
# Locomotion replaces hand targets with its standing pose at arrival. Save
|
||||
# that current pose as the recovery target instead of restoring the stale
|
||||
# marker captured before the approach walk.
|
||||
_marker_start_position = marker.position
|
||||
_use_marker_start = marker.global_position
|
||||
_use_reach_elapsed = 0.0
|
||||
var torso_marker: Marker2D = _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 spot_world: Vector2 = preview.get("spot_world", _use_prop.use_spot_world_position(_use_spot_id))
|
||||
var spot_local: Vector2 = _rig.to_local(spot_world)
|
||||
var low_amount: float = clampf((spot_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: float = signf(spot_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.USE_REACHING
|
||||
_trace_use_reach("reach_started", "", preview)
|
||||
|
||||
|
||||
func _update_use_reach(delta: float) -> void:
|
||||
if not _validate_use_target():
|
||||
return
|
||||
var marker: Marker2D = _hand_marker()
|
||||
if marker == null:
|
||||
_fail_use("out_of_reach")
|
||||
return
|
||||
var spot_world: Vector2 = _use_prop.use_spot_world_position(_use_spot_id)
|
||||
if not spot_world.is_finite():
|
||||
_fail_use("spot_missing")
|
||||
return
|
||||
# Once reaching starts, do not drag the hand across a moving target. The
|
||||
# approach gets one bounded replan; the reach phase only tolerates contact
|
||||
# scale motion and then fails so DIRECT can be corrected.
|
||||
if spot_world.distance_to(_use_planned_spot_world) > USE_REACH_SOLVER.CONTACT_TOLERANCE:
|
||||
_fail_use("target_moved")
|
||||
return
|
||||
_use_reach_elapsed += delta
|
||||
var weight: float = clampf(_use_reach_elapsed / USE_REACH_DURATION, 0.0, 1.0)
|
||||
var eased_weight: float = smoothstep(0.0, 1.0, weight)
|
||||
marker.global_position = _use_marker_start.lerp(spot_world, eased_weight)
|
||||
_update_pickup_crouch(eased_weight)
|
||||
if weight < 1.0:
|
||||
return
|
||||
var final_preview: Dictionary = USE_REACH_SOLVER.solve(_rig, _use_prop, _use_spot_id, _rig.global_position, _is_facing_left())
|
||||
if not bool(final_preview.get("valid", false)) or int(final_preview.get("hand", -1)) != _use_hand:
|
||||
_trace_use_reach("reach_check_failed", String(final_preview.get("reason_code", "out_of_reach")), final_preview)
|
||||
_fail_use(String(final_preview.get("reason_code", "out_of_reach")))
|
||||
return
|
||||
var wrist_position: Vector2 = _solved_wrist_transform().origin
|
||||
if wrist_position.distance_to(spot_world) > USE_REACH_SOLVER.CONTACT_TOLERANCE:
|
||||
_trace_use_reach("contact_failed", "out_of_reach", final_preview)
|
||||
_fail_use("out_of_reach")
|
||||
return
|
||||
use_completed.emit(_use_prop, _use_spot_id, spot_world)
|
||||
_use_marker_recover_start = marker.global_position
|
||||
_use_recover_elapsed = 0.0
|
||||
_phase = Phase.USE_RECOVERING
|
||||
|
||||
|
||||
func _update_use_recovery(delta: float) -> void:
|
||||
_use_recover_elapsed += delta
|
||||
var weight: float = smoothstep(0.0, 1.0, clampf(_use_recover_elapsed / USE_RECOVER_DURATION, 0.0, 1.0))
|
||||
var marker: Marker2D = _hand_marker()
|
||||
if marker != null:
|
||||
marker.global_position = _use_marker_recover_start.lerp(_use_marker_start, weight)
|
||||
_update_pickup_crouch(1.0 - weight)
|
||||
if weight < 1.0:
|
||||
return
|
||||
_restore_hand_target()
|
||||
_restore_pickup_torso()
|
||||
_clear_use_state()
|
||||
_phase = Phase.IDLE
|
||||
_active_action.clear()
|
||||
action_completed.emit()
|
||||
|
||||
|
||||
func _validate_use_target() -> bool:
|
||||
if _use_prop == null or not is_instance_valid(_use_prop) or _use_prop.is_queued_for_deletion():
|
||||
_fail_use("prop_missing")
|
||||
return false
|
||||
if _use_prop.get_use_spot(_use_spot_id).is_empty():
|
||||
_fail_use("spot_missing")
|
||||
return false
|
||||
if _use_prop.interaction_owner_id != 0 or _use_prop.interaction_reserved_by != 0:
|
||||
_fail_use("prop_held")
|
||||
return false
|
||||
if _held_prop != null and is_instance_valid(_held_prop):
|
||||
_fail_use("hands_full")
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _fail_use(reason_code: String) -> void:
|
||||
var action_was_active: bool = _phase == Phase.USE_APPROACHING or _phase == Phase.USE_REACHING or _phase == Phase.USE_RECOVERING
|
||||
_trace_use_reach("failed", reason_code)
|
||||
if _phase == Phase.USE_APPROACHING and _locomotion != null:
|
||||
_locomotion.call("cancel_walk")
|
||||
var prop: PropBlock = _use_prop if _use_prop != null and is_instance_valid(_use_prop) else null
|
||||
var spot_id: String = _use_spot_id
|
||||
_restore_hand_target()
|
||||
_restore_pickup_torso()
|
||||
_clear_use_state()
|
||||
_phase = Phase.IDLE
|
||||
_active_action.clear()
|
||||
use_failed.emit(reason_code, prop, spot_id)
|
||||
if action_was_active:
|
||||
action_completed.emit()
|
||||
|
||||
|
||||
func _trace_use_reach(event_name: String, reason_code: String = "", preview: Dictionary = {}) -> void:
|
||||
if not USE_REACH_TRACE_ENABLED:
|
||||
return
|
||||
var details: Dictionary = {
|
||||
"event": event_name,
|
||||
"reason": reason_code,
|
||||
"phase": Phase.keys()[int(_phase)],
|
||||
"actor": String(_rig.name) if _rig != null and is_instance_valid(_rig) else "<none>",
|
||||
"root": _rig.global_position if _rig != null and is_instance_valid(_rig) else Vector2.INF,
|
||||
"rig_transform": _rig.global_transform if _rig != null and is_instance_valid(_rig) else Transform2D.IDENTITY,
|
||||
"rig_scale": _rig.global_scale if _rig != null and is_instance_valid(_rig) else Vector2.ZERO,
|
||||
"facing_left": _is_facing_left(),
|
||||
"hand": _use_hand,
|
||||
"approach_root": _use_approach_root,
|
||||
"planned_spot": _use_planned_spot_world,
|
||||
"preview": preview,
|
||||
}
|
||||
var marker: Marker2D = _hand_marker()
|
||||
if marker != null:
|
||||
details["hand_marker_world"] = marker.global_position
|
||||
var wrist_position: Vector2 = _solved_wrist_transform().origin if _rig != null and is_instance_valid(_rig) else Vector2.INF
|
||||
details["wrist_world"] = wrist_position
|
||||
if _use_prop != null and is_instance_valid(_use_prop):
|
||||
var spot: Dictionary = _use_prop.get_use_spot(_use_spot_id)
|
||||
var live_spot: Vector2 = _use_prop.use_spot_world_position(_use_spot_id)
|
||||
details["prop_id"] = _use_prop.stage_object_id
|
||||
details["prop_transform"] = _use_prop.global_transform
|
||||
details["spot_id"] = _use_spot_id
|
||||
details["spot_local"] = spot.get("position", Vector2.INF)
|
||||
details["spot_world"] = live_spot
|
||||
details["wrist_to_spot"] = wrist_position.distance_to(live_spot) if wrist_position.is_finite() and live_spot.is_finite() else INF
|
||||
var suffix: String = "Left" if _use_hand == Hand.LEFT else "Right"
|
||||
var upper: Bone2D = _rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm" % suffix)) as Bone2D
|
||||
var lower: Bone2D = _rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm/%sLowerArm" % [suffix, suffix])) as Bone2D
|
||||
if upper != null:
|
||||
details["shoulder_world"] = upper.global_position
|
||||
if lower != null:
|
||||
details["elbow_world"] = lower.global_position
|
||||
print("[UseReachTrace] %s" % str(details))
|
||||
|
||||
|
||||
func _clear_use_state() -> void:
|
||||
_use_prop = null
|
||||
_use_spot_id = ""
|
||||
_use_planned_spot_world = Vector2.INF
|
||||
_use_approach_root = Vector2.ZERO
|
||||
_use_approach_replans = 0
|
||||
_use_reach_elapsed = 0.0
|
||||
_use_recover_elapsed = 0.0
|
||||
_use_marker_start = Vector2.ZERO
|
||||
_use_marker_recover_start = Vector2.ZERO
|
||||
|
||||
|
||||
func _begin_pickup(prop_id: String, requested_hand: String, action: Dictionary) -> bool:
|
||||
|
||||
@@ -183,6 +183,8 @@ signal queue_finished # queue ran to completion (not
|
||||
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)
|
||||
signal use_completed(prop: PropBlock, spot_id: String, world_position: Vector2)
|
||||
signal use_failed(reason_code: String, prop: PropBlock, spot_id: String)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
@@ -262,6 +264,8 @@ func _ready() -> void:
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("configure", self, _locomotion_component, Callable())
|
||||
_prop_interactor_component.connect("prop_event", _on_prop_interaction_event)
|
||||
_prop_interactor_component.connect("use_completed", _on_prop_use_completed)
|
||||
_prop_interactor_component.connect("use_failed", _on_prop_use_failed)
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.configure(self, _locomotion_component, _prop_interactor_component)
|
||||
_action_runner_component.action_queue = _pending_action_queue
|
||||
@@ -532,6 +536,12 @@ func cancel_prop_action() -> void:
|
||||
_prop_interactor_component.call("cancel_action")
|
||||
|
||||
|
||||
func get_held_prop() -> PropBlock:
|
||||
if _prop_interactor_component == null:
|
||||
return null
|
||||
return _prop_interactor_component.call("get_held_prop") as PropBlock
|
||||
|
||||
|
||||
func preview_prop_throw(target: Vector2, queue_prefix: Array[Dictionary] = []) -> Dictionary:
|
||||
if _prop_interactor_component == null:
|
||||
return {}
|
||||
@@ -544,6 +554,12 @@ func preview_prop_set_down(target: Vector2, queue_prefix: Array[Dictionary] = []
|
||||
return _prop_interactor_component.call("preview_set_down", target, queue_prefix)
|
||||
|
||||
|
||||
func preview_prop_use(prop: PropBlock, spot_id: String, actor_root: Vector2 = Vector2.INF, predicted_facing_left: bool = false) -> Dictionary:
|
||||
if _prop_interactor_component == null:
|
||||
return {"known": true, "valid": false, "reason_code": "out_of_reach"}
|
||||
return _prop_interactor_component.call("preview_use", prop, spot_id, actor_root, predicted_facing_left)
|
||||
|
||||
|
||||
func reset_prop_interactions_for_scene_exit() -> void:
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("reset_for_scene_exit")
|
||||
@@ -553,6 +569,14 @@ func _on_prop_interaction_event(event_type: String, prop: PropBlock, reason: Str
|
||||
prop_interaction_event.emit(event_type, prop, reason)
|
||||
|
||||
|
||||
func _on_prop_use_completed(prop: PropBlock, spot_id: String, world_position: Vector2) -> void:
|
||||
use_completed.emit(prop, spot_id, world_position)
|
||||
|
||||
|
||||
func _on_prop_use_failed(reason_code: String, prop: PropBlock, spot_id: String) -> void:
|
||||
use_failed.emit(reason_code, prop, spot_id)
|
||||
|
||||
|
||||
## Public stand-up request. No-op unless the rig is in RAGDOLL.
|
||||
func request_recovery() -> void:
|
||||
if state == RigState.RAGDOLL and _ragdoll_component != null:
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
class_name StickmanUseReachSolver
|
||||
extends RefCounted
|
||||
## Shared, nonmutating reach estimate for DIRECT previews and PLAY actions.
|
||||
|
||||
const STAGE_PHYSICS := preload("res://scripts/stage/stage_physics_policy.gd")
|
||||
const APPROACH_GAP: float = 110.0
|
||||
const CONTACT_TOLERANCE: float = 34.0
|
||||
const MAX_ROUTE_LENGTH: float = 5000.0
|
||||
|
||||
|
||||
static func solve(rig: StickmanRig, prop: PropBlock, spot_id: String, actor_root: Vector2, predicted_facing_left: bool, _options: Dictionary = {}) -> Dictionary:
|
||||
var result: Dictionary = {"known": true, "valid": false, "reason_code": "out_of_reach", "actor_root": actor_root, "spot_world": Vector2.INF, "approach_root": actor_root, "approach_feet": Vector2.INF, "facing_left": predicted_facing_left, "hand": 1, "reach_start": actor_root, "reach_end": Vector2.INF}
|
||||
if rig == null or not is_instance_valid(rig) or not actor_root.is_finite():
|
||||
result["reason_code"] = "out_of_reach"
|
||||
return result
|
||||
if not _transform_is_finite_invertible(rig.global_transform):
|
||||
result["reason_code"] = "out_of_reach"
|
||||
return result
|
||||
if not _has_ground_support(rig, actor_root):
|
||||
result["reason_code"] = "route_blocked"
|
||||
return result
|
||||
if prop == null or not is_instance_valid(prop) or prop.is_queued_for_deletion():
|
||||
result["reason_code"] = "prop_missing"
|
||||
return result
|
||||
if not _transform_is_finite_invertible(prop.global_transform):
|
||||
result["reason_code"] = "spot_missing"
|
||||
return result
|
||||
if prop.get_use_spot(spot_id).is_empty():
|
||||
result["reason_code"] = "spot_missing"
|
||||
return result
|
||||
var held_prop: PropBlock = rig.get_held_prop() if rig.has_method("get_held_prop") else null
|
||||
if held_prop != null and is_instance_valid(held_prop):
|
||||
result["reason_code"] = "hands_full"
|
||||
return result
|
||||
if prop.interaction_owner_id != 0 or prop.interaction_reserved_by != 0:
|
||||
result["reason_code"] = "prop_held"
|
||||
return result
|
||||
var spot_world: Vector2 = prop.use_spot_world_position(spot_id)
|
||||
if not spot_world.is_finite():
|
||||
result["reason_code"] = "spot_missing"
|
||||
return result
|
||||
result["spot_world"] = spot_world
|
||||
result["reach_end"] = spot_world
|
||||
var candidates: Array[Dictionary] = []
|
||||
var saw_ground_route: bool = false
|
||||
var saw_clear_standing: bool = false
|
||||
for hand_value: int in [0, 1]:
|
||||
var current_wrist: Vector2 = _wrist_world(rig, hand_value)
|
||||
var shoulder: Vector2 = _shoulder_world(rig, hand_value)
|
||||
var arm_reach: float = _arm_reach(rig, hand_value)
|
||||
if not current_wrist.is_finite() or not shoulder.is_finite() or arm_reach <= 0.0:
|
||||
continue
|
||||
# Reach is measured from the shoulder joint. The wrist's current pose may
|
||||
# be far from the spot even though IK can rotate the arm to touch it; this
|
||||
# is especially important after locomotion changes the hand-marker pose.
|
||||
if shoulder.distance_to(spot_world) <= arm_reach and _hand_path_is_clear(rig, prop, shoulder, spot_world) and _has_ground_support(rig, actor_root) and _standing_clear(rig, actor_root):
|
||||
candidates.append({"root": actor_root, "hand": hand_value, "facing_left": predicted_facing_left, "wrist": current_wrist, "distance": 0.0})
|
||||
continue
|
||||
for side: float in [-1.0, 1.0]:
|
||||
var approach_root: Vector2 = Vector2(spot_world.x + side * APPROACH_GAP, actor_root.y)
|
||||
var facing_left: bool = approach_root.x > spot_world.x
|
||||
var shoulder_offset: Vector2 = _shoulder_offset(rig, hand_value, predicted_facing_left)
|
||||
var wrist_offset: Vector2 = _wrist_offset(rig, hand_value, predicted_facing_left)
|
||||
if predicted_facing_left != facing_left:
|
||||
shoulder_offset.x = -shoulder_offset.x
|
||||
wrist_offset.x = -wrist_offset.x
|
||||
var candidate_shoulder: Vector2 = approach_root + shoulder_offset
|
||||
var candidate_wrist: Vector2 = approach_root + wrist_offset
|
||||
if candidate_shoulder.distance_to(spot_world) > arm_reach or not _hand_path_is_clear(rig, prop, candidate_shoulder, spot_world):
|
||||
continue
|
||||
var route: Dictionary = _grounded_route(rig, actor_root, approach_root)
|
||||
if not bool(route.get("valid", false)):
|
||||
continue
|
||||
saw_ground_route = true
|
||||
if not _has_ground_support(rig, approach_root) or not _standing_clear(rig, approach_root):
|
||||
continue
|
||||
saw_clear_standing = true
|
||||
candidates.append({"root": approach_root, "hand": hand_value, "facing_left": facing_left, "wrist": candidate_wrist, "distance": float(route.get("distance", INF))})
|
||||
if candidates.is_empty():
|
||||
if not saw_ground_route and not _has_any_ground_route(rig, actor_root, spot_world):
|
||||
result["reason_code"] = "route_blocked"
|
||||
elif saw_ground_route and not saw_clear_standing:
|
||||
result["reason_code"] = "no_standing_room"
|
||||
else:
|
||||
result["reason_code"] = "out_of_reach"
|
||||
return result
|
||||
candidates.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return float(a.get("distance", INF)) < float(b.get("distance", INF))
|
||||
)
|
||||
var chosen: Dictionary = candidates[0]
|
||||
var approach_root: Vector2 = chosen["root"]
|
||||
result["valid"] = true
|
||||
result["reason_code"] = ""
|
||||
result["approach_root"] = approach_root
|
||||
result["approach_feet"] = approach_root - StickmanLocomotion.FOOT_OFFSET
|
||||
result["facing_left"] = bool(chosen["facing_left"])
|
||||
result["hand"] = int(chosen["hand"])
|
||||
result["reach_start"] = chosen["wrist"]
|
||||
return result
|
||||
|
||||
|
||||
static func _wrist_world(rig: StickmanRig, hand_value: int) -> Vector2:
|
||||
var suffix: String = "Left" if hand_value == 0 else "Right"
|
||||
var lower_visual: Node2D = rig.get_node_or_null(NodePath("Body/%sLowerArm" % suffix)) as Node2D
|
||||
var lower_bone: Bone2D = rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm/%sLowerArm" % [suffix, suffix])) as Bone2D
|
||||
if lower_visual == null or lower_bone == null:
|
||||
var hand_marker: Marker2D = rig.get_node_or_null(NodePath("IK_Targets/%s_Hand" % suffix)) as Marker2D
|
||||
return hand_marker.global_position if hand_marker != null else Vector2.INF
|
||||
return lower_visual.to_global(Vector2(0.0, lower_bone.length))
|
||||
|
||||
|
||||
static func _shoulder_world(rig: StickmanRig, hand_value: int) -> Vector2:
|
||||
var suffix: String = "Left" if hand_value == 0 else "Right"
|
||||
var upper: Bone2D = rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm" % suffix)) as Bone2D
|
||||
return upper.global_position if upper != null else rig.global_position
|
||||
|
||||
|
||||
static func _arm_reach(rig: StickmanRig, hand_value: int) -> float:
|
||||
var suffix: String = "Left" if hand_value == 0 else "Right"
|
||||
var upper: Bone2D = rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm" % suffix)) as Bone2D
|
||||
var lower: Bone2D = rig.get_node_or_null(NodePath("Skeleton2D/Torso/%sUpperArm/%sLowerArm" % [suffix, suffix])) as Bone2D
|
||||
if upper == null or lower == null:
|
||||
return 0.0
|
||||
if not is_finite(upper.length) or not is_finite(lower.length):
|
||||
return 0.0
|
||||
# Bone lengths are authored in local space. Measure their world-space
|
||||
# endpoints so resized, mirrored, and nonuniformly scaled rigs use the same
|
||||
# reach that the IK solver can actually produce.
|
||||
var shoulder: Vector2 = upper.global_position
|
||||
var elbow: Vector2 = lower.global_position
|
||||
var wrist: Vector2 = _wrist_world(rig, hand_value)
|
||||
if not shoulder.is_finite() or not elbow.is_finite() or not wrist.is_finite():
|
||||
return 0.0
|
||||
return shoulder.distance_to(elbow) + elbow.distance_to(wrist)
|
||||
|
||||
|
||||
static func _transform_is_finite_invertible(value: Transform2D) -> bool:
|
||||
return value.origin.is_finite() and value.x.is_finite() and value.y.is_finite() and is_finite(value.determinant()) and absf(value.determinant()) > 0.0001
|
||||
|
||||
|
||||
static func _shoulder_offset(rig: StickmanRig, hand_value: int, facing_left: bool) -> Vector2:
|
||||
var local: Vector2 = rig.to_local(_shoulder_world(rig, hand_value))
|
||||
if rig.get_facing_profile() == StickmanRig.FacingProfile.LEFT:
|
||||
local.x = -local.x
|
||||
if facing_left:
|
||||
local.x = -local.x
|
||||
return rig.global_transform.basis_xform(local)
|
||||
|
||||
|
||||
static func _wrist_offset(rig: StickmanRig, hand_value: int, facing_left: bool) -> Vector2:
|
||||
var local: Vector2 = rig.to_local(_wrist_world(rig, hand_value))
|
||||
if rig.get_facing_profile() == StickmanRig.FacingProfile.LEFT:
|
||||
local.x = -local.x
|
||||
if facing_left:
|
||||
local.x = -local.x
|
||||
return rig.global_transform.basis_xform(local)
|
||||
|
||||
|
||||
static func _hand_path_is_clear(rig: StickmanRig, prop: PropBlock, start: Vector2, finish: Vector2) -> bool:
|
||||
if not start.is_finite() or not finish.is_finite():
|
||||
return false
|
||||
var state: PhysicsDirectSpaceState2D = rig.get_world_2d().direct_space_state
|
||||
if state == null:
|
||||
return true
|
||||
var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(start, finish)
|
||||
query.collision_mask = STAGE_PHYSICS.TERRAIN_LAYER | STAGE_PHYSICS.ACTOR_BLOCKING_PROP_LAYER
|
||||
var excluded: Array[RID] = rig.get_collision_body_rids()
|
||||
if prop.get_rid().is_valid():
|
||||
excluded.append(prop.get_rid())
|
||||
query.exclude = excluded
|
||||
return state.intersect_ray(query).is_empty()
|
||||
|
||||
|
||||
static func _grounded_route(rig: StickmanRig, from_root: Vector2, to_root: Vector2) -> Dictionary:
|
||||
var locomotion: StickmanLocomotion = rig.get_locomotion()
|
||||
if locomotion == null or locomotion._nav_agent == null:
|
||||
return {"valid": false, "distance": INF}
|
||||
var nav: NavigationAgent2D = locomotion._nav_agent
|
||||
var map: RID = nav.get_navigation_map()
|
||||
if not map.is_valid() or NavigationServer2D.map_get_iteration_id(map) == 0:
|
||||
return {"valid": false, "distance": INF}
|
||||
var from_feet: Vector2 = from_root - StickmanLocomotion.FOOT_OFFSET
|
||||
var to_feet: Vector2 = to_root - StickmanLocomotion.FOOT_OFFSET
|
||||
var path: PackedVector2Array = NavigationServer2D.map_get_path(map, from_feet, to_feet, true)
|
||||
if path.is_empty() or path[0].distance_to(from_feet) > 180.0 or path[path.size() - 1].distance_to(to_feet) > 180.0:
|
||||
return {"valid": false, "distance": INF}
|
||||
if not _actor_path_is_clear(rig, from_feet, path, from_root - rig.global_position):
|
||||
return {"valid": false, "distance": INF}
|
||||
var distance: float = 0.0
|
||||
for index: int in range(1, path.size()):
|
||||
distance += path[index - 1].distance_to(path[index])
|
||||
if distance > MAX_ROUTE_LENGTH:
|
||||
return {"valid": false, "distance": INF}
|
||||
return {"valid": true, "distance": distance}
|
||||
|
||||
|
||||
static func _actor_path_is_clear(rig: StickmanRig, from_feet: Vector2, path: PackedVector2Array, initial_offset: Vector2) -> bool:
|
||||
var state: PhysicsDirectSpaceState2D = rig.get_world_2d().direct_space_state
|
||||
if state == null:
|
||||
return false
|
||||
var excluded: Array[RID] = rig.get_collision_body_rids()
|
||||
var bodies: Dictionary = rig.get_collision_bodies()
|
||||
var route_points: PackedVector2Array = PackedVector2Array()
|
||||
route_points.append(from_feet)
|
||||
route_points.append_array(path)
|
||||
var offset: Vector2 = initial_offset
|
||||
for index: int in range(1, route_points.size()):
|
||||
var motion: Vector2 = route_points[index] - route_points[index - 1]
|
||||
if absf(motion.x) <= 0.001:
|
||||
offset += motion
|
||||
continue
|
||||
for part_name: String in ["torso", "head"]:
|
||||
var proxy: AnimatableBody2D = bodies.get(part_name) as AnimatableBody2D
|
||||
if proxy == null or not is_instance_valid(proxy):
|
||||
continue
|
||||
var shape_node: CollisionShape2D = proxy.get_node_or_null(NodePath("CollisionShape2D")) as CollisionShape2D
|
||||
if shape_node == null or shape_node.shape == null:
|
||||
continue
|
||||
var query := PhysicsShapeQueryParameters2D.new()
|
||||
query.shape = shape_node.shape
|
||||
query.transform = shape_node.global_transform
|
||||
query.transform.origin += offset
|
||||
query.motion = motion
|
||||
query.collision_mask = STAGE_PHYSICS.ACTOR_BLOCKING_PROP_LAYER
|
||||
query.exclude = excluded
|
||||
var cast: PackedFloat32Array = state.cast_motion(query)
|
||||
if cast.size() > 0 and cast[0] < 0.999:
|
||||
return false
|
||||
offset += motion
|
||||
return true
|
||||
|
||||
|
||||
static func _has_ground_support(rig: StickmanRig, root_position: Vector2) -> bool:
|
||||
var state: PhysicsDirectSpaceState2D = rig.get_world_2d().direct_space_state
|
||||
if state == null:
|
||||
return false
|
||||
var feet: Vector2 = root_position - StickmanLocomotion.FOOT_OFFSET
|
||||
var query: PhysicsRayQueryParameters2D = PhysicsRayQueryParameters2D.create(feet + Vector2(0.0, -24.0), feet + Vector2(0.0, 88.0))
|
||||
query.collision_mask = STAGE_PHYSICS.TERRAIN_LAYER
|
||||
query.exclude = rig.get_collision_body_rids()
|
||||
var hit: Dictionary = state.intersect_ray(query)
|
||||
return not hit.is_empty() and (hit.get("normal", Vector2.UP) as Vector2).dot(Vector2.UP) >= 0.7
|
||||
|
||||
|
||||
static func _standing_clear(rig: StickmanRig, root_position: Vector2) -> bool:
|
||||
var state: PhysicsDirectSpaceState2D = rig.get_world_2d().direct_space_state
|
||||
if state == null:
|
||||
return false
|
||||
var motion: Vector2 = root_position - rig.global_position
|
||||
var excluded: Array[RID] = rig.get_collision_body_rids()
|
||||
for proxy: AnimatableBody2D in rig.get_collision_bodies().values():
|
||||
if proxy == null or not is_instance_valid(proxy):
|
||||
continue
|
||||
var shape_node: CollisionShape2D = proxy.get_node_or_null(NodePath("CollisionShape2D")) as CollisionShape2D
|
||||
if shape_node == null or shape_node.shape == null:
|
||||
continue
|
||||
var query: PhysicsShapeQueryParameters2D = PhysicsShapeQueryParameters2D.new()
|
||||
query.shape = shape_node.shape
|
||||
var candidate_transform: Transform2D = shape_node.global_transform
|
||||
candidate_transform.origin += motion
|
||||
query.transform = candidate_transform
|
||||
query.collision_mask = STAGE_PHYSICS.ACTOR_LAYER | STAGE_PHYSICS.ACTOR_BLOCKING_PROP_LAYER
|
||||
query.exclude = excluded
|
||||
query.margin = 2.0
|
||||
if not state.intersect_shape(query, 8).is_empty():
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _has_any_ground_route(rig: StickmanRig, actor_root: Vector2, spot_world: Vector2) -> bool:
|
||||
for side: float in [-1.0, 1.0]:
|
||||
var candidate: Vector2 = Vector2(spot_world.x + side * APPROACH_GAP, actor_root.y)
|
||||
var locomotion: StickmanLocomotion = rig.get_locomotion()
|
||||
if locomotion == null or locomotion._nav_agent == null:
|
||||
continue
|
||||
var nav: NavigationAgent2D = locomotion._nav_agent
|
||||
var map: RID = nav.get_navigation_map()
|
||||
if not map.is_valid() or NavigationServer2D.map_get_iteration_id(map) == 0:
|
||||
continue
|
||||
var from_feet: Vector2 = actor_root - StickmanLocomotion.FOOT_OFFSET
|
||||
var to_feet: Vector2 = candidate - StickmanLocomotion.FOOT_OFFSET
|
||||
var path: PackedVector2Array = NavigationServer2D.map_get_path(map, from_feet, to_feet, true)
|
||||
if not path.is_empty() and path[0].distance_to(from_feet) <= 180.0 and path[path.size() - 1].distance_to(to_feet) <= 180.0 and _actor_path_is_clear(rig, from_feet, path, actor_root - rig.global_position):
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://doq1r0f3qgr2y
|
||||
Reference in New Issue
Block a user