# Phase 9 Task 2 — Feature: Body-Part Z-Order by Facing Profile ## Overview RIGGING.md Task 2: when the stickman faces different directions, body parts must be drawn in the right order relative to the torso: 1. **Facing left** → left upper/lower arm **and** left upper/lower leg draw **behind** the torso; the right-side limbs draw in front. On the far (behind-torso) side the arm draws **behind** the leg; on the near (in-front) side the arm draws **in front of** the leg. 2. **Facing right** → right upper/lower arm **and** right upper/lower leg draw **behind** the torso; the left-side limbs draw in front, with the same far-arm-behind-leg / near-arm-in-front-of-leg rule. 3. **Facing forward** → all four limb pairs draw **in front of** the torso. 4. In **all** cases the **head draws in front of the torso**. Task 1 already built the harness-level `FacingProfile { LEFT, RIGHT, FORWARD }` state (`_facing_profile`, the "Facing" `MenuButton`, `_apply_facing_profile()`). Task 2 reuses that profile state to also reorder the rig's `Body/*` visual part nodes. ## 1. Mechanism `master_rig.tscn`'s `Body` node is a plain `Node2D` container whose children are the per-part visual nodes: `Head`, `Body` (the torso visual), `LeftUpperLeg`, `RightUpperLeg`, `LeftLowerLeg`, `RightLowerLeg`, `LeftUpperArm`, `RightUpperArm`, `LeftLowerArm`, `RightLowerArm`. All have default `z_index = 0`, and Godot 4 `Node2D` draws siblings in tree order (first child = back, last child = front). The `RemoteTransform2D` drivers only push global transforms — they never touch tree order. Therefore **draw order = child order of the `Body` container**, and reordering is a pure `move_child()` operation. This is safe to do at any time: the adapter mounts shapes as children of each part node (`_mount_shape` adds `Line2D`/`Polygon2D` under the part node), so moving a part node moves its whole shape group and never disturbs intra-part shape order. Per-part authored node names are fixed, so z-order tables are **static string arrays** — no runtime lookup by part key needed (the harness already owns the rig-facing logic for Task 1). ## 2. Z-order tables Back-to-front (first entry = backmost, last = frontmost). Within each limb pair the **upper limb stays behind the lower limb** (the elbow/knee overlaps the lower limb). On the **far** (behind-torso) side the arm pair draws **behind** the leg pair; on the **near** (in-front) side the arm pair draws **in front of** the leg pair (per the user's per-direction spec). The head is always last. `const Z_ORDER_BY_PROFILE := { FacingProfile.FORWARD: [torso, left upper leg, right upper leg, left lower leg, right lower leg, left upper arm, right upper arm, left lower arm, right lower arm, head], FacingProfile.LEFT: [left upper arm, left lower arm, left upper leg, left lower leg, torso, right upper leg, right lower leg, right upper arm, right lower arm, head], FacingProfile.RIGHT: [right upper arm, right lower arm, right upper leg, right lower leg, torso, left upper leg, left lower leg, left upper arm, left lower arm, head] }` with node names from `master_rig.tscn` (`"Head"`, `"Body"`, `"LeftUpperArm"`, …). - **FORWARD** matches the requirement "legs and arms drawn in front of the torso"; all limbs are on the near side, so arms draw in front of legs. - **LEFT** places both left limb pairs behind the torso (left arm behind left leg) and the right pairs in front (right arm in front of right leg) (requirement 1); **RIGHT** mirrors it (requirement 2). - The head is frontmost in all three (requirement 4). ## 3. Implementation — `scripts/test_harness.gd` ### 3a. Constants - `const BODY_CONTAINER_PATH := "Body"` — rig-relative path of the visual container. - `const Z_ORDER_BY_PROFILE: Dictionary = { FacingProfile.FORWARD: [...], ... }` per §2. ### 3b. State - `var _body_container: Node2D = null` — resolved at spawn, cleared in `_free_current_rig()`. ### 3c. Resolution - `_resolve_rig_nodes()` additionally resolves `_body_container` via `_rig.get_node_or_null(NodePath(BODY_CONTAINER_PATH)) as Node2D` (null → `push_warning`). ### 3d. Reorder New `_apply_body_z_order()`: when `_body_container` is valid, walk the profile's ordered part names **back-to-front** and call `_body_container.move_child(part, _body_container.get_child_count() - 1)` for each existing child (skip missing parts — a `.stk` may omit a part). Moving to the end in back-to-front sequence yields the profile order and leaves unknown/extra children untouched at the back. ### 3e. Hooks - `_apply_facing_profile(profile)` calls `_apply_body_z_order()` after setting the `flip_bend_direction` flags (single entry point: menu selection and fresh spawns both flow through it). - No other changes: `_load_and_spawn()` already calls `_apply_facing_profile(_facing_profile)` after spawn, so every loaded rig gets the current profile's z-order. ## 4. Files modified | File | Changes | |---|---| | `scripts/test_harness.gd` | `BODY_CONTAINER_PATH`, `Z_ORDER_BY_PROFILE`, `_body_container`, resolve + clear + `_apply_body_z_order()`, hook in `_apply_facing_profile()`. | | `docs/phase9_task2_zorder_spec.md` | This file. | | `AGENTS.md` | Test-harness section: "Phase 9 Task 2 body-part z-order" bullet. | | `README.md` | Test-harness bullet: facing profiles now also reorder `Body/*` draw order. | | `RIGGING.md` | Mark Task 2 implemented (Task 1 was documented in the same way). | ## 5. Edge cases - **Missing `Body` container** (foreign rig): null-guarded; z-order silently skipped. - **Missing part nodes** (partial `.stk`): skipped by `get_node_or_null`; no warnings spam. - **Re-spawn**: `_body_container` is re-resolved per spawn; `_free_current_rig()` nulls it. - **`z_index`**: all authored `Body/*` nodes use default `z_index = 0`; nothing in the adapter or harness sets it, so tree order remains the sole depth source. ## 6. Design decisions | # | Decision | Justification | |---|---|---| | D1 | Z-order lives in the **harness**, reusing `FacingProfile` state | The rig stays generic; Task 1 already made the harness the owner of facing state; the rig itself has no z-order concept. | | D2 | Upper limb behind lower limb in every profile | Elbow/knee joints overlap the lower limb; matching the authored scene order avoids joint seams. | | D3 | Far-side arms draw behind the legs; near-side arms draw in front of the legs (user-specified per direction) | Matches the user's Left/Right spec: e.g. facing left → left arm behind left leg, right arm in front of right leg. | | D4 | Head always frontmost (last in array) | Explicit requirement 4. | ## 7. Test plan 1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit` (user-requested; no errors). 2. Temporary headless SceneTree script (pattern from the Round 7 spec; deleted afterwards): - Instantiate the harness scene, `add_child` it, call `_load_and_spawn("res://stickmen/basic.stk")`. - Assert `Body` children order equals `Z_ORDER_BY_PROFILE[FacingProfile.FORWARD]`. - Call `_apply_facing_profile(FacingProfile.LEFT)` → assert left limb pairs precede the torso node (left arm pair before left leg pair) and right pairs follow it (right leg pair before right arm pair); repeat for `RIGHT` (mirrored). - Assert `Body/Head` is the last child in all three profiles. 3. Manual F6 check: load a `.stk`, switch Facing — far-side limbs visibly tuck behind the torso; head stays on top. ## 8. Implementation order 1. `scripts/test_harness.gd` — constants, state, resolution, reorder + hook. 2. Parse check + headless verification script (temp, then removed). 3. Docs: `AGENTS.md`, `README.md`, `RIGGING.md`.