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:
@@ -129,6 +129,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
nodes — one node per shape: closed → single `Polygon2D`, open → single `Line2D` width 2). The
|
||||
`RemoteTransform2D` drivers keep their defaults (`update_rotation = true`), so mounted shapes
|
||||
follow their bones in every pose.
|
||||
- **Phase 2 (Sandbox) single-shape support:** `_mount_shapes()` reads a part's `shapes` array
|
||||
when present (v1.2+), and otherwise — when the part dict itself carries `points` — wraps the
|
||||
whole dict as a single shape (`shapes = [pd]`), so v1.0/v1.1 single-shape `.stk` files (e.g.
|
||||
`stickmen/test.stk`) mount as visible geometry instead of being cleared to nothing.
|
||||
- **Phase 9 extension:** also fits the head bone (`Skeleton2D/Torso/Head.position.y =
|
||||
-proportions.torso_length`, x preserved) and mounts the head as **full geometry** like every
|
||||
other part — it clears the `Body/Head` node's inline `@tool` circle script via
|
||||
@@ -245,7 +249,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
physics mode switch plus a `RECOVERING` stand-up state. `enum RigState { ANIMATED, RAGDOLL,
|
||||
RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state:
|
||||
int)`, and public API `set_ragdoll(enabled: bool)` / `toggle_ragdoll()` / `is_in_ragdoll() ->
|
||||
bool` / `request_recovery()`. `_physics_process()` → `_track_momentum(delta)` caches the rig
|
||||
bool` / `request_recovery()` / `snap_to_standing()` (Phase 2 Sandbox: instantly destroys the
|
||||
ragdoll or cancels the recovery tween, sets the 6 IK targets directly to `STAND_POSE`, re-shows
|
||||
`Body/*`, re-enables IK, and returns to `ANIMATED` with no stand-up glide). `_physics_process()`
|
||||
→ `_track_momentum(delta)` caches the rig
|
||||
root's linear/angular velocity from per-frame `global_position`/`global_rotation` deltas, then
|
||||
drives `_update_rest_detection()` (auto-recovery trigger). `_enter_ragdoll()` `stop(true)`s the
|
||||
`AnimationPlayer` (`ANIMATION_PLAYER_PATH` const, keep_state — no pose reset), builds the
|
||||
@@ -512,7 +519,76 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
which removes the collision proxy on `RAGDOLL`, re-adds it on `ANIMATED`/`RECOVERING`, and
|
||||
always `_update_ragdoll_toggle()` (proxy helpers are idempotent, so the toggle handler's own
|
||||
add/remove is harmless). The toggle label reads "Stickman" during `RECOVERING` (since
|
||||
`is_in_ragdoll()` is false).
|
||||
` is_in_ragdoll()` is false).
|
||||
- `scripts/sandbox_stage.gd` — `class_name SandboxStage`, `extends Node2D`; the **Sandbox Stage
|
||||
Builder** root controller (Phase 2, **not wired into the editor**; run via **F6** on
|
||||
`res://scenes/sandbox_stage.tscn`). Owns the EDIT/PLAY mode state machine, placement mode,
|
||||
camera pan/zoom, deletion, status bar, and signal fan-out; instantiates `StageSpawner` /
|
||||
`StageSelection` / `StageGizmos` (via `preload` consts). `enum StageMode { EDIT, PLAY }`
|
||||
(default EDIT). EDIT freezes `RigidBody2D` props with `freeze = true` +
|
||||
`freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC` (script-driven gizmo dragging needs
|
||||
KINEMATIC, not STATIC) and keeps `StickmanRig`s ANIMATED (`set_ragdoll(false)` runs first);
|
||||
PLAY unfreezes props and ragdolls stickmen with `auto_recover = false`. Signals
|
||||
`mode_changed(mode)`, `object_placed(node)`, `object_selected(nodes)`, `object_deselected()`,
|
||||
`object_deleted(nodes)`. Camera: middle-mouse pan, wheel zoom clamped to `@export min_zoom` /
|
||||
`max_zoom` (0.1 / 6.0); mouse→world via `_camera.get_global_mouse_position()` (no SubViewport).
|
||||
`_world_children_selectable()` returns direct `Node2D` children of `World` excluding
|
||||
`RagdollBodyContainer`. **Authored-state / restart-the-sim:** `_authored` (instance id →
|
||||
`{node, position, rotation}`) is updated at **edit time** on every place (`_place_at` →
|
||||
`_save_object_state`) and move/rotate (`transform_committed` → `_on_transform_committed`),
|
||||
and cleared on delete (`_clear_object_state`). `_enter_edit_mode()` stands stickmen, then
|
||||
freezes every prop (`freeze_mode = FREEZE_MODE_KINEMATIC` **before** `freeze = true`, so the
|
||||
body freezes directly as kinematic — never via the static layer, whose transform sync drops a
|
||||
subsequent position set), then `_restore_authored_state()` teleports via
|
||||
`global_position`/`global_rotation` + zeroed velocity. Because the freeze+teleport can take a
|
||||
physics frame or two to settle, `_restore_authored_state()` is also re-asserted over the next
|
||||
few `_physics_process` frames (`_restore_frames_left`) — so every Play session starts from and
|
||||
returns to the same authored state. **Placement ghost:** while a palette
|
||||
item is active, `set_placement_mode(id)` spawns a translucent (`modulate.a = 0.5`),
|
||||
non-colliding (`collision_layer/mask = 0`, frozen) copy of the object reparented out of
|
||||
`World` into `_ghost_holder`; `_process()` tracks it to the cursor (+ snap) via
|
||||
`_update_ghost_position()`, and `_place_at()` re-spawns it after each placement. A ghost
|
||||
**stickman** has its `Skeleton2D` modification stack disabled and its `AnimationPlayer`
|
||||
stopped so it renders as a static standing figure (its limbs don't flex/follow the cursor).
|
||||
**Grid/snap:** an optional `StageGrid` overlay (drawn behind `World`) plus `Grid`/`Snap`
|
||||
`CheckBox` toggles and a `Size` `SpinBox`, persisted to `user://sandbox_settings.json`
|
||||
(`grid_size`/`snap_to_grid`/`show_grid`); snapping rounds the placement cursor and translate
|
||||
drags via `_snap_to_grid()` / `StageGizmos.snap_size`. UI built in code (CanvasLayer +
|
||||
PanelContainer top bar): mode toggle, six palette buttons (built from the spawner registry),
|
||||
Grid/Snap/Size controls, status label.
|
||||
- `scripts/stage_spawner.gd` — `class_name StageSpawner`, `extends RefCounted`; registry-driven
|
||||
spawner (Phase 2). A `_registry: Array[Dictionary]` maps ids to terrain/prop/stickman templates;
|
||||
adding a type = appending an entry (no hard-coded id `match`). Reuses `TerrainUtils.spawn_block`,
|
||||
`PropUtils.spawn_prop`, `StickmanFactory.spawn_from_data` via `preload` consts. Terrain entries
|
||||
store origin-relative point templates (ground/ramp/step); `_spawn_terrain` centers the template
|
||||
bbox on its local origin then sets `block.position` to the cursor, so `global_rotation` =
|
||||
"rotate about center". The Stickman entry caches `res://stickmen/test.stk` once (`load_stk` in
|
||||
`_init`) and applies `STICKMAN_FOOT_OFFSET (0, -385)` so feet land at the cursor. Also exposes
|
||||
a `static get_world_aabb(node)` helper (for a stickman it unions the mounted `Body/*` shape
|
||||
geometry via a recursive `_collect_visual_points()` so the box is centered head-to-feet).
|
||||
- `scripts/stage_selection.gd` — `class_name StageSelection`, `extends RefCounted`; hover/click/
|
||||
box selection via geometric world-space AABB hit-testing (Phase 2). `static get_world_aabb`
|
||||
unions a `Polygon2D` child's world points (terrain/props) or, for a stickman rig, recursively
|
||||
unions its mounted `Body/*` `Line2D`/`Polygon2D` geometry (`_collect_visual_points()`), so the
|
||||
bounding box is centered on the actual figure head-to-feet (not a fixed rect).
|
||||
`_frontmost_at` = highest `World` child index wins,
|
||||
smallest area breaks ties. `_is_selectable` excludes the `RagdollBodyContainer` subtree. Signals
|
||||
`hover_changed(node)` / `selection_changed(nodes)`; public API `get_selected`/`get_primary`/
|
||||
`clear_selection`/`select_only`/`add_to_selection`/`toggle_selection`/`is_selected`/`hit_test`/
|
||||
`update_hover`/`box_select`.
|
||||
- `scripts/stage_gizmos.gd` — `class_name StageGizmos`, `extends Node2D`; hover highlight +
|
||||
selection outline + a rotate ring handle (Phase 2). **No translate handle** — objects are
|
||||
dragged directly by the root (`begin_translate_drag(node, world_pos)` sets the target + starts
|
||||
a TRANSLATE drag; `drag_to` drives `global_position` with optional grid snap via `snap_size`).
|
||||
Pure `_draw()` + distance-based hit-testing (no `Area2D`); `enum Handle { NONE, TRANSLATE,
|
||||
ROTATE }`. Rotation drives `global_rotation` about the AABB center. `transform_committed(node)`
|
||||
emitted on drag end. `set_enabled(false)` hides + disables in PLAY. Also draws the box-select
|
||||
marquee via `set_box_rect(rect)`.
|
||||
- `scripts/stage_grid.gd` — `class_name StageGrid`, `extends Node2D`; the optional world-space
|
||||
grid overlay (Phase 2). Pure `_draw()`: grid lines pan/zoom with the camera (`camera.get_screen_center_position()`
|
||||
+ viewport extent / zoom), with a heavier **major line every 5 cells**; `grid_size` / `enabled`
|
||||
are set by `SandboxStage`. Added as the first child of the stage root so it renders behind
|
||||
`World`; no hit-testing.
|
||||
- Scenes:
|
||||
- `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used
|
||||
for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`,
|
||||
@@ -539,6 +615,12 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
angled ramp; a toggle button flips the rig **Stickman** ↔ **Ragdoll** (removing/restoring
|
||||
the best-effort `StaticBody2D` rig collision proxy); a **Knock Up** button impulses the
|
||||
ragdoll and all props upward.
|
||||
- `scenes/sandbox_stage.tscn` — **standalone staging scene** (Sandbox Stage Builder, Phase 2,
|
||||
not wired into the editor; run via **F6**). Backed by `scripts/sandbox_stage.gd`. Minimal
|
||||
root `Node2D` + script, `Camera2D` at position `(0, -400)` zoom `(0.5, 0.5)`, and an empty
|
||||
`World` (Node2D) container; the `GridLayer` (`StageGrid`), `GizmoLayer` (`StageGizmos`),
|
||||
`PlacementGhost` holder, and the CanvasLayer top-bar UI (mode toggle + six palette buttons +
|
||||
Grid/Snap/Size controls + status label) are built in code at `_ready()`.
|
||||
|
||||
### Body-part data model
|
||||
- 10 internal part keys (ordered): `head`, `torso`, `left_upper_arm`, `left_lower_arm`,
|
||||
|
||||
Reference in New Issue
Block a user