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:
@@ -0,0 +1,501 @@
|
||||
# Phase 8 — Rig Relocation and Runtime Component Modularization
|
||||
|
||||
Status: architecture proposed; awaiting user approval before implementation.
|
||||
|
||||
Source proposal: `plans/PHASE_8_RIG_MODULARIZATION.md`.
|
||||
|
||||
## 1. Outcome and architectural correction
|
||||
|
||||
Move the authored rig scene from `res://master_rig.tscn` to
|
||||
`res://scenes/stickman_rig.tscn` while preserving its existing scene UID
|
||||
(`uid://dr5ef3l2var2s`). Split the 3,159-line `StickmanRig` controller into three child
|
||||
components, but keep `StickmanRig` as the only public facade used by the factory, stage,
|
||||
harnesses, scene I/O, and existing gameplay code.
|
||||
|
||||
One boundary in the source plan should change before implementation: navigation and
|
||||
`walk_to()` belong in `StickmanLocomotion`, not `StickmanActionRunner`. Walking, terrain
|
||||
following, ledge momentum, landing, animation pause/resume, and root displacement share one
|
||||
state machine and one `AnimationPlayer`. Splitting path following from terrain motion would
|
||||
create two writers for the rig transform and require a per-frame synchronization protocol.
|
||||
`StickmanActionRunner` should orchestrate actions and call the locomotion API; it should not
|
||||
implement movement.
|
||||
|
||||
The extraction is structural only. It must not change movement tuning, recovery math, action
|
||||
payloads, signal timing, saved scene data, or editor UI.
|
||||
|
||||
## 2. Targets and ownership
|
||||
|
||||
| Target | Ownership after Phase 8 |
|
||||
| --- | --- |
|
||||
| `scenes/stickman_rig.tscn` | Renamed rig scene. Keeps the existing `Master`, `Body`, `Skeleton2D`, `IK_Targets`, `AnimationTargets`, `AnimationPlayer`, and `AnimationTree` paths. Adds a nonvisual `RigComponents` branch. |
|
||||
| `scripts/stickman_rig.gd` (`StickmanRig`) | Public compatibility facade; authoritative `RigState`; facing profile, bend flags, IK modification resolution, body z-order, root mirror, head pin, shared node lookup, component wiring, deterministic physics scheduling, and public signal forwarding. |
|
||||
| `scripts/stickman_locomotion.gd` (`StickmanLocomotion`) | Navigation agent, walking, terrain probes/following, airborne and landing states, root movement, gait/foot contacts, animated collision proxies, airborne sweeps, and impact reactions. |
|
||||
| `scripts/stickman_ragdoll.gd` (`StickmanRagdoll`) | Momentum tracking, ragdoll definitions and procedural bodies/joints, rest detection, pre-ragdoll/captured-pose data, recovery snap/tween, and ragdoll impulse application. |
|
||||
| `scripts/stickman_action_runner.gd` (`StickmanActionRunner`) | Authored/runtime action queues, reactive injection, action phase machine, wait timing, speech bubble and overlay arbitration, and action/queue/speech signals. |
|
||||
| `scripts/stickman_factory.gd` | Loads the relocated scene; return type and `spawn*` API stay unchanged. |
|
||||
| `scripts/stk_rig_adapter.gd` | No node-path changes. Comments reference the relocated scene. It continues to adapt the root before `_ready()`. |
|
||||
| `scripts/create_animations.gd`, harnesses, tests, active docs | Update live scene-path literals/comments and component-aware tests. |
|
||||
|
||||
`source_stk_path` remains on the root facade because it is instance provenance consumed by
|
||||
stage serialization, not behavior owned by a component.
|
||||
|
||||
No layout, theme, visible node, or user-facing UI change is required.
|
||||
|
||||
## 3. Scene structure
|
||||
|
||||
The existing visual and skeleton hierarchy must remain byte-for-byte equivalent apart from the
|
||||
new component branch and external component script resources. Adapter and harness node paths
|
||||
therefore remain valid.
|
||||
|
||||
```text
|
||||
Master: Node2D [StickmanRig]
|
||||
├── RigComponents: Node
|
||||
│ ├── Locomotion: Node [StickmanLocomotion]
|
||||
│ ├── Ragdoll: Node [StickmanRagdoll]
|
||||
│ └── ActionRunner: Node [StickmanActionRunner]
|
||||
├── Body: Node2D
|
||||
│ └── Body / Head / 8 limb visual containers (unchanged)
|
||||
├── Skeleton2D (unchanged)
|
||||
│ └── Torso/Head + 8 limb Bone2D nodes
|
||||
├── RayCast_Ground (unchanged; legacy)
|
||||
├── IK_Targets (unchanged)
|
||||
├── AnimationTargets (unchanged)
|
||||
├── AnimationPlayer (unchanged)
|
||||
└── AnimationTree (unchanged)
|
||||
|
||||
world/stage parent
|
||||
└── RagdollBodyContainer (runtime only; unchanged location)
|
||||
├── 10 RigidBody2D
|
||||
└── 9 PinJoint2D
|
||||
```
|
||||
|
||||
The components are authored children rather than created in `_ready()`. Missing component nodes
|
||||
produce one `StickmanRig:` warning per missing node and disable only that subsystem. The root must
|
||||
never manufacture a fallback component silently because that would hide a broken scene.
|
||||
|
||||
## 4. Dependency direction and state authority
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C[StickmanRig facade/coordinator]
|
||||
L[StickmanLocomotion]
|
||||
R[StickmanRagdoll]
|
||||
A[StickmanActionRunner]
|
||||
U[Stage / Factory / Harness / Scene IO]
|
||||
|
||||
U -->|existing public API| C
|
||||
C -->|configure + deterministic ticks| L
|
||||
C -->|configure + commands| R
|
||||
C -->|configure + commands| A
|
||||
A -->|walk/speech-independent movement query| L
|
||||
A -->|ragdoll_requested / recovery_requested| C
|
||||
R -->|state_transition_requested| C
|
||||
R -->|ground query + recovery reanchor| L
|
||||
L -->|arrived / facing_requested| C
|
||||
C -->|forwarded public signals| U
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. `StickmanRig.state` is the only high-level state authority. Components receive the current
|
||||
state during commands/ticks and never write it directly.
|
||||
2. `StickmanActionRunner` has no reference to `StickmanRagdoll`; it requests transitions through
|
||||
root-connected signals. This allows queue tests without constructing physics bodies.
|
||||
3. `StickmanRagdoll` may synchronously call only the narrow locomotion recovery/query API; it
|
||||
must not inspect locomotion private fields.
|
||||
4. `StickmanLocomotion` requests facing changes by signal so the facade remains the profile and
|
||||
mirror authority.
|
||||
5. The components do not implement `_physics_process()`. The root is the sole scheduler. This
|
||||
preserves the current update order and avoids relying on Godot sibling processing order.
|
||||
|
||||
## 5. Static component contracts
|
||||
|
||||
The signatures below are the implementation contract. Internal helper names may be retained
|
||||
during mechanical extraction, but cross-component calls must use only these methods/signals.
|
||||
|
||||
### 5.1 `StickmanLocomotion`
|
||||
|
||||
```gdscript
|
||||
class_name StickmanLocomotion
|
||||
extends Node
|
||||
|
||||
signal arrived(target: Vector2)
|
||||
signal facing_requested(profile: int)
|
||||
|
||||
func configure(rig: StickmanRig) -> void
|
||||
func tick_movement(delta: float, rig_state: int) -> void
|
||||
func tick_post_skeleton(delta: float, rig_state: int) -> void
|
||||
|
||||
func walk_to(target: Vector2, speed: float = -1.0) -> bool
|
||||
func cancel_walk(restore_idle_pose: bool = true) -> void
|
||||
func is_walking() -> bool
|
||||
func consume_walk_completed() -> bool
|
||||
|
||||
func begin_terrain_fall() -> void
|
||||
func set_terrain_following_enabled(enabled: bool) -> void
|
||||
func is_action_blocked() -> bool
|
||||
func prepare_for_ragdoll() -> void
|
||||
func restore_after_recovery() -> void
|
||||
|
||||
func probe_ground_y(
|
||||
world_x: float,
|
||||
top_y: float,
|
||||
bottom_y: float,
|
||||
exclude: Array[RID]
|
||||
) -> float
|
||||
func reanchor_for_recovery(feet_world: Vector2, foot_offset: Vector2) -> Vector2
|
||||
|
||||
func set_collision_bodies_enabled(enabled: bool) -> void
|
||||
func apply_collision_reaction(
|
||||
part_name: String,
|
||||
normal: Vector2,
|
||||
impact_speed: float,
|
||||
contact_position: Vector2 = Vector2.INF
|
||||
) -> void
|
||||
```
|
||||
|
||||
`tick_movement()` owns the old clear-pelvis → walking → terrain → settle sequence.
|
||||
`tick_post_skeleton()` owns impact-reaction application and collision-proxy synchronization,
|
||||
which must remain after the facade's head pin. `reanchor_for_recovery()` moves the root and
|
||||
returns the mirror-aware rig-local shift used by `StickmanRagdoll` to rebase captured body data.
|
||||
|
||||
The locomotion component is the source of truth for `TerrainMotion`, navigation, terrain/air
|
||||
tuning constants, collision proxy tables, and reaction constants. Compatibility aliases may
|
||||
remain on `StickmanRig` for one phase, but they must point to component constants rather than
|
||||
duplicate values.
|
||||
|
||||
### 5.2 `StickmanRagdoll`
|
||||
|
||||
```gdscript
|
||||
class_name StickmanRagdoll
|
||||
extends Node
|
||||
|
||||
signal state_transition_requested(next_state: int)
|
||||
signal rest_state_changed(at_rest: bool)
|
||||
|
||||
func configure(rig: StickmanRig, locomotion: StickmanLocomotion) -> void
|
||||
func tick_pre_movement(delta: float, rig_state: int) -> void
|
||||
|
||||
func enter_ragdoll() -> bool
|
||||
func request_recovery() -> bool
|
||||
func snap_to_standing() -> bool
|
||||
func cancel_recovery() -> void
|
||||
func apply_velocity_boost(velocity: Vector2) -> void
|
||||
func is_at_rest() -> bool
|
||||
func initialize_spawn_pose_from_skeleton() -> void
|
||||
```
|
||||
|
||||
`enter_ragdoll()` and recovery methods return whether they actually accepted the transition.
|
||||
They perform the physical handoff before emitting `state_transition_requested`; the root then
|
||||
commits `state` and emits the legacy `state_changed` signal exactly once. The component owns the
|
||||
existing `RAGDOLL_BODIES`, `RAGDOLL_JOINTS`, per-profile joint bands, recovery overrides,
|
||||
captured dictionaries, tween generation, and procedural container lifetime.
|
||||
|
||||
The root provides narrow internal skeleton services used by recovery:
|
||||
|
||||
```gdscript
|
||||
func _get_skeleton() -> Skeleton2D
|
||||
func _get_body_container() -> Node2D
|
||||
func _get_animation_player() -> AnimationPlayer
|
||||
func _get_ik_marker(name: String) -> Marker2D
|
||||
func _get_limb_bones() -> Dictionary
|
||||
func _get_bend_modifications() -> Dictionary
|
||||
func _get_head_bone() -> Bone2D
|
||||
func _get_head_look_at() -> SkeletonModification2DLookAt
|
||||
func _set_limb_mods_enabled(enabled: bool) -> void
|
||||
func _restore_head_lookat_state() -> void
|
||||
func _rearm_ik_stack() -> void
|
||||
```
|
||||
|
||||
These methods are internal collaboration points, not new consumer APIs.
|
||||
|
||||
### 5.3 `StickmanActionRunner`
|
||||
|
||||
```gdscript
|
||||
class_name StickmanActionRunner
|
||||
extends Node
|
||||
|
||||
signal action_started(action: Dictionary, index: int)
|
||||
signal action_finished(action: Dictionary, index: int)
|
||||
signal queue_finished
|
||||
signal queue_changed
|
||||
signal speech_finished
|
||||
signal ragdoll_requested(enabled: bool)
|
||||
signal recovery_requested
|
||||
|
||||
var action_queue: Array[Dictionary] = []
|
||||
|
||||
func configure(rig: StickmanRig, locomotion: StickmanLocomotion) -> void
|
||||
func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void
|
||||
|
||||
func speak(text: String, duration: float) -> void
|
||||
func queue_action(action: Dictionary) -> void
|
||||
func clear_queue() -> void
|
||||
func get_queue() -> Array[Dictionary]
|
||||
func remove_action(index: int) -> void
|
||||
func insert_action(index: int, action: Dictionary) -> void
|
||||
func queue_size() -> int
|
||||
func enqueue_reactive(actions: Array[Dictionary]) -> void
|
||||
func clear_reactive_actions() -> void
|
||||
func start_queue() -> void
|
||||
func stop_queue() -> void
|
||||
func is_queue_running() -> bool
|
||||
func sync_facing(profile: int) -> void
|
||||
```
|
||||
|
||||
`RunnerState` and `ActionPhase` are component-owned. Root keeps enum aliases/definitions only
|
||||
where required for source compatibility. The component owns both primary and overlay speech,
|
||||
creates `SpeechBubble` under `Master` (not under `RigComponents`), and counter-mirrors it through
|
||||
`sync_facing()`.
|
||||
|
||||
## 6. Root compatibility facade
|
||||
|
||||
All current consumer-facing members remain available on `StickmanRig` with unchanged names,
|
||||
parameters, return types, and signal cardinality.
|
||||
|
||||
| Existing root surface | Delegates to / remains in |
|
||||
| --- | --- |
|
||||
| `set_facing_profile`, `get_facing_profile`, bend getters/setters, bend joint queries | Root |
|
||||
| `state`, `state_changed`, `set_ragdoll`, `toggle_ragdoll`, `is_in_ragdoll`, `request_recovery`, `snap_to_standing`, `apply_ragdoll_velocity_boost`, `is_ragdoll_at_rest`, `initialize_spawn_pose_from_skeleton` | Root facade → Ragdoll |
|
||||
| `walk_to`, `is_walking`, `arrived`, `begin_terrain_fall`, `snap_to_terrain`, `set_terrain_following_enabled` | Root facade → Locomotion |
|
||||
| `set_collision_bodies_enabled`, `apply_collision_reaction` | Root facade → Locomotion |
|
||||
| `action_queue`, queue mutation/query methods, reactive methods, `start_queue`, `stop_queue`, `is_queue_running`, `speak` | Root facade → ActionRunner |
|
||||
| `action_started`, `action_finished`, `queue_finished`, `queue_changed`, `speech_finished` | Child signal forwarded by root |
|
||||
| `facing_profile`, four bend exports, `rest_timeout`, `auto_recover`, `walk_speed` | Remain root exports; values synchronize to the owning component during `_ready()` and in setters afterward. |
|
||||
| `source_stk_path` | Root |
|
||||
|
||||
`action_queue` must be a delegating property because tests, scene loading, and older callers may
|
||||
read or assign it directly:
|
||||
|
||||
```gdscript
|
||||
var action_queue: Array[Dictionary]:
|
||||
get:
|
||||
return _action_runner.action_queue if _action_runner != null else []
|
||||
set(value):
|
||||
if _action_runner != null:
|
||||
_action_runner.action_queue = value
|
||||
else:
|
||||
_pending_action_queue = value
|
||||
```
|
||||
|
||||
Direct assignment keeps today's behavior and does not emit `queue_changed`; mutation methods do.
|
||||
Pre-`_ready()` export/property writes are stored on the facade and pushed to components after
|
||||
configuration, matching the existing pre-ready setter guard.
|
||||
|
||||
Private root fields are not compatibility API. Existing tests that inspect `_ragdoll_bodies`,
|
||||
`_terrain_motion`, `_run_queue`, or other private internals must move to the owning component.
|
||||
Do not keep duplicate mutable private state on the root merely to satisfy tests.
|
||||
|
||||
## 7. Physics order and state transitions
|
||||
|
||||
The root retains a single `_physics_process()` scheduler:
|
||||
|
||||
```text
|
||||
Ragdoll.tick_pre_movement
|
||||
├── cache root momentum
|
||||
└── update ragdoll rest detection
|
||||
Locomotion.tick_movement
|
||||
├── clear one-frame pelvis correction
|
||||
├── update walk/navigation
|
||||
├── update terrain/air/landing
|
||||
└── settle walk markers
|
||||
ActionRunner.tick
|
||||
├── update speech clocks
|
||||
└── update action state machine
|
||||
StickmanRig core
|
||||
└── pin mirrored head rotation
|
||||
Locomotion.tick_post_skeleton
|
||||
├── apply impact reactions
|
||||
└── sync animated collision proxies
|
||||
```
|
||||
|
||||
Ragdoll transition flow:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> ANIMATED
|
||||
ANIMATED --> RAGDOLL: root.set_ragdoll(true)\nlocomotion cleanup → build physics → request state
|
||||
RECOVERING --> RAGDOLL: root.set_ragdoll(true)\ncancel tween → rebuild physics
|
||||
RAGDOLL --> RECOVERING: request_recovery()\ncapture → destroy physics → reanchor → snap
|
||||
RECOVERING --> ANIMATED: recovery tween finished
|
||||
RAGDOLL --> ANIMATED: snap_to_standing()
|
||||
RECOVERING --> ANIMATED: snap_to_standing()
|
||||
```
|
||||
|
||||
The facade rejects illegal transition requests before delegating. A component signal can request
|
||||
a state commit, but `_set_state(next_state)` on the facade is the only function that mutates the
|
||||
root `state` and emits `state_changed`.
|
||||
|
||||
## 8. Practical data flows
|
||||
|
||||
### 8.1 Authored queue action
|
||||
|
||||
```gdscript
|
||||
rig.queue_action({"type": "walk_to", "target": Vector2(640, 320), "speed": 300.0})
|
||||
rig.queue_action({"type": "speak", "text": "Made it!", "duration": 2.0})
|
||||
rig.start_queue()
|
||||
```
|
||||
|
||||
```text
|
||||
StickmanRig.queue_action
|
||||
→ ActionRunner.action_queue
|
||||
→ root forwards queue_changed
|
||||
ActionRunner begins walk_to
|
||||
→ Locomotion.walk_to
|
||||
→ Locomotion arrived(target)
|
||||
→ root forwards arrived(target)
|
||||
→ ActionRunner observes consume_walk_completed() and emits action_finished
|
||||
```
|
||||
|
||||
The queue dictionary shape and deep-copy behavior remain unchanged.
|
||||
|
||||
### 8.2 Ragdoll recovery landing handoff
|
||||
|
||||
```text
|
||||
Ragdoll captures torso world center + body pose
|
||||
→ Locomotion.probe_ground_y(x, top, bottom, ragdoll_rids)
|
||||
→ Ragdoll computes feet_world
|
||||
→ Locomotion.reanchor_for_recovery(feet_world, FOOT_OFFSET)
|
||||
moves Master and returns mirror-aware local_shift
|
||||
→ Ragdoll rebases captured positions by local_shift
|
||||
→ Ragdoll snap-solves skeleton and requests RECOVERING
|
||||
```
|
||||
|
||||
The existing capture payload stays internal and unchanged:
|
||||
|
||||
```gdscript
|
||||
{
|
||||
"torso": {"pos": Vector2(...), "rot": 0.42, "half": 195.0},
|
||||
"head": {"pos": Vector2(...), "rot": -0.10, "half": 100.0},
|
||||
# eight limb entries...
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Relocation policy
|
||||
|
||||
Implementation moves the scene once with its UID intact, then updates live references in the
|
||||
same change:
|
||||
|
||||
- `scripts/stickman_factory.gd`
|
||||
- `scripts/physics_test_harness.gd`
|
||||
- `tests/test_phase6a_airborne_landing.gd`
|
||||
- any additional literal discovered by `rg --fixed-strings 'res://master_rig.tscn'`
|
||||
- active comments in `stickman_rig.gd`, `stk_rig_adapter.gd`, `test_harness.gd`, and
|
||||
`create_animations.gd`
|
||||
- implementation-facing comments in the changed scripts
|
||||
|
||||
After the user verifies the implementation in Godot, the documentation phase updates active
|
||||
project docs (`README.md`, `AGENTS.md`, `RIGGING.md`, and the source plan). Historical specs
|
||||
should not receive a blind search/replace because many statements describe the path that existed
|
||||
when that phase shipped. Add a short relocation note where a historical file is still used as
|
||||
live guidance; do not rewrite historical line references or decisions.
|
||||
|
||||
Acceptance requires:
|
||||
|
||||
```powershell
|
||||
rg --fixed-strings 'res://master_rig.tscn' scripts scenes tests project.godot
|
||||
```
|
||||
|
||||
to return no live code/scene/test matches. Human-readable historical mentions may remain when
|
||||
clearly historical.
|
||||
|
||||
## 10. Migration sequence
|
||||
|
||||
1. **Characterize first.** Run the current focused rig suites and record baselines before moving
|
||||
code. Add no behavior changes to the extraction patch.
|
||||
2. **Add component nodes/scripts without relocating the scene.** Wire configuration, root signal
|
||||
forwarding, and the central scheduler. The scene still loads from the old path during this
|
||||
step.
|
||||
3. **Extract action/speech orchestration.** It has the smallest physics surface. Keep all root
|
||||
queue methods as delegates and migrate private runner tests.
|
||||
4. **Extract locomotion plus animated collision.** Move navigation, terrain/air/landing, proxy,
|
||||
and flinch state together so only one component writes the animated root/markers.
|
||||
5. **Extract ragdoll/recovery.** Connect the narrow ground-query/reanchor API and retain the exact
|
||||
instant-handoff ordering.
|
||||
6. **Slim and verify the facade.** Remove duplicate mutable state/constants, leaving aliases only
|
||||
where compatibility requires them. Verify every root public method and signal.
|
||||
7. **Relocate the scene last.** Move to `scenes/stickman_rig.tscn`, preserve the UID, update every
|
||||
live preload/path/comment, force one headless editor rescan for new `class_name` scripts, then
|
||||
run static checks and all focused suites.
|
||||
8. **Stop for editor verification.** Per `spec-pipeline`, do not update README, AGENTS, RIGGING,
|
||||
the source plan, or historical documentation until the user confirms the feature works in
|
||||
Godot. Documentation cleanup is the manually triggered Phase 4.
|
||||
|
||||
## 11. Verification plan
|
||||
|
||||
### Static and scene-load checks
|
||||
|
||||
```powershell
|
||||
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --editor --path "C:\Godot4\stickman" --quit
|
||||
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/stickman_rig.gd" --path "C:\Godot4\stickman"
|
||||
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/stickman_locomotion.gd" --path "C:\Godot4\stickman"
|
||||
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/stickman_ragdoll.gd" --path "C:\Godot4\stickman"
|
||||
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/stickman_action_runner.gd" --path "C:\Godot4\stickman"
|
||||
```
|
||||
|
||||
Add `tests/test_phase8_rig_modularization.gd` covering:
|
||||
|
||||
1. relocated scene loads by new path and retains UID/node paths;
|
||||
2. all three typed component children resolve;
|
||||
3. root public methods delegate and root signals emit once;
|
||||
4. missing optional/component dependency warns instead of crashing;
|
||||
5. queue/speech execution works while no ragdoll bodies exist;
|
||||
6. locomotion walking/terrain ticks work while no ragdoll bodies exist;
|
||||
7. ragdoll enter/recover/snap keeps state order and ten-body/nine-joint cardinality;
|
||||
8. root exports synchronize before and after `_ready()`;
|
||||
9. direct `action_queue` assignment retains compatibility semantics; and
|
||||
10. repeated spawn/free leaves no world-level `RagdollBodyContainer` orphan.
|
||||
|
||||
Run the focused regressions most coupled to the extraction:
|
||||
|
||||
- `tests/test_phase3c_walk_recovery.gd`
|
||||
- `tests/test_phase4_final_bugs.gd`
|
||||
- `tests/test_phase4b_walk.gd`
|
||||
- `tests/test_phase4b_terrain.gd`
|
||||
- `tests/test_phase45_collision_bodies.gd`
|
||||
- `tests/test_phase6a_airborne_landing.gd`
|
||||
- `tests/test_phase5_file_features.gd`
|
||||
- `tests/test_cli_scene.gd`
|
||||
|
||||
Tests should inspect `$RigComponents/Locomotion`, `$RigComponents/Ragdoll`, or
|
||||
`$RigComponents/ActionRunner` for subsystem internals instead of requiring stale private mirrors
|
||||
on `StickmanRig`.
|
||||
|
||||
Manual Godot verification after automated checks:
|
||||
|
||||
- factory/harness loads an `.stk` and all body geometry follows the same bones;
|
||||
- LEFT/RIGHT/FORWARD, head mirror pin, bend menus, and z-order remain unchanged;
|
||||
- queue `walk_to → speak → wait → ragdoll → recover` completes with one signal per event;
|
||||
- walking over ramps, ledge fall, hard landing, prop collision/flinch, and queue landing barrier;
|
||||
- ragdoll knock-up, automatic/manual recovery, recovery interruption, and snap-to-standing;
|
||||
- Sandbox EDIT/DIRECT/PLAY round trip and scene save/load provenance/queue persistence.
|
||||
|
||||
## 12. Risks and guardrails
|
||||
|
||||
| Risk | Guardrail |
|
||||
| --- | --- |
|
||||
| Godot child processing order changes behavior | Components have no physics callbacks; root calls ordered tick methods. |
|
||||
| Walk and terrain both write the root | They live in one locomotion component and retain the current internal order. |
|
||||
| State duplicated between root and ragdoll | Root is sole `RigState` authority; component requests transitions. |
|
||||
| Signals double-fire through forwarding | Child emits subsystem signal; root forwarding is the only consumer-facing emission. Add cardinality tests. |
|
||||
| Recovery reaches through locomotion internals | Only `probe_ground_y()` and `reanchor_for_recovery()` cross that boundary. |
|
||||
| Adapter runs before component `_ready()` | Components do not cache geometry until root `_ready()`; adapter node paths remain unchanged. |
|
||||
| Scene move breaks preload before scripts update | Relocation and all live path updates land atomically; UID is asserted. |
|
||||
| New `class_name` cache is stale in CLI | Run one `--headless --editor --quit` rescan before `--check-only`. |
|
||||
| Private-test migration masks regressions | Add behavior assertions through root API/signals plus component-local focused assertions. |
|
||||
| Broad doc replacement falsifies old specs | Update active docs; annotate historical specs instead of blind replacement. |
|
||||
|
||||
## 13. Decisions requiring approval
|
||||
|
||||
1. **Approve movement ownership change:** `StickmanLocomotion` owns navigation and `walk_to()`;
|
||||
`StickmanActionRunner` only orchestrates it. This intentionally differs from the source plan.
|
||||
2. **Approve animated collision ownership:** collision proxies, airborne sweeps, and impact
|
||||
reactions move with locomotion, avoiding a fourth component in this phase.
|
||||
3. **Approve compatibility scope:** preserve all root public APIs/signals/exports, including direct
|
||||
`action_queue`; migrate tests off private root fields rather than keeping duplicate private
|
||||
mirrors.
|
||||
4. **Approve documentation policy:** after editor verification, update active documentation and
|
||||
preserve historical specs with relocation notes instead of blanket rewriting their historical
|
||||
references.
|
||||
|
||||
Implementation must not begin until these decisions are approved.
|
||||
Reference in New Issue
Block a user