feat: Implement Phase 4 Trigger Events System

- Added a new event-driven system for reactive storytelling, allowing rules like "When X happens, do Y."
- Introduced TriggerArea class for placeable sensors in the stage.
- Enhanced StickmanRig to emit signals for actions and arrivals.
- Updated StageDirectorVisuals to render rules visually with labels and badges.
- Modified StageSpawner to support spawning TriggerAreas.
- Improved text baseline calculations in speech bubbles and rule labels.
- Added tests for text baseline fixes to ensure proper rendering.
- Documented the implementation plan for Phase 4 in PHASE_4_TRIGGER_EVENTS.md.
- Created a polish plan for Phase 4 in PHASE_4b_POLISH.md.
This commit is contained in:
2026-09-01 08:58:07 -04:00
parent bf11a5fab5
commit f208127917
17 changed files with 1538 additions and 8 deletions
+55
View File
@@ -521,6 +521,61 @@ The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's
`recover` waits for `state == RigState.ANIMATED` via the existing `state_changed` signal; `_enter_ragdoll()` calls `_cancel_walking()` so a ragdolled rig has no stale walk/path state.
### 20. Triggers & Event System (Phase 4)
**Phase 4** adds **reactive storytelling** to the Sandbox Stage. While Phase 3a gave directors *sequential* control (actions in a fixed order per stickman), Phase 4 adds *reactive* control — **"When X happens, do Y"** — via event rules that span objects. It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`.
The rule concept is **When → Then**: a *trigger* (an event on some object) fires a list of *actions* on a target object. Rules are stored as `_event_rules: Array[Dictionary]` of `{id, trigger, actions}` in `sandbox_stage.gd`, where `trigger` is `{type, target, ...}` and `actions` is an array of Phase 3a action dicts (`{"type":"walk_to","target":...}`, etc.).
| File | Purpose |
|---|---|
| `res://scripts/trigger_area.gd` | NEW `class_name TriggerArea`, `extends Node2D` — a placeable sensor (`@export size: Vector2`, default 96×96) with `get_area_rect() -> Rect2` and a translucent green `_draw()` fill + dashed border. **No physics, no signals** — it is a pure geometric region evaluated by the stage. |
| `res://scripts/sandbox_stage.gd` | Extended with the rule system: `_event_rules`, the geometric event engine (`_update_area_entry`, `_update_stickman_prop_collision`), the rule-builder UI state machine, and rule auto-cleanup. |
| `res://scripts/stickman_rig.gd` | `arrived` gained a `target: Vector2` payload; new `enqueue_reactive(actions)` appends reactive actions without replaying the queue. |
| `res://scripts/prop_block.gd` | NEW `signal collided(other: Node)` (physics-based, prop-vs-prop). |
| `res://scripts/stage_director_visuals.gd` | Extended with rule visualization + `set_rules()` / `hit_test_rule()` / `hit_test_waypoint()`. |
| `res://scripts/stage_spawner.gd` | New `"area"` palette registry entry (label **"Area"**); duck-typed `get_area_rect` AABB branch in `get_world_aabb`. |
| `res://scripts/stage_selection.gd` | Same duck-typed `get_area_rect` AABB branch for selecting/clicking trigger areas. |
**Trigger types:**
| Trigger type | Target | Fires when |
|---|---|---|
| `arrived_at_waypoint` | Stickman | The stickman completes a `walk_to` (the rig's `arrived` signal). |
| `action_finished` | Stickman | A queue action completes (the rig's `action_finished` signal). |
| `speech_finished` | Stickman | A `speak` bubble auto-hides (the rig's `speech_finished` signal). |
| `entered_area` | Trigger area | A movable (stickman/prop) enters the area's `get_area_rect()` (geometric, `_update_area_entry`). |
| `collided` | Stickman/Prop | A stickman's feet point enters another object's AABB (geometric, `_update_stickman_prop_collision`) **or** a prop physically collides with another prop (`PropBlock.collided` physics signal). |
**Action types** are the Phase 3a set, reused verbatim: `walk_to`, `speak`, `wait`, `ragdoll`, `recover`. Actions always target a **stickman**.
**Rule-building workflow (Edit mode):**
1. Press the **Direct** toggle button (mutually exclusive with palette placement) and click a stickman.
2. The Direct action popup now has a **"⚡ When..."** item (in addition to Walk To / Speak / Wait / Ragdoll / Recover).
3. Choose **"⚡ When..."** → a trigger sub-menu popup (arrived / action finished / speech finished / entered area / collided).
4. Pick the **trigger target** (click a stickman, waypoint, trigger area, or prop depending on trigger type).
5. Choose an **action** from a rule-action popup, then optionally its **target/position** (e.g. click a waypoint for `walk_to`).
6. A popup offers **"Add another action"** (repeat the action step) or **"Done"** to commit the rule.
7. **Esc** is the highest-priority cancel at any step. The rule-builder is a state machine (`RuleStep` enum: `IDLE`, `SELECT_TRIGGER`, `TRIGGER_TARGET`, `SELECT_ACTION`, `ACTION_TARGET`, `ACTION_POSITION`, `PARAMS`) with status-bar hints and toast messages.
**Rule visualization (Edit only):** `StageDirectorVisuals` draws each rule as a **dashed white connector line** from the trigger object to the target, a **green ⚡ trigger badge**, an **orange → action badge**, a dark label with `rule_summary()` text, and a **✕ delete icon**. Clicking the **rule label** reopens the rule for **consequence-only editing** (replaces the rule, keeping the same `id`); clicking the **✕** deletes the rule. Rules are auto-cleaned (`_cleanup_rules_for_nodes`) when any referenced object is deleted.
**Trigger area placement:** the spawn palette gains an **"Area"** entry (from the `"area"` registry id). Placement works like any other palette item (translucent ghost, grid snap, click to place). Trigger areas are selectable/movable like other objects via the duck-typed `get_area_rect` AABB branch in `stage_selection.gd` / `stage_spawner.gd`.
**`enqueue_reactive` semantics:** `StickmanRig.enqueue_reactive(actions: Array[Dictionary]) -> void` appends reactive actions to the rig's action queue. If the runner is `IDLE` it **resumes at the first newly-appended action** — the already-consumed prefix of the queue is not replayed. Sequential Phase 3a queues are untouched; reactive actions are a cross-object addition.
**Prop collision detection (two mechanisms):**
- **Geometric (stage-driven):** `_update_stickman_prop_collision` tests whether a stickman's **feet point** lies inside another object's world AABB (and vice-versa), edge-triggered so `collided` fires once per entry.
- **Physics (self-driven):** `PropBlock` enables `contact_monitor = true`, `max_contacts_reported = 8`, and connects the guarded `body_entered` signal → `_on_body_entered` → emits `collided(other)`, so **prop-vs-prop** collisions are detected by the physics engine rather than geometry.
Edge-triggered dictionaries (which events already fired) are reset on Play/Edit mode entry so a fresh simulation run re-arms every rule.
**Persistence scope:** rules are **in-memory only** — they persist across Play/Edit mode toggles (the stage keeps `_event_rules` alive) but are **not** saved to disk. Disk save is deferred to a future phase.
**Deferred (Phase 5 and beyond):** `explode_prop` / `spawn_prop` action types, rule **conditions** and AND/OR combinators, rule **variables**, and **disk save** of rules are explicitly out of scope for Phase 4.
## File format (`.stk`)
Files are UTF-8 JSON, pretty-printed with tab indentation. The format is versioned and designed to remain **backward/forward compatible** — new fields can be added without breaking older files.