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
+18
View File
@@ -15,6 +15,7 @@ const TERRAIN_UTILS := preload("res://scripts/terrain_utils.gd")
const PROP_UTILS := preload("res://scripts/prop_utils.gd")
const PROP_BLOCK := preload("res://scripts/prop_block.gd")
const STICKMAN_FACTORY := preload("res://scripts/stickman_factory.gd")
const TRIGGER_AREA := preload("res://scripts/trigger_area.gd")
# ---------------------------------------------------------------------------
# Constants
@@ -87,6 +88,8 @@ func spawn(id: String, world_position: Vector2) -> Node2D:
return _spawn_prop(entry, pos)
"stickman":
return _spawn_stickman(pos)
"area":
return _spawn_area(pos)
_:
push_warning("StageSpawner: unknown spawn kind '%s'." % entry.get("kind", ""))
return null
@@ -98,6 +101,9 @@ func spawn(id: String, world_position: Vector2) -> Node2D:
static func get_world_aabb(node: Node2D) -> Rect2:
if node == null or not is_instance_valid(node):
return Rect2()
# Duck-typed TriggerArea: expose its centered local rect through the transform.
if node.has_method("get_area_rect"):
return node.global_transform * (node.call("get_area_rect") as Rect2)
var poly := node.get_node_or_null(NodePath("Polygon2D")) as Polygon2D
if poly != null and not poly.polygon.is_empty():
var rect := Rect2(node.to_global(poly.polygon[0]), Vector2.ZERO)
@@ -174,6 +180,10 @@ func _build_registry() -> void:
"id": "stickman", "label": "Stickman", "kind": "stickman",
"spawn_offset": STICKMAN_FOOT_OFFSET,
},
{
"id": "area", "label": "Area", "kind": "area",
"spawn_offset": Vector2.ZERO,
},
]
@@ -213,6 +223,14 @@ func _spawn_prop(entry: Dictionary, world_position: Vector2) -> PropBlock:
return PROP_UTILS.spawn_prop(_world, world_position, payload, preset, Vector2.ZERO)
func _spawn_area(world_position: Vector2) -> Node2D:
var area: Node2D = TRIGGER_AREA.new()
area.name = "TriggerArea"
area.position = world_position
_world.add_child(area)
return area
func _spawn_stickman(world_position: Vector2) -> StickmanRig:
if _stickman_data.is_empty():
push_warning("StageSpawner: no stickman data loaded; check '%s'." % DEFAULT_STICKMAN_PATH)