Implement collision bodies, physics events, and hit reactions for StickmanRig

- Added a new specification for collision bodies, detailing the implementation requirements and corrections needed in the StickmanRig, PropBlock, and SandboxStage scripts.
- Created a comprehensive test suite for collision bodies, including tests for proxy geometry, physics alignment, reaction handling, ghost collision, and stage contact routing.
- Ensured that the collision system correctly handles ragdoll transitions and maintains the integrity of existing event rules.
- Introduced new methods in StickmanRig for managing collision bodies and applying reactions based on impact direction and speed.
- Updated PropBlock to emit signals for collisions and separations, preserving the order of events for stickman and prop interactions.
This commit is contained in:
2026-09-13 10:31:41 -04:00
parent 04e0f1bea2
commit ce6bcecd00
12 changed files with 1698 additions and 58 deletions
+22 -8
View File
@@ -372,6 +372,19 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
`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 4.5 collision bodies and reactions:** while ANIMATED, `_build_collision_bodies()` creates
ten metadata-tagged `AnimatableBody2D` proxies (head, torso, and eight limbs), stored in
`_collision_bodies`. Proxies are top-level relative to their `Body/*` visuals,
`sync_to_physics = false`, layer/mask 1, and `_sync_collision_bodies()` copies each visual's
global transform every physics frame so RemoteTransform2D/IK poses and whole-rig LEFT
mirroring cannot leave stale shapes. Torso/limb capsules use fitted lengths in the hanging
frame; the head circle is centered from mounted geometry and expands beyond its 90 px minimum
when needed. `set_collision_bodies_enabled()` disables both layer and mask for ghosts/RAGDOLL
and restores them for RECOVERING/ANIMATED. `apply_collision_reaction()` uses contact position
plus local force quadrant to choose mirrored side/upward/downward flinch direction, applies a
capped `REACTION_MAX_ANGLE` offset from a captured base rotation, and decays without cumulative
full rotations. PLAY→DIRECT restoration also resets ANIMATED rigs' authored transform, facing,
IK markers, and torso rotation. Null-guards + `push_warning` prefixed `StickmanRig:` throughout.
- **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)`
@@ -591,11 +604,12 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
None: mass 1.0, friction 0.5, bounce 0.05) and recolor fill/outline for non-`NONE`. Unified
live-update setters push geometry/color changes to all children (null-guarded for `@tool`
editor safety); `_apply_shape()` enables exactly one collision node.
- **Phase 4 collision signal:** new `signal collided(other: Node)`; `_ready()` sets
`contact_monitor = true`, `max_contacts_reported = 8`, and connects the guarded
`body_entered` signal → `_on_body_entered` → `collided.emit(body)`, so **prop-vs-prop**
collisions are reported by the physics engine (the stage's geometric feet-point test covers
stickman-vs-prop separately).
- **Phase 4 / Phase 4.5 collision signals:** `collided(other, contact_normal, contact_position)`
and `separated(other)`; `_ready()` sets `contact_monitor = true`,
`max_contacts_reported = 32`, and shape-cast CCD. The guarded `body_entered` path reads a
matching direct-state contact row as world-space normal/position, with a center-direction
fallback when timing leaves no row. The physics signal covers prop-vs-prop and Phase 4.5
stickman-prop contacts; the old geometric feet-point stickman test is removed.
- `scripts/trigger_area.gd` — `class_name TriggerArea`, `extends Node2D`; a **placeable sensor**
(Phase 4, **not used by the editor**). `@export size: Vector2` (default 96×96), `get_area_rect()
-> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill +
@@ -714,10 +728,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
Array[Dictionary]` of `{id, trigger, actions}` rules (trigger types: `arrived_at_waypoint`,
`action_finished`, `speech_finished`, `entered_area`, `collided`; action types reuse the Phase
3a set: `walk_to`/`speak`/`wait`/`ragdoll`/`recover`). Rules persist across Play/Edit mode
toggles but are **not** saved to disk (Phase 5). A geometric **event engine** runs in PLAY
toggles but are **not** saved to disk (Phase 5). A geometric **area-entry engine** runs in PLAY
(`_update_area_entry` tests each movable's visible world AABB against the current transformed
`TriggerArea` world AABB; `_update_stickman_prop_collision` continues to test a stickman's feet
point against prop AABBs and vice-versa),
`TriggerArea` world AABB); Phase 4.5 routes stickman-prop `collided` events from `PropBlock`
physics contacts instead of polling stickman feet points,
with **edge-triggered** dicts (already-fired events) reset on mode entry. A **rule-builder UI
state machine** (`RuleStep` enum: `IDLE`, `SELECT_TRIGGER`, `TRIGGER_TARGET`, `SELECT_ACTION`,
`ACTION_TARGET`, `ACTION_POSITION`, `PARAMS`) is driven from a **"⚡ When..."** item in the Direct