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)`
+15 -4
View File
@@ -381,13 +381,23 @@ A **unified live-update setter** drives geometry to all children live — a chan
Ragdoll bodies spawn fully visible — the entry handoff is instant (the ragdoll is built at the current solved pose, so there is nothing to fade).
**Angular limits** (`_apply_ragdoll_joint_limits()`): elbows/knees fold **only** — the natural bend goes toward +CW (`-5°..+150°`) or its mirrored CCW variant (`-150°..+5°`) depending on limb side/facing, so limbs never hyperextend; shoulders/hips allow ±160°; the neck is free (`angular_limit_enabled = false`).
**Angular limits** (`_apply_ragdoll_joint_limits()`): elbows/knees get their band from the editable per-profile, per-joint `RAGDOLL_JOINT_BAND_LIMITS` table — a `{ "lower": deg, "upper": deg }` range for the joint's allowed relative angle (0 = straight; a small side ~5 resists hyperextension, a large side ~150 is the fold, and a narrow/locked band such as `{60, 100}` forces a permanently-bent limb). FORWARD arms `{-150°, +5°}`, legs `{-5°, +150°}`; LEFT/RIGHT override both sides. Joints missing from the table fall back to the `RAGDOLL_JOINTS` `limit` string (`elbow_knee` `-5°..+150°`, `elbow_knee_ccw` `-150°..+5°`). Shoulders/hips allow ±160°; the neck is free (`angular_limit_enabled = false`).
**Joint bend configuration (EDIT ME):** every per-joint bend *decision* in the ragdoll cycle lives in three `const` tables at the top of `scripts/stickman_rig.gd`, keyed by facing profile, so joints can be tuned without touching pipeline code:
| Const | Shape | Purpose |
|---|---|---|
| `RAGDOLL_JOINT_BAND_LIMITS` | `[profile][joint] = { "lower": deg, "upper": deg }` | The fall's knee/elbow `PinJoint2D` angular band (in degrees). Missing entries fall back to the `RAGDOLL_JOINTS` `limit` string. |
| `RECOVERY_LEG_SIDE_FORCE` | `[profile][leg] = bool` | Optional override of the leg IK `flip_bend_direction` during the stand-up (`true` = inverted). A missing key means automatic: match the captured fall pose's knee side. |
| `RECOVERY_JOINT_PATH` | `[profile][joint] = "shortest"` | Optional override of the elbow/knee stand-up tween path. A missing key means automatic: pass through straight (0) when the shortest path would cross ±180° (fully-backward). |
**Instant handoff (Phase 11):** `_enter_ragdoll()` stops the player with `keep_state` (no pose reset), builds the ragdoll from the **current solved bone positions** while the IK stack is still enabled, then hides `Body/*` and disables the IK stack in the same call. There is deliberately **no crossfade** — the ragdoll spawns at exactly the same pose, so a fade would only read as ghosting (an earlier blended transition was removed on director feedback).
**Rest detection (auto-recovery):** while `state == RAGDOLL`, `_update_rest_detection()` reads the **Torso** `RigidBody2D`. When it is sleeping **or** its linear velocity ≤ `REST_LINEAR_THRESHOLD` (**5.0 px/s** — tuned up from the original 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s even when settled) and angular velocity ≤ `REST_ANGULAR_THRESHOLD` (0.1 rad/s), a `_rest_timer` accumulates; after `rest_timeout` (exported, default **2.0 s**) plus a `STABILIZATION_DELAY` (0.1 s) hold, and with `auto_recover` (exported, default **true**) enabled, recovery is triggered. `rest_timeout` and `auto_recover` are runtime-adjustable exports.
**Recovery (`_start_recovery()`, also `request_recovery()`):** `_capture_ragdoll_pose()` records the 10 bodies' rig-local `{pos, rot, half}` into `_captured_pose` (`half` = each capsule's half-length, stored as build-time metadata) **plus** a landing anchor — `_captured_landing_center` (the ragdoll torso's world center) and `_captured_ground_y` (the torso's ground-contact line, `center.y + RAGDOLL_TORSO_RADIUS`). Because the ragdoll bodies spawn under a world sibling, the rig root never moves while the figure falls, so before destroying the ragdoll `_start_recovery()` calls `_reanchor_root_to_landing()`, which 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 that root shift. The figure stands up **in place, on the ground**, where the ragdoll landed — not anchoring its hip to the lying hip (a spine-direction hip sits at ground level when the torso lies flat, which would bury the standing feet ~363 px into the ground), and not back at its pre-ragdoll position. The ragdoll is then destroyed and `state = RECOVERING` is set + emitted. `_snap_skeleton_to_pose()` writes the `IK_Targets/Torso` position + rotation, `IK_Targets/Head`, and the 4 limb markers (never the slaved Torso `Bone2D`), re-shows `Body/*`, then 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 `RemoteTransform2D`'s update-position/rotation/scale flags) so TwoBoneIK reliably resumes solving toward the end-effectors after a disable→enable toggle; its runtime diagnosis is gated behind `const DEBUG_RECOVERY := false` (`_recovery_dbg()`, off by default). 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). The snap therefore reproduces the ragdoll's exact final pose (a "sitting" rest stays sitting). `_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 (the earlier bridge-into-the-animation approach caused a visible jump from the captured pose to the animation's first frame), so the tween 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.
**Recovery (`_start_recovery()`, also `request_recovery()`):** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`, which snapshots 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). `_capture_ragdoll_pose()` records the 10 bodies' rig-local `{pos, rot, half}` into `_captured_pose` (`half` = each capsule's half-length, stored as build-time metadata) **and** probes the terrain under the fallen figure via `_probe_ground_y()` (a **7-ray fan** around the landing X — `GROUND_PROBE_X_OFFSETS`, `GROUND_PROBE_START_OFFSET`/`END_OFFSET`, `GROUND_PROBE_TOLERANCE` band, ragdoll RIDs excluded), keeping the probe context in `_probe_top_y`/`_probe_bottom_y`/`_probe_exclude` for the ankle clamp at snap time. Because the ragdoll bodies spawn under a world sibling, the rig root never moves while the figure falls, so before destroying the ragdoll `_start_recovery()` calls `_reanchor_root_to_landing()`, which 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)` (a raw `+ shift` would land every position off by `2·shift.x` under the LEFT X-mirror). The figure stands up **in place, on the ground**, where the ragdoll landed — not anchoring its hip to the lying hip (a spine-direction hip sits at ground level when the torso lies flat, which would bury the standing feet ~363 px into the ground), and not back at its pre-ragdoll position. The ragdoll is then destroyed and `state = RECOVERING` is set + emitted. `_snap_skeleton_to_pose()` writes the captured pose onto the 6 IK-target markers (never the slaved Torso `Bone2D`): the torso marker position via `to_local(center_world − spine_dir·half)` and rotation `chain(spine) − torso_chain_offset`; the limb bones `rot_u = chain(u) − θ_t − bone_angle_u`, `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. 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 `RemoteTransform2D`'s update-position/rotation/scale flags) so TwoBoneIK reliably resumes solving after a disable→enable toggle — then **disables** the 4 arm TwoBoneIK mods + the head LookAt and **enables** the 2 leg mods, each leg's `flip_bend_direction` matched to the captured fall pose's knee side (vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`). The snap therefore reproduces the ragdoll's exact final pose (a "sitting" rest stays sitting). Runtime diagnosis is gated behind `const DEBUG_RECOVERY := false` (`_recovery_dbg()`, off by default).
`_play_stand_up()` then starts a **2.0 s** sine-in-out tween (`STAND_UP_DURATION`) of the 6 markers → their **pre-ragdoll** captured values and of the arm + head bones to the `_recovery_bones` targets computed by the snap — 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 2 leg mods stay enabled). 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. On tween finish, `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint bend flags + 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).
**Interruptibility:** `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. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is **not** modified (the `stand_up` / `walk_left` / `walk_right` animations are baked into the scene's `AnimationLibrary` by the `create_animations.gd` editor script, which runs manually in the editor; the baked `stand_up` is an authored reference and the recovery path does not play it). The ragdoll bodies spawn under a **world sibling** (not the rig root), so the whole-rig Y-axis mirror does not affect them; `walk_left` remains in the library but is **not used at runtime** — `walk_to()` plays the canonical `walk_right` clip for every direction (root-mirrored for LEFT).
@@ -931,7 +941,7 @@ Behavior:
| `res://scripts/stickman_editor.gd` | Editor controller — File/Edit/View menu actions, save/load/clear, JSON v1.5 serialization with multi-shape/rotation/scale, `part_order`, Phase 8 `proportions`/`pivot`/`length`, and Phase 9 Round 5 per-part `guide_offset` export, `settings.json` load/save, editor-wide shape clipboard (Copy/Paste across panels), broadcast of grid/snap settings to panels, Reset Views, populates panels, coordinates selection across panels. |
| `res://scripts/stk_rig_adapter.gd` | **Phase 8, extended by Phase 9 (Rounds 4–6 bugfix).** Standalone runtime adapter (`class_name StkRigAdapter`, `static func apply(stk_data, rig)`): fits an instantiated `master_rig.tscn` to a loaded `.stk` by re-fitting the 8 limb bones (`Skeleton2D/Torso/...` `Bone2D` lengths + lower-bone origins), recalibrating the IK targets (`IK_Targets/Left|Right_Hand`, `Left|Right_Leg`), and mounting the `.stk` shapes onto the `Body/*` visual nodes (**one node per shape**: closed → single `Polygon2D` fill, open → single `Line2D` width 2). Shape mounting recomputes each part's bounding box at mount time (file `pivot`/`length` are no longer trusted) and derives a mount transform in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) via `_compute_mount_transform()`: the part's preview transform `E(P) = C + R(rot)·S·(P − C)` (rotation + scale about the bbox center — the editor's exact Whole-Stickman-preview transform) is composed **first**, then the anchor/alignment θ/bone-fit scale are computed on the **transformed geometry**; rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint). Anchors (raw family rules): head/torso bottom-center `(cx, max_y)`, left horizontal limbs `(max_x, cy)`, right horizontal limbs `(min_x, cy)`, vertically drawn limbs top-center `(cx, min_y)`; alignment rotation θ maps the far end onto `+Y`; scaling is **anisotropic** — only the **auto-detected drawn long axis** (`width >= height`) scales to the bone length (`bone_length/extent`, guard `extent <= 0.0001` → `1.0`), cross-axis thickness stays 1:1. The `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. (Phase 9 Round 5) when a part dict carries `guide_offset`, the mounted geometry is translated by `t = (guide_offset + (A − C)).rotated(−c_node)`; (Phase 9 Round 6) when `guide_offset` is present, the joint anchor is whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is nearest the part's guide joint (`center − guide_offset`), replacing the per-side family choice + 180° flip heuristic for that case (fixing the lower-left-leg and lower-right-arm, which were mounted 180° off their bones) — old files without the key keep the family rules + flip heuristic as the fallback in the driver's bone frame (A = mount anchor incl. the 180° flip rule, C = raw bbox center, `c_node` = driver `RemoteTransform2D.global_rotation`), so the harness reproduces the editor's guide-relative placement 1:1; old files without the key keep the offset-0 behavior (head falls back to `HEAD_CHIN_DROP`). Each `Body/*` container's scale is reset to `(1,1)` / rotation `0` (position untouched). (Phase 9) also fits the head bone (`Head.position.y = -proportions.torso_length`) while mounting the head as **full geometry** — it clears the head's inline `@tool` circle script and mounts `.stk` head shapes as `Line2D`/`Polygon2D`, and zeroes the Head driver's local position so the chin sits on the neck joint; the head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px). `_mount_shapes()` also handles **v1.0/v1.1 single-shape** part dicts (wraps the part dict as one shape when it carries `points` but no `shapes` array), so older `.stk` files mount as visible geometry instead of being cleared. **Not used by the editor** — consumed by the runtime pipeline. |
| `res://scripts/stickman_factory.gd` | **Phase 9.** Runtime entry point (`class_name StickmanFactory`, `extends RefCounted`); a static factory that turns a `.stk` file into a live, rigged `master_rig.tscn` instance. `load_stk(path)` reads + parses the file (`{}` + `push_warning` on failure); `spawn_from_data(stk_data)` instantiates `res://master_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, and returns the rig root **typed as `StickmanRig`** (the rig now carries the `StickmanRig` root script); `spawn(path)` chains them (`null` on empty data). **Not used by the editor.** |
| `res://scripts/stickman_rig.gd` | **Phase 9 Task 4.** `class_name StickmanRig`, `extends Node2D`; the runtime owner of facing direction, per-joint bone bend, `Body/*` z-order, and (Phase 10/11) the **kinematic-to-ragdoll** state switch with instant handoff + stand-up recovery, attached to the `master_rig.tscn` root `Master`. Exports a `facing_profile` preset (`FacingProfile` LEFT/RIGHT/FORWARD, default FORWARD) and four `@export_enum("Normal","Inverted")` per-joint bend vars (`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend`), plus (Phase 11) `rest_timeout` (2.0 s) and `auto_recover` (true) exports. Non-`@tool`: resolves `Skeleton2D`/`Body`/bend joints at runtime, enables its own modification stack, and applies the profile (flag writes + `Body/*` reorder) in `_ready()` and setters. Signals `facing_profile_changed` / `bend_flag_changed` / `state_changed`; public API `set_facing_profile`/`get_facing_profile`, `set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, `get_bend_joint_global_position()`, plus the ragdoll API `set_ragdoll(enabled)`/`toggle_ragdoll()`/`is_in_ragdoll()`/`request_recovery()` with `state` / `enum RigState { ANIMATED, RAGDOLL, RECOVERING }`. (Phase 4b) `_update_walking()` latches `_walk_mode` once per walk (`LATCH_PROBE_MAX_FRAMES` 6), unifies arrival on the final target at `ARRIVE_DISTANCE` (snap-on-arrive), steers to the final target when close, and re-asserts standing markers one frame after stop — fixing the walk-waypoint arrival jitter. Null-guarded (`push_warning` + skip). **Not used by the editor.** |
| `res://scripts/stickman_rig.gd` | **Phase 9 Task 4.** `class_name StickmanRig`, `extends Node2D`; the runtime owner of facing direction, per-joint bone bend, `Body/*` z-order, and (Phase 10/11) the **kinematic-to-ragdoll** state switch with instant handoff + stand-up recovery, attached to the `master_rig.tscn` root `Master`. Exports a `facing_profile` preset (`FacingProfile` LEFT/RIGHT/FORWARD, default FORWARD) and four `@export_enum("Normal","Inverted")` per-joint bend vars (`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend`), plus (Phase 11) `rest_timeout` (2.0 s) and `auto_recover` (true) exports. Non-`@tool`: resolves `Skeleton2D`/`Body`/bend joints at runtime, enables its own modification stack, and applies the profile (flag writes + `Body/*` reorder) in `_ready()` and setters. Signals `facing_profile_changed` / `bend_flag_changed` / `state_changed`; public API `set_facing_profile`/`get_facing_profile`, `set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, `get_bend_joint_global_position()`, plus the ragdoll API `set_ragdoll(enabled)`/`toggle_ragdoll()`/`is_in_ragdoll()`/`request_recovery()` with `state` / `enum RigState { ANIMATED, RAGDOLL, RECOVERING }`. The recovery cycle is **pre-capture → ragdoll pose capture (7-ray ground probe) → mirror-aware re-anchor → snap (entry-captured chain offsets, `(−π, π]`-normalized bones, ankle clamp) → 2.0 s tween to the pre-ragdoll pose (arms/head bone-driven with `_natural_joint_target` for elbows; legs IK-driven) → finish (profile + bend flags + LookAt restored)**, with every per-joint bend decision editable in the `RAGDOLL_JOINT_BAND_LIMITS` / `RECOVERY_LEG_SIDE_FORCE` / `RECOVERY_JOINT_PATH` tables (Joint bend configuration (EDIT ME)). Diagnostics are gated behind `DEBUG_RECOVERY := false`. (Phase 4b) `_update_walking()` latches `_walk_mode` once per walk (`LATCH_PROBE_MAX_FRAMES` 6), unifies arrival on the final target at `ARRIVE_DISTANCE` (snap-on-arrive), steers to the final target when close, and re-asserts standing markers one frame after stop — fixing the walk-waypoint arrival jitter. Null-guarded (`push_warning` + skip). **Not used by the editor.** |
| `res://scripts/create_animations.gd` | **Phase 11.** `@tool extends EditorScript`; a **standalone editor utility** (run manually with `master_rig.tscn` open; not auto-loaded or referenced at runtime) that supersedes the deleted `scripts/create_walk.gd`. `_run()` bakes `walk_left`/`walk_right` (same keyframes as the old script) and a one-shot `stand_up` (`POSE_DOWN` → `POSE_STANDING`, `STAND_UP_DURATION` 2.0, `loop_mode = LOOP_NONE`) into the open scene's default `AnimationLibrary`. The baked `stand_up` is an **authored reference only** — runtime recovery does not play it (`StickmanRig` tweens the IK targets directly from the captured ragdoll pose, since a fixed first keyframe can never match an arbitrary rest pose). `walk_right` is the **canonical walk** clip; `walk_left` remains baked in the library but is **not used at runtime** (facing is set explicitly and the rig root X-mirrors for LEFT). |
| `res://scripts/test_harness.gd` | **Phase 9.** Standalone staging scene (run via **F6** on `res://scenes/test_harness.tscn`, not wired into the editor) for debugging bone scales, vector-drawing offsets, and IK limits in isolation. Top UI bar: "Open .stk…" / quick-select buttons (`stickmen/break.stk`, `stickmen/basic.stk`, `stickmen/test.stk`), "Show Bones" / "Show IK Handles" toggles, loaded-filename label. `SubViewport` world + enabled `Camera2D` (middle-mouse pan, wheel zoom, recenter on spawn); each load frees the previous rig and spawns a fresh one via `StickmanFactory.spawn()`. A world-space debug overlay draws true bone segments (joint dots + parent→child lines, with limb leaf bones drawn out to their IK targets so wrist/ankle joints are visible; the **Head** leaf is the exception — its target is a LookAt aim point, not a joint, so it draws a ~90 px segment along the bone's own direction instead) and colored IK-target markers (hands green, feet blue, head yellow, torso magenta) plus a semi-transparent yellow head-aim line; the **6** `Marker2D` IK targets are click-draggable — the 4 limb targets flex limbs live via `SkeletonModificationStack2D` TwoBoneIK (the rig self-enables its stack), the Torso target translates the whole rig via its `RemoteTransform2D`, and the Head target drives the head's LookAt aim rotation (Phase 9 Round 7). |
| `res://scenes/test_harness.tscn` | **Phase 9.** Standalone staging scene backing `scripts/test_harness.gd` (run via **F6**; not wired into the editor). |
@@ -1063,4 +1073,5 @@ BodyPartPanel.shape_selected() ---(bound to part_name)---> stickman_editor
> **Phase 10 (Kinematic-to-Ragdoll):** adds a reversible **kinematic-to-ragdoll** state switch to the runtime rig. `StickmanRig` gains `enum RigState { ANIMATED, RAGDOLL }`, `var state: RigState`, `signal state_changed(new_state)`, and the `set_ragdoll(enabled)` / `toggle_ragdoll()` / `is_in_ragdoll()` API. In `RAGDOLL` mode the IK modification stack is disabled, the `AnimationPlayer` stopped, and the `Body/*` visuals hidden; a procedural network of **10** `RigidBody2D` (torso `CapsuleShape2D` mass 8.0, head `CircleShape2D` radius 100, limb capsules radius 8) + **9** `PinJoint2D` (elbow/knee fold-only ±bands, shoulder/hip ±160°, neck free) is built in code and reparented into a `"RagdollBodyContainer"` under the rig's **parent** (world root), layer 1/mask 1 so it collides with terrain and props. The rig root's momentum (tracked in `_physics_process`) is applied to the ragdoll Torso body for a seamless handoff. Exiting frees the ragdoll, re-shows `Body/*`, re-enables IK, and stops the animation. The physics harness toggles via its **Stickman ↔ Ragdoll** button, removing the `RigCollisionProxy` on entry and re-adding it (idempotently) on exit. `master_rig.tscn` is **not** modified.
> **Phase 11 (Instant Handoff & Recovery):** replaces the hard ragdoll entry/exit with an **instant handoff** and adds a **stand-up recovery** path. `StickmanRig` gains `enum RigState { ANIMATED, RAGDOLL, RECOVERING }` plus exports `rest_timeout` (2.0 s) and `auto_recover` (true). On entering `RAGDOLL` the ragdoll is built from the **current solved bone positions** (the player is stopped with `keep_state`), then `Body/*` is hidden and the IK stack disabled in the same call — no crossfade, since the ragdoll spawns at exactly the same pose and a fade would only read as ghosting (an earlier `transition_duration` blend was removed on director feedback). Rest detection reads the Torso body — sleeping, or linear ≤ `REST_LINEAR_THRESHOLD` (5.0 px/s, tuned up from the plan's 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s) and angular ≤ 0.1 rad/s — then after `rest_timeout` + `STABILIZATION_DELAY` (0.1 s) with `auto_recover` on, calls `_start_recovery()`. Recovery captures the 10 bodies' rig-local pose **plus a landing anchor** (`_captured_landing_center` = the ragdoll torso's world center, `_captured_ground_y` = torso `center.y + RAGDOLL_TORSO_RADIUS`), re-anchors the rig root so the standing figure's **feet** sit on the ground at the landing X (`_reanchor_root_to_landing()`, `new_root = feet + FOOT_OFFSET`) so the figure stands up **in place, on the ground**, where the ragdoll landed rather than sliding back to its pre-ragdoll root position (the earlier spine-direction-hip anchor sat at ground level for a lying torso and buried the standing feet), destroys the ragdoll, sets `state = RECOVERING` + emits, snap-solves the skeleton via the **6** IK targets (`IK_Targets/Torso` pos+rot, `IK_Targets/Head`, 4 limb markers — never the slaved Torso `Bone2D`), deriving the **hip** from the torso capsule's bottom end (`pos − dir·half`) and the wrist/ankle targets from the lower-limb capsules' far ends (`pos + dir·half`), with the Torso marker rotation subtracting the Torso bone's `bone_angle` (copying the body rotation directly would slam the skeleton −90° and lay it flat), then `_play_stand_up()` tweens the markers **directly** from their captured values to `STAND_POSE` over `STAND_UP_DURATION` (2.0 s, sine ease-in-out) — the baked `stand_up` animation is **not** played, because a fixed first keyframe can never match an arbitrary ragdoll rest pose (the earlier bridge-into-the-animation approach caused a visible jump); `_on_stand_up_finished()` then returns the rig to `ANIMATED`. `request_recovery()` is public (no-op unless in `RAGDOLL`); `set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll, `set_ragdoll(false)` during `RAGDOLL` routes through recovery, and calls are otherwise idempotent. `is_in_ragdoll()` stays `state == RAGDOLL` (so `RECOVERING` reads as "Stickman"). A new `res://scripts/create_animations.gd` editor script (superseding the deleted `create_walk.gd`) bakes `walk_left`/`walk_right`/the one-shot `stand_up` into `master_rig.tscn`'s `AnimationLibrary` (the baked `stand_up` is an authored reference only — recovery does not play it) — **no `.stk` format change**; `master_rig.tscn` scene nodes are unchanged (only its baked animations are added). The physics harness gains a **Rest** `SpinBox` (0.1–10 s, writes `_rig.rest_timeout`), a **"Recover Now"** button (`request_recovery()`), and a `state_changed` hook that removes the `RigCollisionProxy` on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`.
> **Phase 11 (Instant Handoff & Recovery):** replaces the hard ragdoll entry/exit with an **instant handoff** and adds a **stand-up recovery** path. `StickmanRig` gains `enum RigState { ANIMATED, RAGDOLL, RECOVERING }` plus exports `rest_timeout` (2.0 s) and `auto_recover` (true). On entering `RAGDOLL` the ragdoll is built from the **current solved bone positions** (the player is stopped with `keep_state`), then `Body/*` is hidden and the IK stack disabled in the same call — no crossfade, since the ragdoll spawns at exactly the same pose and a fade would only read as ghosting (an earlier `transition_duration` blend was removed on director feedback). Rest detection reads the Torso body — sleeping, or linear ≤ `REST_LINEAR_THRESHOLD` (5.0 px/s, tuned up from the plan's 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s) and angular ≤ 0.1 rad/s — then after `rest_timeout` + `STABILIZATION_DELAY` (0.1 s) with `auto_recover` on, calls `_start_recovery()`. Recovery first calls `_capture_pre_ragdoll_state()` (6 IK markers, 8 limb + head bone rotations, facing profile, 4 bend flags, head LookAt state, per-leg `leg_sides`, and entry chain offsets `torso_chain_offset`/`head_chain_offset`) as the END target, then `_capture_ragdoll_pose()` records the 10 bodies' rig-local pose **and** probes the terrain via `_probe_ground_y()` (a 7-ray fan with a tolerance band, excluding the ragdoll RIDs), re-anchors the rig root so the standing figure's **feet** sit on the ground at the landing X (`_reanchor_root_to_landing()`, `new_root = feet + FOOT_OFFSET`, with a mirror-aware local shift `local_shift = to_local(shift) − to_local(Vector2.ZERO)`), destroys the ragdoll, sets `state = RECOVERING` + emits, snap-solves via `_snap_skeleton_to_pose()` — the 6 IK targets plus direct arm/head bone rotations (`rot_u = chain(u) − θ_t − bone_angle_u`, `rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l`, head via `head_chain_offset`), all normalized into `(−π, π]`, with the ankles clamped onto the terrain and the two leg IK mods enabled with the captured knee side — and finally `_play_stand_up()` runs a **2.0 s** sine-in-out tween of the markers → their pre-ragdoll values and the arm/head bones to the snap-computed `_recovery_bones` targets (elbows via `_natural_joint_target`), legs IK-driven. `_on_stand_up_finished()` restores the facing profile + bend flags + LookAt state, re-enables the arm mods, and returns to `ANIMATED`. The per-joint bend decisions live in three editable tables — `RAGDOLL_JOINT_BAND_LIMITS`, `RECOVERY_LEG_SIDE_FORCE`, `RECOVERY_JOINT_PATH` (Joint bend configuration (EDIT ME)); diagnostics are gated behind `DEBUG_RECOVERY := false`.
Torso bone's `bone_angle` (copying the body rotation directly would slam the skeleton −90° and lay it flat). `request_recovery()` is public (no-op unless in `RAGDOLL`); `set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll, `set_ragdoll(false)` during `RAGDOLL` routes through recovery, and calls are otherwise idempotent. `is_in_ragdoll()` stays `state == RAGDOLL` (so `RECOVERING` reads as "Stickman"). A new `res://scripts/create_animations.gd` editor script (superseding the deleted `create_walk.gd`) bakes `walk_left`/`walk_right`/the one-shot `stand_up` into `master_rig.tscn`'s `AnimationLibrary` (the baked `stand_up` is an authored reference only — recovery does not play it) — **no `.stk` format change**; `master_rig.tscn` scene nodes are unchanged (only its baked animations are added). The physics harness gains a **Rest** `SpinBox` (0.1–10 s, writes `_rig.rest_timeout`), a **"Recover Now"** button (`request_recovery()`), and a `state_changed` hook that removes the `RigCollisionProxy` on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`.
+1
View File
@@ -21,6 +21,7 @@ Each issue has a clear fix and verification steps.
When the stickman recovers from ragdoll, it appears floating above the terrain at the landing position, rather than standing with feet on the ground.
**Root Cause:** `_reanchor_root_to_landing()` calculates the ground contact point using `_captured_ground_y = torso.center.y + RAGDOLL_TORSO_RADIUS`. This assumes the torso is lying flat on its side. If the ragdoll's torso is rotated (as it often is after a fall), the calculated ground point is incorrect.
I
### 2.2. The Fix
+88 -112
View File
@@ -198,9 +198,8 @@ 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. TEMPORARILY true for
## the Round 4 transition debugging — flip back to false once verified.
const DEBUG_RECOVERY := true
## 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:
@@ -242,6 +241,64 @@ const RAGDOLL_JOINTS: Array[Dictionary] = [
{ "child": "right_lower_leg", "parent": "right_upper_leg", "pin_node_path": "Torso/RightUpperLeg/RightLowerLeg", "limit": "elbow_knee" },
]
## ---------------------------------------------------------------------------
## Joint bend configuration (EDIT ME)
## ---------------------------------------------------------------------------
## Every per-joint bend decision in the ragdoll cycle lives in these tables,
## keyed by facing profile, so each joint can be tuned individually without
## touching the pipeline code.
##
## RAGDOLL_JOINT_BAND_LIMITS[profile][joint] — the fall's knee/elbow PinJoint2D
## angular band, in DEGREES, as { "lower": ..., "upper": ... }:
## - The band is the allowed range of the joint's relative angle
## (shin vs thigh / forearm vs upper arm), measured in the pin's frame.
## - 0 = straight. Negative = folds one way; positive = folds the other.
## - Natural shape: a SMALL side (~5) that resists hyperextension the wrong
## way, and a LARGE side (~150) on the fold direction.
## - To reverse a joint's bend: swap the magnitudes, e.g.
## { "lower": -5, "upper": 150 } <-> { "lower": -150, "upper": 5 }.
## - lower must be <= upper (the joint angle must stay inside the band).
## Missing joint entries fall back to the RAGDOLL_JOINTS `limit` string.
const RAGDOLL_JOINT_BAND_LIMITS: Dictionary = {
FacingProfile.FORWARD: {
"left_lower_arm": { "lower": -150.0, "upper": 5.0 },
"right_lower_arm": { "lower": -150.0, "upper": 5.0 },
"left_lower_leg": { "lower": -5.0, "upper": 150.0 },
"right_lower_leg": { "lower": -5.0, "upper": 150.0 },
},
FacingProfile.LEFT: {
"left_lower_arm": { "lower": 60.0, "upper": 100.0 },
"right_lower_arm": { "lower": -150.0, "upper": 5.0 },
"left_lower_leg": { "lower": -100.0, "upper": -90.0 },
"right_lower_leg": { "lower": -5.0, "upper": 150.0 },
},
FacingProfile.RIGHT: {
"left_lower_arm": { "lower": -100.0, "upper": -60.0 },
"right_lower_arm": { "lower": -150.0, "upper": 5.0 },
"left_lower_leg": { "lower": 90.0, "upper": 100.0 },
"right_lower_leg": { "lower": -5.0, "upper": 150.0 },
},
}
## RECOVERY_LEG_SIDE_FORCE[profile][leg] — optional manual override of the leg
## IK `flip_bend_direction` during the stand-up (true = inverted). A missing
## key means automatic: match the captured fall pose's knee side.
const RECOVERY_LEG_SIDE_FORCE: Dictionary = {
FacingProfile.FORWARD: {},
FacingProfile.LEFT: {},
FacingProfile.RIGHT: {},
}
## RECOVERY_JOINT_PATH[profile][joint] — optional override of the elbow/knee
## stand-up tween path: "shortest" = plain shortest rotation. A missing key
## means automatic: pass through straight when the shortest path would cross
## the +/-180 (fully-backward) boundary.
const RECOVERY_JOINT_PATH: Dictionary = {
FacingProfile.FORWARD: {},
FacingProfile.LEFT: {},
FacingProfile.RIGHT: {},
}
# ---------------------------------------------------------------------------
# Exported controls
# ---------------------------------------------------------------------------
@@ -355,7 +412,6 @@ var _stand_up_tween: Tween = null
var _limb_bones: Dictionary = {} # { String: { "upper": Bone2D, "lower": Bone2D } }
var _recovery_bones: Array[Dictionary] = [] # [{ "bone": Bone2D, "target": float }] bones tweened in stand-up
var _pre_ragdoll_state: Dictionary = {} # captured at _enter_ragdoll; recovery's END target
var _debug_recover_time: float = -1.0 # throttle accumulator for the recovery visual prints
var _probe_top_y: float = 0.0 # ground-probe context retained for the ankle clamp
var _probe_bottom_y: float = 0.0
var _probe_exclude: Array[RID] = []
@@ -471,7 +527,6 @@ func _physics_process(delta: float) -> void:
_update_speech(delta)
_update_runner(delta)
_pin_mirrored_head_rotation()
_debug_recovery_visuals(delta)
@@ -922,10 +977,6 @@ func _capture_ragdoll_pose() -> void:
"rot": body.global_rotation - global_rotation,
"half": half,
}
if DEBUG_RECOVERY:
print("[ragdoll-end] %s world_pos=%s world_rot=%.2fdeg local_pos=%s rel_rot=%.2fdeg" % [
key, str(body.global_position), rad_to_deg(body.global_rotation),
str(_captured_pose[key]["pos"]), rad_to_deg(float(_captured_pose[key]["rot"]))])
# Record the ragdoll torso's world center + probe the terrain surface under
# the fallen figure so recovery can re-anchor the root's feet onto wherever
# the ragdoll actually landed (flat ground, slopes and ramp edges included).
@@ -933,8 +984,6 @@ func _capture_ragdoll_pose() -> void:
if torso != null and is_instance_valid(torso):
_captured_landing_center = torso.global_position
_captured_ground_y = _probe_ground_y(torso.global_position.x, top_y, bottom_y, exclude_rids)
if DEBUG_RECOVERY:
print("[ragdoll-end] landing_center=%s ground_y=%.1f" % [str(_captured_landing_center), _captured_ground_y])
# Retain the probe context so the ankle clamp in _snap_skeleton_to_pose()
# can re-probe the terrain at each ankle's X using the same extents and
# exclusion set (without re-running the capture loop).
@@ -1071,36 +1120,6 @@ func _recovery_dbg() -> void:
])
## Prints the rendered world rotations of the 10 Body/* visual part nodes plus
## the 4 limb bone world rotations, throttled to every 0.25 s while RECOVERING
## (DEBUG_RECOVERY only). Used to pinpoint where a limb appears to spin.
func _debug_recovery_visuals(delta: float) -> void:
if not DEBUG_RECOVERY:
return
if state != RigState.RECOVERING:
_debug_recover_time = -1.0
return
if _debug_recover_time < 0.0:
_debug_recover_time = 0.0
_debug_recover_time += delta
if _debug_recover_time < 0.25:
return
_debug_recover_time = 0.0
if _body_container == null or not is_instance_valid(_body_container):
return
var parts: Array[String] = ["Body", "Head", "LeftUpperArm", "LeftLowerArm", "RightUpperArm", "RightLowerArm", "LeftUpperLeg", "LeftLowerLeg", "RightUpperLeg", "RightLowerLeg"]
for p: String in parts:
var node := _body_container.get_node_or_null(NodePath(p)) as Node2D
if node != null and is_instance_valid(node):
print("[recover-vis] %s rot=%.2fdeg" % [p, rad_to_deg(node.global_rotation)])
for joint: String in LIMB_BONE_PATHS:
var pair: Dictionary = _limb_bones.get(joint, {})
for part: String in ["upper", "lower"]:
var bone: Bone2D = pair.get(part)
if bone != null and is_instance_valid(bone):
print("[recover-bone] %s %s local=%.2fdeg global=%.2fdeg" % [joint, part, rad_to_deg(bone.rotation), rad_to_deg(bone.global_rotation)])
## Converts a captured relative angle (ragdoll body world rotation minus the
## rig root's global_rotation) into the skeleton's chain-space angle. Under the
## LEFT whole-rig X-mirror the root's global_rotation decomposes as (PI - phi),
@@ -1221,10 +1240,6 @@ func _snap_skeleton_to_pose() -> void:
theta_t = _wrap_toward(raw_rot, pre_torso_rot)
theta_t = wrapf(theta_t, -PI, PI)
torso_marker.rotation = theta_t
if DEBUG_RECOVERY:
print("[recover-start] torso rel=%.2fdeg chain=%.2fdeg raw=%.2fdeg pre=%.2fdeg final=%.2fdeg hip_local=%s" % [
rad_to_deg(torso_rel), rad_to_deg(torso_chain), rad_to_deg(raw_rot),
rad_to_deg(pre_torso_rot), rad_to_deg(torso_marker.rotation), str(torso_marker.position)])
var head_marker := _get_ik_marker("Head")
if head_marker != null:
@@ -1285,12 +1300,6 @@ func _snap_skeleton_to_pose() -> void:
lower.rotation = _wrap_toward(
lower_chain - upper_chain + deg_to_rad(upper.bone_angle) - deg_to_rad(lower.bone_angle),
float(pre_pair.get("lower", lower.rotation)))
if DEBUG_RECOVERY:
print("[recover-start] %s upper_chain=%.2fdeg lower_chain=%.2fdeg rot_u=%.2fdeg rot_l=%.2fdeg pre_u=%.2fdeg pre_l=%.2fdeg" % [
joint, rad_to_deg(upper_chain), rad_to_deg(lower_chain),
rad_to_deg(upper.rotation), rad_to_deg(lower.rotation),
rad_to_deg(float(pre_pair.get("upper", upper.rotation))),
rad_to_deg(float(pre_pair.get("lower", lower.rotation)))])
# Normalize the snapped rotations into (-PI, PI]: Godot's Skeleton2D
# re-wraps bone rotations on its next internal process, which would
# otherwise change the tween's captured START and send it the long
@@ -1313,15 +1322,16 @@ func _snap_skeleton_to_pose() -> void:
# The joint's rendered bend is offset by bone_angle_lower -
# bone_angle_upper (e.g. -90deg for the legs): evaluate the natural
# rule in that rendered space so the knee unfolds through straight
# instead of being sent the long way around.
var target_l := _natural_joint_target(lower.rotation, pre_l, deg_to_rad(lower.bone_angle - upper.bone_angle))
# instead of being sent the long way around. A "shortest" entry in
# RECOVERY_JOINT_PATH skips the natural rule entirely.
var target_l: float
var path_override := String(RECOVERY_JOINT_PATH.get(facing_profile, {}).get(joint, ""))
if path_override == "shortest":
target_l = lower.rotation + wrapf(pre_l - lower.rotation, -PI, PI)
else:
target_l = _natural_joint_target(lower.rotation, pre_l, deg_to_rad(lower.bone_angle - upper.bone_angle))
_recovery_bones.append({ "bone": upper, "target": target_u })
_recovery_bones.append({ "bone": lower, "target": target_l })
if DEBUG_RECOVERY:
print("[stand-up] %s %.2fdeg -> %.2fdeg (sweep %.2fdeg)" % [
upper.name, rad_to_deg(upper.rotation), rad_to_deg(target_u), rad_to_deg(target_u - upper.rotation)])
print("[stand-up] %s %.2fdeg -> %.2fdeg (sweep %.2fdeg)" % [
lower.name, rad_to_deg(lower.rotation), rad_to_deg(target_l), rad_to_deg(target_l - lower.rotation)])
# The head ragdoll body is a "visual" body spawned with rotation 0, so its
# cloned visual carries the entry head chain as a baked offset — captured
@@ -1330,16 +1340,9 @@ func _snap_skeleton_to_pose() -> void:
var head_rel := float((_captured_pose["head"] as Dictionary).get("rot", 0.0))
var raw_head := _chain_angle_from_captured(head_rel) - head_chain_offset - theta_t
_head_bone.rotation = _wrap_toward(raw_head, pre_head_rotation)
if DEBUG_RECOVERY:
print("[recover-start] head rel=%.2fdeg head_chain_offset=%.2fdeg raw=%.2fdeg pre=%.2fdeg final=%.2fdeg" % [
rad_to_deg(head_rel), rad_to_deg(head_chain_offset),
rad_to_deg(raw_head), rad_to_deg(pre_head_rotation), rad_to_deg(_head_bone.rotation)])
_head_bone.rotation = wrapf(_head_bone.rotation, -PI, PI)
var target_h := _head_bone.rotation + wrapf(pre_head_rotation - _head_bone.rotation, -PI, PI)
_recovery_bones.append({ "bone": _head_bone, "target": target_h })
if DEBUG_RECOVERY:
print("[stand-up] %s %.2fdeg -> %.2fdeg (sweep %.2fdeg)" % [
_head_bone.name, rad_to_deg(_head_bone.rotation), rad_to_deg(target_h), rad_to_deg(target_h - _head_bone.rotation)])
# Show the kinematic puppet first so it appears already in the captured
# pose, then re-arm the stack (torso driver) with the limb TwoBoneIK mods
@@ -1360,7 +1363,11 @@ func _snap_skeleton_to_pose() -> void:
continue
var leg_sides: Dictionary = _pre_ragdoll_state.get("leg_sides", {})
var entry: Dictionary = leg_sides.get(leg_joint, {})
if not entry.is_empty():
var force_profile: Dictionary = RECOVERY_LEG_SIDE_FORCE.get(facing_profile, {})
if force_profile.has(leg_joint):
# Manual override from the editable RECOVERY_LEG_SIDE_FORCE table.
mod.flip_bend_direction = bool(force_profile[leg_joint])
elif not entry.is_empty():
# Derive the captured knee side from the RAGDOLL capture in world
# space. The bone nodes are NOT reliable here: the torso marker's
# driver has not pushed the re-anchored hip yet, so bone globals
@@ -1385,23 +1392,7 @@ func _snap_skeleton_to_pose() -> void:
var entry_side := float(entry.get("side", captured_side))
var entry_flag := bool(entry.get("flag", false))
mod.flip_bend_direction = entry_flag if captured_side == entry_side else not entry_flag
if DEBUG_RECOVERY:
print("[leg-side] %s captured_side=%s entry_side=%s flag=%s" % [
leg_joint, str(captured_side), str(entry_side), str(mod.flip_bend_direction)])
mod.enabled = true
if DEBUG_RECOVERY:
for joint: String in BEND_JOINTS:
var dbg_mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(joint) as SkeletonModification2DTwoBoneIK
print("[recover-start] ik %s enabled=%s" % [joint, str(dbg_mod != null and dbg_mod.enabled)])
print("[recover-start] lookat enabled=%s stack_enabled=%s" % [
str(_head_look_at != null and _head_look_at.enabled),
str(_skeleton != null and _skeleton.modification_stack != null and _skeleton.modification_stack.enabled)])
if DEBUG_RECOVERY and _body_container != null and is_instance_valid(_body_container):
var snap_parts: Array[String] = ["Body", "Head", "LeftUpperArm", "LeftLowerArm", "RightUpperArm", "RightLowerArm", "LeftUpperLeg", "LeftLowerLeg", "RightUpperLeg", "RightLowerLeg"]
for p: String in snap_parts:
var node := _body_container.get_node_or_null(NodePath(p)) as Node2D
if node != null and is_instance_valid(node):
print("[recover-snap-vis] %s rot=%.2fdeg" % [p, rad_to_deg(node.global_rotation)])
func _set_marker_from_body(marker_name: String, body_key: String) -> void:
@@ -1467,12 +1458,6 @@ func _tween_markers_to(target_pose: Dictionary, duration: float) -> Tween:
## Stand-up tween complete: settle into ANIMATED.
func _on_stand_up_finished() -> void:
if DEBUG_RECOVERY and _body_container != null and is_instance_valid(_body_container):
var end_parts: Array[String] = ["Body", "Head", "LeftUpperArm", "LeftLowerArm", "RightUpperArm", "RightLowerArm", "LeftUpperLeg", "LeftLowerLeg", "RightUpperLeg", "RightLowerLeg"]
for p: String in end_parts:
var node := _body_container.get_node_or_null(NodePath(p)) as Node2D
if node != null and is_instance_valid(node):
print("[recover-end-vis] %s rot=%.2fdeg" % [p, rad_to_deg(node.global_rotation)])
# Restore the pre-ragdoll facing profile + per-joint bend flags so the
# re-enabled IK holds the exact pre-ragdoll pose.
set_facing_profile(int(_pre_ragdoll_state.get("facing_profile", int(facing_profile))))
@@ -1690,35 +1675,26 @@ func _build_ragdoll_joint(entry: Dictionary) -> void:
pin.node_a = pin.get_path_to(parent_body)
pin.node_b = pin.get_path_to(child_body)
pin.softness = RAGDOLL_TARGET_SOFTNESS
_apply_ragdoll_joint_limits(pin, entry["limit"])
if DEBUG_RECOVERY:
# Diagnostic: profile + the applied band + the joint's INITIAL relative
# angle (child vs parent body) — if the initial angle sits outside the
# band, the pin re-folds the limb at ragdoll entry.
var rel := wrapf(child_body.rotation - parent_body.rotation, -PI, PI)
print("[ragdoll-joint] %s limit=%s profile=%s band=[%.1f, %.1f]deg initial_rel=%.1fdeg" % [
child_key, entry["limit"], FacingProfile.keys()[facing_profile],
rad_to_deg(pin.angular_limit_lower), rad_to_deg(pin.angular_limit_upper),
rad_to_deg(rel)])
_apply_ragdoll_joint_limits(pin, entry["limit"], str(entry["child"]))
## Applies angular limits to a ragdoll PinJoint2D. The natural elbow/knee fold
## follows the LIMB (encoded in the static `limit` strings: left arm/right leg
## fold +CW, right arm/left leg fold -CCW) and is profile-independent — the IK
## bend flags and the LEFT whole-rig mirror do NOT change it (the ragdoll's
## world fold direction stays with the limb). `shoulder_hip` is ±160°; any
## other `limit` leaves the joint free.
func _apply_ragdoll_joint_limits(pin: PinJoint2D, limit: String) -> void:
## Applies angular limits to a ragdoll PinJoint2D. The elbow/knee band comes
## from the editable RAGDOLL_JOINT_BAND_LIMITS table (per profile, per joint,
## in degrees); the static `limit` string is only a fallback for joints
## missing from the table. `shoulder_hip` is ±160°; other limits leave the
## joint free.
func _apply_ragdoll_joint_limits(pin: PinJoint2D, limit: String, child_key: String = "") -> void:
match limit:
"elbow_knee", "elbow_knee_ccw":
var ccw := limit == "elbow_knee_ccw"
var band: Dictionary = RAGDOLL_JOINT_BAND_LIMITS.get(facing_profile, {}).get(child_key, {})
if band.is_empty():
if limit == "elbow_knee":
band = { "lower": -5.0, "upper": 150.0 }
else:
band = { "lower": -150.0, "upper": 5.0 }
pin.angular_limit_enabled = true
if ccw:
pin.angular_limit_lower = -deg_to_rad(150.0)
pin.angular_limit_upper = deg_to_rad(5.0)
else:
pin.angular_limit_lower = -deg_to_rad(5.0)
pin.angular_limit_upper = deg_to_rad(150.0)
pin.angular_limit_lower = deg_to_rad(float(band.get("lower", -5.0)))
pin.angular_limit_upper = deg_to_rad(float(band.get("upper", 150.0)))
"shoulder_hip":
pin.angular_limit_enabled = true
pin.angular_limit_lower = -deg_to_rad(160.0)
+85
View File
@@ -409,3 +409,88 @@ Verified against the user's RIGHT data: RightLeg knee −159.7° (world −97.5
minimal-world-sweep choice).
- [ ] `[stand-up]` lines show every segment sweep ≤ 180°; end pose exactly pre-ragdoll.
- [ ] No console errors. Flip `DEBUG_RECOVERY` to `false` once verified.
---
# Verified final state
Shipped and verified working. The round-by-round history above is retained for context; the
design below is what is actually in `scripts/stickman_rig.gd` and documented in `AGENTS.md` /
`README.md`.
## Ragdoll fall
- **Bodies / joints:** 10 `RigidBody2D` (torso capsule r12 mass 8.0, head circle r100 mass 2.0,
8 limb capsules r8 masses 1.0–2.0) + 9 `PinJoint2D` (`softness = RAGDOLL_TARGET_SOFTNESS`),
built in code and reparented into `"RagdollBodyContainer"` under the rig's parent; layer/mask 1.
- **Visual cloning:** `_clone_part_visuals()` reads each body entry's `visual_path` `Body/*` node
and clones every `Line2D`/`Polygon2D` child with points baked into the ragdoll body's local
frame — no gray bones unless the part is missing/empty (fallback only).
- **Ground probe:** `_probe_ground_y()` fans **7** rays (`GROUND_PROBE_X_OFFSETS`) around the
landing X, from `top_y − GROUND_PROBE_START_OFFSET` to `bottom_y + GROUND_PROBE_END_OFFSET`,
accepting only hits inside `GROUND_PROBE_TOLERANCE` of the ragdoll extent and excluding the
ragdoll body RIDs (`query.exclude`). Called in `_capture_ragdoll_pose()` (landing ground) and
again per-ankle in `_snap_skeleton_to_pose()` (ankle clamp).
- **Joint angular limits:** `_apply_ragdoll_joint_limits()` reads the editable
`RAGDOLL_JOINT_BAND_LIMITS[profile][joint] = { "lower": deg, "upper": deg }` table (the joint's
allowed relative angle; 0 = straight; a small side ~5 resists hyperextension and a large side
~150 is the fold; narrow/locked bands such as `{60, 100}` force a permanently-bent limb).
FORWARD arms `{-150°, +5°}`, legs `{-5°, +150°}`; LEFT/RIGHT override both sides. Joints
missing from the table fall back to the `RAGDOLL_JOINTS` `limit` string
(`elbow_knee`/`elbow_knee_ccw`). Shoulder/hip ±160°, neck free.
## Recovery pipeline (final)
1. **Pre-ragdoll capture** — `_enter_ragdoll()` → `_capture_pre_ragdoll_state()` 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 is the
recovery **END target** (possibly a mid-walk pose; the animation is not resumed).
2. **Ragdoll pose capture** — `_capture_ragdoll_pose()` records each body's rig-local
`{pos, rot, half}` and probes the terrain; the probe context is stored in
`_probe_top_y` / `_probe_bottom_y` / `_probe_exclude`.
3. **Re-anchor** — `_reanchor_root_to_landing()` moves the root so the standing feet sit on the
ground at the landing X (`new_root = (_captured_landing_center.x, _captured_ground_y) +
FOOT_OFFSET`) and re-bases the captured positions with a **mirror-aware local shift**
`local_shift = to_local(shift) − to_local(Vector2.ZERO)`.
4. **Snap** — `_snap_skeleton_to_pose()` writes the captured pose: torso marker
`position = to_local(center_world − spine_dir·half)`, `rotation = chain(spine) −
torso_chain_offset`; limb bones `rot_u = chain(u) − θ_t − bone_angle_u`,
`rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l`; head `chain(head) −
head_chain_offset − θ_t`. All snapped rotations normalized into `(−π, π]` because Godot's
`Skeleton2D` re-wraps bone rotations internally (otherwise the tween's captured START is
corrupted). Ankles clamped onto the terrain. Re-shows `Body/*`, `_rearm_ik_stack()`, then
disables the 4 arm mods + head LookAt and enables the 2 leg IK mods with
`flip_bend_direction` matched to the captured knee side (`leg_sides`) or forced by
`RECOVERY_LEG_SIDE_FORCE`.
5. **Stand-up** — `_play_stand_up()` runs a **2.0 s** sine-in-out tween of the 6 markers → their
**pre-ragdoll** values and the arm + head bones → the `_recovery_bones` targets computed by
the snap. Upper arm bones use the shortest path; lower arm bones use
`_natural_joint_target(current, target, offset)` (`offset = bone_angle_lower −
bone_angle_upper`; through-straight rule evaluated in the joint's rendered space; 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.
6. **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 (including mid-walk poses).
`_pin_mirrored_head_rotation()` pins the head only while `ANIMATED` (during `RECOVERING` the
stand-up tween drives the head bone itself). `set_ragdoll(true)` during `RECOVERING` kills the
tween and rebuilds the ragdoll; `set_ragdoll(false)` during `RAGDOLL` routes through recovery;
calls are idempotent. `is_in_ragdoll()` stays `state == RAGDOLL`.
## Tuning tables (EDIT ME)
Every per-joint bend decision of the cycle is editable without touching pipeline code:
| Const | Shape | Purpose |
|---|---|---|
| `RAGDOLL_JOINT_BAND_LIMITS` | `[profile][joint] = { "lower": deg, "upper": deg }` | Fall knee/elbow `PinJoint2D` band. Missing entries fall back to the `RAGDOLL_JOINTS` `limit` string. |
| `RECOVERY_LEG_SIDE_FORCE` | `[profile][leg] = bool` | Optional leg IK `flip_bend_direction` override (`true` = inverted). Missing = match the captured fall pose's knee side. |
| `RECOVERY_JOINT_PATH` | `[profile][joint] = "shortest"` | Optional elbow/knee tween-path override. Missing = pass through straight when the shortest path would cross ±180°. |
`const DEBUG_RECOVERY := false` (shipped OFF); `_recovery_dbg()` remains as the gated trace and
the temporary per-step debug prints have been stripped.