feat: Implement kinematic-to-ragdoll transition system

- Added KINEMATIC_BLENDING_AND_RECOVERY.md to outline features for smooth transitions between kinematic and ragdoll states, including visual and physical blending, and ragdoll recovery.
- Introduced KINEMATIC_TO_RAGDOLL.md detailing the objectives, scope, and core architecture for transitioning the stickman from kinematic to ragdoll mode.
- Created KINEMATIC_TO_RAGDOLL_SPEC.md as an implementation specification, verifying codebase facts and correcting the initial plan based on Godot 4.4 source.
- Enhanced StickmanRig with state management for animated and ragdoll modes, including momentum preservation and ragdoll construction.
- Updated physics_test_harness to support toggling between kinematic and ragdoll states with user input.
This commit is contained in:
2026-08-27 00:05:17 -04:00
parent 6edf3e53e3
commit e3df1cc5c0
12 changed files with 1325 additions and 57 deletions
+46 -8
View File
@@ -236,6 +236,27 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`_nodes_ready`; a `_nodes_ready` guard makes pre-`_ready` setters store-only (robust against
setter timing during `PackedScene.instantiate()`). Null-guards + `push_warning` prefixed
`"StickmanRig: "` throughout; never crashes.
- **Phase 10 ragdoll state system:** `StickmanRig` owns a reversible `ANIMATED ⇄ RAGDOLL`
physics mode switch. `enum RigState { ANIMATED, RAGDOLL }`, `var state: RigState`
(default `ANIMATED`), `signal state_changed(new_state: int)`, and public API
`set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() -> bool`.
`_physics_process()` → `_track_momentum(delta)` caches the rig root's linear/angular
velocity from per-frame `global_position`/`global_rotation` deltas. `_enter_ragdoll()`
disables the IK modification stack, `stop()`s the `AnimationPlayer` (`ANIMATION_PLAYER_PATH`
const), hides `Body/*`, builds the ragdoll, then sets `state` + emits `state_changed`.
`_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.02.0;
`collision_layer`/`collision_mask` = 1) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`,
one per non-root body pinned at the child bone's origin, `softness` 0.0). 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. `_exit_ragdoll()` `queue_free()`s the container, re-shows `Body/*`, re-enables
the IK stack, `stop()`s the animation, and reverts `state` to `ANIMATED`. All ragdoll nodes
are spawned procedurally — `master_rig.tscn` is **not** modified.
- `scripts/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static
factory** and the **runtime entry point** (Phase 9, **not used by the editor**) that turns a
`.stk` file into a live, rigged `master_rig.tscn` instance:
@@ -414,14 +435,29 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
staging scene root (Vector Terrain System / Dynamic Vector Props, **not wired into the editor**;
run via **F6** on `res://scenes/physics_test_harness.tscn`). Builds flat ground, angled ramps, and stepped
`TerrainBlock` instances via `TerrainUtils`, instantiates `res://master_rig.tscn` standing on the
flat ground, and handles camera input. **Keys 1/2/3** spawn dynamic props above the angled ramp
via `PropUtils.spawn_prop()` (`PROP_SPAWN_POSITION = (300, -300)`): **1** Wood Crate
(`create_box()`, `WOOD`, velocity `(60,0)`), **2** Bouncy Ball (`create_ball()`, `RUBBER`,
`(-80,0)`), **3** Heavy Plank (`create_plank()`, `METAL`, `(30,-40)`). Adds a **best-effort
flat ground, and handles camera input. A top-bar UI (`_build_ui()`, a `CanvasLayer` +
`PanelContainer` matching `test_harness`'s style) replaces the old key bindings: **Spawn
Crate** / **Spawn Ball** / **Spawn Plank** buttons spawn dynamic props above the angled ramp
via `PropUtils.spawn_prop()` (`PROP_SPAWN_POSITION = (300, -300)`: Wood Crate
(`create_box()`, `WOOD`, velocity `(60,0)`), Bouncy Ball (`create_ball()`, `RUBBER`,
`(-80,0)`), Heavy Plank (`create_plank()`, `METAL`, `(30,-40)`). Adds a **best-effort
`StaticBody2D` collision proxy** (`RigCollisionProxy`, `_add_rig_collision_proxy()`) since the
rig has **no physics bodies of its own** — a 240×1000 px `RectangleShape2D` centered at `(0,-500)`
(`RIG_PROXY_SIZE`/`RIG_PROXY_CENTER`) matching the standing figure's world bounds, so props
( `RIG_PROXY_SIZE`/`RIG_PROXY_CENTER`) matching the standing figure's world bounds, so props
bounce/rest against it; the proxy is a code-only stand-in, not part of the rig.
- **Phase 10 ragdoll trigger:** the harness stores the spawned rig in `_rig: StickmanRig`
and shows a toggle-mode `Button` (`_ragdoll_toggle`) whose text flips
**"Stickman"** ↔ **"Ragdoll"** (`_update_ragdoll_toggle()`, `set_pressed_no_signal`
keeps the label in sync without retriggering). Toggling calls `_rig.set_ragdoll(pressed)`,
then on entry calls `_remove_rig_collision_proxy()` (the ragdoll collides directly with the
terrain) and on exit `_add_rig_collision_proxy()`. `_add_rig_collision_proxy()` is idempotent
— it first `_find_rig_collision_proxy()` (a direct child named `"RigCollisionProxy"`) and
returns early if one exists, so rapid toggling leaves no duplicate proxies.
- **Phase 10 external forces:** a **"Knock Up"** button (`_knock_up()`) tests impulses beyond
gravity: when the rig is in RAGDOLL mode it calls `StickmanRig.apply_ragdoll_velocity_boost(
KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ragdoll body,
preserving internal structure), and applies the same upward velocity delta to every dynamic
prop (`RigidBody2D` child of `_environment`) so the whole pile flies up together.
- Scenes:
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
@@ -443,9 +479,11 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
Dynamic Vector Props, not wired into the editor; run via **F6**). Backed by
`scripts/physics_test_harness.gd`. Root `Node2D` + script, `Camera2D` at position `(0, -400)`
zoom `0.5`, empty Environment container. Wheel-zoom scales between `0.25x` and `3.0x`
(resolution independence / vector outline thickness); middle-drag pans. Keys **1/2/3** spawn
dynamic props (`PropUtils`) above the angled ramp; a best-effort `StaticBody2D` rig collision
proxy provides a surface for props to bounce/rest against.
(resolution independence / vector outline thickness); middle-drag pans. Top-bar buttons
**Spawn Crate / Spawn Ball / Spawn Plank** spawn dynamic props (`PropUtils`) above the
angled ramp; a toggle button flips the rig **Stickman** ↔ **Ragdoll** (removing/restoring
the best-effort `StaticBody2D` rig collision proxy); a **Knock Up** button impulses the
ragdoll and all props upward.
### Body-part data model
- 10 internal part keys (ordered): `head`, `torso`, `left_upper_arm`, `left_lower_arm`,