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:
2026-09-18 14:11:53 -04:00
parent 9a22322dba
commit db4ff8d939
29 changed files with 2820 additions and 2747 deletions
+22 -13
View File
@@ -15,6 +15,7 @@ extends SceneTree
const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn")
const RIG := preload("res://scripts/stickman_rig.gd")
const LOCOMOTION := preload("res://scripts/stickman_locomotion.gd")
const SETTLE_FRAMES := 60
const MAX_FRAMES := 300
@@ -68,27 +69,33 @@ func _walk_case(label: String, target_feet: Vector2, expected_mode: String) -> v
_arrived_count = 0
rig.arrived.connect(_on_arrived)
rig.walk_to(target_feet, 200.0)
_check(not rig._walk_mode_latched, "%s: mode not latched before map sync" % label)
var locomotion: StickmanLocomotion = rig.get_locomotion()
_check(locomotion != null, "%s: locomotion component is available" % label)
if locomotion == null:
stage.queue_free()
await process_frame
return
_check(not locomotion._walk_mode_latched, "%s: mode not latched before map sync" % label)
var frames := 0
while not rig._walk_done and frames < MAX_FRAMES:
while not locomotion._walk_done and frames < MAX_FRAMES:
await physics_frame
frames += 1
_check(rig._walk_done, "%s: walk finished within %d frames (used %d)" % [label, MAX_FRAMES, frames])
_check(locomotion._walk_done, "%s: walk finished within %d frames (used %d)" % [label, MAX_FRAMES, frames])
_check(_arrived_count == 1, "%s: exactly one arrived signal (got %d)" % [label, _arrived_count])
_check(rig._walk_mode_latched, "%s: mode is latched" % label)
_check(rig._walk_mode == expected_mode,
"%s: mode latched to '%s' (got '%s')" % [label, expected_mode, rig._walk_mode])
_check(locomotion._walk_mode_latched, "%s: mode is latched" % label)
_check(locomotion._walk_mode == expected_mode,
"%s: mode latched to '%s' (got '%s')" % [label, expected_mode, locomotion._walk_mode])
# Final position equals the snapped root target (feet target + FOOT_OFFSET).
var final_root: Vector2 = target_feet + RIG.FOOT_OFFSET
var final_root: Vector2 = target_feet + LOCOMOTION.FOOT_OFFSET
_check(rig.global_position.distance_to(final_root) <= 0.01,
"%s: position snapped to final root (dist=%.3f)" % [label, rig.global_position.distance_to(final_root)])
# Body-bob settle: Torso marker restored to STAND_POSE (0,10).
# Completion freezes the solved final animation frame; it must not be reset
# or drift while the root is at rest.
var torso = rig.get_node_or_null(NodePath("IK_Targets/Torso"))
var frozen_torso_position: Vector2 = Vector2.ZERO
if torso != null:
var stand_torso: Dictionary = RIG.STAND_POSE["Torso"]
var expected_pos: Vector2 = stand_torso.get("pos", Vector2(0, 10))
_check((torso as Node2D).position.distance_to(expected_pos) <= 1.0,
"%s: Torso marker restored to standing pose (dist=%.3f)" % [label, (torso as Node2D).position.distance_to(expected_pos)])
frozen_torso_position = (torso as Node2D).position
_check(frozen_torso_position.is_finite(), "%s: Torso final animation marker is finite" % label)
else:
_check(false, "%s: IK_Targets/Torso missing" % label)
# Post-arrival stability for 60 physics frames.
@@ -101,6 +108,9 @@ func _walk_case(label: String, target_feet: Vector2, expected_mode: String) -> v
break
_check(settled and rig.global_position == p0,
"%s: position unchanged for %d frames after arrival" % [label, SETTLE_FRAMES])
if torso != null:
_check((torso as Node2D).position.distance_to(frozen_torso_position) <= 0.01,
"%s: Torso final animation marker remains frozen" % label)
_check(not rig.is_walking(), "%s: is_walking false after arrival" % label)
stage.queue_free()
@@ -114,4 +124,3 @@ func _check(condition: bool, message: String) -> void:
else:
_failures += 1
print("FAIL: " + message)