Add ramp scene and implement Phase 6 for dynamic IK and terrain-following locomotion

- Created a new scene file `ramp.scene` with terrain and stickman objects, including event rules for ragdoll and recovery actions.
- Documented Phase 6 implementation in `PHASE_6_GROUND_SNAPPING.md`, detailing the continuous dynamic IK, terrain-following locomotion, and constraints for stickman rigs.
- Defined scene structure, layered IK ownership, probe contracts, and per-frame solve order for terrain interaction.
- Verified functionality for terrain snapping, stance/swing blending, pelvis/root compensation, and navigation synchronization.
This commit is contained in:
2026-09-16 09:33:07 -04:00
parent a7c58705e1
commit 08247eadef
7 changed files with 1088 additions and 44 deletions
+32 -7
View File
@@ -388,6 +388,19 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
recreate a flinch after STOP. PLAY→DIRECT restoration also resets ANIMATED rigs' authored
transform, facing, IK markers, and torso rotation. Null-guards + `push_warning` prefixed
`StickmanRig:` throughout.
- **Phase 6 continuous terrain locomotion:** terrain following is PLAY-only. `begin_terrain_fall()`
arms a kinematic gravity fall for unsupported rigs; `snap_to_terrain()` is retained as a
compatibility alias, and `set_terrain_following_enabled(false)` clears contacts and restores
live leg targets from `AnimationTargets/*_Base`. The center support uses a three-ray fan
(`[-24, 0, +24]`); each lower-leg endpoint uses an independent three-ray fan (`[-4, 0, +4]`).
Rays accept only `TerrainBlock` colliders with upward-facing normals, exclude the rig's own
collision-proxy RIDs, and choose the highest valid surface. Runtime root elevation accumulates
across consecutive ramps/steps; unsupported rigs free-fall with bounded gravity, and walking
animation pauses until landing. The terrain controller writes final `IK_Targets/Left_Leg` /
`Right_Leg` positions from layered animation base markers, complementary continuous stance/swing
weights, world-space stance locks, and a predictive 24 px foot-penetration guard. It also filters
lower floors beneath overlapping ramps and applies centerline pelvis compensation. `DEBUG_TERRAIN`
is `false` in the shipped build; temporary `[terrain]` traces are opt-in.
- **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)`
@@ -674,13 +687,14 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
` is_in_ragdoll()` is false).
- `scripts/sandbox_stage.gd` — `class_name SandboxStage`, `extends Node2D`; the **Sandbox Stage
Builder** root controller (Phase 2, **not wired into the editor**; run via **F6** on
`res://scenes/sandbox_stage.tscn`). Owns the EDIT/PLAY mode state machine, placement mode,
`res://scenes/sandbox_stage.tscn`). Owns the EDIT/DIRECT/PLAY mode state machine, placement mode,
camera pan/zoom, deletion, status bar, and signal fan-out; instantiates `StageSpawner` /
`StageSelection` / `StageGizmos` (via `preload` consts). `enum StageMode { EDIT, PLAY }`
`StageSelection` / `StageGizmos` (via `preload` consts). `enum StageMode { EDIT, DIRECT, PLAY }`
(default EDIT). EDIT freezes `RigidBody2D` props with `freeze = true` +
`freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC` (script-driven gizmo dragging needs
KINEMATIC, not STATIC) and keeps `StickmanRig`s ANIMATED (`set_ragdoll(false)` runs first);
PLAY unfreezes props and ragdolls stickmen with `auto_recover = false`. Signals
PLAY unfreezes props, enables PLAY-only terrain following/free-fall, and starts stickman queues
with `auto_recover = false`; ragdoll/recover remain explicit actions. Signals
`mode_changed(mode)`, `object_placed(node)`, `object_selected(nodes)`, `object_deselected()`,
`object_deleted(nodes)`. Camera: middle-mouse pan, wheel zoom clamped to `@export min_zoom` /
`max_zoom` (0.1 / 6.0); mouse→world via `_camera.get_global_mouse_position()` (no SubViewport).
@@ -727,12 +741,13 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
next stage click appends `{"type":"walk_to","target":world_pos}` and which **Esc** cancels (Esc
priority: pending target → exit direct mode → existing placement clears). Builds a code-built
`NavigationRegion2D` (`_build_navigation()`, child of the stage **not** `World` so it is never
hit-tested) carrying a procedural `NavigationPolygon` from per-`TerrainBlock` convex decomposition
(`_rebake_navigation()`, `Geometry2D.decompose_polygon_in_convex` + fan triangulation, world-space
via `block.transform * p`); a `_nav_dirty` flag re-bakes once per frame (`_process`) on terrain
hit-tested) carrying a procedural `NavigationPolygon` from exposed upward-surface ribbons and
short step bridges (`_rebake_navigation()`, world-space via `block.transform * p`); a `_nav_dirty`
flag re-bakes once per frame (`_process`) on terrain
place / move / rotate (`transform_committed`) / delete. Instantiates a `StageDirectorVisuals`
overlay (`_build_director_visuals()`). **Play mode change (D3):** `_enter_play_mode()` no longer
auto-ragdolls stickmen — it now sets each rig `auto_recover = false` and calls `start_queue()`;
auto-ragdolls stickmen — it sets each rig `auto_recover = false`, calls `begin_terrain_fall()`,
then calls `start_queue()`;
`ragdoll`/`recover` are explicit queue actions; props still unfreeze. `_enter_edit_mode()`
`stop_queue()`s then `snap_to_standing()`s stickmen and re-enables the visuals. Wires
`node.queue_changed → _director_visuals.mark_dirty` when a stickman is placed; `object_deleted` →
@@ -774,6 +789,16 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
outside→overlap, does not repeat while overlapping, and re-arms after exit. The area loop's prop
enumeration is correctly scoped. Verified by `tests/test_phase4c_area.gd` (19 assertions,
all passed) and manual editor verification of A entering an area causing B's speech bubble.
- **Phase 6 runtime terrain/events:** `_enter_play_mode()` calls `begin_terrain_fall()` before
`start_queue()`. EDIT/DIRECT disable terrain following and restore authored transforms. The
navigation bake uses shallow exposed upward-surface ribbons and connects neighboring surface
endpoints only within 64 px horizontal / 96 px vertical step limits; the `NavigationAgent2D`
remains at `(0, 385)` and follows the selected walk speed while terrain owns vertical root motion.
`_update_area_entry()` unions each visible stickman's current and previous world AABB so fast
free-fall still emits `entered_area`. In a reactive batch, the first takeover cancels existing
work once while later takeover actions remain ordered; `[ragdoll, recover]` therefore ragdolls,
waits for rest, then recovers. Terrain diagnostics are gated by `StickmanRig.DEBUG_TERRAIN` and
are off by default.
- **Phase 3b asset-library selector integration:** the **Stickman** and **Prop** palette buttons no
longer place directly — pressing them opens a modal **selector grid** (`_selector: AssetSelector`,
an instantiated `scenes/asset_selector.tscn` `PopupPanel`, added to the UI `CanvasLayer` in