Files
stickman/docs/tech_debt_and_optimizations.md

27 KiB
Raw Permalink Blame History

Technical Debt & Optimization Log

Overview

This document tracks known technical debt, optimization opportunities, and minor improvements for the Stickman Studio project. Items are prioritized by impact and effort. Update this log as issues are identified, resolved, or deprioritized.


Current Observations

# Issue Priority Status Notes
1 Neck Joint Free Rotation — The head can spin 360° during violent ragdoll tumbles, which looks unnatural. Medium Open Consider adding angular_limit_enabled = true with ±45° limits and low stiffness to allow natural lolling while preventing full rotations.
2 Torso Capsule Origin — The torso capsule's local origin should be at its midpoint for natural rotation. Currently derived from bone distance; verify alignment. Low Open Test by rotating the torso body in ragdoll mode — it should spin about its center, not its top. Adjust position offset if needed.
3 Collision Layers Separation — Bodies and terrain share layer 1/mask 1. This may cause selfcollision issues (limbs clipping through each other) under high stress. Low Open Future enhancement: assign ragdoll limbs to layer 2, terrain to layer 1, and use masks to allow limblimb collision only where desired.
4 Performance (Ragdoll Pooling) — Spawning 10 bodies + 9 joints procedurally is fine for a single rig. If the scene ever contains dozens of ragdolls, consider a pooling system to avoid allocation spikes. Low Open Not needed now, but worth noting if scaling to large crowds.
5 Line2D ↔ Capsule Radius Match — Limbs use Line2D width 16, ragdoll capsules radius 8. These align visually. Low Resolved Verified during implementation. No action needed.
6 Recovery Animation Starting Pose — The stand_up animation must work from any captured ragdoll pose. Currently uses a fixed start frame. High Resolved Phase 11: _start_recovery() captures the 10 bodies' riglocal pose, snapsolves the skeleton via the 6 IK targets, then _play_stand_up() tweens the markers directly from the captured values to STAND_POSE (STAND_UP_DURATION, sine easeinout) — the baked stand_up animation is not played (a fixed first keyframe can never match an arbitrary rest pose; the earlier bridgeintotheanimation approach caused a visible jump and was removed). Revision: the snap now derives the hip (pos dir·half) and wrist/ankle (pos + dir·half) from the capsule ends and subtracts the Torso bone's bone_angle for the marker rotation, so recovery starts from the ragdoll's exact final pose (e.g. sitting stays sitting). (20260827)
7 Transition Visual Pop — The crossfade between kinematic and ragdoll currently uses a simple modulate.a lerp. This may cause ghosting if the kinematic and ragdoll poses are misaligned. Medium Resolved Phase 11: the entry is now an instant handoff — the ragdoll is built from the current solved bone positions (AnimationPlayer.stop(true) keeps the pose), then Body/* is hidden and the IK stack disabled in the same call. The earlier opacity crossfade + pinsoftness ramp was removed on director feedback (it read as ghosting, since both poses are identical). Recovery snapsolves the kinematic skeleton to the captured ragdoll pose before reshowing Body/*, eliminating the pop on both directions. (20260827)
8 Rest Timeout UI — The director can adjust rest_timeout via inspector, but there is no inworld UI in the physics harness yet. Low Resolved Phase 11: added a Rest SpinBox (0.110 s, step 0.1) to the harness UI that writes _rig.rest_timeout (runtimeonly), plus a "Recover Now" button → _rig.request_recovery(). (20260827)
9 Animation Generation DRYcreate_walk.gd is a standalone script. It should be merged into a unified create_animations.gd that also generates stand_up and idle animations. Medium Resolved Phase 11: create_walk.gd deleted; new scripts/create_animations.gd (@tool extends EditorScript) bakes walk_left/walk_right (same keyframes) and a oneshot stand_up into the scene's AnimationLibrary (the baked stand_up is an authored reference only — runtime recovery tweens the IK targets directly). (20260827)
10 Rig Collision Proxy Readdition — The proxy is readded on ragdoll exit, but may cause a brief visual pop if it appears while the kinematic rig is visible. Low Open Phase 11 still readds the proxy as soon as RECOVERING begins (state_changed handler), while Body/* is already visible — the static box can pop in around the standing figure before the standup completes. Consider delaying readdition until after recovery finishes (ANIMATED). (20260827)
11 Stage Freeze Abstraction — Sandbox Stage EDITmode freezing is typespecific: RigidBody2D.freeze_mode = FREEZE_MODE_KINEMATIC for props, StickmanRig.set_ragdoll(false) for stickmen, nothing for StaticBody2D terrain. There is no unified "freeze" abstraction over the mixed physics population. Low Open A future physics type (e.g. Area2Dbased sensors) will need another case in scripts/sandbox_stage.gd _enter_edit_mode() / _enter_play_mode(). Consider a ducktyped set_simulating(bool) interface once more physical object kinds appear. (20260827)
12 Stage AABB Selection PrecisionStageSelection.get_world_aabb uses conservative worldspace AABBs (polygon point union / fixed rig rect), not pointinpolygon. Low Open Clicks in the boundingbox corners of large or rotated terrain may select a block even outside its polygon, and overlapping blocks can misselect. Refine with Geometry2D.is_point_in_polygon() for TerrainBlock/PropBlock polygons (and circle distance for ball props) once selection precision matters. (20260827)
13 Slope-aware walking physics & dynamic obstacle avoidance (deferred) — Phase 3a bakes a real navigation mesh (per-TerrainBlock polygon decomposition into a code-built NavigationRegion2D) and walk_to follows NavigationAgent2D paths, but the figure walks the path with an upright pose (no tilt to the slope, no physics sliding) and avoidance_enabled = false, so it can path through props and other stickmen. Medium Open A stickman crossing a ramp/stair follows the sloped footprint but looks flat-footed, and does not avoid moving props or each other. Future: tilt/rotate the figure to the path slope and enable RVO avoidance (avoidance_enabled, avoidance layers, velocity handling) once props/other stickmen are registered as obstacles. (20260829)
14 Phase 4 event engine is O(rigs×props + areas×movables) per physics framesandbox_stage.gd _update_area_entry() / _update_stickman_prop_collision() run all-pairs geometric tests every physics frame in PLAY. Low Open Fine at sandbox scale, but degrades quadratically with dozens of dynamic objects. A cell-keyed spatial broadphase (e.g. _grid_cells) can cull candidate pairs before the AABB/feet-point tests. Phase 4b implemented that index (see #16, now Resolved) but the event engine does not yet query it — #14 remains Open until _update_area_entry() / _update_stickman_prop_collision() use it. (20260830 / note updated 20260902)
15 Walk arrival jitter (mode re-evaluated per frame + arrival-radius mismatch)StickmanRig._update_walking() re-computes _walk_mode from is_target_reachable() every physics frame, and finishes via is_navigation_finished() (NAV_TARGET_DESIRED_DISTANCE 12 px, feet-space) or the ARRIVE_DISTANCE 8 px root-space guard. Near the nav-mesh boundary (clicked waypoints often sit just off placed terrain) is_target_reachable() can flip, swapping root_target between the nav path point and the raw waypoint — two points with a small vertical offset → up/down jitter. Medium Resolved Phase 4b: StickmanRig._update_walking() now latches _walk_mode once per walk (probes up to LATCH_PROBE_MAX_FRAMES = 6 after the map syncs, latching "nav" when reachable or "direct" on the bound) so it can never flip between frames, unifies arrival on the final target at ARRIVE_DISTANCE 8 px (snap-on-arrive), steers straight at the final target when within 2 × ARRIVE_DISTANCE, and re-asserts the standing markers one extra physics frame after stop (_settle_walk_markers()) to clear any residual body-bob. Exactly one arrive fires; global_position is unchanged after arrival. See plans/PHASE_4b_SPEC.md §3. (2026-09-02)
16 No grid spatial dictionary — terrain placement (sandbox_stage.gd _place_at) is single-click with no occupancy tracking; the 3-state "empty/same-type/blocked" drag-paint query and any spatial broadphase need a cell→nodes index. Low Resolved Phase 4b adds an advisory grid spatial dictionary _grid_cells (cell Vector2i @ TERRAIN_GRID_SIZE 16 → Array[Node2D]) plus _rasterize_aabb_to_cells() to index every world AABB's covered cells; it drives the terrain drag-painting 3-state ghost query and the director target-validity test (skipping cells whose nodes include a TerrainBlock). It is populated on place, rebuilt on move/rotate/delete, and is never authoritative (the World tree is). It is not yet wired into the Phase 4 event engine — #14 stays Open for that. See plans/PHASE_4b_SPEC.md §2.6. (2026-09-02)
17 Thumbnail caching has no eviction / cap; render is deferred one-per-frame — Phase 3b (stickman_library.gd / prop_library.gd + thumbnails/*) caches rig/prop thumbnails to user://thumbnails/ keyed by stickman basename+mtime and prop id_v<PROP_VERSION>. Low Open PNGs accumulate unboundedly on disk (only clean_stale_stickmen prunes superseded basenames; nothing caps total bytes), and in-headless the renderers return null → a placeholder is shown until a manual F6 run populates real captures. Future: a disk-size/LRU eviction policy, a version/cleanup sweep on stage open, and an explicit cache-warm pass (or skip-the-placeholder note) for headless/CI. (2026-09-03)
18 Asset selection is session-only, not savedStageSpawner.selected_stickman_path / selected_prop_id reset on scene reload (Phase 3b decision, per spec). Low Open Deliberate for Phase 3b (spec §2 decision 1: no disk save, do not extend _save_settings). A future persistence phase could persist the last-chosen stickman/prop to user://sandbox_settings.json for convenience. (2026-09-03)
19 Phase 3c editor-popup host-visibility discipline is ad-hoc — the new QueuePanel / RulePanel / ActionEditor / RuleEditor are exclusive = true PopupPanels, so the stage must hide a host popup before entering a stage-click capture (or opening a nested editor) and re-show it on resolve/cancel. That logic is spread across _open_action_editor, _on_action_editor_committed / _cancelled, _begin_walk_edit, and _on_rule_editor_trigger_target_requested, with one-off _restore_queue_panel / _restore_rule_panel / _restore_rule_build_panel helpers. Medium Open Works, but fragile: any future editor popup must remember to hide its host before every capture or the exclusive window swallows the stage click. A small popup-stack abstraction (push host → capture → pop) would make that impossible to forget. (2026-09-04)
20 Rule Panel "Add Rule" has no source in waypoint-filtered panels_on_rule_panel_add_requested bails with a toast when _rule_panel_source_id < 0, which is exactly the state of the waypoint-filtered "Edit Trigger Rules" panel (opened from the waypoint context menu). Low Open The trigger source is genuinely ambiguous there (a waypoint can be targeted by rules authored by several stickmen). Acceptable for now; a future pass could default the source to _ctx_waypoint_rig (the rig that owns the waypoint) or open the builder in "any stickman" (source 1) mode. (2026-09-04)
21 Phase 3c drag-reorder uses nearest-row-center, not an insertion pointQueuePanel / RulePanel reorder computes the drop target as the row whose vertical center is nearest the mouse, then moves the dragged item to that index. Low Open Functional, but the drop position can read off-by-one near row boundaries (it snaps to a whole row rather than an edge between rows). A true insertion-point indicator (a line drawn between rows) would be clearer; not worth the complexity until reordering long queues/rules is common. (2026-09-04)
22 --check-only --script needs a prior --editor rescan for new class_name scripts — a freshly added class_name (e.g. QueuePanel) is not resolvable by --check-only --script res://scripts/sandbox_stage.gd until --headless --editor --quit regenerates .godot/global_script_class_cache.cfg. Low Open Verification-workflow gotcha only (not a runtime bug): in-editor the cache is always current. CI/headless scripts that add new class_names should run an --editor pass before per-script --check-only. (2026-09-04)
23 Phase 3c confirmation dialog is popped over an exclusive panel — Queue delete / Clear All (_on_queue_panel_delete_requested / _on_queue_panel_clear_requested) and Rule delete / Clear All (_on_rule_panel_delete_requested / _on_rule_panel_clear_requested) call the shared ConfirmationDialog (_ask_confirm) without first hiding the exclusive = true QueuePanel / RulePanel. Low Open Distinct facet of #19 (host-visibility discipline): popping a second exclusive window over a visible exclusive panel logs a non-fatal engine warning and the confirmation may render non-modal. It still functions (the panel is re-shown after the callback) — cosmetic. A fix would hide the host panel around _ask_confirm and re-show it in the confirm callback (and on the dialog's cancel/hide). Deliberate non-debt note: plan §11.2's separate confirmation dialog for editing ragdoll/recover actions was implemented instead as a paramless ActionEditor pre-fill — recorded in docs/phase_3c_editor_spec.md §2 decision 5, not as a debt row. (2026-09-04)

Suggested Future Improvements (Beyond Current Scope)

Improvement Description Status / Priority
Soft Transition Blending Add physical blending (joint stiffness ramp) to complement visual crossfade. Done, then removed — implemented with Phase 11, but the whole crossfade was removed on director feedback (ghosting); entry is now an instant handoff.
Ragdoll Recovery Interruptibility Allow the director to force a mode switch midrecovery (e.g., if they want the character to ragdoll again immediately). Done
Multiple Rig Support Ensure all state variables are instancespecific (already true) and that the harness can manage multiple rigs. Future
Animation Blending (IK vs. Physics) Blend between the animationdriven pose and the ragdoll pose during transition to prevent snapping. Future
Save/Load for Ragdoll State Save the current ragdoll pose to .stk (e.g., for storyboarding a fall). Future

How to Use This Log

  1. New Issues: Add a row with #, description, Priority (Low/Medium/High), Status (Open/In Progress/Resolved/Deprecated), and Notes.
  2. Resolving Issues: Change Status to Resolved and add a note with the date and fix details.
  3. Deprioritizing: If an issue is no longer relevant, mark it Deprecated with a brief reason.
  4. Quarterly Review: Revisit this log every major milestone to reassess priorities.

Change Log

Date Change
2026-08-26 Initial creation — migrated observations from Phase 10 review.
2026-08-27 Phase 11 resolved #6 (recovery starting pose), #7 (transition visual pop), #8 (rest timeout UI), #9 (animation generation DRY); #10 (proxy readdition) remains open with updated scope. Later revision: standup recovery switched from bridgeintobakedanimation to a direct marker tween (captured pose → STAND_POSE), fixing a visible jump; baked stand_up kept as authored reference only. Second revision: ragdoll entry builds from the current solved bone positions (IK disabled only after the blend completes) and the recovery snap derives joint ends from capsule halfheights with Torso bone_angle compensation, fixing the entry posepop and the "recovery starts lying" bugs. Third revision: the entire crossfade/blend (transition_duration, BlendDirection, opacity fade, softness ramp) was removed on director feedback — entry is now an instant handoff (build at current pose → hide Body/* → disable IK in one call), since the ragdoll spawns at the identical pose and a fade only read as ghosting.
2026-08-27 Sandbox Stage Builder (Phase 2) added scripts/sandbox_stage.gd + stage_spawner.gd / stage_selection.gd / stage_gizmos.gd + scenes/sandbox_stage.tscn. Logged #11 (no unified freeze abstraction over the mixed StaticBody2D / RigidBody2D / Node2D population) and #12 (selection hittesting uses worldspace AABBs rather than pointinpolygon).
2026-08-29 Phase 3a (Core Director Functionality) spec written (docs/phase_3a_spec.md). Logged #13 (slope-aware walking physics + dynamic obstacle avoidance deferred — the nav mesh itself is built in 3a). Notable non-debt decisions recorded in the spec: Play mode now runs the director script instead of auto-ragdolling stickmen; walk_to(target) treats target as a feet/ground destination via FOOT_OFFSET, with the NavigationAgent2D child placed at the feet (0,+385) so it paths on the ground-level nav mesh.
2026-08-29 walk_to stops-after-a-few-px bug fixed. _update_walking (Phase 3a) now defers nav reads until NavigationServer2D.map_get_iteration_id(...) != 0 (map-sync guard) and forces the path query via get_next_path_position() before any empty-path/finished check. Follow-up (same day): the initial "warn + finish in place" unreachable-target policy was itself reported as "stickman stands still with a waypoint" and was replaced by hybrid nav/direct steering — an on-mesh target follows the nav path (_walk_mode = "nav"), an off-mesh/unreachable target walks straight to the clicked waypoint (_walk_mode = "direct", root target = waypoint + FOOT_OFFSET), with no push_warning; _walk_path_grace removed (map-sync guard + forced path query replace it); the debug trace now carries `mode=nav
2026-08-30 Phase 4 (Triggers & Event System) implemented: scripts/trigger_area.gd (NEW placeable sensor), sandbox_stage.gd rule system (_event_rules, geometric event engine _update_area_entry / _update_stickman_prop_collision, rule-builder UI state machine, _cleanup_rules_for_nodes), stickman_rig.gd (arrived gains a target payload; new enqueue_reactive), prop_block.gd (collided physics signal), stage_director_visuals.gd rule visualization, stage_spawner.gd / stage_selection.gd "area" palette + duck-typed get_area_rect. Logged #14 (all-pairs event engine scales O(rigs×props + areas×movables); spatial hash suggested).
2026-09-02 Phase 4b (Polish) spec written (plans/PHASE_4b_SPEC.md). Logged #15 (walk-arrival jitter: _walk_mode re-evaluated per frame + arrival-radius mismatch) and #16 (no grid spatial dictionary; Phase 4b adds one for terrain drag-painting + event-engine broadphase).
2026-09-02 Phase 4b (Polish) implemented. Resolved #15 (walk-mode latch once per walk, unified ARRIVE_DISTANCE arrival with snap-on-arrive, steer-to-final-when-close, one-frame marker re-assert) and #16 (grid spatial dictionary _grid_cells @ TERRAIN_GRID_SIZE for the drag-painting 3-state query + director target-validity). #14 remains Open — the dictionary is not yet wired into the Phase 4 event engine (note updated). Added res://sandbox_theme.json, scripts/stage_placement_overlay.gd, the 3-segment mode switcher / status bar / badge / frame / cursors, and director tooltip/trajectory/reticle UX.
2026-09-03 Phase 3b (Asset Library) implemented (docs/phase_3b_asset_grid_spec.md): scripts/stickman_library.gd / prop_library.gd, scripts/thumbnails/thumbnail_cache.gd / stickman_thumbnail.gd / prop_thumbnail.gd, scripts/asset_selector.gd + scenes/asset_selector.tscn. The Stickman/Prop palette buttons now open visual selector grids (pagination 12/page, session-only selection, Browse/Refresh); StageSpawner registry becomes ground/ramp/step/prop/stickman/area (crate/ball removed) with selected_* session state + per-path stickman cache; SandboxStage owns the selector open/close flow, Esc priority, and the one-per-frame lazy thumbnail drain. Logged #17 (thumbnail cache growth / headless placeholder) and #18 (session-only selection). Verified with the new headless suite tests/test_phase3b_library.gd plus the updated tests/test_phase4b1_fixes.gd.
2026-09-03 Selector UI bugfix round (5 bugs, documented via tests/test_phase3b_ui_fixes.gd + docs in docs/phase_3b_asset_grid_spec.md / README.md / AGENTS.md): AssetSelector.open() drops its selection params (selected_path/selected_id) — no cell is pre-highlighted on open (selected stylebox removed); the selector re-centers on window resize (size_changedpopup_centered()); the stage adds a dim backdrop _selector_dim (SELECTOR_DIM_ALPHA 0.5) behind the grid; the selector's popup_hide routes to _on_selector_cancelled() (idempotency-guarded) so an outside-click un-presses the palette button; and the Direct-mode action popup opens right of the clicked stickman (_world_to_screen + 24 px) instead of at the cursor. Reviewed #17 and #18 — neither is obsolete (both concern thumbnail-cache growth/headless placeholders and session-only persistence, orthogonal to these UI fixes), so both remain Open unchanged; no duplicate rows introduced.
2026-09-03 Rule-builder popup-anchor bugfix round (sandbox_stage.gd, documented in README.md / AGENTS.md): the Phase 4 rule-builder context menus used to re-pop at the live mouse position on every re-open, so cycling " When…" → "⬅ Back to actions" → "When…" walked the menu down the screen. Now a session anchor records the first context menu's screen position (_popup_anchor: Rect2i / _popup_anchor_set; Direct first menu = right of the clicked stickman; rule-label edit entry = the click position) and all child popups reuse it via _set_popup_anchor(rect) / _clear_popup_anchor() / _popup_anchor_rect(), until cleared on confirm (_finalize_rule), cancel (_cancel_rule_build), or Direct-mode/flow exit (_clear_director_pending) — but not on _reset_rule_builder() (Back-to-actions reuses it). No numbered debt row described the old cursor-following behavior, so no row was flipped to Resolved and no duplicates introduced; recorded here in the Change Log only.
2026-09-04 Phase 3c (Editor Tools — Action & Rule Editing) implemented: scripts/action_registry.gd / trigger_registry.gd (extensible action/trigger templates), scripts/action_editor.gd / rule_editor.gd / queue_panel.gd / rule_panel.gd + scenes/*.tscn shells, scripts/waypoint_context.gd, plus integration in sandbox_stage.gd (unified CaptureKind target-capture, "Edit Queue…/Edit Rules…" + right-click stickman/waypoint context menus, confirmation dialog, consequence-only rule editor) and stage_director_visuals.gd (hit_test_waypoint_action, waypoint edit highlight). Logged #19 (ad-hoc popup host-visibility discipline), #20 ("Add Rule" has no source in waypoint-filtered panels), #21 (drag-reorder nearest-row-center), #22 (--check-only --script needs a prior --editor rescan), and later the same day #23 (Phase 3c confirmation dialog popped over an exclusive panel — the delete/clear-all confirm paths do not hide the QueuePanel/RulePanel first). Documented the plan-§11.2 deviation (paramless ActionEditor pre-fill instead of a separate ragdoll/recover confirmation dialog) in docs/phase_3c_editor_spec.md §2 decision 5 — a design decision, so no debt row. Static-verified via headless --editor --quit + per-script --check-only; runtime F6 flows (queue/rule panel edit-delete-reorder, waypoint context menu, visual walk edit, full vs consequence-only rule editor) still need manual verification.

End of Log