Add unique identifier for test_phase3c_walk_recovery.gd

This commit is contained in:
2026-09-06 13:09:37 -04:00
parent 1f91f3d2e5
commit 80595a0273
53 changed files with 6806 additions and 471 deletions
+46 -9
View File
@@ -228,7 +228,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`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
`Body/*` draw-order tables, back-to-front). Exports: `facing_profile: FacingProfile` (default
`FORWARD`, a preset whose setter writes the four per-joint vars + reorders `Body/*`) and an
`FORWARD`, a preset whose setter writes the four per-joint vars, reorders `Body/*`, and
applies a whole-rig Y-axis mirror for LEFT — `Master.scale.x = -1`; RIGHT/FORWARD `(1,1)`) 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). Recovery exports: `rest_timeout` (2.0 s),
@@ -245,6 +246,25 @@ 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.
- **Whole-rig Y-axis mirror (replaces `_apply_head_flip()`, 2026 design change):** Facing LEFT
is applied as a **whole-rig Y-axis mirror** — `Master.scale.x = -1` (RIGHT/FORWARD → `(1,1)`) —
so the head AND body mirror together and face the correct direction. The old `_apply_head_flip()`
and the `Body/Head.scale.x` mirror are **removed** (the Head Pivot node's driver transform is
untouched). Per-joint `flip_bend_direction` flags (`PROFILE_FLAGS`) and `Z_ORDER_BY_PROFILE` are
**kept provisionally** (unchanged): the mirror reflects the whole skeleton + `IK_Targets` +
mounted `Body/*` geometry, while z-order (depth) is unaffected by an X-mirror. `walk_to()`
always plays the canonical `walk_right` clip (X-mirrored by the root for LEFT; `walk_left` is
no longer used at runtime) and sets the facing profile explicitly. Two **head-related fixes**
under the mirror: the master_rig.tscn head `RemoteTransform2D` (`Skeleton2D/Torso/Head/Pivot`)
no longer sets `update_scale = false` — it pushes the **full transform** like every other `Body`
driver, so `Body/Head.scale` stays identity under the mirrored root (the old partial-channel
push re-canonicalized the scale and caused per-frame Y-flips/wrap-jumps). And
`_apply_head_lookat_mirror_mode()` (called from `_apply_profile()`) **disables** the head
`SkeletonModification2DLookAt` when facing LEFT and pins the head bone rotation to the FORWARD
canonical aim (π); `_pin_mirrored_head_rotation()` re-asserts that pin each `_physics_process`
frame while ANIMATED/RECOVERING so recovery's stack re-arm can't let LookAt flip the bone.
RIGHT/FORWARD re-enable the LookAt. Consequence: interactive head-aiming while facing LEFT is
intentionally static.
- **Phase 10 ragdoll state system:** `StickmanRig` owns a reversible `ANIMATED ⇄ RAGDOLL`
physics mode switch plus a `RECOVERING` stand-up state. `enum RigState { ANIMATED, RAGDOLL,
RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state:
@@ -280,18 +300,31 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`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,
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/*` before re-enabling the IK stack so TwoBoneIK solves toward the end-effectors. Snap
`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` (sine ease-in-out, `_tween_markers_to()`); the baked
`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`,
@@ -335,8 +368,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
unreachable warning**; the debug trace now carries a `mode=nav|direct` field. Off-by-default
diagnostics: `DEBUG_WALK` + `_walk_dbg()` (rig) and `DEBUG_STAGE` + `_stage_dbg()` (sandbox_stage).
Walking is kinematic
(`global_position.move_toward`); movement composes with the `walk_left`/`walk_right` in-place limb
animations (each has a discrete `.:facing_profile` track).
(`global_position.move_toward`); movement composes with the canonical `walk_right` in-place limb
animation, played for every direction — X-mirrored by the rig root for LEFT (facing is set
explicitly by `walk_to()`/`set_facing_profile()`; `walk_left` is no longer used at runtime and
the animation `.:facing_profile` tracks are neutralized/removed).
- **Phase 4 triggers:** the `arrived` signal gained a `target: Vector2` payload (emits
`_walk_target_feet`) so the stage can match waypoints for `arrived_at_waypoint` rules; new
`enqueue_reactive(actions: Array[Dictionary]) -> void` appends reactive actions to the action
@@ -356,7 +391,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
(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
(via `_generate_pose_animation()`, `STAND_UP_DURATION` = 2.0, `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.
@@ -476,8 +511,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`PlaybackState {STOPPED, PLAYING, PAUSED}` via the button handlers + the `animation_finished`
signal (guarded by `_loop`) — no polling in `_process`. Changing the dropdown selection stops
playback; `_free_current_rig()` clears `_anim_player`, the dropdown, `_selected_animation`,
and state. Playing `walk_right` also sets the rig's `facing_profile` via its animation track →
export setter → the existing `_on_facing_profile_changed` handling (menu `[√] ` + redraw). No
and state. The animation `.:facing_profile` tracks are **neutralized/removed** — facing is now
set explicitly via `set_facing_profile()` / `walk_to()` (which sets LEFT/RIGHT from the walk
direction and root-mirrors for LEFT), so playing `walk_right` no longer flips the rig's profile
through an animation track. No
persistence to disk. The "Facing" menu and all animation controls are **hidden until an .stk is
loaded** (`_set_rig_controls_visible(false)` at the end of `_build_ui()` and in
`_free_current_rig()`; shown on successful spawn in `_load_and_spawn()`).