From 31282313be2ff3b8fcdde13db15cb7aa6ad5dd66 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Fri, 11 Sep 2026 14:17:08 -0400 Subject: [PATCH] docs: Add diagnostic plan for Trigger Areas not firing, including root causes and fix procedures --- plans/PHASE_4c_AREA.md | 176 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 plans/PHASE_4c_AREA.md diff --git a/plans/PHASE_4c_AREA.md b/plans/PHASE_4c_AREA.md new file mode 100644 index 0000000..6878f00 --- /dev/null +++ b/plans/PHASE_4c_AREA.md @@ -0,0 +1,176 @@ +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).