Refactor animation generation: replace create_walk.gd with create_animations.gd

- Removed the old create_walk.gd script, which generated walk animations.
- Introduced create_animations.gd to unify the generation of walk_left, walk_right, and stand_up animations.
- Added a new SpinBox for rest timeout configuration in physics_test_harness.gd.
- Enhanced StickmanRig to support automatic recovery from ragdoll state with configurable timeout.
- Implemented recovery logic in StickmanRig, allowing for smooth transitions from ragdoll to animated state.
- Updated animation generation logic to use new pose templates for standing and lying down positions.
This commit is contained in:
2026-08-27 12:01:54 -04:00
parent e3df1cc5c0
commit 0e971d99b1
12 changed files with 678 additions and 159 deletions
+77 -22
View File
@@ -218,7 +218,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
Task 4). Attached to the `Master` root node of `master_rig.tscn`. **Non-`@tool`** — node
resolution, flag writes, and z-order reordering run only at runtime (`_ready` + setters on a
live instance). Enums `FacingProfile { LEFT, RIGHT, FORWARD }` (values are the harness facing-menu
ids) and `BendDirection { NORMAL, INVERTED }`. Constants (moved from the harness): `SKELETON_PATH`,
ids), `BendDirection { NORMAL, INVERTED }`, and `RigState { ANIMATED, RAGDOLL, RECOVERING }`.
Constants (moved from the harness): `SKELETON_PATH`,
`BODY_CONTAINER_PATH`, `BEND_JOINTS` (`["LeftArm","RightArm","LeftLeg","RightLeg"]`),
`BEND_JOINT_BONE_PATHS` (each joint → its lower `Bone2D` NodePath relative to `Skeleton2D`),
`PROFILE_FLAGS` (per-profile `flip_bend_direction` sets), `Z_ORDER_BY_PROFILE` (per-profile
@@ -226,7 +227,11 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`FORWARD`, a preset whose setter writes the four per-joint vars + reorders `Body/*`) and an
`@export_group("Bend Direction")` of four `@export_enum("Normal","Inverted")` vars
`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend` (defaults
NORMAL/INVERTED/INVERTED/NORMAL = FORWARD). Signals `facing_profile_changed(profile)` /
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`.
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
`get_bend_joint_global_position(joint)` (unknown joint → `push_warning` + no-op/`false`/
@@ -237,26 +242,57 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
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.
physics mode switch plus a `RECOVERING` stand-up state. `enum RigState { ANIMATED, RAGDOLL,
RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state:
int)`, and public API `set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() ->
bool` / `request_recovery()`. `_physics_process()` → `_track_momentum(delta)` caches the rig
root's linear/angular velocity from per-frame `global_position`/`global_rotation` deltas, then
drives `_update_rest_detection()` (auto-recovery trigger). `_enter_ragdoll()` `stop(true)`s the
`AnimationPlayer` (`ANIMATION_PLAYER_PATH` const, keep_state — no pose reset), builds the
ragdoll from the CURRENT solved bone positions while the IK stack is still enabled (disabling
it first would revert the bones to the authored rest pose, popping the figure), then hides
`Body/*` and disables the IK stack **immediately** — an instant handoff with no crossfade (the
ragdoll spawns at exactly the same pose, so a fade would only read as ghosting), sets `state =
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.02.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.
- **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), `_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/*` before re-enabling the IK stack so TwoBoneIK solves toward the end-effectors. 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` (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").
- `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:
@@ -267,6 +303,16 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
(typed as `StickmanRig` since the rig now carries the `StickmanRig` root script).
- `static func spawn(path: String) -> StickmanRig` — `load_stk()` then `spawn_from_data()`;
returns `null` on empty data.
- `scripts/create_animations.gd` — `@tool extends EditorScript`; a **standalone editor utility**
(run manually with `master_rig.tscn` open, **not** auto-loaded or referenced at runtime).
Supersedes the deleted `scripts/create_walk.gd`. `_run()` bakes `walk_left`/`walk_right`
(same keyframes as the old script, via `_generate_walk_animation()`) and a one-shot `stand_up`
(via `_generate_pose_animation()`, `STAND_UP_DURATION` = 0.8, `loop_mode = LOOP_NONE`) into the
open scene's default `AnimationLibrary`. `stand_up` keys the 6 `IK_Targets/*:position` tracks
plus a `IK_Targets/Torso:rotation` track from `POSE_DOWN` (a generic "lying on back" pose) to
`POSE_STANDING` (matching `master_rig.tscn` defaults), with `POSE_PATHS`/`POSE_MARKERS` consts.
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).
- `scripts/test_harness.gd` — **standalone staging scene** (Phase 9, **not wired into the editor**;
run via **F6** on `res://scenes/test_harness.tscn`) for debugging bone scales, vector drawing
offsets, and IK limits in isolation. Top UI bar: "Open .stk…" button → `FileDialog` (`*.stk`);
@@ -458,6 +504,15 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
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.
- **Phase 11 recovery UI:** a `Label("Rest")` + `SpinBox` (`_rest_timeout_spinbox`, min 0.1 /
max 10.0 / step 0.1, initialized to `_rig.rest_timeout` after `_build_ui`) and a **"Recover
Now"** button (`_recover_now()` → `_rig.request_recovery()`) are added after the "Knock Up"
button. `_on_rest_timeout_changed(v)` writes `_rig.rest_timeout = v` (runtime-only, no
settings.json). `_spawn_rig()` connects `_rig.state_changed` → `_on_rig_state_changed()`,
which removes the collision proxy on `RAGDOLL`, re-adds it on `ANIMATED`/`RECOVERING`, and
always `_update_ragdoll_toggle()` (proxy helpers are idempotent, so the toggle handler's own
add/remove is harmless). The toggle label reads "Stickman" during `RECOVERING` (since
`is_in_ragdoll()` is false).
- Scenes:
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,