Add unique identifier for test_phase3c_walk_recovery.gd
This commit is contained in:
+158
-22
@@ -120,7 +120,7 @@ const REST_LINEAR_THRESHOLD := 5.0
|
||||
const REST_ANGULAR_THRESHOLD := 0.1
|
||||
|
||||
## Duration of the stand-up tween (captured pose -> STAND_POSE).
|
||||
const STAND_UP_DURATION := 0.8
|
||||
const STAND_UP_DURATION := 2.0
|
||||
|
||||
## Extra hold after rest is detected before recovery captures the pose.
|
||||
const STABILIZATION_DELAY := 0.1
|
||||
@@ -169,6 +169,9 @@ const SPEECH_BUBBLE_OFFSET := Vector2(0.0, -640.0)
|
||||
## Debug gate for the Phase 3a walk/runner trace. Ship OFF.
|
||||
const DEBUG_WALK := false
|
||||
|
||||
## Debug gate for the ragdoll-recovery re-solve trace. Ship OFF.
|
||||
const DEBUG_RECOVERY := false
|
||||
|
||||
## Prints a `[walk] `-prefixed message only when DEBUG_WALK is on.
|
||||
func _walk_dbg(msg: String) -> void:
|
||||
if DEBUG_WALK:
|
||||
@@ -290,6 +293,8 @@ var _nodes_ready: bool = false
|
||||
var _skeleton: Skeleton2D = null
|
||||
var _body_container: Node2D = null
|
||||
var _torso_bone: Bone2D = null
|
||||
var _head_bone: Bone2D = null
|
||||
var _head_look_at: SkeletonModification2DLookAt = null
|
||||
var _bend_joint_bones: Dictionary = {} # { String : Bone2D } (lower bones)
|
||||
var _bend_modifications: Dictionary = {} # { String : SkeletonModification2DTwoBoneIK }
|
||||
|
||||
@@ -314,6 +319,8 @@ var _cached_angular_velocity: float = 0.0
|
||||
var _rest_timer: float = 0.0
|
||||
var _stabilize_timer: float = 0.0
|
||||
var _captured_pose: Dictionary = {} # { String : {pos, rot, half} } (rig-local)
|
||||
var _captured_landing_center: Vector2 = Vector2.ZERO # ragdoll torso's world center at capture
|
||||
var _captured_ground_y: float = 0.0 # torso's ground-contact line (center.y + radius)
|
||||
var _stand_up_tween: Tween = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -369,6 +376,9 @@ func _ready() -> void:
|
||||
_torso_bone = _skeleton.get_node_or_null(NodePath("Torso")) as Bone2D
|
||||
if _torso_bone == null:
|
||||
push_warning("StickmanRig: missing 'Torso' bone in Skeleton2D.")
|
||||
_head_bone = _skeleton.get_node_or_null(NodePath("Torso/Head")) as Bone2D
|
||||
if _head_bone == null:
|
||||
push_warning("StickmanRig: missing 'Torso/Head' bone in Skeleton2D.")
|
||||
|
||||
_anim_player = get_node_or_null(NodePath(ANIMATION_PLAYER_PATH)) as AnimationPlayer
|
||||
if _anim_player == null:
|
||||
@@ -381,6 +391,7 @@ func _ready() -> void:
|
||||
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
||||
if stack != null:
|
||||
stack.enabled = true
|
||||
pass
|
||||
else:
|
||||
push_warning("StickmanRig: Skeleton2D has no modification_stack assigned.")
|
||||
|
||||
@@ -421,6 +432,25 @@ func _physics_process(delta: float) -> void:
|
||||
_settle_walk_markers()
|
||||
_update_speech(delta)
|
||||
_update_runner(delta)
|
||||
_pin_mirrored_head_rotation()
|
||||
|
||||
|
||||
|
||||
|
||||
## Re-asserts the head bone's FORWARD canonical aim rotation each frame while
|
||||
## facing LEFT with the kinematic puppet live (ANIMATED or RECOVERING). The
|
||||
## LookAt modification is disabled in that state (_apply_head_lookat_mirror_mode)
|
||||
## so nothing else rewrites the bone; the per-frame pin covers recovery, where
|
||||
## the IK stack is re-enabled mid-tween.
|
||||
func _pin_mirrored_head_rotation() -> void:
|
||||
if facing_profile != FacingProfile.LEFT or state == RigState.RAGDOLL:
|
||||
return
|
||||
if _head_look_at == null or not is_instance_valid(_head_look_at):
|
||||
return
|
||||
if _head_look_at.enabled:
|
||||
_head_look_at.enabled = false
|
||||
if _head_bone != null and is_instance_valid(_head_bone):
|
||||
_head_bone.rotation = 0
|
||||
|
||||
|
||||
func _track_momentum(delta: float) -> void:
|
||||
@@ -562,6 +592,7 @@ func snap_to_standing() -> void:
|
||||
# Re-show the kinematic puppet and re-enable IK.
|
||||
if _skeleton != null and is_instance_valid(_skeleton) and _skeleton.modification_stack != null:
|
||||
_skeleton.modification_stack.enabled = true
|
||||
|
||||
if _body_container != null and is_instance_valid(_body_container):
|
||||
_body_container.visible = true
|
||||
_body_container.modulate.a = 1.0
|
||||
@@ -613,8 +644,12 @@ func _resolve_bend_modifications() -> void:
|
||||
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
||||
if stack == null:
|
||||
return
|
||||
_head_look_at = null
|
||||
for i: int in stack.modification_count:
|
||||
var mod := stack.get_modification(i)
|
||||
if mod is SkeletonModification2DLookAt:
|
||||
_head_look_at = mod as SkeletonModification2DLookAt
|
||||
continue
|
||||
if not (mod is SkeletonModification2DTwoBoneIK):
|
||||
continue
|
||||
var ik := mod as SkeletonModification2DTwoBoneIK
|
||||
@@ -638,21 +673,39 @@ func _apply_profile() -> void:
|
||||
left_leg_bend = BendDirection.INVERTED if bool(flags.get("LeftLeg", false)) else BendDirection.NORMAL
|
||||
right_leg_bend = BendDirection.INVERTED if bool(flags.get("RightLeg", false)) else BendDirection.NORMAL
|
||||
_apply_body_z_order()
|
||||
_apply_head_flip()
|
||||
# Whole-rig Y-axis mirror (spec §9a): facing LEFT mirrors the entire figure
|
||||
# -- head + body + IK targets + mounted Body/* geometry -- so the head AND
|
||||
# body face the correct direction together. RIGHT/FORWARD keep an identity
|
||||
# scale. An X-mirror does not affect depth (draw order), so
|
||||
# Z_ORDER_BY_PROFILE is unchanged.
|
||||
scale = Vector2(-1.0, 1.0) if facing_profile == FacingProfile.LEFT else Vector2(1.0, 1.0)
|
||||
_apply_head_lookat_mirror_mode()
|
||||
facing_profile_changed.emit(int(facing_profile))
|
||||
|
||||
func _apply_head_flip() -> void:
|
||||
var head := get_node_or_null("Skeleton2D/Torso/Head") as Node2D
|
||||
var pivot := get_node_or_null("Skeleton2D/Torso/Head/Pivot") as Node2D
|
||||
if pivot != null:
|
||||
var is_left := (facing_profile == FacingProfile.LEFT)
|
||||
if is_left:
|
||||
# Mirror local X and invert double the bone's rotation to mirror in world space
|
||||
pivot.scale = Vector2(-1.0, 1.0)
|
||||
pivot.rotation = -1.0 * head.rotation
|
||||
else:
|
||||
pivot.scale = Vector2(1.0, 1.0)
|
||||
pivot.rotation = 0.0
|
||||
|
||||
## The SkeletonModification2DLookAt that aims the Head bone is NOT
|
||||
## mirror-invariant: under the whole-rig Y-axis mirror (FacingProfile.LEFT) the
|
||||
## root's negative X scale reflects the head-bone frame, and the LookAt writes a
|
||||
## bone rotation 180 degrees off the FORWARD aim (PI -> 0 headless). Because the
|
||||
## mounted head geometry is offset from Body/Head's local origin (the chin sits
|
||||
## at the neck and the head extends away from it), that 180-degree error flips
|
||||
## the head to hang BELOW the neck instead of sitting above it -- the whole-rig
|
||||
## mirror then displaces the head ~2x its mount offset relative to the torso.
|
||||
## Fix: when facing LEFT, disable the LookAt and pin the head bone to the
|
||||
## FORWARD canonical aim rotation (PI), so the head is a rigid mirror of the
|
||||
## FORWARD pose; RIGHT/FORWARD re-enable the LookAt. The pin is re-asserted
|
||||
## every physics frame while the kinematic puppet is live (ANIMATED/RECOVERING)
|
||||
## because re-enabling the IK stack during recovery would otherwise let the
|
||||
## LookAt flip the bone again.
|
||||
func _apply_head_lookat_mirror_mode() -> void:
|
||||
if _head_look_at == null or not is_instance_valid(_head_look_at):
|
||||
return
|
||||
var mirrored := facing_profile == FacingProfile.LEFT
|
||||
_head_look_at.enabled = false#not mirrored
|
||||
if mirrored and _head_bone != null and is_instance_valid(_head_bone):
|
||||
_head_bone.rotation = PI
|
||||
elif not mirrored:
|
||||
_head_bone.rotation = 0
|
||||
|
||||
## Per-joint setter notify: updates the resolved TwoBoneIK mod's
|
||||
## flip_bend_direction and emits bend_flag_changed. No-op before _ready (the
|
||||
@@ -717,6 +770,8 @@ func _enter_ragdoll() -> void:
|
||||
## the real joint ends (hip / wrist / ankle) instead of body midpoints.
|
||||
func _capture_ragdoll_pose() -> void:
|
||||
_captured_pose.clear()
|
||||
_captured_landing_center = Vector2.ZERO
|
||||
_captured_ground_y = 0.0
|
||||
for key: String in _ragdoll_bodies:
|
||||
var body := _ragdoll_bodies[key] as RigidBody2D
|
||||
if body == null or not is_instance_valid(body):
|
||||
@@ -726,17 +781,46 @@ func _capture_ragdoll_pose() -> void:
|
||||
"rot": body.global_rotation - global_rotation,
|
||||
"half": float(body.get_meta("half_height", 0.0)),
|
||||
}
|
||||
# Record the ragdoll torso's world center + its ground-contact line so
|
||||
# recovery can re-anchor the root's feet onto wherever the ragdoll landed.
|
||||
var torso := _ragdoll_bodies.get("torso") as RigidBody2D
|
||||
if torso != null and is_instance_valid(torso):
|
||||
_captured_landing_center = torso.global_position
|
||||
_captured_ground_y = torso.global_position.y + RAGDOLL_TORSO_RADIUS
|
||||
|
||||
|
||||
func _start_recovery() -> void:
|
||||
if state != RigState.RAGDOLL:
|
||||
return
|
||||
_capture_ragdoll_pose()
|
||||
_destroy_ragdoll()
|
||||
_reanchor_root_to_landing()
|
||||
state = RigState.RECOVERING
|
||||
state_changed.emit(int(state))
|
||||
_snap_skeleton_to_pose()
|
||||
_play_stand_up()
|
||||
|
||||
|
||||
## Re-anchors the rig root so the standing figure's feet sit on the ground at
|
||||
## the ragdoll's landing X. The ragdoll bodies live under a world sibling (not
|
||||
## the rig), so the rig root never moved while it fell; without this the
|
||||
## stand-up tween would drag the figure back to the root's pre-ragdoll world
|
||||
## position.
|
||||
func _reanchor_root_to_landing() -> void:
|
||||
if not _captured_pose.has("torso"):
|
||||
return
|
||||
var feet_world := Vector2(_captured_landing_center.x, _captured_ground_y)
|
||||
var old_root := global_position
|
||||
var new_root := feet_world + FOOT_OFFSET
|
||||
global_position = new_root
|
||||
# Re-base the captured (rig-local) pose onto the re-anchored root so the
|
||||
# snap reproduces the ragdoll's world pose, not the pre-ragdoll one.
|
||||
var shift := old_root - new_root
|
||||
for key: String in _captured_pose:
|
||||
var entry: Dictionary = _captured_pose[key]
|
||||
entry["pos"] = entry.get("pos", Vector2.ZERO) + shift
|
||||
|
||||
|
||||
## Kills any in-flight stand-up tween so a re-entry into RAGDOLL starts from a
|
||||
## clean slate.
|
||||
func _cancel_recovery() -> void:
|
||||
@@ -745,6 +829,59 @@ func _cancel_recovery() -> void:
|
||||
_stand_up_tween = null
|
||||
|
||||
|
||||
## Re-enables the Skeleton2D modification stack after RAGDOLL and forces it to
|
||||
## resume solving. A disabled->enabled toggle alone can leave the stack's
|
||||
## internal solve state stale (the classic Godot disable->enable quirk), so we
|
||||
## explicitly re-setup the stack when it reports !is_setup, ensure Skeleton2D
|
||||
## processes internally (required for the stack to execute), and re-assert the
|
||||
## Torso marker's RemoteTransform2D update flags so the hip bone keeps
|
||||
## following IK_Targets/Torso through the stand-up tween.
|
||||
func _rearm_ik_stack() -> void:
|
||||
if _skeleton == null or not is_instance_valid(_skeleton):
|
||||
return
|
||||
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
||||
if stack != null:
|
||||
if not stack.get_is_setup():
|
||||
stack.setup()
|
||||
stack.enabled = true
|
||||
_skeleton.set_process_internal(true)
|
||||
# Torso marker -> Torso bone driver: ensure it forwards position/rotation.
|
||||
var torso_marker := _get_ik_marker("Torso")
|
||||
if torso_marker != null:
|
||||
var driver := torso_marker.get_node_or_null("RemoteTransform2D") as RemoteTransform2D
|
||||
if driver != null:
|
||||
driver.update_position = true
|
||||
driver.update_rotation = true
|
||||
driver.update_scale = true
|
||||
_recovery_dbg()
|
||||
|
||||
|
||||
## Logs the IK stack + bone-following state during recovery (off by default).
|
||||
func _recovery_dbg() -> void:
|
||||
if not DEBUG_RECOVERY:
|
||||
return
|
||||
var stack: SkeletonModificationStack2D = null
|
||||
var stack_enabled := false
|
||||
var stack_setup := false
|
||||
var internal := false
|
||||
if _skeleton != null and is_instance_valid(_skeleton):
|
||||
stack = _skeleton.modification_stack
|
||||
stack_enabled = stack != null and stack.enabled
|
||||
stack_setup = stack != null and stack.get_is_setup()
|
||||
internal = _skeleton.is_processing_internal()
|
||||
var torso_bone_pos := Vector2.ZERO
|
||||
if _torso_bone != null and is_instance_valid(_torso_bone):
|
||||
torso_bone_pos = _torso_bone.global_position
|
||||
var torso_marker := _get_ik_marker("Torso")
|
||||
var torso_marker_pos := torso_marker.global_position if torso_marker != null else Vector2.ZERO
|
||||
var limb := _bend_joint_bones.get("LeftLeg") as Bone2D
|
||||
var limb_pos := limb.global_position if limb != null and is_instance_valid(limb) else Vector2.ZERO
|
||||
print("[recovery] stack.enabled=%s is_setup=%s internal=%s torso_bone=%s torso_marker=%s left_lower_leg=%s" % [
|
||||
str(stack_enabled), str(stack_setup), str(internal),
|
||||
str(torso_bone_pos), str(torso_marker_pos), str(limb_pos),
|
||||
])
|
||||
|
||||
|
||||
## Marker-driven kinematic snap: writes the captured pose onto the 6 IK-target
|
||||
## markers (NOT the Torso Bone2D, which is slaved to its marker via
|
||||
## RemoteTransform2D), then re-enables IK so TwoBoneIK solves the limbs toward
|
||||
@@ -786,8 +923,7 @@ func _snap_skeleton_to_pose() -> void:
|
||||
if _body_container != null and is_instance_valid(_body_container):
|
||||
_body_container.visible = true
|
||||
_body_container.modulate.a = 1.0
|
||||
if _skeleton != null and is_instance_valid(_skeleton) and _skeleton.modification_stack != null:
|
||||
_skeleton.modification_stack.enabled = true
|
||||
_rearm_ik_stack()
|
||||
|
||||
|
||||
func _set_marker_from_body(marker_name: String, body_key: String) -> void:
|
||||
@@ -814,7 +950,7 @@ func _get_ik_marker(name: String) -> Marker2D:
|
||||
## (sine ease-in-out). No baked animation — a fixed first keyframe can never
|
||||
## match an arbitrary ragdoll rest pose, so the tween starts from wherever the
|
||||
## snap left the markers.
|
||||
func _play_stand_up() -> void:
|
||||
func _play_stand_up() -> void:
|
||||
_stand_up_tween = _tween_markers_to(STAND_POSE, STAND_UP_DURATION)
|
||||
if _stand_up_tween != null:
|
||||
_stand_up_tween.finished.connect(_on_stand_up_finished)
|
||||
@@ -842,6 +978,7 @@ func _tween_markers_to(target_pose: Dictionary, duration: float) -> Tween:
|
||||
func _on_stand_up_finished() -> void:
|
||||
if _skeleton != null and is_instance_valid(_skeleton) and _skeleton.modification_stack != null:
|
||||
_skeleton.modification_stack.enabled = true
|
||||
pass
|
||||
if _body_container != null and is_instance_valid(_body_container):
|
||||
_body_container.visible = true
|
||||
_body_container.modulate.a = 1.0
|
||||
@@ -1052,15 +1189,14 @@ func walk_to(target: Vector2, speed: float = -1.0) -> void:
|
||||
])
|
||||
|
||||
var dx := target.x - global_position.x
|
||||
var anim_name: String
|
||||
if dx < -0.5:
|
||||
set_facing_profile(FacingProfile.LEFT)
|
||||
anim_name = "walk_left"
|
||||
elif dx > 0.5:
|
||||
set_facing_profile(FacingProfile.RIGHT)
|
||||
anim_name = "walk_right"
|
||||
else:
|
||||
anim_name = "walk_right"
|
||||
# Always play the canonical walk_right clip. For LEFT the rig root is
|
||||
# X-mirrored (whole-rig Y-axis mirror) and the same clip plays mirrored;
|
||||
# walk_left is no longer used at runtime.
|
||||
var anim_name := "walk_right"
|
||||
if _anim_player != null and is_instance_valid(_anim_player) and _anim_player.has_animation(anim_name):
|
||||
_anim_player.play(anim_name)
|
||||
_walking = true
|
||||
|
||||
Reference in New Issue
Block a user