Add sandbox stage builder scripts and functionality

- Introduced StageGizmos for hover highlighting, selection outlines, and rotation handles in the sandbox stage builder.
- Added StageGrid for an optional world-space grid overlay that adjusts with camera panning and zooming.
- Implemented StageSelection for geometric hit-testing and selection management of nodes in the sandbox.
- Created StageSpawner as a registry-driven factory for spawning terrain, props, and stickmen, allowing for dynamic template management.
- Each script includes necessary constants, state management, and public API methods for interaction.
This commit is contained in:
2026-08-28 21:53:55 -04:00
parent 0e971d99b1
commit ef30931b20
17 changed files with 1800 additions and 6 deletions
+60 -1
View File
@@ -391,6 +391,59 @@ Ragdoll bodies spawn fully visible — the entry handoff is instant (the ragdoll
**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 — `master_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).
### 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 flip between **Edit Mode** (build) and **Play Mode** (physics simulation). It is **not wired into the editor** — run via **F6** on `res://scenes/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 `Node2D`s geometrically, gizmos drive `global_position`/`global_rotation`, and core events are exposed as signals for future phases (Action Queue, Triggers, Save/Load).
| File | Purpose |
|---|---|
| `res://scenes/sandbox_stage.tscn` | The stage scene: root `Node2D` + `Camera2D` + empty `World` container. |
| `res://scripts/sandbox_stage.gd` | `class_name SandboxStage`, `extends Node2D` — root controller (mode state machine, placement, camera, deletion, status bar, signals). |
| `res://scripts/stage_spawner.gd` | `class_name StageSpawner`, `extends RefCounted` — registry-driven factory reusing `TerrainUtils` / `PropUtils` / `StickmanFactory`. |
| `res://scripts/stage_selection.gd` | `class_name StageSelection`, `extends RefCounted` — hover/click/box selection via geometric AABB hit-testing. |
| `res://scripts/stage_gizmos.gd` | `class_name StageGizmos`, `extends Node2D` — hover highlight, selection outline, rotate ring handle. |
| `res://scripts/stage_grid.gd` | `class_name StageGrid`, `extends Node2D` — optional world-space grid overlay (major line every 5 cells). |
**Mode management:**
- **Edit** (default) — `RigidBody2D` props are frozen (`freeze = true` + `freeze_mode = FREEZE_MODE_KINEMATIC`), stickmen stand as `ANIMATED` puppets, gizmos are visible, and selection is active.
- **Play** — props unfreeze and fall, stickmen ragdoll (`set_ragdoll(true)` with `auto_recover = false`), gizmos are hidden, and selection is cleared.
- The mode toggle button (leftmost) flips between the two; a `mode_changed(mode: int)` signal is emitted on every toggle, and the camera view persists across the switch.
- **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** (six text buttons, built from the spawner registry):
- **Ground / Ramp / Step** — `TerrainBlock` terrain, placed by centering the template on its local origin so rotation pivots on the block's center.
- **Crate** (`create_box()` + `WOOD`) / **Ball** (`create_ball()` + `RUBBER`) — `PropBlock` dynamic props.
- **Stickman** — a `StickmanRig` spawned from a cached `res://stickmen/test.stk` via `StickmanFactory.spawn_from_data()`, offset `(0, -385)` so the feet land on the cursor.
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. The next left-click spawns the object there. Placement repeats until you press **Escape** or click a different button. Each placement emits `object_placed(node)`.
**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.
- The **primary** selection shows a white bounding box and a blue **rotate** ring (around the box); drag the ring to rotate about the center. 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 (1100 px).
- Grid size, snap, and grid visibility persist to `user://sandbox_settings.json`.
**Deletion & camera:**
- **Delete** / **Backspace** removes all selected objects (`queue_free()`) and emits `object_deleted(nodes)`.
- **Middle-mouse drag** pans; **mouse wheel** zooms within the exported `min_zoom` (0.1) / `max_zoom` (6.0) bounds. The view persists across mode toggles.
**Status bar:** shows `Mode: EDIT/PLAY | Objects: N | Selected: <name or count>` and updates live on spawn, selection, deletion, and mode changes.
> **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, but stickmen do **not** autonomously walk up/down them yet (planned for a later phase with `NavigationAgent2D` + IK).
## 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.
@@ -527,7 +580,7 @@ Behavior:
| `res://scenes/stickman_editor.tscn` | **Main scene** — editor layout, File/Edit/View menu bar, dialogs (`GridConfigDialog` + SpinBox), column containers (unique-name nodes). |
| `res://scenes/body_part_panel.tscn` | Reusable single body-part editor panel (title, drawing area, context menu, `ColorPickerPopup`); expands vertically in its column. |
| `res://scripts/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/stk_rig_adapter.gd` | **Phase 8, extended by Phase 9 (Rounds 46 bugfix).** Standalone runtime adapter (`class_name StkRigAdapter`, `static func apply(stk_data, rig)`): fits an instantiated `master_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|Right_Hand`, `Left|Right_Leg`), and mounting the `.stk` shapes onto the `Body/*` visual nodes (**one node per shape**: closed → single `Polygon2D` fill, open → single `Line2D` width 2). Shape mounting recomputes each part's bounding box at mount time (file `pivot`/`length` are no longer trusted) and derives a mount transform in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) via `_compute_mount_transform()`: the part's preview transform `E(P) = C + R(rot)·S·(P C)` (rotation + scale about the bbox center — the editor's exact Whole-Stickman-preview transform) is composed **first**, then the anchor/alignment θ/bone-fit scale are computed on the **transformed geometry**; rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint). Anchors (raw family rules): head/torso bottom-center `(cx, max_y)`, left horizontal limbs `(max_x, cy)`, right horizontal limbs `(min_x, cy)`, vertically drawn limbs top-center `(cx, min_y)`; alignment rotation θ maps the far end onto `+Y`; 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. The `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. (Phase 9 Round 5) when a part dict carries `guide_offset`, the mounted geometry is translated by `t = (guide_offset + (A C)).rotated(c_node)`; (Phase 9 Round 6) when `guide_offset` is present, the joint anchor is whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is nearest the part's guide joint (`center guide_offset`), replacing the per-side family choice + 180° flip heuristic for that case (fixing the lower-left-leg and lower-right-arm, which were mounted 180° off their bones) — old files without the key keep the family rules + flip heuristic as the fallback in the driver's bone frame (A = mount anchor incl. the 180° flip rule, C = raw bbox center, `c_node` = driver `RemoteTransform2D.global_rotation`), so the harness reproduces the editor's guide-relative placement 1:1; old files without the key keep the offset-0 behavior (head falls back to `HEAD_CHIN_DROP`). Each `Body/*` container's scale is reset to `(1,1)` / rotation `0` (position untouched). (Phase 9) also fits the head bone (`Head.position.y = -proportions.torso_length`) while mounting the head as **full geometry** — it clears the head's inline `@tool` circle script and mounts `.stk` head shapes as `Line2D`/`Polygon2D`, and zeroes the Head driver's local position so the chin sits on the neck joint; the head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px). **Not used by the editor** — consumed by the runtime pipeline. |
| `res://scripts/stk_rig_adapter.gd` | **Phase 8, extended by Phase 9 (Rounds 46 bugfix).** Standalone runtime adapter (`class_name StkRigAdapter`, `static func apply(stk_data, rig)`): fits an instantiated `master_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|Right_Hand`, `Left|Right_Leg`), and mounting the `.stk` shapes onto the `Body/*` visual nodes (**one node per shape**: closed → single `Polygon2D` fill, open → single `Line2D` width 2). Shape mounting recomputes each part's bounding box at mount time (file `pivot`/`length` are no longer trusted) and derives a mount transform in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) via `_compute_mount_transform()`: the part's preview transform `E(P) = C + R(rot)·S·(P C)` (rotation + scale about the bbox center — the editor's exact Whole-Stickman-preview transform) is composed **first**, then the anchor/alignment θ/bone-fit scale are computed on the **transformed geometry**; rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint). Anchors (raw family rules): head/torso bottom-center `(cx, max_y)`, left horizontal limbs `(max_x, cy)`, right horizontal limbs `(min_x, cy)`, vertically drawn limbs top-center `(cx, min_y)`; alignment rotation θ maps the far end onto `+Y`; 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. The `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. (Phase 9 Round 5) when a part dict carries `guide_offset`, the mounted geometry is translated by `t = (guide_offset + (A C)).rotated(c_node)`; (Phase 9 Round 6) when `guide_offset` is present, the joint anchor is whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is nearest the part's guide joint (`center guide_offset`), replacing the per-side family choice + 180° flip heuristic for that case (fixing the lower-left-leg and lower-right-arm, which were mounted 180° off their bones) — old files without the key keep the family rules + flip heuristic as the fallback in the driver's bone frame (A = mount anchor incl. the 180° flip rule, C = raw bbox center, `c_node` = driver `RemoteTransform2D.global_rotation`), so the harness reproduces the editor's guide-relative placement 1:1; old files without the key keep the offset-0 behavior (head falls back to `HEAD_CHIN_DROP`). Each `Body/*` container's scale is reset to `(1,1)` / rotation `0` (position untouched). (Phase 9) also fits the head bone (`Head.position.y = -proportions.torso_length`) while mounting the head as **full geometry** — it clears the head's inline `@tool` circle script and mounts `.stk` head shapes as `Line2D`/`Polygon2D`, and zeroes the Head driver's local position so the chin sits on the neck joint; the head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px). `_mount_shapes()` also handles **v1.0/v1.1 single-shape** part dicts (wraps the part dict as one shape when it carries `points` but no `shapes` array), so older `.stk` files mount as visible geometry instead of being cleared. **Not used by the editor** — consumed by the runtime pipeline. |
| `res://scripts/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 `master_rig.tscn` instance. `load_stk(path)` reads + parses the file (`{}` + `push_warning` on failure); `spawn_from_data(stk_data)` instantiates `res://master_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/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 `master_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 }`. Null-guarded (`push_warning` + skip). **Not used by the editor.** |
| `res://scripts/create_animations.gd` | **Phase 11.** `@tool extends EditorScript`; a **standalone editor utility** (run manually with `master_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` 0.8, `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). |
@@ -539,6 +592,12 @@ Behavior:
| `res://scenes/physics_test_harness.tscn` | **Vector Terrain System / Dynamic Vector Props.** Standalone staging scene backing `scripts/physics_test_harness.gd` (run via **F6**; not wired into the editor). |
| `res://scripts/prop_block.gd` | **Dynamic Vector Props.** `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) and live-updating exports. |
| `res://scripts/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`). |
| `res://scripts/sandbox_stage.gd` | **Sandbox Stage Builder.** `class_name SandboxStage`, `extends Node2D` — root controller: EDIT/PLAY mode state machine (freezes props with `FREEZE_MODE_KINEMATIC`, ragdolls stickmen in PLAY), placement mode, camera pan/zoom, deletion, status bar, and signal fan-out (`mode_changed` / `object_placed` / `object_selected` / `object_deselected` / `object_deleted`). Standalone staging scene run via **F6**; not wired into the editor. |
| `res://scripts/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 and caches `stickmen/test.stk` for the Stickman palette entry. |
| `res://scripts/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_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_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://scenes/sandbox_stage.tscn` | **Sandbox Stage Builder.** Standalone staging scene backing `scripts/sandbox_stage.gd` (run via **F6**; not wired into the editor): root `Node2D` + `Camera2D` + empty `World`; the gizmo layer and CanvasLayer top bar are built in code. |
| `res://scripts/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/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. |