Add initial draft for Phase 7 UI Polish plan
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
# AGENTS.md — stickman (Godot 4.4)
|
||||
# AGENTS.md — stickman (Godot 4.7.1)
|
||||
|
||||
## Project type
|
||||
- **Godot 4.4** 2D/GUI project (Forward Plus renderer)
|
||||
|
||||
- **Godot 4.7.1** 2D/GUI project (Forward Plus renderer)
|
||||
- Headless tests + command-line scene playback exist (see "Command-line testing & debug playback" below); the GUI editor runs via `run/main_scene`.
|
||||
- **Main scene:** `res://scenes/stickman_editor.tscn` (set as `run/main_scene` in `project.godot`)
|
||||
|
||||
## Command-line testing & debug playback
|
||||
|
||||
- **Godot binary (4.7.1) — resolve per platform.** Commands below use `<godot>` as shorthand:
|
||||
- macOS: `/Applications/Godot.app/Contents/MacOS/Godot`
|
||||
- Windows (PowerShell): `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe"`
|
||||
@@ -21,17 +23,28 @@
|
||||
- **Richer debug logs:** append `--verbose` to the scene-playback (or headless) command to surface engine warnings/errors alongside your own `print()` lines.
|
||||
|
||||
## Project overview
|
||||
Stickman Studio is an editor tool for drawing and assembling stick figures. It is a
|
||||
`Control`-based GUI (not physics/animation) in its current phase. Body-part vector shapes
|
||||
are authored per-panel (each panel supports **multiple shapes** with Z-ordering) and
|
||||
assembled in a "Whole Stickman" preview that supports translation, rotation, and scale.
|
||||
|
||||
Stickman Studio is a kid-friendly 2D scene-building and directing project—not only a
|
||||
stick-figure editor. Its broader product direction is an approachable moviemaker where
|
||||
children can create and customize stickmen, build sets with floors/terrain and props,
|
||||
arrange characters and objects, direct behavior with queued actions and event-triggered
|
||||
rules, and play the resulting scenes.
|
||||
|
||||
The codebase currently has two primary workflows. The main `Control`-based stickman editor
|
||||
authors body-part vector shapes and assembles them in the "Whole Stickman" preview. The
|
||||
Sandbox Stage provides the wider scene workflow through EDIT/DIRECT/PLAY modes, terrain,
|
||||
props, live stickman rigs, action queues, and events. The editor remains the configured
|
||||
`run/main_scene`; the Sandbox Stage and its runtime systems are separate scenes and tools
|
||||
that form the foundation of the broader moviemaking experience.
|
||||
|
||||
## Required addon
|
||||
|
||||
- **Scalable Vector Shapes 2D** (v2.27.7) at `addons/curved_lines_2d/`
|
||||
- Declared dependency for the project. The current editor UI does not instantiate it directly,
|
||||
but keep it present — it is required for the legacy `stick.tscn` rig.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `scripts/stickman_editor.gd` — `extends Control`; the main controller. Owns the menu bar,
|
||||
save/load/clear flow, JSON (de)serialization, and populates the 10 body-part panels.
|
||||
Writes `FILE_VERSION "1.5"`; auto-migrates `"1.0"`–`"1.4"` files on load. Coordinates cross-panel
|
||||
@@ -44,7 +57,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
otherwise. `pivot`/`length`/`proportions` are write-only metadata — never read back on load,
|
||||
recomputed on every save.
|
||||
- **Phase 9 Round 5 guide-offset export:** each part's save dict also gains `"guide_offset":
|
||||
{x, y}` = (part bbox center in preview space) − (guide joint in preview space), computed in
|
||||
{x, y}` = (part bbox center in preview space) − (guide joint in preview space), computed in
|
||||
`_collect_all_shape_data()` as `(pos + pivot) - _whole_preview.get_guide_joint_preview(...)` —
|
||||
a **pure master-space delta** (both points are preview-world coordinates, so panel-size terms
|
||||
cancel). The part→joint map is `const GUIDE_JOINT_FOR_PART` (head→"Neck" — the head bone's
|
||||
@@ -124,7 +137,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
gizmos and drag highlight. The guide is **centered** at the default view and after
|
||||
`Reset Views`, computed at draw time from the live preview size via
|
||||
`_guide_to_preview()` = `(master_pos - GUIDE_FIGURE_CENTER) * GUIDE_SCALE +
|
||||
preview_area.size * 0.5`, so it also re-centers on window resize; it remains a
|
||||
preview_area.size * 0.5`, so it also re-centers on window resize; it remains a
|
||||
world-space fixture that moves with pan/zoom. Joint positions are **hardcoded
|
||||
constants** derived from the `master_rig.tscn` rest pose (`GUIDE_JOINTS`: 13 anchors
|
||||
Head/Neck/Shoulders/Elbows/Wrists/Hips/Knees/Ankles;
|
||||
@@ -149,7 +162,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
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
|
||||
-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
|
||||
`set_script(null)` and mounts `.stk` head shapes as `Line2D`/`Polygon2D`. Dead helpers
|
||||
`_mount_head_circle`, `_compute_shapes_bbox`, and `_first_shape_color` were removed.
|
||||
@@ -220,7 +233,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
(`j_prime = E(J_raw)` or `f_pt_prime = E(F_pt_raw)`) is **nearest the part's stored guide
|
||||
joint** (`center − guide_offset`): if `d_far < d_joint` (strict) the far end attaches
|
||||
(`anchor = f_pt_prime`, `v = −f_prime`), else the family end (`anchor = j_prime`, `v =
|
||||
f_prime`). This replaces the per-side family choice **and** the 180° flip heuristic for the
|
||||
f_prime`). This replaces the per-side family choice **and** the 180° flip heuristic for the
|
||||
`guide_offset` case, fixing the **lower left leg** (knee now at the joint, was the ankle)
|
||||
and **lower right arm** (elbow now at the joint, was the wrist), both 180° off their bones
|
||||
because the user's drawn-side conventions are inconsistent per part. The nearest-end rule
|
||||
@@ -230,8 +243,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
the key keep the previous family rules + flip heuristic exactly as before. `theta`, `s`, the
|
||||
Round 5 offset `t`, and the `HEAD_CHIN_DROP` fallback are unchanged — they consume
|
||||
`anchor`/`v` generically.
|
||||
Targets `master_rig.tscn` node paths; every node lookup is null-guarded (missing node →
|
||||
`push_warning` + skip, never crash). Consumed by a future runtime pipeline.
|
||||
Targets `master_rig.tscn` node paths; every node lookup is null-guarded (missing node →
|
||||
`push_warning` + skip, never crash). Consumed by a future runtime pipeline.
|
||||
- `scripts/stickman_rig.gd` — `class_name StickmanRig`, `extends Node2D`; the **runtime owner of
|
||||
facing direction, per-joint bone bend, and `Body/*` z-order** for `master_rig.tscn` (Phase 9
|
||||
Task 4). Attached to the `Master` root node of `master_rig.tscn`. **Non-`@tool`** — node
|
||||
@@ -291,9 +304,9 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
intentionally static.
|
||||
- **Phase 10 ragdoll state system:** `StickmanRig` owns a reversible `ANIMATED ⇄ RAGDOLL`
|
||||
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()` / `snap_to_standing()` (Phase 2 Sandbox: instantly destroys the
|
||||
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()` / `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
|
||||
@@ -304,89 +317,83 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
it first would revert the bones to the authored rest pose, popping the figure), then hides
|
||||
`Body/*` and disables the IK stack **immediately** — an instant handoff with no crossfade (the
|
||||
ragdoll spawns at exactly the same pose, so a fade would only read as ghosting), sets `state =
|
||||
RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D`
|
||||
RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D`
|
||||
container `"RagdollBodyContainer"` under the rig's **parent** (world root; fallback
|
||||
`get_tree().current_scene`) and populates it from the `RAGDOLL_BODIES` table (**10**
|
||||
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
|
||||
2.0, limb capsules radius 8 masses 1.0–2.0; `collision_layer`/`collision_mask` = 1, bodies
|
||||
spawn fully visible) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`, one per
|
||||
non-root body pinned at the child bone's origin, `softness = RAGDOLL_TARGET_SOFTNESS` at
|
||||
build). Each body clones the rig's own mounted visual geometry into the ragdoll body frame
|
||||
via `_clone_part_visuals()` (reads the entry's `visual_path` `Body/*` node, copies each
|
||||
`Line2D`/`Polygon2D` child with points baked into the body's local space), falling back to a
|
||||
gray capsule / white circle when the part node is missing or has no mounted shapes. Angular
|
||||
limits via `_apply_ragdoll_joint_limits()`: elbow/knee bands come from the editable
|
||||
`RAGDOLL_JOINT_BAND_LIMITS` table (per profile, per joint) — FORWARD arms `{-150°, +5°}`,
|
||||
legs `{-5°, +150°}`; LEFT/RIGHT override both sides (e.g. locked `{60°, 100°}`); the
|
||||
`elbow_knee`/`elbow_knee_ccw` `limit` strings are only the fallback for joints missing from
|
||||
the table. Shoulder/hip ±160°, neck free. Cached
|
||||
momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the
|
||||
same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the
|
||||
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
|
||||
**not** modified.
|
||||
`RigidBody2D`: torso `CapsuleShape2D` radius 12 mass 8.0, head `CircleShape2D` radius 100 mass
|
||||
2.0, limb capsules radius 8 masses 1.0–2.0; `collision_layer`/`collision_mask` = 1, bodies
|
||||
spawn fully visible) and the `RAGDOLL_JOINTS` table (**9** `PinJoint2D`, one per
|
||||
non-root body pinned at the child bone's origin, `softness = RAGDOLL_TARGET_SOFTNESS` at
|
||||
build). Each body clones the rig's own mounted visual geometry into the ragdoll body frame
|
||||
via `_clone_part_visuals()` (reads the entry's `visual_path` `Body/*` node, copies each
|
||||
`Line2D`/`Polygon2D` child with points baked into the body's local space), falling back to a
|
||||
gray capsule / white circle when the part node is missing or has no mounted shapes. Angular
|
||||
limits via `_apply_ragdoll_joint_limits()`: elbow/knee bands come from the editable
|
||||
`RAGDOLL_JOINT_BAND_LIMITS` table (per profile, per joint) — FORWARD arms `{-150°, +5°}`,
|
||||
legs `{-5°, +150°}`; LEFT/RIGHT override both sides (e.g. locked `{60°, 100°}`); the
|
||||
`elbow_knee`/`elbow_knee_ccw` `limit` strings are only the fallback for joints missing from
|
||||
the table. Shoulder/hip ±160°, neck free. Cached
|
||||
momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the
|
||||
same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the
|
||||
harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is
|
||||
**not** modified.
|
||||
- **Phase 11 instant handoff + recovery:** `_update_rest_detection()` (only when `state ==
|
||||
RAGDOLL`)
|
||||
RAGDOLL`)
|
||||
reads `_ragdoll_bodies["torso"]`: when its linear/angular velocity drops below
|
||||
`REST_LINEAR_THRESHOLD`/`REST_ANGULAR_THRESHOLD` (or it is sleeping) it accumulates
|
||||
`_rest_timer`; after `rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover`
|
||||
on it calls `_start_recovery()`.
|
||||
- **Pre-ragdoll capture:** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`,
|
||||
which snapshots, into `_pre_ragdoll_state`, 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).
|
||||
- **Recovery capture + landing:** `_start_recovery()` (also `request_recovery()`, no-op
|
||||
unless in RAGDOLL) calls `_capture_ragdoll_pose()` — records the 10 bodies' rig-local
|
||||
`{pos, rot, half}` (`half` = each capsule's half-length from build-time metadata) and
|
||||
probes the terrain for the ground surface under the fallen figure via `_probe_ground_y()`
|
||||
(a **7-ray fan** around the landing X, `GROUND_PROBE_START_OFFSET`/`END_OFFSET`,
|
||||
`GROUND_PROBE_TOLERANCE` band, ragdoll RIDs excluded via `query.exclude`); the probe
|
||||
context is retained in `_probe_top_y`/`_probe_bottom_y`/`_probe_exclude` for the ankle
|
||||
clamp at snap time. It then `_destroy_ragdoll()`s, calls `_reanchor_root_to_landing()`
|
||||
(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)`, so the re-base survives
|
||||
the LEFT X-mirror), sets `state = RECOVERING` + emits.
|
||||
- **Snap:** `_snap_skeleton_to_pose()` writes the captured pose: the torso marker via
|
||||
`to_local(center_world − spine_dir·half)` with rotation `chain(spine) − torso_chain_offset`;
|
||||
the limb bones `rot_u = chain(u) − θ_t − bone_angle_u` and
|
||||
`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. The snap also
|
||||
computes and stores the per-segment tween targets in `_recovery_bones`. 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's
|
||||
`RemoteTransform2D` update flags), then **disables** the 4 arm TwoBoneIK mods + the head
|
||||
LookAt and **enables** the 2 leg mods (bend side matched to the captured fall pose's knee
|
||||
side vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`).
|
||||
- **Stand-up:** `_play_stand_up()` starts a 2.0 s sine-in-out tween (`STAND_UP_DURATION`)
|
||||
of the 6 markers → their **pre-ragdoll** values and of the arm + head bones to the
|
||||
`_recovery_bones` targets — 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 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).
|
||||
- **Finish:** `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint
|
||||
bend flags + head LookAt state (`_restore_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).
|
||||
- `_pin_mirrored_head_rotation()` pins the head only while `ANIMATED`. Interruptible:
|
||||
`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. `is_in_ragdoll()` stays
|
||||
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman"). Runtime diagnosis is
|
||||
gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces the
|
||||
stack + bone-following state — the temporary per-step debug prints have been stripped).
|
||||
on it calls `_start_recovery()`. - **Pre-ragdoll capture:** `_enter_ragdoll()` first calls `_capture_pre_ragdoll_state()`,
|
||||
which snapshots, into `_pre_ragdoll_state`, 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). - **Recovery capture + landing:** `_start_recovery()` (also `request_recovery()`, no-op
|
||||
unless in RAGDOLL) calls `_capture_ragdoll_pose()` — records the 10 bodies' rig-local
|
||||
`{pos, rot, half}` (`half` = each capsule's half-length from build-time metadata) and
|
||||
probes the terrain for the ground surface under the fallen figure via `_probe_ground_y()`
|
||||
(a **7-ray fan** around the landing X, `GROUND_PROBE_START_OFFSET`/`END_OFFSET`,
|
||||
`GROUND_PROBE_TOLERANCE` band, ragdoll RIDs excluded via `query.exclude`); the probe
|
||||
context is retained in `_probe_top_y`/`_probe_bottom_y`/`_probe_exclude` for the ankle
|
||||
clamp at snap time. It then `_destroy_ragdoll()`s, calls `_reanchor_root_to_landing()`
|
||||
(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)`, so the re-base survives
|
||||
the LEFT X-mirror), sets `state = RECOVERING` + emits. - **Snap:** `_snap_skeleton_to_pose()` writes the captured pose: the torso marker via
|
||||
`to_local(center_world − spine_dir·half)` with rotation `chain(spine) − torso_chain_offset`;
|
||||
the limb bones `rot_u = chain(u) − θ_t − bone_angle_u` and
|
||||
`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. The snap also
|
||||
computes and stores the per-segment tween targets in `_recovery_bones`. 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's
|
||||
`RemoteTransform2D` update flags), then **disables** the 4 arm TwoBoneIK mods + the head
|
||||
LookAt and **enables** the 2 leg mods (bend side matched to the captured fall pose's knee
|
||||
side vs the entry side recorded in `leg_sides`, or forced via `RECOVERY_LEG_SIDE_FORCE`). - **Stand-up:** `_play_stand_up()` starts a 2.0 s sine-in-out tween (`STAND_UP_DURATION`)
|
||||
of the 6 markers → their **pre-ragdoll** values and of the arm + head bones to the
|
||||
`_recovery_bones` targets — 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 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). - **Finish:** `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint
|
||||
bend flags + head LookAt state (`_restore_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). - `_pin_mirrored_head_rotation()` pins the head only while `ANIMATED`. Interruptible:
|
||||
`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. `is_in_ragdoll()` stays
|
||||
`state == RigState.RAGDOLL` (so `RECOVERING` reads as "Stickman"). Runtime diagnosis is
|
||||
gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces the
|
||||
stack + bone-following state — the temporary per-step debug prints have been stripped).
|
||||
- **Phase 4.5 collision bodies and reactions:** while ANIMATED, `_build_collision_bodies()` creates
|
||||
ten metadata-tagged `AnimatableBody2D` proxies (head, torso, and eight limbs), stored in
|
||||
`_collision_bodies`. Proxies are top-level relative to their `Body/*` visuals,
|
||||
@@ -431,55 +438,55 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
assertions; the implementation is user-verified (2026-09-17).
|
||||
- **Phase 3a director functionality:** adds navigation/walking, speech, an action queue, and a
|
||||
queue-runner state machine. Enums `RunnerState { IDLE, EXECUTING }` and `ActionPhase { NONE,
|
||||
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`
|
||||
WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }`. Constants `FOOT_OFFSET := (0, -385)`
|
||||
(feet → root; matches `StageSpawner.STICKMAN_FOOT_OFFSET`), `NAV_AGENT_LOCAL_POS := (0, 385)`
|
||||
(`== -FOOT_OFFSET`), `ARRIVE_DISTANCE` / `NAV_PATH_DESIRED_DISTANCE` / `NAV_TARGET_DESIRED_DISTANCE`
|
||||
/ `SPEECH_BUBBLE_OFFSET`. New signals `arrived`, `action_started(action, index)`,
|
||||
`action_finished(action, index)`, `queue_finished` (on completion, not stop), `queue_changed`
|
||||
(any queue mutation), `speech_finished`. Exports `walk_speed` (300.0). Public API: `walk_to(
|
||||
target, speed = -1.0)` (feet/ground destination; no-op unless `state == ANIMATED`), `is_walking()`,
|
||||
target, speed = -1.0)` (feet/ground destination; no-op unless `state == ANIMATED`), `is_walking()`,
|
||||
`speak(text, duration)` (lazily creates a `SpeechBubble` child at `SPEECH_BUBBLE_OFFSET`, auto-hides
|
||||
+ emits `speech_finished`), the queue API `queue_action` / `clear_queue` / `get_queue` /
|
||||
`remove_action` / `insert_action` / `queue_size` (all mutations emit `queue_changed`), and the
|
||||
runner API `start_queue` / `stop_queue` / `is_queue_running`. `_ready()` builds a
|
||||
`NavigationAgent2D` child at `NAV_AGENT_LOCAL_POS` (feet, on the ground-level nav mesh;
|
||||
`avoidance_enabled = false`, `max_speed = walk_speed`, shared default nav map layer 1). A
|
||||
`_physics_process` ordering of `_track_momentum` → `_update_rest_detection` → `_update_walking`
|
||||
→ `_update_speech` → `_update_runner` drives the runner state machine per `ActionPhase`
|
||||
(`walk_to` → wait for `_walk_done`; `speak` → wait for `!_speech_active`; `wait` → `_phase_timer`
|
||||
countdown; `ragdoll` → `set_ragdoll(true)` then wait `is_ragdoll_at_rest()`; `recover` →
|
||||
`request_recovery()` then wait `state == ANIMATED`). `is_ragdoll_at_rest()` is a new public
|
||||
query exposing the rest result **regardless of `auto_recover`** (the runner polls it; auto-recovery
|
||||
logic is unchanged). `_enter_ragdoll()` additionally calls `_cancel_walking(false)` (no stale
|
||||
walk/path state while preserving the current solved pose for instant ragdoll handoff) and resets
|
||||
`_ragdoll_at_rest = false`. **Walk fix (2026-08-29, hybrid policy):**
|
||||
`_update_walking` defers all nav reads until `NavigationServer2D.map_get_iteration_id(...) != 0`
|
||||
(map-sync guard), forces the path query via `get_next_path_position()` **before** any empty-path /
|
||||
finished check (the read-only `get_current_navigation_path()` alone never triggers a
|
||||
repath), then branches on `is_target_reachable()`: an **on-mesh target** follows the nav path
|
||||
(`_walk_mode = "nav"`), while an **off-mesh/unreachable target** switches to **direct
|
||||
straight-line steering** toward the clicked waypoint (`_walk_mode = "direct"`, root target =
|
||||
waypoint + `FOOT_OFFSET`) — a supported case with **no** `push_warning` (the old "warn + finish
|
||||
in place" policy was replaced; the rig never stands still at a waypoint). There is **no**
|
||||
`_walk_path_grace` variable (the map-sync guard + forced path query replace it) and **no
|
||||
unreachable warning**; the debug trace now carries a `mode=nav|direct` field. Off-by-default
|
||||
diagnostics: `DEBUG_WALK` + `_walk_dbg()` (rig) and `DEBUG_STAGE` + `_stage_dbg()` (sandbox_stage).
|
||||
Walking is kinematic
|
||||
(`global_position.move_toward`); movement composes with the canonical `walk_right` in-place limb
|
||||
animation, played for every direction — X-mirrored by the rig root for LEFT (facing is set
|
||||
explicitly by `walk_to()`/`set_facing_profile()`; `walk_left` is no longer used at runtime and
|
||||
the animation `.:facing_profile` tracks are neutralized/removed). `_finish_walk()` captures the
|
||||
eight current limb-bone rotations, calls `AnimationPlayer.stop(true)`, and leaves all six IK
|
||||
marker properties at their current values; no marker reset occurs at queue completion. LEFT idle
|
||||
disables mirrored TwoBoneIK while preserving that stopped marker/bone frame; it does not restore
|
||||
the factory pose. `snap_to_standing()` is the explicit stage-reset path that restores the
|
||||
factory-captured spawn pose.
|
||||
- emits `speech_finished`), the queue API `queue_action` / `clear_queue` / `get_queue` /
|
||||
`remove_action` / `insert_action` / `queue_size` (all mutations emit `queue_changed`), and the
|
||||
runner API `start_queue` / `stop_queue` / `is_queue_running`. `_ready()` builds a
|
||||
`NavigationAgent2D` child at `NAV_AGENT_LOCAL_POS` (feet, on the ground-level nav mesh;
|
||||
`avoidance_enabled = false`, `max_speed = walk_speed`, shared default nav map layer 1). A
|
||||
`_physics_process` ordering of `_track_momentum` → `_update_rest_detection` → `_update_walking`
|
||||
→ `_update_speech` → `_update_runner` drives the runner state machine per `ActionPhase`
|
||||
(`walk_to` → wait for `_walk_done`; `speak` → wait for `!_speech_active`; `wait` → `_phase_timer`
|
||||
countdown; `ragdoll` → `set_ragdoll(true)` then wait `is_ragdoll_at_rest()`; `recover` →
|
||||
`request_recovery()` then wait `state == ANIMATED`). `is_ragdoll_at_rest()` is a new public
|
||||
query exposing the rest result **regardless of `auto_recover`** (the runner polls it; auto-recovery
|
||||
logic is unchanged). `_enter_ragdoll()` additionally calls `_cancel_walking(false)` (no stale
|
||||
walk/path state while preserving the current solved pose for instant ragdoll handoff) and resets
|
||||
`_ragdoll_at_rest = false`. **Walk fix (2026-08-29, hybrid policy):**
|
||||
`_update_walking` defers all nav reads until `NavigationServer2D.map_get_iteration_id(...) != 0`
|
||||
(map-sync guard), forces the path query via `get_next_path_position()` **before** any empty-path /
|
||||
finished check (the read-only `get_current_navigation_path()` alone never triggers a
|
||||
repath), then branches on `is_target_reachable()`: an **on-mesh target** follows the nav path
|
||||
(`_walk_mode = "nav"`), while an **off-mesh/unreachable target** switches to **direct
|
||||
straight-line steering** toward the clicked waypoint (`_walk_mode = "direct"`, root target =
|
||||
waypoint + `FOOT_OFFSET`) — a supported case with **no** `push_warning` (the old "warn + finish
|
||||
in place" policy was replaced; the rig never stands still at a waypoint). There is **no**
|
||||
`_walk_path_grace` variable (the map-sync guard + forced path query replace it) and **no
|
||||
unreachable warning**; the debug trace now carries a `mode=nav|direct` field. Off-by-default
|
||||
diagnostics: `DEBUG_WALK` + `_walk_dbg()` (rig) and `DEBUG_STAGE` + `_stage_dbg()` (sandbox_stage).
|
||||
Walking is kinematic
|
||||
(`global_position.move_toward`); movement composes with the canonical `walk_right` in-place limb
|
||||
animation, played for every direction — X-mirrored by the rig root for LEFT (facing is set
|
||||
explicitly by `walk_to()`/`set_facing_profile()`; `walk_left` is no longer used at runtime and
|
||||
the animation `.:facing_profile` tracks are neutralized/removed). `_finish_walk()` captures the
|
||||
eight current limb-bone rotations, calls `AnimationPlayer.stop(true)`, and leaves all six IK
|
||||
marker properties at their current values; no marker reset occurs at queue completion. LEFT idle
|
||||
disables mirrored TwoBoneIK while preserving that stopped marker/bone frame; it does not restore
|
||||
the factory pose. `snap_to_standing()` is the explicit stage-reset path that restores the
|
||||
factory-captured spawn pose.
|
||||
- **Phase 4 triggers:** the `arrived` signal gained a `target: Vector2` payload (emits
|
||||
`_walk_target_feet`) so the stage can match waypoints for `arrived_at_waypoint` rules; new
|
||||
`enqueue_reactive(actions: Array[Dictionary]) -> void` dispatches reactive actions into a
|
||||
private runtime lane without mutating `action_queue`: Overlay speech runs concurrently in one
|
||||
replaceable slot, Sequential actions cancel the current primary action while retaining authored
|
||||
future work, and Takeover actions cancel both primary and overlay work before taking control.
|
||||
`_walk_target_feet`) so the stage can match waypoints for `arrived_at_waypoint` rules; new
|
||||
`enqueue_reactive(actions: Array[Dictionary]) -> void` dispatches reactive actions into a
|
||||
private runtime lane without mutating `action_queue`: Overlay speech runs concurrently in one
|
||||
replaceable slot, Sequential actions cancel the current primary action while retaining authored
|
||||
future work, and Takeover actions cancel both primary and overlay work before taking control.
|
||||
- `scripts/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static
|
||||
factory** and the **runtime entry point** (Phase 9, **not used by the editor**) that turns a
|
||||
`.stk` file into a live, rigged `master_rig.tscn` instance:
|
||||
@@ -584,7 +591,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`offset_top = 40.0`, `offset_right = -8.0`, `offset_left = -COORDS_PANEL_WIDTH` (320.0),
|
||||
`grow_vertical = GROW_DIRECTION_END` + `grow_horizontal = GROW_DIRECTION_BEGIN`
|
||||
(auto-height/width, grows left so text never runs off-screen), `mouse_filter =
|
||||
MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for selection);
|
||||
MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for selection);
|
||||
StyleBoxFlat
|
||||
bg `Color(0,0,0,0.55)`, border `Color(1,1,1,0.12)` w1, corner radius 4, content margin 8.
|
||||
Consts `COORDS_PANEL_WIDTH := 320.0` and `COORD_BONE_PATHS` (10 Skeleton2D-relative bone
|
||||
@@ -613,7 +620,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
(`_apply_loop_mode()`); playback state is tracked by the enum
|
||||
`PlaybackState {STOPPED, PLAYING, PAUSED}` via the button handlers + the `animation_finished`
|
||||
signal (guarded by `_loop`) — no polling in `_process`. Changing the dropdown selection stops
|
||||
playback; `_free_current_rig()` clears `_anim_player`, the dropdown, `_selected_animation`,
|
||||
playback; `_free_current_rig()` clears `_anim_player`, the dropdown, `_selected_animation`,
|
||||
and state. The animation `.:facing_profile` tracks are **neutralized/removed** — facing is now
|
||||
set explicitly via `set_facing_profile()` / `walk_to()` (which sets LEFT/RIGHT from the walk
|
||||
direction and root-mirrors for LEFT), so playing `walk_right` no longer flips the rig's profile
|
||||
@@ -647,7 +654,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
(`BUILD_SOLIDS`) in `POLYGON` mode, or `CollisionShape2D` + `CircleShape2D` (48-segment
|
||||
radial loop, `CIRCLE_SEGMENTS = 48`) in `CIRCLE` mode, toggled via `shape_type`. Exported
|
||||
properties: `shape_type` (`@export_enum("Polygon","Circle")`), `polygon_points:
|
||||
PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_width`, and
|
||||
PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_width`, and
|
||||
`material_preset` (`@export_enum("None","Wood","Rubber","Cardboard","Metal")`). Presets set
|
||||
`mass` + `physics_material_override` via static `mass_for()`/`physics_material()`/`tint_for()`
|
||||
(Wood: mass 3.0, friction 0.6, bounce 0.1; Rubber: mass 0.5, friction 0.9, bounce 0.85;
|
||||
@@ -663,17 +670,17 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
stickman-prop contacts; the old geometric feet-point stickman test is removed.
|
||||
- `scripts/trigger_area.gd` — `class_name TriggerArea`, `extends Node2D`; a **placeable sensor**
|
||||
(Phase 4, **not used by the editor**). `@export size: Vector2` (default 96×96), `get_area_rect()
|
||||
-> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill +
|
||||
-> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill +
|
||||
dashed border). **No physics and no signals** — it is a pure geometric region evaluated by
|
||||
`sandbox_stage.gd`'s event engine (`_update_area_entry`) for `entered_area` rules.
|
||||
- `scripts/prop_utils.gd` — `class_name PropUtils`, `extends RefCounted`; static factory
|
||||
(**not used by the editor**):
|
||||
- `create_box(size := Vector2(48,48))` / `create_ball(radius := 24.0)` /
|
||||
`create_plank(length := 160.0, thickness := 16.0)` / `create_triangle(base := 56.0, height
|
||||
:= 48.0)` — primitive generators returning shape-payload dictionaries with default
|
||||
:= 48.0)` — primitive generators returning shape-payload dictionaries with default
|
||||
dimensions + color themes (wood/rubber/metal/cardboard).
|
||||
- `spawn_prop(container, position, shape_payload, material_preset := WOOD, initial_velocity
|
||||
:= Vector2.ZERO) -> PropBlock` — instantiates a `PropBlock`, applies the payload
|
||||
:= Vector2.ZERO) -> PropBlock` — instantiates a `PropBlock`, applies the payload
|
||||
(`shape_type` + geometry + colors via `_apply_shape_payload()`, sanitizing polygon points
|
||||
through `TerrainUtils.sanitize_points()`), sets `linear_velocity` after `add_child` (only
|
||||
when non-zero), and returns the spawned prop.
|
||||
@@ -689,7 +696,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`(-80,0)`), Heavy Plank (`create_plank()`, `METAL`, `(30,-40)`). Adds a **best-effort
|
||||
`StaticBody2D` collision proxy** (`RigCollisionProxy`, `_add_rig_collision_proxy()`) since the
|
||||
rig has **no physics bodies of its own** — a 240×1000 px `RectangleShape2D` centered at `(0,-500)`
|
||||
( `RIG_PROXY_SIZE`/`RIG_PROXY_CENTER`) matching the standing figure's world bounds, so props
|
||||
( `RIG_PROXY_SIZE`/`RIG_PROXY_CENTER`) matching the standing figure's world bounds, so props
|
||||
bounce/rest against it; the proxy is a code-only stand-in, not part of the rig.
|
||||
- **Phase 10 ragdoll trigger:** the harness stores the spawned rig in `_rig: StickmanRig`
|
||||
and shows a toggle-mode `Button` (`_ragdoll_toggle`) whose text flips
|
||||
@@ -701,7 +708,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
returns early if one exists, so rapid toggling leaves no duplicate proxies.
|
||||
- **Phase 10 external forces:** a **"Knock Up"** button (`_knock_up()`) tests impulses beyond
|
||||
gravity: when the rig is in RAGDOLL mode it calls `StickmanRig.apply_ragdoll_velocity_boost(
|
||||
KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ragdoll body,
|
||||
KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ragdoll body,
|
||||
preserving internal structure), and applies the same upward velocity delta to every dynamic
|
||||
prop (`RigidBody2D` child of `_environment`) so the whole pile flies up together.
|
||||
- **Phase 11 recovery UI:** a `Label("Rest")` + `SpinBox` (`_rest_timeout_spinbox`, min 0.1 /
|
||||
@@ -781,7 +788,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`node.queue_changed → _director_visuals.mark_dirty` when a stickman is placed; `object_deleted` →
|
||||
`mark_dirty()`. `_set_build_controls_visible()` also hides the Direct button in PLAY.
|
||||
- **Phase 4 triggers & event system:** adds a When→Then **rule system** — `_event_rules:
|
||||
Array[Dictionary]` of `{id, trigger, actions}` rules (trigger types: `arrived_at_waypoint`,
|
||||
Array[Dictionary]` of `{id, trigger, actions}` rules (trigger types: `arrived_at_waypoint`,
|
||||
`action_finished`, `speech_finished`, `entered_area`, `collided`; action types reuse the Phase
|
||||
3a set: `walk_to`/`speak`/`wait`/`ragdoll`/`recover`). Rules persist across Play/Edit mode
|
||||
toggles but are **not** saved to disk (Phase 5). A geometric **area-entry engine** runs in PLAY
|
||||
@@ -832,7 +839,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
an instantiated `scenes/asset_selector.tscn` `PopupPanel`, added to the UI `CanvasLayer` in
|
||||
`_build_ui()` with theme/font overrides applied via `AssetSelector.apply_font`), backed by a
|
||||
dim backdrop `_selector_dim` (a black `ColorRect` at `SELECTOR_DIM_ALPHA` = 0.5, `mouse_filter =
|
||||
MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only while the selector
|
||||
MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only while the selector
|
||||
is open). State
|
||||
`_selector_open`/`_selector_kind`, `_thumbnail_queue`, `_thumbnail_busy`. `_ready()` builds
|
||||
`_stickman_library`/`_thumbnail_cache` and the two thumbnail renderer `Node`s
|
||||
@@ -927,7 +934,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
are `static func` and cannot run in a `const`). Public API: `static get_entries()` (builds once,
|
||||
returns the 4 templates), `static get_ids()`, `static get_entry(id) -> Dictionary` (`{}` if
|
||||
unknown), `static get_default_id() -> String` (`"crate"`). Template dicts carry `{id, name,
|
||||
material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOOD`), Ball/Rubber
|
||||
material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOOD`), Ball/Rubber
|
||||
(`create_ball()` + `RUBBER`), Plank/Metal (`create_plank()` + `METAL`), Triangle/Cardboard
|
||||
(`create_triangle()` + `CARDBOARD`) — generator color themes already match the presets.
|
||||
- `scripts/thumbnails/thumbnail_cache.gd` — `class_name ThumbnailCache`, `extends RefCounted`
|
||||
@@ -953,7 +960,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
identical to the rig actually placed on stage, not a re-implementation of the editor preview.
|
||||
- `scripts/thumbnails/prop_thumbnail.gd` — `class_name PropThumbnail`, `extends Node` (Phase 3b,
|
||||
**not used by the editor**). Renders a prop template into a `Texture2D`. `const SIZE :=
|
||||
Vector2i(200, 200)`. `_ready()` builds the same offscreen `SubViewport` + `Camera2D` pattern as
|
||||
Vector2i(200, 200)`. `_ready()` builds the same offscreen `SubViewport` + `Camera2D` pattern as
|
||||
`StickmanThumbnail`. `render(payload, material_preset) -> Texture2D` mirrors `PropBlock`'s
|
||||
geometry into a plain `Node2D` with a `Polygon2D` (fill) + `Line2D` (closed loop, round
|
||||
joints/caps) — **not** a `RigidBody2D`, so nothing falls under gravity inside the viewport;
|
||||
@@ -969,13 +976,13 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
stickmen, then `popup_centered()`), `set_entries(entries)` (used by Refresh — resets page +
|
||||
thumbnails), `set_thumbnail(entry, tex)` (lazy handoff from the stage's drain), `close()`, and
|
||||
`apply_font(ui_font, emoji_font)` (walks the authored controls). `_ready()` sets `exclusive =
|
||||
true` and wires the footer buttons; `_unhandled_input` maps `KEY_ESCAPE` → `cancelled.emit()`
|
||||
true` and wires the footer buttons; `_unhandled_input` maps `KEY_ESCAPE` → `cancelled.emit()`
|
||||
(the Window's built-in Esc close is not relied on). Re-centers on window resize: the root's
|
||||
`size_changed` signal re-runs `popup_centered()` while the popup is visible. Cell build:
|
||||
`_rebuild()` frees the `%GridContainer` children, toggles the empty-state `%EmptyLabel`,
|
||||
shows/hides Prev/Next/page label by page count, and slices the current page via the
|
||||
**static pure** `page_bounds(total, page, page_size) -> Dictionary {start, end, total,
|
||||
page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect`
|
||||
page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect`
|
||||
(cached/placeholder texture) + a name `Label` + (prop only) a material-badge `Label`; cells emit
|
||||
`item_selected`. No cell is pre-highlighted on open (the previous selection highlight styling
|
||||
was removed). The `.tscn` is a **minimal shell**
|
||||
@@ -1014,9 +1021,9 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
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.
|
||||
- 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`,
|
||||
@@ -1061,11 +1068,12 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
palette-button selector grids.
|
||||
|
||||
### Body-part data model
|
||||
|
||||
- 10 internal part keys (ordered): `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`.
|
||||
- Shape dictionary: `{ "shape_type": String, "points": Array[{x,y}], "color": "#hex",
|
||||
"closed": bool, "vertex_flags": Array[int] }`.
|
||||
"closed": bool, "vertex_flags": Array[int] }`.
|
||||
- `shape_type` values: `"line"`, `"rectangle"`, `"circle"`, `""` (empty).
|
||||
Descriptive tag in Phase 2 — rendering uses `closed`.
|
||||
- `closed`: `true` = filled + closed outline, `false` = open outline-only.
|
||||
@@ -1073,7 +1081,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`1` = user-created via "Create Point" (hollow rectangle).
|
||||
- **Phase 4:** A panel stores a `shapes[]` array of shape dictionaries. Z-order = array
|
||||
position (first = back, last = front). Per-part data includes `{shapes[], position,
|
||||
rotation, scale}`.
|
||||
rotation, scale}`.
|
||||
- **Phase 8:** per-part data adds `pivot` `{x, y}` (local bounding-box center = rotation
|
||||
origin) and `length` (float, bbox extent along the segment axis) for format v1.4; the
|
||||
`.stk` root also gains a top-level `proportions` object (5 rig bone lengths). All three
|
||||
@@ -1086,6 +1094,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
`"1.0"`–`"1.4"` files auto-migrate on load).
|
||||
|
||||
### settings.json (Phase 6)
|
||||
|
||||
- Persisted editor preferences written to `settings.json` via `_save_settings()` and
|
||||
loaded in `_load_settings()`.
|
||||
- Keys: `version`, `grid_size`, `snap_to_grid`, `recent_colors`, `show_pose_guide`.
|
||||
@@ -1096,12 +1105,14 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and
|
||||
via `_broadcast_settings()`.
|
||||
|
||||
### Legacy scene (do not delete)
|
||||
|
||||
- `stick.tscn` — the original rigged/animated figure using `Skeleton2D` + IK targets +
|
||||
`RemoteTransform2D` + `Line2D` limbs, plus an embedded `@tool` script drawing the head
|
||||
circle. Animations: "walk", "walk_to", "RESET". **Not** the current main scene; kept for
|
||||
future animation/rigging phases.
|
||||
|
||||
## Editor conventions
|
||||
|
||||
- `.godot/` is gitignored; never edit it manually.
|
||||
- Scene files (`*.tscn`) and `.import` files are text-based; use Godot's editor for complex changes.
|
||||
- UID references (Godot 4 native) exist in scenes; do not change them by hand.
|
||||
|
||||
Reference in New Issue
Block a user