ryan 36032ebb1e Add asset creation skill and artist agent for Stickman Studio
- Introduced a new artist agent to create and review vector, raster, clothing, prop, and animation assets based on user prompts or reference images.
- Created an asset-creation skill to manage the delegation, review, approval, and handoff processes for graphical assets and animations.
- Added project format routing documentation to guide asset creation and ensure consistency in Stickman Studio.
- Established a review workflow for asset promotion, ensuring thorough validation and user approval before finalizing assets.
- Documented the specifications for scenery, backgrounds, and vector surface finishes to enhance the child experience in the Sandbox Stage.
2026-10-15 14:35:44 -04:00
2026-10-03 19:13:39 -04:00
2026-08-08 00:00:50 -04:00
2026-08-08 00:00:50 -04:00
2026-08-08 00:00:50 -04:00
2026-10-03 19:13:39 -04:00

Stickman Studio

A Godot 4.7.1 project for creating and directing 2D scenes. Stickman Studio includes a stickman editor for drawing body-part shapes and assembling figures, plus a Sandbox Stage for building sets, directing actions, and playing scenes.

Overview

The editor is organized as 11 sub-windows in a 3-column layout:

  • Left column (5) — Head, Left Upper Arm, Left Lower Arm, Left Upper Leg, Left Lower Leg
  • Center column (5) — Torso, Right Upper Arm, Right Lower Arm, Right Upper Leg, Right Lower Leg
  • Right column (1) — Whole Stickman preview

The editor is driven by three menus: File (Save / Load / Clear), Edit (Configure Grid... / Snap to Grid), and View (Reset Views / Hide or Show Pose Guide).

Each body-part panel is an independent vector drawing surface with per-panel zoom and panning (middle-mouse drag). A configurable background grid helps align vertices, and snap-to-grid can be enabled for both vertex dragging and whole-figure assembly. Each panel supports multiple shapes with Z-ordering controls (Send Back / Bring Forward) and shape-level Copy/Paste and Mirror operations. The Whole Stickman panel assembles every part into one figure — treating all shapes in a panel as a single unit — and lets you reposition, rotate, and scale each part, reorder the parts (Z-order), and mirror them.

Drawing surfaces clip their content to the panel bounds: shapes and parts no longer render outside a panel's borders when dragged or panned. A selected shape can be dragged to reposition it within its panel.

Requirements

Item Version
Engine Godot 4.7.1
Addon Scalable Vector Shapes 2D (addons/curved_lines_2d/)

The legacy scenes/legacy/stick.tscn scene (a rigged walk animation using Skeleton2D + IK) remains for reference but is no longer the main scene.

Running the project

Run the project from the Godot editor:

  1. Open the project folder in Godot 4.7.1.
  2. Press F5, or open and run res://scenes/main.tscn (which hosts the Stickman Editor).

The main scene is configured as run/main_scene="res://scenes/main.tscn" in project.godot.

Repository layout

  • scripts/ and scenes/ are grouped by feature, with editors, stage systems, stickman runtime, props, terrain, speech, shared UI, and tools kept in their own folders.
  • Editable libraries are stored in content/stickmen/, content/props/, and content/stages/; fonts remain in resources/fonts/.
  • Maintained project documentation is in docs/guides/, docs/architecture/, docs/specs/, and docs/plans/. Older plans and references are in docs/archive/.
  • ContentPaths keeps old project asset references loadable after moves while leaving external file paths unchanged.

Sandbox Stage (F6)

Run res://scenes/stage/sandbox_stage.tscn with F6 to build and direct a scene. EDIT is for placing and transforming stage objects. DIRECT is for authoring stickman actions and event rules. Press Space or Play to start a run; press Space or Direct to freeze it. Space acts on each key press, so holding it does not repeatedly toggle the run. Play or Space resumes a paused run from its frozen pose and time. The stage clock is visible in the top bar as minutes, seconds, and centiseconds; it shows 00:00.00 in EDIT and planning DIRECT.

While a run is paused in DIRECT, you can add or edit stickman actions and event rules against the paused scene. Pending actions can be reordered among themselves; the active action stays fixed. Completed or interrupted actions can be deleted, with deletion taking effect on the next run from t = 0. Event rules are not reordered. Objects cannot be moved or placed while paused; return to EDIT for object changes. Reset or entering EDIT restores authored object transforms and time zero while keeping action and rule edits. Paused edits autosave after a short debounce. A paused walk or recovery resumes with its animation/tween phase intact; recovery does not start a second ragdoll, and recovered feet keep their IK targets aligned instead of crossing.

Quick Start

1. Create a body part

  1. Locate one of the 10 body-part panels (e.g. Head).
  2. Right-click anywhere inside its drawing area to open the context menu.
  3. Choose a starter shape:
    • Line — a 120 px horizontal segment (2 vertices, open shape).
    • Rectangle — a 100 × 60 px rectangle (4 vertices, closed shape).
    • Circle — a 12-segment polygon (12 vertices, closed shape, radius 40).

The shape draws immediately and syncs to the Whole Stickman panel.

Rendering behavior:

  • Closed shapes (closed: true) fill their interior with the chosen color (draw_colored_polygon) and draw a 2 px outline.
  • Open shapes (closed: false) render as an outline-only segment with no fill.

Body-part panels expand vertically to fill their column height.

2. Edit vertices

Each panel supports vertex-level editing:

  1. Select a shape — Left-click anywhere inside a shape to select it. The outline turns white.
  2. Create a vertex — With a shape selected, right-click on its outline and choose "Create Point". A new vertex (shown as a hollow rectangle) is added at the nearest edge midpoint.
  3. Drag vertices — Left-click on any vertex handle (filled circle = original, hollow rectangle = user-created) and drag to reshape. The shape conforms in real time. Release to apply. With Snap to Grid enabled, the dragged vertex jumps to the nearest grid intersection.
  4. Delete a vertex — Right-click on a vertex handle and choose Remove Point (see Deleting below).
  5. Deselect — Left-click outside the shape or select a different panel. Only one shape is selected at a time.

Move a whole shape (Phase 5):

  1. Left-click a shape not on a vertex handle and drag to reposition the entire shape within the panel.
  2. Release to commit. All vertices translate together; the shape's Z-order is unchanged.
  3. With Snap to Grid enabled, the dragged shape jumps to the nearest grid intersection on release.

3. Zoom & Pan

Scroll the mouse wheel inside any body-part panel or the Whole Stickman preview to zoom. Pan the view with the middle-mouse button:

  • Wheel up — zoom in by 10%
  • Wheel down — zoom out by 10%
  • Middle-mouse drag — pan the view in any direction
  • Range — 30% (minimum) to 300% (maximum)
  • Vertex handles remain visible at all zoom levels.
  • Each panel has independent zoom and pan offset.
  • Panning takes priority over vertex dragging (middle-mouse press cancels any in-progress drag).
  • Content is clipped to the panel bounds (Phase 5) — shapes, grid, and gizmos cannot render outside a panel's drawing area, even when panned or dragged beyond its edge. This keeps each panel self-contained.
  • Use View → Reset Views to restore all panels to 100% zoom and the origin pan offset.

4. Assemble a stickman

  1. Create shapes for every part you want in the figure.
  2. In the Whole Stickman panel, drag any part to reposition it in the figure.
    • Parts get a generous hit area and show a yellow highlight while being dragged.
    • A short label (H, T, LUA, RUA, LUL, RLL, …) marks each part.
    • Use the mouse wheel to zoom in/out and middle-mouse to pan for precise positioning.
    • With Snap to Grid enabled, the mouse pointer snaps to the nearest grid intersection and the part follows.
  3. Optionally type a name in the Stickman Name field at the top.
  4. Right-click a selected part (Phase 5) to reorder or mirror it:
    • Send Back / Bring Forward — change the part's draw order in the preview.
    • Mirror X / Mirror Y — flip the part around its bounding-box center.

5. Pose Silhouette Guide

The Whole Stickman preview can render a pose silhouette guide: a semi-transparent, color-coded stick figure that marks the rest-pose pivot joints of the runtime rig defined by stickman_rig.tscn. Its 13 joint anchors (Head, Neck, Left/Right Shoulder, Left/Right Elbow, Left/Right Wrist, Hips, Left/Right Knee, Left/Right Ankle) correspond to the pivot locations used by the Skeleton2D rig, so you can align your body-part shapes to the same pivots.

The guide is a view aid only: it is drawn above the grid and in front of user parts (ghosting over them), moves with pan/zoom, is not hit-testable (it never intercepts part dragging or selection), and is not saved to the .stk file.

Color coding (both lines and joint dots use these color groups):

Group Joints Color Line alpha Joint alpha
Left (cyan/blue) LeftShoulder, LeftElbow, LeftWrist, LeftKnee, LeftAnkle (0.35, 0.70, 1.00) 0.45 0.65
Right (orange/red) RightShoulder, RightElbow, RightWrist, RightKnee, RightAnkle (1.00, 0.50, 0.20) 0.45 0.65
Central (white) Hips, Neck, Head (1.00, 1.00, 1.00) 0.45 0.65
  • Lines (limbs + spine) render semi-transparent with line alpha 0.45; the head outline is a white semi-transparent circle of radius 100 (1:1 stickman_rig.tscn scale) centered on the Head joint.
  • Joint dots are constant 6 px on screen at every zoom level (GUIDE_JOINT_RADIUS / _zoom).
  • The guide is ON by default. Toggle it via View → Hide Pose Guide / View → Show Pose Guide (the menu item is a dynamic, text-only label — no checkmark — reading "Hide Pose Guide" while the guide is visible and "Show Pose Guide" while it is hidden); the setting persists to user://settings.json (show_pose_guide).

Centered at the default view: the guide's figure bounding box is centered at the preview-area center, computed at draw time from the live preview size (GUIDE_FIGURE_CENTER = (0, -93.75); _guide_to_preview() maps master_pos to (master_pos - GUIDE_FIGURE_CENTER) * GUIDE_SCALE + preview_area.size * 0.5). It is centered at startup and after Reset Views (zoom 1 / pan 0), and re-centers automatically when the window is resized. Because it remains a world-space fixture, panning/zooming moves it with the grid and parts.

1:1 master-rig scale (deliberate): the guide matches stickman_rig.tscn proportions exactly (a figure of ≈ 336 × 940 px, roughly x ∈ [-168, 168], y ∈ [-563.5, 376]). Because the default starter parts are small, the guide intentionally dwarfs them. At 1:1 zoom the ~940 px figure is taller than a typical panel, so the head and feet clip equally above/below the fold when centered — accepted. GUIDE_SCALE/GUIDE_FIGURE_CENTER are the tunables if a different size or placement is wanted later.

6. Grid & Panning

All 10 body-part panels and the Whole Stickman preview share a global background grid (default cell size 15 px, Phase 5; previously 5 px):

  1. Click Edit → Configure Grid... and set the grid size.
    • SpinBox accepts 1–100 px per cell.
    • The setting is applied to every panel simultaneously and persists to user://settings.json.
  2. Toggle Edit → Snap to Grid to enable snapping (Phase 5: the checkable menu item now reliably shows its ✓ checkmark when toggled):
    • On vertices — while dragging a vertex handle, it jumps to the nearest grid intersection.
    • On assembly — while dragging a part in the Whole Stickman preview, the pointer snaps and the part follows.
    • On shape drag — while dragging a whole shape (Phase 5), it snaps on release.
    • Existing vertices are not retroactively snapped; snapping only applies to new drags.
  3. Pan — hold the middle-mouse button and drag anywhere in a panel to pan the view.
    • Panning uses the same transform as zoom, and each panel keeps an independent offset.
    • View → Reset Views restores 100% zoom and the origin offset on all panels.
  4. Grid lines are drawn behind shapes and scale with pan/zoom, so they stay aligned to world coordinates.

7. Changing Colors

Colors are easy to change on any existing shape:

  1. Right-click on a shape (either selected or with the cursor over it) and choose Color....
  2. A ColorPicker popup appears with the shape's current color pre-loaded.
  3. Adjust the color — the shape updates live as a preview.
  4. Click OK to commit the change or Cancel to revert to the original color.

The chosen color is stored in the shape's color field and is saved/loaded with the .stk file.

8. Deleting

Two levels of deletion are available:

Delete an entire shape:

  1. Right-click on a shape (either selected or with the cursor over it) and choose Delete.
    • Delete is only offered when a shape exists and the mouse is over it.
  2. The shape is removed from the panel and the Whole Stickman preview.

Remove a single vertex:

  1. Right-click on a vertex handle and choose Remove Point.
  2. If 2 vertices remain, the shape becomes an open line (closed = false).
  3. If 1 vertex or fewer remains, the entire shape is cleared.

9. Multiple Shapes & Z-Ordering

Each body-part panel can contain more than one shape. New shapes are created via the standard right-click context menu and are drawn on top of existing shapes. Shapes are drawn in Z-order (last in the list = frontmost).

Z-ordering controls:

  • Right-click on a shape and choose Send Back to move it one step behind (earlier in draw order).
  • Right-click on a shape and choose Bring Forward to move it one step ahead (later in draw order).

Z-order is preserved in the .stk save file (shapes are stored in draw order within the shapes array).

Only one shape within a panel can be selected at a time for vertex editing. Left-click a shape to select it; right-click also selects the shape under the mouse for context menu operations.

Shape Copy & Paste (Phase 5):

  1. Right-click on a shape and choose Copy to place a deep copy of the shape on the editor-wide clipboard.
  2. Right-click (on empty space or a shape) and choose Paste to insert the clipboard shape at the click position.
    • Paste only appears while the clipboard is populated.
    • The clipboard persists after pasting and is shared across all 10 body-part panels — a shape copied in one panel can be pasted into any other.
    • The pasted shape becomes the newly selected (topmost) shape.
    • Copy → Clear → Paste semantics: the clipboard survives figure clear.

Shape Mirroring (Phase 5):

  1. Right-click on a selected shape and choose Mirror X or Mirror Y.
  2. The shape's vertices are mirrored around the shape's bounding-box center.
  3. Mirroring recomputes the points array in place — no extra fields are stored; the result is saved as normal vertex data.

10. Whole Stickman Manipulation (Selection, Rotation, Scale)

The Whole Stickman preview treats all shapes in a body-part panel as one combined object. Each part can be independently translated, rotated, and scaled.

Selection:

  • Left-click on any part in the Whole Stickman preview to select it.
  • A white bounding box appears around the selected part, along with manipulation gizmos.
  • Click empty space to deselect.

Translation:

  • Left-click and drag a selected part to reposition it. Snap-to-grid applies to the pointer position when enabled.

Rotation:

  • When a part is selected, a filled circle appears centered below the bounding box.
  • Click and drag the rotation circle to rotate the part around its center.
  • Hold Ctrl while rotating to snap to 15-degree increments (relative to the original 0° position).
  • Rotation is absolute: 0° always means the original orientation the object was created with.
  • The accumulated rotation value is saved in the .stk file.

Scale:

  • When a part is selected, cross/plus markers appear at all four corners of the bounding box.
  • Click and drag any corner cross to freely scale the part.
  • Hold Ctrl while scaling to lock the aspect ratio (scale uniformly).
  • Grid snapping snaps the corner position to the nearest grid intersection.
  • Scale is relative to the created size: (1.0, 1.0) = original size.
  • The scale value is saved in the .stk file. Scale may be negative for mirrored parts.
  • Part mirroring (Phase 5) — right-click a selected part and choose Mirror X or Mirror Y to mirror it around its bounding-box center. This is implemented by negating the part's scale factor along the chosen axis (no vertex data is changed).

Part Z-ordering (Phase 5):

  • Right-click a selected part and choose Send Back or Bring Forward to move it one step within the preview's draw order.
  • Selection respects Z-order: hit-testing runs front-to-back, so the frontmost part under the pointer is selected.
  • Part Z-order is saved in the .stk file as the top-level part_order array.

11. Save

  1. Click File → Save.
  2. Choose a location and name. The default extension is .stk (appended automatically if omitted).
  3. Click Save. The figure is written as JSON.

12. Load

  1. Click File → Load.
  2. Select a .stk file.
  3. On success, all body-part panels and the Whole Stickman preview are populated. On failure, an error dialog reports the problem (missing file, parse error, or unsupported version).

v1.0, v1.1, v1.2, v1.3, and v1.4 files are automatically migrated to v1.5 on load (v1.0/v1.1 single shapes wrapped in a shapes array, rotation defaults to 0, scale defaults to (1,1); files without part_order fall back to the default part order). pivot, length, proportions, and guide_offset are write-only metadata recomputed from live shapes on every save — never read back — so old files load unchanged and gain these keys on their next save.

13. Clear

  1. Click File → Clear.
  2. A confirmation dialog warns that the current stickman will be cleared.
  3. Confirm to reset all panels and the preview (zoom resets to 100%, pan offset to origin).

14. Test Harness & Stickman Factory (Phase 9)

Phase 9 adds the runtime pipeline that turns a saved .stk file into a live, rigged stickman_rig.tscn instance — plus a standalone staging scene for debugging it in isolation. Neither is wired into the editor.

Stickman Factory (res://scripts/characters/stickman/stickman_factory.gd) — the runtime entry point:

Method Signature Behavior
load_stk static func load_stk(path: String) -> Dictionary Reads a .stk file via FileAccess + JSON.parse_string. Returns {} and push_warning on failure.
spawn_from_data static func spawn_from_data(stk_data: Dictionary) -> Node2D Instantiates res://scenes/characters/stickman/stickman_rig.tscn, calls StkRigAdapter.apply(stk_data, rig), returns the rig root.
spawn static func spawn(path: String) -> Node2D Chains load_stk() → spawn_from_data(). Returns null on empty data.

The factory is the intended runtime API: StickmanFactory.spawn("res://content/stickmen/basic.stk") yields a rigged StickmanRig (the root of stickman_rig.tscn) ready to add to the scene tree. StkRigAdapter (Phase 8, extended by Phase 9) now also fits the head bone (Head.position.y = -proportions.torso_length) and mounts the head as full geometry like every other part (clearing its inline @tool circle script); the mounted head is dropped HEAD_CHIN_DROP (28 px) below the neck so its chin aligns with the pose-guide circle's bottom and overlaps the torso (Phase 9 Round 4).

Test Harness (res://scenes/tools/test_harness.tscn) — run via F6 on the scene (NOT via the main editor scene). Controls:

  • Open .stk… — FileDialog filtered to *.stk; quick-select buttons for res://content/stickmen/break.stk, res://content/stickmen/basic.stk, res://content/stickmen/test.stk.
  • Show Bones / Show IK Handles — checkboxes toggling the debug overlay.
  • Loaded filename — status label showing the currently loaded file.
  • Pan / zoom — middle-mouse drag to pan, mouse-wheel to zoom the Camera2D; the camera recenters on each spawn.
  • IK drag — click and drag any of the 6 Marker2D IK handles (IK_Targets/Left_Hand, Right_Hand, Left_Leg, Right_Leg flex the limb via TwoBoneIK; IK_Targets/Torso translates the whole rig rigidly via its RemoteTransform2D; IK_Targets/Head drives the head's SkeletonModification2DLookAt aim rotation) (Phase 9 Round 7). The rig self-enables its modification stack in _ready().
  • Facing — a MenuButton (leftmost in the top bar) applying a preset to the rig's StickmanRig exported facing_profile, which sets the rig's TwoBoneIK Flip Bend Direction flags: Left (arms normal, legs inverted), Right (arms inverted, legs normal), Forward (RightArm / LeftLeg inverted — the rig's default). Facing Left now applies a whole-rig Y-axis mirror — Master.scale.x = -1 (RIGHT/FORWARD → (1,1)) — so the head and body mirror together and face the correct direction (this replaces the old head-only Body/Head.scale.x = -1 mirror; _apply_head_flip() is removed). Two head-related fixes make the mirror stable: the head RemoteTransform2D (Skeleton2D/Torso/Head/Pivot) pushes the full transform (no update_scale = false), so Body/Head.scale stays identity under the mirrored root (the old partial-channel push re-canonicalized the scale and caused per-frame Y-flips/wrap-jumps); and when facing LEFT _apply_head_lookat_mirror_mode() disables the head SkeletonModification2DLookAt and pins the head bone to the FORWARD canonical aim (π), re-asserted each physics frame by _pin_mirrored_head_rotation() while ANIMATED/RECOVERING (RIGHT/FORWARD re-enable the LookAt) — so interactive head-aiming is intentionally static while facing LEFT. The current profile is prefixed [√] on the menu labels and persists across rig loads (Phase 9 Task 1, Task 4).
  • Bend-direction toggle — right-click an elbow or knee joint in the viewport to pop a context menu that inverts that joint's TwoBoneIK bend direction ("Invert Bend" → "Normal Bend" and back). Only the 4 elbows/knees are targets (Phase 9 Task 1).
  • Body-part z-order — the Facing profile also reorders the rig's Body/* visual part nodes (tree order = draw order), now owned by the rig's StickmanRig._apply_body_z_order(): Forward draws all limbs in front of the torso, Left tucks the left arm/leg pairs behind the torso (right pairs in front), Right tucks the right pairs behind; upper limbs sit behind lower limbs, far-side (behind-torso) arms draw behind the legs while near-side arms draw in front of them, and the head is always frontmost (Phase 9 Task 2, Task 4). The per-profile z-order tables are unchanged and kept provisionally — an X-mirror does not affect depth (draw order).
  • Animation — a dropdown (populated per spawn from the rig's AnimationPlayer.get_animation_list(), walk_right pre-selected) plus Play/Pause/Resume (label swaps with playback state), Stop, and Loop (default ON, persists across loads) controls. The harness drives the rig's AnimationPlayer directly by node path (ANIMATION_PLAYER_PATH); loop writes Animation.loop_mode before play, and playback state is tracked via the button handlers + the animation_finished signal (no polling). The AnimationTree node remains an untouched placeholder. The animation .:facing_profile tracks are neutralized/removed — facing is now set explicitly by walk_to()/set_facing_profile() (LEFT root-mirrors the rig and plays the same canonical walk_right clip mirrored; walk_left is no longer used at runtime), so playing walk_right no longer flips the profile through an animation track (Phase 9 Task 5).

Debug overlay (a world-space Node2D _draw()): true bone segments drawn between each Bone2D origin and its Bone2D children (color-coded left cyan / right orange / central white, with a joint dot per bone), with limb leaf bones drawn out to their IK targets so the forearm/shin segments and wrist/ankle joints are visible (Phase 9 Round 2) and the Head leaf drawn along the bone's own direction (~90 px, since its IK target is a LookAt aim point, not a joint) (Phase 9 Round 3), when Show Bones is on; colored markers at the six IK targets — hands green, feet blue, head yellow, torso magenta (Phase 9 Round 7) — plus a semi-transparent yellow aim line from the Head bone to the head marker, when Show IK Handles is on. Each load frees the previous rig and spawns a fresh one.

15. Vector Terrain System

The Vector Terrain System is a standalone, reusable component for building crisp, resolution-independent vector terrain (flat ground, angled ramps, stepped platforms) — with matching 2D collision. It is not wired into the editor; it is exercised through a dedicated staging scene run via F6.

TerrainBlock (res://scripts/terrain/terrain_block.gd) — @tool class_name TerrainBlock, extends StaticBody2D. A single terrain segment that builds its three children in code:

Child Node type Purpose
Interior fill Polygon2D Fills the block's interior with fill_color.
Vector border Line2D Crisp outline using outline_color / outline_width; auto-closes the loop by appending the first vertex to the end, with LINE_JOINT_ROUND and round caps.
Collision CollisionPolygon2D Physical body; BUILD_SOLIDS solid decomposition (supports concave blocks).

Exported properties (polygon_points: PackedVector2Array, fill_color, outline_color, outline_width) are driven by a single unified setter that pushes vertex changes to all three children live — no manual rebuilds.

TerrainUtils (res://scripts/terrain/terrain_utils.gd) — class_name TerrainUtils, extends RefCounted, static utility.

  • sanitize_points(points: PackedVector2Array, grid_size: float = 16.0) -> PackedVector2Array — sanitizes a raw vertex list through a fixed pipeline, in order:
    1. Grid snap — rounds each vertex to the grid_size grid (default 16.0).
    2. Redundancy removal — a local _simplify_polyline() (Godot 4.7 has no Geometry2D.simplify_polyline()), which drops consecutive duplicates, a closing duplicate when last == first, and collinear vertices.
    3. Clockwise enforcement — Geometry2D.is_polygon_clockwise() reverses the winding if it is not already clockwise, guaranteeing clockwise output.
  • spawn_block(...) — factory that sanitizes the raw input vectors (sanitize_points), creates a TerrainBlock, applies the cleaned points, and adds it to the target container.

physics_test_harness.tscn / scripts/tools/physics_test_harness.gd — class_name PhysicsTestHarness, extends Node2D; a standalone staging scene (run via F6; not wired into the editor). It builds flat ground, angled ramps, and stepped TerrainBlock instances via TerrainUtils, then instantiates res://scenes/characters/stickman/stickman_rig.tscn standing on the flat ground. The scene root is a Node2D + script with a Camera2D at position (0, -400) zoom 0.5 and an empty Environment container. The camera wheel-zoom scales between 0.25x and 3.0x (to test resolution independence / vector outline thickness); middle-drag pans. It also hosts the Dynamic Vector Props spawner (see below): press 1/2/3 to drop physics props above the angled ramp, plus a best-effort StaticBody2D collision proxy for the rig (which has no physics bodies of its own). A toggle-mode Button (label "Stickman" ↔ "Ragdoll") flips the rig's kinematic-to-ragdoll mode (see §17); a Rest SpinBox (0.1–10 s) writes _rig.rest_timeout (runtime-only) and a "Recover Now" button calls _rig.request_recovery(). The harness connects the rig's state_changed signal, which removes the RigCollisionProxy on RAGDOLL and re-adds it (idempotently) on ANIMATED/RECOVERING.

16. Dynamic Vector Props

The Dynamic Vector Props system is a standalone, reusable component for building physical, dynamic props (crates, balls, planks) as RigidBody2D bodies with crisp vector rendering and matching collision. It is not wired into the editor; it is exercised through the physics test harness staging scene via F6.

PropBlock (res://scripts/props/prop_block.gd) — @tool class_name PropBlock, extends RigidBody2D. A single physical prop whose children are built in code, @tool-safe (null-guarded for editor safety):

Child Node type Purpose
Interior fill Polygon2D Fills the prop's interior with fill_color.
Vector outline Line2D Crisp outline using outline_color / outline_width; auto-closes the loop by appending the first vertex to the end, with LINE_JOINT_ROUND / LINE_CAP_ROUND.
Collision (polygon mode) CollisionPolygon2D BUILD_SOLIDS solid decomposition; used for POLYGON shape type.
Collision (circle mode) CollisionShape2D + CircleShape2D Radial 48-segment loop (CIRCLE_SEGMENTS = 48); used for CIRCLE shape type.

Exported properties:

Property Type Default Behavior
shape_type @export_enum("Polygon","Circle") POLYGON Selects which geometry + which collision node is enabled (polygon ↔ circle).
polygon_points PackedVector2Array empty Polygon geometry; applies only when shape_type == POLYGON.
radius float 32.0 Circle radius; applies only when shape_type == CIRCLE.
fill_color Color (0.55, 0.35, 0.15) Interior fill.
outline_color Color (0.15, 0.08, 0.02) Vector outline.
outline_width float 2.0 Vector outline thickness.
material_preset @export_enum("None","Wood","Rubber","Cardboard","Metal") None Sets mass + physics_material_override (PhysicsMaterial.friction / .bounce) and themed colors.

Material presets (mass_for / physics_material / tint_for static factories):

Preset mass friction bounce Fill tint
Wood 3.0 0.6 0.1 (0.55, 0.38, 0.2)
Rubber 0.5 0.9 0.85 (0.9, 0.2, 0.2)
Cardboard 0.4 0.3 0.05 (0.85, 0.72, 0.45)
Metal 8.0 0.9 0.0 (0.5, 0.55, 0.6)
None 1.0 0.5 0.05 default fill

A unified live-update setter drives geometry to all children live — a change to polygon_points / radius / shape_type pushes to the Polygon2D, Line2D, and the active collision node with no manual rebuilds. Non-NONE presets also recolor fill_color and outline_color (outline = tint_for(preset).darkened(0.55)).

PropUtils (res://scripts/props/prop_utils.gd) — class_name PropUtils, extends RefCounted, static factory.

  • Primitive generators return shape-payload dictionaries with default dimensions + color themes:
    • create_box(size := Vector2(48,48), ...) — a 4-point POLYGON (wood theme).
    • create_ball(radius := 24.0, ...) — a CIRCLE payload (rubber theme).
    • create_plank(length := 160.0, thickness := 16.0, ...) — a 4-point POLYGON (metal theme).
    • create_triangle(base := 56.0, height := 48.0, ...) — a 3-point POLYGON (cardboard theme).
  • spawn_prop(container, position, shape_payload, material_preset := WOOD, initial_velocity := Vector2.ZERO) -> PropBlock — factory that instantiates a PropBlock, applies the payload (shape_type + geometry + colors), sets the material preset, sets linear_velocity after add_child (when non-zero), and returns the spawned prop. Polygon points are sanitized through TerrainUtils.sanitize_points().

Physics test harness controls (extended): the physics_test_harness.gd scene now accepts 1 / 2 / 3 key presses to spawn props above the angled ramp (spawn point (300, -300)), tumbling them down the terrain:

Key Prop Primitive Preset Initial velocity
1 Wood Crate create_box() WOOD (60, 0)
2 Bouncy Ball create_ball() RUBBER (-80, 0)
3 Heavy Plank create_plank() METAL (30, -40)

Rig collision proxy caveat: the standalone physics harness still uses a best-effort code-only StaticBody2D proxy (RigCollisionProxy) for its simple standing figure. The Sandbox Stage uses the Phase 4.5 per-part collision bodies documented in §20.2 instead. In ragdoll mode (see §17) the harness proxy is removed on entry so the physical ragdoll collides directly with the terrain, and re-added on exit.

17. Kinematic-to-Ragdoll State System

StickmanRig (the runtime root of stickman_rig.tscn) can switch between physics modes via a reversible state machine with three states:

  • ANIMATED (default) — the kinematic Skeleton2D + IK puppet is visible and driven by the AnimationPlayer / SkeletonModificationStack2D. The rig root's momentum is cached each physics frame (_physics_process → _track_momentum()) so it can be handed off on transition.
  • RAGDOLL — the kinematic rig is frozen (AnimationPlayer stopped with keep_state) and the Body/* visuals hidden immediately; a procedural network of RigidBody2D + PinJoint2D nodes spawned in code takes over, letting the figure fall/tumble against the terrain and props. The ragdoll is built from the current solved bone positions (the IK stack is disabled only after the build), so the handoff is instant and seamless — the ragdoll appears in exactly the pose the figure was in, no crossfade.
  • RECOVERING — the ragdoll has been destroyed and the skeleton is being snapped back to the captured rest pose, then tweened upright (IK targets captured → standing) before returning to ANIMATED.

Public API (StickmanRig):

Member Signature Behavior
state var state: RigState Current mode — RigState.ANIMATED, RigState.RAGDOLL, or RigState.RECOVERING.
state_changed signal state_changed(new_state: int) Emitted on every transition with the RigState enum value.
is_in_ragdoll() func is_in_ragdoll() -> bool state == RigState.RAGDOLL (so RECOVERING reads as "Stickman").
set_ragdoll(enabled) func set_ragdoll(enabled: bool) -> void Enters/exits ragdoll; idempotent (no-op when already in the target state).
toggle_ragdoll() func toggle_ragdoll() -> void set_ragdoll(not is_in_ragdoll()).
request_recovery() func request_recovery() -> void Forces the stand-up recovery path; no-op unless in RAGDOLL.

Momentum handoff: _track_momentum(delta) computes the rig root's linear/angular velocity from per-frame global_position / global_rotation deltas and caches them. On entering ragdoll, the cached velocities are applied directly to the ragdoll Torso body (linear_velocity + angular_velocity), so the figure continues its current motion seamlessly.

Ragdoll construction (_build_ragdoll()): bodies and joints are built in code and reparented into a Node2D container named "RagdollBodyContainer" added as a child of the rig's parent (the world root) — not the rig, which carries its spawn offset — so all bodies/joints are placed in world coordinates.

Property Value
Body count 10 RigidBody2D
Torso CapsuleShape2D (radius 12, length = distance between the Torso and Head bone origins), mass 8.0
Head CircleShape2D (radius 100), mass 2.0
8 limbs CapsuleShape2D (radius 8, length = bone.length), masses 1.0–2.0
Joint count 9 PinJoint2D (one per non-root body, pinned at the child bone's origin), softness = RAGDOLL_TARGET_SOFTNESS (0.2) at build
Collision collision_layer / collision_mask = 1 (matches TerrainBlock / PropBlock)

Ragdoll bodies spawn fully visible — the entry handoff is instant (the ragdoll is built at the current solved pose, so there is nothing to fade).

Angular limits (_apply_ragdoll_joint_limits()): elbows/knees get their band from the editable per-profile, per-joint RAGDOLL_JOINT_BAND_LIMITS table — a { "lower": deg, "upper": deg } range for the joint's allowed relative angle (0 = straight; a small side ~5 resists hyperextension, a large side ~150 is the fold, and a narrow/locked band such as {60, 100} forces a permanently-bent limb). FORWARD arms {-150°, +5°}, legs {-5°, +150°}; LEFT/RIGHT override both sides. Joints missing from the table fall back to the RAGDOLL_JOINTS limit string (elbow_knee -5°..+150°, elbow_knee_ccw -150°..+5°). Shoulders/hips allow ±160°; the neck is free (angular_limit_enabled = false).

Joint bend configuration (EDIT ME): every per-joint bend decision in the ragdoll cycle lives in three const tables at the top of scripts/characters/stickman/stickman_rig.gd, keyed by facing profile, so joints can be tuned without touching pipeline code:

Const Shape Purpose
RAGDOLL_JOINT_BAND_LIMITS [profile][joint] = { "lower": deg, "upper": deg } The fall's knee/elbow PinJoint2D angular band (in degrees). Missing entries fall back to the RAGDOLL_JOINTS limit string.
RECOVERY_LEG_SIDE_FORCE [profile][leg] = bool Optional override of the leg IK flip_bend_direction during the stand-up (true = inverted). A missing key means automatic: match the captured fall pose's knee side.
RECOVERY_JOINT_PATH [profile][joint] = "shortest" Optional override of the elbow/knee stand-up tween path. A missing key means automatic: pass through straight (0) when the shortest path would cross ±180° (fully-backward).

Instant handoff (Phase 11): _enter_ragdoll() stops the player with keep_state (no pose reset), builds the ragdoll from the current solved bone positions while the IK stack is still enabled, then hides Body/* and disables the IK stack in the same call. There is deliberately no crossfade — the ragdoll spawns at exactly the same pose, so a fade would only read as ghosting (an earlier blended transition was removed on director feedback).

Rest detection (auto-recovery): while state == RAGDOLL, _update_rest_detection() reads the Torso RigidBody2D. When it is sleeping or its linear velocity ≤ REST_LINEAR_THRESHOLD (5.0 px/s — tuned up from the original 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s even when settled) and angular velocity ≤ REST_ANGULAR_THRESHOLD (0.1 rad/s), a _rest_timer accumulates; after rest_timeout (exported, default 2.0 s) plus a STABILIZATION_DELAY (0.1 s) hold, and with auto_recover (exported, default true) enabled, recovery is triggered. rest_timeout and auto_recover are runtime-adjustable exports.

Recovery (_start_recovery(), also request_recovery()): _enter_ragdoll() first calls _capture_pre_ragdoll_state(), which snapshots the 6 IK marker values, the 8 limb bone rotations, the head bone rotation, the facing profile, the 4 live bend flags, the head LookAt enabled state, per-leg knee side + flag (leg_sides), and the chain-space entry offsets torso_chain_offset / head_chain_offset — captured at entry so the snap needs no authored geometry constants and the LEFT mirror's ambiguous global_rotation decomposition (±π) cancels out; this snapshot is the recovery END target (possibly a mid-walk pose; the animation is not resumed). _capture_ragdoll_pose() records the 10 bodies' rig-local {pos, rot, half} into _captured_pose (half = each capsule's half-length, stored as build-time metadata) and probes the terrain under the fallen figure via _probe_ground_y() (a 7-ray fan around the landing X — GROUND_PROBE_X_OFFSETS, GROUND_PROBE_START_OFFSET/END_OFFSET, GROUND_PROBE_TOLERANCE band, ragdoll RIDs excluded), keeping the probe context in _probe_top_y/_probe_bottom_y/_probe_exclude for the ankle clamp at snap time. Because the ragdoll bodies spawn under a world sibling, the rig root never moves while the figure falls, so before destroying the ragdoll _start_recovery() calls _reanchor_root_to_landing(), which translates the rig root so the standing figure's feet sit on the ground at the landing X — feet = (_captured_landing_center.x, _captured_ground_y), new_root = feet + FOOT_OFFSET — and re-bases the captured rig-local positions with a mirror-aware local shift local_shift = to_local(shift) − to_local(Vector2.ZERO) (a raw + shift would land every position off by 2·shift.x under the LEFT X-mirror). The figure stands up in place, on the ground, where the ragdoll landed — not anchoring its hip to the lying hip (a spine-direction hip sits at ground level when the torso lies flat, which would bury the standing feet ~363 px into the ground), and not back at its pre-ragdoll position. The ragdoll is then destroyed and state = RECOVERING is set + emitted. _snap_skeleton_to_pose() writes the captured pose onto the 6 IK-target markers (never the slaved Torso Bone2D): the torso marker position via to_local(center_world − spine_dir·half) and rotation chain(spine) − torso_chain_offset; the limb bones rot_u = chain(u) − θ_t − bone_angle_u, rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l; the head via chain(head) − head_chain_offset − θ_t. All snapped rotations are normalized into (−π, π] because Godot's Skeleton2D re-wraps bone rotations internally (which would otherwise corrupt the tween's captured start). The ankle markers are clamped up onto the terrain surface via a per-ankle _probe_ground_y() at each ankle's X. It re-shows Body/*, calls _rearm_ik_stack() — a defensive re-setup (re-runs setup() when the stack reports !get_is_setup(), re-asserts enabled = true and Skeleton2D.set_process_internal(true), and re-asserts the Torso marker RemoteTransform2D's update-position/rotation/scale flags) so TwoBoneIK reliably resumes solving after a disable→enable toggle — then disables the 4 arm TwoBoneIK mods + the head LookAt and enables the 2 leg mods, each leg's flip_bend_direction matched to the captured fall pose's knee side (vs the entry side recorded in leg_sides, or forced via RECOVERY_LEG_SIDE_FORCE). The snap therefore reproduces the ragdoll's exact final pose (a "sitting" rest stays sitting). Runtime diagnosis is gated behind const DEBUG_RECOVERY := false (_recovery_dbg(), off by default).

_play_stand_up() then starts a 2.0 s sine-in-out tween (STAND_UP_DURATION) of the 6 markers → their pre-ragdoll captured values and of the arm + head bones to the _recovery_bones targets computed by the snap — upper arm bones take the shortest path, lower arm bones use _natural_joint_target(current, target, offset) (offset = bone_angle_lower − bone_angle_upper; the through-straight rule is evaluated in the joint's rendered space and a "shortest" entry in RECOVERY_JOINT_PATH bypasses it). The legs are IK-driven toward their tweening, ground-clamped ankle markers (the 2 leg mods stay enabled). The arm mods + head LookAt stay disabled during the tween. The baked stand_up animation is not played — a fixed first keyframe can never match an arbitrary ragdoll rest pose. On tween finish, _on_stand_up_finished() restores the pre-ragdoll facing profile + per-joint bend flags + head LookAt state, re-enables the arm mods, re-shows Body/*, sets state = ANIMATED, and emits — the resumed IK solve reproduces the pre-ragdoll pose exactly (the end pose matches the pre-ragdoll frame, including mid-walk poses).

Interruptibility: set_ragdoll(true) during RECOVERING kills the stand-up tween and rebuilds the ragdoll; set_ragdoll(false) during RAGDOLL routes through _start_recovery(); repeated set_ragdoll calls are idempotent. All ragdoll nodes are spawned procedurally — stickman_rig.tscn is not modified (the stand_up / walk_left / walk_right animations are baked into the scene's AnimationLibrary by the create_animations.gd editor script, which runs manually in the editor; the baked stand_up is an authored reference and the recovery path does not play it). The ragdoll bodies spawn under a world sibling (not the rig root), so the whole-rig Y-axis mirror does not affect them; walk_left remains in the library but is not used at runtime — walk_to() plays the canonical walk_right clip for every direction (root-mirrored for LEFT).

18. Sandbox Stage Builder

The Sandbox Stage Builder (Phase 2) is a standalone, kid-friendly director sandbox: a visual stage where you place terrain, props, and stickmen from a palette, then switch between Edit (build), Direct (director/rule authoring), and Play (physics simulation). It is not wired into the editor — run via F6 on res://scenes/stage/sandbox_stage.tscn.

The stage is intentionally extendable: the spawn palette is registry-driven (adding an object type = appending one dictionary entry), selection hit-tests arbitrary Node2Ds geometrically, gizmos drive global_position/global_rotation, and core events are exposed as signals for future phases (Action Queue, Triggers, Save/Load). Styling defaults (fonts/sizes/colors/grid) live in a hand-editable res://sandbox_theme.json (Phase 4b, §21).

File Purpose
res://scenes/stage/sandbox_stage.tscn The stage scene: root Node2D + Camera2D + empty World container.
res://scripts/editors/stage/sandbox_stage.gd class_name SandboxStage, extends Node2D — root controller (3-mode state machine, placement + drag-painting, camera, deletion, bottom status bar, mode badge/frame/cursors, signals); Phase 3b instantiates the AssetSelector grid popup + the two thumbnail renderers, owns the selector open/close flow and the lazy per-frame thumbnail drain, and wires the Stickman/Prop palette buttons to the selector (§22). Phase 3c adds the editor-tool wiring: the QueuePanel / RulePanel / ActionEditor / RuleEditor / WaypointContext instantiation, the unified CaptureKind target-capture system, "Edit Queue…"/"Edit Rules…" entry points + right-click stickman/waypoint context menus, the shared confirmation dialog, and consequence-only rule editing (§23).
res://scripts/stage/stage_spawner.gd class_name StageSpawner, extends RefCounted — registry-driven factory reusing TerrainUtils / PropUtils / StickmanFactory; exposes is_terrain_id() / get_template_aabb() / spawn_id tagging. get_template_aabb() returns the sanitized template AABB (mirrors _spawn_terrain()'s 16-px grid pass), so it doubles as the block-unit paint stride. Phase 3b: registry ids ground/ramp/step/prop/stickman/area (separate crate/ball entries removed); holds the session state selected_stickman_path / selected_prop_id and a per-path _stickman_cache; prop and stickman spawn the selected asset.
res://scripts/characters/stickman/stickman_library.gd Phase 3b class_name StickmanLibrary, extends RefCounted — scans res://content/stickmen/*.stk into {path, name, data} entry models (corrupt/missing-body_parts files skipped, empty stickman_name → filename basename), with make_entry(path) for arbitrary Browse-chosen paths (§22).
res://scripts/props/prop_library.gd Phase 3b class_name PropLibrary, extends RefCounted — static registry of the 4 prop templates (Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard) with their PropUtils.create_*() payloads + material presets; get_default_id() = "crate" (§22).
res://scripts/shared/ui/asset_selector.gd Phase 3b class_name AssetSelector, extends PopupPanel — grid UI controller (root of scenes/stage/asset_selector.tscn): pagination 12/page (4×3), Prev/Next/page label, empty-state label, per-cell thumbnail + name + (prop) material badge, hover styling (no pre-highlight on open), window-resize re-centering, Browse/Refresh/Close wiring, Esc handling; signals item_selected / cancelled / browse_requested / refresh_requested (§22).
res://scripts/stage/stage_selection.gd class_name StageSelection, extends RefCounted — hover/click/box selection via geometric AABB hit-testing.
res://scripts/stage/stage_gizmos.gd class_name StageGizmos, extends Node2D — hover highlight, selection outline, rotate ring handle.
res://scripts/stage/stage_grid.gd class_name StageGrid, extends Node2D — optional world-space grid overlay (major line every 5 cells).
res://scripts/stage/stage_placement_overlay.gd Phase 4b class_name StagePlacementOverlay, extends Node2D — world-space overlay drawing the terrain drag-painting guide line + the director action trajectory / ghost marker.
res://scripts/shared/thumbnails/stickman_thumbnail.gd Phase 3b class_name StickmanThumbnail, extends Node — renders a parsed .stk into a 200×200 Texture2D by spawning the real rig (StickmanFactory.spawn_from_data) inside an offscreen SubViewport, framing it and capturing after a double frame_post_draw (§22).
res://scripts/shared/thumbnails/prop_thumbnail.gd Phase 3b class_name PropThumbnail, extends Node — renders a prop template into a 200×200 Texture2D via a lightweight Polygon2D + Line2D visual (no RigidBody2D, so no gravity), tinted by the material preset (§22).
res://scripts/shared/thumbnails/thumbnail_cache.gd Phase 3b class_name ThumbnailCache, extends RefCounted — disk PNG cache under user://thumbnails/: stickman key = basename_mtime, prop key = id_v<PROP_VERSION>; load_png/save_png/clean_stale_stickmen (§22).
res://scenes/stage/asset_selector.tscn Phase 3b PopupPanel root + asset_selector.gd — minimal shell/layout skeleton (title bar, empty GridContainer, footer Prev/Next/Browse/Refresh/Close); all dynamic per-cell content is built in code at runtime (§22).
res://sandbox_theme.json Phase 4b hand-editable styling defaults (font paths/sizes, grid snap default, mode accent colors). Phase 3c extends the fonts block with style variants (ui_font_bold/ui_font_italic), per-widget size/style flags, and an optional per-widget {size, bold, italic} object form (§21.3 / spec §14). Loaded at _ready(); missing/malformed falls back to built-in constants.

Mode management — a single 3-segment switcher [ ✏️ Edit | 🎬 Direct | ▶️ Play ] sits at the far left of the top bar (enum StageMode { EDIT, DIRECT, PLAY }). Each mode shows a contextual toolbar and a mode badge pill in the viewport's top-left corner (✏️ EDIT cyan, 🎬 DIRECTING amber, ▶️ SIMULATING green), sourced from sandbox_theme.json mode_colors:

  • Edit (default) — spawner palette + Grid/Snap/Size controls visible; the construction grid is shown. RigidBody2D props are frozen (freeze = true + freeze_mode = FREEZE_MODE_KINEMATIC), stickmen stand as ANIMATED puppets, gizmos are visible, and selection is active. Cursor is a crosshair.
  • Direct — internally an "edit-with-direct" state: props frozen / stickmen standing / gizmos enabled (same as Edit) so a director can click stickmen and build action queues and rules, but the spawner/grid controls are hidden and the toolbar shows only a hint label (Click a stickman to direct, or ⚡ When… for rules). The grid is hidden entirely, and a thin amber viewfinder frame borders the viewport (a full-screen MOUSE_FILTER_IGNORE panel). Cursor is a crosshair.
  • Play — layout tools and the grid hidden; props unfreeze and fall, and each stickman's action queue runs (start_queue(), auto_recover = false). Gizmos are hidden, selection is cleared, and the mode switcher is the only toolbar control. Cursor is the default arrow.
  • The mode switcher no-ops when the mode is unchanged (set_mode() guard); a mode_changed(mode: int) signal carries the new value (0/1/2) on every real switch, and the camera view persists across switches. Esc exits Direct → Edit.
  • Per-mode cursor — _apply_cursor() sets CURSOR_CROSS for Edit/Direct and CURSOR_ARROW for Play, and swaps to a runtime-generated amber flag/reticle custom cursor whenever a click-awaiting director step is active (§21); a generated Image/ImageTexture (no asset file needed).
  • Restart-the-sim: each object's position/rotation is saved whenever you place, move, or rotate it; returning to Edit restores that authored state (and zeroes prop velocity), so every Play session starts from the same authored layout. Stickmen snap straight back to standing (no stand-up glide).

Spawn palette (text buttons built from the spawner registry — Ground / Ramp / Step / Prop / Stickman / Area):

  • Ground / Ramp / Step — TerrainBlock terrain. Single terrain palette items are drag-painted (§21): left-click-and-drag paints a staircase run of blocks; a terrain drag commits on release. Non-terrain (props/stickman/area) palette items keep the Phase 2 single-click repeated placement.
  • Prop (Phase 3b) — opens a selector grid (§22) of the 4 prop templates; selecting one sets the selected_prop_id and enters placement mode spawning that PropBlock (PropUtils.create_*() + the matching material preset). Replaces the earlier separate Crate / Ball buttons.
  • Stickman (Phase 3b) — opens a selector grid (§22) of every .stk in res://content/stickmen/; selecting one sets the selected_stickman_path and enters placement mode spawning a StickmanRig from that file (offset (0, -385) so the feet land on the cursor). Replaces the previous hard-coded single-test.stk placement.
  • Area (Phase 4) — a TriggerArea sensor.

Clicking a palette button enters placement mode, which shows a translucent ghost of the object under the cursor (snapped to the grid when Snap is on); the stickman ghost is a static standing figure. Left-click keeps the Phase 2 repeated-placement behavior (each click/drag commits one placement and the tool stays active); right-click (or Esc) is the explicit "put the tool down" gesture — it cancels any in-progress drag, leaves already-placed cells if a drag had committed, and un-toggles the palette button. Each placement emits object_placed(node). The Stickman and Prop buttons open a selector grid first (a modal PopupPanel, §22) and only enter placement mode once an asset is chosen.

Selection & gizmos (Edit only):

  • Hover highlights the object under the cursor (subtle outline).
  • Left-click selects an object (deselects the previous); click empty space to deselect.
  • Direct drag — click and drag an object to move it (no separate move handle); hold Shift while clicking to toggle its selection.
  • Box-select — drag on empty space draws a marquee; release selects everything inside. Hold Shift to add to the selection.
  • Every selected object shows a white bounding box. A blue rotate ring appears around a single selection (drag it to rotate about the center); it is hidden for multi-selections. Hold Ctrl while rotating to snap to 15° increments.
  • Multi-translate — dragging any selected object moves the whole selection together. The ring is hit-tested first, so ring clicks rotate rather than move.
  • object_selected(nodes) / object_deselected() are emitted on selection changes.

Grid & snap:

  • A Grid checkbox toggles a world-space grid overlay (major line every 5 cells); a Snap checkbox rounds placement and dragging to the grid; a Size spinbox sets the cell size (1–100 px).
  • The grid shows only in Edit mode — it is hidden entirely in Direct and Play (Phase 4b).
  • Grid size, snap, and grid visibility persist to user://sandbox_settings.json. On first run the initial default grid size seeds from sandbox_theme.json grid.snap_size (15.0).

Deletion & camera:

  • Delete / Backspace removes all selected objects (queue_free()) and emits object_deleted(nodes). Deleting also clears the hover highlight (StageSelection.clear_hover()), so a queued-for-deletion node no longer leaves a stale yellow hover box behind.
  • Middle-mouse drag pans; mouse wheel zooms within the exported min_zoom (0.1) / max_zoom (6.0) bounds. Touchpad: pinch to zoom, two-finger drag to pan. The view persists across mode toggles.

Status bar (bottom): a bottom-anchored 28 px bar mirrors the stickman editor's StatusBar pattern. The left label carries the live status text (Objects: N | Selected: <name or count>, plus any pending-walk / rule-builder / toast hints — the Mode: prefix moved to the badge pill). The right label shows live mouse world coordinates as X: ### Y: ###, polled each frame from _camera.get_global_mouse_position() (world-space, so the numbers pan/zoom with the view).

Extendability contract: the spawner uses a Dictionary registry (no hard-coded match on ids), the World container accepts any Node2D, gizmos work on any object via global_position/global_rotation, and the root exposes mode_changed / object_placed / object_selected / object_deselected / object_deleted signals — all hooks for the future Action Queue, Trigger, and Save/Load phases. Ramps/stairs can be placed and props/ragdolls will slide on them, and (Phase 3a) stickmen now walk up/down them via NavigationAgent2D — see §19.

19. Director Tool (Phase 3a)

The Director Tool (Phase 3a) turns the Sandbox Stage into a mini director's workspace: click a stickman, choose actions from a popup, see the script as waypoints/badges in Edit mode, then press Play to run every stickman's action queue. It is not wired into the editor — run via F6 on res://scenes/stage/sandbox_stage.tscn.

File Purpose
res://scripts/characters/stickman/stickman_rig.gd Extended with navigation/walking, speech, an action queue, and the queue runner state machine (see API below).
res://scripts/speech/stickman_speech_bubble.gd class_name SpeechBubble, extends Node2D — a world-space speech bubble drawn via _draw() (ThemeDB.fallback_font), a child of the rig above the head.
res://scripts/stage/stage_director_visuals.gd class_name StageDirectorVisuals, extends Node2D — Edit-mode director overlay (waypoint dots, dashed connectors, action badges, order numbers); hidden in Play. Phase 3c adds hit_test_waypoint_action() (rig/index/pos), set_edit_waypoint()/clear_edit_waypoint() with the pulsing edit highlight for visual walk re-placement, and rule-label → editor routing (§23).
res://scripts/editors/stage/sandbox_stage.gd Extended with the Direct palette button, the action popup + speak/wait dialogs, a code-built NavigationRegion2D re-baked on terrain edits, and Play mode now starting each stickman's queue.

Direct tool workflow (Edit):

  1. Press the Direct toggle button (mutually exclusive with palette placement). A status hint prompts "Click a stickman".
  2. Left-click a stickman → an action popup opens to the right of the clicked stickman (its world position converted to screen, offset 24 px) with Walk To / Speak / Wait / Ragdoll / Recover.
  3. Choose an action:
    • Walk To → enters pending mode; the next left-click on the stage appends {"type":"walk_to","target":click_pos}. Esc cancels the pending target.
    • Speak → a text dialog (AcceptDialog + LineEdit); confirms append {"type":"speak","text":...,"duration":2.0}.
    • Wait → a duration dialog (SpinBox, 0.1–10 s); confirms append {"type":"wait","duration":...}.
    • Ragdoll / Recover → append {"type":"ragdoll"} / {"type":"recover"} immediately.
  4. Waypoints/badges update immediately (queue_changed → dirty flag). Esc exits Direct mode.

Action queue semantics:

  • Each StickmanRig owns its own action_queue: Array[Dictionary]; index order = execution order = visual order (no separate ids). Unknown type → push_warning + the action is skipped (treated as completed).
  • Action shapes (the type key is the discriminator):
type required keys optional keys
"walk_to" "target": Vector2 (feet destination) "speed": float
"speak" "text": String "duration": float
"wait" "duration": float —
"ragdoll" — —
"recover" — —
  • Queues are in-memory only in 3a (no serialization; lost on scene reload).

Waypoint & badge visuals (Edit only): StageDirectorVisuals reads each rig's queue and, per action in order, draws a blue waypoint dot (white outline, order number) at each walk_to target, dashed connectors between consecutive dots (and from the rig's current feet position to the first dot), and badges (speech bubble / clock / X / up-arrow glyph + order number) for non-walk actions anchored to the stickman's position at that point in the sequence — the position derived by simulating the queue (start at the rig's feet; each walk_to advances the anchor; a non-walk action anchors at the position when it is reached). Consecutive badges at the same point stack upward. All sizes divide by the camera zoom so markers stay screen-constant. Pure _draw() — no hit-testing. Hidden in Play.

Play execution:

  • Play mode now runs the director script — stickmen stay ANIMATED and each rig's start_queue() is called (previously they auto-ragdolled on Play). ragdoll / recover are now explicit queue actions; a stickman only falls when directed. Props still unfreeze and tumble (and can knock a directed ragdoll). auto_recover = false in Play (the director owns recovery).
  • On return to Edit (or Direct): each stickman stop_queue(), disables terrain following, then snap_to_standing(), and clear_reactive_actions() strips any rule-injected (reactive) actions appended during the Play session — so reactive waypoint/badge markers do not accumulate across runs, while the authored sequential queue still replays. The waypoint overlay is then re-enabled.
  • Multiple stickmen act simultaneously and independently (per-rig queues + per-rig runners, no shared state).

Continuous terrain following (Phase 6):

  • Terrain adaptation is enabled only in Play. _enter_play_mode() calls begin_terrain_fall() before start_queue(). An unsupported animated rig free-falls kinematically with bounded gravity; walking animation pauses while airborne and resumes on landing. EDIT and DIRECT disable the controller and restore the authored transform/standing pose.
  • The center support sample uses three downward rays at horizontal offsets [-24, 0, +24]; each lower-leg endpoint uses an independent three-ray fan at [-4, 0, +4]. A hit must be a TerrainBlock with an upward-facing normal, and the rig's own collision-proxy RIDs are excluded. The highest valid surface wins, preventing a lower floor under a ramp from pulling a leg through the ramp.
  • stickman_rig.tscn separates animated AnimationTargets/Left_Leg_Base and Right_Leg_Base markers from live IK_Targets/Left_Leg and Right_Leg targets. The rig blends complementary continuous stance/swing weights, holds stance contacts in world space, and applies a predictive 24 px foot guard for smooth contact transitions. Root elevation accumulates across ramps and steps, with centerline pelvis compensation; no foot/ankle bones are added.
  • Navigation uses shallow ribbons over exposed upward-facing TerrainBlock edges, plus bridges between surface endpoints within 64 px horizontally and 96 px vertically for valid steps. The NavigationAgent2D remains at local (0, +385) and uses the selected walk speed; navigation supplies horizontal progress while terrain following owns vertical root motion and arrival avoids Y-correction jitter.

Nav-mesh behavior:

  • A code-built NavigationRegion2D (child of the stage, not World, so it is never hit-tested) carries a procedural NavigationPolygon generated from exposed upward-facing TerrainBlock edges. Each surface becomes a shallow ribbon; neighboring endpoints are bridged only within the 64 px horizontal / 96 px vertical step limits, so solid interiors, walls, undersides, and large gaps are not route surfaces.
  • Auto re-bake on any terrain edit: the nav mesh is marked dirty on terrain place / move / rotate (transform_committed) / delete and re-baked once per frame (coalescing bursts) in _process.
  • Each stickman's NavigationAgent2D is a child of the rig at the feet (local (0, +385) = -FOOT_OFFSET), so it sits on the ground-level mesh; agent and region share the default navigation map, layer 1.
  • walk_to(target) treats target as a feet/ground destination; the rig converts ground-level path points back to root positions with FOOT_OFFSET := (0, -385). Walking is hybrid: an on-mesh target follows the nav path (_walk_mode = "nav"), while an off-mesh / unreachable target (empty path or is_target_reachable() == false) switches to direct straight-line steering toward the clicked waypoint (_walk_mode = "direct", root target = waypoint + FOOT_OFFSET) — it is not rejected with a warning and the rig does not stand still. Nav reads are deferred until the map has synced (map_get_iteration_id(...) != 0) and the path query is forced via get_next_path_position() before any reachability/finished check, so a fresh target is never misjudged as finished/unreachable after a single step.
  • Pathing is kinematic (global_position.move_toward); avoidance_enabled = false in 3a, so stickmen path through props and each other (deferred — see docs/architecture/tech_debt_and_optimizations.md #13).
  • Walk/nav debugging (off by default): const DEBUG_WALK in stickman_rig.gd (per-frame walk trace with mode=nav|direct, plus one-shot walk_to/finish/cancel prints) and const DEBUG_STAGE in sandbox_stage.gd ([stage] target capture, [nav] baked verts/polys, [stage] PLAY rigs).

StickmanRig director API surface:

Member Signature Behavior
RunnerState / ActionPhase enum { IDLE, EXECUTING } / enum { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING } The queue-runner state machine advanced in _physics_process.
FOOT_OFFSET const := Vector2(0.0, -385.0) Feet → root (ground point → hips).
walk_speed @export var walk_speed: float = 300.0 Kinematic walk speed.
walk_to func walk_to(target: Vector2, speed: float = -1.0) -> void Start walking so the feet land at target; no-op unless state == ANIMATED.
is_walking func is_walking() -> bool Whether a walk is in progress.
speak func speak(text: String, duration: float) -> void Show a SpeechBubble for duration s; auto-hides + emits speech_finished.
queue_action / clear_queue / get_queue / remove_action / insert_action / queue_size — The mutable action queue; all mutations emit queue_changed.
clear_reactive_actions func clear_reactive_actions() -> void Drops rule-injected (reactive-tagged) actions, restoring the authored sequential queue; no-op while the runner is EXECUTING (callers invoke it on mode exit after stop_queue()).
start_queue / stop_queue / is_queue_running — Runner control. stop_queue() aborts without emitting queue_finished.
is_ragdoll_at_rest func is_ragdoll_at_rest() -> bool Whether the ragdoll has rested (independent of auto_recover); the runner waits on it for the ragdoll action.
arrived signal arrived walk_to reached its destination.
action_started / action_finished signal(action: Dictionary, index: int) Emitted per action as the runner begins/completes it.
queue_finished signal queue_finished The queue ran to completion (not on stop).
queue_changed signal queue_changed Any queue mutation — drives the visuals dirty flag.
speech_finished signal speech_finished The speech bubble auto-hid after speak().

recover waits for state == RigState.ANIMATED via the existing state_changed signal; _enter_ragdoll() calls _cancel_walking() so a ragdolled rig has no stale walk/path state.

20. Triggers & Event System (Phase 4)

Phase 4 adds reactive storytelling to the Sandbox Stage. While Phase 3a gave directors sequential control (actions in a fixed order per stickman), Phase 4 adds reactive control — "When X happens, do Y" — via event rules that span objects. It is not wired into the editor — run via F6 on res://scenes/stage/sandbox_stage.tscn.

The rule concept is When → Then: a trigger (an event on some object) fires a list of actions on a target object. Rules are stored as _event_rules: Array[Dictionary] of {id, trigger, actions} in sandbox_stage.gd, where trigger carries {type, source, target, ...} instance references and actions is an array of Phase 3a action dicts ({"type":"walk_to","target":...}, etc.).

File Purpose
res://scripts/stage/trigger_area.gd NEW class_name TriggerArea, extends Node2D — a placeable sensor (@export size: Vector2, default 96×96) with a centered local-space get_area_rect() -> Rect2 and a translucent green _draw() fill + dashed border. No physics, no signals — it is a pure geometric region evaluated by the stage.
res://scripts/editors/stage/sandbox_stage.gd Extended with the rule system: _event_rules, the geometric area-entry engine, physics contact routing for stickman/prop collisions (Phase 4.5), the rule-builder UI state machine, and rule auto-cleanup.
res://scripts/characters/stickman/stickman_rig.gd arrived gained a target: Vector2 payload; new enqueue_reactive(actions) appends reactive actions without replaying the queue.
res://scripts/props/prop_block.gd NEW physics contact signals collided(other, contact_normal, contact_position) and separated(other); prop-vs-prop and Phase 4.5 stickman contacts share the contact-monitoring path.
res://scripts/stage/stage_director_visuals.gd Extended with rule visualization + set_rules() / hit_test_rule() / hit_test_waypoint().
res://scripts/stage/stage_spawner.gd New "area" palette registry entry (label "Area"); duck-typed get_area_rect AABB branch in get_world_aabb.
res://scripts/stage/stage_selection.gd Same duck-typed get_area_rect AABB branch for selecting/clicking trigger areas.

Trigger types:

Trigger type Target Fires when
arrived_at_waypoint Stickman The stickman completes a walk_to (the rig's arrived signal).
action_finished Stickman A queue action completes (the rig's action_finished signal).
speech_finished Stickman A speak bubble auto-hides (the rig's speech_finished signal).
entered_area Trigger area A movable (stickman/prop) visibly overlaps the area's transformed world AABB (geometric, _update_area_entry).
collided Stickman/Prop Phase 4.5: a PropBlock physically contacts a stickman's per-part collision proxy; prop-prop contacts continue through PropBlock.collided.

Action types are the Phase 3a set, reused verbatim: walk_to, speak, wait, ragdoll, recover. Actions always target a stickman.

Rule-building workflow (Edit mode):

  1. Press the Direct toggle button (mutually exclusive with palette placement) and click a stickman.
  2. The Direct action popup now has a "⚡ When..." item (in addition to Walk To / Speak / Wait / Ragdoll / Recover).
  3. Choose "⚡ When..." → a trigger sub-menu popup (arrived / action finished / speech finished / entered area / collided).
  4. Pick the trigger target (click a stickman, waypoint, trigger area, or prop depending on trigger type).
  5. Choose an action from a rule-action popup, then optionally its target/position (e.g. click a waypoint for walk_to).
  6. A popup offers "Add another action" (repeat the action step) or "Done" to commit the rule.
  7. Esc is the highest-priority cancel at any step. The rule-builder is a state machine (RuleStep enum: IDLE, SELECT_TRIGGER, TRIGGER_TARGET, SELECT_ACTION, ACTION_TARGET, ACTION_POSITION, PARAMS) with status-bar hints and toast messages.
  8. Choosing "⬅ Back to actions" in the trigger sub-menu does not cancel the rule flow — it resets the rule builder and re-opens the stickman's action popup (the context rig is preserved), so you can back out of building a rule and pick a sequential action instead.

All rule-builder context menus are session-anchored: the first popup in a flow (the action popup right of the clicked stickman, or the rule-label edit entry at the click) records its screen position, and every child popup (trigger sub-menu, rule-action popup, "⬅ Back to actions", "Add another action") reopens at that same recorded position until the rule is confirmed or the flow is cancelled — so cycling menus never walks down the screen.

Rule visualization (Edit only): StageDirectorVisuals draws each rule as a dashed white connector line from the trigger object to the target, a green ⚡ trigger badge, an orange → action badge, a dark label with rule_summary() text, and a ✕ delete icon. Clicking the rule label reopens the rule for consequence-only editing (replaces the rule, keeping the same id); clicking the ✕ deletes the rule. Rules are auto-cleaned (_cleanup_rules_for_nodes) when any referenced object is deleted.

Trigger area placement: the spawn palette gains an "Area" entry (from the "area" registry id). Placement works like any other palette item (translucent ghost, grid snap, click to place). Trigger areas are selectable/movable like other objects via the duck-typed get_area_rect AABB branch in stage_selection.gd / stage_spawner.gd.

enqueue_reactive semantics: StickmanRig.enqueue_reactive(actions: Array[Dictionary]) -> void appends reactive actions to the rig's action queue, tagging each with reactive = true. If the runner is IDLE it resumes at the first newly-appended action — the already-consumed prefix of the queue is not replayed. Sequential Phase 3a queues are untouched; reactive actions are a cross-object addition. Because they are tagged, clear_reactive_actions() can strip them on return to Edit/Direct (see §19), so reactive actions injected across multiple Play sessions never accumulate on top of the authored sequential queue.

20.1 Phase 4c — Trigger Area Reliability

Phase 4c fixes entered_area source matching and visible overlap detection. In PLAY, _update_area_entry() enumerates committed TriggerArea nodes and ANIMATED stickmen plus unfrozen props. Each movable is tested against the area's current transformed world AABB, using the visible object's world AABB (with a point fallback only for a future movable with no bounds). For Phase 6, the current bound is unioned with the previous physics-frame bound, so a fast-falling stickman cannot tunnel through an area between samples. This means a stickman's leg or another visible part can enter an area even when its feet anchor remains outside; the area itself still supports move/rotate/scale transforms.

The event remains edge-triggered per (area instance ID, movable instance ID) pair: one entered_area event is emitted when that pair changes from outside to overlapping, no event repeats while it remains overlapping, and leaving then re-entering emits again. Rule matching requires both the trigger's source ID and target area ID; source = -1 remains the explicit wildcard for any movable. The stage's prop enumeration is correctly scoped so unfrozen props participate without accidentally changing the area loop.

Reactive takeover batches preserve action order. The first takeover cancels existing primary/overlay work once; later takeover actions in the same batch remain queued. Thus a rule containing ragdoll followed by recover executes the ragdoll, waits for rest, and then recovers instead of allowing the later action to erase the earlier one.

Verified coverage in tests/test_phase4c_area.gd: 19 assertions passed, including source-specific and wildcard matching, one-shot/re-entry edges, independent area/movable pairs, transformed areas, visible leg-level overlap with feet outside, speak queue normalization and visible speech execution, no-rule no-op behavior, stale action targets, and deletion cleanup. The editor verification also confirmed that When Stickman A enters Area → Stickman B speaks fires when A walks through the area.

Prop collision detection: Phase 4.5 replaces the old stickman feet-point test with physics contacts between PropBlock and the rig's per-part collision bodies. PropBlock still reports prop-vs-prop contacts through its physics signal; see §20.2 for routing, debouncing, and reaction details.

Edge-triggered dictionaries (which events already fired) are reset on Play/Edit mode entry so a fresh simulation run re-arms every rule.

Persistence scope: rules are in-memory only — they persist across Play/Edit mode toggles (the stage keeps _event_rules alive) but are not saved to disk. Disk save is deferred to a future phase.

Deferred (Phase 5 and beyond): explode_prop / spawn_prop action types, rule conditions and AND/OR combinators, rule variables, and disk save of rules are explicitly out of scope for Phase 4.

20.2 Phase 4.5 — Per-Part Collision Bodies and Hit Reactions

Phase 4.5 gives an ANIMATED StickmanRig a physical collision surface in the Sandbox Stage. It is runtime-only and does not change stickman_rig.tscn, the .stk format, or the editor layout.

  • StickmanRig builds ten metadata-tagged CollisionBody AnimatableBody2D proxies (head, torso, and eight limbs). They are top-level relative to their Body/* visual containers, use layer/mask 1, set sync_to_physics = false, and copy each rendered visual's global transform in _sync_collision_bodies() every physics frame. This explicit synchronization keeps proxies aligned with RemoteTransform2D-driven geometry during poses and WALK-left mirroring.
  • Limb and torso proxies use fitted CapsuleShape2Ds in the adapter's hanging frame. The head uses a CircleShape2D centered from the mounted head geometry's local bounds, with a minimum radius of 90 px and growth when the mounted geometry is larger. These are intentionally approximate collision shapes, not per-shape polygons.
  • PropBlock enables contact monitoring and shape-cast CCD (continuous_cd = CCD_MODE_CAST_SHAPE). Its collided signal includes the normalized world-space contact normal and world-space contact position; a guarded direct-state lookup has a center-direction fallback when the contact row is unavailable. separated mirrors body exit.
  • SandboxStage routes a proxy's stickman_rig / part_name metadata to StickmanRig.apply_collision_reaction(). A (rig, prop) pair tracks active proxy body instance IDs: each newly entered part may react, while the authored rig→prop collided rule fires only on the empty-to-nonempty edge. Ragdoll bodies are ignored for animated reactions and rule dispatch; prop-prop ordering remains prop→other.
  • Reactions are bounded offsets from the bone rotation captured at impact, capped at 0.6 rad, then decayed back to that base rotation. Contact position and force direction in the proxy's local frame select the correct side/quadrant, including mirrored left-facing and upward/downward impacts; the old cumulative full-rotation behavior is not used.
  • Switching PLAY back to DIRECT restores each rig's authored position and rotation, facing profile, IK markers, and torso rotation, including rigs that were still ANIMATED. Collision proxies are disabled for placement ghosts and during RAGDOLL, then restored for RECOVERING/ANIMATED.

Focused verification: tests/test_phase45_collision_bodies.gd (223 assertions), with Phase 4b (53), Phase 4c (19), and Phase 3c (96) regression suites also green after the final fixes.

21. Phase 4b Polish — Mode Switcher, Theme, Terrain Painting, Director UX, Walk Jitter Fix

Phase 4b is a polish + bugfix pass over the Sandbox Stage Builder. It does not add new gameplay systems; it restructures the top bar into a 3-segment mode switcher with contextual toolbars, adds a bottom status bar with live mouse coordinates, a mode badge/frame and per-mode cursors, upgrades terrain placement from single-click into drag-to-paint with a grid spatial dictionary, makes the director's "pick a target" flows kid-friendly (cursor-attached tooltip, rubber-band trajectory, reticle cursor), introduces a hand-editable res://sandbox_theme.json, and fixes the walk-waypoint arrival jitter. Run via F6 on res://scenes/stage/sandbox_stage.tscn; not wired into the editor. See §18 for the mode-switcher / status-bar / badge / cursor surface; this section details the placement, styling, director-UX, and bugfix internals.

21.1 Terrain drag-painting (Edit mode)

Terrain palette items (Ground/Ramp/Step) replace single-click placement with a drag-to-paint "drawing" workflow. Drag-painting quantizes to block units, not 16-px cells. The block-unit stride is the active terrain template's sanitized AABB extent (StageSpawner.get_template_aabb(id).size, computed over the same 16-px terrain-grid sanitize pass _spawn_terrain() actually places):

Template Sanitized stride
Ground 192 × 32
Ramp 192 × 128
Step 256 × 256

Block centers are block_cell * stride, and Bresenham runs over block units. A cursor-following translucent placement ghost (block-unit snapped) shows the block the next paint would stamp — it is freed while a drag is in progress and re-armed after each commit (preserving LMB repeated placement); RMB / Esc puts the tool down.

  1. Select a terrain palette item. Pressing LMB on the stage sets a fixed anchor block unit; dragging updates a target block unit and computes the ordered run between them.
  2. Shift locks the trajectory to a cardinal axis — if |dx| >= |dy| the y-delta is zeroed, else the x-delta (a pure 0°/90°/180°/270° run, no diagonals). Re-evaluated per motion event.
  3. A high-contrast dashed guide line (color from sandbox_theme.json mode_colors.guide_line, default #22c6ff) draws from the anchor to the target block unit via the StagePlacementOverlay (a world-space Node2D sibling of the grid/gizmos). It appears only during a real drag (anchor ≠ target) and disappears instantly on release / Esc / RMB — a single-click placement never draws the guide line or guide circles.
  4. The run uses Bresenham's line algorithm over block units. Along a horizontal/vertical run (including Shift-locked) blocks tile edge-to-edge — no overlap, no gaps; along a free diagonal they tile corner-to-corner (adjacent diagonal blocks share exactly a corner point — zero overlap, visually acceptable corner gaps). Each block unit is classified against the grid spatial dictionary into one of three states, and the per-block ghosts are tinted accordingly:
    • 1 – empty → green ghost → instantiated on release.
    • 2 – occupied by the same block type (same registry spawn_id, e.g. another ground) → neutral/transparent ghost → skipped on release (no double-create / no z-fight). Freshly painted in-drag blocks are marked so a drag crossing its own path skips re-stamping.
    • 3 – occupied by a different/conflicting object (prop/stickman/area/another terrain type) → muted-red ghost → skipped on release.
  5. Release commits the batch atomically: only the "empty" block units spawn (all in one frame), the nav mesh is marked dirty once (_nav_dirty = true, not per node), the grid dictionary is rebuilt, and object_placed(node) fires per node. LMB keeps repeated placement active for the next drag (the placement ghost re-arms).
  6. RMB (or Esc) ends draw/placement mode — it cancels an in-progress drag (nothing is placed until release), clears the guide line, and un-toggles the palette button and restores the cursor.

Grid spatial dictionary (_grid_cells): a runtime Dictionary on SandboxStage keyed by 16-px grid-cell Vector2i (StageSpawner.TERRAIN_GRID_SIZE) → Array[Node2D]. It is advisory only — it drives the 3-state occupancy query (green/neutral/red tint + skip) but is never authoritative for physics or for the block-unit paint stride; the World tree is the source of truth. Occupancy marks all 16-px cells covered by the node's world AABB via _rasterize_aabb_to_cells() (for content/props/stickmen/areas it rasterizes StageSelection.get_world_aabb()), so one Ground block (192×32) spans ≈ 12×2 dictionary cells even though it paints as a single block unit. It is populated on place, updated on move/rotate/delete, and rebuilt on grid-size change. Terrain nodes carry a spawn_id string (TerrainBlock.spawn_id, set by StageSpawner.spawn_id) so same-template overlaps are detectable; StageSpawner.is_terrain_id() distinguishes terrain registry entries and get_template_aabb(id) exposes a terrain template's sanitized local AABB for ghost sizing / block-unit stride / cell rasterization.

21.2 Director targeting UX (Direct / rule-building)

While a click-awaiting director step is active — a pending Walk To target, or the "When…" rule steps (TRIGGER_TARGET / ACTION_TARGET / ACTION_POSITION) — the stage shows a combined workflow:

  • Reticle cursor — _apply_cursor() swaps to a runtime-generated amber flag/reticle cursor (Image.create + ImageTexture, no asset file) whenever _is_awaiting_click() is true.
  • Cursor-attached floating tooltip pill — a dark, rounded high-contrast label following the cursor (offset ~20 px, flipping near the screen edges) reading e.g. 🚩 Click to set walk target, 🎯 Click the trigger area, 💥 Click the prop, etc. (_action_hint_text()). Esc or cancel clears it.
  • Rubber-band dashed trajectory + ghost marker — drawn by StagePlacementOverlay from the action's origin (the stickman's feet for a walk/rule action, or the trigger/action anchor node for the "When…" flows) to the cursor; green when the target is valid and red when invalid (the point lies inside a TerrainBlock AABB, via the grid dictionary). A semi-transparent flag/ring + crosshair ghost marker sits at the target, grid-snapped when Snap is on.

21.3 res://sandbox_theme.json (styling defaults)

The Sandbox theme is schema 2.0 and now covers the code-built chrome, palette, selectors, dialogs, overlays, reusable widget data, fonts, colors, and grid defaults. See the complete current key reference, examples, legacy compatibility notes, and troubleshooting guide in docs/architecture/SANDBOX_THEME.md.

The short version: edit res://sandbox_theme.json, restart the Sandbox scene to reload it, and remember that the live grid size/snap/grid-visibility remain in user://sandbox_settings.json. Missing or malformed theme data falls back to built-in defaults; missing fonts fall back to ThemeDB.fallback_font with a warning. Speech font/size/emoji lists are reusable-widget data reserved for a future speech-customization UI and do not yet alter saved speech actions.

Director-context rule-connector refresh (bugfix): SandboxStage._on_transform_committed() now calls StageDirectorVisuals.mark_dirty() after a move/rotate, so translating a TriggerArea (or any rule-anchoring object) moves its dashed connector and ⚡/→ badges to the new position on drag end. (Rule anchors were already computed live each _draw(); the missing mark_dirty() was leaving them stale because a _draw() never ran.) Deleting a referenced area already triggers _cleanup_rules_for_nodes → set_rules → mark_dirty.

21.4 Walk-waypoint arrival jitter fix (StickmanRig)

The Phase 3a walk could jitter up/down at a waypoint instead of stopping. Root cause: _update_walking() re-computed _walk_mode ("nav" vs "direct") from is_target_reachable() every physics frame, and a waypoint near the nav-mesh boundary could flip that reachability, swapping root_target between the nav path point and the raw waypoint — two targets with a small vertical offset. Fix (all in stickman_rig.gd):

  1. Mode latch once per walk. After the map syncs, _update_walking() probes for up to LATCH_PROBE_MAX_FRAMES (6): it latches "nav" as soon as the agent reports the target reachable, or latches "direct" when the probe bound is reached (genuinely off-mesh). It is never re-evaluated mid-walk, so the rig cannot oscillate between two targets.
  2. Unified arrival radius against the FINAL target. Both branches check global_position.distance_to(_walk_target_feet + FOOT_OFFSET) <= ARRIVE_DISTANCE (8 px root-space), then snap global_position onto the final target before _finish_walk("arrive") — removing any residual offset. The "nav" branch also terminates via nav-finish: when is_navigation_finished() reports the agent at the path's end and the rig is within 2 × ARRIVE_DISTANCE of the final target, it snaps to the final target and finishes — so an on-mesh waypoint whose final path point sits just outside the 8 px radius still stops dead-on instead of drifting. (The Phase 3a unconditional is_navigation_finished()-at-12 px finish path is gone; nav-finish now fires only when already close.)
  3. Steer to the final target when close. In the "nav" branch, when within 2 × ARRIVE_DISTANCE of the final target the rig moves straight at it, ignoring a possibly behind-path next_feet point (prevents reversing).
  4. Atomic stop + marker re-assert. _finish_walk() stops the player, restores the standing markers, then re-asserts them one extra physics frame (_walk_settle_frames = 1 → _settle_walk_markers()), so a residual ±12.5 px walk body-bob keyframe does not pop on arrival.

The result: mode stays constant for the whole walk, exactly one arrived fires, the rig's global_position is unchanged after arrival, and the walk/body-bob animation stops cleanly. DEBUG_WALK / DEBUG_STAGE (off by default) can be enabled to capture the mode/dist/next/final trace at arrival.

21.5 Head LookAt solver fix (stickman_rig.tscn)

stickman_rig.tscn's Head SkeletonModification2DLookAt now uses a full-range, non-inverted band solved in global space (constraint_angle_min = -180, constraint_angle_max = 180, constraint_invert = false, constraint_in_localspace = false). Under the previous ~55°-clamped, inverted, local-space band, the aim solver could oscillate frame-to-frame as the look direction crossed the band boundary, causing a per-frame mirror of the head rather than smooth convergence. With the full-range global band there is no boundary to cross, so dragging the Head IK handle converges cleanly onto the aim point (see §14 Phase 9 Round 7).

22. Asset Library — Stickman & Prop Selector Grids (Phase 3b)

Phase 3b replaces the Sandbox Stage Builder's hard-coded single-stickman and single-prop palette buttons (Crate / Ball / always-test.stk Stickman) with visual selector grids: clicking Stickman or Prop opens a modal grid popup (PopupPanel) of selectable assets; picking one sets the spawner's session selection and enters placement mode with that asset's ghost. It is not wired into the editor — run via F6 on res://scenes/stage/sandbox_stage.tscn. No .stk format change (the .stk schema below is untouched this phase). Pixel rendering (thumbnails) is manual/F6 verification only; headless tests never assert on pixels.

Workflow: press the Stickman or Prop palette button → the grid opens (modal PopupPanel, ESC-closeable) → click a cell → the selection is cached session-only, the grid closes, and placement mode begins (ghost of the selected asset appears). Placing repeats the selected asset; switching selection re-opens the grid. Ground/Ramp/Step and Area remain direct placement buttons, unchanged.

Two grids:

Grid Backing Cells Details
Stickman StickmanLibrary.scan() of res://content/stickmen/*.stk one per .stk (name = stickman_name else filename basename) Rig-rendered thumbnail per cell; Browse… (*.stk FileDialog) selects an arbitrary path; Refresh rescans; empty-state label when no files; single-item skip — exactly one file bypasses the grid and places directly
Prop PropLibrary (4 static templates) Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard Thumbnail + name + material badge; always a single page

Selection is session-only: the chosen selected_stickman_path / selected_prop_id live in memory on StageSpawner, persist across EDIT ⇄ DIRECT ⇄ PLAY toggles, and reset on scene reload. No disk save (user://sandbox_settings.json is not extended). If the currently selected stickman path is no longer in the scan, the first entry is selected instead on open/refresh.

Selector grid (AssetSelector / scenes/stage/asset_selector.tscn): a PopupPanel root (exclusive = true) built as a minimal shell — authored title bar, empty GridContainer, footer (Prev / page label / Next / Browse… / Refresh / Close) — with all dynamic per-cell content (texture + name + material badge) built in code at runtime. open(kind, entries) titles the popup ("Choose Your Stickman" / "Choose a Prop"), hides Browse/Refresh for props, and popup_centered(). Pagination slices 12/page (4×3) with Prev/Next hidden when a single page; a static pure page_bounds(total, page, page_size) helper backs the slicing. Empty state shows a "no stickmen found" label instead of an empty grid. No cell is pre-highlighted on open (the previous selection-highlight border was removed). The popup re-centers on window resize (size_changed → popup_centered() while visible). Signals: item_selected(entry), cancelled(), browse_requested(), refresh_requested().

Thumbnails (rig-accurate, cached): each 200×200 thumbnail is rendered lazily, one per frame, by a Node renderer owning a persistent offscreen SubViewport (transparent_bg, UPDATE_ALWAYS) with an enabled in-viewport Camera2D:

  • StickmanThumbnail.render(stk_data) spawns the real rig (StickmanFactory.spawn_from_data), frames it via StageSpawner.get_world_aabb (degenerate-figure bbox falls back to a fixed rect), awaits RenderingServer.frame_post_draw twice, captures, and frees the rig. This produces a figure identical to what actually gets placed — not a re-implementation of the editor preview.
  • PropThumbnail.render(payload, material_preset) builds a lightweight Polygon2D + Line2D visual (mirroring PropBlock geometry but not a RigidBody2D, so nothing falls under gravity), tinted via PropBlock.tint_for for non-NONE presets.
  • Both return null (treated as a placeholder by the selector) when the capture is blank/empty — the headless-renderer degrade.

Thumbnail cache (ThumbnailCache, user://thumbnails/): rendered PNGs are written to disk and reused on later opens:

Kind Directory Key Invalidation
Stickmen user://thumbnails/stickmen/ "<basename>_<mtime>" (FileAccess.get_modified_time) A modified .stk yields a new key → PNG missing → regenerate; clean_stale_stickmen deletes superseded PNGs for the same basename
Props user://thumbnails/props/ "<id>_v<PROP_VERSION>" (PROP_VERSION = 1) Regenerate when PROP_VERSION bumps or the PNG is missing

StageSpawner seeds the cache in _init; SandboxStage._drain_thumbnail_queue() (called from _process) pops one queued entry per frame, awaits its render, save_pngs it, and hands the texture back via _selector.set_thumbnail(entry, tex) — so scanning 50+ files never stalls the UI (a placeholder texture shows until each cell's thumbnail arrives).

Palette integration & Esc priority: _on_palette_toggled routes stickman/prop presses to _open_selector(id) (not straight to set_placement_mode); un-pressing while the matching selector is open closes it. While the grid is open a dim backdrop (_selector_dim, a black ColorRect at SELECTOR_DIM_ALPHA = 0.5, mouse-ignoring, on the UI CanvasLayer behind the selector) is shown. _open_selector forces the palette button pressed, performs the single-item skip, and (for stickman) reselects the first entry when the selected path is absent. On item_selected the spawner's selected_stickman_path/selected_prop_id are set, the selector closes, and placement begins. Browse… builds a one-off entry via StickmanLibrary.make_entry(path) (toast on failure). Esc is handled in the AssetSelector (_unhandled_input → cancelled) and by the stage in its existing Esc priority chain — rule step → pending walk target → terrain drag → selector → DIRECT → placement → selection. An outside-click that closes the modal popup fires its popup_hide signal, which is routed to _on_selector_cancelled() (idempotency-guarded) so it likewise un-presses the palette button. While the selector is open, _handle_world_click / _handle_mouse_motion early-return (belt-and-suspenders on top of the modal PopupPanel).

New scripts: stickman_library.gd / prop_library.gd / thumbnails/thumbnail_cache.gd / thumbnails/stickman_thumbnail.gd / thumbnails/prop_thumbnail.gd / asset_selector.gd (+ scenes/stage/asset_selector.tscn). Modified: stage_spawner.gd (registry crate/ball → prop; selected_stickman_path/selected_prop_id session state; per-path _stickman_cache; new get_selected_stickman_path()/get_selected_prop_id() getters), sandbox_stage.gd (selector integration), and tests/test_phase4b1_fixes.gd (spawn("crate") → spawn("prop")).

Verification: new headless suite tests/test_phase3b_library.gd (extends SceneTree, no pixel assertions) covering StickmanLibrary.scan()/corrupt-skip/make_entry, PropLibrary.get_entries()/get_default_id(), the StageSpawner registry ids (ground/ramp/step/prop/stickman/area), _spawn_prop/_spawn_stickman honoring selected_prop_id/selected_stickman_path, ThumbnailCache key/path formatting, AssetSelector pagination math (PAGE_SIZE == 12), and scene-load checks. Spec: docs/specs/phase_3b_asset_grid_spec.md.

23. Editor Tools — Action & Rule Editing (Phase 3c)

Phase 3c makes the Sandbox Stage's action queues and event rules fully editable. Where Phase 3a let directors only append actions and Phase 4 only create/delete rules, Phase 3c adds edit / delete / drag-reorder for both, a waypoint right-click context menu with visual walk re-placement and insert-before/after, and both a full and a consequence-only rule editor. The entire system is registry-driven — the action and trigger templates live in two const registries, and the panels/editors generate their UI from them, so a future action/trigger type is a one-entry append. It is not wired into the editor — run via F6 on res://scenes/stage/sandbox_stage.tscn. No .stk format change.

New scripts:

File class_name / extends Responsibility
res://scripts/stage/action_registry.gd ActionRegistry / RefCounted Registry of the 5 action templates (walk_to/speak/wait/ragdoll/recover: label, icon, param spec). Static accessors types() / has_type / label / icon, plus to_rule_action() / from_rule_action() (flat queue-action ⇄ rule-action shape conversions) and summarize().
res://scripts/stage/trigger_registry.gd TriggerRegistry / RefCounted Registry of the 5 trigger templates (label, icon, target_type: waypoint/action_type/none/area/prop). Static types() / has_type / label / icon / target_type / summarize().
res://scripts/editors/stage/queue_panel.gd QueuePanel / PopupPanel Action Queue panel (scrollable list of one stickman's actions, each with ✎ / ✕ / drag-to-reorder ≡). Mutations delegate to the stage via signals; reorders call the rig's remove_action/insert_action. Root of scenes/editors/stage/queue_panel.tscn.
res://scripts/editors/stage/rule_panel.gd RulePanel / PopupPanel Rule list panel filtered by the stage (source stickman or a waypoint), with ✎ / ✕ / drag-to-reorder and Add Rule / Clear All. Reordering emits the new order of the displayed rule ids; the stage maps them back onto its full _event_rules, preserving un-filtered rules' positions. Root of scenes/editors/stage/rule_panel.tscn.
res://scripts/editors/stage/action_editor.gd ActionEditor / PopupPanel Single-action property editor (add & edit). Type dropdown + param fields are generated from ActionRegistry. A walk_to target is captured on the stage (target_requested() → the stage hides the editor, captures a click, calls set_walk_target()).
res://scripts/editors/stage/rule_editor.gd RuleEditor / PopupPanel Rule editor in two modes — full (trigger type + target + actions all editable) and consequence (trigger read-only; only the actions editable). Trigger targets and action actors are captured on the stage via signals.
res://scripts/stage/waypoint_context.gd WaypointContext / PopupMenu Right-click menu for a walk_to waypoint: ✎ Edit this Walk, ✕ Delete this Walk, ⬆ Insert action before, ⬇ Insert action after, and ⚡ Edit Trigger Rules (enabled + shows a count when rules target this waypoint). Item ids: EDIT_WALK/DELETE_WALK/INSERT_BEFORE/INSERT_AFTER/EDIT_TRIGGER_RULES.
res://scenes/editors/stage/queue_panel.tscn, rule_panel.tscn, action_editor.tscn, rule_editor.tscn minimal shells Each .tscn is a bare PopupPanel + root script; all UI is built in code at _ready() (consistent with the asset_selector.tscn pattern).

Modified scripts: sandbox_stage.gd (Phase 3c wiring) and stage_director_visuals.gd (waypoint action hit-testing + pulsing edit highlight + rule-label click → editor).

Theming: the four Phase 3c widgets above are themeable — each accepts an apply_font(ui_font, emoji_font, sizes) call driven by the extended sandbox_theme.json fonts block (style variants ui_font_bold/ui_font_italic, per-widget queue/rule_panel_font_size, action/rule_editor_font_size, panel_row_font_size/panel_title_font_size, style flags panel_title_bold/rule_label_bold/badge_bold, and the optional action_popup {size, bold, italic} object form; action_popup_emoji_size is now consumed). See §21.3 and docs/specs/phase_3c_editor_spec.md §14.

Entry points:

Entry point Gesture Opens
Edit Queue Direct action popup → "📋 Edit Queue…" or right-click a stickman → "📋 Edit Queue…" QueuePanel for that rig
Edit Rules Direct action popup → "⚡ Edit Rules…" or right-click a stickman → "⚡ Edit Rules…" RulePanel filtered to that stickman as trigger source
Edit Trigger Rules Right-click a waypoint → "⚡ Edit Trigger Rules" RulePanel filtered to rules whose arrived_at_waypoint trigger targets that waypoint
Edit a walk Right-click a waypoint → "✎ Edit this Walk", or a walk action's ✎ in the Queue panel Visual walk edit — enters a POSITION target capture with the waypoint highlighted by a pulsing amber ring; the next stage click moves the target
Edit a rule Click a rule's dashed label on stage → consequence-only editor; or a rule's ✎ in the Rule panel → full editor RuleEditor (consequence / full)
Delete / reorder / add ✕ / drag ≡ / "➕ Add Action Rule" in the panels; "Add Rule" only when a source stickman panel is open

Unified target capture (CaptureKind): the ad-hoc "pending target" flows from Phase 3a/4 (walk target, rule trigger target, rule action actor) are unified into a single stage-click capture system on SandboxStage: enum CaptureKind { NONE, WAYPOINT, AREA, PROP, STICKMAN, POSITION }. _begin_capture(kind, hint, on_resolve, on_cancel) sets the current capture kind + status hint + cursor, routes the next left-click through _resolve_capture() (which hit-tests against the kind's expected target — a waypoint dot, TriggerArea, PropBlock, StickmanRig, or a snapped free position), then invokes the resolve callback; Esc runs the cancel callback (which re-pops the calling editor/panel). This replaces the previously scattered per-flow pending states.

Data flow (typical edit-a-queue-action): Queue panel row ✎ → stage _on_queue_panel_edit_requested(index) → for a non-walk action opens ActionEditor.open_edit(action) (pre-filled) with the QueuePanel hidden; on OK committed(action) → stage rewrites the rig queue via remove_action+insert_action → queue_changed → director visuals mark_dirty() → waypoints/badges redraw; _restore_queue_panel() re-pops the (refreshed) panel. For a walk_to, editing routes through the visual walk-edit capture instead. Rule editing follows the same hide-editor → capture (trigger target / actor) → re-pop pattern.

Registry-driven extensibility (per plan §4/§10): adding a new action type = appending one entry to ActionRegistry.ACTION_TEMPLATES (with a params spec); the ActionEditor type dropdown, param fields, and the panels' summaries all appear automatically. Adding a new trigger type = one entry to TriggerRegistry.TRIGGER_TEMPLATES (with a target_type); the RuleEditor trigger dropdown and the RulePanel trigger summaries pick it up. The action data model stays a Dictionary, so new keys are free (the editor shows editable fields for known params keys and ignores unknown ones gracefully).

Backward compatibility: editing preserves each action's type/params and each rule's id; pre-existing (Phase 3a/4-authored) queues and rules load and display through the new panels unchanged. No queue/rule persistence changes — both remain in-memory across EDIT ⇄ DIRECT ⇄ PLAY toggles and reset on scene reload.

Verification: new headless suite tests/test_phase3c_editor.gd (extends SceneTree, 253 assertions) covering both registries (lookups, summaries, unknown-type handling), scene-shell instantiation, ActionEditor open_new/open_edit pre-fill + signal flow, RuleEditor full vs consequence modes + get_action() + id preservation, QueuePanel/RulePanel render + add/edit/delete/clear/reorder flows (mutating the rig queue via its API), WaypointContext item ids + trigger-rules enable/count, StageDirectorVisuals.hit_test_waypoint_action(), the CaptureKind begin/cancel/resolve + Esc priority, and backward compatibility of pre-existing queues/rules. Run:

& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase3c_editor.gd --path .

Spec: docs/specs/phase_3c_editor_spec.md.

24. Prop Interactions and Throw Planning (Phase 11)

The Sandbox Stage action queue supports one-hand prop pickup, drop, and targeted throws. In Direct, select a stickman and add or edit Pick Up Prop, Drop Prop, or Throw Prop in its action queue. A pickup selects a stage prop and a hand (Auto, Left, or Right); a throw captures a stage-world point.

Throw target and guide:

  • The cursor point is the requested target. The effective target is clamped to 3,600 px from the stickman's root; if the cursor is farther away, the guide ends at the capped point while still marking the cursor position.
  • The solver accounts for stage gravity and chooses a valid descending arc whose release speed does not exceed 2,100 px/s. Upward targets are supported when reachable; gravity can reduce their reachable distance. Targets that cannot be reached within the solver's speed and flight-time limits are shown as unreachable.
  • The guide is clipped at the first prop/world collision. In open air, it ends at the selected target; the prop continues under normal physics after that point.

Queue-aware editing preview: when editing a queued throw, the planning guide looks at earlier actions in that queue. If an earlier pickup can provide the prop, the preview uses its grip and hand and estimates the pickup approach when needed. A hint such as “Assumes Step 2 pickup succeeds.” identifies the assumption; uncertain preceding actions label the guide as estimated. This preview does not run the pickup or change live prop, rig, or queue state. If no held or planned prop can be resolved, the guide is replaced by a neutral marker and an informational hint rather than a misleading trajectory. At runtime, the throw is recalculated from the actual prop, actor position, and release pose.

File format (.stk)

Files are UTF-8 JSON, pretty-printed with tab indentation. The format is versioned and designed to remain backward/forward compatible — new fields can be added without breaking older files.

{
  "version": "1.5",
  "stickman_name": "Bob",
  "part_order": [
    "head",
    "torso",
    "left_upper_arm",
    "left_lower_arm",
    "right_upper_arm",
    "right_lower_arm",
    "left_upper_leg",
    "left_lower_leg",
    "right_upper_leg",
    "right_lower_leg"
  ],
  "proportions": {
    "upper_arm_length": 168.0,
    "lower_arm_length": 200.0,
    "upper_leg_length": 200.0,
    "lower_leg_length": 200.0,
    "torso_length": 391.5
  },
  "body_parts": {
    "head": {
      "shapes": [
        {
          "shape_type": "circle",
          "closed": true,
          "points": [
            { "x": 120, "y": 40 },
            { "x": 124, "y": 39 }
          ],
          "color": "#000000",
          "vertex_flags": [0, 0, 1, 0]
        }
      ],
      "position": { "x": 150, "y": 40 },
      "rotation": 0.0,
      "scale": { "x": 1.0, "y": 1.0 },
      "pivot": { "x": 122.0, "y": 39.5 },
      "length": 10.0,
      "guide_offset": { "x": 122.0, "y": 431.0 }
    }
  },
  "metadata": {
    "created_at": "2026-08-05T12:00:00",
    "modified_at": "2026-08-05T12:00:00"
  }
}

Root object

Key Type Description
version string Format version. Currently "1.5". Loading supports "1.0"–"1.5" (auto-migrated).
stickman_name string Optional display name for the figure.
part_order array[string] Phase 5. Render/Z-order of parts in the Whole Stickman preview, front-to-back semantics per array position (first = back, last = front). Absent on v1.0–v1.2 files; defaults to the internal part-key order when missing.
proportions object Phase 8. Rig bone lengths used by the runtime StkRigAdapter. Object with 5 float keys: upper_arm_length (168.0), lower_arm_length (200.0), upper_leg_length (200.0), lower_leg_length (200.0), torso_length (391.5). Hardcoded master-rig rest-pose constants (from stickman_rig.tscn), not measured from the user's shapes. Write-only metadata — never read back on load.
body_parts object Map of body_part_name → shape objects. Keyed by the 10 internal part names below.
metadata.created_at string Timestamp written on save.
metadata.modified_at string Timestamp written on save.

Part keys

head, torso, left_upper_arm, left_lower_arm, right_upper_arm, right_lower_arm, left_upper_leg, left_lower_leg, right_upper_leg, right_lower_leg

Part object

Each body part is an object with the following keys:

Key Type Description
shapes array Array of shape objects (see below). Draw order = array order (first = back, last = front). Wrapped as a single-element array when migrating v1.0/v1.1 files.
position object { "x": float, "y": float } position of the part within the Whole Stickman preview.
rotation float Rotation in degrees. 0.0 = original orientation. Defaults to 0.0 for v1.0/v1.1 files.
scale object { "x": float, "y": float } scale factors relative to created size. 1.0 = original size. May be negative (Phase 5) to represent mirroring along an axis. Defaults to { "x": 1.0, "y": 1.0 } for older files.
pivot object Phase 8. { "x": float, "y": float } — the local rotation origin, i.e. the bounding-box center of all shape points in the part's local drawing space (before position is applied). Always written for all 10 parts; { "x": 0.0, "y": 0.0 } for an empty part. Write-only metadata — never read back on load.
length float Phase 8. The part's bounding-box extent along its segment axis, in local pixels: width (max_x - min_x) for the 4 arm parts, height (max_y - min_y) for torso, legs, and head. 0.0 for an empty part. Write-only metadata — never read back on load.
guide_offset object Phase 9 Round 5. { "x": float, "y": float } — the part's bbox center in preview space minus its silhouette-guide joint in preview space (a pure master-space delta; the panel-size terms cancel). The adapter uses it to reproduce the editor's guide-relative placement in the harness. Write-only metadata — never read back on load; v1.0–v1.4 files load unchanged and gain the key on their next save.

Shape object

Each entry in the shapes array:

Key Type Description
shape_type string "line", "rectangle", "circle", or "" (empty). Descriptive tag only — rendering uses closed.
closed bool Whether the polygon is closed (filled) or open (outline-only).
points array Array of { "x": float, "y": float } vertex positions local to the drawing area.
color string Hex colour, e.g. "#000000". Changeable in-app via right-click → Color... (Phase 3).
vertex_flags array[int] Same length as points. 0 = original vertex, 1 = user-created via "Create Point". Controls handle appearance.

On load, parts missing from the file fall back to built-in default preview positions. v1.0 files have closed inferred from shape_type and vertex_flags default to all zeros. v1.0/v1.1 files have their single shape wrapped in a shapes array and default rotation/scale applied. Files without part_order (v1.0–v1.2) use the default part-key order. Note that shape mirroring (Phase 5) rewrites the points array in place, so no new shape fields are required — the resulting vertices load identically across all supported versions.

File format (.prop)

The Prop Editor (Phase 9, extended by Phase 10a) authors vector prop assets saved as .prop files, stored under res://content/props/*.prop. Files are UTF-8 JSON, pretty-printed with tab indentation, versioned, and designed to remain backward compatible — the format stores the editor's full multi-shape stack, while a deterministic converter (PropFile.flatten_to_payload()) collapses it into the single-shape payload PropUtils.spawn_prop() consumes at runtime (frontmost closed polygon → else circle → else the first shape; lines are editor-only and never become collision geometry).

In the Prop Editor, Arrange exposes Transform Whole Prop, Normalize Size, Center Prop, and Show Stickman Guide in that order. Whole-prop move, origin-based rotation, and uniform resize preview from a snapshot and commit once; Snap rounds movement, Ctrl snaps rotation to 15°, and Esc cancels. The Sandbox Stage has a separate single-prop corner scale gesture whose visual and collision bounds remain synchronized during PLAY.

{
  "version": "1.0",
  "prop_name": "My Crate",
  "material_preset": "wood",
  "shapes": [
    {
      "shape_type": "polygon",
      "points": [
        { "x": -24.0, "y": -24.0 },
        { "x": 24.0, "y": -24.0 },
        { "x": 24.0, "y": 24.0 },
        { "x": -24.0, "y": 24.0 }
      ],
      "fill_color": "#8C6133",
      "outline_color": "#40290F",
      "outline_width": 2.0,
      "closed": true,
      "vertex_flags": [0, 0, 0, 0],
      "reshaped": false
    },
    {
      "shape_type": "circle",
      "radius": 24.0,
      "fill_color": "#E63333",
      "outline_color": "#590D0D",
      "outline_width": 2.0,
      "closed": true,
      "vertex_flags": [],
      "reshaped": false
    }
  ],
  "metadata": {
    "created_at": "2026-09-18T10:00:00",
    "modified_at": "2026-09-18T10:00:00"
  }
}

Root object

Key Type Description
version string Format version. Currently "1.0". A non-"1.0" version is rejected on load.
prop_name string Optional display name; an empty name falls back to the filename basename.
material_preset string "none" | "wood" | "rubber" | "cardboard" | "metal". Missing/invalid → "none". Sets mass + physics_material_override (from PropBlock's static preset tables).
shapes array Array of shape objects (see below). Draw/Z-order = array order (first = back, last = front).
metadata.created_at string Timestamp written on save. Write-only — ignored on load.
metadata.modified_at string Timestamp written on save. Write-only — ignored on load.

Shape object

Each entry in the shapes array:

Key Type Description
shape_type string "polygon", "circle", or "line". Missing → "polygon".
points array Array of { "x": float, "y": float } vertices — polygon/line only. Missing → [].
radius float Circle only; default 24.0.
fill_color / outline_color string Hex colour (#RRGGBB / #RRGGBBAA). Missing → the material tint (from PropBlock.tint_for).
outline_width float Vector outline thickness. Default 2.0 (PropBlock.DEFAULT_OUTLINE_WIDTH).
closed bool Polygon/circle true (filled); line false (outline-only).
vertex_flags array[int] Same length as points. 0 = original vertex, 1 = user-created via "Create Point" (circles: empty).
reshaped bool Round 6. false by default. Set true when the shape's vertices were edited (insert/remove/drag) or a circle was scaled non-uniformly; cleared by set_shape_radius(). The inspector hides primitive-specific fields (today: the circle Radius row) when set. Additive/backward-compatible — old files load as false.

Backward compatibility rule: unknown/missing keys fall back to defaults, and only additive keys may be introduced — never change the meaning of an existing key. The reshaped flag is the Round 6 addition; a material_preset of "none" and a missing shape_type both fall back gracefully.

Save-serialization note (Round 5): point coordinates must be written through a Variant-coercion helper (_coerce_points). Assigning an Array of {x,y} dictionaries directly to a typed PackedVector2Array silently zeroes every coordinate in Godot — files saved by the Round 1–4 build were unrecoverable.

Settings file (user://settings.json)

Phase 3 introduces a global settings file stored in Godot's user data directory (user://), separate from the .stk figure files. It persists the grid configuration and is versioned for future extensibility:

{
  "version": "1.0",
  "grid_size": 15,
  "snap_to_grid": false,
  "show_pose_guide": true
}

Settings keys

Key Type Default Description
version string "1.0" Settings file version (for future extensibility).
grid_size int 15 Grid interval in pixels, applied equally to width and height. Default changed from 5 to 15 in Phase 5. Clamped to 1–100 on load.
snap_to_grid bool false Whether snap-to-grid is active.
show_pose_guide bool true Whether the pose silhouette guide is visible in the Whole Stickman preview.

Behavior:

  • Load — read on editor startup; if the file is missing or fails to parse, defaults (grid_size = 15, snap_to_grid = false, show_pose_guide = true) are used silently (no error dialog).
  • Save — written whenever the user changes the grid size, toggles Snap to Grid, or toggles the pose guide.
  • Scope — global; all body-part panels and the Whole Stickman preview share the same grid size and snap setting.
  • Pan offsets are NOT persisted — they reset on load, clear, and "Reset Views".

Prop Editor settings (Phase 9, separate files): the standalone Prop Editor uses its own user-data files — user://prop_editor_settings.json (schema { "version": 1, "recent_props": [...] }, the last up-to-5 opened/saved .prop paths, most-recent-first) and user://prop_editor_autosave.prop (the debounced 1.0 s autosave, written atomically via PropFile.save_safe with metadata.autosave/source_path/modified_unix; cleared on save/load/new/discard, kept after Restore, and dropped when stale at startup). The Prop Editor does not read or write user://settings.json.

Project structure

Path Purpose
res://project.godot Engine config; sets main scene to the editor and enabled features.
res://scenes/main.tscn Startup wrapper configured as run/main_scene; hosts the Stickman Editor.
res://scenes/editors/stickman/stickman_editor.tscn Stickman Editor layout, File/Edit/View menu bar, dialogs (GridConfigDialog + SpinBox), column containers (unique-name nodes).
res://scenes/editors/stickman/body_part_panel.tscn Reusable single body-part editor panel (title, drawing area, context menu, ColorPickerPopup); expands vertically in its column.
res://scripts/editors/stickman/stickman_editor.gd Editor controller — File/Edit/View menu actions, save/load/clear, JSON v1.5 serialization with multi-shape/rotation/scale, part_order, Phase 8 proportions/pivot/length, and Phase 9 Round 5 per-part guide_offset export, settings.json load/save, editor-wide shape clipboard (Copy/Paste across panels), broadcast of grid/snap settings to panels, Reset Views, populates panels, coordinates selection across panels.
res://scripts/characters/stickman/stk_rig_adapter.gd Phase 8, extended by Phase 9 (Rounds 4–6 bugfix). Standalone runtime adapter (class_name StkRigAdapter, static func apply(stk_data, rig)): fits an instantiated stickman_rig.tscn to a loaded .stk by re-fitting the 8 limb bones (Skeleton2D/Torso/... Bone2D lengths + lower-bone origins), recalibrating the IK targets (`IK_Targets/Left
res://scripts/characters/stickman/stickman_factory.gd Phase 9. Runtime entry point (class_name StickmanFactory, extends RefCounted); a static factory that turns a .stk file into a live, rigged stickman_rig.tscn instance. load_stk(path) reads + parses the file ({} + push_warning on failure); spawn_from_data(stk_data) instantiates res://scenes/characters/stickman/stickman_rig.tscn, calls StkRigAdapter.apply(stk_data, rig), and returns the rig root typed as StickmanRig (the rig now carries the StickmanRig root script); spawn(path) chains them (null on empty data). Not used by the editor.
res://scripts/characters/stickman/stickman_rig.gd Phase 9 Task 4. class_name StickmanRig, extends Node2D; the runtime owner of facing direction, per-joint bone bend, Body/* z-order, and (Phase 10/11) the kinematic-to-ragdoll state switch with instant handoff + stand-up recovery, attached to the stickman_rig.tscn root Master. Exports a facing_profile preset (FacingProfile LEFT/RIGHT/FORWARD, default FORWARD) and four @export_enum("Normal","Inverted") per-joint bend vars (left_arm_bend/right_arm_bend/left_leg_bend/right_leg_bend), plus (Phase 11) rest_timeout (2.0 s) and auto_recover (true) exports. Non-@tool: resolves Skeleton2D/Body/bend joints at runtime, enables its own modification stack, and applies the profile (flag writes + Body/* reorder) in _ready() and setters. Signals facing_profile_changed / bend_flag_changed / state_changed; public API set_facing_profile/get_facing_profile, set_joint_bend_flipped/get_joint_bend_flipped, get_bend_joints(), get_bend_joint_global_position(), plus the ragdoll API set_ragdoll(enabled)/toggle_ragdoll()/is_in_ragdoll()/request_recovery() with state / enum RigState { ANIMATED, RAGDOLL, RECOVERING }. The recovery cycle is pre-capture → ragdoll pose capture (7-ray ground probe) → mirror-aware re-anchor → snap (entry-captured chain offsets, (−π, π]-normalized bones, ankle clamp) → 2.0 s tween to the pre-ragdoll pose (arms/head bone-driven with _natural_joint_target for elbows; legs IK-driven) → finish (profile + bend flags + LookAt restored), with every per-joint bend decision editable in the RAGDOLL_JOINT_BAND_LIMITS / RECOVERY_LEG_SIDE_FORCE / RECOVERY_JOINT_PATH tables (Joint bend configuration (EDIT ME)). Diagnostics are gated behind DEBUG_RECOVERY := false. Phase 6 adds PLAY-only begin_terrain_fall() / set_terrain_following_enabled(), center/per-foot TerrainBlock ray fans with self-proxy filtering, layered leg base markers vs live IK targets, continuous stance/swing blending with a predictive foot guard, accumulated root elevation/pelvis compensation across ramps/steps, and bounded free-fall. (Phase 4b) _update_walking() latches _walk_mode once per walk (LATCH_PROBE_MAX_FRAMES 6), unifies arrival on the final target at ARRIVE_DISTANCE (snap-on-arrive), steers to the final target when close, and re-asserts standing markers one frame after stop — fixing the walk-waypoint arrival jitter. (Phase 4.5) builds ten top-level AnimatableBody2D collision proxies, explicitly synchronizes them to the rendered Body/* transforms each physics frame, derives the head circle center/radius from mounted geometry (90 px minimum), and applies bounded, contact-quadrant-aware bone flinches that decay to captured base rotations. snap_to_standing() and the stage's PLAY→DIRECT reset restore authored transforms and pose state. Null-guarded (push_warning + skip). Not used by the editor.
res://scripts/tools/create_animations.gd Phase 11. @tool extends EditorScript; a standalone editor utility (run manually with stickman_rig.tscn open; not auto-loaded or referenced at runtime) that supersedes the deleted scripts/create_walk.gd. _run() bakes walk_left/walk_right (same keyframes as the old script) and a one-shot stand_up (POSE_DOWN → POSE_STANDING, STAND_UP_DURATION 2.0, loop_mode = LOOP_NONE) into the open scene's default AnimationLibrary. The baked stand_up is an authored reference only — runtime recovery does not play it (StickmanRig tweens the IK targets directly from the captured ragdoll pose, since a fixed first keyframe can never match an arbitrary rest pose). walk_right is the canonical walk clip; walk_left remains baked in the library but is not used at runtime (facing is set explicitly and the rig root X-mirrors for LEFT).
res://scripts/tools/test_harness.gd Phase 9. Standalone staging scene (run via F6 on res://scenes/tools/test_harness.tscn, not wired into the editor) for debugging bone scales, vector-drawing offsets, and IK limits in isolation. Top UI bar: "Open .stk…" / quick-select buttons (content/stickmen/break.stk, content/stickmen/basic.stk, content/stickmen/test.stk), "Show Bones" / "Show IK Handles" toggles, loaded-filename label. SubViewport world + enabled Camera2D (middle-mouse pan, wheel zoom, recenter on spawn); each load frees the previous rig and spawns a fresh one via StickmanFactory.spawn(). A world-space debug overlay draws true bone segments (joint dots + parent→child lines, with limb leaf bones drawn out to their IK targets so wrist/ankle joints are visible; the Head leaf is the exception — its target is a LookAt aim point, not a joint, so it draws a ~90 px segment along the bone's own direction instead) and colored IK-target markers (hands green, feet blue, head yellow, torso magenta) plus a semi-transparent yellow head-aim line; the 6 Marker2D IK targets are click-draggable — the 4 limb targets flex limbs live via SkeletonModificationStack2D TwoBoneIK (the rig self-enables its stack), the Torso target translates the whole rig via its RemoteTransform2D, and the Head target drives the head's LookAt aim rotation (Phase 9 Round 7).
res://scenes/tools/test_harness.tscn Phase 9. Standalone staging scene backing scripts/tools/test_harness.gd (run via F6; not wired into the editor).
res://scripts/terrain/terrain_block.gd Vector Terrain System. class_name TerrainBlock, extends StaticBody2D — a reusable vector terrain component building Polygon2D (fill) + Line2D (border) + CollisionPolygon2D (BUILD_SOLIDS, supports concave) children in code. Has a spawn_id: String property (set by StageSpawner) so same-template terrain overlaps are detectable during drag-painting.
res://scripts/terrain/terrain_utils.gd Vector Terrain System. class_name TerrainUtils, extends RefCounted — static sanitize_points() (grid snap → local _simplify_polyline() → clockwise enforcement) and a spawn_block() factory.
res://scripts/tools/physics_test_harness.gd Vector Terrain System / Dynamic Vector Props. Standalone staging scene root building flat/ramp/step terrain via TerrainUtils, instantiating stickman_rig.tscn, spawning props via 1/2/3 (PropUtils), and adding a rig collision proxy (run via F6; not wired into the editor). A toggle-mode button flips the rig's kinematic-to-ragdoll mode via _rig.set_ragdoll(), plus (Phase 11) a Rest SpinBox (writes _rig.rest_timeout) and "Recover Now" button (_rig.request_recovery()); the rig's state_changed signal removes the proxy on RAGDOLL and re-adds it (idempotently) on ANIMATED/RECOVERING.
res://scenes/tools/physics_test_harness.tscn Vector Terrain System / Dynamic Vector Props. Standalone staging scene backing scripts/tools/physics_test_harness.gd (run via F6; not wired into the editor).
res://scripts/props/prop_block.gd Dynamic Vector Props / Phase 4.5. class_name PropBlock, extends RigidBody2D — a reusable physical prop building Polygon2D (fill) + Line2D (outline) + CollisionPolygon2D/CollisionShape2D (polygon/circle collision) children in code, with material presets (mass + friction/bounce), live-updating exports, contact monitoring, shape-cast CCD, and collided(other, contact_normal, contact_position) / separated(other) signals carrying world-space contact data. Also the single source of truth for the Prop Editor's inspector physics readouts.
res://scripts/editors/prop/prop_editor.gd Prop Editor (Phase 9). class_name PropEditor, extends Control — standalone vector-prop authoring controller (run via F6 on res://scenes/editors/prop/prop_editor.tscn; not the main scene). Owns the 📁 File (🆕 New / 💾 Save / 📄 Save As… / 📂 Load… / 🕑 Recent) and 👁️ View (🔍 Reset View) menus, the top-right Grid spinbox + Snap checkbox, the centered Arrange/Reshape mode toolbar, the material-preset + per-shape inspector (physics readouts from PropBlock; hides the circle Radius row + shows a "reshaped" note when a shape's reshaped flag is set), the status bar, the window title builder, the exit dialog (💾 Save / 🗑️ Discard / ❌ Cancel), recent-props persistence (user://prop_editor_settings.json, max 5), and the debounced autosave/recovery pipeline (user://prop_editor_autosave.prop via PropFile.save_safe). Saves export a cached 200×200 thumbnail.
res://scripts/editors/prop/prop_canvas.gd Prop Editor (Phase 9). class_name PropCanvas, extends Control — multi-shape vector canvas: cursor-centered zoom/pan (wheel, pinch, middle-mouse), EditMode { TRANSFORM, POINTS } ("Arrange"/"Reshape") gating input + drawing only, rotation dot + corner scale crosses (Ctrl = 15° snap / aspect lock), color-matched vertex handles with Create/Remove Point (0.6 s flash), per-mode right-click menus, a local clipboard, mirror + z-order, the per-shape reshaped flag, sanitize-on-commit via TerrainUtils, and two-tone halo selection.
res://scripts/editors/prop/prop_file.gd Prop Editor (Phase 9). class_name PropFile, extends RefCounted — static .prop v1.0 schema + converters: to_file_dict/from_file_dict (round-trip, version rejection, missing-key defaults), preset_to_string/from_string, sanitize_shape, flatten_to_payload (frontmost closed polygon → circle → first shape), save/load, and the atomic save_safe (tmp/.bak swap). Points must be coerced via _coerce_points when serializing (Round 5).
res://scripts/editors/prop/prop_theme.gd Prop Editor (Phase 9). class_name PropTheme, extends RefCounted — validated loader for res://prop_theme.json (deep-merge, warn-once, clamped accessors); hierarchy theme file → DEFAULT_CONFIG → PropBlock constants. Physics is never themeable. Holds the Round 2–4 ui tokens (fonts, hit targets, colors). Applied through a local Theme only.
res://scenes/editors/prop/prop_editor.tscn Prop Editor (Phase 9). Standalone scene backing scripts/editors/prop/prop_editor.gd (run via F6; not the main scene): minimal shell (%MenuBar in %TopRow beside %TopRightControls, %TopBar/%PropNameEdit, %MainLayout/%CanvasPanel/%CanvasArea/%ModeBar/%PropCanvas, %InspectorColumn/%InspectorList, status bar, dialogs); UI built in code.
res://scripts/props/prop_utils.gd Dynamic Vector Props. class_name PropUtils, extends RefCounted — static create_box() / create_ball() / create_plank() / create_triangle() primitive generators and a spawn_prop() factory (sanitizes polygon points via TerrainUtils). PropFile.flatten_to_payload() produces the payload it consumes.
res://scripts/editors/stage/sandbox_stage.gd Sandbox Stage Builder / Phase 4.5. class_name SandboxStage, extends Node2D — root controller: enum StageMode { EDIT, DIRECT, PLAY } state machine (freezes props with FREEZE_MODE_KINEMATIC; runs stickman queues + rags and props/areas in PLAY), placement mode + terrain drag-painting, a grid spatial dictionary, camera pan/zoom, deletion, bottom status bar, mode badge/frame/cursors, the res://sandbox_theme.json loader, and signal fan-out (mode_changed(mode: int) / object_placed / object_selected / object_deselected / object_deleted). Phase 3b instantiates the AssetSelector popup + thumbnail renderers, owns the selector open/close flow and lazy per-frame thumbnail drain (§22). Phase 3c instantiates the queue/rule panels + editors + waypoint context, owns the unified CaptureKind target-capture system, the "Edit Queue…"/"Edit Rules…" + right-click context entry points, the shared confirmation dialog, and consequence-only rule editing (§23). Phase 4.5 routes PropBlock physics contacts through proxy metadata, tracks per-part body IDs for pair debouncing, preserves rig→prop rule ordering, purges stale pairs on separation/deletion/ragdoll, and restores authored rig state when returning PLAY→DIRECT. Standalone staging scene run via F6; not wired into the editor.
res://scripts/stage/stage_spawner.gd Sandbox Stage Builder. class_name StageSpawner, extends RefCounted — registry-driven factory (Array[Dictionary], no id match); reuses TerrainUtils / PropUtils / StickmanFactory; centers terrain on its origin. Exposes is_terrain_id() / get_template_aabb() and tags spawned terrain with a spawn_id. get_template_aabb(id) mirrors _spawn_terrain()'s sanitize pass (TerrainUtils.sanitize_points at TERRAIN_GRID_SIZE 16), so the returned extent matches the real placed footprint — e.g. the 200-px-wide Ground template returns a 192-px stride — and drives the block-unit paint stride, ghost sizing, and cell rasterization. Phase 3b: registry ids ground/ramp/step/prop/stickman/area (separate crate/ball removed); holds selected_stickman_path / selected_prop_id session state + a per-path _stickman_cache; prop/stickman spawn the selected asset (§22).
res://scripts/characters/stickman/stickman_library.gd Asset Library (Phase 3b). class_name StickmanLibrary, extends RefCounted — scans res://content/stickmen/*.stk into {path, name, data} entries (corrupt/missing-body_parts skipped; name = stickman_name else filename basename); make_entry(path) for Browse-chosen paths.
res://scripts/props/prop_library.gd Asset Library (Phase 3b). class_name PropLibrary, extends RefCounted — static registry of the 4 prop templates (Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard); get_default_id() = "crate".
res://scripts/shared/ui/asset_selector.gd Asset Library (Phase 3b). class_name AssetSelector, extends PopupPanel — grid popup controller (root of scenes/stage/asset_selector.tscn): pagination 12/page, empty state, per-cell thumbnail + name + prop material badge, Browse/Refresh/Close, Esc.
res://scripts/shared/thumbnails/stickman_thumbnail.gd Asset Library (Phase 3b). class_name StickmanThumbnail, extends Node — renders a parsed .stk to a Texture2D via the real rig in an offscreen SubViewport.
res://scripts/shared/thumbnails/prop_thumbnail.gd Asset Library (Phase 3b). class_name PropThumbnail, extends Node — renders a prop template to a Texture2D (lightweight non-physics visual).
res://scripts/shared/thumbnails/thumbnail_cache.gd Asset Library (Phase 3b). class_name ThumbnailCache, extends RefCounted — disk PNG cache (user://thumbnails/) keyed by basename+mtime (stickmen) / id_v<PROP_VERSION> (props); load/save/stale cleanup.
res://scenes/stage/asset_selector.tscn Asset Library (Phase 3b). PopupPanel root + asset_selector.gd — minimal shell (title bar, empty grid, footer); dynamic cells built in code.
res://scripts/stage/action_registry.gd Editor Tools (Phase 3c). class_name ActionRegistry, extends RefCounted — const registry of the 5 action templates (walk_to/speak/wait/ragdoll/recover: label/icon/params); static types()/has_type/label/icon, to_rule_action()/from_rule_action(), summarize().
res://scripts/stage/trigger_registry.gd Editor Tools (Phase 3c). class_name TriggerRegistry, extends RefCounted — const registry of the 5 trigger templates (label/icon/target_type); static types()/has_type/label/icon/target_type/summarize().
res://scripts/editors/stage/queue_panel.gd Editor Tools (Phase 3c). class_name QueuePanel, extends PopupPanel — Action Queue panel (one stickman's actions with ✎/✕/drag-reorder); mutates the rig queue via its API.
res://scripts/editors/stage/rule_panel.gd Editor Tools (Phase 3c). class_name RulePanel, extends PopupPanel — rule list panel filtered by source stickman or waypoint, with ✎/✕/drag-reorder + Add Rule/Clear All.
res://scripts/editors/stage/action_editor.gd Editor Tools (Phase 3c). class_name ActionEditor, extends PopupPanel — add/edit single-action editor; type dropdown + params generated from ActionRegistry; walk_to target captured on stage.
res://scripts/editors/stage/rule_editor.gd Editor Tools (Phase 3c). class_name RuleEditor, extends PopupPanel — rule editor in full or consequence-only modes (trigger read-only); actions add/edit/remove.
res://scripts/stage/waypoint_context.gd Editor Tools (Phase 3c). class_name WaypointContext, extends PopupMenu — right-click waypoint menu: Edit/Delete Walk, Insert action before/after, Edit Trigger Rules.
res://scenes/editors/stage/queue_panel.tscn / rule_panel.tscn / action_editor.tscn / rule_editor.tscn Editor Tools (Phase 3c). Minimal PopupPanel shells (bare root + root script); all UI is built in code at _ready().
res://scripts/stage/stage_selection.gd Sandbox Stage Builder. class_name StageSelection, extends RefCounted — hover/click/box selection via geometric world-space AABB hit-testing (frontmost World child wins; RagdollBodyContainer subtree excluded); hover_changed / selection_changed signals.
res://scripts/stage/stage_gizmos.gd Sandbox Stage Builder. class_name StageGizmos, extends Node2D — hover highlight + selection outline + rotate ring via _draw() and distance-based hit-testing; objects are dragged directly (no move handle); drives global_position / global_rotation; emits transform_committed.
res://scripts/stage/stage_grid.gd Sandbox Stage Builder. class_name StageGrid, extends Node2D — optional world-space grid overlay (major line every 5 cells) that pans/zooms with the camera; grid_size / enabled set by SandboxStage.
res://scripts/stage/stage_placement_overlay.gd Sandbox Stage Builder (Phase 4b). class_name StagePlacementOverlay, extends Node2D — world-space overlay drawing the terrain drag-painting dashed guide line (set_terrain_guide / clear_terrain_guide) and the director action rubber-band trajectory + ghost marker (set_action_trajectory / clear_action); pure drawing, no hit-testing.
res://sandbox_theme.json Sandbox Stage Builder (Phase 4b). Hand-editable styling defaults for the sandbox (font paths/sizes, grid snap default, mode accent + guide-line colors); loaded by SandboxStage._load_theme() with defaults on missing/malformed file. Phase 3c extends the fonts block (bold/italic variant paths, per-widget sizes/style flags, optional action_popup object form) and drives the panels'/editors' apply_font(...) (§21.3 / spec §14).
res://scenes/stage/sandbox_stage.tscn Sandbox Stage Builder. Standalone staging scene backing scripts/editors/stage/sandbox_stage.gd (run via F6; not wired into the editor): root Node2D + Camera2D + empty World; the gizmo layer, placement overlay, and CanvasLayer UI (mode switcher, toolbars, bottom status bar, badge, tooltip) are built in code.
res://scripts/editors/stickman/body_part_panel.gd Multi-shape creation, vertex editing, shape dragging, per-panel zoom & pan, grid drawing & snap-to-grid, ColorPicker, shape/vertex delete, Z-ordering (Send Back / Bring Forward), shape Copy/Paste, shape Mirror X/Y, drawing (fill + outline for closed shapes).
res://scripts/editors/stickman/whole_stickman_preview.gd Assembly preview, drag-to-reposition, part selection with white bounding box, rotation gizmo (circle below box) with Ctrl 15° snap, scale gizmo (corner crosses) with Ctrl aspect lock, part Z-ordering (Send Back / Bring Forward) via part_order, part Mirror X/Y (scale negation), zoom & pan, grid drawing & snap-to-grid, pose silhouette guide (Phase 7), part hit-bounds, labels, and (Phase 9 Round 5) get_guide_joint_preview() — the preview-space position of a guide joint, used by the editor to export per-part guide_offset.
res://addons/curved_lines_2d/ Scalable Vector Shapes 2D addon (v2.27.7) — required dependency.
res://scenes/legacy/scenes/legacy/stick.tscn Legacy rigged/animated stick figure scene (Skeleton2D + IK). Not used by the editor.
res://AGENTS.md Guidance for AI agents working in this codebase.

Menu bar structure

File          Edit                  View
────────────  ────────────────────  ───────────
Save          Configure Grid...     Reset Views
Load          ─────────             ─────────────
────────      Snap to Grid  (check) Hide/Show Pose Guide
Clear
  • File — Save, Load, Clear (Phase 1).
  • Edit — Configure Grid... (dialog with a 1–100 SpinBox) and the checkable Snap to Grid toggle (Phase 3).
  • View — Reset Views (resets zoom to 100% and pan offset to origin on every panel) (Phase 3) and the dynamic text-only Hide/Show Pose Guide toggle (Phase 7): reads "Hide Pose Guide" while the guide is visible and "Show Pose Guide" while it is hidden.

Context menu (per body-part panel)

The right-click context menu is context-sensitive. Item IDs:

ID Label Shown when
0 Line Empty space right-click
1 Rectangle Empty space right-click
2 Circle Empty space right-click
3 Create Point Shape under mouse, near outline
4 Color... Shape under mouse
5 Delete Shape under mouse
6 Remove Point Right-click on a vertex handle
7 Send Back Shape under mouse
8 Bring Forward Shape under mouse
9 Copy Shape under mouse (Phase 5)
10 Paste Shape or empty space right-click; shown only when the clipboard is populated (Phase 5)
11 Mirror X Shape under mouse (Phase 5)
12 Mirror Y Shape under mouse (Phase 5)

Context menu (Whole Stickman preview)

Right-clicking a selected part opens the preview's context menu:

ID Label Description
0 Send Back Move the selected part one step back in part_order (Phase 5)
1 Bring Forward Move the selected part one step forward in part_order (Phase 5)
2 Mirror X Mirror the selected part around its bounding-box center (scale X negated) (Phase 5)
3 Mirror Y Mirror the selected part around its bounding-box center (scale Y negated) (Phase 5)

Signal flow

BodyPartPanel.shape_changed(data) ---(bound to part_name)---> stickman_editor
        |                                                           |
        |  _on_body_part_shape_changed                                |
        +--------------------------> WholeStickmanPreview            |
                                       .set_body_parts(all_data)       |
                                             ^                         |
        WholeStickmanPreview.part_moved(name, pos)  (drag handling) --+

BodyPartPanel.shape_selected() ---(bound to part_name)---> stickman_editor
                                                              |
                                      _on_body_part_shape_selected
                                         (deselects all other panels)
  • BodyPartPanel emits shape_changed(shapes_array) whenever a shape is created, a vertex is dragged (on release), a color is committed via the ColorPicker (on OK), a shape is deleted, a vertex is removed via "Remove Point", or Z-order changes via "Send Back"/"Bring Forward". The signal carries the full Array[Dictionary] of all shapes in the panel. Note: clear_shape() does not emit the signal; the editor's Clear flow refreshes the preview directly.
  • BodyPartPanel emits shape_selected when left-clicked; the editor deselects all other panels.
  • stickman_editor.gd collects shape data from all 10 panels and forwards it to WholeStickmanPreview.set_body_parts().
  • WholeStickmanPreview emits part_moved(part_name, new_position) during translation drags. Rotation and scale are stored internally and polled by the editor at save time via get_part_rotation() / get_part_scale(). Part Z-order is polled the same way via get_part_order() and saved to the top-level part_order field (Phase 5).
  • Shape Copy/Paste (Phase 5) is owned by the editor: _copy_shape() stores a deep duplicate in _shape_clipboard, _broadcast_clipboard_state() tells every panel whether Paste should be offered, and _paste_shape() inserts the clipboard into the target panel at the click position. The clipboard persists after pasting and is shared across all panels.

Phase 4: Each body part can have multiple shapes in Z-order. The Whole Stickman preview treats all shapes in a panel as one combined object, with selection (white bounding box), rotation (circle gizmo below box), and scale (cross gizmos at corners). The .stk format evolved to v1.2 with a shapes array per part plus rotation and scale fields. Rotation snaps to 15° with Ctrl; scale locks aspect ratio with Ctrl.

Phase 5: Adds drawing-surface clipping, shape dragging, an editor-wide shape clipboard (Copy/Paste across panels), shape Mirror X/Y (vertex recompute), and preview object Z-ordering (Send Back/Bring Forward) with Mirror X/Y (scale negation). The format evolved to v1.3: a new top-level part_order array stores preview Z-order; scale may be negative for mirrored parts; shape mirroring stores no new fields because it rewrites points. The default grid size changed from 5 px to 15 px, and the Snap to Grid checkmark now renders correctly when toggled. v1.0–v1.2 files remain backward compatible and are migrated on load.

Phase 6 — Continuous Dynamic IK & Terrain-Following Locomotion (implemented and editor-verified): PLAY enables a kinematic terrain controller. Unsupported rigs free-fall under bounded gravity; walking animation pauses while airborne and resumes on landing. The centerline uses a three-ray terrain fan and each lower-leg endpoint uses an independent three-ray fan; only upward-facing TerrainBlock hits are accepted, with self collision proxies excluded. AnimationTargets/*_Base markers hold the animation pose while live leg IK targets receive continuous stance/swing blending, world-space stance locks, a predictive foot guard, and centerline pelvis/root compensation. Runtime root elevation accumulates over ramps and steps. EDIT/DIRECT disable terrain following and restore authored transforms. The Sandbox navigation bake uses exposed upward-surface ribbons with short step bridges; NavigationAgent2D remains at (0, +385) and terrain owns vertical motion while arrival remains baseline-aligned. Trigger areas use swept visible bounds across physics frames to detect fast-fall entries. Reactive takeover batches preserve order, so ragdoll followed by recover executes both in sequence. DEBUG_TERRAIN is off by default. No foot/ankle bones were added.

Phase 7: Adds the pose silhouette guide to the Whole Stickman preview — a semi-transparent, color-coded stick figure (left cyan-blue, right orange-red, central white) whose 13 joint anchors match the rest-pose pivots of the future stickman_rig.tscn rig, drawn above the grid and in front of user parts (ghosting over them, below the selection gizmos and drag highlight), centered in the preview at the default view. Toggleable via the dynamic View → Hide/Show Pose Guide label (on by default, persisted to settings.json). The guide is a view aid only: no .stk format change — FILE_VERSION stays "1.3".

Phase 8: The .stk export evolved to v1.4 with write-only metadata the editor never reads back. A top-level proportions object stores the 5 master-rig rest-pose bone lengths (upper_arm_length 168.0, lower_arm_length 200.0, upper_leg_length 200.0, lower_leg_length 200.0, torso_length 391.5) — hardcoded constants from stickman_rig.tscn, not measured from the user's shapes. Each body_parts entry gains pivot (local bounding-box center = rotation origin) and length (bbox extent along the segment axis: width for arms, height for torso/legs/head). v1.0–v1.3 files load unchanged and gain these keys on their next save. A new standalone res://scripts/characters/stickman/stk_rig_adapter.gd (class_name StkRigAdapter) fits an instantiated stickman_rig.tscn to a loaded .stk (bone fitting + IK recalibration + visual shape mount); it is not used by the editor and is reserved for a future runtime pipeline.

Phase 9: Adds the runtime pipeline for turning a .stk file into a live, rigged stickman_rig.tscn instance, plus a standalone staging scene to debug it. A new res://scripts/characters/stickman/stickman_factory.gd (class_name StickmanFactory) provides the runtime entry point: load_stk(path) reads/parses a .stk (FileAccess + JSON.parse_string, {} + push_warning on failure), spawn_from_data(stk_data) instantiates stickman_rig.tscn and applies StkRigAdapter.apply(stk_data, rig), and spawn(path) chains them (null on empty data). StkRigAdapter is extended to also fit the head bone (Head.position.y = -proportions.torso_length) and to mount the head as full geometry like every other part — clearing its inline @tool circle script and mounting .stk head shapes as Line2D/Polygon2D (removed the dead _mount_head_circle, _compute_shapes_bbox, and _first_shape_color helpers). A new standalone scene/resource pair, res://scenes/tools/test_harness.tscn + res://scripts/tools/test_harness.gd (run via F6), loads .stk files (open dialog + quick-select for content/stickmen/break.stk, content/stickmen/basic.stk, content/stickmen/test.stk), toggles a world-space debug overlay (true bone segments — parent→child lines with joint dots, limb leaf bones drawn to their IK targets and the Head leaf along its own direction — plus IK-target markers), pans/zooms a Camera2D, and lets you click-drag the 4 limb IK targets with live TwoBoneIK flexing (SkeletonModificationStack2D enabled on spawn). Neither the factory nor the harness is wired into the editor. No .stk format change — FILE_VERSION stays "1.4".

Phase 9 Round 2 bugfix: StkRigAdapter._mount_shapes() no longer trusts the file's pivot/length; it recomputes each part's bounding box at mount time and mounts geometry in the rig's hanging convention (joint anchor at the local origin, far end along local +Y) with a per-family alignment rotation (head 0, torso π, left limbs −π/2, right limbs +π/2, vertical-drawn limbs 0). Anchors: head/torso bottom-center, left horizontal limbs max_x, right horizontal limbs min_x, vertical limbs top-center. Scaling is anisotropic — only the auto-detected drawn long axis (width >= height) scales to the bone length (bone_length/extent, guard extent <= 0.0001 → 1.0), cross-axis thickness stays 1:1. Driver rotation neutralization was removed — the 10 RemoteTransform2D drivers keep update_rotation = true, so mounted shapes follow their bones under IK flexing. Body/* container transforms are reset (scale (1,1), rotation 0, position untouched), the Head driver's local position is zeroed so the chin lands on the neck joint, and DEFAULT_LINE_WIDTH is 2.0 (editor's 2 px outline). The harness _draw_bones() was rewritten to draw true parent→child bone segments with leaf bones out to their IK targets (wrist/ankle joints now visible). Per docs/specs/phase9_round2_bugfix_spec.md; verified with a 46-assertion headless smoke test.

Phase 9 Round 3 bugfix: StkRigAdapter._mount_shapes() now reads each part's rotation (degrees, default 0.0) and scale ({x,y}, default (1,1)) and composes the preview's part transform E(P) = C + R(rot)·S·(P − C) (scale-then-rotate about the bbox center) before the hanging-convention mount — the anchor, alignment θ, and bone-fit scale are computed on the transformed geometry, so every rotation/scale the user applied in the Whole Stickman preview renders in the harness. Rotations near ±180° (|wrapf(rot)| > 0.75π) swap the attachment to the drawn far end so flips are visible (the 180° torso shows its drawn neck end at the hip joint); the bone-fit scale s = bone_length/|V| is measured on the transformed extent so user-scaled parts are not double-fitted. The head mounts upright (θ = 0, s = 1) but still applies the part scale via E (face ≈160 px), chin at the neck joint. _mount_shape() mounts one node per shape — closed → single Polygon2D (fill only), open → single Line2D (width 2) — removing the previous Polygon2D + Line2D outline duplication. The harness head-leaf overlay now draws along the bone's own direction (the LookAt aim point is not a joint) instead of a line to IK_Targets/Head, eliminating the bone sticking out of the head. Per docs/specs/phase9_round3_bugfix_spec.md; verified with a 26-assertion headless smoke test.

Phase 9 Round 4 bugfix: the mounted head was too high in the harness — its chin (the local origin) landed on the neck joint at y = −391.5, floating above the torso. The editor's silhouette guide draws the head as a radius-100 circle centered at the Head joint (0, −463.5) (bottom at −363.5), so StkRigAdapter now applies a +28 px rig-space translation (const HEAD_CHIN_DROP := 28.0) to the mounted head points after the part transform and the (θ = 0, s = 1) transform. The chin lands at world ≈ (0, −363.5) — the guide circle's bottom — overlapping the torso (top at −391.5) by 28 px, matching the editor guide. The drop is flip-agnostic (a rig-space fixture applied after part scale/rotation). Per docs/specs/phase9_round4_bugfix_spec.md; verified with a 15-assertion headless smoke test.

Phase 9 Round 5: the .stk export evolved to v1.5 with per-part guide_offset — the part's bbox center minus its silhouette-guide joint (both in preview space, a pure master-space delta). StkRigAdapter applies it only when present as a node-frame translation t = (guide_offset + (A − C)).rotated(−c_node) (A = the mount anchor incl. the 180° flip rule, C = the raw bbox center, c_node = the part's driver RemoteTransform2D.global_rotation at apply time), so the harness reproduces the editor's guide-relative placement 1:1; old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4 HEAD_CHIN_DROP. The head maps to the guide Neck joint — the head bone's rig attachment origin, not the circle center (which sits 72 px above the neck). guide_offset is write-only metadata like pivot/length — the load path ignores it, so v1.0–v1.4 files load unchanged and gain the key on their next save. Note: .stk files must be re-saved from the editor to populate the offsets. Per docs/specs/phase9_round5_bugfix_spec.md.

Phase 9 Round 6 bugfix: StkRigAdapter._compute_mount_transform() now selects the joint anchor as whichever transformed end (E(J_raw) or E(F_pt_raw)) is nearest the part's stored guide joint (center − guide_offset) when a part carries guide_offset. This replaces the per-side family choice and the 180° flip heuristic for that case, fixing the lower left leg and lower right arm, which were mounted 180° off their bones (the far end attached at the joint) because the user's drawn-side conventions are inconsistent across parts — the stored guide placement is the ground truth for which drawn end is the joint. The nearest-end rule naturally preserves the 180° flip behavior (a flipped part's far end lands nearest the joint), the head chin, and every previously-correct case. Old files without the key keep the family rules + flip heuristic exactly as before. theta, s, the Round 5 offset t, and the head HEAD_CHIN_DROP fallback are unchanged. Per docs/specs/phase9_round6_bugfix_spec.md; verified with a 32-assertion headless smoke test.

Phase 9 Round 7: the test harness (scripts/tools/test_harness.gd) now exposes 6 draggable IK handles. IK_HANDLE_PATHS gains "Head" (IK_Targets/Head, the SkeletonModification2DLookAt aim point) and "Torso" (IK_Targets/Torso, whose child RemoteTransform2D moves the hip bone). Dragging the Torso handle moves bones only (no target following) — the marker's RemoteTransform2D translates the hip bone, and the whole skeleton + Body/* visuals follow rigidly, while the limb/head targets stay put so dragging the figure away from them stretches the limbs toward the stationary targets (per user decision). Dragging the Head handle drives the Head bone's LookAt rotation (the solver now uses a full-range, non-inverted band solved in global space — constraint_angle_min = -180 / max = 180, constraint_invert = false, constraint_in_localspace = false — so the head converges to the aim point with no per-frame mirror oscillation; the earlier authored ~55° clamp is gone); Body/Head follows. _handle_color() colors the head marker yellow (HANDLE_COLOR_HEAD) and the torso marker magenta (HANDLE_COLOR_TORSO); hands stay green, feet blue. The IK overlay additionally draws a null-guarded semi-transparent yellow aim line from the Head bone origin to the head marker (visual aid for the LookAt test). No .stk format change. Per docs/specs/phase9_round7_feature_spec.md; verified with a 17-assertion headless test.

Phase 10 (Kinematic-to-Ragdoll): adds a reversible kinematic-to-ragdoll state switch to the runtime rig. StickmanRig gains enum RigState { ANIMATED, RAGDOLL }, var state: RigState, signal state_changed(new_state), and the set_ragdoll(enabled) / toggle_ragdoll() / is_in_ragdoll() API. In RAGDOLL mode the IK modification stack is disabled, the AnimationPlayer stopped, and the Body/* visuals hidden; a procedural network of 10 RigidBody2D (torso CapsuleShape2D mass 8.0, head CircleShape2D radius 100, limb capsules radius 8) + 9 PinJoint2D (elbow/knee fold-only ±bands, shoulder/hip ±160°, neck free) is built in code and reparented into a "RagdollBodyContainer" under the rig's parent (world root), layer 1/mask 1 so it collides with terrain and props. The rig root's momentum (tracked in _physics_process) is applied to the ragdoll Torso body for a seamless handoff. Exiting frees the ragdoll, re-shows Body/*, re-enables IK, and stops the animation. The physics harness toggles via its Stickman ↔ Ragdoll button, removing the RigCollisionProxy on entry and re-adding it (idempotently) on exit. stickman_rig.tscn is not modified.

Phase 11 (Instant Handoff & Recovery): replaces the hard ragdoll entry/exit with an instant handoff and adds a stand-up recovery path. StickmanRig gains enum RigState { ANIMATED, RAGDOLL, RECOVERING } plus exports rest_timeout (2.0 s) and auto_recover (true). On entering RAGDOLL the ragdoll is built from the current solved bone positions (the player is stopped with keep_state), then Body/* is hidden and the IK stack disabled in the same call — no crossfade, since the ragdoll spawns at exactly the same pose and a fade would only read as ghosting (an earlier transition_duration blend was removed on director feedback). Rest detection reads the Torso body — sleeping, or linear ≤ REST_LINEAR_THRESHOLD (5.0 px/s, tuned up from the plan's 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s) and angular ≤ 0.1 rad/s — then after rest_timeout + STABILIZATION_DELAY (0.1 s) with auto_recover on, calls _start_recovery(). Recovery first calls _capture_pre_ragdoll_state() (6 IK markers, 8 limb + head bone rotations, facing profile, 4 bend flags, head LookAt state, per-leg leg_sides, and entry chain offsets torso_chain_offset/head_chain_offset) as the END target, then _capture_ragdoll_pose() records the 10 bodies' rig-local pose and probes the terrain via _probe_ground_y() (a 7-ray fan with a tolerance band, excluding the ragdoll RIDs), re-anchors the rig root so the standing figure's feet sit on the ground at the landing X (_reanchor_root_to_landing(), new_root = feet + FOOT_OFFSET, with a mirror-aware local shift local_shift = to_local(shift) − to_local(Vector2.ZERO)), destroys the ragdoll, sets state = RECOVERING + emits, snap-solves via _snap_skeleton_to_pose() — the 6 IK targets plus direct arm/head bone rotations (rot_u = chain(u) − θ_t − bone_angle_u, rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l, head via head_chain_offset), all normalized into (−π, π], with the ankles clamped onto the terrain and the two leg IK mods enabled with the captured knee side — and finally _play_stand_up() runs a 2.0 s sine-in-out tween of the markers → their pre-ragdoll values and the arm/head bones to the snap-computed _recovery_bones targets (elbows via _natural_joint_target), legs IK-driven. _on_stand_up_finished() restores the facing profile + bend flags + LookAt state, re-enables the arm mods, and returns to ANIMATED. The per-joint bend decisions live in three editable tables — RAGDOLL_JOINT_BAND_LIMITS, RECOVERY_LEG_SIDE_FORCE, RECOVERY_JOINT_PATH (Joint bend configuration (EDIT ME)); diagnostics are gated behind DEBUG_RECOVERY := false. Torso bone's bone_angle (copying the body rotation directly would slam the skeleton −90° and lay it flat). request_recovery() is public (no-op unless in RAGDOLL); set_ragdoll(true) during RECOVERING kills the stand-up tween and rebuilds the ragdoll, set_ragdoll(false) during RAGDOLL routes through recovery, and calls are otherwise idempotent. is_in_ragdoll() stays state == RAGDOLL (so RECOVERING reads as "Stickman"). A new res://scripts/tools/create_animations.gd editor script (superseding the deleted create_walk.gd) bakes walk_left/walk_right/the one-shot stand_up into stickman_rig.tscn's AnimationLibrary (the baked stand_up is an authored reference only — recovery does not play it) — no .stk format change; stickman_rig.tscn scene nodes are unchanged (only its baked animations are added). The physics harness gains a Rest SpinBox (0.1–10 s, writes _rig.rest_timeout), a "Recover Now" button (request_recovery()), and a state_changed hook that removes the RigCollisionProxy on RAGDOLL and re-adds it (idempotently) on ANIMATED/RECOVERING.

Phase 9 Prop Editor (Rounds 1–7): a standalone, kid-friendly vector-prop authoring tool — the authoring counterpart to the stickman editor — run via F6 on res://scenes/editors/prop/prop_editor.tscn (not the main scene). It adds a new .prop v1.0 JSON format (res://content/props/*.prop, multi-shape stack + deterministic PropFile.flatten_to_payload() conversion), a dedicated PropCanvas multi-shape canvas, a validated PropTheme (res://prop_theme.json) with a Round 2–4 ui token section, and a save-side 200×200 thumbnail pipeline reusing PropThumbnail + a mtime-keyed ThumbnailCache.prop_file_png. Round highlights: R2 — two explicit canvas modes ("Arrange"/"Reshape"), the stickman-style bottom rotation dot + corner scale crosses, color-matched 3 px vertex handles, and cursor-projection "Create Point" with a 0.6 s flash; R3 — POINTS-mode right-click is point-editing only (segment proximity via hit_test_segment), centered mode toolbar with emoji, window title Untitled — file.prop *, exit dialog (💾/🗑️/❌), and a debounced autosave/recovery prompt; R4 — selected-mode is clearly outlined (FOCUS_NONE + accent-border pressed stylebox), bigger theme-driven defaults, the Grid spinbox + Snap checkbox moved to the menu row (Edit menu removed), File → New with a save prompt, and Ball removed from the add menus; R5 — File → 🕑 Recent (last 5, persisted) and a critical serialization fix (typed PackedVector2Array assignments from {x,y} dicts silently zeroed every coordinate — _serialize_shape now routes through _coerce_points); R6 — Recent updates on save, a per-shape reshaped flag gates the circle Radius row, and fill-adaptive selection contrast; R7 — two-tone halo selection (dark backing + bright core) so outlines/boxes/dots/crosses/handles read on any background. Several Prop Editor rows are logged in docs/architecture/tech_debt_and_optimizations.md (#29–#35).

S
Description
Stickman for Godot
Readme
29 MiB
Languages
GDScript 99.9%
Python 0.1%