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:
+24
-15
@@ -73,8 +73,10 @@ func _add_rule(stage: Node2D, source: Node2D, area: TriggerArea, target: Stickma
|
||||
|
||||
|
||||
func _clear_actions(rig: StickmanRig) -> void:
|
||||
rig.action_queue.clear()
|
||||
rig.stop_queue()
|
||||
rig.clear_queue()
|
||||
var runner := rig.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
runner.hide_speech()
|
||||
|
||||
|
||||
func _test_spawner_and_rule_contract() -> void:
|
||||
@@ -102,31 +104,35 @@ func _test_source_specific_edges() -> void:
|
||||
var world_area_rect: Rect2 = (area.global_transform * area.get_area_rect()).abs()
|
||||
_check(not world_area_rect.has_point(stage._movable_point(source)), "regression setup keeps feet outside the leg-level area")
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.size() == 1, "source crossing queues exactly one consequence")
|
||||
var action: Dictionary = target.action_queue[0]
|
||||
_check(action.get("type", "") == "speak" and action.get("text", "") == "source only" and bool(action.get("reactive", false)), "speak consequence is normalized as a reactive action")
|
||||
var runner := target.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
_check(not runner._overlay_speech.is_empty(), "source crossing starts one overlay consequence")
|
||||
_check(String(runner._overlay_speech.get("text", "")) == "source only", "speak consequence is normalized into overlay speech")
|
||||
# SceneTree.physics_frame is emitted before node _physics_process callbacks;
|
||||
# wait through the following tick so the runner can begin the action.
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
_check(target._speech_active and target._speech_bubble != null and target._speech_bubble.visible, "queued consequence executes as a visible speech bubble")
|
||||
_check(runner._speech_bubble != null and runner._speech_bubble.visible, "queued consequence executes as a visible speech bubble")
|
||||
runner._overlay_speech["time_left"] = 0.5
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.size() == 1, "polling while inside does not repeat")
|
||||
_check(is_equal_approx(float(runner._overlay_speech.get("time_left", 0.0)), 0.5), "polling while inside does not repeat")
|
||||
_move_rig_body_center(source, Vector2(-1500.0, 0.0))
|
||||
stage._update_area_entry()
|
||||
# The swept-bounds detector deliberately retains the previous pose for one
|
||||
# poll, so a second far-away sample commits the exit edge.
|
||||
stage._update_area_entry()
|
||||
_move_rig_body_center(source, area.global_position)
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.size() == 2, "exit then re-entry queues one additional consequence")
|
||||
_check(is_equal_approx(float(runner._overlay_speech.get("time_left", 0.0)), 1.0), "exit then re-entry restarts the consequence")
|
||||
_clear_actions(target)
|
||||
_move_rig_body_center(other, area.global_position)
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.is_empty(), "different stickman cannot trigger source-specific rule")
|
||||
_check(runner._overlay_speech.is_empty(), "different stickman cannot trigger source-specific rule")
|
||||
var prop := stage._spawner.spawn("prop", Vector2(-1500.0, 0.0)) as PropBlock
|
||||
stage._finalize_placed_node(prop)
|
||||
prop.freeze = false
|
||||
prop.global_position = Vector2.ZERO
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.is_empty(), "unfrozen prop cannot trigger source-specific rule")
|
||||
_check(runner._overlay_speech.is_empty(), "unfrozen prop cannot trigger source-specific rule")
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
@@ -141,16 +147,18 @@ func _test_wildcard_and_pair_independence() -> void:
|
||||
stage._update_area_entry()
|
||||
_move_rig_body_center(source_a, area_a.global_position)
|
||||
stage._update_area_entry()
|
||||
var runner := target.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
runner._overlay_speech["time_left"] = 0.25
|
||||
_move_rig_body_center(source_a, Vector2(-1500.0, 0.0))
|
||||
stage._update_area_entry()
|
||||
_move_rig_body_center(source_b, area_a.global_position)
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.size() == 2, "wildcard source fires independently for multiple movables")
|
||||
_check(is_equal_approx(float(runner._overlay_speech.get("time_left", 0.0)), 1.0), "wildcard source fires independently for multiple movables")
|
||||
_clear_actions(target)
|
||||
_add_rule(stage, source_b, area_b, target, "second pair")
|
||||
_move_rig_body_center(source_b, area_b.global_position)
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.size() == 1, "second area/source pair has independent edge state")
|
||||
_check(String(runner._overlay_speech.get("text", "")) == "second pair", "second area/source pair has independent edge state")
|
||||
await _free_stage(stage)
|
||||
|
||||
|
||||
@@ -163,15 +171,16 @@ func _test_transforms_points_and_cleanup() -> void:
|
||||
var source := _spawn_rig(stage, Vector2(-300.0, 0.0))
|
||||
var target := _spawn_rig(stage, Vector2(2000.0, 0.0))
|
||||
_add_rule(stage, source, area, target, "transformed")
|
||||
_check(stage._movable_point(source) == source.global_position - STICKMAN_RIG.FOOT_OFFSET, "stickman overlap point is feet, not root")
|
||||
_check(stage._movable_point(source) == source.global_position - StickmanLocomotion.FOOT_OFFSET, "stickman overlap point is feet, not root")
|
||||
_move_rig_body_center(source, area.global_position)
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.size() == 1, "current transformed area overlaps visible stickman geometry")
|
||||
var runner := target.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
_check(String(runner._overlay_speech.get("text", "")) == "transformed", "current transformed area overlaps visible stickman geometry")
|
||||
_clear_actions(target)
|
||||
var no_rule_area := _spawn_area(stage, Vector2(-500.0, 0.0))
|
||||
source.global_position = no_rule_area.global_position + STICKMAN_RIG.FOOT_OFFSET
|
||||
source.global_position = no_rule_area.global_position + StickmanLocomotion.FOOT_OFFSET
|
||||
stage._update_area_entry()
|
||||
_check(target.action_queue.is_empty(), "area with no matching rule is a no-op")
|
||||
_check(runner._overlay_speech.is_empty(), "area with no matching rule is a no-op")
|
||||
var stale_rule := _add_rule(stage, source, area, target, "stale")
|
||||
var stale_id := target.get_instance_id()
|
||||
target.free()
|
||||
|
||||
Reference in New Issue
Block a user