Implement Phase 8: Rig Relocation and Component Modularization
- Moved the rig scene from `res://master_rig.tscn` to `res://scenes/stickman_rig.tscn`, preserving its UID. - Split the `StickmanRig` controller into three components: `StickmanLocomotion`, `StickmanRagdoll`, and `StickmanActionRunner`. - Updated the architecture to ensure `StickmanRig` acts as the public facade for the new components. - Created a new test suite `test_phase8_rig_modularization.gd` to verify the integrity of the relocated rig and its components. - Added checks for scene loading, component resolution, action queue delegation, and state authority.
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
# * After Back the rule-builder state is IDLE/empty AND _action_popup is
|
||||
# visible again (PopupMenu.visible IS observable headless).
|
||||
#
|
||||
# Bug 3 (head mirror jitter): master_rig.tscn's Head LookAt modification is a
|
||||
# Bug 3 (head mirror jitter): scenes/stickman_rig.tscn's Head LookAt modification is a
|
||||
# full-range (-180..180), non-inverted, non-local-space constraint.
|
||||
# * Constraint flags/band are asserted directly on the instantiated rig.
|
||||
# * The Head marker is settled at the walk-left pose (-100,-614), then
|
||||
@@ -261,7 +261,7 @@ func _test_bug3_head_lookat() -> void:
|
||||
"head bone / Body/Head visual / Head aim marker all resolve")
|
||||
|
||||
# 2) Standing-still check at STAND_POSE: no frame-to-frame motion at all.
|
||||
var stand_pos: Vector2 = (RIG.STAND_POSE["Head"] as Dictionary)["pos"]
|
||||
var stand_pos: Vector2 = (StickmanRagdoll.STAND_POSE["Head"] as Dictionary)["pos"]
|
||||
head_target.position = stand_pos
|
||||
for i: int in WARMUP_FRAMES:
|
||||
await physics_frame
|
||||
@@ -316,7 +316,7 @@ func _measure_head(head_bone: Bone2D, head_body: Node2D, frames: int) -> Diction
|
||||
|
||||
func _test_bug4_reactive_actions() -> void:
|
||||
print("")
|
||||
print("--- Bug 4: enqueue_reactive tags + clear_reactive_actions restores ---")
|
||||
print("--- Bug 4: reactive runtime lane never mutates authored actions ---")
|
||||
var stage := _new_stage()
|
||||
var rig: StickmanRig = stage._spawner.spawn("stickman", Vector2(0, 0))
|
||||
_check(rig != null, "stickman rig spawns for the queue test")
|
||||
@@ -328,6 +328,7 @@ func _test_bug4_reactive_actions() -> void:
|
||||
rig.queue_action({ "type": "speak", "text": "authored-a", "duration": 2.0 })
|
||||
rig.queue_action({ "type": "wait", "duration": 1.0 })
|
||||
_check(rig.queue_size() == 2, "authored queue holds 2 actions")
|
||||
var runner := rig.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
|
||||
# Cycle 1: a reactive action is injected (rule firing during PLAY), which
|
||||
# auto-starts the runner; leaving PLAY stops the queue first, then
|
||||
@@ -336,13 +337,13 @@ func _test_bug4_reactive_actions() -> void:
|
||||
rig.queue_changed.connect(func(): qc1.append(true))
|
||||
var r1: Array[Dictionary] = [{ "type": "speak", "text": "reactive-1", "duration": 1.0 }]
|
||||
rig.enqueue_reactive(r1)
|
||||
_check(rig.queue_size() == 3,
|
||||
"reactive injection grows the queue to 3 (got %d)" % rig.queue_size())
|
||||
_check(_count_reactive(rig) == 1,
|
||||
"injected action is tagged reactive=true")
|
||||
_check(rig.is_queue_running(),
|
||||
"enqueue_reactive auto-resumes the runner from IDLE")
|
||||
_check(qc1.size() == 1, "enqueue_reactive emits queue_changed")
|
||||
_check(rig.queue_size() == 2,
|
||||
"reactive injection leaves the authored queue at 2 (got %d)" % rig.queue_size())
|
||||
_check(runner._run_queue.size() == 2 and _runtime_reactive_count(runner) == 0 and not runner._overlay_speech.is_empty(),
|
||||
"overlay speak leaves only authored items in the primary runtime lane")
|
||||
_check(not rig.is_queue_running(),
|
||||
"overlay speak does not start the primary queue")
|
||||
_check(qc1.is_empty(), "runtime-only injection does not emit authored queue_changed")
|
||||
|
||||
rig.stop_queue()
|
||||
rig.clear_reactive_actions()
|
||||
@@ -351,7 +352,7 @@ func _test_bug4_reactive_actions() -> void:
|
||||
_check(_action_types(rig) == ["speak", "wait"],
|
||||
"queue types after clear are exactly the authored actions (got %s)"
|
||||
% str(_action_types(rig)))
|
||||
_check(qc1.size() == 2, "clear_reactive_actions emits queue_changed on change")
|
||||
_check(qc1.is_empty(), "runtime cleanup does not emit authored queue_changed")
|
||||
|
||||
# Cycle 2: a bigger reactive burst then stop + clear again - counts must
|
||||
# stay stable (no 1,2,3 accumulation across repeated Play sessions).
|
||||
@@ -360,10 +361,10 @@ func _test_bug4_reactive_actions() -> void:
|
||||
{ "type": "ragdoll" },
|
||||
]
|
||||
rig.enqueue_reactive(r2)
|
||||
_check(rig.queue_size() == 4,
|
||||
"second injection grows to 4 (got %d)" % rig.queue_size())
|
||||
_check(_count_reactive(rig) == 2,
|
||||
"second burst tags both actions reactive")
|
||||
_check(rig.queue_size() == 2 and runner._run_queue.size() == 3,
|
||||
"takeover keeps 2 authored future items behind one reactive action")
|
||||
_check(bool(runner._run_queue[0].get("reactive", false)),
|
||||
"takeover action leads the runtime lane as reactive")
|
||||
rig.stop_queue()
|
||||
rig.clear_reactive_actions()
|
||||
_check(rig.queue_size() == 2,
|
||||
@@ -372,25 +373,25 @@ func _test_bug4_reactive_actions() -> void:
|
||||
"queue types stay stable across two reactive cycles (got %s)"
|
||||
% str(_action_types(rig)))
|
||||
|
||||
# clear_reactive_actions() must be a no-op while the runner is executing.
|
||||
# Runtime cleanup may run while executing, but it never mutates authored work.
|
||||
rig.start_queue()
|
||||
_check(rig.is_queue_running(), "runner is EXECUTING after start_queue")
|
||||
var r3: Array[Dictionary] = [{ "type": "recover" }]
|
||||
rig.enqueue_reactive(r3)
|
||||
_check(rig.queue_size() == 3,
|
||||
"reactive action can still append while the runner is executing")
|
||||
_check(rig.queue_size() == 2 and bool(runner._run_queue[0].get("reactive", false)),
|
||||
"reactive takeover enters runtime lane while authored queue stays unchanged")
|
||||
var qc3: Array = []
|
||||
rig.queue_changed.connect(func(): qc3.append(true))
|
||||
rig.clear_reactive_actions()
|
||||
_check(rig.queue_size() == 3 and _count_reactive(rig) == 1,
|
||||
"clear_reactive_actions is a no-op while EXECUTING (reactive action stays)")
|
||||
_check(rig.queue_size() == 2 and _runtime_reactive_count(runner) == 0,
|
||||
"clear_reactive_actions removes runtime reactive work while preserving authored actions")
|
||||
_check(qc3.is_empty(),
|
||||
"no queue_changed emitted by the EXECUTING no-op")
|
||||
"runtime cleanup emits no authored queue_changed")
|
||||
rig.stop_queue()
|
||||
_check(not rig.is_queue_running(), "runner returns to IDLE after stop_queue")
|
||||
rig.clear_reactive_actions()
|
||||
_check(rig.queue_size() == 2 and _count_reactive(rig) == 0,
|
||||
"after stop, clear drops the injected reactive action (queue 2)")
|
||||
_check(rig.queue_size() == 2 and _runtime_reactive_count(runner) == 0,
|
||||
"after stop, authored queue remains stable at 2")
|
||||
|
||||
await _free_stage(stage)
|
||||
|
||||
@@ -407,6 +408,14 @@ func _count_reactive(rig: StickmanRig) -> int:
|
||||
return n
|
||||
|
||||
|
||||
func _runtime_reactive_count(runner: StickmanActionRunner) -> int:
|
||||
var count := 0
|
||||
for item: Dictionary in runner._run_queue:
|
||||
if bool(item.get("reactive", false)):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _action_types(rig: StickmanRig) -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
for a: Dictionary in rig.get_queue():
|
||||
|
||||
Reference in New Issue
Block a user