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:
@@ -337,6 +337,11 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
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).
|
||||
- **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
|
||||
queue and, if the runner is `IDLE`, resumes at the **first newly-appended action** (no replay of
|
||||
the already-consumed queue prefix). Sequential Phase 3a queues are untouched.
|
||||
- `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:
|
||||
@@ -510,6 +515,16 @@ 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).
|
||||
- `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 on the node's global position), and `_draw()` (translucent green fill +
|
||||
dashed border). **No physics and no signals** — it is a pure geometric region evaluated by
|
||||
`sandbox_stage.gd`'s event engine (`_update_area_entry`) for `entered_area` rules.
|
||||
- `scripts/prop_utils.gd` — `class_name PropUtils`, `extends RefCounted`; static factory
|
||||
(**not used by the editor**):
|
||||
- `create_box(size := Vector2(48,48))` / `create_ball(radius := 24.0)` /
|
||||
@@ -616,6 +631,20 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`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` →
|
||||
`mark_dirty()`. `_set_build_controls_visible()` also hides the Direct button in PLAY.
|
||||
- **Phase 4 triggers & event system:** adds a When→Then **rule system** — `_event_rules:
|
||||
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
|
||||
(`_update_area_entry` tests a movable's feet/position against each `TriggerArea.get_area_rect()`;
|
||||
`_update_stickman_prop_collision` tests a stickman's feet point against prop AABBs and vice-versa),
|
||||
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
|
||||
action popup: trigger sub-menu popup → trigger-target click → rule-action popup → target/position
|
||||
→ "Add another action / Done" popup. **Esc** has highest priority; status-bar hints + toast
|
||||
messages guide the flow. Rule **label click** → consequence-only edit (replaces the rule, same
|
||||
`id`); **✕** deletes; `_cleanup_rules_for_nodes` auto-removes rules referencing deleted objects.
|
||||
- `scripts/stickman_speech_bubble.gd` — `class_name SpeechBubble`, `extends Node2D`; a **world-space
|
||||
speech bubble** drawn in `_draw()` (Phase 3a, **not used by the editor**). Child of a `StickmanRig`
|
||||
at `SPEECH_BUBBLE_OFFSET` (above the head), so it follows the figure and scales with the camera.
|
||||
@@ -637,6 +666,11 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
(start at rig feet + `FOOT_OFFSET`; each `walk_to` advances the anchor; non-walk anchors at the
|
||||
current position), consecutive badges stacking `(0, -28)/zoom`. All sizes divided by `_zoom()` so
|
||||
markers stay screen-constant; pure `_draw()`, no hit-testing; hidden in PLAY via `set_enabled(false)`.
|
||||
- **Phase 4 rule visualization:** `_draw()` also renders `_event_rules` (set via `set_rules()`) —
|
||||
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** (the label + icon are hit-testable via `hit_test_rule()`, `Vector2.INF` sentinel from
|
||||
`hit_test_waypoint()`); click-label → consequence-only edit, click-✕ → delete.
|
||||
- `scripts/stage_spawner.gd` — `class_name StageSpawner`, `extends RefCounted`; registry-driven
|
||||
spawner (Phase 2). A `_registry: Array[Dictionary]` maps ids to terrain/prop/stickman templates;
|
||||
adding a type = appending an entry (no hard-coded id `match`). Reuses `TerrainUtils.spawn_block`,
|
||||
@@ -647,6 +681,9 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`_init`) and applies `STICKMAN_FOOT_OFFSET (0, -385)` so feet land at the cursor. Also exposes
|
||||
a `static get_world_aabb(node)` helper (for a stickman it unions the mounted `Body/*` shape
|
||||
geometry via a recursive `_collect_visual_points()` so the box is centered head-to-feet).
|
||||
- **Phase 4 area palette entry:** a new `"area"` registry entry (label "Area") → `_spawn_area()`
|
||||
instantiates a `TriggerArea`; `get_world_aabb()` gains a **duck-typed** `get_area_rect` AABB
|
||||
branch (if the node responds to `get_area_rect()`, use its `Rect2` as the world bounds).
|
||||
- `scripts/stage_selection.gd` — `class_name StageSelection`, `extends RefCounted`; hover/click/
|
||||
box selection via geometric world-space AABB hit-testing (Phase 2). `static get_world_aabb`
|
||||
unions a `Polygon2D` child's world points (terrain/props) or, for a stickman rig, recursively
|
||||
@@ -656,7 +693,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
smallest area breaks ties. `_is_selectable` excludes the `RagdollBodyContainer` subtree. Signals
|
||||
`hover_changed(node)` / `selection_changed(nodes)`; public API `get_selected`/`get_primary`/
|
||||
`clear_selection`/`select_only`/`add_to_selection`/`toggle_selection`/`is_selected`/`hit_test`/
|
||||
`update_hover`/`box_select`.
|
||||
`update_hover`/`box_select`. (Phase 4) `get_world_aabb` also handles `TriggerArea` objects via
|
||||
the same **duck-typed** `get_area_rect` AABB branch, so trigger areas are selectable/hit-testable.
|
||||
- `scripts/stage_gizmos.gd` — `class_name StageGizmos`, `extends Node2D`; hover highlight +
|
||||
selection outlines + a rotate ring (Phase 2). **No translate handle** — objects are dragged
|
||||
directly by the root; `set_targets(nodes: Array[Node2D])` holds the current multi-selection,
|
||||
|
||||
Reference in New Issue
Block a user