502 lines
23 KiB
Markdown
502 lines
23 KiB
Markdown
# Phase 8 — Rig Relocation and Runtime Component Modularization
|
|
|
|
Status: architecture proposed; awaiting user approval before implementation.
|
|
|
|
Source proposal: `docs/archive/plans/PHASE_8_RIG_MODULARIZATION.md`.
|
|
|
|
## 1. Outcome and architectural correction
|
|
|
|
Move the authored rig scene from `res://master_rig.tscn` to
|
|
`res://scenes/characters/stickman/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/characters/stickman/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/characters/stickman/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/characters/stickman/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/characters/stickman/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/characters/stickman/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/characters/stickman/stickman_factory.gd` | Loads the relocated scene; return type and `spawn*` API stay unchanged. |
|
|
| `scripts/characters/stickman/stk_rig_adapter.gd` | No node-path changes. Comments reference the relocated scene. It continues to adapt the root before `_ready()`. |
|
|
| `scripts/tools/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/characters/stickman/stickman_factory.gd`
|
|
- `scripts/tools/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`, `docs/guides/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/characters/stickman/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/characters/stickman/stickman_rig.gd" --path "C:\Godot4\stickman"
|
|
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/characters/stickman/stickman_locomotion.gd" --path "C:\Godot4\stickman"
|
|
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/characters/stickman/stickman_ragdoll.gd" --path "C:\Godot4\stickman"
|
|
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://scripts/characters/stickman/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.
|