Phase 4.4 Diagnostic — Trigger Areas Not Firing markdown # Phase 4.4 Diagnostic — Trigger Areas Not Firing ## 1. Overview The AGENTS.md documentation claims `_update_area_entry()` exists and evaluates `entered_area` rules, but the user reports **nothing happens when a rule is applied to an area**. This plan is a diagnostic + fix procedure to isolate and repair the break. ## 2. Suspected Root Causes (Ranked) | # | Suspected Cause | Symptom | | --- | ---------------------------------------------------------- | ------------------------------------------ | | 1 | `_update_area_entry()` never called in PLAY | No events fire at all | | 2 | Rule's `trigger.target` reference goes stale (bad weakref) | Event fires but action target invalid | | 3 | Edge-trigger dictionary uses wrong key | Event fires once, then never re-fires | | 4 | Area rect coordinates wrong (local vs. global) | Stickman visually inside, geometry says no | | 5 | `TriggerArea` not a child of `World` | Detection loop never sees the area | | 6 | Rule builder doesn't actually save `entered_area` rules | Rule appears created, `_event_rules` empty | | 7 | Stickman's feet anchor wrong | Uses bone position instead of feet | ## 3. Diagnostic Procedure (Do First) Ask the developer to add **4 temporary debug prints** and run a test in PLAY mode. Report which prints appear. ### Print 1 — Top of `_update_area_entry()` ```gdscript print("[AREA] checking ", _world.get_child_count(), " world children") ``` If this never prints: \_update_area_entry() is not called from \_physics_process in PLAY. Check the call site. Print 2 — Before rule matching ```gdscript print("[AREA] area=", area.name, " rect=", area.get_area_rect(), " movable=", movable.name, " pos=", pos) ``` If this never prints: the detection loop isn't iterating areas, or the area isn't a TriggerArea child of World. Print 3 — When a rule matches ```gdscript print("[RULE] matched rule id=", rule.id, " for area=", area.name) ``` If this never prints but Print 2 does: the rule's trigger.target.get_ref() doesn't equal area. Reference is stale or wrong type. Print 4 — Before action executes ```gdscript print("[ACTION] executing ", action.type, " on target=", action.target.get_ref()) ``` If this never prints but Print 3 does: the rule matched but actions array is empty, or execution is guarded out. 4. Fix Plan (Per Root Cause) Fix 1 — Missing \_physics_process Call The event engine must run only in PLAY mode: ```gdscript func _physics_process(delta: float) -> void: if current_mode != StageMode.PLAY: return _update_area_entry() _update_stickman_prop_collision() ``` Verify the call exists and is guarded by the PLAY check. Fix 2 — Stale trigger.target Reference Rules must store a live weakref to the TriggerArea node, not a serialized position or instance ID. At rule creation: ```gdscript rule.trigger.target = weakref(area_node) ``` At evaluation: ```gdscript var target = rule.trigger.target.get_ref() if target == null: continue # skip stale rule if target != area: continue ``` If the reference is stored as anything else, rewrite the rule builder to use weakref(). Fix 3 — Wrong Edge-Trigger Key The edge-trigger dictionary must key on BOTH the area ID and the movable ID: ```gdscript var edge_key := str(area.get_instance_id()) + "_" + str(movable.get_instance_id()) if _area_fired_events.has(edge_key): continue _area_fired_events[edge_key] = true # fire rules... ``` Using only the area ID means only one stickman can fire the event. Using only the movable ID means the same stickman can't trigger two different areas. Fix 4 — Wrong Area Coordinates TriggerArea.get_area_rect() must return a world-space Rect2: ```gdscript func get_area_rect() -> Rect2: return Rect2(global_position - size * 0.5, size) ``` Do not use position (local) — the area may be a child of a moved parent. For the movable's position, use the stickman's feet: ```gdscript var pos := stickman.global_position + StickmanRig.FOOT_OFFSET ``` Fix 5 — Area Not in World The palette spawner must add the TriggerArea as a direct child of World, not \_ghost_holder or any other intermediate node. The spawner's commit path (\_place_at) should call: ```gdscript _world.add_child(area) ``` Verify the placement path — the ghost is spawned first (into \_ghost_holder), then the actual object is added to World on commit. If the commit path is broken for areas specifically, fix \_spawn_area(). Fix 6 — Rule Builder Doesn't Save entered_area The "⚡ When..." flow must actually append a rule to \_event_rules with: ```gdscript { "id": rule_id, "trigger": { "type": "entered_area", "target": weakref(area_node), }, "actions": [ ... ] } ``` Add a debug print in \_finalize_rule() to verify the rule is committed with the correct shape. Fix 7 — Wrong Feet Anchor Use stickman.global_position + StickmanRig.FOOT_OFFSET for the "position" tested against the area rect. Do not use bone positions or the rig root alone. 5. Acceptance Criteria - [ ] Placing an Area via the palette creates a TriggerArea in World. - [ ] A rule with an entered_area trigger can be created (targeting a specific area). - [ ] Walking a stickman into the area in PLAY fires the rule. - [ ] The rule's action executes on the correct target. - [ ] The event fires once per entry (edge-triggered). - [ ] Leaving and re-entering the area re-fires the event. - [ ] Multiple areas + multiple stickmen work independently. - [ ] No console warnings/errors when Play starts. 6. Test Cases | # | Test | Expected | | --- | ---------------------------------------------- | --------------------------- | | 1 | Stickman walks into area speaks | Speech bubble appears once | | 2 | Stickman walks out and back in speaks again | Speech bubble appears twice | | 3 | Stickman stands still in area for 5s no repeat | Speech bubble appears once | | 4 | Two stickmen enter same area each fires | Two events fire | | 5 | Stickman enters area with no rule | Nothing happens, no errors | | 6 | Rule targets deleted area | No crash warning logged | | 7 | Area moved in Edit | Play New position used | | 8 | 5 areas + 5 stickmen + 5 rules | All fire independently | 7. Deliverable A fix commit that resolves whichever of the 7 root causes the 4 debug prints isolate. Include a brief note in the commit message describing which root cause was found and what was changed. 8. Out of Scope New trigger types (e.g., exited_area, stay_in_area).