Refactor ragdoll recovery system: implement joint bend limits and recovery path overrides

- Updated `DEBUG_RECOVERY` to false for production.
- Introduced `RAGDOLL_JOINT_BAND_LIMITS` for customizable joint bend limits based on facing profile.
- Added `RECOVERY_LEG_SIDE_FORCE` for manual control of leg IK bend direction during recovery.
- Implemented `RECOVERY_JOINT_PATH` to allow optional tween path overrides for elbow/knee recovery.
- Removed debug print statements related to recovery visuals to clean up output.
- Adjusted `_apply_ragdoll_joint_limits` to utilize new bend limits configuration.
- Enhanced documentation in `ragdoll-cleanup.md` to reflect changes in the recovery pipeline and tuning tables.
This commit is contained in:
2026-09-10 01:09:30 -04:00
parent 699b623be3
commit 63c64bd529
5 changed files with 276 additions and 164 deletions
+87 -48
View File
@@ -235,7 +235,16 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
NORMAL/INVERTED/INVERTED/NORMAL = FORWARD). Recovery exports: `rest_timeout` (2.0 s),
`auto_recover` (true), plus recovery constants
`STAND_POSE`/`IK_TARGET_PATHS`/`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD`/
`STAND_UP_DURATION`/`STABILIZATION_DELAY`/`RAGDOLL_TARGET_SOFTNESS`.
`STAND_UP_DURATION`/`STABILIZATION_DELAY`/`RAGDOLL_TARGET_SOFTNESS`. The
**Joint bend configuration (EDIT ME)** block holds every per-joint bend decision of the
ragdoll cycle, keyed by facing profile, so joints can be tuned without touching pipeline
code: `RAGDOLL_JOINT_BAND_LIMITS[profile][joint] = {"lower": deg, "upper": deg}` (the
fall's knee/elbow `PinJoint2D` band — 0 = straight, small side ~5 resists hyperextension,
large side ~150 is the fold; missing entries fall back to the `RAGDOLL_JOINTS` `limit`
string), `RECOVERY_LEG_SIDE_FORCE[profile][leg]` (optional leg IK `flip_bend_direction`
override; missing = match the captured fall pose's knee side), and
`RECOVERY_JOINT_PATH[profile][joint]` (optional `"shortest"` elbow/knee tween-path
override; missing = pass through straight when the shortest path would cross ±180°).
Signals `facing_profile_changed(profile)` /
`bend_flag_changed(joint, flipped)`. Public API: `set_facing_profile`/`get_facing_profile`,
`set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, and
@@ -283,56 +292,86 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D`
container `"RagdollBodyContainer"` under the rig's **parent** (world root; fallback
`get_tree().current_scene`) and populates it from the `RAGDOLL_BODIES` table (**10**
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
2.0, limb capsules radius 8 masses 1.0–2.0; `collision_layer`/`collision_mask` = 1, bodies
spawn fully visible) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`, one per
non-root body pinned at the child bone's origin, `softness = RAGDOLL_TARGET_SOFTNESS` at
build). Angular limits via `_apply_ragdoll_joint_limits()`: `elbow_knee` folds +CW
`-5°..+150°`, `elbow_knee_ccw` `-150°..+5°`, `shoulder_hip` ±160°, default (neck) free. Cached
momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the
same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
**not** modified.
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
2.0, limb capsules radius 8 masses 1.0–2.0; `collision_layer`/`collision_mask` = 1, bodies
spawn fully visible) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`, one per
non-root body pinned at the child bone's origin, `softness = RAGDOLL_TARGET_SOFTNESS` at
build). Each body clones the rig's own mounted visual geometry into the ragdoll body frame
via `_clone_part_visuals()` (reads the entry's `visual_path` `Body/*` node, copies each
`Line2D`/`Polygon2D` child with points baked into the body's local space), falling back to a
gray capsule / white circle when the part node is missing or has no mounted shapes. Angular
limits via `_apply_ragdoll_joint_limits()`: elbow/knee bands come from the editable
`RAGDOLL_JOINT_BAND_LIMITS` table (per profile, per joint) — FORWARD arms `{-150°, +5°}`,
legs `{-5°, +150°}`; LEFT/RIGHT override both sides (e.g. locked `{60°, 100°}`); the
`elbow_knee`/`elbow_knee_ccw` `limit` strings are only the fallback for joints missing from
the table. Shoulder/hip ±160°, neck free. Cached
momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the
same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
**not** modified.
- **Phase 11 instant handoff + recovery:** `_update_rest_detection()` (only when `state ==
RAGDOLL`)
reads `_ragdoll_bodies["torso"]`: when its linear/angular velocity drops below
`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD` it accumulates `_rest_timer`; after
`rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover` on it calls
`_start_recovery()`. `_start_recovery()` (also `request_recovery()`, no-op unless in
RAGDOLL) captures the 10 bodies' rig-local `{pos, rot, half}` into `_captured_pose` (`half` =
each capsule's half-length from build-time metadata) plus a landing anchor
(`_captured_landing_center` = the ragdoll torso's world center, `_captured_ground_y` =
`torso.center.y + RAGDOLL_TORSO_RADIUS`), `_reanchor_root_to_landing()`s (translates the rig
root so the standing figure's **feet** sit on the ground at the landing X — `feet =
(_captured_landing_center.x, _captured_ground_y)`, `new_root = feet + FOOT_OFFSET` — and
re-bases the captured rig-local positions by the root shift; the figure stands up **in place,
on the ground**, where the ragdoll landed, since the ragdoll bodies live under a world sibling
and the root never moved while it fell), `_destroy_ragdoll()`s,
sets `state = RECOVERING` + emits, then `_snap_skeleton_to_pose()` — a **marker-driven** snap
writing `IK_Targets/Torso.position`/`.rotation`, `IK_Targets/Head.position`, and the 4 limb
markers (never the slaved `Torso` Bone2D), re-showing
`Body/*`, then calling `_rearm_ik_stack()` — a defensive re-setup (re-sets up the
`SkeletonModificationStack2D` when it reports `!get_is_setup()`, re-asserts `enabled = true`
and `Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker's
`RemoteTransform2D` update-position/rotation/scale flags) so the stack reliably resumes solving
toward the end-effectors after a disable→enable toggle. Runtime diagnosis of the re-arm is
gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces stack
enabled/setup/internal + bone-following state). Snap
geometry: the ragdoll capsules span joint origin→tip along their +X, so the **hip** is derived
as `torso.pos − spine_dir·half` and the wrist/ankle targets as `lower_body.pos + dir·half`;
the Torso marker rotation subtracts the Torso Bone2D's `bone_angle` (bone world angle =
marker rotation + bone_angle — copying the body rotation directly would slam the skeleton −90°
and lay it flat).
`_play_stand_up()` then tweens the 6 markers **directly** from their captured values to
`STAND_POSE` over `STAND_UP_DURATION` (2.0 s, sine ease-in-out, `_tween_markers_to()`); the baked
`"stand_up"` animation is **not** played (a fixed first keyframe can never match an arbitrary
ragdoll rest pose, so recovery starts from wherever the snap left the markers). On tween
finish `_on_stand_up_finished()` re-enables IK, re-shows `Body/*`, sets `state = ANIMATED`,
and emits. Interruptible: `set_ragdoll(true)`
during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll;
`set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated
`set_ragdoll` calls are idempotent. `is_in_ragdoll()` stays
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman").
`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD` (or it is sleeping) it accumulates
`_rest_timer`; after `rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover`
on it calls `_start_recovery()`.
- **Pre-ragdoll capture:** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`,
which snapshots, into `_pre_ragdoll_state`, the 6 IK marker values, the 8 limb bone
rotations, the head bone rotation, the facing profile, the 4 live bend flags, the head
LookAt enabled state, per-leg knee side + flag (`leg_sides`), and the chain-space entry
offsets `torso_chain_offset` / `head_chain_offset` — captured at entry so the snap needs
**no authored geometry constants** and the LEFT mirror's ambiguous `global_rotation`
decomposition (±π) cancels out. This snapshot is the recovery **END target** (possibly a
mid-walk pose; the animation is not resumed).
- **Recovery capture + landing:** `_start_recovery()` (also `request_recovery()`, no-op
unless in RAGDOLL) calls `_capture_ragdoll_pose()` — records the 10 bodies' rig-local
`{pos, rot, half}` (`half` = each capsule's half-length from build-time metadata) and
probes the terrain for the ground surface under the fallen figure via `_probe_ground_y()`
(a **7-ray fan** around the landing X, `GROUND_PROBE_START_OFFSET`/`END_OFFSET`,
`GROUND_PROBE_TOLERANCE` band, ragdoll RIDs excluded via `query.exclude`); the probe
context is retained in `_probe_top_y`/`_probe_bottom_y`/`_probe_exclude` for the ankle
clamp at snap time. It then `_destroy_ragdoll()`s, calls `_reanchor_root_to_landing()`
(translates the rig root so the standing figure's **feet** sit on the ground at the
landing X — `feet = (_captured_landing_center.x, _captured_ground_y)`, `new_root = feet +
FOOT_OFFSET` — and re-bases the captured rig-local positions with a **mirror-aware local
shift** `local_shift = to_local(shift) − to_local(Vector2.ZERO)`, so the re-base survives
the LEFT X-mirror), sets `state = RECOVERING` + emits.
- **Snap:** `_snap_skeleton_to_pose()` writes the captured pose: the torso marker via
`to_local(center_world − spine_dir·half)` with rotation `chain(spine) − torso_chain_offset`;
the limb bones `rot_u = chain(u) − θ_t − bone_angle_u` and
`rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l`; the head via
`chain(head) − head_chain_offset − θ_t`. All snapped rotations are normalized into
`(−π, π]` because Godot's `Skeleton2D` re-wraps bone rotations internally (which would
otherwise corrupt the tween's captured start). The ankle markers are clamped up onto the
terrain surface via a per-ankle `_probe_ground_y()` at each ankle's X. The snap also
computes and stores the per-segment tween targets in `_recovery_bones`. It re-shows
`Body/*`, calls `_rearm_ik_stack()` (a defensive re-setup — re-runs `setup()` when the
stack reports `!get_is_setup()`, re-asserts `enabled = true` and
`Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker's
`RemoteTransform2D` update flags), then **disables** the 4 arm TwoBoneIK mods + the head
LookAt and **enables** the 2 leg mods (bend side matched to the captured fall pose's knee
side vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`).
- **Stand-up:** `_play_stand_up()` starts a 2.0 s sine-in-out tween (`STAND_UP_DURATION`)
of the 6 markers → their **pre-ragdoll** values and of the arm + head bones to the
`_recovery_bones` targets — upper arm bones take the shortest path, lower arm bones use
`_natural_joint_target(current, target, offset)` (offset = `bone_angle_lower −
bone_angle_upper`; the through-straight rule is evaluated in the joint's rendered space
and a `"shortest"` entry in `RECOVERY_JOINT_PATH` bypasses it). The **legs are IK-driven**
toward their tweening, ground-clamped ankle markers. The arm mods + head LookAt stay
disabled during the tween. The baked `"stand_up"` animation is **not** played (a fixed
first keyframe can never match an arbitrary ragdoll rest pose).
- **Finish:** `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint
bend flags + head LookAt state (`_restore_head_lookat_state()`), re-enables the arm mods,
re-shows `Body/*`, sets `state = ANIMATED`, and emits. The resumed IK solve reproduces the
pre-ragdoll pose exactly (the end pose matches the pre-ragdoll frame, including mid-walk
poses).
- `_pin_mirrored_head_rotation()` pins the head only while `ANIMATED`. Interruptible:
`set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll;
`set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated
`set_ragdoll` calls are idempotent. `is_in_ragdoll()` stays
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman"). Runtime diagnosis is
gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces the
stack + bone-following state — the temporary per-step debug prints have been stripped).
- **Phase 3a director functionality:** adds navigation/walking, speech, an action queue, and a
queue-runner state machine. Enums `RunnerState { IDLE, EXECUTING }` and `ActionPhase { NONE,
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`