Phase 14 - Organized directories.

This commit is contained in:
2026-10-03 19:13:39 -04:00
parent 6666173c4f
commit d950fd9d9a
307 changed files with 1452 additions and 1110 deletions
+362
View File
@@ -0,0 +1,362 @@
# Sandbox theme reference
`res://sandbox_theme.json` is the hand-editable presentation configuration for
the Sandbox Stage (`res://scenes/stage/sandbox_stage.tscn`). It is loaded by
`SandboxStage` during `_ready()`, before the code-built UI is created. The
loader is `res://scripts/stage/sandbox_theme.gd` (`class_name SandboxTheme`).
The committed file uses schema `"2.0"`. The theme controls presentation
defaults only; it does not save a scene, placement, or action data.
## Load, merge, and validation rules
The load path is:
```text
res://sandbox_theme.json
│
▼
SandboxTheme.DEFAULT_CONFIG
│ deep-merge file values
▼
SandboxStage._build_ui() and visual overlays
```
- Missing, unreadable, malformed, or non-object JSON uses the built-in default
configuration and emits one warning. The stage continues to run.
- Missing nested keys inherit the built-in value. Unknown keys are retained in
the raw dictionary but have no effect until a consumer is added.
- `stylebox(component)` resolves styles in this order:
`global` → `<section>.style_defaults` → the component object. For example,
`palette` is resolved from `global`, then `palette.style_defaults`, then
`palette`.
- Colors must be strings in `#RRGGBB` or `#RRGGBBAA` form. Invalid colors use
the requested fallback and emit a warning.
- Numeric accessors accept finite integers/floats and clamp them to the range
requested by the consumer. Invalid or non-finite numbers use that consumer's
fallback. Size arrays are accepted when they contain two numeric values;
individual controls apply their own minimum sizes.
- Font paths are `res://` resource paths. A missing resource, or a resource
that is not a `Font`, uses `ThemeDB.fallback_font` and emits a warning.
- The theme is loaded once per Sandbox Stage instance. Restart the scene (F6)
to see edits; there is no live file watcher or reload button.
The live grid size, snap toggle, and grid visibility are stored in
`user://sandbox_settings.json`. `grid.snap_size` supplies the initial grid-size
default; it does not overwrite a value already saved in user settings.
## Schema overview
Every current top-level block is listed below. The values shown are the
defaults in the committed `sandbox_theme.json` (not necessarily the fallback
used by an older caller).
### Current consumer status
The schema intentionally includes forward-looking values for the reusable
widget and speech work. The current Sandbox runtime applies these groups:
- `global` style values used by resolved styleboxes and the selector control
theme (background/border/radius/padding); `global.text_color` and
`global.muted_text_color` are available presentation values but are not
applied to every existing label automatically.
- `fonts.ui_font`, `emoji_font`, bold/italic variants, the nested `sizes`
entries for action popups, tooltips, status pills, queue/rule/action editors,
panel rows/titles, `file_menu`, `file_menu_popup`, and `mode_switcher`, plus
the related `flags`.
- `mode_colors.edit`, `direct`, `play`, and `guide`; `grid.snap_size` as the
first-run grid default; dialog sizes/fonts; `chrome.mode_switcher` width;
palette width/card size/drag threshold/style; selector cell and thumbnail
sizes; and all `overlays` values.
These declared values currently remain reserved or require a legacy adapter:
- `chrome.top_bar.height`, `status_bar.height`, `mode_frame`, `mode_badge`,
and `tooltip` describe the intended chrome contract, but the corresponding
layout/style constants are still authored in `SandboxStage`.
- `popups.action_menu.font_size` and `popups.asset_selector.backdrop_alpha`
are reserved component overrides. The active action-menu size comes from
`fonts.sizes.action_popup`; selector `cell_size` and `thumbnail_size` are
active.
- `palette.icon_size` and `palette.selected_border_width` are reserved for
the palette card renderer; the current card has its own glyph/border values.
- `fonts.sizes.assignment_badge`, `rule_label`, `asset_label`, and
`asset_badge` are part of the resolved schema, but the current direct call
into `StageDirectorVisuals` still reads the legacy flat badge/label names.
Use the legacy names documented below when changing those director visuals.
- `fonts.presets` and every `widgets` list are exposed through reusable
controls, but the current Sandbox Speak action does not instantiate those
controls yet. They are future speech-customization data.
### `version`
| Key | Format | Current value | Notes |
|---|---|---:|---|
| `version` | string | `"2.0"` | Use `"2.0"` when using the nested schema below. |
### `global`
Global style values are inherited by `SandboxTheme.stylebox()` and the default
control theme.
| Key | Format | Default | Purpose |
|---|---|---:|---|
| `font_preset` | string | `"ui"` | Named base-font selection for future/general UI use. |
| `font_size` | integer | `18` | Default `Control` font size. |
| `text_color`, `muted_text_color` | color string | `#F5F7FA`, `#AAB2C0` | Primary and secondary text colors. |
| `bg_color`, `surface_color` | color string | `#171B24F2`, `#252B38FA` | Global and raised-surface backgrounds. |
| `border_color` | color string | `#FFFFFF33` | Default border color. |
| `border_width` | integer | `1` | Default border width. |
| `corner_radius` | integer | `10` | Default corner radius. |
| `padding`, `spacing` | integer | `8`, `8` | Default style padding and layout spacing. |
### `fonts`
Font paths may be empty (`""`) to use the engine fallback. `presets` is an
object of arbitrary preset IDs; each entry has a display `label` and a `path`.
The committed file provides `ui`, `comic`, `pixel`, and `scary` entries.
| Key | Format | Default | Consumer |
|---|---|---:|---|
| `ui_font`, `emoji_font` | `res://` path or empty | `""` | Base UI and emoji-capable stage/director controls. |
| `ui_font_bold`, `ui_font_italic` | `res://` path or empty | `""` | Optional style variants; bold can be generated from the base font when no dedicated path is supplied. |
| `presets` | object `{id:{label,path}}` | see JSON | `font_presets()` and future font selectors. |
| `sizes.action_popup` | integer | `24` | Director/action popup text. |
| `sizes.action_popup_emoji` | integer | `22` | Effective popup size when a popup font is active (Godot popup menus have one font size). |
| `sizes.assignment_badge` | integer | `20` | Reserved resolved-theme value; current director visuals use legacy `fonts.assignment_badge_font_size`. |
| `sizes.rule_label` | integer | `18` | Reserved resolved-theme value; current director visuals use legacy `fonts.rule_label_font_size`. |
| `sizes.status_pill` | integer | `18` | Mode badge/pill. |
| `sizes.tooltip` | integer | `18` | Cursor-attached targeting tooltip. |
| `sizes.file_menu` | integer | `22` | `File` button in the top bar. |
| `sizes.file_menu_popup` | integer | `26` | File menu and Open Recent popup rows. |
| `sizes.mode_switcher` | integer | `24` | Edit/Direct/Play buttons. |
| `sizes.queue_panel`, `sizes.rule_panel` | integer | `22`, `22` | Queue and rule editor panels. |
| `sizes.action_editor`, `sizes.rule_editor` | integer | `22`, `22` | Action and rule editor controls. |
| `sizes.panel_row`, `sizes.panel_title` | integer | `20`, `24` | Editor row text and panel titles. |
| `sizes.asset_label`, `sizes.asset_badge` | integer | `18`, `14` | Reserved asset-card text sizing. |
| `flags.panel_title_bold` | boolean | `true` | Bold queue/rule panel titles. |
| `flags.rule_label_bold` | boolean | `false` | Bold director rule labels. |
| `flags.badge_bold` | boolean | `true` | Bold director badges. |
| `flags.action_popup_bold`, `flags.action_popup_italic` | boolean | `false`, `false` | Popup style flags. |
### `mode_colors`
Each value is a color string. These accents are used by the mode badge/frame,
active mode controls, targeting feedback, and related stage chrome.
| Key | Default | Purpose |
|---|---|---|
| `edit` | `#22C6FF` | Edit-mode accent. |
| `direct` | `#FFB300` | Direct-mode accent. |
| `play` | `#33DD77` | Play-mode accent. |
| `guide` | `#22C6FF` | Terrain painting guide line. |
### `grid`
| Key | Format | Default | Purpose |
|---|---|---:|---|
| `snap_size` | finite number | `15.0` | Initial grid size, clamped to the stage's supported range. |
### `chrome`
`style_defaults` is the optional common style object for chrome components. Its
style keys follow the `global` style formats (`bg_color`, `border_color`,
`border_width`, `corner_radius`, `padding`).
| Key | Format | Default | Purpose |
|---|---|---:|---|
| `style_defaults` | style object | `{ "bg_color":"#171B24F2", "corner_radius":10 }` | Shared chrome style base. |
| `top_bar.height` | number | `64` | Top navigation bar height. |
| `status_bar.height` | number | `38` | Bottom status bar height. |
| `mode_switcher.segment_min_width` | number | `124` | Minimum width of each Edit/Direct/Play segment. |
| `mode_frame.border_width` | integer | `3` | Colored stage frame width. |
| `mode_badge.corner_radius` | integer | `12` | Mode badge corner radius. |
| `mode_badge.padding` | number | `10` | Mode badge padding. |
| `tooltip.bg_color` | color | `#101010E6` | Targeting tooltip background. |
| `tooltip.border_width` | integer | `1` | Targeting tooltip border. |
| `dialogs.queue_size` | `[width,height]` | `[680,520]` | Queue editor window minimum size. |
| `dialogs.rule_size` | `[width,height]` | `[720,560]` | Rule editor window minimum size. |
| `dialogs.action_size` | `[width,height]` | `[560,390]` | Action editor window minimum size. |
| `dialogs.confirm_size` | `[width,height]` | `[620,300]` | Rename, unsaved, missing-assets, and other confirmation/error windows. |
| `dialogs.font_size` | integer | `22` | Dialog controls/buttons. |
| `dialogs.message_font_size` | integer | `26` | Centered AcceptDialog message text. |
### `popups`
`style_defaults` accepts the normal style keys and is inherited by popup
components.
| Key | Format | Default | Purpose |
|---|---|---:|---|
| `style_defaults` | style object | bg `#202633FA`, border `#FFFFFF33`, radius `12`, padding `12` | Common popup surface. |
| `action_menu.font_size` | integer | `24` | Reserved action-menu component override; use `fonts.sizes.action_popup` for the active menu. |
| `dialog` | object | `{}` | Reserved per-dialog popup overrides. |
| `asset_selector.backdrop_alpha` | finite number | `0.55` | Reserved selector backdrop opacity. |
| `asset_selector.cell_size` | `[width,height]` | `[190,220]` | Prop/stickman selector cell size. |
| `asset_selector.thumbnail_size` | `[width,height]` | `[156,156]` | Prop/stickman selector thumbnail size. |
### `palette`
`style_defaults` uses normal style keys and styles the stage placement palette.
| Key | Format | Default | Purpose |
|---|---|---:|---|
| `style_defaults` | style object | bg `#252B38FA`, radius `10`, padding `6` | Palette surface/card base. |
| `dock_width` | number | `116` | Palette width in Edit mode and stage-frame inset. |
| `card_size` | `[width,height]` | `[96,94]` | Placement-card minimum size; the card keeps room for its label. |
| `icon_size` | number | `30` | Reserved palette icon/glyph size. |
| `drag_threshold` | number | `8` | Pointer distance before a card becomes a drag. |
| `selected_border_width` | integer | `3` | Reserved selected palette card border. |
### `overlays`
`style_defaults` controls dashed overlay geometry in world pixels; colors are
RGBA strings.
| Key | Format | Default | Purpose |
|---|---|---:|---|
| `style_defaults.line_width` | number | `2` | Dashed trajectory/guide width. |
| `style_defaults.dash_length` | number | `8` | Dash length. |
| `style_defaults.dash_gap` | number | `5` | Gap length. |
| `terrain_guide.color` | color | `#22C6FF` | Terrain paint guide. |
| `valid.color` | color | `#4DE66680` | Valid director target/placement feedback. |
| `same_type.color` | color | `#FFFFFF26` | Same-type placement feedback. |
| `invalid.color` | color | `#FF4D4D80` | Invalid target/placement feedback. |
### `widgets`
These lists feed reusable controls (`EmojiQuickGrid`, `FontPresetPicker`, and
`PresetSizeToggle`). They are data sources, not speech-action persistence.
| Key | Format | Default | Purpose |
|---|---|---|---|
| `swatches` | array of color strings | white, black, cyan, amber, green, red | Color selector entries. |
| `size_presets` | array of `{id,label,value}` objects | Small/Medium/HUGE (`20/28/40`) | Size selector entries; `value` is an integer-like size. |
| `emoji` | array of strings | eight emoji entries | Emoji quick-grid entries. |
## Practical edits
Make the top navigation easier to read:
```json
{
"fonts": {
"sizes": {
"file_menu": 26,
"file_menu_popup": 30,
"mode_switcher": 28
}
}
}
```
Change the mode frame and make the stage colors more distinct:
```json
{
"mode_colors": {
"edit": "#4DD9FF",
"direct": "#FF8A3D",
"play": "#65E68A",
"guide": "#4DD9FF"
},
"chrome": { "mode_frame": { "border_width": 4 } }
}
```
Give selectors larger cards and thumbnails:
```json
{
"popups": {
"asset_selector": {
"cell_size": [220, 260],
"thumbnail_size": [190, 190]
}
}
}
```
Make kid-facing editors and confirmations larger:
```json
{
"chrome": {
"dialogs": {
"queue_size": [760, 600],
"rule_size": [800, 640],
"font_size": 26,
"message_font_size": 30
}
},
"fonts": {
"sizes": {
"queue_panel": 24,
"rule_panel": 24,
"action_editor": 24,
"rule_editor": 24,
"panel_row": 22,
"panel_title": 28
}
}
}
```
## Legacy v1 files
Older Phase 4b/3c theme files used flat keys such as
`fonts.action_popup_font_size`, `fonts.queue_panel_font_size`,
`fonts.panel_title_font_size`, and mode keys such as
`mode_colors.edit_accent` and `mode_colors.guide_line`. They remain accepted.
The loader aliases the legacy mode names and selected flat font-size names into
the nested structure, while `SandboxStage` retains the old precedence for a
file that does not declare version `"2.0"`.
The complete legacy names still read by the current runtime are:
```text
fonts.action_popup_font_size fonts.action_popup_emoji_size
fonts.assignment_badge_font_size fonts.assignment_badge_radius
fonts.rule_label_font_size fonts.status_pill_font_size
fonts.tooltip_font_size fonts.queue_panel_font_size
fonts.rule_panel_font_size fonts.action_editor_font_size
fonts.rule_editor_font_size fonts.panel_row_font_size
fonts.panel_title_font_size fonts.panel_title_bold
fonts.rule_label_bold fonts.badge_bold
fonts.action_popup = { size, bold, italic }
mode_colors.edit_accent mode_colors.direct_accent
mode_colors.play_accent mode_colors.guide_line
```
For a migration, copy the values into the nested keys in this document and set
`"version": "2.0"`. Keep only one spelling of each setting so the result is
easy to maintain.
## Fonts, emoji, and troubleshooting
- Use project-relative paths such as `res://fonts/kids_font.ttf`; do not use a
machine-specific absolute path.
- If text falls back to the Godot default, check the path, import status, and
that the resource is actually a `Font`/font file. Restart the Sandbox after
correcting it.
- Emoji availability depends on the selected font and platform glyph support.
A missing `emoji_font` is non-fatal; the normal UI fallback remains usable.
- If a color or number is ignored, check JSON commas/quotes, use a supported
color form, and keep numbers finite. The warning identifies the affected key.
- If a selector or dialog looks cramped, increase its size array and its
related font size together; restart F6 to reload the file.
## Speech customization status
`fonts.presets` (`ui`, `comic`, `pixel`, `scary`), `widgets.size_presets`,
`widgets.swatches`, and `widgets.emoji` are the reusable data contract for a
future speech-customization UI. They are intentionally available now so a
future Speak dialog can consume the same theme.
The current Sandbox speech action still stores and renders text through the
existing `StickmanSpeechBubble`; these theme lists do not yet change saved
speech actions, bubble shape, bubble colors, or bubble font at runtime. Do not
assume that changing a preset changes an existing speech action. Treat
speech-specific fields under `popups.dialog` or future bubble sections as
reserved until that feature is implemented.
@@ -0,0 +1,46 @@
# Repository organization
## Maintained layout
| Path | Responsibility |
|---|---|
| `scenes/main.tscn` | Startup wrapper; configured as `run/main_scene` and hosts the Stickman Editor. |
| `scenes/editors/{stickman,prop,stage}/` | Feature editor scenes. |
| `scenes/stage/` | Sandbox Stage runtime scenes. |
| `scenes/characters/stickman/` | Stickman runtime rig scenes. |
| `scenes/tools/` | Test and physics harness scenes. |
| `scenes/legacy/` | Preserved legacy rig scenes. |
| `scripts/editors/{stickman,prop,stage}/` | Editor controllers and feature-specific editing systems. |
| `scripts/{stage,characters/stickman,props,terrain,speech}/` | Runtime and domain systems grouped by feature. |
| `scripts/shared/` | Reusable UI, thumbnail, and content-path utilities. |
| `scripts/tools/` | Harness and rig-authoring scripts. |
| `content/{stickmen,props,stages}/` | Project-managed `.stk`, `.prop`, and `.scene` libraries. |
| `resources/fonts/` | Project fonts. |
| `docs/{guides,architecture,specs,plans}/` | Maintained guides, architecture references, specifications, and active plans. |
| `docs/archive/` | Older project references and completed plan drafts. |
Scripts and scenes remain separate roots, with feature-specific editor code and scenes grouped by workflow. Godot `.gd.uid` files stay beside their scripts; scene resource UIDs are retained when scenes move.
## Content path compatibility
`ContentPaths` is implemented in `scripts/shared/content_paths.gd`. Its `canonicalize(path: String) -> String` helper maps project-relative legacy paths to the new library locations:
| Legacy project path | Canonical path |
|---|---|
| `res://stickmen/<file>.stk` | `res://content/stickmen/<file>.stk` |
| `res://props/<file>.prop` | `res://content/props/<file>.prop` |
| `res://game_scenes/<file>.scene` | `res://content/stages/<file>.scene` |
Equivalent project-relative inputs (for example, `stickmen/<file>.stk`) map to relative `content/...` paths, and absolute paths under the project root keep their absolute form while their legacy directory is rewritten. `user://` paths, unrelated relative paths, unknown project folders, and absolute paths outside the project are passed through unchanged. Keep path-bearing loads, saves, persisted references, recents, and recovery metadata routed through `canonicalize()` so existing project references continue to resolve after the move.
## Serialized file compatibility
The organization change moves libraries and code without changing the `.stk`, `.prop`, or `.scene` document schemas. It adds no serialized fields and requires no format-version bump. Existing field meanings and codec migrations remain authoritative:
| Format | Current codec version | Compatibility |
|---|---|---|
| `.stk` | `1.5` | The Stickman Editor accepts `1.0`–`1.4` files and migrates them on load. |
| `.prop` | `1.0` | `PropFile` accepts version `1.0`; missing supported optional fields use codec defaults. |
| `.scene` | `1.1` | `StageSceneIO` accepts `1.0` and migrates it to `1.1` on load. |
When a stage is loaded or saved, its document path and path-valued asset references are canonicalized. A legacy reference such as `res://stickmen/basic.stk` may therefore be serialized as `res://content/stickmen/basic.stk` on the next save. This changes the stored path value only: the `.scene` field structure and version remain unchanged, with no added keys.
@@ -0,0 +1,63 @@
# Speech System — As Built
**Updated:** 2026-10-03
This is the current implementation overview for speech authoring and playback. It records the reviewed UX and the behavior present in the stage scripts. The detailed specifications linked below provide payload and layout specifics; older plans and superseded proposals are historical context, not the current UI contract.
## Editor flow
`SandboxStage` opens a speech editing session anchored to the character and keeps a live bubble preview on the stage. `SpeechComposer` owns the editable draft, selected bubble, Message editor, recovery data, and preview clock. Stage-side controls are split by scope:
| Control | Scope | Current behavior |
| --- | --- | --- |
| **Speech Style** | Selected bubble | Message, Frame, Letters, and Effects; selecting a stage word targets that word, otherwise text/font/size/color/effect choices apply to the bubble. |
| **Show speech** | Whole sequence | Show all or One at a time, Stack or Same spot placement actions, and a separate Connect bubbles toggle. More speech settings contains reveal, entrance motion, timing, and Keep acting while talking. |
| **Preview speech** | Whole sequence | Plays the current draft on the stage without committing it. |
| **Done styling** | Style controls | Hides Speech Style while leaving the speech session open. |
| **Done speech / Cancel speech** | Editing session | Done commits the validated draft to its queue/rule destination. Cancel discards the session, with confirmation for changed drafts. |
Draft changes are recoverable while editing. They do not mutate the queued action until Done speech. Scene saving remains a separate operation after the in-memory speech commit.
## Bubble editing and presentation
- Each bubble has its own message, frame, font, size, text color, effects, and stage position. Numbered stage badges select bubbles; Earlier/Later changes their reading order without changing placement.
- Add bubble inserts after the selected bubble and carries over its bubble-level appearance defaults. The current cap is eight bubbles, with a hint at five. Remove is unavailable for the last bubble; Undo appears only after a removal and restores that bubble.
- Stack arranges the sequence upward from Bubble 1. Same spot aligns bubbles to Bubble 1's center. These are placement actions; Connect bubbles is a separate on/off presentation setting. With connections on, the sequence builds cumulatively: Bubble 1 points to the character and later balloons attach to the prior bubble. Think uses thought dots; SFX has no connector.
- Show all and One at a time remain the sequence visibility choices. Reveal supports all at once, letter by letter, and word by word. Entrance motion is Bounce in or Just appear; timing is Normal, Slow, or Slower. Reactive speech can use Keep acting while talking.
- The Message sub-dialog is attached to Speech Style and does not dim the stage. Its text field stays in place as the emoji grid opens and the dialog grows downward. Opening Message puts the caret at the end; emoji inserts at the caret.
- Text edits preserve valid word styling: edits within styled words retain styling on remaining/replacement content; inserted text inherits the preceding token's style, or the following token's style at the start. Bubble-level controls change only the selected bubble unless a stage word is targeted.
- Talk, Shout, and Whisper render a continuous filled body-and-tail contour. Think uses dots; SFX is tailless. Text is measured against the selected font and size with added inner padding, including larger Shout padding. Preview and runtime use the same generated bubble geometry; no external bubble SVG is required.
- Speech order badges remain screen-sized across camera zoom and do not move when selection changes. Crowded badges are sorted into a row/rail; intersecting bubble frames form a sorted row above the largest frame when space allows. Speech panels avoid the resolved badge bounds.
- Bubble dragging hit-tests the smallest bubble under the pointer, breaking equal-size ties in favor of the topmost bubble. A click where bubbles overlap still opens the bubble picker; clicking a badge selects that numbered bubble and brings it forward for editing.
## Data and rendering flow
Speech is stored as a normalized action `Dictionary` with action-wide sequence settings and an ordered `bubbles[]` array. Each bubble stores plain text, visual defaults, optional whole-word style spans, and placement. `SpeechPayload` validates and normalizes data before the stage preview, queue/rule routing, scene serialization, or runtime playback. Legacy single-text speech remains loadable; legacy comic-chain payloads are normalized to the current connection setting.
The editor preview and runtime playback share `SpeechBubble`. `StickmanActionRunner` owns runtime elapsed time and speech completion; `StageSceneIO` and `ActionRegistry` preserve speech fields in saved scenes and rule actions. The editor preview is separate from runner playback and does not advance or replace runtime speech state.
## Main implementation files
| File | Responsibility |
| --- | --- |
| `scripts/editors/stage/speech_composer.gd`, `scenes/editors/stage/speech_composer.tscn` | Draft editing, bubble management, Message/Emoji UI, recovery, style context, and preview state. |
| `scripts/speech/speech_style_toolbar.gd` | Bubble-scoped stage controls and attached choosers. |
| `scripts/speech/speech_show_toolbar.gd` | Whole-sequence controls, layout presets, preview/done/cancel, and More settings. |
| `scripts/speech/speech_payload.gd` | Payload defaults, validation, migration, word spans, and duration calculation. |
| `scripts/speech/stickman_speech_bubble.gd` | Shared preview/playback renderer, bubble measurement and geometry, word targeting, order badges, and bubble hit testing. |
| `scripts/editors/stage/sandbox_stage.gd` | Session lifecycle, stage input routing, toolbar placement, badge avoidance, and queue/rule commit. |
| `scripts/characters/stickman/stickman_action_runner.gd` | Runtime speech clocks and bubble ownership. |
| `scripts/stage/stage_scene_io.gd`, `scripts/stage/action_registry.gd` | Scene and rule serialization adapters. |
| `scripts/stage/sandbox_theme.gd`, `sandbox_theme.json`, `resources/fonts/` | Theme-backed speech font presets and per-font base sizes; Comic, Pixel, and Scary font assets are included. |
## Detailed behavior specifications
- [Whole-sequence Show controls](../docs/specs/speech_whole_sequence_show.md)
- [Speech Style toolbar](../docs/specs/speech_style_toolbar.md), [live editing](../docs/specs/speech_style_toolbar_live_edit.md), and [Message/Emoji/Timing](../docs/specs/speech_message_emoji_timing.md)
- [Bubble layout presets](../docs/specs/speech_bubble_layout_presets.md) and [bubble visuals](../docs/specs/speech_bubble_visuals.md)
- [Badge stability, grouping, and toolbar avoidance](../docs/specs/speech_badge_stability.md)
- [Phase 13 payload and playback details](../docs/specs/phase_13_speech_bubbles.md)
## Validation status
The user reviewed the recent badge clustering and overlapping-bubble drag behavior and reported that the speech editor looked good. Focused Godot syntax checks and `git diff --check` passed for those latest fixes. The saved `content/stages/talk.scene` had also loaded through the Sandbox Stage CLI without a skipped-speech warning during the earlier Phase 13 verification (2026-09-28). This does not claim a complete automated or GUI regression pass for every speech control and runtime mode.
@@ -0,0 +1,149 @@
# 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 | **Ragdoll Neck Angular Limits** — The ragdoll neck `PinJoint2D` remains unrestricted, so a violent physical tumble can still rotate the head freely. | Medium | Open | Phase 4.5 fixes the separate ANIMATED collision-flinch accumulation bug by bounding offsets from a captured base rotation; it does not change ragdoll neck limits. Consider a tuned angular band/stiffness when natural ragdoll head motion is revisited. |
| 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 self‑collision 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 limb‑limb 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 | **Animated Collision Proxy Approximation** — Stickman collision proxies use fixed-radius capsules for torso and limbs rather than exact mounted-shape polygons. | Low | Open | Phase 4.5 synchronizes ten proxies to the rendered parts and grows the head circle to cover mounted geometry (minimum radius 90 px), but torso/limb capsule radii remain approximate and do not adapt to arbitrary custom widths. Consider per-shape collision generation only if visible misses justify the complexity. (2026-09-12) |
| 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' rig‑local pose, snap‑solves 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 ease‑in‑out) — the baked `stand_up` animation is **not** played (a fixed first keyframe can never match an arbitrary rest pose; the earlier bridge‑into‑the‑animation 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). (2026‑08‑27) |
| 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 + pin‑softness ramp was **removed on director feedback** (it read as ghosting, since both poses are identical). Recovery snap‑solves the kinematic skeleton to the captured ragdoll pose before re‑showing `Body/*`, eliminating the pop on both directions. (2026‑08‑27) |
| 8 | **Rest Timeout UI** — The director can adjust `rest_timeout` via inspector, but there is no in‑world UI in the physics harness yet. | Low | ✅ Resolved | Phase 11: added a Rest `SpinBox` (0.1–10 s, step 0.1) to the harness UI that writes `_rig.rest_timeout` (runtime‑only), plus a "Recover Now" button → `_rig.request_recovery()`. (2026‑08‑27) |
| 9 | **Animation Generation DRY** — `create_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/tools/create_animations.gd` (`@tool extends EditorScript`) bakes `walk_left`/`walk_right` (same keyframes) and a one‑shot `stand_up` into the scene's `AnimationLibrary` (the baked `stand_up` is an authored reference only — runtime recovery tweens the IK targets directly). (2026‑08‑27) |
| 10 | **Rig Collision Proxy Re‑addition** — The proxy is re‑added on ragdoll exit, but may cause a brief visual pop if it appears while the kinematic rig is visible. | Low | Open | Phase 11 still re‑adds 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 stand‑up completes. Consider delaying re‑addition until after recovery finishes (`ANIMATED`). (2026‑08‑27) |
| 11 | **Stage Freeze Abstraction** — Sandbox Stage EDIT‑mode freezing is type‑specific: `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. `Area2D`‑based sensors) will need another case in `scripts/editors/stage/sandbox_stage.gd` `_enter_edit_mode()` / `_enter_play_mode()`. Consider a duck‑typed `set_simulating(bool)` interface once more physical object kinds appear. (2026‑08‑27) |
| 12 | **Stage AABB Selection Precision** — `StageSelection.get_world_aabb` uses conservative world‑space AABBs (polygon point union / fixed rig rect), not point‑in‑polygon. | Low | Open | Clicks in the bounding‑box corners of large or rotated terrain may select a block even outside its polygon, and overlapping blocks can mis‑select. Refine with `Geometry2D.is_point_in_polygon()` for `TerrainBlock`/`PropBlock` polygons (and circle distance for ball props) once selection precision matters. (2026‑08‑27) |
| 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 content/props/other stickmen are registered as obstacles. (2026‑08‑29) |
| 14 | **Phase 4 event engine is O(rigs×props + areas×movables) per physics frame** — `sandbox_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 visible-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. (2026‑08‑30 / note updated 2026‑09‑02) |
| 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 `docs/archive/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 `docs/archive/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 saved** — `StageSpawner.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` `PopupPanel`s, 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 point** — `QueuePanel` / `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/editors/stage/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_name`s 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/specs/phase_3c_editor_spec.md` §2 decision 5, not as a debt row. (2026-09-04) |
| 24 | **Sandbox rule references use session instance IDs at runtime** — trigger sources/targets and action actors use `Object.get_instance_id()` integers; StageSceneIO remaps these through per-save `file_id` values. | Low | Open | This is correct for the current in-memory engine and save/load remapping. Phase 11’s approved spec adds persistent per-instance prop `stage_object_id` values for queue/rule prop actions and upgrades the scene format to 1.1; implementation and compatibility verification remain open. (2026‑09‑11; spec decision 2026‑09‑24) |
| 25 | **Contact direct-state timing fallback** — `PropBlock` may receive `body_entered` before a matching direct-state contact row is available. | Low | Open | `_on_body_entered()` uses the documented center-direction / prop-position fallback when the row is absent, so the signal remains safe but the normal and position are less precise on that edge. (2026-09-12) |
| 26 | **Impact speed is a coarse magnitude** — collision reactions currently scale from `prop.linear_velocity.length()`, not normal-relative velocity or an impulse computed from contact mass. | Low | Open | This keeps the signal contract simple and deterministic. Use the normal component and/or contact impulse if reaction strength needs to reflect glancing impacts accurately. (2026-09-12) |
| 27 | **Post-IK reaction timing is engine-order-sensitive** — bounded flinch offsets are applied at the end of `StickmanRig._physics_process()`, after the IK/head-pin work. | Low | Open | The current slot is verified and prevents cumulative rotation, but future changes to Skeleton2D modification ordering could overwrite or delay the visible reaction. Revisit only with a targeted engine-order regression. (2026-09-12) |
| 28 | **Airborne collision sweep cost scales per proxy** — Phase 6a tests the horizontal movement against each of the rig's ten synchronized collision proxies every airborne physics frame. | Low | Open | The per-proxy `PhysicsShapeQueryParameters2D.cast_motion()` checks reuse the existing proxy shapes and avoid a second body-size approximation; this is acceptable for the current sandbox scale. With many simultaneously falling rigs, the cost grows with `rigs × 10` casts per physics frame. Consider a combined query shape or broadphase candidate filter if airborne crowds become a target. (2026-09-17) |
| 29 | **Prop Editor multi-shape → single-shape `PropBlock` flattening** — Phase 9 (`.prop` v1.0) stores a full multi-shape `shapes[]` stack, but `PropFile.flatten_to_payload()` collapses it into the one payload `PropUtils.spawn_prop()` consumes (frontmost closed polygon → circle → first shape). Also: a circle whose vertices were hand-edited still flattens by an average-distance radius, and a non-NONE preset thumbnail ignores payload colors because `PropThumbnail` re-tints from the preset. | Low | Open | Deliberate contract until a multi-shape `PropBlock` composition (multiple `Polygon2D`/`Line2D` children + merged collision) exists; recorded per `docs/specs/phase_9_prop_editor.md` §6. Custom `.prop` thumbnails are mtime-keyed (`ThumbnailCache.prop_file_key`/`prop_file_png`, `<basename>_<mtime>.png`), so re-edits invalidate the cached PNG; built-in template PNGs stay `id_v<PROP_VERSION>`-keyed. (2026-09-18) |
| 30 | **Prop Editor Round 2 interaction model** — vertex editing and shape transform are separated into two explicit canvas modes (`EditMode { TRANSFORM, POINTS }`, kid-facing "Move & Resize" / "Edit Points") driven by an always-visible toolbar toggle; "Create Point" inserts at the cursor's segment projection and flashes the new vertex (0.6 s) instead of inserting an invisible collinear midpoint. | Low | Open | Deliberate, user-approved design (`docs/specs/phase_9_prop_editor_round2.md` §5): modes gate *input* only — all public math APIs stay mode-agnostic. "Create Point" stays outline-only per user decision; auto-begin-drag was rejected as modal/surprising. Revisit only if kids find the two modes confusing in practice. (2026-09-18) |
| 31 | **Prop Editor Round 3 chrome/autosave decisions** — exit-save skips the thumbnail render (fast, deterministic quit; the PNG is a cache nicety), autosave is debounced 1.0 s to `user://prop_editor_autosave.prop` via `PropFile.save_safe` (atomic tmp/bak swap), and all human-facing strings (mode labels/emoji, menu copy) live in the `prop_editor.gd` controller while the canvas stays string-free. | Low | Open | User-approved (`docs/specs/phase_9_prop_editor_round3.md` §6). `save_safe` leaves a `.bak` window during the rename — acceptable on desktop, revisit if autosaves ever run on a filesystem without atomic rename. (2026-09-18) |
| 32 | **Prop Editor mode buttons drop keyboard focus** — `_make_mode_button` sets `focus_mode = FOCUS_NONE` because the Button `focus` stylebox draws after `pressed` and (with the themed `focus` = normal panel) masked the selected/accent state until the cursor moved away. | Low | Open | Round 4 fix (`docs/specs/phase_9_prop_editor_round4.md`). Trade-off: the Arrange/Reshape toggle is mouse-only. Revisit if keyboard accessibility for the mode toggle becomes a requirement — a focus stylebox matching the pressed state (or a focus-visible-only border) would restore it. (2026-09-19) |
| 33 | **Prop File save-serialization zeroed every point (fixed, Round 5)** — `PropFile._serialize_shape` assigned an export-format `Array` of `{x,y}` dicts directly to a typed `PackedVector2Array`; Godot's implicit conversion cannot build `Vector2` from a dictionary and silently yielded `Vector2.ZERO` per element, so every `.prop` saved by Round 1-4 wrote all points as `(0,0)` (loading as invisible/degenerate geometry). | High | ✅ Resolved | Round 5: `_serialize_shape` now routes points through `_coerce_points`; regression assertions check point *values* through `to_file_dict` and the full round-trip, plus an end-to-end canvas→save→load test. The previously saved `content/props/eye.prop` and `content/props/box.prop` are unrecoverable (coordinates were zeroed at save time) and were re-authored. Lesson: any typed `Packed*Array` assignment from dictionary/JSON data needs a Variant-coercion helper. (2026-09-19) |
| 34 | **Prop Editor per-shape `reshaped` flag** — every shape carries `reshaped` (false by default; set by vertex insert/remove/drag and non-uniform circle scaling, cleared by `set_shape_radius`), which gates primitive-specific inspector fields (today: the circle Radius row, replaced by a muted "reshaped" note). | Low | Open | Round 6 (`docs/specs/phase_9_prop_editor_round6.md`). Only the circle consumes the flag today; future primitive params (box W/H, plank length/thickness) should reuse it instead of adding per-type detection. An "un-reshape" affordance (restoring a clean primitive from its params) is a possible future feature. (2026-09-19) |
| 35 | **Prop Editor selection uses a two-tone halo** — after Round 6's fill-based contrast failed for the reported "shape in front of a white shape" case (the selection extends beyond the shape onto the background), every selection element (outline polyline, bbox, rotation dot, scale crosses, vertex handles) is drawn as a dark backing pass + bright core pass. | Low | Open | Round 7 (`docs/specs/phase_9_prop_editor_round7.md`). The pair is visible on any background but doubles the selection draw calls; negligible at sandbox scale. If a custom selection color is ever wanted (e.g. per-user theme), it should replace the core pass while keeping the halo. (2026-09-19) |
| 36 | **Phase 9a anchor roles have no gameplay consumers yet** — `.prop` assets can author/query `grip`, `emitter`, `impact`, `mount`, and `custom`, but Phase 9a is Prop Editor-only. Secondary grips do not drive two-hand IK, and no role dispatches runtime effects, damage, mounting, or equip behavior. | Medium | Open | User-approved staging in `docs/specs/phase_9a_prop_attach.md`: land a stable authoring/schema seam plus an isolated one-hand IK preview first. A separately approved future runtime phase must design custom-prop loading, equip/release ownership, two-hand IK, emitters, melee impacts, accessory mounting, throwing, and selector/stage persistence without assuming the editor preview is gameplay-authoritative. (2026-09-20) |
| 37 | **Prop Editor still has no document-wide undo/redo** — Phase 9a anchor edits use the existing dirty/autosave/recovery pipeline, but there is no `UndoRedo` history for either shapes or anchors. | Medium | Open | An anchor-only stack was explicitly rejected because Arrange/Reshape edits would remain non-undoable and the UI would be inconsistent. Future work should introduce one document-level command/history boundary covering shapes, anchors, material/name changes, and preset replacement together. (2026-09-20) |
| 38 | **Phase 9a preview does not establish runtime attachment parity** — the Prop Editor uses a private, non-physics, full-multi-shape rig preview with editor-only facing and z-order conventions. It intentionally proves anchor authoring and arm-IK evaluation, not Sandbox loading, collision, physics, mirroring, per-limb interleaving, or attachment lifecycle. | Medium | Open | A future runtime spec must independently validate the anchor transform against gameplay animation/physics and decide its visual representation, sockets or solved-wrist API, mirror strategy, collision/freeze behavior, per-limb draw order, release/ragdoll handling, and multi-shape fidelity. Do not treat preview acceptance tests as runtime tests. (2026-09-20) |
| 39 | **Phase 9b prop centering is geometric AABB centering, not physical center of mass** — `Center Prop` uses the union AABB of internal polygon/line/circle points and deliberately excludes outline expansion, anchors, material mass, and collision geometry. | Low | Open | This is deterministic and appropriate for the fixed authoring origin, but a future physics-pivot workflow may need a separately designed center-of-mass or collision-bounds command. Phase 9b keeps canonical `origin` fixed/serialized, hides it from editing, never auto-centers after edits, and uses an explicit-save warning heuristic of `max(1024 px, 2 × bounds diagonal)`. Document-wide undo remains tracked separately by #37. (2026-09-20) |
| 40 | **Phase 9c walking preview uses exclusive gait-or-manual ownership of the selected hand target** — the private AnimationPlayer owns the target while gait plays; beginning a cyan-target drag pauses the gait before pointer input takes ownership. | Low | Open | This deterministic policy prevents AnimationPlayer and manual IK from overwriting the same Marker2D in one frame. It means an author cannot add a live hand offset while the walk continues. If that becomes valuable, design a preview-only additive offset/blend layer rather than relying on process order. The gait remains an in-place, non-physics authoring preview and is not navigation/runtime parity (#38). (2026-09-21) |
| 41 | **Phase 10 custom prop collision uses one convex hull** — the planned Sandbox prop renders every authored layer, but collision encloses all valid closed-shape points in one solid hull. | Low | Planned | User-approved in `docs/specs/phase_10_prop_integration.md`: one fixture keeps dynamic rigid bodies stable and avoids overlapping per-shape contacts. Hollows and concavities collide as filled space; revisit compound collision only if playtesting shows material misses. `PropFile.flatten_to_payload()` remains for legacy single-shape consumers; #29 stays open until implementation lands. (2026-09-21) |
| 42 | **Phase 10 Browse keeps external `.prop` paths as scene asset IDs** — arbitrary filesystem files are used in place rather than copied into `res://content/props/`. | Medium | Planned | This matches stickman Browse and avoids filename/import conflicts, but scenes depend on the external path remaining valid. A missing custom prop becomes an inert placeholder that preserves its original scene entry and rule references through later saves; restoring the file and reloading resolves it. Existing missing-stickman rule skipping is unchanged. See `docs/specs/phase_10_prop_integration.md`. (2026-09-21) |
| 43 | **Phase 10 thumbnail invalidation relies on path plus mtime** — custom cache keys will hash the full canonical path and include file modification time. | Low | Planned | This prevents equal basenames in different folders from sharing a PNG, while retaining cheap refresh checks. A filesystem with coarse timestamp resolution can miss two same-tick edits; content hashing is deferred unless observed. Cleanup must not remove built-in or unrelated external thumbnails. See `docs/specs/phase_10_prop_integration.md`. (2026-09-21) |
| 44 | **Phase 10a Sandbox prop scaling depends on rigid-body collision scale behavior** — the approved corner gesture changes `PropBlock.scale`, so visual and physics bounds must agree for built-in circles/polygons and custom one-hull props. | Medium | Resolved | `PropBlock.begin_play_instance_scale()` transfers authored scale into child visuals and collision geometry during PLAY, including custom one-hull props and the nonuniform circle fallback, then `end_play_instance_scale()` restores the authored root scale in EDIT. `tests/test_phase10a_prop_scaling.gd` verifies scaled polygon and circle extents, rotated polygon contacts, dynamic PLAY contacts, and legacy nonuniform circle ellipse contacts. (2026-09-22) |
| 45 | **Phase 10a geometric normalization excludes stroke and physical mass distribution** — Normalize Size fits the union of shape points to 96 px on its longest axis, scaling about the fixed prop origin while leaving outline width unchanged. | Low | Planned | This preserves anchor offsets and current Center Prop AABB semantics; an off-origin prop remains proportionally off-origin. A future visual-stroke fit or center-of-mass sizing tool would require a separate contract. Existing #39 tracks AABB versus physical center; #37 tracks missing document-wide undo. See `docs/specs/phase_10a_prop_scaling.md`. (2026-09-21) |
| 46 | **Phase 12 live queue editing needs session-only action identity and history** — authored indices shift during edits, and a reactive takeover replaces the runner's unconsumed runtime slice. | Medium | ✅ Resolved | Implemented stable session action IDs and a separate COMPLETED/INTERRUPTED ledger. Reactive consequences remain transient; a canceled primary action stays INTERRUPTED and runs only on the next `t = 0` run. Pending actions alone can reorder, active remains fixed, and consumed entries are delete-only. IDs are not serialized. Verified during Phase 12 editor verification. (2026-09-24) |
| 47 | **Phase 12 pause requires explicit ownership of physics and procedural clocks** — ragdoll bodies, the runner, locomotion AnimationPlayer, and recovery tween have independent clocks. | Medium | ✅ Resolved | Pause/resume now snapshots and restores dynamic prop/ragdoll state, pauses runner and recovery-tween clocks, and resumes only locomotion animation that Phase 12 paused. A recovery already in progress stays in `RECOVERING` and completes after Resume; pausing does not rebuild a ragdoll. Stable ragdoll ownership supports multiple rigs. User verified the recovery-pause path and visible recovery result. (2026-09-24) |
| 48 | **Phase 12 paused paths use one representative ground anchor** — the nearest terrain ray below live feet or ragdoll bounds supplies the pending path origin. | Low | ✅ Resolved | Implemented the approved representative ground anchor, with a live-feet fallback when the probe finds no surface. Multi-contact path planning across a sprawled ragdoll and uneven terrain remains outside Phase 12, but the current anchor is stable and inspectable for paused queue authoring. (2026-09-24) |
| 49 | **Phase 12 event edges must be rebaselined without replay** — freezing bodies may emit contact exits/entries, and the stage polls area overlap before child rig movement. | Medium | ✅ Resolved | Event dispatch remains gated until a silent contact/overlap baseline is established on Resume. Existing overlaps do not refire because of the pause transition, and rules authored while paused apply to future events only. Reset clears runtime event edges. (2026-09-24) |
| 50 | **Phase 12 paused-edit autosave stores authored state, not the frozen snapshot** — queue/rule edits occur at `t > 0` while stage bodies are at temporary physical transforms. | Medium | ✅ Resolved | A 1.0 s debounce writes the existing `.scene` schema using authored object transforms plus current queues/rules. The timer can finish after Resume; runtime pose, velocities, reactive consequences, and action history are session-only. Paused edits therefore autosave without turning the live pose into the authored starting layout. (2026-09-24) |
| 51 | **Phase 11 throw distance and speed are shared across prop types** — the landing-target solver does not yet tune throwability by material or weight. | Low | Open | Deliberately deferred from the approved landing-target amendment. If playtesting needs it, add an explicit gameplay throwability factor based on prop weight/material; do not rely on rigid-body mass to change gravity or mutate physical mass for throw balance. Current shared limits are a 3,600 px target radius and 2,100 px/s speed cap. (2026-09-25) |
| 52 | **Throw target preview ignores earlier queued pickups** — the live preview requires `_held_prop`, so editing a pending throw after a pending pickup at `t = 0` cannot show the actual planned throw arc. | Medium | ✅ Resolved | The preview now infers a virtual held-prop state from the authored queue prefix and provides a pure, non-mutating planning trajectory using the prop/grip and predicted pickup stand-off. When a prop cannot be resolved, it shows a neutral explanation; runtime still recomputes from actual state. User verified the result. (2026-09-25) |
| 53 | **Phase 13 speech stacks may exceed very small stage viewports** — long simultaneous stacks compete with the side composer and stage chrome. | Low | Planned | The spec sets defensive eight-card and per-card text bounds, keeps frames near a stable screen size, fits the displayed group within the visible stage area without changing saved character-relative placement, and requires a visible overflow cue when a stack cannot fit legibly. Responsive paging for unusually small windows is deferred until observed in editor playtesting. See `docs/specs/phase_13_speech_bubbles.md`. (2026-09-26) |
| 54 | **Phase 13 font samples vary by platform until fonts are bundled** — OS `SystemFont` aliases can resolve Comic/Pixel/Scary choices to different families or generic fallbacks. | Low | Deferred | User explicitly deferred font bundling. Keep system-font previews now; a later task can add visually distinct, redistributable fonts with license/attribution files and point the speech presets to them. See `docs/specs/phase_13_speech_bubbles.md`. (2026-09-27) |
| 55 | **Speech styling has no general Undo history** — style choices update the recoverable draft immediately, but committed text, bubble order/placement, and styling share no undo stack. | Medium | Deferred | User chose immediate style application and deferred Undo for this release. Save speech commits the complete draft to the scene; composer Cancel discards the whole edit session. A future undo command boundary should cover the whole speech draft; token offsets must remain coherent after text edits. See `docs/specs/speech_style_toolbar_live_edit.md`. (2026-09-29) |
| 56 | **RichTextLabel has no dependable per-token 44 px hit rectangles for mixed-font speech** — exact metadata clicks can target narrow punctuation only at its rendered glyph. | Low | Planned | Use a temporary picker of tokens on the tapped rendered line, each with a 44 × 44 px hit area, while retaining exact metadata taps for comfortable words. Avoid estimating glyph boxes from raw string widths because shaping and wrapping can disagree. Revisit a dedicated token renderer only if editor playtesting shows the picker is insufficient. See `docs/specs/speech_style_toolbar.md`. (2026-09-28) |
| 57 | **Speech composition must temporarily own stage input** — normal scene controls can otherwise select or alter characters and props while the user is editing speech. | Medium | Planned | Gate stage object interactions while the composer is open; keep speech bubble selection/drag, explicit speech placement, and camera-only navigation available. A non-bubble stage click clears the token target but must not activate the object underneath. Verify deferred token clicks and drag thresholds. See `docs/specs/speech_style_toolbar_refinements.md`. (2026-09-28) |
| 58 | **Speech-wide Show settings retain per-bubble render copies** — action-level reveal and motion settings are mirrored into every bubble visual for the current renderer and duration calculations. | Low | Planned | `SpeechPayload.normalize()` is the synchronization boundary; scene and rule encoders must retain the new action-level fields. Older mixed bubble settings intentionally migrate from the first bubble when edited, so later distinct motion/reveal choices are no longer expressible in the group model. If a future renderer reads action-level settings directly, the duplicate visual keys can be removed in a versioned payload migration. See `docs/specs/speech_whole_sequence_show.md`. (2026-09-30) |
| 59 | **Stage-only speech edits remain drafts until session completion** — a recoverable draft can differ from its queue/rule target while the child is styling. | Medium | Planned | Done speech is the single validated in-memory commit boundary. A Scene Save request during composition is deferred until the user explicitly completes speech; it never commits implicitly or writes the stale queue/rule target. Cancel speech confirms before discarding an edited draft. This avoids premature persistence of blank bubbles and stale scene data. Recovery remains draft-only. See `docs/specs/speech_stage_only_editor.md`. (2026-09-30) |
---
## 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 mid‑recovery (e.g., if they want the character to ragdoll again immediately). | ✅ Done |
| **Multiple Rig Support** | Ensure all state variables are instance‑specific (already true) and that the harness can manage multiple rigs. | Future |
| **Animation Blending (IK vs. Physics)** | Blend between the animation‑driven 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 re‑addition) remains open with updated scope. Later revision: stand‑up recovery switched from bridge‑into‑baked‑animation 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 half‑heights with Torso `bone_angle` compensation, fixing the entry pose‑pop 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/editors/stage/sandbox_stage.gd` + `stage_spawner.gd` / `stage_selection.gd` / `stage_gizmos.gd` + `scenes/stage/sandbox_stage.tscn`. Logged #11 (no unified freeze abstraction over the mixed `StaticBody2D` / `RigidBody2D` / `Node2D` population) and #12 (selection hit‑testing uses world‑space AABBs rather than point‑in‑polygon). |
| 2026-08-29 | Phase 3a (Core Director Functionality) spec written (`docs/specs/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|direct`. Off-by-default `DEBUG_WALK` (`stickman_rig.gd`) / `DEBUG_STAGE` (`sandbox_stage.gd`) traces added. #13 remains **Open** (slope physics + RVO avoidance are still deferred; the fix only changes unreachable-target handling). Logged in `docs/known_issues.md`; verified with a 44-assertion headless regression suite. |
| 2026-08-30 | Phase 4 (Triggers & Event System) implemented: `scripts/stage/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 (`docs/archive/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/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/specs/phase_3b_asset_grid_spec.md`): `scripts/characters/stickman/stickman_library.gd` / `prop_library.gd`, `scripts/shared/thumbnails/thumbnail_cache.gd` / `stickman_thumbnail.gd` / `prop_thumbnail.gd`, `scripts/shared/ui/asset_selector.gd` + `scenes/stage/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/specs/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_changed` → `popup_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/stage/action_registry.gd` / `trigger_registry.gd` (extensible action/trigger templates), `scripts/editors/stage/action_editor.gd` / `rule_editor.gd` / `queue_panel.gd` / `rule_panel.gd` + `scenes/*.tscn` shells, `scripts/stage/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/specs/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. |
| 2026-09-11 | Phase 4c (Trigger Area Reliability) verified: `sandbox_stage.gd` `entered_area` now requires source + area instance IDs and detects visible world-AABB overlap for ANIMATED content/stickmen/unfrozen props, edge-triggered per area/movable pair. The prop enumeration scope bug was corrected. `tests/test_phase4c_area.gd` passed all 19 assertions, including transformed-area, leg-level overlap, wildcard/re-entry, speech consequence, stale-target, and deletion-cleanup coverage; editor verification confirmed the speech bubble fires for the configured source stickman. Logged #24 (session-only instance-ID rule references). |
| 2026-09-12 | Phase 4.5 collision bodies verified: ten explicitly synchronized `AnimatableBody2D` proxies replace the old feet-point stickman collision poll; `PropBlock` supplies world-space contact data with shape-cast CCD; stage routing preserves rig→prop rules with per-part pair debouncing; bounded quadrant-aware flinches and PLAY→DIRECT authored-pose restoration were verified. `tests/test_phase45_collision_bodies.gd` passed 223 assertions; Phase 4b (53), Phase 4c (19), and Phase 3c (96) remained green. Reconciled #1/#5 and retained #25–#27 as intentional implementation limitations. |
| 2026-09-17 | Phase 6a (Airborne Momentum, Procedural Air Pose, and Landing Absorption) implemented and user-verified. `StickmanRig` now carries signed ledge momentum with tuned exponential drag (`AIR_DRAG_X = 0.75`), swept horizontal proxy collision, additive free-fall pose with forward-before-lift arms, and height-driven landing absorption: falls below 80 px are soft; 80..900 px maps to 32..160 px torso compression and 270..520 px hand drop over 0.30 s. Independent foot contacts preserve ramps/stairs, while `LANDING_IMPACT` blocks primary queue progression and direct ragdoll remains an immediate preemption. `tests/test_phase6a_airborne_landing.gd` passed 24 assertions; `tests/test_phase4b_terrain.gd` passed 8 assertions. Logged #28 (per-proxy airborne sweep scaling). |
| 2026-09-20 | Phase 9a Prop Attach specification written and refined (`docs/specs/phase_9a_prop_attach.md`). Approved architecture: append Attach to the existing Arrange/Reshape system; additive role-based `.prop` anchors with a fixed synthesized `origin`; and an isolated Prop Editor stickman preview that uses real arm IK while showing the full authored prop. All Sandbox/custom-prop loading, attachment, sockets, physics/collision, lifecycle, and runtime tests are explicitly deferred. Logged #36 (all gameplay consumers deferred), #37 (document-wide undo remains absent), and #38 (editor preview is not a runtime-parity guarantee). Existing #29 remains open independently because the current runtime still flattens multi-shape props. |
| 2026-09-20 | Phase 9b Prop Centering specification written (`docs/specs/phase_9b_prop_centering.md`). Approved architecture: hide canonical origin from editable anchors while retaining it in `.prop` v1.0; draw a passive constant-screen-size `(0,0)` marker in all three modes; add an always-visible Center Prop document transaction; center the authoring camera on origin for startup/New/load/reset and preserve zoom after centering; and warn explicit saves with Center & Save / Save Anyway / Cancel when displacement exceeds `max(1024 px, 2 × bounds diagonal)`. Autosave stays silent and automatic recenter-on-edit is explicitly rejected. Logged #39 (AABB authoring center is not a future physics center of mass); #37 continues to track document-wide undo. |
| 2026-09-21 | Phase 9c Prop Editor UX specification written (`docs/specs/phase_9c_prop_editor_ux.md`). Approved architecture: a single Front Pose / Walk Left / Walk Right preview-motion selector with dynamic Pause/Resume; manual arm IK pauses gait before taking target ownership; modal typed anchor rename; Show Other Markers controls origin plus unselected anchors; one global Snap source; theme-driven `#262C39` inspector; container-driven unclipped chrome; simplified full-figure preview with no blue border. The preview directly plays the proven in-place gait/mirror policy and remains isolated from navigation, physics, actions, and Sandbox state. Logged #40 (exclusive gait/manual hand-target ownership). |
| 2026-09-21 | Phase 10 custom prop integration specification written (`docs/specs/phase_10_prop_integration.md`) after user approval of full layered visuals with one convex collision hull, direct external-path Browse, and missing-prop placeholders that preserve scene entries and rule references. The design reuses Sandbox prop freeze/restore and save flows, extends PropLibrary with a merged scan while retaining its four-template API, and gives custom thumbnails path-unique mtime keys. Logged #41–#43; #29 remains open pending implementation. |
| 2026-09-21 | Phase 10a Prop Scaling specification written (`docs/specs/phase_10a_prop_scaling.md`) after approval of origin-based Normalize Size and single-`PropBlock` uniform corner scaling around its local origin; an initial numeric document-scale control was removed during final UI refinement. The editor scales points, circle radius, and non-origin anchors as one transaction; a world-space 1:1 stickman guide defaults OFF and persists only in editor settings. Sandbox reuses its existing scene scale and PLAY→EDIT authored-state lifecycle. Logged #44 for physics collision verification and #45 for geometry-only fit; #37 and #39 remain open. |
| 2026-09-22 | Phase 10a whole-prop drag amendment approved (`docs/specs/phase_10a_prop_scaling.md` §3.4). In Arrange, a visible **Resize Whole Prop** toggle displays one union-bounds outline with large handles; dragging previews all shape points, circle radii, and non-origin anchors uniformly about canonical `(0,0)` from a captured snapshot. Release calls the existing document-scale transaction once; Esc, mode switch, toggle OFF, New, Load, and Clear discard a live preview. The preview stays outside authoritative save/autosave state, and the toggle adds no `.prop` field or fourth primary mode. Existing per-shape Arrange behavior remains when the toggle is OFF. Document-wide undo stays deferred under #37; this drag should become one future undo command. |
| 2026-09-22 | Phase 10a whole-prop transform extension approved and implemented (`docs/specs/phase_10a_prop_scaling.md` §3.4). The prior Resize Whole Prop toggle becomes one **Transform Whole Prop** tool that mirrors Arrange: drag visible geometry to move the whole document, drag the union rotation dot to rotate it about canonical `(0,0)`, and drag a corner for the existing uniform origin-based resize. Move translates every point and non-origin anchor (snapping the baseline delta when Snap is ON); rotate transforms every point/anchor position and adds the angle to editable anchor orientations; circle radius changes only on resize. All gestures preview from snapshots outside authoritative save state and commit once on release; Esc cancellation, selection restoration, mode lifecycle, `.prop` v1.0, and deferred document undo remain unchanged. |
| 2026-09-22 | Phase 10a implementation and final UI were user verified. The Arrange top bar is ordered **Transform Whole Prop**, **Normalize Size**, **Center Prop**, **Show Stickman Guide**; the numeric scale textbox and Apply Scale button are removed. Transform Whole Prop and Normalize Size are hidden in Reshape and Attach, while Center Prop and Show Stickman Guide remain available. Whole-prop move, 15-degree Ctrl rotation, uniform resize, anchor following, Snap movement, and Esc cancellation are documented in `docs/guides/prop_editor_user_guide.md`; focused tests and the Prop Editor startup smoke test passed. |
| 2026-09-24 | Phase 12 Live Director implemented and editor-verified. PLAY/DIRECT pause and resume preserve simulation time and live state; the top-bar clock is visible, Space toggles on key press, and paused DIRECT supports action/event editing without object transforms. Pending-only reorder, consumed-action deletion, interrupted-action history, contact rebaseline, and authored-state autosave are documented in the README and spec. Follow-up fixes make locomotion animation resume reliably, preserve a paused recovery tween instead of re-ragdolling, and clear stale foot-contact locks so recovery does not cross the feet. Rows #46–#50 resolved; no `.scene` format change. |
| 2026-09-24 | Phase 11 Prop Interactions architecture spec written (`docs/specs/phase_11_prop_interactions.md`) after approval of fixed stage-world throw targets, one prop in one compatible hand per stickman, and persistent per-instance prop IDs. The design adds a per-rig PropInteractor, keeps pickup auto-approach inside one authored queue action, carries existing .prop anchors into runtime, uses a bounded Godot-coordinate ballistic solver, emits events only after committed prop transitions, and migrates StageSceneIO 1.0 scenes to 1.1. Row #24 remains Open pending implementation. |
| 2026-09-25 | Phase 11 throw UX amendment approved (`docs/specs/phase_11_prop_interactions.md` §1.1): treat the saved cursor point as aim plus power (distance maps smoothly to a 300–1,250 px/s capped speed), preview the collision-limited ballistic impact, and replace the short windup with a reach-clamped overhand arc that releases during its forward sweep. The existing `{type, target_position}` queue payload remains unchanged. |
| 2026-09-25 | Phase 11 throw landing-target amendment approved (`docs/specs/phase_11_prop_interactions.md` §1.2): clamp the cursor's effective target to 800 px from the stickman root, solve the fastest valid descending trajectory under a 1,250 px/s cap, distinguish the raw cursor/effective target/early collision markers, and preserve the fixed world-space queue payload. Open-air targets remain valid; prop weight/material throwability is deferred under #51. |
| 2026-09-25 | Phase 11 throw-range tuning updated after user feedback: target radius 3,600 px and speed cap 2,100 px/s. The queue-aware, non-mutating target-preview amendment (`docs/specs/phase_11_prop_interactions.md` §1.3) was approved, implemented, and user verified; row #52 is resolved. Throw limits remain shared across prop types under open row #51. |
| 2026-09-26 | Phase 13 comic speech specification drafted for user sign-off (`docs/specs/phase_13_speech_bubbles.md`). Approved choices: a side composer with a live character-attached preview; simultaneous stacks by default or one-at-a-time transitions; automatic reading time with optional longer choices; character-relative placement; and reactive speech that temporarily takes visual priority, then resumes queued speech at its saved time. One tap targets one whole word; effect, font, and color controls apply to that word, while font/color controls edit bubble defaults with no target. The target remains visible while controls take focus; separate tapped words retain separate safe, validated style records. Drag highlighting, Shift-click, and multiselect/append are outside the interaction. The architecture uses one normalized Dictionary payload across queue/rule JSON, a shared RichTextLabel renderer, and runner-owned elapsed clocks for pause-safe replay. It preserves legacy `{text,duration}` actions. Logged #53 for tiny-viewport stack fit; arbitrary BBCode and per-bubble Resource files are deliberate scope exclusions, not open defects. |
| 2026-09-27 | Phase 13 composer refinement written for final user sign-off (`docs/specs/phase_13_speech_bubbles.md`): one active text editor with high-contrast snippet tabs and a vertical-only responsive inspector; example frame/font/size controls; 16 named text-color swatches; graphical whole-word target; separate Effects/Show/Timing groups; Show all/One at a time and Normal/Slow/Slower labels mapped to existing serialized IDs; clarified Bounce in/Simply appear; no placement arrows or auto-seconds label; live-head triangular tails with no line crossing the character; and a full replay followed by a one-second blank stage. User chose recoverable local drafts while retaining **Save speech** as the commit point. Draft recovery never mutates queues/rules/scene state; Save commits and clears, Cancel discards and clears, and stale targets recover only as an unbound draft pending an explicit destination. |
| 2026-09-27 | User approved the Phase 13 composer refinement for implementation. Font binaries are explicitly deferred; keep OS system-font aliases for the sample buttons and retain #54 as deferred. |
| 2026-09-27 | User approved the Phase 13 follow-up (`docs/specs/phase_13_speech_bubbles.md` §9): keep core inspector cards open with larger ribbon headers; clarify word targeting; fix full-word markers and selected-control states; give bubbles independent character-local placement and an above-character stagger that never fits downward across the actor; route scrolling to the composer under the pointer; and preserve valid speech through queue save/load while rejecting partial scene saves. Code is implemented, syntax checks passed, and in-editor verification is pending. |
| 2026-09-27 | Phase 13 verification follow-up: strengthened rounded word-marker coverage/clipping, added per-word size styling alongside font and color, and made speech commits confirm that the action actually entered the character queue before closing the composer. Syntax checks passed; verify save/reload and marker behavior in-editor. |
| 2026-09-28 | Fixed Phase 13 scene-load rejection for saved per-word styles: JSON parsing yields whole-number offsets as floats, while speech validation required `int`. Validation now accepts finite integral numeric offsets and normalization stores them as integers. `content/stages/talk.scene` loaded through the Sandbox Stage CLI without a skipped-speech warning; the scene file was not modified. |
| 2026-09-28 | Speech styling toolbar architecture spec written for final user sign-off (`docs/specs/speech_style_toolbar.md`). The stage-anchored Frame/Letters/Effects toolbar uses one theme-backed chooser and a provisional visual copy: Done commits a style to the composer draft; Cancel discards it without affecting recovery. A shared token scanner extends saved half-open style offsets to punctuation, and a temporary rendered-line token picker provides 44 px choices when glyphs are cramped. The inspector keeps Message and Save speech. User deferred general Undo; logged #55 and #56. No implementation code was changed in this phase. |
| 2026-09-28 | Speech style toolbar refinements approved and implemented (`docs/specs/speech_style_toolbar_refinements.md`). The chooser now sizes to content with overflow scrolling, ribbon headings, active categories, and centered large buttons; stage clicks clear the token target while preserving bubble selection, and speech composition blocks unrelated scene edits while retaining bubble controls and camera navigation. The full-sequence control is **Preview speech** beside Save speech. Godot headless syntax checks pass; in-editor verification is pending. |
| 2026-09-29 | Speech style live-edit refinement approved and implemented (`docs/specs/speech_style_toolbar_live_edit.md`). Choices now update the recoverable draft directly; token effects demo for one second without replaying the sequence; **Done styling** hides the toolbar while keeping speech editing open; selected choice borders persist on hover; and the token target uses a translucent rounded marker. Godot headless syntax checks pass; editor verification is pending. |
| 2026-09-29 | Speech style follow-up fixes: hold bubble geometry steady during the one-second token effect demo; center speech text after font/size changes; and make whole-bubble font/size/color defaults clear matching per-word overrides. Godot headless syntax checks pass; editor verification is pending. |
---
| 2026-09-29 | Speech Message/Emoji/Timing follow-up approved for implementation (docs/specs/speech_message_emoji_timing.md): move Message plus its emoji picker and action-wide Timing to the stage toolbar; use two Speech/Style rows; make unselected Effects apply to the whole bubble via a backward-compatible bubble default with token overrides; keep text editing separate from stage token targeting and preserve unaffected style spans. |
| 2026-09-30 | Stage speech-wide Show architecture specification written for user sign-off (`docs/specs/speech_whole_sequence_show.md`). No bubble focus presents Bubbles, Words appear, and Bubble motion directly beside the group; a bubble/token click swaps to Speech Style, while Done styling or an empty stage click returns to group controls. The action owns reveal and motion values, all existing/future bubbles inherit them, and older mixed settings migrate from the first bubble. Logged #58 for synchronized per-bubble render copies. No implementation code changed. |
| 2026-09-30 | Stage-only speech editor architecture specification drafted for user sign-off (`docs/specs/speech_stage_only_editor.md`). Removes the Comic Speech Inspector, moves Show/Timing/Preview/More to Show Speech and Message/Frame/Letters/Effects plus sequence controls to Speech Style, and keeps persistent Done speech and Cancel speech controls. Draft recovery remains immediate; Done commits once to the queue/rule buffer, while Scene Save waits for the explicit Done commit. New bubble defaults, placement, one-step removal Undo, order controls, input ownership, and the existing eight-bubble cap are specified. Logged #59. No implementation code changed. |
| 2026-10-01 | Comic Chain speech behavior revised after preview feedback (`docs/specs/speech_comic_chain_layout.md`): Comic chain is the third persistent **Show bubbles** choice and builds cumulatively, while Stack/Same spot remain placement actions. Entering the mode applies measured Stack positions. Later balloon connectors use blunt joins drawn behind predecessor frames; Think uses dots. Older `bubble_layout: comic_chain` payloads migrate to `arrangement: comic_chain`. |
| 2026-10-03 | Speech bubble order badges now use selection-independent positions (`docs/specs/speech_badge_stability.md`). Badge clustering responds to actual proximity; Show Speech and Speech Style panels avoid the stable badge bounds instead of moving badges when selection changes. |
| 2026-10-03 | Speech badge clusters now also form when bubble frames intersect. For overlapping bubbles, the sorted badge row anchors above the largest frame, preventing a smaller bubble's badge from appearing through the larger bubble's text. |
_End of Log_