# Phase 9 Task 4 — Refactor: Facing Direction & Bone Bend into the Rig ## Overview RIGGING.md Task 4: facing direction and per-joint bone bend must become **part of `master_rig.tscn`**, not the test harness. A rigged stickman instance should carry its own facing profile and bend flags so different instances can face different directions / bend differently at the same time. These must be **exported controls** on the rig so they are easy to access from the inspector and from runtime code. Today the harness owns all of this: - `enum FacingProfile { LEFT, RIGHT, FORWARD }` (Task 1), - `PROFILE_FLAGS` — per-profile `flip_bend_direction` sets for the 4 TwoBoneIK joints, - `Z_ORDER_BY_PROFILE` — per-profile `Body/*` draw-order tables (Task 2), - `BEND_JOINTS` / `BEND_JOINT_BONE_PATHS` — the 4 upper↔lower limb connectors, - `_facing_profile`, `_bend_joint_bones`, `_bend_modifications`, `_body_container` state, - `_apply_facing_profile()`, `_apply_body_z_order()`, `_resolve_bend_joints()`, and `_ensure_modification_stack_enabled()`. Task 4 moves that state + logic into a new script attached to the `master_rig.tscn` root, and the harness becomes a thin driver that reads/writes the rig's exported properties. ## 1. New rig script — `scripts/stickman_rig.gd` A new `class_name StickmanRig` script, `extends Node2D`, attached to the `Master` root node of `master_rig.tscn` (the root currently has **no** script). It is the single owner of facing profile + per-joint bend flags + `Body/*` z-order. ### 1a. Class declaration & enums ```gdscript class_name StickmanRig extends Node2D ## Facing profiles. Values are used directly as facing-menu item ids in the ## harness (0/1/2), so they must stay stable. enum FacingProfile { LEFT, RIGHT, FORWARD } ## Per-joint bend direction. INVERTED == flip_bend_direction = true. enum BendDirection { NORMAL, INVERTED } ``` The enums live **in the rig script** (not a shared script). The harness references them as `StickmanRig.FacingProfile.LEFT` / `StickmanRig.BendDirection.INVERTED` (idiomatic `class_name` enum access — no autoload or singleton needed). ### 1b. Constants (moved verbatim from `test_harness.gd`) ```gdscript const SKELETON_PATH := "Skeleton2D" const BODY_CONTAINER_PATH := "Body" ## Bend joints (upper↔lower limb connectors) whose TwoBoneIK "Flip Bend ## Direction" flag is user-controllable. const BEND_JOINTS: Array[String] = ["LeftArm", "RightArm", "LeftLeg", "RightLeg"] ## Lower-bone NodePath (relative to Skeleton2D) per bend joint. Used both to ## resolve the TwoBoneIK modification (via joint_two_bone2d_node) and to report ## each joint's world position for hit-testing. const BEND_JOINT_BONE_PATHS: Dictionary = { "LeftArm": "Torso/LeftUpperArm/LeftLowerArm", "RightArm": "Torso/RightUpperArm/RightLowerArm", "LeftLeg": "Torso/LeftUpperLeg/LeftLowerLeg", "RightLeg": "Torso/RightUpperLeg/RightLowerLeg", } ## flip_bend_direction value per facing profile, keyed by bend-joint name. const PROFILE_FLAGS: Dictionary = { FacingProfile.LEFT: { "LeftArm": false, "RightArm": false, "LeftLeg": true, "RightLeg": true }, FacingProfile.RIGHT: { "LeftArm": true, "RightArm": true, "LeftLeg": false, "RightLeg": false }, FacingProfile.FORWARD: { "LeftArm": false, "RightArm": true, "LeftLeg": true, "RightLeg": false }, } ## Body/* visual part node names in draw order (back-to-front) per profile. ## First entry backmost, last frontmost. Upper limbs behind lower limbs; on the ## far (behind-torso) side the arm pair draws behind the leg pair, on the near ## side the arm pair draws in front; head always frontmost. const Z_ORDER_BY_PROFILE: Dictionary = { FacingProfile.FORWARD: [ "Body", "LeftUpperLeg", "RightUpperLeg", "LeftLowerLeg", "RightLowerLeg", "LeftUpperArm", "RightUpperArm", "LeftLowerArm", "RightLowerArm", "Head", ], FacingProfile.LEFT: [ "LeftUpperArm", "LeftLowerArm", "LeftUpperLeg", "LeftLowerLeg", "Body", "RightUpperLeg", "RightLowerLeg", "RightUpperArm", "RightLowerArm", "Head", ], FacingProfile.RIGHT: [ "RightUpperArm", "RightLowerArm", "RightUpperLeg", "RightLowerLeg", "Body", "LeftUpperLeg", "LeftLowerLeg", "LeftUpperArm", "LeftLowerArm", "Head", ], } ``` ### 1c. Exported properties (the "exported controls") ```gdscript ## Facing preset. Setting it overwrites the four per-joint bend values from ## PROFILE_FLAGS and reorders Body/* children. Default FORWARD. @export var facing_profile: FacingProfile = FacingProfile.FORWARD: set(value): if facing_profile == value: return facing_profile = value _apply_profile() ## Per-joint bend direction (the actual flip_bend_direction source of truth). ## Defaults match the FORWARD profile. These are individually overridable after ## a profile is applied (drifting away from the preset, exactly like the current ## harness right-click behavior). @export_group("Bend Direction") @export_enum("Normal", "Inverted") var left_arm_bend: int = BendDirection.NORMAL: set(value): _set_joint_bend_inverted("LeftArm", value == BendDirection.INVERTED) @export_enum("Normal", "Inverted") var right_arm_bend: int = BendDirection.INVERTED: set(value): _set_joint_bend_inverted("RightArm", value == BendDirection.INVERTED) @export_enum("Normal", "Inverted") var left_leg_bend: int = BendDirection.INVERTED: set(value): _set_joint_bend_inverted("LeftLeg", value == BendDirection.INVERTED) @export_enum("Normal", "Inverted") var right_leg_bend: int = BendDirection.NORMAL: set(value): _set_joint_bend_inverted("RightLeg", value == BendDirection.INVERTED) ``` Semantics: - `facing_profile` is a **preset**. Its setter writes the four `*_bend` vars (through their own setters, so mod flags stay in sync) and then reorders `Body/*` children, then emits `facing_profile_changed`. It early-outs if the value is unchanged (prevents redundant re-applies, e.g. re-spawning with the default profile). - The four `*_bend` vars are the **per-joint source of truth**. Each setter stores the value, updates the resolved `SkeletonModification2DTwoBoneIK.flip_bend_direction`, and emits `bend_flag_changed`. `BendDirection.INVERTED` → `flip_bend_direction = true`. - All setters are **guarded by a `_nodes_ready` flag** (set at the end of `_ready()`): before the rig has resolved its children (e.g. while `PackedScene.instantiate()` is still hydrating the serialized exports), the setter only stores the value; `_ready()` then applies the full state once. This makes the apply path order-independent and robust against setter timing during instantiation. ### 1d. Signals ```gdscript ## Emitted when the facing preset changes (after flags + z-order are applied). signal facing_profile_changed(profile: int) ## Emitted when a single joint's bend direction changes. `flipped` is the new ## flip_bend_direction value (true == inverted). signal bend_flag_changed(joint: String, flipped: bool) ``` The harness primarily refreshes menu labels on `about_to_popup` (the established pattern used by the editor's snap/guide menus), so the signals are not strictly required for label sync — but they are emitted for programmatic consumers and match the repo's signal-driven data-flow convention. ### 1e. Public methods ```gdscript ## Facing preset accessors. func set_facing_profile(profile: int) -> void func get_facing_profile() -> int ## Per-joint bend accessors (bool form mirrors flip_bend_direction directly). func set_joint_bend_flipped(joint: String, flipped: bool) -> void func get_joint_bend_flipped(joint: String) -> bool ## The list of bend-joint names (["LeftArm", "RightArm", "LeftLeg", "RightLeg"]). func get_bend_joints() -> Array[String] ## World position of a bend joint (its lower bone's global_position) for ## right-click hit-testing. Unknown joint → push_warning + Vector2.ZERO. func get_bend_joint_global_position(joint: String) -> Vector2 ``` `set_facing_profile` / `set_joint_bend_flipped` are the runtime API; the exported var setters funnel into the same internal apply functions, so there is exactly one mutation path (no drift between the inspector values, the internal state, and the live mod flags). ### 1f. Internal state & resolution ```gdscript var _nodes_ready: bool = false var _skeleton: Skeleton2D = null var _body_container: Node2D = null var _bend_joint_bones: Dictionary = {} # { String : Bone2D } (lower bones) var _bend_modifications: Dictionary = {} # { String : SkeletonModification2DTwoBoneIK } ``` - `_ready()`: resolves `_skeleton` (`get_node_or_null(SKELETON_PATH)`), `_body_container` (`get_node_or_null(BODY_CONTAINER_PATH)`), the 4 lower `Bone2D`s via `BEND_JOINT_BONE_PATHS`, and the 4 TwoBoneIK modifications; enables the modification stack (`stack.enabled = true`); then applies the current `facing_profile` once (`_apply_profile()`) and sets `_nodes_ready = true`. - `_resolve_bend_modifications()` mirrors the current harness `_resolve_bend_joints()` matching: walk `_skeleton.modification_stack`, and for each `SkeletonModification2DTwoBoneIK` match by `joint_two_bone2d_node == NodePath(BEND_JOINT_BONE_PATHS[joint])` (**no hardcoded stack index**). Missing bone/mod → `push_warning` + skip (never crash). - `_apply_profile()`: writes the four `*_bend` vars from `PROFILE_FLAGS[profile]` (through their setters), then `_apply_body_z_order()`. - `_apply_body_z_order()`: preserves the Task 2 algorithm — walk the profile's ordered part names **back-to-front** and `move_child(part, get_child_count() - 1)` for each existing child; unknown/ extra children stay at the back; missing parts skipped silently. ### 1g. Editor-time vs runtime - **The script is NOT `@tool`.** Exported properties still appear in the editor inspector (they are serialized into `master_rig.tscn` when the scene is saved), but the script body — node resolution, `flip_bend_direction` writes, `Body/*` reordering, mod-stack enabling — runs **only at runtime** (`_ready()` + setters on a live instance). - Rationale: `SkeletonModificationStack2D` only solves at runtime, so an in-editor live preview of bend direction would not be meaningful, and mutating `Body/*` child order / mod sub-resources in the editor would dirty the scene and fight the editor's undo/ownership. Runtime-only application is the safe minimum the task explicitly permits ("at minimum runtime"). Making it `@tool` later (gated behind `Engine.is_editor_hint()`) is a possible future enhancement, not part of this task. ### 1h. Failure handling Matches repo style throughout (`get_node_or_null`, `push_warning`, skip): - Missing `Skeleton2D`, `Body`, a bend-joint bone, or a TwoBoneIK mod → `push_warning` (prefixed `"StickmanRig: ..."`) and skip that piece; the rig never crashes. - Unknown joint name in `set_joint_bend_flipped` / `get_joint_bend_flipped` / `get_bend_joint_global_position` → `push_warning` and no-op / `false` / `Vector2.ZERO` respectively. - Empty `Body` container or missing part nodes → z-order reorder silently skips (as today). ## 2. `master_rig.tscn` changes 1. **Attach the script** to the root `Master` node: `script = ExtResource("…")` for `res://scripts/stickman_rig.gd`. The exported defaults get serialized (`facing_profile = 2`, `left_arm_bend = 0`, `right_arm_bend = 1`, `left_leg_bend = 1`, `right_leg_bend = 0`). 2. **Align authored state to the FORWARD default** (Q1 **resolved: yes**). So the scene's as-authored state == the runtime default (and the editor view of the raw scene is coherent): - TwoBoneIK `flip_bend_direction` (currently authored = the **RIGHT** profile, see §6): - `LeftArm` (`…TwoBoneIK_f0s26`, `target_nodepath = ../IK_Targets/Left_Hand`): `true` → **remove the flag / `false`**. - `RightArm` (`…TwoBoneIK_yvxej`): `true` → **unchanged**. - `RightLeg` (`…TwoBoneIK_ylko5`): absent → **unchanged** (stays `false`). - `LeftLeg` (`…TwoBoneIK_2leu7`): absent → **add `flip_bend_direction = true`**. - `Body/*` child order: the authored order is `Head, Body, LeftUpperLeg, RightUpperLeg, LeftLowerLeg, RightLowerLeg, LeftUpperArm, RightUpperArm, LeftLowerArm, RightLowerArm`. Move the `Body/Head` node to the **end** → order becomes exactly the FORWARD table. (Safe: the `RemoteTransform2D` drivers and the adapter reference `Body/*` by name, never by index.) Both edits are pure scene text edits. Runtime behavior is unchanged either way (the rig applies FORWARD on `_ready`), so this only affects the as-authored appearance and makes the "default leaves the scene as-authored" verification true. ## 3. Harness changes — `scripts/test_harness.gd` The harness stops owning bend/facing state and drives the rig script. It keeps a thin UI mirror for menu labels + re-application across respawns (the rig remains the authority over the actual flags). ### 3a. Removed | Harness element | Disposition | |---|---| | `enum FacingProfile` | Delete — use `StickmanRig.FacingProfile`. | | `PROFILE_FLAGS`, `Z_ORDER_BY_PROFILE` | Delete — moved to `StickmanRig`. | | `BEND_JOINTS`, `BEND_JOINT_BONE_PATHS` | Delete — the rig exposes `get_bend_joints()` + `get_bend_joint_global_position()`. | | `BODY_CONTAINER_PATH` | Delete — only the rig needs it. | | `_body_container` | Delete (resolve + clear). | | `_bend_joint_bones`, `_bend_modifications` | Delete (resolve + clear). | | `_apply_facing_profile()` | Delete — call `_rig_script.set_facing_profile()`. | | `_apply_body_z_order()` | Delete — moved to the rig. | | `_resolve_bend_joints()` | Delete — moved to the rig's `_resolve_bend_modifications()`. | | `_ensure_modification_stack_enabled()` | Delete — the rig enables its own stack in `_ready()`. | Note: `SKELETON_PATH` **stays** in the harness (still used to resolve `_skeleton` for the bone overlay and the coordinates panel). `_facing_profile` **stays** but is repurposed as a UI mirror (§3b). ### 3b. State (changed / added) ```gdscript var _rig_script: StickmanRig = null var _facing_profile: int = StickmanRig.FacingProfile.FORWARD # UI mirror only var _context_joint: String = "" ``` - `_facing_profile` is now only "the user's last-selected facing profile", used for the `[√] ` menu prefix and to re-apply after each spawn. The rig's exported `facing_profile` is the actual state. - `_context_joint` is unchanged (which joint the right-click menu targets). ### 3c. Spawn flow — `_load_and_spawn()` 1. `_free_current_rig()` (unchanged except it clears `_rig_script` instead of the deleted state). 2. `var rig := StickmanFactory.spawn(path)`; `_rig_script = rig as StickmanRig` (null-guarded: foreign rig without the script → `push_warning` and disable facing/bend UI). 3. Connect signals **before** `add_child`: `_rig_script.facing_profile_changed.connect(_on_facing_profile_changed)`, `_rig_script.bend_flag_changed.connect(_on_bend_flag_changed)` (both may be no-ops; label sync is done on `about_to_popup`, but connecting keeps future consumers working). 4. `_world.add_child(_rig)` (rig `_ready` runs here, applies its default FORWARD and enables its mod stack). 5. `_resolve_rig_nodes()` — now only resolves `_skeleton`, `_ik_handles`, `_coord_bones` (drops `_body_container` and `_resolve_bend_joints()`). 6. `_rig_script.set_facing_profile(_facing_profile)` — re-applies the remembered selection (no-op when it equals the rig's FORWARD default). ### 3d. Facing menu - Menu item ids stay `StickmanRig.FacingProfile.LEFT/RIGHT/FORWARD`. - `_on_facing_menu_id_pressed(id)`: `_facing_profile = id`; if `_rig_script` valid → `_rig_script.set_facing_profile(id)`; then `_update_facing_menu_labels()`. - `_facing_menu_label(profile)`: unchanged (`[√] ` prefix based on `_facing_profile`). ### 3e. Right-click bend toggle - `_hit_test_bend_joint(world_pos)`: iterate `_rig_script.get_bend_joints()`; position from `_rig_script.get_bend_joint_global_position(joint)`; same `JOINT_HIT_RADIUS_PX / _camera.zoom.x` nearest-joint selection. - `_handle_right_click()`: unchanged flow (set `_context_joint`, set label, popup). - `_context_menu_label(joint)`: `"Normal Bend" if _rig_script.get_joint_bend_flipped(joint) else "Invert Bend"` (fallback `"Invert Bend"` when the rig script is missing). - `_on_context_menu_id_pressed(_id)`: if `_context_joint` non-empty and `_rig_script` valid → `_rig_script.set_joint_bend_flipped(_context_joint, not _rig_script.get_joint_bend_flipped(_context_joint))`. ### 3f. Signal handlers (optional but included) ```gdscript func _on_facing_profile_changed(_profile: int) -> void: _facing_profile = _rig_script.get_facing_profile() if _rig_script else _facing_profile _update_facing_menu_labels() _debug_overlay.queue_redraw() func _on_bend_flag_changed(_joint: String, _flipped: bool) -> void: _debug_overlay.queue_redraw() ``` (These keep the `[√] ` prefix in sync if the profile is ever changed from outside the menu; the menu still refreshes on `about_to_popup` as the primary mechanism.) ## 4. Factory / adapter impact - `stickman_factory.gd`: **no functional change.** `spawn_from_data` / `spawn` still return the rig root (now carrying the `StickmanRig` script); narrow the return type to `StickmanRig` (`return RIG_SCENE.instantiate() as StickmanRig`) for stronger typing (Q6 **resolved: yes**). - `stk_rig_adapter.gd`: **no change.** The adapter mounts shapes onto `Body/*` by node path and does not read or write bend/facing state. The rig's `_ready` (z-order + flags + stack-enable) runs **after** `StkRigAdapter.apply()` (which happens inside `spawn_from_data`, before the rig enters the tree), so mounted shape children are already present when the rig reorders `Body/*` — moving a part node moves its whole shape group, exactly as today. ## 5. `master_rig_builder.gd`, `master_rig2.tscn` Out of scope and untouched (per `docs/phase9_round1_bugfix_spec.md`): `master_rig_builder.gd` builds a **different** rig (`Sticky/Stickman/.../Hip` naming), and `master_rig2.tscn` is a pose-override variant not referenced by the factory. The new `StickmanRig` script targets `master_rig.tscn` only. ## 6. Defaults & backward compatibility - **Default profile = FORWARD** (`facing_profile = FacingProfile.FORWARD`), matching the current harness default `_facing_profile = FacingProfile.FORWARD`. On a fresh spawn the rig applies FORWARD flags `{LeftArm:false, RightArm:true, LeftLeg:true, RightLeg:false}` and the FORWARD z-order — **identical to today's runtime behavior**. - **Authored `master_rig.tscn` flags are NOT currently FORWARD** — they are the **RIGHT** profile (`LeftArm:true, RightArm:true, LeftLeg:false, RightLeg:false`, see `master_rig.tscn` lines 23/31 and the absent flags on the two leg mods). The harness already overwrites these at runtime, so behavior is unchanged before/after this refactor; the mismatch only matters for the "as-authored == default" goal (§2 / Q1). - **No `.stk` format change.** `FILE_VERSION` stays `"1.5"`. Bend/facing state is rig-instance state, not figure data; it is never serialized into `.stk`. - **No `settings.json` change.** The harness facing selection stays non-persistent (as today). - Old `.stk` files, foreign rigs, and partial figures load unchanged (null-guarded resolution). ## 7. Files modified | File | Changes | |---|---| | `scripts/stickman_rig.gd` | **New.** `class_name StickmanRig extends Node2D` — enums, constants, exported properties, signals, methods, runtime resolution/apply. | | `master_rig.tscn` | Attach `StickmanRig` to root `Master`; (recommended) align authored TwoBoneIK flags + `Body/*` order to FORWARD (§2). | | `scripts/test_harness.gd` | Delete bend/facing ownership (§3a); add `_rig_script` + repurposed `_facing_profile`; rewire spawn, facing menu, right-click toggle (§3c–3f). | | `scripts/stickman_factory.gd` | Narrow `spawn_from_data`/`spawn` return type to `StickmanRig` (Q6). | | `docs/phase9_task4_refactor_spec.md` | This file. | | `AGENTS.md` | Add `scripts/stickman_rig.gd` bullet; update `test_harness.gd` bullet (facing/bend now drive the rig); note `master_rig.tscn` root script. | | `README.md` | Project-structure table row for `stickman_rig.gd`; update test-harness + factory bullets. | | `RIGGING.md` | Mark Task 4 implemented. | ## 8. Edge cases - **No rig script / foreign rig** (`rig as StickmanRig` == null): `push_warning`; facing menu and right-click bend toggle become no-ops; bone overlay/coords still work. - **Missing `Skeleton2D` / `Body` / bones / mods**: `push_warning` + skip per section; rig never crashes. - **Setters before `_ready`** (during instantiation): guarded by `_nodes_ready`; `_ready` applies the full state once — no ordering bug. - **Re-spawn**: the rig is freed and a fresh one spawns; the harness re-applies the remembered `_facing_profile` (§3c). `_free_current_rig()` clears `_rig_script` (not the deleted state). - **Manual bend override then profile change**: setting a profile overwrites all four per-joint flags (the preset wins), exactly like the current `_apply_facing_profile()`. - **Unknown `Body/*` children**: stay at the back during reorder (unchanged Task 2 semantics). - **Head always frontmost**: preserved in all three `Z_ORDER_BY_PROFILE` tables. ## 9. Design decisions | # | Decision | Justification | |---|---|---| | D1 | New `class_name StickmanRig extends Node2D` on the `master_rig.tscn` root | The rig is the natural owner of per-instance facing/bend; a `class_name` script makes it a typed, reusable API for the harness and future consumers. | | D2 | Enums live in the rig script (`StickmanRig.FacingProfile` / `StickmanRig.BendDirection`) | No autoload/singleton; idiomatic `class_name` enum access; single source of truth both sides can reference. | | D3 | `facing_profile` is a preset; the four `*_bend` enum exports are the per-joint source of truth | Matches the harness's existing preset-then-override behavior (RIGGING.md: "user is free to change bend manually"); the enum dropdowns give readable "Normal"/"Inverted" inspector labels (the task's "per-joint bend enums" hint). | | D4 | Public bool-form methods (`set_joint_bend_flipped`/`get_joint_bend_flipped`) alongside the enum exports | `flip_bend_direction` is a bool; the bool form is the engine-accurate contract and keeps the harness context-menu label logic unchanged. | | D5 | TwoBoneIK resolved by `joint_two_bone2d_node` NodePath matching, not stack index | Preserves the existing (Task 1) resolution approach; robust to stack reordering. | | D6 | Non-`@tool` script; apply only at runtime | IK doesn't solve in-editor; mutating `Body/*` order / mod flags in-editor would dirty the scene; runtime-only is the task's permitted minimum. | | D7 | Harness keeps a `_facing_profile` **UI mirror** but not the bend authority | Preserves current UX (selection persists across respawns, `[√] ` label) without the harness owning flags/z-order. | | D8 | Rig enables its own mod stack in `_ready()` | Facing/bend are meaningless until the stack is live; the rig should self-enable at runtime (single consumer today always enables it). | | D9 | Rig exposes `get_bend_joint_global_position()` | Lets the harness drop `BEND_JOINT_BONE_PATHS`/`_bend_joint_bones` entirely; the rig owns the whole bend domain. | ## 10. Test plan 1. **Parse check** (same as prior tasks): `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit` — no errors. 2. **Headless SceneTree verification** (temporary script, deleted after; pattern from Round 7): - `StickmanFactory.spawn("res://stickmen/basic.stk")` → cast root to `StickmanRig` (non-null); `add_child` it; assert `get_facing_profile() == FacingProfile.FORWARD` and the 4 mods' flags equal the FORWARD set; assert `Body` child order == `Z_ORDER_BY_PROFILE[FORWARD]` and `Body/Head` is last. - `set_facing_profile(FacingProfile.LEFT)` → assert flags + `Body` order match LEFT (left pairs before torso, head last); repeat `RIGHT`. - `set_joint_bend_flipped("LeftArm", true)` → assert `get_joint_bend_flipped("LeftArm") == true`, the matched TwoBoneIK mod `flip_bend_direction == true`, and the other three unchanged. - Instantiate a rig **without** adding it to a tree, set `facing_profile` before `add_child`, then `add_child` → assert the pre-set profile was honored by `_ready` (guards D-setters). 3. **Harness code review**: facing menu ids use `StickmanRig.FacingProfile`; spawn connects signals before `add_child` and re-applies `_facing_profile`; right-click toggle reads/writes the rig script, not a mod directly; `_resolve_rig_nodes()` no longer touches bend/z-order state. 4. **Manual F6** (`res://scenes/test_harness.tscn`): - Load `basic.stk`; figure starts FORWARD (unchanged from before). - Facing menu → Left/Right: limbs tuck behind the torso correctly, head stays frontmost; `[√] ` moves; context menu labels reflect the new per-joint flags. - Right-click an elbow/knee → "Invert Bend"/"Normal Bend" toggles the rig property and the label flips; the limb visibly bends the other way. - Load another `.stk` → the remembered facing profile re-applies to the fresh rig. 5. **Cleanup** temp verification files. ## 11. Implementation order 1. `scripts/stickman_rig.gd` — enums, constants, exports, signals, methods, runtime apply. 2. `master_rig.tscn` — attach script; align authored flags + `Body/*` order to FORWARD (Q1). 3. `scripts/test_harness.gd` — remove ownership, add `_rig_script`, rewire spawn/menus/toggle. 4. `scripts/stickman_factory.gd` — (optional) narrow return type. 5. Parse check + headless verification (temp, then removed). 6. Docs: `AGENTS.md`, `README.md`, `RIGGING.md`, this spec. ## 12. Open questions — RESOLVED (user approval) - **Q1 — Authored scene ≠ FORWARD default.** **Resolved: YES — edit `master_rig.tscn`** to align the authored TwoBoneIK flags and `Body/*` order to FORWARD (§2). - **Q2 — Per-joint export representation.** **Resolved: enum form** — `BendDirection` with `@export_enum("Normal","Inverted")` dropdowns. - **Q3 — `@tool` vs runtime-only.** **Resolved: runtime-only** (non-`@tool`; exports still inspector-editable). - **Q4 — Mod-stack enabling.** **Resolved: YES** — move `stack.enabled = true` into the rig's `_ready()`; harness drops `_ensure_modification_stack_enabled()`. - **Q5 — Harness `_facing_profile` UI mirror.** **Resolved: YES** — keep the lightweight mirror for the `[√] ` label + respawn re-application; the rig is the authority. - **Q6 — Factory return type.** **Resolved: YES** — narrow `spawn_from_data`/`spawn` return type to `StickmanRig`.