From d950fd9d9acc6822b5db2801fef2cf4f6d071901 Mon Sep 17 00:00:00 2001 From: Ryan Kegel Date: Sat, 3 Oct 2026 19:13:39 -0400 Subject: [PATCH] Phase 14 - Organized directories. --- .codex/agents/architect.toml | 10 +- .codex/agents/writer.toml | 2 +- .codex/skills/spec-pipeline/SKILL.md | 4 +- .opencode/agents/architect.md | 4 +- .opencode/agents/writer.md | 2 +- .opencode/skills/spec-pipeline/SKILL.md | 2 +- AGENTS.md | 160 +++++----- README.md | 282 +++++++++--------- {props => content/props}/baseball.prop | 0 {props => content/props}/box.prop | 0 {props => content/props}/briefcase.prop | 0 {props => content/props}/eye.prop | 0 {game_scenes => content/stages}/bend.scene | 2 +- {game_scenes => content/stages}/manwalk.scene | 6 +- {game_scenes => content/stages}/ramp.scene | 2 +- {game_scenes => content/stages}/speech.scene | 2 +- {game_scenes => content/stages}/talk.scene | 2 +- .../stages}/test_fall.scene | 2 +- .../stages}/test_scene.scene | 4 +- .../stages}/test_scene2.scene | 2 +- .../stages}/test_scene3.scene | 2 +- .../stages}/throw_case.scene | 6 +- {stickmen => content/stickmen}/bad.stk | 0 {stickmen => content/stickmen}/basic.stk | 0 {stickmen => content/stickmen}/break.stk | 0 {stickmen => content/stickmen}/test.stk | 0 docs/README.md | 14 + docs/{ => architecture}/SANDBOX_THEME.md | 4 +- docs/architecture/repository_organization.md | 46 +++ .../speech_system_as_built.md | 30 +- .../tech_debt_and_optimizations.md | 120 ++++---- PROJECT.md => docs/archive/PROJECT.md | 0 ROADMAP.md => docs/archive/ROADMAP.md | 0 .../archive/plans}/COMMAND_LINE_SCENE.md | 0 .../archive/plans}/DYNAMIC_VECTOR_PROPS.md | 0 .../plans}/KINEMATIC_BLENDING_AND_RECOVERY.md | 0 .../archive/plans}/KINEMATIC_TO_RAGDOLL.md | 0 .../plans}/KINEMATIC_TO_RAGDOLL_SPEC.md | 0 .../plans}/PHASE_10_PROP_INTEGRATION.md | 0 .../archive/plans}/PHASE_10a_PROP_SCALING.md | 0 .../plans}/PHASE_11_PROP_INTERACTIONS.md | 0 .../archive/plans}/PHASE_12_LIVE_DIRECTOR.md | 0 .../archive/plans}/PHASE_13_SPEECH_BUBBLES.md | 0 .../archive/plans}/PHASE_3a_CORE_DIRECTOR.md | 0 .../archive/plans}/PHASE_3b_ASSET_GRID.md | 0 .../archive/plans}/PHASE_3c_EDITOR.md | 0 .../archive/plans}/PHASE_3c_SPEC.md | 0 .../plans}/PHASE_4.5_COLLISION_BODIES.md | 0 .../archive/plans}/PHASE_4_FINAL_BUGS.md | 0 .../archive/plans}/PHASE_4_TRIGGER_EVENTS.md | 0 .../archive/plans}/PHASE_4b_POLISH.md | 0 .../archive/plans}/PHASE_4b_POLISH.md.bugs.md | 0 .../archive/plans}/PHASE_4b_SPEC.md | 0 .../archive/plans}/PHASE_4c_AREA.md | 0 .../archive/plans}/PHASE_5_FILE_FEATURES.md | 0 .../archive/plans}/PHASE_6_GROUND_SNAPPING.md | 0 .../plans}/PHASE_6a_AIRBORNE_LANDING.md | 0 .../archive/plans}/PHASE_7_UI_POLISH.md | 0 .../plans}/PHASE_8_RIG_MODULARIZATION.md | 0 .../archive/plans}/PHASE_9_PROP_EDITOR.md | 0 .../archive/plans}/PHASE_9a_PROP_ATTACH.md | 0 .../archive/plans}/RAGDOLL_CLEANUP.md | 0 .../archive/plans}/SANDBOX_STAGE_BUILDER.md | 0 .../archive/plans}/VECTOR_TERRAIN_SYSTEM.md | 0 RIGGING.md => docs/guides/RIGGING.md | 10 +- docs/{ => guides}/prop_editor_user_guide.md | 6 +- BUGS.md => docs/known_issues.md | 20 +- docs/plans/README.md | 3 + .../specs}/phase-4.5-collision-bodies.md | 8 +- docs/{ => specs}/phase2_spec.md | 12 +- docs/{ => specs}/phase3_spec.md | 16 +- docs/{ => specs}/phase7_round1_spec.md | 22 +- docs/{ => specs}/phase7_spec.md | 12 +- docs/{ => specs}/phase8_spec.md | 38 +-- docs/{ => specs}/phase9_round1_bugfix_spec.md | 30 +- docs/{ => specs}/phase9_round2_bugfix_spec.md | 10 +- docs/{ => specs}/phase9_round3_bugfix_spec.md | 20 +- docs/{ => specs}/phase9_round4_bugfix_spec.md | 8 +- docs/{ => specs}/phase9_round5_bugfix_spec.md | 18 +- docs/{ => specs}/phase9_round6_bugfix_spec.md | 8 +- .../{ => specs}/phase9_round7_feature_spec.md | 10 +- docs/{ => specs}/phase9_task2_zorder_spec.md | 16 +- docs/{ => specs}/phase9_task3_coords_spec.md | 12 +- .../{ => specs}/phase9_task4_refactor_spec.md | 40 +-- .../phase9_task5_animation_spec.md | 40 +-- .../specs}/phase_10_prop_integration.md | 10 +- .../specs}/phase_10a_prop_scaling.md | 16 +- .../specs}/phase_11_prop_interactions.md | 2 +- .../specs}/phase_12_live_director.md | 18 +- .../specs}/phase_13_speech_bubbles.md | 8 +- docs/{ => specs}/phase_3a_spec.md | 28 +- docs/{ => specs}/phase_3b_asset_grid_spec.md | 76 ++--- docs/{ => specs}/phase_3c_editor_spec.md | 54 ++-- {specs => docs/specs}/phase_4_final_bugs.md | 20 +- {specs => docs/specs}/phase_4c_area.md | 14 +- .../specs}/phase_6a_airborne_landing.md | 12 +- {specs => docs/specs}/phase_7_ui_polish.md | 44 +-- .../specs}/phase_8_rig_modularization.md | 36 +-- {specs => docs/specs}/phase_9_prop_editor.md | 54 ++-- .../specs}/phase_9_prop_editor_round2.md | 22 +- .../specs}/phase_9_prop_editor_round3.md | 32 +- .../specs}/phase_9_prop_editor_round4.md | 10 +- .../specs}/phase_9_prop_editor_round5.md | 10 +- .../specs}/phase_9_prop_editor_round6.md | 6 +- .../specs}/phase_9_prop_editor_round7.md | 2 +- {specs => docs/specs}/phase_9a_prop_attach.md | 18 +- .../specs}/phase_9b_prop_centering.md | 14 +- .../specs}/phase_9c_prop_editor_ux.md | 16 +- {specs => docs/specs}/ragdoll-cleanup.md | 8 +- .../specs}/speech_badge_stability.md | 0 .../specs}/speech_bubble_layout_presets.md | 0 .../specs}/speech_bubble_visuals.md | 0 .../specs}/speech_comic_chain_layout.md | 0 .../specs}/speech_message_emoji_timing.md | 0 .../specs}/speech_stage_only_editor.md | 18 +- {specs => docs/specs}/speech_style_toolbar.md | 6 +- .../specs}/speech_style_toolbar_live_edit.md | 10 +- .../speech_style_toolbar_refinements.md | 8 +- .../specs}/speech_whole_sequence_show.md | 0 project.godot | 2 +- .../stickman}/stickman_rig.tscn | 10 +- .../prop}/prop_attach_preview.tscn | 2 +- scenes/{ => editors/prop}/prop_editor.tscn | 4 +- scenes/{ => editors/stage}/action_editor.tscn | 2 +- scenes/{ => editors/stage}/queue_panel.tscn | 2 +- scenes/{ => editors/stage}/rule_editor.tscn | 2 +- scenes/{ => editors/stage}/rule_panel.tscn | 2 +- .../{ => editors/stage}/speech_composer.tscn | 2 +- .../stickman}/body_part_panel.tscn | 2 +- .../stickman}/stickman_editor.tscn | 4 +- .../legacy/master_rig2.tscn | 0 stick.tscn => scenes/legacy/stick.tscn | 0 main.tscn => scenes/main.tscn | 2 +- scenes/{ => stage}/asset_selector.tscn | 2 +- scenes/{ => stage}/sandbox_stage.tscn | 2 +- scenes/{ => tools}/physics_test_harness.tscn | 2 +- scenes/{ => tools}/test_harness.tscn | 2 +- .../stickman}/missing_stickman_placeholder.gd | 0 .../missing_stickman_placeholder.gd.uid | 0 .../stickman}/stickman_action_runner.gd | 6 +- .../stickman}/stickman_action_runner.gd.uid | 0 .../stickman}/stickman_factory.gd | 6 +- .../stickman}/stickman_factory.gd.uid | 0 .../stickman}/stickman_library.gd | 4 +- .../stickman}/stickman_library.gd.uid | 0 .../stickman}/stickman_locomotion.gd | 0 .../stickman}/stickman_locomotion.gd.uid | 0 .../stickman}/stickman_prop_interactor.gd | 2 +- .../stickman}/stickman_prop_interactor.gd.uid | 0 .../stickman}/stickman_ragdoll.gd | 0 .../stickman}/stickman_ragdoll.gd.uid | 0 .../{ => characters/stickman}/stickman_rig.gd | 4 +- .../stickman}/stickman_rig.gd.uid | 0 .../stickman}/stk_rig_adapter.gd | 2 +- .../stickman}/stk_rig_adapter.gd.uid | 0 .../{ => editors/prop}/prop_attach_preview.gd | 10 +- .../prop}/prop_attach_preview.gd.uid | 0 scripts/{ => editors/prop}/prop_canvas.gd | 10 +- scripts/{ => editors/prop}/prop_canvas.gd.uid | 0 scripts/{ => editors/prop}/prop_editor.gd | 50 +++- scripts/{ => editors/prop}/prop_editor.gd.uid | 0 scripts/{ => editors/prop}/prop_file.gd | 12 +- scripts/{ => editors/prop}/prop_file.gd.uid | 0 scripts/{ => editors/prop}/prop_theme.gd | 2 +- scripts/{ => editors/prop}/prop_theme.gd.uid | 0 scripts/{ => editors/stage}/action_editor.gd | 2 +- .../{ => editors/stage}/action_editor.gd.uid | 0 scripts/{ => editors/stage}/queue_panel.gd | 6 +- .../{ => editors/stage}/queue_panel.gd.uid | 0 scripts/{ => editors/stage}/rule_editor.gd | 4 +- .../{ => editors/stage}/rule_editor.gd.uid | 0 scripts/{ => editors/stage}/rule_panel.gd | 4 +- scripts/{ => editors/stage}/rule_panel.gd.uid | 0 scripts/{ => editors/stage}/sandbox_stage.gd | 121 +++++--- .../{ => editors/stage}/sandbox_stage.gd.uid | 0 .../{ => editors/stage}/speech_composer.gd | 6 +- .../stage}/speech_composer.gd.uid | 0 .../{ => editors/stickman}/body_part_panel.gd | 0 .../stickman}/body_part_panel.gd.uid | 0 .../{ => editors/stickman}/stickman_editor.gd | 11 +- .../stickman}/stickman_editor.gd.uid | 0 .../stickman}/whole_stickman_preview.gd | 0 .../stickman}/whole_stickman_preview.gd.uid | 0 .../{ => props}/missing_prop_placeholder.gd | 0 .../missing_prop_placeholder.gd.uid | 0 scripts/{ => props}/prop_anchor_data.gd | 0 scripts/{ => props}/prop_anchor_data.gd.uid | 0 scripts/{ => props}/prop_anchor_overlay.gd | 2 +- .../{ => props}/prop_anchor_overlay.gd.uid | 0 scripts/{ => props}/prop_block.gd | 4 +- scripts/{ => props}/prop_block.gd.uid | 0 scripts/{ => props}/prop_library.gd | 11 +- scripts/{ => props}/prop_library.gd.uid | 0 scripts/{ => props}/prop_utils.gd | 4 +- scripts/{ => props}/prop_utils.gd.uid | 0 scripts/{ => props}/prop_visuals.gd | 0 scripts/{ => props}/prop_visuals.gd.uid | 0 scripts/shared/content_paths.gd | 55 ++++ scripts/shared/content_paths.gd.uid | 1 + .../{ => shared}/thumbnails/prop_thumbnail.gd | 4 +- .../thumbnails/prop_thumbnail.gd.uid | 0 .../thumbnails/stickman_thumbnail.gd | 4 +- .../thumbnails/stickman_thumbnail.gd.uid | 0 .../thumbnails/thumbnail_cache.gd | 0 .../thumbnails/thumbnail_cache.gd.uid | 0 scripts/{ => shared}/ui/asset_card.gd | 0 scripts/{ => shared}/ui/asset_card.gd.uid | 0 scripts/{ => shared/ui}/asset_selector.gd | 2 +- scripts/{ => shared/ui}/asset_selector.gd.uid | 0 .../{ => shared}/ui/color_swatch_selector.gd | 0 .../ui/color_swatch_selector.gd.uid | 0 scripts/{ => shared}/ui/emoji_quick_grid.gd | 0 .../{ => shared}/ui/emoji_quick_grid.gd.uid | 0 scripts/{ => shared}/ui/font_preset_picker.gd | 0 .../{ => shared}/ui/font_preset_picker.gd.uid | 0 scripts/{ => shared}/ui/preset_size_toggle.gd | 0 .../{ => shared}/ui/preset_size_toggle.gd.uid | 0 scripts/{ => speech}/speech_payload.gd | 0 scripts/{ => speech}/speech_payload.gd.uid | 0 scripts/{ => speech}/speech_show_toolbar.gd | 0 .../{ => speech}/speech_show_toolbar.gd.uid | 0 scripts/{ => speech}/speech_style_toolbar.gd | 4 +- .../{ => speech}/speech_style_toolbar.gd.uid | 0 scripts/{ => speech}/speech_word_effect.gd | 0 .../{ => speech}/speech_word_effect.gd.uid | 0 .../{ => speech}/stickman_speech_bubble.gd | 8 +- .../stickman_speech_bubble.gd.uid | 0 scripts/{ => stage}/action_registry.gd | 0 scripts/{ => stage}/action_registry.gd.uid | 0 scripts/{ => stage}/sandbox_theme.gd | 0 scripts/{ => stage}/sandbox_theme.gd.uid | 0 scripts/{ => stage}/stage_director_visuals.gd | 4 +- .../{ => stage}/stage_director_visuals.gd.uid | 0 scripts/{ => stage}/stage_gizmos.gd | 4 +- scripts/{ => stage}/stage_gizmos.gd.uid | 0 scripts/{ => stage}/stage_grid.gd | 0 scripts/{ => stage}/stage_grid.gd.uid | 0 .../{ => stage}/stage_placement_overlay.gd | 0 .../stage_placement_overlay.gd.uid | 0 scripts/{ => stage}/stage_scene_io.gd | 9 +- scripts/{ => stage}/stage_scene_io.gd.uid | 0 scripts/{ => stage}/stage_selection.gd | 0 scripts/{ => stage}/stage_selection.gd.uid | 0 scripts/{ => stage}/stage_spawner.gd | 18 +- scripts/{ => stage}/stage_spawner.gd.uid | 0 scripts/{ => stage}/trigger_area.gd | 0 scripts/{ => stage}/trigger_area.gd.uid | 0 scripts/{ => stage}/trigger_registry.gd | 0 scripts/{ => stage}/trigger_registry.gd.uid | 0 scripts/{ => stage}/waypoint_context.gd | 2 +- scripts/{ => stage}/waypoint_context.gd.uid | 0 scripts/{ => terrain}/terrain_block.gd | 0 scripts/{ => terrain}/terrain_block.gd.uid | 0 scripts/{ => terrain}/terrain_utils.gd | 0 scripts/{ => terrain}/terrain_utils.gd.uid | 0 clear_pose.gd => scripts/tools/clear_pose.gd | 0 .../tools/clear_pose.gd.uid | 0 scripts/{ => tools}/cli_scene_launcher.gd | 4 +- scripts/{ => tools}/cli_scene_launcher.gd.uid | 0 scripts/{ => tools}/create_animations.gd | 2 +- scripts/{ => tools}/create_animations.gd.uid | 0 scripts/{ => tools}/master_rig_builder.gd | 0 scripts/{ => tools}/master_rig_builder.gd.uid | 0 scripts/{ => tools}/physics_test_harness.gd | 12 +- .../{ => tools}/physics_test_harness.gd.uid | 0 scripts/{ => tools}/test_harness.gd | 16 +- scripts/{ => tools}/test_harness.gd.uid | 0 tests/test_cli_scene.gd | 4 +- tests/test_content_paths.gd | 120 ++++++++ tests/test_content_paths.gd.uid | 1 + tests/test_phase10_prop_integration.gd | 22 +- tests/test_phase10a_prop_scaling.gd | 10 +- tests/test_phase3b_library.gd | 36 +-- tests/test_phase3b_ui_fixes.gd | 8 +- tests/test_phase3c_bugfix.gd | 2 +- tests/test_phase3c_editor.gd | 26 +- tests/test_phase3c_walk_recovery.gd | 4 +- tests/test_phase45_collision_bodies.gd | 2 +- tests/test_phase4_final_bugs.gd | 20 +- tests/test_phase4b1_fixes.gd | 12 +- tests/test_phase4b2_fixes.gd | 10 +- tests/test_phase4b_grid_dirty.gd | 8 +- tests/test_phase4b_logic.gd | 8 +- tests/test_phase4b_popup_anchor.gd | 6 +- tests/test_phase4b_stage.gd | 6 +- tests/test_phase4b_terrain.gd | 6 +- tests/test_phase4b_walk.gd | 6 +- tests/test_phase4c_area.gd | 6 +- tests/test_phase5_file_features.gd | 10 +- tests/test_phase6a_airborne_landing.gd | 6 +- tests/test_phase7_palette.gd | 4 +- tests/test_phase7_theme.gd | 2 +- tests/test_phase7_ui_followup.gd | 2 +- tests/test_phase7_widgets.gd | 8 +- tests/test_phase8_edit_direct_pose.gd | 2 +- tests/test_phase8_ragdoll_walk_handoff.gd | 2 +- tests/test_phase8_rig_modularization.gd | 2 +- tests/test_phase9a_prop_attach.gd | 10 +- tests/test_phase9b_prop_centering.gd | 6 +- tests/test_phase9c_prop_editor_ux.gd | 10 +- tests/test_prop_editor_canvas.gd | 2 +- tests/test_prop_editor_file.gd | 6 +- tests/test_prop_editor_round3.gd | 26 +- tests/test_prop_editor_theme.gd | 4 +- tests/test_prop_editor_thumb.gd | 2 +- tests/test_text_baseline_fix.gd | 8 +- tools/sync_doc/sync_sources.py | 10 +- 307 files changed, 1452 insertions(+), 1110 deletions(-) rename {props => content/props}/baseball.prop (100%) rename {props => content/props}/box.prop (100%) rename {props => content/props}/briefcase.prop (100%) rename {props => content/props}/eye.prop (100%) rename {game_scenes => content/stages}/bend.scene (98%) rename {game_scenes => content/stages}/manwalk.scene (98%) rename {game_scenes => content/stages}/ramp.scene (99%) rename {game_scenes => content/stages}/speech.scene (97%) rename {game_scenes => content/stages}/talk.scene (98%) rename {game_scenes => content/stages}/test_fall.scene (99%) rename {game_scenes => content/stages}/test_scene.scene (98%) rename {game_scenes => content/stages}/test_scene2.scene (99%) rename {game_scenes => content/stages}/test_scene3.scene (99%) rename {game_scenes => content/stages}/throw_case.scene (98%) rename {stickmen => content/stickmen}/bad.stk (100%) rename {stickmen => content/stickmen}/basic.stk (100%) rename {stickmen => content/stickmen}/break.stk (100%) rename {stickmen => content/stickmen}/test.stk (100%) create mode 100644 docs/README.md rename docs/{ => architecture}/SANDBOX_THEME.md (99%) create mode 100644 docs/architecture/repository_organization.md rename docs/{ => architecture}/speech_system_as_built.md (71%) rename docs/{ => architecture}/tech_debt_and_optimizations.md (64%) rename PROJECT.md => docs/archive/PROJECT.md (100%) rename ROADMAP.md => docs/archive/ROADMAP.md (100%) rename {plans => docs/archive/plans}/COMMAND_LINE_SCENE.md (100%) rename {plans => docs/archive/plans}/DYNAMIC_VECTOR_PROPS.md (100%) rename {plans => docs/archive/plans}/KINEMATIC_BLENDING_AND_RECOVERY.md (100%) rename {plans => docs/archive/plans}/KINEMATIC_TO_RAGDOLL.md (100%) rename {plans => docs/archive/plans}/KINEMATIC_TO_RAGDOLL_SPEC.md (100%) rename {plans => docs/archive/plans}/PHASE_10_PROP_INTEGRATION.md (100%) rename {plans => docs/archive/plans}/PHASE_10a_PROP_SCALING.md (100%) rename {plans => docs/archive/plans}/PHASE_11_PROP_INTERACTIONS.md (100%) rename {plans => docs/archive/plans}/PHASE_12_LIVE_DIRECTOR.md (100%) rename {plans => docs/archive/plans}/PHASE_13_SPEECH_BUBBLES.md (100%) rename {plans => docs/archive/plans}/PHASE_3a_CORE_DIRECTOR.md (100%) rename {plans => docs/archive/plans}/PHASE_3b_ASSET_GRID.md (100%) rename {plans => docs/archive/plans}/PHASE_3c_EDITOR.md (100%) rename {plans => docs/archive/plans}/PHASE_3c_SPEC.md (100%) rename {plans => docs/archive/plans}/PHASE_4.5_COLLISION_BODIES.md (100%) rename {plans => docs/archive/plans}/PHASE_4_FINAL_BUGS.md (100%) rename {plans => docs/archive/plans}/PHASE_4_TRIGGER_EVENTS.md (100%) rename {plans => docs/archive/plans}/PHASE_4b_POLISH.md (100%) rename {plans => docs/archive/plans}/PHASE_4b_POLISH.md.bugs.md (100%) rename {plans => docs/archive/plans}/PHASE_4b_SPEC.md (100%) rename {plans => docs/archive/plans}/PHASE_4c_AREA.md (100%) rename {plans => docs/archive/plans}/PHASE_5_FILE_FEATURES.md (100%) rename {plans => docs/archive/plans}/PHASE_6_GROUND_SNAPPING.md (100%) rename {plans => docs/archive/plans}/PHASE_6a_AIRBORNE_LANDING.md (100%) rename {plans => docs/archive/plans}/PHASE_7_UI_POLISH.md (100%) rename {plans => docs/archive/plans}/PHASE_8_RIG_MODULARIZATION.md (100%) rename {plans => docs/archive/plans}/PHASE_9_PROP_EDITOR.md (100%) rename {plans => docs/archive/plans}/PHASE_9a_PROP_ATTACH.md (100%) rename {plans => docs/archive/plans}/RAGDOLL_CLEANUP.md (100%) rename {plans => docs/archive/plans}/SANDBOX_STAGE_BUILDER.md (100%) rename {plans => docs/archive/plans}/VECTOR_TERRAIN_SYSTEM.md (100%) rename RIGGING.md => docs/guides/RIGGING.md (88%) rename docs/{ => guides}/prop_editor_user_guide.md (99%) rename BUGS.md => docs/known_issues.md (82%) create mode 100644 docs/plans/README.md rename {specs => docs/specs}/phase-4.5-collision-bodies.md (98%) rename docs/{ => specs}/phase2_spec.md (91%) rename docs/{ => specs}/phase3_spec.md (95%) rename docs/{ => specs}/phase7_round1_spec.md (91%) rename docs/{ => specs}/phase7_spec.md (95%) rename docs/{ => specs}/phase8_spec.md (88%) rename docs/{ => specs}/phase9_round1_bugfix_spec.md (90%) rename docs/{ => specs}/phase9_round2_bugfix_spec.md (94%) rename docs/{ => specs}/phase9_round3_bugfix_spec.md (92%) rename docs/{ => specs}/phase9_round4_bugfix_spec.md (91%) rename docs/{ => specs}/phase9_round5_bugfix_spec.md (90%) rename docs/{ => specs}/phase9_round6_bugfix_spec.md (94%) rename docs/{ => specs}/phase9_round7_feature_spec.md (92%) rename docs/{ => specs}/phase9_task2_zorder_spec.md (91%) rename docs/{ => specs}/phase9_task3_coords_spec.md (91%) rename docs/{ => specs}/phase9_task4_refactor_spec.md (92%) rename docs/{ => specs}/phase9_task5_animation_spec.md (88%) rename {specs => docs/specs}/phase_10_prop_integration.md (93%) rename {specs => docs/specs}/phase_10a_prop_scaling.md (89%) rename {specs => docs/specs}/phase_11_prop_interactions.md (99%) rename {specs => docs/specs}/phase_12_live_director.md (82%) rename {specs => docs/specs}/phase_13_speech_bubbles.md (94%) rename docs/{ => specs}/phase_3a_spec.md (97%) rename docs/{ => specs}/phase_3b_asset_grid_spec.md (89%) rename docs/{ => specs}/phase_3c_editor_spec.md (92%) rename {specs => docs/specs}/phase_4_final_bugs.md (97%) rename {specs => docs/specs}/phase_4c_area.md (91%) rename {specs => docs/specs}/phase_6a_airborne_landing.md (95%) rename {specs => docs/specs}/phase_7_ui_polish.md (92%) rename {specs => docs/specs}/phase_8_rig_modularization.md (88%) rename {specs => docs/specs}/phase_9_prop_editor.md (88%) rename {specs => docs/specs}/phase_9_prop_editor_round2.md (88%) rename {specs => docs/specs}/phase_9_prop_editor_round3.md (93%) rename {specs => docs/specs}/phase_9_prop_editor_round4.md (59%) rename {specs => docs/specs}/phase_9_prop_editor_round5.md (68%) rename {specs => docs/specs}/phase_9_prop_editor_round6.md (83%) rename {specs => docs/specs}/phase_9_prop_editor_round7.md (92%) rename {specs => docs/specs}/phase_9a_prop_attach.md (98%) rename {specs => docs/specs}/phase_9b_prop_centering.md (98%) rename {specs => docs/specs}/phase_9c_prop_editor_ux.md (96%) rename {specs => docs/specs}/ragdoll-cleanup.md (98%) rename {specs => docs/specs}/speech_badge_stability.md (100%) rename {specs => docs/specs}/speech_bubble_layout_presets.md (100%) rename {specs => docs/specs}/speech_bubble_visuals.md (100%) rename {specs => docs/specs}/speech_comic_chain_layout.md (100%) rename {specs => docs/specs}/speech_message_emoji_timing.md (100%) rename {specs => docs/specs}/speech_stage_only_editor.md (85%) rename {specs => docs/specs}/speech_style_toolbar.md (94%) rename {specs => docs/specs}/speech_style_toolbar_live_edit.md (89%) rename {specs => docs/specs}/speech_style_toolbar_refinements.md (89%) rename {specs => docs/specs}/speech_whole_sequence_show.md (100%) rename scenes/{ => characters/stickman}/stickman_rig.tscn (97%) rename scenes/{ => editors/prop}/prop_attach_preview.tscn (68%) rename scenes/{ => editors/prop}/prop_editor.tscn (96%) rename scenes/{ => editors/stage}/action_editor.tscn (54%) rename scenes/{ => editors/stage}/queue_panel.tscn (54%) rename scenes/{ => editors/stage}/rule_editor.tscn (54%) rename scenes/{ => editors/stage}/rule_panel.tscn (54%) rename scenes/{ => editors/stage}/speech_composer.tscn (53%) rename scenes/{ => editors/stickman}/body_part_panel.tscn (95%) rename scenes/{ => editors/stickman}/stickman_editor.tscn (97%) rename master_rig2.tscn => scenes/legacy/master_rig2.tscn (100%) rename stick.tscn => scenes/legacy/stick.tscn (100%) rename main.tscn => scenes/main.tscn (86%) rename scenes/{ => stage}/asset_selector.tscn (96%) rename scenes/{ => stage}/sandbox_stage.tscn (87%) rename scenes/{ => tools}/physics_test_harness.tscn (87%) rename scenes/{ => tools}/test_harness.tscn (86%) rename scripts/{ => characters/stickman}/missing_stickman_placeholder.gd (100%) rename scripts/{ => characters/stickman}/missing_stickman_placeholder.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_action_runner.gd (98%) rename scripts/{ => characters/stickman}/stickman_action_runner.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_factory.gd (86%) rename scripts/{ => characters/stickman}/stickman_factory.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_library.gd (92%) rename scripts/{ => characters/stickman}/stickman_library.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_locomotion.gd (100%) rename scripts/{ => characters/stickman}/stickman_locomotion.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_prop_interactor.gd (99%) rename scripts/{ => characters/stickman}/stickman_prop_interactor.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_ragdoll.gd (100%) rename scripts/{ => characters/stickman}/stickman_ragdoll.gd.uid (100%) rename scripts/{ => characters/stickman}/stickman_rig.gd (99%) rename scripts/{ => characters/stickman}/stickman_rig.gd.uid (100%) rename scripts/{ => characters/stickman}/stk_rig_adapter.gd (99%) rename scripts/{ => characters/stickman}/stk_rig_adapter.gd.uid (100%) rename scripts/{ => editors/prop}/prop_attach_preview.gd (97%) rename scripts/{ => editors/prop}/prop_attach_preview.gd.uid (100%) rename scripts/{ => editors/prop}/prop_canvas.gd (99%) rename scripts/{ => editors/prop}/prop_canvas.gd.uid (100%) rename scripts/{ => editors/prop}/prop_editor.gd (96%) rename scripts/{ => editors/prop}/prop_editor.gd.uid (100%) rename scripts/{ => editors/prop}/prop_file.gd (96%) rename scripts/{ => editors/prop}/prop_file.gd.uid (100%) rename scripts/{ => editors/prop}/prop_theme.gd (99%) rename scripts/{ => editors/prop}/prop_theme.gd.uid (100%) rename scripts/{ => editors/stage}/action_editor.gd (99%) rename scripts/{ => editors/stage}/action_editor.gd.uid (100%) rename scripts/{ => editors/stage}/queue_panel.gd (97%) rename scripts/{ => editors/stage}/queue_panel.gd.uid (100%) rename scripts/{ => editors/stage}/rule_editor.gd (98%) rename scripts/{ => editors/stage}/rule_editor.gd.uid (100%) rename scripts/{ => editors/stage}/rule_panel.gd (98%) rename scripts/{ => editors/stage}/rule_panel.gd.uid (100%) rename scripts/{ => editors/stage}/sandbox_stage.gd (97%) rename scripts/{ => editors/stage}/sandbox_stage.gd.uid (100%) rename scripts/{ => editors/stage}/speech_composer.gd (99%) rename scripts/{ => editors/stage}/speech_composer.gd.uid (100%) rename scripts/{ => editors/stickman}/body_part_panel.gd (100%) rename scripts/{ => editors/stickman}/body_part_panel.gd.uid (100%) rename scripts/{ => editors/stickman}/stickman_editor.gd (97%) rename scripts/{ => editors/stickman}/stickman_editor.gd.uid (100%) rename scripts/{ => editors/stickman}/whole_stickman_preview.gd (100%) rename scripts/{ => editors/stickman}/whole_stickman_preview.gd.uid (100%) rename scripts/{ => props}/missing_prop_placeholder.gd (100%) rename scripts/{ => props}/missing_prop_placeholder.gd.uid (100%) rename scripts/{ => props}/prop_anchor_data.gd (100%) rename scripts/{ => props}/prop_anchor_data.gd.uid (100%) rename scripts/{ => props}/prop_anchor_overlay.gd (98%) rename scripts/{ => props}/prop_anchor_overlay.gd.uid (100%) rename scripts/{ => props}/prop_block.gd (98%) rename scripts/{ => props}/prop_block.gd.uid (100%) rename scripts/{ => props}/prop_library.gd (91%) rename scripts/{ => props}/prop_library.gd.uid (100%) rename scripts/{ => props}/prop_utils.gd (97%) rename scripts/{ => props}/prop_utils.gd.uid (100%) rename scripts/{ => props}/prop_visuals.gd (100%) rename scripts/{ => props}/prop_visuals.gd.uid (100%) create mode 100644 scripts/shared/content_paths.gd create mode 100644 scripts/shared/content_paths.gd.uid rename scripts/{ => shared}/thumbnails/prop_thumbnail.gd (96%) rename scripts/{ => shared}/thumbnails/prop_thumbnail.gd.uid (100%) rename scripts/{ => shared}/thumbnails/stickman_thumbnail.gd (92%) rename scripts/{ => shared}/thumbnails/stickman_thumbnail.gd.uid (100%) rename scripts/{ => shared}/thumbnails/thumbnail_cache.gd (100%) rename scripts/{ => shared}/thumbnails/thumbnail_cache.gd.uid (100%) rename scripts/{ => shared}/ui/asset_card.gd (100%) rename scripts/{ => shared}/ui/asset_card.gd.uid (100%) rename scripts/{ => shared/ui}/asset_selector.gd (98%) rename scripts/{ => shared/ui}/asset_selector.gd.uid (100%) rename scripts/{ => shared}/ui/color_swatch_selector.gd (100%) rename scripts/{ => shared}/ui/color_swatch_selector.gd.uid (100%) rename scripts/{ => shared}/ui/emoji_quick_grid.gd (100%) rename scripts/{ => shared}/ui/emoji_quick_grid.gd.uid (100%) rename scripts/{ => shared}/ui/font_preset_picker.gd (100%) rename scripts/{ => shared}/ui/font_preset_picker.gd.uid (100%) rename scripts/{ => shared}/ui/preset_size_toggle.gd (100%) rename scripts/{ => shared}/ui/preset_size_toggle.gd.uid (100%) rename scripts/{ => speech}/speech_payload.gd (100%) rename scripts/{ => speech}/speech_payload.gd.uid (100%) rename scripts/{ => speech}/speech_show_toolbar.gd (100%) rename scripts/{ => speech}/speech_show_toolbar.gd.uid (100%) rename scripts/{ => speech}/speech_style_toolbar.gd (99%) rename scripts/{ => speech}/speech_style_toolbar.gd.uid (100%) rename scripts/{ => speech}/speech_word_effect.gd (100%) rename scripts/{ => speech}/speech_word_effect.gd.uid (100%) rename scripts/{ => speech}/stickman_speech_bubble.gd (99%) rename scripts/{ => speech}/stickman_speech_bubble.gd.uid (100%) rename scripts/{ => stage}/action_registry.gd (100%) rename scripts/{ => stage}/action_registry.gd.uid (100%) rename scripts/{ => stage}/sandbox_theme.gd (100%) rename scripts/{ => stage}/sandbox_theme.gd.uid (100%) rename scripts/{ => stage}/stage_director_visuals.gd (99%) rename scripts/{ => stage}/stage_director_visuals.gd.uid (100%) rename scripts/{ => stage}/stage_gizmos.gd (98%) rename scripts/{ => stage}/stage_gizmos.gd.uid (100%) rename scripts/{ => stage}/stage_grid.gd (100%) rename scripts/{ => stage}/stage_grid.gd.uid (100%) rename scripts/{ => stage}/stage_placement_overlay.gd (100%) rename scripts/{ => stage}/stage_placement_overlay.gd.uid (100%) rename scripts/{ => stage}/stage_scene_io.gd (97%) rename scripts/{ => stage}/stage_scene_io.gd.uid (100%) rename scripts/{ => stage}/stage_selection.gd (100%) rename scripts/{ => stage}/stage_selection.gd.uid (100%) rename scripts/{ => stage}/stage_spawner.gd (94%) rename scripts/{ => stage}/stage_spawner.gd.uid (100%) rename scripts/{ => stage}/trigger_area.gd (100%) rename scripts/{ => stage}/trigger_area.gd.uid (100%) rename scripts/{ => stage}/trigger_registry.gd (100%) rename scripts/{ => stage}/trigger_registry.gd.uid (100%) rename scripts/{ => stage}/waypoint_context.gd (94%) rename scripts/{ => stage}/waypoint_context.gd.uid (100%) rename scripts/{ => terrain}/terrain_block.gd (100%) rename scripts/{ => terrain}/terrain_block.gd.uid (100%) rename scripts/{ => terrain}/terrain_utils.gd (100%) rename scripts/{ => terrain}/terrain_utils.gd.uid (100%) rename clear_pose.gd => scripts/tools/clear_pose.gd (100%) rename clear_pose.gd.uid => scripts/tools/clear_pose.gd.uid (100%) rename scripts/{ => tools}/cli_scene_launcher.gd (91%) rename scripts/{ => tools}/cli_scene_launcher.gd.uid (100%) rename scripts/{ => tools}/create_animations.gd (98%) rename scripts/{ => tools}/create_animations.gd.uid (100%) rename scripts/{ => tools}/master_rig_builder.gd (100%) rename scripts/{ => tools}/master_rig_builder.gd.uid (100%) rename scripts/{ => tools}/physics_test_harness.gd (95%) rename scripts/{ => tools}/physics_test_harness.gd.uid (100%) rename scripts/{ => tools}/test_harness.gd (98%) rename scripts/{ => tools}/test_harness.gd.uid (100%) create mode 100644 tests/test_content_paths.gd create mode 100644 tests/test_content_paths.gd.uid diff --git a/.codex/agents/architect.toml b/.codex/agents/architect.toml index 4cf96e6..84f9e34 100644 --- a/.codex/agents/architect.toml +++ b/.codex/agents/architect.toml @@ -11,14 +11,14 @@ You are the Lead Systems Architect specializing in Godot 4 and idiomatic GDScrip 1. **Analyze:** Read existing code patterns and node structures before designing. 2. **Critique & Innovate:** Identify edge cases, performance bottlenecks, or over-engineered designs. Offer superior GDScript patterns (e.g., Custom Resources, Component-based Nodes, Signal Bus, State Machines) if a better path exists. 3. **Design:** Define API payload shapes, static GDScript type hints, node hierarchies, and signal flows. -4. **Log:** Append trade-offs to `docs/tech_debt_and_optimizations.md`. +4. **Log:** Append trade-offs to `docs/architecture/tech_debt_and_optimizations.md`. --- ## TWO-PHASE WORKFLOW ### PHASE 1: Scope, Critique & Pitch (DEFAULT FIRST TURN) -Unless explicitly instructed by the prompt to "Skip clarification and build spec immediately," DO NOT write to `specs/[feature-name].md` during your first response. +Unless explicitly instructed by the prompt to "Skip clarification and build spec immediately," DO NOT write to `docs/specs/[feature-name].md` during your first response. Instead, analyze the requirement and output a **Clarification & Architecture Brief**: @@ -38,7 +38,7 @@ Instead, analyze the requirement and output a **Clarification & Architecture Bri ### PHASE 2: Spec Creation & Handoff (AFTER USER FEEDBACK) Once the user responds to Phase 1 (or approves the pitch), perform the following: -1. **Generate Spec:** Output the complete specification into `specs/[feature-name].md` containing: +1. **Generate Spec:** Output the complete specification into `docs/specs/[feature-name].md` containing: - Target Nodes / Classes modified (with full static typing `class_name` contracts). - Signal interfaces & static payload shapes. - Layout/Theme updates required. @@ -46,7 +46,7 @@ Once the user responds to Phase 1 (or approves the pitch), perform the following - **Concrete Examples & Data Flows:** Code snippets, sample dictionary/resource shapes, or step-by-step state transformations. - **Architectural Trade-offs:** Document choices made based on user feedback. -2. **Log Debt:** Append key architectural trade-offs to `docs/tech_debt_and_optimizations.md`. +2. **Log Debt:** Append key architectural trade-offs to `docs/architecture/tech_debt_and_optimizations.md`. 3. **Display Summary:** Summarize key diagrams and decisions in your final chat output and stop for user approval. -""" \ No newline at end of file +""" diff --git a/.codex/agents/writer.toml b/.codex/agents/writer.toml index 1c5ec67..698efa9 100644 --- a/.codex/agents/writer.toml +++ b/.codex/agents/writer.toml @@ -10,7 +10,7 @@ You are a technical writer for Godot 4 / GDScript projects. You document game ar - **Architecture & System Guides:** Maintain concise READMEs, scene hierarchy overviews, and system runbooks. - **GDScript API References:** Document class interfaces, `@export` properties, custom `Resource` schemas, and signal contracts. -- **Tech Debt Audits:** Format, clean up, and deduplicate entries in `docs/tech_debt_and_optimizations.md`. +- **Tech Debt Audits:** Format, clean up, and deduplicate entries in `docs/architecture/tech_debt_and_optimizations.md`. ### Execution Rules diff --git a/.codex/skills/spec-pipeline/SKILL.md b/.codex/skills/spec-pipeline/SKILL.md index 0ad4c74..0d9a75b 100644 --- a/.codex/skills/spec-pipeline/SKILL.md +++ b/.codex/skills/spec-pipeline/SKILL.md @@ -23,11 +23,11 @@ description: "Lean implementation loop: UI/UX (Ideas -> Spec) -> Architect (Clar 4. **Phase 1b: User Feedback & Spec Generation** - User approves, modifies, or selects choices from `@architect`'s proposal. - - `@architect` writes the finalized specification to `specs/[feature-name].md` and logs decisions in `docs/tech_debt_and_optimizations.md`. + - `@architect` writes the finalized specification to `docs/specs/[feature-name].md` and logs decisions in `docs/architecture/tech_debt_and_optimizations.md`. - **STOP:** Wait for final user sign-off on the spec before invoking developer. 5. **Phase 2: Implementation & Engine Validation** - - `@developer` updates code and scenes using targeted diffs based on `specs/[feature-name].md`. + - `@developer` updates code and scenes using targeted diffs based on `docs/specs/[feature-name].md`. - `@developer` runs headless syntax verification. - If static check fails, `@developer` fixes the code immediately. diff --git a/.opencode/agents/architect.md b/.opencode/agents/architect.md index fedeb6b..a2459a8 100644 --- a/.opencode/agents/architect.md +++ b/.opencode/agents/architect.md @@ -16,7 +16,7 @@ You are the Lead Systems Architect. Refer to `AGENTS.md` for project directory s 1. **Analyze:** Read existing code patterns before designing. 2. **Design:** Define API payload shapes, GDScript type hints, and signal flows. -3. **Log:** Append trade-offs to `docs/tech_debt_and_optimizations.md`. +3. **Log:** Append trade-offs to `docs/architecture/tech_debt_and_optimizations.md`. ## Workflow @@ -25,4 +25,4 @@ You are the Lead Systems Architect. Refer to `AGENTS.md` for project directory s - Target Nodes / Classes modified. - Signal interfaces & static type contracts. - Layout/Theme updates required. -3. **Handoff:** Save to `specs/[feature-name].md` and stop for user approval. +3. **Handoff:** Save to `docs/specs/[feature-name].md` and stop for user approval. diff --git a/.opencode/agents/writer.md b/.opencode/agents/writer.md index 6ef94ba..6c0dc4a 100644 --- a/.opencode/agents/writer.md +++ b/.opencode/agents/writer.md @@ -16,7 +16,7 @@ You are a technical writer for Godot 4 / GDScript projects. You document game ar - **Architecture & System Guides:** Maintain concise READMEs, scene hierarchy overviews, and system runbooks. - **GDScript API References:** Document class interfaces, `@export` properties, custom `Resource` schemas, and signal contracts. -- **Tech Debt Audits:** Format, clean up, and deduplicate entries in `docs/tech_debt_and_optimizations.md`. +- **Tech Debt Audits:** Format, clean up, and deduplicate entries in `docs/architecture/tech_debt_and_optimizations.md`. ### Execution Rules diff --git a/.opencode/skills/spec-pipeline/SKILL.md b/.opencode/skills/spec-pipeline/SKILL.md index 4be3666..07e3034 100644 --- a/.opencode/skills/spec-pipeline/SKILL.md +++ b/.opencode/skills/spec-pipeline/SKILL.md @@ -6,7 +6,7 @@ description: "Lean implementation loop: Architect -> Developer -> User Verificat ## Execution Logic 1. **Phase 1: Architecture (Optional for small edits)** - - `@architect` defines signatures, node structures, or spec updates in `specs/[feature-name].md`. + - `@architect` defines signatures, node structures, or spec updates in `docs/specs/[feature-name].md`. - **STOP:** Wait for user approval on design choices before invoking developer. 2. **Phase 2: Implementation & Engine Validation** diff --git a/AGENTS.md b/AGENTS.md index 77d257d..b07c9b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,16 @@ - **Godot 4.7.1** 2D/GUI project (Forward Plus renderer) - Headless tests + command-line scene playback exist (see "Command-line testing & debug playback" below); the GUI editor runs via `run/main_scene`. -- **Main scene:** `res://scenes/stickman_editor.tscn` (set as `run/main_scene` in `project.godot`) +- **Main scene:** `res://scenes/main.tscn` (set as `run/main_scene` in `project.godot`; hosts the Stickman Editor) + +## Repository layout + +- `scripts/` and `scenes/` stay as separate roots with matching feature groups. Editor controllers live in `scripts/editors/{stickman,prop,stage}`; stage, stickman runtime, props, terrain, and speech systems live in `scripts/{stage,characters/stickman,props,terrain,speech}`. +- Reusable UI, thumbnails, and `ContentPaths` live in `scripts/shared/`. Harnesses and rig-authoring scripts live in `scripts/tools/`. +- The startup wrapper is `scenes/main.tscn`. Feature scenes are grouped under `scenes/editors/`, runtime stage scenes under `scenes/stage/`, stickman rigs under `scenes/characters/stickman/`, tools under `scenes/tools/`, and preserved old rigs under `scenes/legacy/`. +- Project libraries live in `content/stickmen/`, `content/props/`, and `content/stages/`; fonts stay in `resources/fonts/`. +- Maintained docs live in `docs/{guides,architecture,specs,plans}/`; older plans and project references live in `docs/archive/`. Keep `README.md` and `AGENTS.md` at the root. +- `ContentPaths.canonicalize(path)` maps only old project paths under `stickmen/`, `props/`, and `game_scenes/` into `content/`. It preserves unknown and external paths. Keep stage serialization and persisted paths routed through it when adding path-bearing data. ## Command-line testing & debug playback @@ -14,11 +23,12 @@ - `` = the repo root (e.g. `/Users/ryan/Projects/stickman`, or `C:\Godot4\stickman` on Windows). - **Run a headless test suite** (`tests/*.gd` extend `SceneTree`, print PASS/FAIL, `quit(0/1)`): ` --headless --path --script res://tests/test_cli_scene.gd` + - Path migration compatibility: `tests/test_content_paths.gd`. - Prop Editor suites (same pattern): `tests/test_prop_editor_file.gd`, `tests/test_prop_editor_theme.gd`, `tests/test_prop_editor_canvas.gd`, `tests/test_prop_editor_thumb.gd`, `tests/test_prop_editor_round3.gd`. - **Launch the Sandbox Stage and auto-PLAY a `.scene`** (visual/runtime debugging — `print()` output streams to the launching terminal): - ` --path res://scenes/sandbox_stage.tscn -- --play --scene .scene` - - Args after `--` are parsed by `scripts/cli_scene_launcher.gd` (`CliSceneLauncher.parse`) via `OS.get_cmdline_user_args()`. + ` --path res://scenes/stage/sandbox_stage.tscn -- --play --scene .scene` + - Args after `--` are parsed by `scripts/tools/cli_scene_launcher.gd` (`CliSceneLauncher.parse`) via `OS.get_cmdline_user_args()`. - `--scene ` (also `--scene=` or a single bare positional) loads the scene; `--play` enters PLAY after a successful load. - `--play` requires a scene — alone it shows the error dialog and stays blank/EDIT. - A load failure shows a "Could Not Load Scene" dialog and falls back to a blank scene. @@ -41,18 +51,18 @@ that form the foundation of the broader moviemaking experience. The **Prop Editor** (Phase 9) is a standalone, kid-friendly vector-prop authoring tool — the authoring counterpart to the stickman editor — for creating, editing, and saving `.prop` -vector prop assets. It is launched via **F6** on `res://scenes/prop_editor.tscn` and is **not** +vector prop assets. It is launched via **F6** on `res://scenes/editors/prop/prop_editor.tscn` and is **not** the configured `run/main_scene`. ## Required addon - **Scalable Vector Shapes 2D** (v2.27.7) at `addons/curved_lines_2d/` - Declared dependency for the project. The current editor UI does not instantiate it directly, - but keep it present — it is required for the legacy `stick.tscn` rig. + but keep it present — it is required for the legacy `scenes/legacy/stick.tscn` rig. ## Architecture -- `scripts/stickman_editor.gd` — `extends Control`; the main controller. Owns the menu bar, +- `scripts/editors/stickman/stickman_editor.gd` — `extends Control`; the main controller. Owns the menu bar, save/load/clear flow, JSON (de)serialization, and populates the 10 body-part panels. Writes `FILE_VERSION "1.5"`; auto-migrates `"1.0"`–`"1.4"` files on load. Coordinates cross-panel selection so only one shape is selected at a time (`shape_selected` → deselect others). @@ -98,7 +108,7 @@ the configured `run/main_scene`. and broadcasts. `_broadcast_settings()` pushes the value to the preview via `WholeStickmanPreview.set_show_guide(_show_guide)` (called in `_ready()` after `_load_settings()`). -- `scripts/body_part_panel.gd` — `class_name BodyPartPanel`, `extends PanelContainer`. +- `scripts/editors/stickman/body_part_panel.gd` — `class_name BodyPartPanel`, `extends PanelContainer`. Reusable per-part editor. Public API: - `set_shape_data(data: Variant)` — import shape data (Array or single Dictionary; used on Load/Clear) - `get_shape_data() -> Array[Dictionary]` — export array of `{shape_type, points, color, closed, vertex_flags}` @@ -130,7 +140,7 @@ the configured `run/main_scene`. - **Selection gizmos always on top:** bounding box, rotation circle, and scale crosses for the selected part are drawn in a second pass after all parts, via `_selected_gizmo_bounds`, so they always render in front. -- `scripts/whole_stickman_preview.gd` — `class_name WholeStickmanPreview`, `extends Control`; +- `scripts/editors/stickman/whole_stickman_preview.gd` — `class_name WholeStickmanPreview`, `extends Control`; the assembly preview. Owns per-part position/rotation/scale, Z-order (`_part_order`), selection + gizmos, grid drawing, and pan/zoom. Public API includes `set_body_parts()`, `set_show_guide(enabled: bool)`, `reset_view()`, `is_cursor_over_preview(global_pos)`, and @@ -146,7 +156,7 @@ the configured `run/main_scene`. `_guide_to_preview()` = `(master_pos - GUIDE_FIGURE_CENTER) * GUIDE_SCALE + preview_area.size * 0.5`, so it also re-centers on window resize; it remains a world-space fixture that moves with pan/zoom. Joint positions are **hardcoded - constants** derived from the `master_rig.tscn` rest pose (`GUIDE_JOINTS`: 13 anchors + constants** derived from the `stickman_rig.tscn` rest pose (`GUIDE_JOINTS`: 13 anchors Head/Neck/Shoulders/Elbows/Wrists/Hips/Knees/Ankles; `GUIDE_SCALE = 1.0`, `GUIDE_FIGURE_CENTER = (0, -93.75)`, `GUIDE_HEAD_RADIUS = 100.0`). Color-coded: left limbs cyan-blue `Color(0.35, 0.70, 1.00)`, right limbs orange-red @@ -154,9 +164,9 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a dots at alpha `0.65`. Joint dots use `GUIDE_JOINT_RADIUS / _zoom` (6 px constant screen size). The guide is pure drawing — **no hit-testing** is added, so it never intercepts part dragging/selection. -- `scripts/prop_editor.gd` — `class_name PropEditor`, `extends Control`; the **Prop Editor** +- `scripts/editors/prop/prop_editor.gd` — `class_name PropEditor`, `extends Control`; the **Prop Editor** controller (Phase 9, **not wired into the editor**; launched via **F6** on - `res://scenes/prop_editor.tscn`). Owns the menu bar, save/load/new flow, `.prop` + `res://scenes/editors/prop/prop_editor.tscn`). Owns the menu bar, save/load/new flow, `.prop` (de)serialization, the material-preset + per-shape inspector, the mode toolbar, the status bar, the window title, the exit dialog, and the autosave/recovery pipeline. State: `_theme: PropTheme`, `_material_preset: int` (`PropBlock.MaterialPreset`), `_current_path`, @@ -249,7 +259,7 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a Ctrl snaps rotation to 15°, editable anchors follow, and Esc cancels the preview. Reshape and Attach hide Transform Whole Prop and Normalize Size; Center Prop and Show Stickman Guide remain available. -- `scripts/prop_canvas.gd` — `class_name PropCanvas`, `extends Control`; the Prop Editor's +- `scripts/editors/prop/prop_canvas.gd` — `class_name PropCanvas`, `extends Control`; the Prop Editor's **multi-shape vector canvas** (Phase 9, **not used by the editor**), mirroring `BodyPartPanel`'s proven input model (cursor-centered zoom, pinch/pan gestures, middle-mouse pan, vertex drag, right-click context menu, z-order) while adding prop-specific needs @@ -329,9 +339,9 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a and corner gestures use snapshot-only previews and commit one atomic document edit on release; `set_whole_prop_transform_enabled()` and related transform names are the preferred API. Resize-named compatibility aliases remain internally for existing controller paths. -- `scripts/prop_file.gd` — `class_name PropFile`, `extends RefCounted`; the static `.prop` +- `scripts/editors/prop/prop_file.gd` — `class_name PropFile`, `extends RefCounted`; the static `.prop` schema + converters (Phase 9, **not used by the editor**). Consts `FILE_VERSION := "1.0"`, - `FILE_EXT := ".prop"`, `DEFAULT_DIR := "res://props"`, `SUPPORTED_VERSIONS: ["1.0"]`, + `FILE_EXT := ".prop"`, `DEFAULT_DIR := "res://content/props"`, `SUPPORTED_VERSIONS: ["1.0"]`, `PRESET_STRINGS` (index == `PropBlock.MaterialPreset`: none/wood/rubber/cardboard/metal), `SHAPE_STRINGS` (polygon/circle/line), `DEFAULT_RADIUS`, `DEFAULT_CIRCLE_SEGMENTS`. - **Schema (v1.0):** top-level `version`, `prop_name`, `material_preset`, `shapes[]`, and @@ -362,7 +372,7 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a element becomes `Vector2.ZERO`). Round 1–4 files saved this way are unrecoverable. Any typed `Packed*Array` assignment from dictionary/JSON data must go through a Variant-coercion helper, and regression tests must assert point **values**, not just counts. -- `scripts/prop_theme.gd` — `class_name PropTheme`, `extends RefCounted`; the **validated theme +- `scripts/editors/prop/prop_theme.gd` — `class_name PropTheme`, `extends RefCounted`; the **validated theme loader** for `res://prop_theme.json` (Phase 9, **not used by the editor**), modeled on `SandboxTheme`. Consts `SCHEMA_VERSION := "1.0"`, `DEFAULT_PATH := "res://prop_theme.json"`, and `DEFAULT_CONFIG` (mirrors the committed file). `load_from(path)` deep-merges the file over @@ -383,14 +393,14 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a `button_min_height`, `row_min_height`, `mode_button_min_height`, `text_color`, `text_muted_color`, `border_color`, `accent_color`. All are clamped with `DEFAULT_CONFIG` fallbacks and applied through a **local** `Theme` (no global GUI mutation). -- `scripts/thumbnails/thumbnail_cache.gd` — see its existing section; the Phase 9 extension +- `scripts/shared/thumbnails/thumbnail_cache.gd` — see its existing section; the Phase 9 extension adds `prop_file_key(path)` → `"_"` and `prop_file_png(path)` → `PROP_DIR + "/" + key + ".png"` for **custom `.prop`** files (mtime-keyed so a re-edited prop invalidates its cached PNG). Built-in template thumbnails keep `prop_png(id)` → `"_v.png"`; `stickman_key`/`stickman_png` are unchanged. -- `scripts/stk_rig_adapter.gd` — `class_name StkRigAdapter`, `extends RefCounted`; a **standalone +- `scripts/characters/stickman/stk_rig_adapter.gd` — `class_name StkRigAdapter`, `extends RefCounted`; a **standalone runtime adapter** (Phase 8, **not referenced by the editor**, extended by Phase 9). - `static func apply(stk_data, rig)` fits an instantiated `master_rig.tscn` to a loaded `.stk` + `static func apply(stk_data, rig)` fits an instantiated `stickman_rig.tscn` to a loaded `.stk` dictionary, calling three private helpers in order: `_fit_bones` (re-fits the 8 limb `Bone2D` lengths + lower-bone origins, and zeroes the Head driver's local position so the chin sits on the neck joint), `_recalibrate_ik` (repositions the `IK_Targets/Left|Right_Hand` and @@ -401,7 +411,7 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a - **Phase 2 (Sandbox) single-shape support:** `_mount_shapes()` reads a part's `shapes` array when present (v1.2+), and otherwise — when the part dict itself carries `points` — wraps the whole dict as a single shape (`shapes = [pd]`), so v1.0/v1.1 single-shape `.stk` files (e.g. - `stickmen/test.stk`) mount as visible geometry instead of being cleared to nothing. + `content/stickmen/test.stk`) mount as visible geometry instead of being cleared to nothing. - **Phase 9 extension:** also fits the head bone (`Skeleton2D/Torso/Head.position.y = -proportions.torso_length`, x preserved) and mounts the head as **full geometry** like every other part — it clears the `Body/Head` node's inline `@tool` circle script via @@ -484,11 +494,11 @@ f_prime`). This replaces the per-side family choice **and** the 180° flip heuri the key keep the previous family rules + flip heuristic exactly as before. `theta`, `s`, the Round 5 offset `t`, and the `HEAD_CHIN_DROP` fallback are unchanged — they consume `anchor`/`v` generically. - Targets `master_rig.tscn` node paths; every node lookup is null-guarded (missing node → + Targets `stickman_rig.tscn` node paths; every node lookup is null-guarded (missing node → `push_warning` + skip, never crash). Consumed by a future runtime pipeline. -- `scripts/stickman_rig.gd` — `class_name StickmanRig`, `extends Node2D`; the **runtime owner of - facing direction, per-joint bone bend, and `Body/*` z-order** for `master_rig.tscn` (Phase 9 - Task 4). Attached to the `Master` root node of `master_rig.tscn`. **Non-`@tool`** — node +- `scripts/characters/stickman/stickman_rig.gd` — `class_name StickmanRig`, `extends Node2D`; the **runtime owner of + facing direction, per-joint bone bend, and `Body/*` z-order** for `stickman_rig.tscn` (Phase 9 + Task 4). Attached to the `Master` root node of `stickman_rig.tscn`. **Non-`@tool`** — node resolution, flag writes, and z-order reordering run only at runtime (`_ready` + setters on a live instance). Enums `FacingProfile { LEFT, RIGHT, FORWARD }` (values are the harness facing-menu ids), `BendDirection { NORMAL, INVERTED }`, and `RigState { ANIMATED, RAGDOLL, RECOVERING }`. @@ -533,7 +543,7 @@ f_prime`). This replaces the per-side family choice **and** the 180° flip heuri mounted `Body/*` geometry, while z-order (depth) is unaffected by an X-mirror. `walk_to()` always plays the canonical `walk_right` clip (X-mirrored by the root for LEFT; `walk_left` is no longer used at runtime) and sets the facing profile explicitly. Two **head-related fixes** - under the mirror: the master_rig.tscn head `RemoteTransform2D` (`Skeleton2D/Torso/Head/Pivot`) + under the mirror: the stickman_rig.tscn head `RemoteTransform2D` (`Skeleton2D/Torso/Head/Pivot`) no longer sets `update_scale = false` — it pushes the **full transform** like every other `Body` driver, so `Body/Head.scale` stays identity under the mirrored root (the old partial-channel push re-canonicalized the scale and caused per-frame Y-flips/wrap-jumps). And @@ -576,7 +586,7 @@ RAGDOLL` + emits. `_build_ragdoll()` creates a `Node2D` the table. Shoulder/hip ±160°, neck free. Cached momentum is applied to the torso body. `apply_ragdoll_velocity_boost(velocity)` applies the same velocity delta (mass-scaled `apply_central_impulse`) to every ragdoll body — used by the - harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is + harness "Knock Up" button. All ragdoll nodes are spawned procedurally — `stickman_rig.tscn` is **not** modified. - **Phase 11 instant handoff + recovery:** `_update_rest_detection()` (only when `state == RAGDOLL`) @@ -728,30 +738,30 @@ target, speed = -1.0)` (feet/ground destination; no-op unless `state == ANIMATED private runtime lane without mutating `action_queue`: Overlay speech runs concurrently in one replaceable slot, Sequential actions cancel the current primary action while retaining authored future work, and Takeover actions cancel both primary and overlay work before taking control. -- `scripts/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static +- `scripts/characters/stickman/stickman_factory.gd` — `class_name StickmanFactory`, `extends RefCounted`; a **static factory** and the **runtime entry point** (Phase 9, **not used by the editor**) that turns a - `.stk` file into a live, rigged `master_rig.tscn` instance: + `.stk` file into a live, rigged `stickman_rig.tscn` instance: - `static func load_stk(path: String) -> Dictionary` — reads a `.stk` file (`FileAccess` + `JSON.parse_string`); returns `{}` + `push_warning` on failure. - `static func spawn_from_data(stk_data: Dictionary) -> StickmanRig` — instantiates - `res://master_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, returns the rig root + `res://scenes/characters/stickman/stickman_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, returns the rig root (typed as `StickmanRig` since the rig now carries the `StickmanRig` root script). - `static func spawn(path: String) -> StickmanRig` — `load_stk()` then `spawn_from_data()`; returns `null` on empty data. -- `scripts/create_animations.gd` — `@tool extends EditorScript`; a **standalone editor utility** - (run manually with `master_rig.tscn` open, **not** auto-loaded or referenced at runtime). +- `scripts/tools/create_animations.gd` — `@tool extends EditorScript`; a **standalone editor utility** + (run manually with `stickman_rig.tscn` open, **not** auto-loaded or referenced at runtime). Supersedes the deleted `scripts/create_walk.gd`. `_run()` bakes `walk_left`/`walk_right` (same keyframes as the old script, via `_generate_walk_animation()`) and a one-shot `stand_up` (via `_generate_pose_animation()`, `STAND_UP_DURATION` = 2.0, `loop_mode = LOOP_NONE`) into the open scene's default `AnimationLibrary`. `stand_up` keys the 6 `IK_Targets/*:position` tracks plus a `IK_Targets/Torso:rotation` track from `POSE_DOWN` (a generic "lying on back" pose) to - `POSE_STANDING` (matching `master_rig.tscn` defaults), with `POSE_PATHS`/`POSE_MARKERS` consts. + `POSE_STANDING` (matching `stickman_rig.tscn` defaults), with `POSE_PATHS`/`POSE_MARKERS` consts. The baked `stand_up` is an **authored reference only** — runtime recovery does not play it (StickmanRig tweens the IK targets directly from the captured ragdoll pose). -- `scripts/test_harness.gd` — **standalone staging scene** (Phase 9, **not wired into the editor**; - run via **F6** on `res://scenes/test_harness.tscn`) for debugging bone scales, vector drawing +- `scripts/tools/test_harness.gd` — **standalone staging scene** (Phase 9, **not wired into the editor**; + run via **F6** on `res://scenes/tools/test_harness.tscn`) for debugging bone scales, vector drawing offsets, and IK limits in isolation. Top UI bar: "Open .stk…" button → `FileDialog` (`*.stk`); - quick-select buttons for `stickmen/break.stk`, `stickmen/basic.stk`, `stickmen/test.stk`; "Show + quick-select buttons for `content/stickmen/break.stk`, `content/stickmen/basic.stk`, `content/stickmen/test.stk`; "Show Bones" / "Show IK Handles" checkboxes; a status label showing the loaded filename. Viewport: `SubViewportContainer` → `SubViewport` → world `Node2D` + enabled `Camera2D`; middle-mouse pan, mouse-wheel zoom, camera recenters on each spawn. Each load frees the previous rig and spawns a @@ -869,7 +879,7 @@ MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for select persistence to disk. The "Facing" menu and all animation controls are **hidden until an .stk is loaded** (`_set_rig_controls_visible(false)` at the end of `_build_ui()` and in `_free_current_rig()`; shown on successful spawn in `_load_and_spawn()`). -- `scripts/terrain_block.gd` — `class_name TerrainBlock`, `extends StaticBody2D`; a **reusable +- `scripts/terrain/terrain_block.gd` — `class_name TerrainBlock`, `extends StaticBody2D`; a **reusable vector terrain component** (Vector Terrain System, **not used by the editor**). Builds its three children in code: `Polygon2D` (interior fill, `fill_color`), `Line2D` (crisp vector border, `outline_color`/`outline_width`, auto-closed loop by appending the first vertex to the end, @@ -877,7 +887,7 @@ MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for select supports concave blocks). Exported properties: `polygon_points: PackedVector2Array`, `fill_color`, `outline_color`, `outline_width`; a unified setter pushes vertex changes to all three children live (no manual rebuilds). -- `scripts/terrain_utils.gd` — `class_name TerrainUtils`, `extends RefCounted`; static utility +- `scripts/terrain/terrain_utils.gd` — `class_name TerrainUtils`, `extends RefCounted`; static utility (**not used by the editor**): - `sanitize_points(points: PackedVector2Array, grid_size: float = 16.0) -> PackedVector2Array` — sanitization pipeline in order: grid snap → redundancy removal via a **local @@ -887,7 +897,7 @@ MOUSE_FILTER_IGNORE` on the panel (the label itself stays interactive for select guaranteeing clockwise output). - `spawn_block(...)` — factory that sanitizes raw input vectors (`sanitize_points`), creates a `TerrainBlock`, applies the cleaned points, and adds it to the target container. -- `scripts/prop_block.gd` — `class_name PropBlock`, `extends RigidBody2D`; a **reusable +- `scripts/props/prop_block.gd` — `class_name PropBlock`, `extends RigidBody2D`; a **reusable dynamic vector prop component** (Dynamic Vector Props, **not used by the editor**), `@tool`. Builds its children in code: `Polygon2D` (interior fill, `fill_color`), `Line2D` (crisp vector outline, `outline_color`/`outline_width`, auto-closed loop by appending the first @@ -914,12 +924,12 @@ PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_wi collision geometry, including the custom one-hull path and nonuniform circle fallback; returning to EDIT restores the authored root scale. Preset mass and material physics stay unchanged. -- `scripts/trigger_area.gd` — `class_name TriggerArea`, `extends Node2D`; a **placeable sensor** +- `scripts/stage/trigger_area.gd` — `class_name TriggerArea`, `extends Node2D`; a **placeable sensor** (Phase 4, **not used by the editor**). `@export size: Vector2` (default 96×96), `get_area_rect() -> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill + dashed border). **No physics and no signals** — it is a pure geometric region evaluated by `sandbox_stage.gd`'s event engine (`_update_area_entry`) for `entered_area` rules. -- `scripts/prop_utils.gd` — `class_name PropUtils`, `extends RefCounted`; static factory +- `scripts/props/prop_utils.gd` — `class_name PropUtils`, `extends RefCounted`; static factory (**not used by the editor**): - `create_box(size := Vector2(48,48))` / `create_ball(radius := 24.0)` / `create_plank(length := 160.0, thickness := 16.0)` / `create_triangle(base := 56.0, height @@ -930,10 +940,10 @@ PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_wi (`shape_type` + geometry + colors via `_apply_shape_payload()`, sanitizing polygon points through `TerrainUtils.sanitize_points()`), sets `linear_velocity` after `add_child` (only when non-zero), and returns the spawned prop. -- `scripts/physics_test_harness.gd` — `class_name PhysicsTestHarness`, `extends Node2D`; standalone +- `scripts/tools/physics_test_harness.gd` — `class_name PhysicsTestHarness`, `extends Node2D`; standalone staging scene root (Vector Terrain System / Dynamic Vector Props, **not wired into the editor**; - run via **F6** on `res://scenes/physics_test_harness.tscn`). Builds flat ground, angled ramps, and stepped - `TerrainBlock` instances via `TerrainUtils`, instantiates `res://master_rig.tscn` standing on the + run via **F6** on `res://scenes/tools/physics_test_harness.tscn`). Builds flat ground, angled ramps, and stepped + `TerrainBlock` instances via `TerrainUtils`, instantiates `res://scenes/characters/stickman/stickman_rig.tscn` standing on the flat ground, and handles camera input. A top-bar UI (`_build_ui()`, a `CanvasLayer` + `PanelContainer` matching `test_harness`'s style) replaces the old key bindings: **Spawn Crate** / **Spawn Ball** / **Spawn Plank** buttons spawn dynamic props above the angled ramp @@ -966,9 +976,9 @@ KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ra always `_update_ragdoll_toggle()` (proxy helpers are idempotent, so the toggle handler's own add/remove is harmless). The toggle label reads "Stickman" during `RECOVERING` (since ` is_in_ragdoll()` is false). -- `scripts/sandbox_stage.gd` — `class_name SandboxStage`, `extends Node2D`; the **Sandbox Stage +- `scripts/editors/stage/sandbox_stage.gd` — `class_name SandboxStage`, `extends Node2D`; the **Sandbox Stage Builder** root controller (Phase 2, **not wired into the editor**; run via **F6** on - `res://scenes/sandbox_stage.tscn`). Owns the EDIT/DIRECT/PLAY mode state machine, placement mode, + `res://scenes/stage/sandbox_stage.tscn`). Owns the EDIT/DIRECT/PLAY mode state machine, placement mode, camera pan/zoom, deletion, status bar, and signal fan-out; instantiates `StageSpawner` / `StageSelection` / `StageGizmos` (via `preload` consts). `enum StageMode { EDIT, DIRECT, PLAY }` (default EDIT). EDIT freezes `RigidBody2D` props with `freeze = true` + @@ -1085,7 +1095,7 @@ Array[Dictionary]` of `{id, trigger, actions}` rules (trigger types: `arrived_at are off by default. - **Phase 3b asset-library selector integration:** the **Stickman** and **Prop** palette buttons no longer place directly — pressing them opens a modal **selector grid** (`_selector: AssetSelector`, - an instantiated `scenes/asset_selector.tscn` `PopupPanel`, added to the UI `CanvasLayer` in + an instantiated `scenes/stage/asset_selector.tscn` `PopupPanel`, added to the UI `CanvasLayer` in `_build_ui()` with theme/font overrides applied via `AssetSelector.apply_font`), backed by a dim backdrop `_selector_dim` (a black `ColorRect` at `SELECTOR_DIM_ALPHA` = 0.5, `mouse_filter = MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only while the selector @@ -1115,7 +1125,7 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh early-return while `_selector_open` (belt-and-suspenders over the modal popup). Selection is **session-only** — persists across EDIT/DIRECT/PLAY toggles, resets on scene reload, **no disk save** (`_save_settings` is untouched). -- `scripts/stickman_speech_bubble.gd` — `class_name SpeechBubble`, `extends Node2D`; a **world-space +- `scripts/speech/stickman_speech_bubble.gd` — `class_name SpeechBubble`, `extends Node2D`; a **world-space speech bubble** drawn in `_draw()` (Phase 3a, **not used by the editor**). Child of a `StickmanRig` at `SPEECH_BUBBLE_OFFSET` (above the head), so it follows the figure and scales with the camera. Consts `FONT_SIZE`/`PADDING`/`TAIL_HEIGHT`/`MAX_WIDTH`/`BG_COLOR`/`BORDER_COLOR`/`TEXT_COLOR`. @@ -1123,7 +1133,7 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh a downward tail triangle centered on the rig-local origin, then `draw_string(...)`; `visible = false` by default, no hit-testing. Public API: `show_text(text)` (store, `visible = true`, `queue_redraw()`), `hide_bubble()`. Driven by `StickmanRig.speak()`; the rig owns hiding + the `speech_finished` signal. -- `scripts/stage_director_visuals.gd` — `class_name StageDirectorVisuals`, `extends Node2D`; the +- `scripts/stage/stage_director_visuals.gd` — `class_name StageDirectorVisuals`, `extends Node2D`; the **Edit-mode director overlay** (Phase 3a, **not used by the editor**), mirroring `StageGrid`/ `StageGizmos`. State `camera`/`world`/`enabled`/`_dirty`; public `set_enabled(value)` / `mark_dirty()`. `_process()` redraws on a dirty flag; `_draw()` reads each rig's queue via @@ -1141,7 +1151,7 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh badge**, an orange **→ action badge**, a dark label with `rule_summary()` text, and a **✕ delete icon** (the label + icon are hit-testable via `hit_test_rule()`, `Vector2.INF` sentinel from `hit_test_waypoint()`); click-label → consequence-only edit, click-✕ → delete. -- `scripts/stage_spawner.gd` — `class_name StageSpawner`, `extends RefCounted`; registry-driven +- `scripts/stage/stage_spawner.gd` — `class_name StageSpawner`, `extends RefCounted`; registry-driven spawner (Phase 2). A `_registry: Array[Dictionary]` maps ids to terrain/prop/stickman templates; adding a type = appending an entry (no hard-coded id `match`). Reuses `TerrainUtils.spawn_block`, `PropUtils.spawn_prop`, `StickmanFactory.spawn_from_data` via `preload` consts. Terrain entries @@ -1157,7 +1167,7 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh `ground/ramp/step` (terrain) + `prop` + `stickman` + `area` — the separate `crate`/`ball` entries were **removed** and a single `"prop"` entry (label "Prop", kind `"prop"`) added, so `get_spawnable_ids()` == `["ground","ramp","step","prop","stickman","area"]`. Session state: - `selected_stickman_path` (default `DEFAULT_STICKMAN_PATH` = `res://stickmen/test.stk`) and + `selected_stickman_path` (default `DEFAULT_STICKMAN_PATH` = `res://content/stickmen/test.stk`) and `selected_prop_id` (default `"crate"`), read/written by the sandbox selector (Phase 3b) — they are **session-only** (no disk save). `_stickman_cache: Dictionary` (path → parsed data) seeds the default path in `_init` and lazily loads+parses any newly selected path on first spawn. @@ -1165,9 +1175,9 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh so feet land at the cursor); `_spawn_prop` looks the **selected** prop id up via `PropLibrary.get_entry(id)` and spawns its payload (`PropUtils.spawn_prop`) + material preset. New getters `get_selected_stickman_path()` / `get_selected_prop_id()` (status + tests). -- `scripts/stickman_library.gd` — `class_name StickmanLibrary`, `extends RefCounted` (Phase 3b, - **not used by the editor**). Scans `res://stickmen/*.stk` into entry models for the asset - selector. `const STICKMEN_DIR := "res://stickmen"`; `var entries: Array[Dictionary]`. Public API: +- `scripts/characters/stickman/stickman_library.gd` — `class_name StickmanLibrary`, `extends RefCounted` (Phase 3b, + **not used by the editor**). Scans `res://content/stickmen/*.stk` into entry models for the asset + selector. `const STICKMEN_DIR := "res://content/stickmen"`; `var entries: Array[Dictionary]`. Public API: `scan(dir_path: String = STICKMEN_DIR) -> Array[Dictionary]` (via `DirAccess` + per-file `StickmanFactory.load_stk`; a corrupt or missing-`body_parts` file is **skipped** with a `push_warning` — never `{}`-as-entry; the display `name` = `stickman_name` (stripped) if @@ -1177,7 +1187,7 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh (ad-hoc single-file entry for **Browse**: `load_stk` + name fallback; `{}` on load failure or missing `body_parts`). Entry shape `{ "path", "name", "data" }`; thumbnails are **not** embedded in entries — resolved via `ThumbnailCache` at display time by the selector. -- `scripts/prop_library.gd` — `class_name PropLibrary`, `extends RefCounted` (Phase 3b, **not used +- `scripts/props/prop_library.gd` — `class_name PropLibrary`, `extends RefCounted` (Phase 3b, **not used by the editor**). A **static** registry of the 4 built-in prop templates for the asset selector. `static var _templates: Array[Dictionary]` (lazily built on first access — `PropUtils.create_*()` are `static func` and cannot run in a `const`). Public API: `static get_entries()` (builds once, @@ -1186,7 +1196,7 @@ MOUSE_FILTER_IGNORE`, on the UI `CanvasLayer` behind the selector, shown only wh material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOOD`), Ball/Rubber (`create_ball()` + `RUBBER`), Plank/Metal (`create_plank()` + `METAL`), Triangle/Cardboard (`create_triangle()` + `CARDBOARD`) — generator color themes already match the presets. -- `scripts/thumbnails/thumbnail_cache.gd` — `class_name ThumbnailCache`, `extends RefCounted` +- `scripts/shared/thumbnails/thumbnail_cache.gd` — `class_name ThumbnailCache`, `extends RefCounted` (Phase 3b, **not used by the editor**). Disk PNG cache under `user://thumbnails/`. Consts `STICKMEN_DIR := "user://thumbnails/stickmen"`, `PROP_DIR := "user://thumbnails/props"`, `PROP_VERSION := 1`. Public API: `stickman_key(path)` → `"_"` @@ -1195,7 +1205,7 @@ material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOO thumbnails); `load_png(png_path) -> Texture2D` (`Image.load_from_file` + `ImageTexture`, `null` if missing/unloadable); `save_png(tex, png_path) -> Error` (creates the parent dir); `ensure_dir`; `clean_stale_stickmen(valid_keys)` (deletes `basename_*` PNGs not in the valid key set). -- `scripts/thumbnails/stickman_thumbnail.gd` — `class_name StickmanThumbnail`, `extends Node` +- `scripts/shared/thumbnails/stickman_thumbnail.gd` — `class_name StickmanThumbnail`, `extends Node` (Phase 3b, **not used by the editor**). Renders a parsed `.stk` into a `Texture2D` for the asset selector by spawning the **real rig**. `const SIZE := Vector2i(200, 200)`. `_ready()` builds one persistent offscreen `SubViewport` (`transparent_bg`, `render_target_update_mode = UPDATE_ALWAYS`) @@ -1207,7 +1217,7 @@ material_preset, material_label, payload}` — Crate/Wood (`create_box()` + `WOO texture, frees the rig, and returns an `ImageTexture` — or `null` (a placeholder) when the capture is blank/empty (the **headless degrade**; pixel rendering is manual/F6 verification only). Output is identical to the rig actually placed on stage, not a re-implementation of the editor preview. -- `scripts/thumbnails/prop_thumbnail.gd` — `class_name PropThumbnail`, `extends Node` (Phase 3b, +- `scripts/shared/thumbnails/prop_thumbnail.gd` — `class_name PropThumbnail`, `extends Node` (Phase 3b, **not used by the editor**). Renders a prop template into a `Texture2D`. `const SIZE := Vector2i(200, 200)`. `_ready()` builds the same offscreen `SubViewport` + `Camera2D` pattern as `StickmanThumbnail`. `render(payload, material_preset) -> Texture2D` mirrors `PropBlock`'s @@ -1216,8 +1226,8 @@ Vector2i(200, 200)`. `_ready()` builds the same offscreen `SubViewport` + `Camer circle payloads are re-expanded via `PropBlock.CIRCLE_SEGMENTS`; non-`NONE` presets tint fill via `PropBlock.tint_for` and darken the outline; then frames + double-`frame_post_draw` captures and returns an `ImageTexture` or `null` (blank → placeholder). -- `scripts/asset_selector.gd` — `class_name AssetSelector`, `extends PopupPanel` (Phase 3b, **not - used by the editor**); root script of `scenes/asset_selector.tscn`. A **grid popup controller**. +- `scripts/shared/ui/asset_selector.gd` — `class_name AssetSelector`, `extends PopupPanel` (Phase 3b, **not + used by the editor**); root script of `scenes/stage/asset_selector.tscn`. A **grid popup controller**. Signals `item_selected(entry)`, `cancelled()`, `browse_requested()`, `refresh_requested()`. `const COLUMNS := 4`, `ROWS := 3`, `PAGE_SIZE := 12`, `CELL_MIN_SIZE`, `THUMB_SIZE`. `var kind` (`"stickman"` | `"prop"`). Public API: `open(kind, entries)` (sets @@ -1239,7 +1249,7 @@ page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect `%EmptyLabel`/`%PageLabel`/`%PrevButton`/`%NextButton`/`%BrowseButton`/`%RefreshButton`/ `%CloseButton`); all dynamic per-cell content is built in code at runtime because the entry set is dynamic. -- `scripts/stage_selection.gd` — `class_name StageSelection`, `extends RefCounted`; hover/click/ +- `scripts/stage/stage_selection.gd` — `class_name StageSelection`, `extends RefCounted`; hover/click/ box selection via geometric world-space AABB hit-testing (Phase 2). `static get_world_aabb` unions a `Polygon2D` child's world points (terrain/props) or, for a stickman rig, recursively unions its mounted `Body/*` `Line2D`/`Polygon2D` geometry (`_collect_visual_points()`), so the @@ -1250,7 +1260,7 @@ page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect `clear_selection`/`select_only`/`add_to_selection`/`toggle_selection`/`is_selected`/`hit_test`/ `update_hover`/`box_select`. (Phase 4) `get_world_aabb` also handles `TriggerArea` objects via the same **duck-typed** `get_area_rect` AABB branch, so trigger areas are selectable/hit-testable. -- `scripts/stage_gizmos.gd` — `class_name StageGizmos`, `extends Node2D`; hover highlight + +- `scripts/stage/stage_gizmos.gd` — `class_name StageGizmos`, `extends Node2D`; hover highlight + selection outlines + a rotate ring (Phase 2). **No translate handle** — objects are dragged directly by the root; `set_targets(nodes: Array[Node2D])` holds the current multi-selection, and `begin_translate_drag(node, world_pos)` starts a TRANSLATE drag that moves **all** selected @@ -1271,46 +1281,46 @@ page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect existing transform signals. Other selections keep the existing gizmos. `transform_committed(nodes: Array[Node2D])` emitted on drag end. `set_enabled(false)` hides + disables in PLAY. Also draws the box-select marquee via `set_box_rect(rect)`. -- `scripts/stage_grid.gd` — `class_name StageGrid`, `extends Node2D`; the optional world-space +- `scripts/stage/stage_grid.gd` — `class_name StageGrid`, `extends Node2D`; the optional world-space grid overlay (Phase 2). Pure `_draw()`: grid lines pan/zoom with the camera (`camera.get_screen_center_position()` - viewport extent / zoom), with a heavier **major line every 5 cells**; `grid_size` / `enabled` are set by `SandboxStage`. Added as the first child of the stage root so it renders behind `World`; no hit-testing. - Scenes: - - `scenes/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used + - `scenes/editors/stickman/stickman_editor.tscn` — main editor layout; unique-name nodes (`%Prefix`) used for typed `@onready` access: `%MenuBar`, `%StickmanNameEdit`, `%LeftColumn`, `%CenterColumn`, `%WholeStickmanPreview`, `%SaveDialog`, `%LoadDialog`, `%ClearConfirmDialog`, `%ErrorDialog`, `%StatusBar`, `%CursorCoords`, `%SnapStatus`. - **Phase 6 status bar:** `StatusBar` is an `HBoxContainer` bottom-anchored at 28 px height containing `CursorCoords` (left) and `SnapStatus` (right); `MainLayout` `offset_bottom` is `-28.0` to leave room for it. - - `scenes/body_part_panel.tscn` — instantiated 10× at runtime (5 per column). Each panel + - `scenes/editors/stickman/body_part_panel.tscn` — instantiated 10× at runtime (5 per column). Each panel sets `size_flags_vertical = SIZE_EXPAND_FILL` so the panels expand to fill the column height in their parent VBoxContainer. - - `scenes/test_harness.tscn` — **standalone staging scene** (Phase 9, not wired into the - editor; run via **F6**). Backed by `scripts/test_harness.gd`. Top UI bar with file open / + - `scenes/tools/test_harness.tscn` — **standalone staging scene** (Phase 9, not wired into the + editor; run via **F6**). Backed by `scripts/tools/test_harness.gd`. Top UI bar with file open / quick-select, "Show Bones" / "Show IK Handles" / "Show Coords" toggles, and a loaded-filename status label; `SubViewport` world with an enabled `Camera2D` (middle-mouse pan, wheel zoom, recenter on spawn); interactive limb IK via `SkeletonModificationStack2D` TwoBoneIK. - - `scenes/physics_test_harness.tscn` — **standalone staging scene** (Vector Terrain System / + - `scenes/tools/physics_test_harness.tscn` — **standalone staging scene** (Vector Terrain System / Dynamic Vector Props, not wired into the editor; run via **F6**). Backed by - `scripts/physics_test_harness.gd`. Root `Node2D` + script, `Camera2D` at position `(0, -400)` + `scripts/tools/physics_test_harness.gd`. Root `Node2D` + script, `Camera2D` at position `(0, -400)` zoom `0.5`, empty Environment container. Wheel-zoom scales between `0.25x` and `3.0x` (resolution independence / vector outline thickness); middle-drag pans. Top-bar buttons **Spawn Crate / Spawn Ball / Spawn Plank** spawn dynamic props (`PropUtils`) above the angled ramp; a toggle button flips the rig **Stickman** ↔ **Ragdoll** (removing/restoring the best-effort `StaticBody2D` rig collision proxy); a **Knock Up** button impulses the ragdoll and all props upward. - - `scenes/sandbox_stage.tscn` — **standalone staging scene** (Sandbox Stage Builder, Phase 2, - not wired into the editor; run via **F6**). Backed by `scripts/sandbox_stage.gd`. Minimal + - `scenes/stage/sandbox_stage.tscn` — **standalone staging scene** (Sandbox Stage Builder, Phase 2, + not wired into the editor; run via **F6**). Backed by `scripts/editors/stage/sandbox_stage.gd`. Minimal root `Node2D` + script, `Camera2D` at position `(0, -400)` zoom `(0.5, 0.5)`, and an empty `World` (Node2D) container; the `GridLayer` (`StageGrid`), `GizmoLayer` (`StageGizmos`), `PlacementGhost` holder, and the CanvasLayer top-bar UI (mode toggle + six palette buttons + Grid/Snap/Size controls + status label) are built in code at `_ready()`. Phase 3b instantiates the `AssetSelector` grid popup (see below) into the UI `CanvasLayer`. - - `scenes/asset_selector.tscn` — **standalone asset-selector grid popup** (Phase 3b, not wired - into the editor). Rooted at `AssetSelector` (`PopupPanel`, `scripts/asset_selector.gd`); a + - `scenes/stage/asset_selector.tscn` — **standalone asset-selector grid popup** (Phase 3b, not wired + into the editor). Rooted at `AssetSelector` (`PopupPanel`, `scripts/shared/ui/asset_selector.gd`); a **minimal shell/layout skeleton** — authored title bar, empty `GridContainer`, empty-state `Label`, and a footer (`Prev` / page label / `Next` / `Browse…` / `Refresh` / `Close`), all as unique-name nodes (`%TitleLabel`, `%GridContainer`, `%EmptyLabel`, `%PageLabel`, `%PrevButton`, @@ -1318,8 +1328,8 @@ page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect (thumbnail + name + prop material badge) is built in code at runtime because the entry set is dynamic; instantiated by `sandbox_stage.gd` (`_build_ui`) for the **Stickman** / **Prop** palette-button selector grids. - - `scenes/prop_editor.tscn` — **standalone Prop Editor scene** (Phase 9, not wired into the - editor; launched via **F6**). Backed by `scripts/prop_editor.gd`, minimal shell with + - `scenes/editors/prop/prop_editor.tscn` — **standalone Prop Editor scene** (Phase 9, not wired into the + editor; launched via **F6**). Backed by `scripts/editors/prop/prop_editor.gd`, minimal shell with `%MenuBar` inside `%TopRow` (beside `%TopRightControls` — the grid spinbox + snap checkbox), `%TopBar`/`%PropNameEdit`, `%MainLayout`/`%CanvasPanel`/`%CanvasArea`/`%ModeBar`/`%PropCanvas`, `%InspectorColumn`/`%InspectorList`, a bottom status bar with `%SaveStatus` left + @@ -1355,7 +1365,7 @@ rotation, scale}`. ### `.prop` file format (Phase 9) - The Prop Editor's `.prop` vector-prop assets are versioned JSON (`version "1.0"`), - defined in `README.md`, saved under `res://props/*.prop`, and converted to a runtime + defined in `README.md`, saved under `res://content/props/*.prop`, and converted to a runtime payload by `PropFile.flatten_to_payload()`. - Shape dictionary (per entry in `shapes[]`): `{ "shape_type": "polygon"|"circle"|"line", "points": Array[{x,y}] (polygon/line), "radius": float (circle), "fill_color": "#hex", @@ -1388,7 +1398,7 @@ rotation, scale}`. ### Legacy scene (do not delete) -- `stick.tscn` — the original rigged/animated figure using `Skeleton2D` + IK targets + +- `scenes/legacy/stick.tscn` — the original rigged/animated figure using `Skeleton2D` + IK targets + `RemoteTransform2D` + `Line2D` limbs, plus an embedded `@tool` script drawing the head circle. Animations: "walk", "walk_to", "RESET". **Not** the current main scene; kept for future animation/rigging phases. diff --git a/README.md b/README.md index e8d9278..78b000a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stickman Studio -A Godot 4.4 editor tool for creating and assembling stick figures. Stickman Studio lets you draw individual body-part shapes, edit vertices, assemble them into a whole stickman, and save/load your figure to a JSON file (`.stk`) for later use or animation scripting. +A Godot 4.7.1 project for creating and directing 2D scenes. Stickman Studio includes a stickman editor for drawing body-part shapes and assembling figures, plus a Sandbox Stage for building sets, directing actions, and playing scenes. ## Overview @@ -20,23 +20,30 @@ Drawing surfaces **clip** their content to the panel bounds: shapes and parts no | Item | Version | |---|---| -| Engine | Godot 4.4 or newer | +| Engine | Godot 4.7.1 | | Addon | Scalable Vector Shapes 2D (`addons/curved_lines_2d/`) | -> The legacy `stick.tscn` scene (a rigged walk animation using `Skeleton2D` + IK) remains in the project root for reference but is **no longer the main scene**. +> The legacy `scenes/legacy/stick.tscn` scene (a rigged walk animation using `Skeleton2D` + IK) remains for reference but is **no longer the main scene**. ## Running the project -No CLI build, test, or lint commands are used. Run the project from the Godot editor: +Run the project from the Godot editor: -1. Open the project folder in **Godot 4.4**. -2. Press **F5**, or open and run `res://scenes/stickman_editor.tscn`. +1. Open the project folder in **Godot 4.7.1**. +2. Press **F5**, or open and run `res://scenes/main.tscn` (which hosts the Stickman Editor). -The main scene is configured as `run/main_scene="res://scenes/stickman_editor.tscn"` in `project.godot`. +The main scene is configured as `run/main_scene="res://scenes/main.tscn"` in `project.godot`. + +### Repository layout + +- `scripts/` and `scenes/` are grouped by feature, with editors, stage systems, stickman runtime, props, terrain, speech, shared UI, and tools kept in their own folders. +- Editable libraries are stored in `content/stickmen/`, `content/props/`, and `content/stages/`; fonts remain in `resources/fonts/`. +- Maintained project documentation is in `docs/guides/`, `docs/architecture/`, `docs/specs/`, and `docs/plans/`. Older plans and references are in `docs/archive/`. +- `ContentPaths` keeps old project asset references loadable after moves while leaving external file paths unchanged. ### Sandbox Stage (F6) -Run `res://scenes/sandbox_stage.tscn` with **F6** to build and direct a scene. **EDIT** is for +Run `res://scenes/stage/sandbox_stage.tscn` with **F6** to build and direct a scene. **EDIT** is for placing and transforming stage objects. **DIRECT** is for authoring stickman actions and event rules. Press **Space** or **Play** to start a run; press Space or **Direct** to freeze it. Space acts on each key press, so holding it does not repeatedly toggle the run. **Play** or Space @@ -116,7 +123,7 @@ Scroll the mouse wheel inside any body-part panel or the Whole Stickman preview ### 5. Pose Silhouette Guide -The Whole Stickman preview can render a **pose silhouette guide**: a semi-transparent, color-coded stick figure that marks the rest-pose pivot joints of the future rig defined by `master_rig.tscn`. Its 13 joint anchors (Head, Neck, Left/Right Shoulder, Left/Right Elbow, Left/Right Wrist, Hips, Left/Right Knee, Left/Right Ankle) correspond to the exact pivot locations a future `Skeleton2D` rig will use, so you can align your body-part shapes to the same pivots. +The Whole Stickman preview can render a **pose silhouette guide**: a semi-transparent, color-coded stick figure that marks the rest-pose pivot joints of the runtime rig defined by `stickman_rig.tscn`. Its 13 joint anchors (Head, Neck, Left/Right Shoulder, Left/Right Elbow, Left/Right Wrist, Hips, Left/Right Knee, Left/Right Ankle) correspond to the pivot locations used by the `Skeleton2D` rig, so you can align your body-part shapes to the same pivots. The guide is a **view aid only**: it is drawn above the grid **and in front of user parts** (ghosting over them), moves with pan/zoom, is **not** hit-testable (it never intercepts part dragging or selection), and is **not** saved to the `.stk` file. @@ -128,13 +135,13 @@ The guide is a **view aid only**: it is drawn above the grid **and in front of u | Right (orange/red) | RightShoulder, RightElbow, RightWrist, RightKnee, RightAnkle | `(1.00, 0.50, 0.20)` | `0.45` | `0.65` | | Central (white) | Hips, Neck, Head | `(1.00, 1.00, 1.00)` | `0.45` | `0.65` | -- **Lines** (limbs + spine) render semi-transparent with line alpha `0.45`; the **head outline** is a white semi-transparent circle of radius 100 (1:1 `master_rig.tscn` scale) centered on the Head joint. +- **Lines** (limbs + spine) render semi-transparent with line alpha `0.45`; the **head outline** is a white semi-transparent circle of radius 100 (1:1 `stickman_rig.tscn` scale) centered on the Head joint. - **Joint dots** are constant **6 px** on screen at every zoom level (`GUIDE_JOINT_RADIUS / _zoom`). - The guide is **ON by default**. Toggle it via **View → Hide Pose Guide** / **View → Show Pose Guide** (the menu item is a dynamic, text-only label — no checkmark — reading "Hide Pose Guide" while the guide is visible and "Show Pose Guide" while it is hidden); the setting persists to `user://settings.json` (`show_pose_guide`). **Centered at the default view:** the guide's figure bounding box is centered at the preview-area center, computed at draw time from the live preview size (`GUIDE_FIGURE_CENTER = (0, -93.75)`; `_guide_to_preview()` maps `master_pos` to `(master_pos - GUIDE_FIGURE_CENTER) * GUIDE_SCALE + preview_area.size * 0.5`). It is centered at startup and after **Reset Views** (zoom 1 / pan 0), and re-centers automatically when the window is resized. Because it remains a **world-space fixture**, panning/zooming moves it with the grid and parts. -**1:1 master-rig scale (deliberate):** the guide matches `master_rig.tscn` proportions exactly (a figure of ≈ 336 × 940 px, roughly x ∈ [-168, 168], y ∈ [-563.5, 376]). Because the default starter parts are small, the guide intentionally **dwarfs** them. At 1:1 zoom the ~940 px figure is taller than a typical panel, so the head and feet clip equally above/below the fold when centered — accepted. `GUIDE_SCALE`/`GUIDE_FIGURE_CENTER` are the tunables if a different size or placement is wanted later. +**1:1 master-rig scale (deliberate):** the guide matches `stickman_rig.tscn` proportions exactly (a figure of ≈ 336 × 940 px, roughly x ∈ [-168, 168], y ∈ [-563.5, 376]). Because the default starter parts are small, the guide intentionally **dwarfs** them. At 1:1 zoom the ~940 px figure is taller than a typical panel, so the head and feet clip equally above/below the fold when centered — accepted. `GUIDE_SCALE`/`GUIDE_FIGURE_CENTER` are the tunables if a different size or placement is wanted later. ### 6. Grid & Panning @@ -259,21 +266,21 @@ The Whole Stickman preview treats all shapes in a body-part panel as **one combi ### 14. Test Harness & Stickman Factory (Phase 9) -Phase 9 adds the **runtime pipeline** that turns a saved `.stk` file into a live, rigged `master_rig.tscn` instance — plus a standalone staging scene for debugging it in isolation. **Neither is wired into the editor.** +Phase 9 adds the **runtime pipeline** that turns a saved `.stk` file into a live, rigged `stickman_rig.tscn` instance — plus a standalone staging scene for debugging it in isolation. **Neither is wired into the editor.** -**Stickman Factory (`res://scripts/stickman_factory.gd`)** — the runtime entry point: +**Stickman Factory (`res://scripts/characters/stickman/stickman_factory.gd`)** — the runtime entry point: | Method | Signature | Behavior | |---|---|---| | `load_stk` | `static func load_stk(path: String) -> Dictionary` | Reads a `.stk` file via `FileAccess` + `JSON.parse_string`. Returns `{}` and `push_warning` on failure. | -| `spawn_from_data` | `static func spawn_from_data(stk_data: Dictionary) -> Node2D` | Instantiates `res://master_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, returns the rig root. | +| `spawn_from_data` | `static func spawn_from_data(stk_data: Dictionary) -> Node2D` | Instantiates `res://scenes/characters/stickman/stickman_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, returns the rig root. | | `spawn` | `static func spawn(path: String) -> Node2D` | Chains `load_stk()` → `spawn_from_data()`. Returns `null` on empty data. | -The factory is the intended runtime API: `StickmanFactory.spawn("res://stickmen/basic.stk")` yields a rigged `StickmanRig` (the root of `master_rig.tscn`) ready to add to the scene tree. `StkRigAdapter` (Phase 8, extended by Phase 9) now also fits the head bone (`Head.position.y = -proportions.torso_length`) and mounts the head as **full geometry** like every other part (clearing its inline `@tool` circle script); the mounted head is dropped `HEAD_CHIN_DROP` (28 px) below the neck so its chin aligns with the pose-guide circle's bottom and overlaps the torso (Phase 9 Round 4). +The factory is the intended runtime API: `StickmanFactory.spawn("res://content/stickmen/basic.stk")` yields a rigged `StickmanRig` (the root of `stickman_rig.tscn`) ready to add to the scene tree. `StkRigAdapter` (Phase 8, extended by Phase 9) now also fits the head bone (`Head.position.y = -proportions.torso_length`) and mounts the head as **full geometry** like every other part (clearing its inline `@tool` circle script); the mounted head is dropped `HEAD_CHIN_DROP` (28 px) below the neck so its chin aligns with the pose-guide circle's bottom and overlaps the torso (Phase 9 Round 4). -**Test Harness (`res://scenes/test_harness.tscn`)** — run via **F6** on the scene (NOT via the main editor scene). Controls: +**Test Harness (`res://scenes/tools/test_harness.tscn`)** — run via **F6** on the scene (NOT via the main editor scene). Controls: -- **Open .stk…** — `FileDialog` filtered to `*.stk`; quick-select buttons for `res://stickmen/break.stk`, `res://stickmen/basic.stk`, `res://stickmen/test.stk`. +- **Open .stk…** — `FileDialog` filtered to `*.stk`; quick-select buttons for `res://content/stickmen/break.stk`, `res://content/stickmen/basic.stk`, `res://content/stickmen/test.stk`. - **Show Bones / Show IK Handles** — checkboxes toggling the debug overlay. - **Loaded filename** — status label showing the currently loaded file. - **Pan / zoom** — middle-mouse drag to pan, mouse-wheel to zoom the `Camera2D`; the camera recenters on each spawn. @@ -289,7 +296,7 @@ Debug overlay (a world-space `Node2D` `_draw()`): true bone **segments** drawn b The **Vector Terrain System** is a standalone, reusable component for building **crisp, resolution-independent vector terrain** (flat ground, angled ramps, stepped platforms) — with matching 2D collision. It is **not wired into the editor**; it is exercised through a dedicated staging scene run via **F6**. -**`TerrainBlock` (`res://scripts/terrain_block.gd`)** — `@tool` `class_name TerrainBlock`, `extends StaticBody2D`. A single terrain segment that builds its three children in code: +**`TerrainBlock` (`res://scripts/terrain/terrain_block.gd`)** — `@tool` `class_name TerrainBlock`, `extends StaticBody2D`. A single terrain segment that builds its three children in code: | Child | Node type | Purpose | |---|---|---| @@ -299,7 +306,7 @@ The **Vector Terrain System** is a standalone, reusable component for building * **Exported properties** (`polygon_points: PackedVector2Array`, `fill_color`, `outline_color`, `outline_width`) are driven by a single unified setter that pushes vertex changes to all three children live — no manual rebuilds. -**`TerrainUtils` (`res://scripts/terrain_utils.gd`)** — `class_name TerrainUtils`, `extends RefCounted`, static utility. +**`TerrainUtils` (`res://scripts/terrain/terrain_utils.gd`)** — `class_name TerrainUtils`, `extends RefCounted`, static utility. - `sanitize_points(points: PackedVector2Array, grid_size: float = 16.0) -> PackedVector2Array` — sanitizes a raw vertex list through a fixed pipeline, in order: 1. **Grid snap** — rounds each vertex to the `grid_size` grid (default **16.0**). @@ -307,13 +314,13 @@ The **Vector Terrain System** is a standalone, reusable component for building * 3. **Clockwise enforcement** — `Geometry2D.is_polygon_clockwise()` reverses the winding if it is not already clockwise, **guaranteeing clockwise output**. - `spawn_block(...)` — factory that sanitizes the raw input vectors (`sanitize_points`), creates a `TerrainBlock`, applies the cleaned points, and adds it to the target container. -**`physics_test_harness.tscn` / `scripts/physics_test_harness.gd`** — `class_name PhysicsTestHarness`, `extends Node2D`; a **standalone staging scene** (run via **F6**; not wired into the editor). It builds flat ground, angled ramps, and stepped `TerrainBlock` instances via `TerrainUtils`, then instantiates `res://master_rig.tscn` standing on the flat ground. The scene root is a `Node2D` + script with a `Camera2D` at position `(0, -400)` zoom `0.5` and an empty `Environment` container. The camera wheel-zoom scales between **0.25x and 3.0x** (to test resolution independence / vector outline thickness); middle-drag pans. It also hosts the **Dynamic Vector Props** spawner (see below): press **1/2/3** to drop physics props above the angled ramp, plus a best-effort `StaticBody2D` collision proxy for the rig (which has no physics bodies of its own). A toggle-mode **Button** (label "Stickman" ↔ "Ragdoll") flips the rig's **kinematic-to-ragdoll** mode (see §17); a **Rest** `SpinBox` (0.1–10 s) writes `_rig.rest_timeout` (runtime-only) and a **"Recover Now"** button calls `_rig.request_recovery()`. The harness connects the rig's `state_changed` signal, which removes the `RigCollisionProxy` on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`. +**`physics_test_harness.tscn` / `scripts/tools/physics_test_harness.gd`** — `class_name PhysicsTestHarness`, `extends Node2D`; a **standalone staging scene** (run via **F6**; not wired into the editor). It builds flat ground, angled ramps, and stepped `TerrainBlock` instances via `TerrainUtils`, then instantiates `res://scenes/characters/stickman/stickman_rig.tscn` standing on the flat ground. The scene root is a `Node2D` + script with a `Camera2D` at position `(0, -400)` zoom `0.5` and an empty `Environment` container. The camera wheel-zoom scales between **0.25x and 3.0x** (to test resolution independence / vector outline thickness); middle-drag pans. It also hosts the **Dynamic Vector Props** spawner (see below): press **1/2/3** to drop physics props above the angled ramp, plus a best-effort `StaticBody2D` collision proxy for the rig (which has no physics bodies of its own). A toggle-mode **Button** (label "Stickman" ↔ "Ragdoll") flips the rig's **kinematic-to-ragdoll** mode (see §17); a **Rest** `SpinBox` (0.1–10 s) writes `_rig.rest_timeout` (runtime-only) and a **"Recover Now"** button calls `_rig.request_recovery()`. The harness connects the rig's `state_changed` signal, which removes the `RigCollisionProxy` on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`. ### 16. Dynamic Vector Props The **Dynamic Vector Props** system is a standalone, reusable component for building **physical, dynamic props** (crates, balls, planks) as `RigidBody2D` bodies with crisp vector rendering and matching collision. It is **not wired into the editor**; it is exercised through the physics test harness staging scene via **F6**. -**`PropBlock` (`res://scripts/prop_block.gd`)** — `@tool` `class_name PropBlock`, `extends RigidBody2D`. A single physical prop whose children are built in code, `@tool`-safe (null-guarded for editor safety): +**`PropBlock` (`res://scripts/props/prop_block.gd`)** — `@tool` `class_name PropBlock`, `extends RigidBody2D`. A single physical prop whose children are built in code, `@tool`-safe (null-guarded for editor safety): | Child | Node type | Purpose | |---|---|---| @@ -346,7 +353,7 @@ The **Dynamic Vector Props** system is a standalone, reusable component for buil A **unified live-update setter** drives geometry to all children live — a change to `polygon_points` / `radius` / `shape_type` pushes to the `Polygon2D`, `Line2D`, and the active collision node with no manual rebuilds. Non-`NONE` presets also recolor `fill_color` and `outline_color` (outline = `tint_for(preset).darkened(0.55)`). -**`PropUtils` (`res://scripts/prop_utils.gd`)** — `class_name PropUtils`, `extends RefCounted`, static factory. +**`PropUtils` (`res://scripts/props/prop_utils.gd`)** — `class_name PropUtils`, `extends RefCounted`, static factory. - **Primitive generators** return shape-payload dictionaries with default dimensions + color themes: - `create_box(size := Vector2(48,48), ...)` — a 4-point `POLYGON` (wood theme). @@ -367,7 +374,7 @@ A **unified live-update setter** drives geometry to all children live — a chan ### 17. Kinematic-to-Ragdoll State System -`StickmanRig` (the runtime root of `master_rig.tscn`) can switch between physics modes via a reversible state machine with three states: +`StickmanRig` (the runtime root of `stickman_rig.tscn`) can switch between physics modes via a reversible state machine with three states: - **`ANIMATED`** (default) — the kinematic `Skeleton2D` + IK puppet is visible and driven by the `AnimationPlayer` / `SkeletonModificationStack2D`. The rig root's momentum is cached each physics frame (`_physics_process` → `_track_momentum()`) so it can be handed off on transition. - **`RAGDOLL`** — the kinematic rig is frozen (`AnimationPlayer` stopped with `keep_state`) and the `Body/*` visuals hidden **immediately**; a procedural network of `RigidBody2D` + `PinJoint2D` nodes spawned in code takes over, letting the figure fall/tumble against the terrain and props. The ragdoll is built from the **current solved bone positions** (the IK stack is disabled only after the build), so the handoff is instant and seamless — the ragdoll appears in exactly the pose the figure was in, no crossfade. @@ -401,7 +408,7 @@ Ragdoll bodies spawn fully visible — the entry handoff is instant (the ragdoll **Angular limits** (`_apply_ragdoll_joint_limits()`): elbows/knees get their band from the editable per-profile, per-joint `RAGDOLL_JOINT_BAND_LIMITS` table — a `{ "lower": deg, "upper": deg }` range for the joint's allowed relative angle (0 = straight; a small side ~5 resists hyperextension, a large side ~150 is the fold, and a narrow/locked band such as `{60, 100}` forces a permanently-bent limb). FORWARD arms `{-150°, +5°}`, legs `{-5°, +150°}`; LEFT/RIGHT override both sides. Joints missing from the table fall back to the `RAGDOLL_JOINTS` `limit` string (`elbow_knee` `-5°..+150°`, `elbow_knee_ccw` `-150°..+5°`). Shoulders/hips allow ±160°; the neck is free (`angular_limit_enabled = false`). -**Joint bend configuration (EDIT ME):** every per-joint bend *decision* in the ragdoll cycle lives in three `const` tables at the top of `scripts/stickman_rig.gd`, keyed by facing profile, so joints can be tuned without touching pipeline code: +**Joint bend configuration (EDIT ME):** every per-joint bend *decision* in the ragdoll cycle lives in three `const` tables at the top of `scripts/characters/stickman/stickman_rig.gd`, keyed by facing profile, so joints can be tuned without touching pipeline code: | Const | Shape | Purpose | |---|---|---| @@ -417,30 +424,30 @@ Ragdoll bodies spawn fully visible — the entry handoff is instant (the ragdoll `_play_stand_up()` then starts a **2.0 s** sine-in-out tween (`STAND_UP_DURATION`) of the 6 markers → their **pre-ragdoll** captured values and of the arm + head bones to the `_recovery_bones` targets computed by the snap — upper arm bones take the shortest path, lower arm bones use `_natural_joint_target(current, target, offset)` (offset = `bone_angle_lower − bone_angle_upper`; the through-straight rule is evaluated in the joint's rendered space and a `"shortest"` entry in `RECOVERY_JOINT_PATH` bypasses it). The **legs are IK-driven** toward their tweening, ground-clamped ankle markers (the 2 leg mods stay enabled). The arm mods + head LookAt stay disabled during the tween. The baked `stand_up` animation is **not** played — a fixed first keyframe can never match an arbitrary ragdoll rest pose. On tween finish, `_on_stand_up_finished()` restores the pre-ragdoll facing profile + per-joint bend flags + head LookAt state, re-enables the arm mods, re-shows `Body/*`, sets `state = ANIMATED`, and emits — the resumed IK solve reproduces the pre-ragdoll pose exactly (the end pose matches the pre-ragdoll frame, including mid-walk poses). -**Interruptibility:** `set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll; `set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated `set_ragdoll` calls are idempotent. All ragdoll nodes are spawned procedurally — `master_rig.tscn` is **not** modified (the `stand_up` / `walk_left` / `walk_right` animations are baked into the scene's `AnimationLibrary` by the `create_animations.gd` editor script, which runs manually in the editor; the baked `stand_up` is an authored reference and the recovery path does not play it). The ragdoll bodies spawn under a **world sibling** (not the rig root), so the whole-rig Y-axis mirror does not affect them; `walk_left` remains in the library but is **not used at runtime** — `walk_to()` plays the canonical `walk_right` clip for every direction (root-mirrored for LEFT). +**Interruptibility:** `set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll; `set_ragdoll(false)` during `RAGDOLL` routes through `_start_recovery()`; repeated `set_ragdoll` calls are idempotent. All ragdoll nodes are spawned procedurally — `stickman_rig.tscn` is **not** modified (the `stand_up` / `walk_left` / `walk_right` animations are baked into the scene's `AnimationLibrary` by the `create_animations.gd` editor script, which runs manually in the editor; the baked `stand_up` is an authored reference and the recovery path does not play it). The ragdoll bodies spawn under a **world sibling** (not the rig root), so the whole-rig Y-axis mirror does not affect them; `walk_left` remains in the library but is **not used at runtime** — `walk_to()` plays the canonical `walk_right` clip for every direction (root-mirrored for LEFT). ### 18. Sandbox Stage Builder -The **Sandbox Stage Builder** (Phase 2) is a standalone, kid-friendly director sandbox: a visual stage where you place terrain, props, and stickmen from a palette, then switch between **Edit** (build), **Direct** (director/rule authoring), and **Play** (physics simulation). It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`. +The **Sandbox Stage Builder** (Phase 2) is a standalone, kid-friendly director sandbox: a visual stage where you place terrain, props, and stickmen from a palette, then switch between **Edit** (build), **Direct** (director/rule authoring), and **Play** (physics simulation). It is **not wired into the editor** — run via **F6** on `res://scenes/stage/sandbox_stage.tscn`. The stage is intentionally **extendable**: the spawn palette is registry-driven (adding an object type = appending one dictionary entry), selection hit-tests arbitrary `Node2D`s geometrically, gizmos drive `global_position`/`global_rotation`, and core events are exposed as signals for future phases (Action Queue, Triggers, Save/Load). Styling defaults (fonts/sizes/colors/grid) live in a hand-editable `res://sandbox_theme.json` (Phase 4b, §21). | File | Purpose | |---|---| -| `res://scenes/sandbox_stage.tscn` | The stage scene: root `Node2D` + `Camera2D` + empty `World` container. | -| `res://scripts/sandbox_stage.gd` | `class_name SandboxStage`, `extends Node2D` — root controller (3-mode state machine, placement + drag-painting, camera, deletion, bottom status bar, mode badge/frame/cursors, signals); **Phase 3b** instantiates the `AssetSelector` grid popup + the two thumbnail renderers, owns the selector open/close flow and the lazy per-frame thumbnail drain, and wires the Stickman/Prop palette buttons to the selector (§22). **Phase 3c** adds the editor-tool wiring: the `QueuePanel` / `RulePanel` / `ActionEditor` / `RuleEditor` / `WaypointContext` instantiation, the unified `CaptureKind` target-capture system, "Edit Queue…"/"Edit Rules…" entry points + right-click stickman/waypoint context menus, the shared confirmation dialog, and consequence-only rule editing (§23). | -| `res://scripts/stage_spawner.gd` | `class_name StageSpawner`, `extends RefCounted` — registry-driven factory reusing `TerrainUtils` / `PropUtils` / `StickmanFactory`; exposes `is_terrain_id()` / `get_template_aabb()` / `spawn_id` tagging. `get_template_aabb()` returns the **sanitized** template AABB (mirrors `_spawn_terrain()`'s 16-px grid pass), so it doubles as the block-unit paint stride. **Phase 3b:** registry ids `ground/ramp/step/prop/stickman/area` (separate `crate`/`ball` entries removed); holds the session state `selected_stickman_path` / `selected_prop_id` and a per-path `_stickman_cache`; `prop` and `stickman` spawn the **selected** asset. | -| `res://scripts/stickman_library.gd` | **Phase 3b** `class_name StickmanLibrary`, `extends RefCounted` — scans `res://stickmen/*.stk` into `{path, name, data}` entry models (corrupt/missing-`body_parts` files skipped, empty `stickman_name` → filename basename), with `make_entry(path)` for arbitrary Browse-chosen paths (§22). | -| `res://scripts/prop_library.gd` | **Phase 3b** `class_name PropLibrary`, `extends RefCounted` — static registry of the 4 prop templates (Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard) with their `PropUtils.create_*()` payloads + material presets; `get_default_id()` = `"crate"` (§22). | -| `res://scripts/asset_selector.gd` | **Phase 3b** `class_name AssetSelector`, `extends PopupPanel` — grid UI controller (root of `scenes/asset_selector.tscn`): pagination 12/page (4×3), Prev/Next/page label, empty-state label, per-cell thumbnail + name + (prop) material badge, hover styling (no pre-highlight on open), window-resize re-centering, Browse/Refresh/Close wiring, Esc handling; signals `item_selected` / `cancelled` / `browse_requested` / `refresh_requested` (§22). | -| `res://scripts/stage_selection.gd` | `class_name StageSelection`, `extends RefCounted` — hover/click/box selection via geometric AABB hit-testing. | -| `res://scripts/stage_gizmos.gd` | `class_name StageGizmos`, `extends Node2D` — hover highlight, selection outline, rotate ring handle. | -| `res://scripts/stage_grid.gd` | `class_name StageGrid`, `extends Node2D` — optional world-space grid overlay (major line every 5 cells). | -| `res://scripts/stage_placement_overlay.gd` | **Phase 4b** `class_name StagePlacementOverlay`, `extends Node2D` — world-space overlay drawing the terrain drag-painting guide line + the director action trajectory / ghost marker. | -| `res://scripts/thumbnails/stickman_thumbnail.gd` | **Phase 3b** `class_name StickmanThumbnail`, `extends Node` — renders a parsed `.stk` into a 200×200 `Texture2D` by spawning the **real rig** (`StickmanFactory.spawn_from_data`) inside an offscreen `SubViewport`, framing it and capturing after a double `frame_post_draw` (§22). | -| `res://scripts/thumbnails/prop_thumbnail.gd` | **Phase 3b** `class_name PropThumbnail`, `extends Node` — renders a prop template into a 200×200 `Texture2D` via a lightweight `Polygon2D` + `Line2D` visual (no `RigidBody2D`, so no gravity), tinted by the material preset (§22). | -| `res://scripts/thumbnails/thumbnail_cache.gd` | **Phase 3b** `class_name ThumbnailCache`, `extends RefCounted` — disk PNG cache under `user://thumbnails/`: stickman key = `basename_mtime`, prop key = `id_v`; `load_png`/`save_png`/`clean_stale_stickmen` (§22). | -| `res://scenes/asset_selector.tscn` | **Phase 3b** `PopupPanel` root + `asset_selector.gd` — minimal shell/layout skeleton (title bar, empty `GridContainer`, footer Prev/Next/Browse/Refresh/Close); all dynamic per-cell content is built in code at runtime (§22). | +| `res://scenes/stage/sandbox_stage.tscn` | The stage scene: root `Node2D` + `Camera2D` + empty `World` container. | +| `res://scripts/editors/stage/sandbox_stage.gd` | `class_name SandboxStage`, `extends Node2D` — root controller (3-mode state machine, placement + drag-painting, camera, deletion, bottom status bar, mode badge/frame/cursors, signals); **Phase 3b** instantiates the `AssetSelector` grid popup + the two thumbnail renderers, owns the selector open/close flow and the lazy per-frame thumbnail drain, and wires the Stickman/Prop palette buttons to the selector (§22). **Phase 3c** adds the editor-tool wiring: the `QueuePanel` / `RulePanel` / `ActionEditor` / `RuleEditor` / `WaypointContext` instantiation, the unified `CaptureKind` target-capture system, "Edit Queue…"/"Edit Rules…" entry points + right-click stickman/waypoint context menus, the shared confirmation dialog, and consequence-only rule editing (§23). | +| `res://scripts/stage/stage_spawner.gd` | `class_name StageSpawner`, `extends RefCounted` — registry-driven factory reusing `TerrainUtils` / `PropUtils` / `StickmanFactory`; exposes `is_terrain_id()` / `get_template_aabb()` / `spawn_id` tagging. `get_template_aabb()` returns the **sanitized** template AABB (mirrors `_spawn_terrain()`'s 16-px grid pass), so it doubles as the block-unit paint stride. **Phase 3b:** registry ids `ground/ramp/step/prop/stickman/area` (separate `crate`/`ball` entries removed); holds the session state `selected_stickman_path` / `selected_prop_id` and a per-path `_stickman_cache`; `prop` and `stickman` spawn the **selected** asset. | +| `res://scripts/characters/stickman/stickman_library.gd` | **Phase 3b** `class_name StickmanLibrary`, `extends RefCounted` — scans `res://content/stickmen/*.stk` into `{path, name, data}` entry models (corrupt/missing-`body_parts` files skipped, empty `stickman_name` → filename basename), with `make_entry(path)` for arbitrary Browse-chosen paths (§22). | +| `res://scripts/props/prop_library.gd` | **Phase 3b** `class_name PropLibrary`, `extends RefCounted` — static registry of the 4 prop templates (Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard) with their `PropUtils.create_*()` payloads + material presets; `get_default_id()` = `"crate"` (§22). | +| `res://scripts/shared/ui/asset_selector.gd` | **Phase 3b** `class_name AssetSelector`, `extends PopupPanel` — grid UI controller (root of `scenes/stage/asset_selector.tscn`): pagination 12/page (4×3), Prev/Next/page label, empty-state label, per-cell thumbnail + name + (prop) material badge, hover styling (no pre-highlight on open), window-resize re-centering, Browse/Refresh/Close wiring, Esc handling; signals `item_selected` / `cancelled` / `browse_requested` / `refresh_requested` (§22). | +| `res://scripts/stage/stage_selection.gd` | `class_name StageSelection`, `extends RefCounted` — hover/click/box selection via geometric AABB hit-testing. | +| `res://scripts/stage/stage_gizmos.gd` | `class_name StageGizmos`, `extends Node2D` — hover highlight, selection outline, rotate ring handle. | +| `res://scripts/stage/stage_grid.gd` | `class_name StageGrid`, `extends Node2D` — optional world-space grid overlay (major line every 5 cells). | +| `res://scripts/stage/stage_placement_overlay.gd` | **Phase 4b** `class_name StagePlacementOverlay`, `extends Node2D` — world-space overlay drawing the terrain drag-painting guide line + the director action trajectory / ghost marker. | +| `res://scripts/shared/thumbnails/stickman_thumbnail.gd` | **Phase 3b** `class_name StickmanThumbnail`, `extends Node` — renders a parsed `.stk` into a 200×200 `Texture2D` by spawning the **real rig** (`StickmanFactory.spawn_from_data`) inside an offscreen `SubViewport`, framing it and capturing after a double `frame_post_draw` (§22). | +| `res://scripts/shared/thumbnails/prop_thumbnail.gd` | **Phase 3b** `class_name PropThumbnail`, `extends Node` — renders a prop template into a 200×200 `Texture2D` via a lightweight `Polygon2D` + `Line2D` visual (no `RigidBody2D`, so no gravity), tinted by the material preset (§22). | +| `res://scripts/shared/thumbnails/thumbnail_cache.gd` | **Phase 3b** `class_name ThumbnailCache`, `extends RefCounted` — disk PNG cache under `user://thumbnails/`: stickman key = `basename_mtime`, prop key = `id_v`; `load_png`/`save_png`/`clean_stale_stickmen` (§22). | +| `res://scenes/stage/asset_selector.tscn` | **Phase 3b** `PopupPanel` root + `asset_selector.gd` — minimal shell/layout skeleton (title bar, empty `GridContainer`, footer Prev/Next/Browse/Refresh/Close); all dynamic per-cell content is built in code at runtime (§22). | | `res://sandbox_theme.json` | **Phase 4b** hand-editable styling defaults (font paths/sizes, grid snap default, mode accent colors). **Phase 3c** extends the `fonts` block with style variants (`ui_font_bold`/`ui_font_italic`), per-widget size/style flags, and an optional per-widget `{size, bold, italic}` object form (§21.3 / spec §14). Loaded at `_ready()`; missing/malformed falls back to built-in constants. | **Mode management** — a single **3-segment switcher** `[ ✏️ Edit | 🎬 Direct | ▶️ Play ]` sits at the far left of the top bar (`enum StageMode { EDIT, DIRECT, PLAY }`). Each mode shows a **contextual toolbar** and a **mode badge pill** in the viewport's top-left corner (`✏️ EDIT` cyan, `🎬 DIRECTING` amber, `▶️ SIMULATING` green), sourced from `sandbox_theme.json` `mode_colors`: @@ -456,7 +463,7 @@ The stage is intentionally **extendable**: the spawn palette is registry-driven - **Ground / Ramp / Step** — `TerrainBlock` terrain. Single **terrain** palette items are **drag-painted** (§21): left-click-and-drag paints a staircase run of blocks; a terrain drag commits on release. Non-terrain (props/stickman/area) palette items keep the Phase 2 **single-click repeated placement**. - **Prop** (Phase 3b) — opens a **selector grid** (§22) of the 4 prop templates; selecting one sets the `selected_prop_id` and enters placement mode spawning that `PropBlock` (`PropUtils.create_*()` + the matching material preset). Replaces the earlier separate **Crate** / **Ball** buttons. -- **Stickman** (Phase 3b) — opens a **selector grid** (§22) of every `.stk` in `res://stickmen/`; selecting one sets the `selected_stickman_path` and enters placement mode spawning a `StickmanRig` from that file (offset `(0, -385)` so the feet land on the cursor). Replaces the previous hard-coded single-`test.stk` placement. +- **Stickman** (Phase 3b) — opens a **selector grid** (§22) of every `.stk` in `res://content/stickmen/`; selecting one sets the `selected_stickman_path` and enters placement mode spawning a `StickmanRig` from that file (offset `(0, -385)` so the feet land on the cursor). Replaces the previous hard-coded single-`test.stk` placement. - **Area** (Phase 4) — a `TriggerArea` sensor. Clicking a palette button enters **placement mode**, which shows a translucent **ghost** of the object under the cursor (snapped to the grid when Snap is on); the stickman ghost is a static standing figure. **Left-click** keeps the Phase 2 repeated-placement behavior (each click/drag commits one placement and the tool stays active); **right-click** (or **Esc**) is the explicit **"put the tool down"** gesture — it cancels any in-progress drag, leaves already-placed cells if a drag had committed, and un-toggles the palette button. Each placement emits `object_placed(node)`. The **Stickman** and **Prop** buttons open a selector grid first (a modal `PopupPanel`, §22) and only enter placement mode once an asset is chosen. @@ -488,14 +495,14 @@ Clicking a palette button enters **placement mode**, which shows a translucent * ### 19. Director Tool (Phase 3a) -The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's workspace: click a stickman, choose actions from a popup, see the script as waypoints/badges in Edit mode, then press **Play** to run every stickman's action queue. It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`. +The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's workspace: click a stickman, choose actions from a popup, see the script as waypoints/badges in Edit mode, then press **Play** to run every stickman's action queue. It is **not wired into the editor** — run via **F6** on `res://scenes/stage/sandbox_stage.tscn`. | File | Purpose | |---|---| -| `res://scripts/stickman_rig.gd` | Extended with navigation/walking, speech, an action queue, and the queue runner state machine (see API below). | -| `res://scripts/stickman_speech_bubble.gd` | `class_name SpeechBubble`, `extends Node2D` — a world-space speech bubble drawn via `_draw()` (`ThemeDB.fallback_font`), a child of the rig above the head. | -| `res://scripts/stage_director_visuals.gd` | `class_name StageDirectorVisuals`, `extends Node2D` — Edit-mode director overlay (waypoint dots, dashed connectors, action badges, order numbers); hidden in Play. **Phase 3c** adds `hit_test_waypoint_action()` (rig/index/pos), `set_edit_waypoint()`/`clear_edit_waypoint()` with the pulsing edit highlight for visual walk re-placement, and rule-label → editor routing (§23). | -| `res://scripts/sandbox_stage.gd` | Extended with the **Direct** palette button, the action popup + speak/wait dialogs, a code-built `NavigationRegion2D` re-baked on terrain edits, and Play mode now starting each stickman's queue. | +| `res://scripts/characters/stickman/stickman_rig.gd` | Extended with navigation/walking, speech, an action queue, and the queue runner state machine (see API below). | +| `res://scripts/speech/stickman_speech_bubble.gd` | `class_name SpeechBubble`, `extends Node2D` — a world-space speech bubble drawn via `_draw()` (`ThemeDB.fallback_font`), a child of the rig above the head. | +| `res://scripts/stage/stage_director_visuals.gd` | `class_name StageDirectorVisuals`, `extends Node2D` — Edit-mode director overlay (waypoint dots, dashed connectors, action badges, order numbers); hidden in Play. **Phase 3c** adds `hit_test_waypoint_action()` (rig/index/pos), `set_edit_waypoint()`/`clear_edit_waypoint()` with the pulsing edit highlight for visual walk re-placement, and rule-label → editor routing (§23). | +| `res://scripts/editors/stage/sandbox_stage.gd` | Extended with the **Direct** palette button, the action popup + speak/wait dialogs, a code-built `NavigationRegion2D` re-baked on terrain edits, and Play mode now starting each stickman's queue. | **Direct tool workflow (Edit):** @@ -535,7 +542,7 @@ The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's - Terrain adaptation is enabled only in **Play**. `_enter_play_mode()` calls `begin_terrain_fall()` before `start_queue()`. An unsupported animated rig free-falls kinematically with bounded gravity; walking animation pauses while airborne and resumes on landing. EDIT and DIRECT disable the controller and restore the authored transform/standing pose. - The center support sample uses three downward rays at horizontal offsets `[-24, 0, +24]`; each lower-leg endpoint uses an independent three-ray fan at `[-4, 0, +4]`. A hit must be a `TerrainBlock` with an upward-facing normal, and the rig's own collision-proxy RIDs are excluded. The highest valid surface wins, preventing a lower floor under a ramp from pulling a leg through the ramp. -- `master_rig.tscn` separates animated `AnimationTargets/Left_Leg_Base` and `Right_Leg_Base` markers from live `IK_Targets/Left_Leg` and `Right_Leg` targets. The rig blends complementary continuous stance/swing weights, holds stance contacts in world space, and applies a predictive 24 px foot guard for smooth contact transitions. Root elevation accumulates across ramps and steps, with centerline pelvis compensation; no foot/ankle bones are added. +- `stickman_rig.tscn` separates animated `AnimationTargets/Left_Leg_Base` and `Right_Leg_Base` markers from live `IK_Targets/Left_Leg` and `Right_Leg` targets. The rig blends complementary continuous stance/swing weights, holds stance contacts in world space, and applies a predictive 24 px foot guard for smooth contact transitions. Root elevation accumulates across ramps and steps, with centerline pelvis compensation; no foot/ankle bones are added. - Navigation uses shallow ribbons over exposed upward-facing TerrainBlock edges, plus bridges between surface endpoints within 64 px horizontally and 96 px vertically for valid steps. The `NavigationAgent2D` remains at local `(0, +385)` and uses the selected walk speed; navigation supplies horizontal progress while terrain following owns vertical root motion and arrival avoids Y-correction jitter. **Nav-mesh behavior:** @@ -544,7 +551,7 @@ The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's - **Auto re-bake** on any terrain edit: the nav mesh is marked dirty on terrain place / move / rotate (`transform_committed`) / delete and re-baked once per frame (coalescing bursts) in `_process`. - Each stickman's `NavigationAgent2D` is a child of the rig **at the feet** (local `(0, +385)` = `-FOOT_OFFSET`), so it sits on the ground-level mesh; agent and region share the default navigation map, layer 1. - `walk_to(target)` treats `target` as a **feet/ground destination**; the rig converts ground-level path points back to root positions with `FOOT_OFFSET := (0, -385)`. Walking is **hybrid**: an **on-mesh target** follows the nav path (`_walk_mode = "nav"`), while an **off-mesh / unreachable target** (empty path **or** `is_target_reachable() == false`) switches to **direct straight-line steering** toward the clicked waypoint (`_walk_mode = "direct"`, root target = waypoint + `FOOT_OFFSET`) — it is **not** rejected with a warning and the rig does **not** stand still. Nav reads are deferred until the map has synced (`map_get_iteration_id(...) != 0`) and the path query is forced via `get_next_path_position()` before any reachability/finished check, so a fresh target is never misjudged as finished/unreachable after a single step. -- Pathing is kinematic (`global_position.move_toward`); `avoidance_enabled = false` in 3a, so stickmen path through props and each other (deferred — see `docs/tech_debt_and_optimizations.md` #13). +- Pathing is kinematic (`global_position.move_toward`); `avoidance_enabled = false` in 3a, so stickmen path through props and each other (deferred — see `docs/architecture/tech_debt_and_optimizations.md` #13). - **Walk/nav debugging (off by default):** `const DEBUG_WALK` in `stickman_rig.gd` (per-frame walk trace with `mode=nav|direct`, plus one-shot `walk_to`/finish/cancel prints) and `const DEBUG_STAGE` in `sandbox_stage.gd` (`[stage]` target capture, `[nav]` baked verts/polys, `[stage] PLAY rigs`). **StickmanRig director API surface:** @@ -571,19 +578,19 @@ The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's ### 20. Triggers & Event System (Phase 4) -**Phase 4** adds **reactive storytelling** to the Sandbox Stage. While Phase 3a gave directors *sequential* control (actions in a fixed order per stickman), Phase 4 adds *reactive* control — **"When X happens, do Y"** — via event rules that span objects. It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`. +**Phase 4** adds **reactive storytelling** to the Sandbox Stage. While Phase 3a gave directors *sequential* control (actions in a fixed order per stickman), Phase 4 adds *reactive* control — **"When X happens, do Y"** — via event rules that span objects. It is **not wired into the editor** — run via **F6** on `res://scenes/stage/sandbox_stage.tscn`. The rule concept is **When → Then**: a *trigger* (an event on some object) fires a list of *actions* on a target object. Rules are stored as `_event_rules: Array[Dictionary]` of `{id, trigger, actions}` in `sandbox_stage.gd`, where `trigger` carries `{type, source, target, ...}` instance references and `actions` is an array of Phase 3a action dicts (`{"type":"walk_to","target":...}`, etc.). | File | Purpose | |---|---| -| `res://scripts/trigger_area.gd` | NEW `class_name TriggerArea`, `extends Node2D` — a placeable sensor (`@export size: Vector2`, default 96×96) with a centered **local-space** `get_area_rect() -> Rect2` and a translucent green `_draw()` fill + dashed border. **No physics, no signals** — it is a pure geometric region evaluated by the stage. | -| `res://scripts/sandbox_stage.gd` | Extended with the rule system: `_event_rules`, the geometric area-entry engine, physics contact routing for stickman/prop collisions (Phase 4.5), the rule-builder UI state machine, and rule auto-cleanup. | -| `res://scripts/stickman_rig.gd` | `arrived` gained a `target: Vector2` payload; new `enqueue_reactive(actions)` appends reactive actions without replaying the queue. | -| `res://scripts/prop_block.gd` | NEW physics contact signals `collided(other, contact_normal, contact_position)` and `separated(other)`; prop-vs-prop and Phase 4.5 stickman contacts share the contact-monitoring path. | -| `res://scripts/stage_director_visuals.gd` | Extended with rule visualization + `set_rules()` / `hit_test_rule()` / `hit_test_waypoint()`. | -| `res://scripts/stage_spawner.gd` | New `"area"` palette registry entry (label **"Area"**); duck-typed `get_area_rect` AABB branch in `get_world_aabb`. | -| `res://scripts/stage_selection.gd` | Same duck-typed `get_area_rect` AABB branch for selecting/clicking trigger areas. | +| `res://scripts/stage/trigger_area.gd` | NEW `class_name TriggerArea`, `extends Node2D` — a placeable sensor (`@export size: Vector2`, default 96×96) with a centered **local-space** `get_area_rect() -> Rect2` and a translucent green `_draw()` fill + dashed border. **No physics, no signals** — it is a pure geometric region evaluated by the stage. | +| `res://scripts/editors/stage/sandbox_stage.gd` | Extended with the rule system: `_event_rules`, the geometric area-entry engine, physics contact routing for stickman/prop collisions (Phase 4.5), the rule-builder UI state machine, and rule auto-cleanup. | +| `res://scripts/characters/stickman/stickman_rig.gd` | `arrived` gained a `target: Vector2` payload; new `enqueue_reactive(actions)` appends reactive actions without replaying the queue. | +| `res://scripts/props/prop_block.gd` | NEW physics contact signals `collided(other, contact_normal, contact_position)` and `separated(other)`; prop-vs-prop and Phase 4.5 stickman contacts share the contact-monitoring path. | +| `res://scripts/stage/stage_director_visuals.gd` | Extended with rule visualization + `set_rules()` / `hit_test_rule()` / `hit_test_waypoint()`. | +| `res://scripts/stage/stage_spawner.gd` | New `"area"` palette registry entry (label **"Area"**); duck-typed `get_area_rect` AABB branch in `get_world_aabb`. | +| `res://scripts/stage/stage_selection.gd` | Same duck-typed `get_area_rect` AABB branch for selecting/clicking trigger areas. | **Trigger types:** @@ -636,7 +643,7 @@ Edge-triggered dictionaries (which events already fired) are reset on Play/Edit ### 20.2 Phase 4.5 — Per-Part Collision Bodies and Hit Reactions -Phase 4.5 gives an ANIMATED `StickmanRig` a physical collision surface in the Sandbox Stage. It is runtime-only and does not change `master_rig.tscn`, the `.stk` format, or the editor layout. +Phase 4.5 gives an ANIMATED `StickmanRig` a physical collision surface in the Sandbox Stage. It is runtime-only and does not change `stickman_rig.tscn`, the `.stk` format, or the editor layout. - `StickmanRig` builds ten metadata-tagged `CollisionBody` `AnimatableBody2D` proxies (head, torso, and eight limbs). They are top-level relative to their `Body/*` visual containers, use layer/mask `1`, set `sync_to_physics = false`, and copy each rendered visual's global transform in `_sync_collision_bodies()` every physics frame. This explicit synchronization keeps proxies aligned with `RemoteTransform2D`-driven geometry during poses and WALK-left mirroring. - Limb and torso proxies use fitted `CapsuleShape2D`s in the adapter's hanging frame. The head uses a `CircleShape2D` centered from the mounted head geometry's local bounds, with a minimum radius of `90 px` and growth when the mounted geometry is larger. These are intentionally approximate collision shapes, not per-shape polygons. @@ -649,7 +656,7 @@ Focused verification: `tests/test_phase45_collision_bodies.gd` (223 assertions), ### 21. Phase 4b Polish — Mode Switcher, Theme, Terrain Painting, Director UX, Walk Jitter Fix -**Phase 4b** is a **polish + bugfix** pass over the Sandbox Stage Builder. It does **not** add new gameplay systems; it restructures the top bar into a **3-segment mode switcher** with contextual toolbars, adds a **bottom status bar** with live mouse coordinates, a **mode badge/frame** and per-mode cursors, upgrades **terrain placement** from single-click into **drag-to-paint** with a grid spatial dictionary, makes the **director's "pick a target" flows** kid-friendly (cursor-attached tooltip, rubber-band trajectory, reticle cursor), introduces a hand-editable **`res://sandbox_theme.json`**, and fixes the **walk-waypoint arrival jitter**. Run via **F6** on `res://scenes/sandbox_stage.tscn`; not wired into the editor. See §18 for the mode-switcher / status-bar / badge / cursor surface; this section details the placement, styling, director-UX, and bugfix internals. +**Phase 4b** is a **polish + bugfix** pass over the Sandbox Stage Builder. It does **not** add new gameplay systems; it restructures the top bar into a **3-segment mode switcher** with contextual toolbars, adds a **bottom status bar** with live mouse coordinates, a **mode badge/frame** and per-mode cursors, upgrades **terrain placement** from single-click into **drag-to-paint** with a grid spatial dictionary, makes the **director's "pick a target" flows** kid-friendly (cursor-attached tooltip, rubber-band trajectory, reticle cursor), introduces a hand-editable **`res://sandbox_theme.json`**, and fixes the **walk-waypoint arrival jitter**. Run via **F6** on `res://scenes/stage/sandbox_stage.tscn`; not wired into the editor. See §18 for the mode-switcher / status-bar / badge / cursor surface; this section details the placement, styling, director-UX, and bugfix internals. #### 21.1 Terrain drag-painting (Edit mode) @@ -673,7 +680,7 @@ Block centers are `block_cell * stride`, and Bresenham runs over block units. A 5. **Release** commits the batch **atomically**: only the "empty" block units spawn (all in one frame), the nav mesh is marked dirty **once** (`_nav_dirty = true`, not per node), the grid dictionary is rebuilt, and `object_placed(node)` fires per node. LMB keeps repeated placement active for the next drag (the placement ghost re-arms). 6. **RMB** (or **Esc**) ends draw/placement mode — it cancels an in-progress drag (nothing is placed until release), clears the guide line, and un-toggles the palette button and restores the cursor. -**Grid spatial dictionary** (`_grid_cells`): a runtime `Dictionary` on `SandboxStage` keyed by 16-px grid-cell `Vector2i` (`StageSpawner.TERRAIN_GRID_SIZE`) → `Array[Node2D]`. It is **advisory only** — it drives the 3-state occupancy query (green/neutral/red tint + skip) but is **never authoritative for physics** or for the block-unit paint stride; the `World` tree is the source of truth. Occupancy marks **all 16-px cells covered by the node's world AABB** via `_rasterize_aabb_to_cells()` (for props/stickmen/areas it rasterizes `StageSelection.get_world_aabb()`), so one Ground block (192×32) spans ≈ 12×2 dictionary cells even though it paints as a single block unit. It is populated on place, updated on move/rotate/delete, and rebuilt on grid-size change. Terrain nodes carry a `spawn_id` string (`TerrainBlock.spawn_id`, set by `StageSpawner.spawn_id`) so same-template overlaps are detectable; `StageSpawner.is_terrain_id()` distinguishes terrain registry entries and `get_template_aabb(id)` exposes a terrain template's **sanitized** local AABB for ghost sizing / block-unit stride / cell rasterization. +**Grid spatial dictionary** (`_grid_cells`): a runtime `Dictionary` on `SandboxStage` keyed by 16-px grid-cell `Vector2i` (`StageSpawner.TERRAIN_GRID_SIZE`) → `Array[Node2D]`. It is **advisory only** — it drives the 3-state occupancy query (green/neutral/red tint + skip) but is **never authoritative for physics** or for the block-unit paint stride; the `World` tree is the source of truth. Occupancy marks **all 16-px cells covered by the node's world AABB** via `_rasterize_aabb_to_cells()` (for content/props/stickmen/areas it rasterizes `StageSelection.get_world_aabb()`), so one Ground block (192×32) spans ≈ 12×2 dictionary cells even though it paints as a single block unit. It is populated on place, updated on move/rotate/delete, and rebuilt on grid-size change. Terrain nodes carry a `spawn_id` string (`TerrainBlock.spawn_id`, set by `StageSpawner.spawn_id`) so same-template overlaps are detectable; `StageSpawner.is_terrain_id()` distinguishes terrain registry entries and `get_template_aabb(id)` exposes a terrain template's **sanitized** local AABB for ghost sizing / block-unit stride / cell rasterization. #### 21.2 Director targeting UX (Direct / rule-building) @@ -689,7 +696,7 @@ The Sandbox theme is schema `2.0` and now covers the code-built chrome, palette, selectors, dialogs, overlays, reusable widget data, fonts, colors, and grid defaults. See the complete current key reference, examples, legacy compatibility notes, and troubleshooting guide in -[docs/SANDBOX_THEME.md](docs/SANDBOX_THEME.md). +[docs/architecture/SANDBOX_THEME.md](docs/architecture/SANDBOX_THEME.md). The short version: edit `res://sandbox_theme.json`, restart the Sandbox scene to reload it, and remember that the live grid size/snap/grid-visibility remain @@ -711,13 +718,13 @@ The Phase 3a walk could **jitter up/down at a waypoint** instead of stopping. Ro The result: `mode` stays constant for the whole walk, exactly one `arrived` fires, the rig's `global_position` is unchanged after arrival, and the walk/body-bob animation stops cleanly. `DEBUG_WALK` / `DEBUG_STAGE` (off by default) can be enabled to capture the `mode`/`dist`/`next`/`final` trace at arrival. -#### 21.5 Head LookAt solver fix (`master_rig.tscn`) +#### 21.5 Head LookAt solver fix (`stickman_rig.tscn`) -`master_rig.tscn`'s Head `SkeletonModification2DLookAt` now uses a **full-range, non-inverted band solved in global space** (`constraint_angle_min = -180`, `constraint_angle_max = 180`, `constraint_invert = false`, `constraint_in_localspace = false`). Under the previous ~55°-clamped, inverted, local-space band, the aim solver could oscillate frame-to-frame as the look direction crossed the band boundary, causing a per-frame **mirror** of the head rather than smooth convergence. With the full-range global band there is no boundary to cross, so dragging the Head IK handle converges cleanly onto the aim point (see §14 Phase 9 Round 7). +`stickman_rig.tscn`'s Head `SkeletonModification2DLookAt` now uses a **full-range, non-inverted band solved in global space** (`constraint_angle_min = -180`, `constraint_angle_max = 180`, `constraint_invert = false`, `constraint_in_localspace = false`). Under the previous ~55°-clamped, inverted, local-space band, the aim solver could oscillate frame-to-frame as the look direction crossed the band boundary, causing a per-frame **mirror** of the head rather than smooth convergence. With the full-range global band there is no boundary to cross, so dragging the Head IK handle converges cleanly onto the aim point (see §14 Phase 9 Round 7). ### 22. Asset Library — Stickman & Prop Selector Grids (Phase 3b) -**Phase 3b** replaces the Sandbox Stage Builder's hard-coded single-stickman and single-prop palette buttons (**Crate** / **Ball** / always-`test.stk` **Stickman**) with **visual selector grids**: clicking **Stickman** or **Prop** opens a modal grid popup (`PopupPanel`) of selectable assets; picking one sets the spawner's session selection and enters placement mode with that asset's ghost. It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`. **No `.stk` format change** (the `.stk` schema below is untouched this phase). Pixel rendering (thumbnails) is **manual/F6 verification only**; headless tests never assert on pixels. +**Phase 3b** replaces the Sandbox Stage Builder's hard-coded single-stickman and single-prop palette buttons (**Crate** / **Ball** / always-`test.stk` **Stickman**) with **visual selector grids**: clicking **Stickman** or **Prop** opens a modal grid popup (`PopupPanel`) of selectable assets; picking one sets the spawner's session selection and enters placement mode with that asset's ghost. It is **not wired into the editor** — run via **F6** on `res://scenes/stage/sandbox_stage.tscn`. **No `.stk` format change** (the `.stk` schema below is untouched this phase). Pixel rendering (thumbnails) is **manual/F6 verification only**; headless tests never assert on pixels. **Workflow:** press the **Stickman** or **Prop** palette button → the grid opens (modal `PopupPanel`, ESC-closeable) → click a cell → the selection is cached session-only, the grid closes, and placement mode begins (ghost of the selected asset appears). Placing repeats the **selected** asset; switching selection re-opens the grid. `Ground`/`Ramp`/`Step` and `Area` remain direct placement buttons, unchanged. @@ -725,12 +732,12 @@ The result: `mode` stays constant for the whole walk, exactly one `arrived` fire | Grid | Backing | Cells | Details | |---|---|---|---| -| **Stickman** | `StickmanLibrary.scan()` of `res://stickmen/*.stk` | one per `.stk` (name = `stickman_name` else filename basename) | Rig-rendered thumbnail per cell; **Browse…** (`*.stk` `FileDialog`) selects an arbitrary path; **Refresh** rescans; **empty-state** label when no files; **single-item skip** — exactly one file bypasses the grid and places directly | +| **Stickman** | `StickmanLibrary.scan()` of `res://content/stickmen/*.stk` | one per `.stk` (name = `stickman_name` else filename basename) | Rig-rendered thumbnail per cell; **Browse…** (`*.stk` `FileDialog`) selects an arbitrary path; **Refresh** rescans; **empty-state** label when no files; **single-item skip** — exactly one file bypasses the grid and places directly | | **Prop** | `PropLibrary` (4 static templates) | Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard | Thumbnail + name + **material badge**; always a single page | **Selection is session-only:** the chosen `selected_stickman_path` / `selected_prop_id` live in memory on `StageSpawner`, persist across `EDIT ⇄ DIRECT ⇄ PLAY` toggles, and reset on scene reload. **No disk save** (`user://sandbox_settings.json` is not extended). If the currently selected stickman path is no longer in the scan, the first entry is selected instead on open/refresh. -**Selector grid (`AssetSelector` / `scenes/asset_selector.tscn`):** a `PopupPanel` root (`exclusive = true`) built as a minimal shell — authored title bar, empty `GridContainer`, footer (`Prev` / page label / `Next` / `Browse…` / `Refresh` / `Close`) — with **all dynamic per-cell content** (texture + name + material badge) built in code at runtime. `open(kind, entries)` titles the popup ("Choose Your Stickman" / "Choose a Prop"), hides Browse/Refresh for props, and `popup_centered()`. Pagination slices **12/page (4×3)** with Prev/Next hidden when a single page; a static pure `page_bounds(total, page, page_size)` helper backs the slicing. Empty state shows a "no stickmen found" label instead of an empty grid. No cell is pre-highlighted on open (the previous selection-highlight border was removed). The popup re-centers on window resize (`size_changed` → `popup_centered()` while visible). Signals: `item_selected(entry)`, `cancelled()`, `browse_requested()`, `refresh_requested()`. +**Selector grid (`AssetSelector` / `scenes/stage/asset_selector.tscn`):** a `PopupPanel` root (`exclusive = true`) built as a minimal shell — authored title bar, empty `GridContainer`, footer (`Prev` / page label / `Next` / `Browse…` / `Refresh` / `Close`) — with **all dynamic per-cell content** (texture + name + material badge) built in code at runtime. `open(kind, entries)` titles the popup ("Choose Your Stickman" / "Choose a Prop"), hides Browse/Refresh for props, and `popup_centered()`. Pagination slices **12/page (4×3)** with Prev/Next hidden when a single page; a static pure `page_bounds(total, page, page_size)` helper backs the slicing. Empty state shows a "no stickmen found" label instead of an empty grid. No cell is pre-highlighted on open (the previous selection-highlight border was removed). The popup re-centers on window resize (`size_changed` → `popup_centered()` while visible). Signals: `item_selected(entry)`, `cancelled()`, `browse_requested()`, `refresh_requested()`. **Thumbnails (rig-accurate, cached):** each 200×200 thumbnail is rendered **lazily, one per frame**, by a `Node` renderer owning a persistent offscreen `SubViewport` (`transparent_bg`, `UPDATE_ALWAYS`) with an enabled in-viewport `Camera2D`: @@ -749,30 +756,30 @@ The result: `mode` stays constant for the whole walk, exactly one `arrived` fire **Palette integration & Esc priority:** `_on_palette_toggled` routes `stickman`/`prop` presses to `_open_selector(id)` (not straight to `set_placement_mode`); un-pressing while the matching selector is open closes it. While the grid is open a **dim backdrop** (`_selector_dim`, a black `ColorRect` at `SELECTOR_DIM_ALPHA` = 0.5, mouse-ignoring, on the UI `CanvasLayer` behind the selector) is shown. `_open_selector` forces the palette button pressed, performs the single-item skip, and (for stickman) reselects the first entry when the selected path is absent. On `item_selected` the spawner's `selected_stickman_path`/`selected_prop_id` are set, the selector closes, and placement begins. `Browse…` builds a one-off entry via `StickmanLibrary.make_entry(path)` (toast on failure). Esc is handled in the `AssetSelector` (`_unhandled_input` → `cancelled`) and by the stage in its existing **Esc priority chain** — rule step → pending walk target → terrain drag → **selector** → DIRECT → placement → selection. An outside-click that closes the modal popup fires its `popup_hide` signal, which is routed to `_on_selector_cancelled()` (idempotency-guarded) so it likewise un-presses the palette button. While the selector is open, `_handle_world_click` / `_handle_mouse_motion` early-return (belt-and-suspenders on top of the modal `PopupPanel`). -**New scripts:** `stickman_library.gd` / `prop_library.gd` / `thumbnails/thumbnail_cache.gd` / `thumbnails/stickman_thumbnail.gd` / `thumbnails/prop_thumbnail.gd` / `asset_selector.gd` (+ `scenes/asset_selector.tscn`). **Modified:** `stage_spawner.gd` (registry `crate`/`ball` → `prop`; `selected_stickman_path`/`selected_prop_id` session state; per-path `_stickman_cache`; new `get_selected_stickman_path()`/`get_selected_prop_id()` getters), `sandbox_stage.gd` (selector integration), and `tests/test_phase4b1_fixes.gd` (`spawn("crate")` → `spawn("prop")`). +**New scripts:** `stickman_library.gd` / `prop_library.gd` / `thumbnails/thumbnail_cache.gd` / `thumbnails/stickman_thumbnail.gd` / `thumbnails/prop_thumbnail.gd` / `asset_selector.gd` (+ `scenes/stage/asset_selector.tscn`). **Modified:** `stage_spawner.gd` (registry `crate`/`ball` → `prop`; `selected_stickman_path`/`selected_prop_id` session state; per-path `_stickman_cache`; new `get_selected_stickman_path()`/`get_selected_prop_id()` getters), `sandbox_stage.gd` (selector integration), and `tests/test_phase4b1_fixes.gd` (`spawn("crate")` → `spawn("prop")`). -**Verification:** new headless suite `tests/test_phase3b_library.gd` (`extends SceneTree`, no pixel assertions) covering `StickmanLibrary.scan()`/corrupt-skip/`make_entry`, `PropLibrary.get_entries()`/`get_default_id()`, the `StageSpawner` registry ids (`ground/ramp/step/prop/stickman/area`), `_spawn_prop`/`_spawn_stickman` honoring `selected_prop_id`/`selected_stickman_path`, `ThumbnailCache` key/path formatting, `AssetSelector` pagination math (`PAGE_SIZE == 12`), and scene-load checks. Spec: `docs/phase_3b_asset_grid_spec.md`. +**Verification:** new headless suite `tests/test_phase3b_library.gd` (`extends SceneTree`, no pixel assertions) covering `StickmanLibrary.scan()`/corrupt-skip/`make_entry`, `PropLibrary.get_entries()`/`get_default_id()`, the `StageSpawner` registry ids (`ground/ramp/step/prop/stickman/area`), `_spawn_prop`/`_spawn_stickman` honoring `selected_prop_id`/`selected_stickman_path`, `ThumbnailCache` key/path formatting, `AssetSelector` pagination math (`PAGE_SIZE == 12`), and scene-load checks. Spec: `docs/specs/phase_3b_asset_grid_spec.md`. ### 23. Editor Tools — Action & Rule Editing (Phase 3c) -**Phase 3c** makes the Sandbox Stage's **action queues and event rules fully editable**. Where Phase 3a let directors only *append* actions and Phase 4 only *create/delete* rules, Phase 3c adds edit / delete / drag-reorder for both, a **waypoint right-click context menu** with visual walk re-placement and insert-before/after, and both a **full** and a **consequence-only** rule editor. The entire system is **registry-driven** — the action and trigger templates live in two const registries, and the panels/editors generate their UI from them, so a future action/trigger type is a one-entry append. It is **not wired into the editor** — run via **F6** on `res://scenes/sandbox_stage.tscn`. **No `.stk` format change.** +**Phase 3c** makes the Sandbox Stage's **action queues and event rules fully editable**. Where Phase 3a let directors only *append* actions and Phase 4 only *create/delete* rules, Phase 3c adds edit / delete / drag-reorder for both, a **waypoint right-click context menu** with visual walk re-placement and insert-before/after, and both a **full** and a **consequence-only** rule editor. The entire system is **registry-driven** — the action and trigger templates live in two const registries, and the panels/editors generate their UI from them, so a future action/trigger type is a one-entry append. It is **not wired into the editor** — run via **F6** on `res://scenes/stage/sandbox_stage.tscn`. **No `.stk` format change.** **New scripts:** | File | `class_name` / extends | Responsibility | |---|---|---| -| `res://scripts/action_registry.gd` | `ActionRegistry` / `RefCounted` | Registry of the 5 action templates (`walk_to`/`speak`/`wait`/`ragdoll`/`recover`: label, icon, param spec). Static accessors `types()` / `has_type` / `label` / `icon`, plus `to_rule_action()` / `from_rule_action()` (flat queue-action ⇄ rule-action shape conversions) and `summarize()`. | -| `res://scripts/trigger_registry.gd` | `TriggerRegistry` / `RefCounted` | Registry of the 5 trigger templates (label, icon, `target_type`: `waypoint`/`action_type`/`none`/`area`/`prop`). Static `types()` / `has_type` / `label` / `icon` / `target_type` / `summarize()`. | -| `res://scripts/queue_panel.gd` | `QueuePanel` / `PopupPanel` | Action Queue panel (scrollable list of one stickman's actions, each with ✎ / ✕ / drag-to-reorder ≡). Mutations delegate to the stage via signals; reorders call the rig's `remove_action`/`insert_action`. Root of `scenes/queue_panel.tscn`. | -| `res://scripts/rule_panel.gd` | `RulePanel` / `PopupPanel` | Rule list panel filtered by the stage (source stickman **or** a waypoint), with ✎ / ✕ / drag-to-reorder and **Add Rule** / **Clear All**. Reordering emits the new order of the *displayed* rule ids; the stage maps them back onto its full `_event_rules`, preserving un-filtered rules' positions. Root of `scenes/rule_panel.tscn`. | -| `res://scripts/action_editor.gd` | `ActionEditor` / `PopupPanel` | Single-action property editor (add & edit). Type dropdown + param fields are generated from `ActionRegistry`. A `walk_to` target is captured on the stage (`target_requested()` → the stage hides the editor, captures a click, calls `set_walk_target()`). | -| `res://scripts/rule_editor.gd` | `RuleEditor` / `PopupPanel` | Rule editor in two modes — **`full`** (trigger type + target + actions all editable) and **`consequence`** (trigger read-only; only the actions editable). Trigger targets and action actors are captured on the stage via signals. | -| `res://scripts/waypoint_context.gd` | `WaypointContext` / `PopupMenu` | Right-click menu for a `walk_to` waypoint: **✎ Edit this Walk**, **✕ Delete this Walk**, **⬆ Insert action before**, **⬇ Insert action after**, and **⚡ Edit Trigger Rules** (enabled + shows a count when rules target this waypoint). Item ids: `EDIT_WALK`/`DELETE_WALK`/`INSERT_BEFORE`/`INSERT_AFTER`/`EDIT_TRIGGER_RULES`. | -| `res://scenes/queue_panel.tscn`, `rule_panel.tscn`, `action_editor.tscn`, `rule_editor.tscn` | minimal shells | Each `.tscn` is a bare `PopupPanel` + root script; **all UI is built in code** at `_ready()` (consistent with the `asset_selector.tscn` pattern). | +| `res://scripts/stage/action_registry.gd` | `ActionRegistry` / `RefCounted` | Registry of the 5 action templates (`walk_to`/`speak`/`wait`/`ragdoll`/`recover`: label, icon, param spec). Static accessors `types()` / `has_type` / `label` / `icon`, plus `to_rule_action()` / `from_rule_action()` (flat queue-action ⇄ rule-action shape conversions) and `summarize()`. | +| `res://scripts/stage/trigger_registry.gd` | `TriggerRegistry` / `RefCounted` | Registry of the 5 trigger templates (label, icon, `target_type`: `waypoint`/`action_type`/`none`/`area`/`prop`). Static `types()` / `has_type` / `label` / `icon` / `target_type` / `summarize()`. | +| `res://scripts/editors/stage/queue_panel.gd` | `QueuePanel` / `PopupPanel` | Action Queue panel (scrollable list of one stickman's actions, each with ✎ / ✕ / drag-to-reorder ≡). Mutations delegate to the stage via signals; reorders call the rig's `remove_action`/`insert_action`. Root of `scenes/editors/stage/queue_panel.tscn`. | +| `res://scripts/editors/stage/rule_panel.gd` | `RulePanel` / `PopupPanel` | Rule list panel filtered by the stage (source stickman **or** a waypoint), with ✎ / ✕ / drag-to-reorder and **Add Rule** / **Clear All**. Reordering emits the new order of the *displayed* rule ids; the stage maps them back onto its full `_event_rules`, preserving un-filtered rules' positions. Root of `scenes/editors/stage/rule_panel.tscn`. | +| `res://scripts/editors/stage/action_editor.gd` | `ActionEditor` / `PopupPanel` | Single-action property editor (add & edit). Type dropdown + param fields are generated from `ActionRegistry`. A `walk_to` target is captured on the stage (`target_requested()` → the stage hides the editor, captures a click, calls `set_walk_target()`). | +| `res://scripts/editors/stage/rule_editor.gd` | `RuleEditor` / `PopupPanel` | Rule editor in two modes — **`full`** (trigger type + target + actions all editable) and **`consequence`** (trigger read-only; only the actions editable). Trigger targets and action actors are captured on the stage via signals. | +| `res://scripts/stage/waypoint_context.gd` | `WaypointContext` / `PopupMenu` | Right-click menu for a `walk_to` waypoint: **✎ Edit this Walk**, **✕ Delete this Walk**, **⬆ Insert action before**, **⬇ Insert action after**, and **⚡ Edit Trigger Rules** (enabled + shows a count when rules target this waypoint). Item ids: `EDIT_WALK`/`DELETE_WALK`/`INSERT_BEFORE`/`INSERT_AFTER`/`EDIT_TRIGGER_RULES`. | +| `res://scenes/editors/stage/queue_panel.tscn`, `rule_panel.tscn`, `action_editor.tscn`, `rule_editor.tscn` | minimal shells | Each `.tscn` is a bare `PopupPanel` + root script; **all UI is built in code** at `_ready()` (consistent with the `asset_selector.tscn` pattern). | **Modified scripts:** `sandbox_stage.gd` (Phase 3c wiring) and `stage_director_visuals.gd` (waypoint action hit-testing + pulsing edit highlight + rule-label click → editor). -**Theming:** the four Phase 3c widgets above are themeable — each accepts an `apply_font(ui_font, emoji_font, sizes)` call driven by the extended `sandbox_theme.json` `fonts` block (style variants `ui_font_bold`/`ui_font_italic`, per-widget `queue/rule_panel_font_size`, `action/rule_editor_font_size`, `panel_row_font_size`/`panel_title_font_size`, style flags `panel_title_bold`/`rule_label_bold`/`badge_bold`, and the optional `action_popup` `{size, bold, italic}` object form; `action_popup_emoji_size` is now consumed). See §21.3 and `docs/phase_3c_editor_spec.md` §14. +**Theming:** the four Phase 3c widgets above are themeable — each accepts an `apply_font(ui_font, emoji_font, sizes)` call driven by the extended `sandbox_theme.json` `fonts` block (style variants `ui_font_bold`/`ui_font_italic`, per-widget `queue/rule_panel_font_size`, `action/rule_editor_font_size`, `panel_row_font_size`/`panel_title_font_size`, style flags `panel_title_bold`/`rule_label_bold`/`badge_bold`, and the optional `action_popup` `{size, bold, italic}` object form; `action_popup_emoji_size` is now consumed). See §21.3 and `docs/specs/phase_3c_editor_spec.md` §14. **Entry points:** @@ -799,7 +806,7 @@ The result: `mode` stays constant for the whole walk, exactly one `arrived` fire & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase3c_editor.gd --path . ``` -Spec: `docs/phase_3c_editor_spec.md`. +Spec: `docs/specs/phase_3c_editor_spec.md`. ### 24. Prop Interactions and Throw Planning (Phase 11) @@ -876,7 +883,7 @@ Files are UTF-8 JSON, pretty-printed with tab indentation. The format is version | `version` | `string` | Format version. Currently `"1.5"`. Loading supports `"1.0"`–`"1.5"` (auto-migrated). | | `stickman_name` | `string` | Optional display name for the figure. | | `part_order` | `array[string]` | **Phase 5.** Render/Z-order of parts in the Whole Stickman preview, front-to-back semantics per array position (first = back, last = front). Absent on v1.0–v1.2 files; defaults to the internal part-key order when missing. | -| `proportions` | `object` | **Phase 8.** Rig bone lengths used by the runtime `StkRigAdapter`. Object with 5 float keys: `upper_arm_length` (168.0), `lower_arm_length` (200.0), `upper_leg_length` (200.0), `lower_leg_length` (200.0), `torso_length` (391.5). Hardcoded **master-rig rest-pose constants** (from `master_rig.tscn`), not measured from the user's shapes. Write-only metadata — never read back on load. | +| `proportions` | `object` | **Phase 8.** Rig bone lengths used by the runtime `StkRigAdapter`. Object with 5 float keys: `upper_arm_length` (168.0), `lower_arm_length` (200.0), `upper_leg_length` (200.0), `lower_leg_length` (200.0), `torso_length` (391.5). Hardcoded **master-rig rest-pose constants** (from `stickman_rig.tscn`), not measured from the user's shapes. Write-only metadata — never read back on load. | | `body_parts` | `object` | Map of `body_part_name → shape` objects. Keyed by the 10 internal part names below. | | `metadata.created_at` | `string` | Timestamp written on save. | | `metadata.modified_at` | `string` | Timestamp written on save. | @@ -915,7 +922,7 @@ Each entry in the `shapes` array: ## File format (`.prop`) -The **Prop Editor** (Phase 9, extended by Phase 10a) authors vector prop assets saved as `.prop` files, stored under `res://props/*.prop`. Files are UTF-8 JSON, pretty-printed with tab indentation, versioned, and designed to remain **backward compatible** — the format stores the editor's full multi-shape stack, while a deterministic converter (`PropFile.flatten_to_payload()`) collapses it into the single-shape payload `PropUtils.spawn_prop()` consumes at runtime (frontmost closed polygon → else circle → else the first shape; lines are editor-only and never become collision geometry). +The **Prop Editor** (Phase 9, extended by Phase 10a) authors vector prop assets saved as `.prop` files, stored under `res://content/props/*.prop`. Files are UTF-8 JSON, pretty-printed with tab indentation, versioned, and designed to remain **backward compatible** — the format stores the editor's full multi-shape stack, while a deterministic converter (`PropFile.flatten_to_payload()`) collapses it into the single-shape payload `PropUtils.spawn_prop()` consumes at runtime (frontmost closed polygon → else circle → else the first shape; lines are editor-only and never become collision geometry). In the Prop Editor, Arrange exposes **Transform Whole Prop**, **Normalize Size**, **Center Prop**, and **Show Stickman Guide** in that order. Whole-prop move, @@ -1028,53 +1035,54 @@ Behavior: | Path | Purpose | |---|---| | `res://project.godot` | Engine config; sets main scene to the editor and enabled features. | -| `res://scenes/stickman_editor.tscn` | **Main scene** — editor layout, File/Edit/View menu bar, dialogs (`GridConfigDialog` + SpinBox), column containers (unique-name nodes). | -| `res://scenes/body_part_panel.tscn` | Reusable single body-part editor panel (title, drawing area, context menu, `ColorPickerPopup`); expands vertically in its column. | -| `res://scripts/stickman_editor.gd` | Editor controller — File/Edit/View menu actions, save/load/clear, JSON v1.5 serialization with multi-shape/rotation/scale, `part_order`, Phase 8 `proportions`/`pivot`/`length`, and Phase 9 Round 5 per-part `guide_offset` export, `settings.json` load/save, editor-wide shape clipboard (Copy/Paste across panels), broadcast of grid/snap settings to panels, Reset Views, populates panels, coordinates selection across panels. | -| `res://scripts/stk_rig_adapter.gd` | **Phase 8, extended by Phase 9 (Rounds 4–6 bugfix).** Standalone runtime adapter (`class_name StkRigAdapter`, `static func apply(stk_data, rig)`): fits an instantiated `master_rig.tscn` to a loaded `.stk` by re-fitting the 8 limb bones (`Skeleton2D/Torso/...` `Bone2D` lengths + lower-bone origins), recalibrating the IK targets (`IK_Targets/Left|Right_Hand`, `Left|Right_Leg`), and mounting the `.stk` shapes onto the `Body/*` visual nodes (**one node per shape**: closed → single `Polygon2D` fill, open → single `Line2D` width 2). Shape mounting recomputes each part's bounding box at mount time (file `pivot`/`length` are no longer trusted) and derives a mount transform in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) via `_compute_mount_transform()`: the part's preview transform `E(P) = C + R(rot)·S·(P − C)` (rotation + scale about the bbox center — the editor's exact Whole-Stickman-preview transform) is composed **first**, then the anchor/alignment θ/bone-fit scale are computed on the **transformed geometry**; rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint). Anchors (raw family rules): head/torso bottom-center `(cx, max_y)`, left horizontal limbs `(max_x, cy)`, right horizontal limbs `(min_x, cy)`, vertically drawn limbs top-center `(cx, min_y)`; alignment rotation θ maps the far end onto `+Y`; scaling is **anisotropic** — only the **auto-detected drawn long axis** (`width >= height`) scales to the bone length (`bone_length/extent`, guard `extent <= 0.0001` → `1.0`), cross-axis thickness stays 1:1. The `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. (Phase 9 Round 5) when a part dict carries `guide_offset`, the mounted geometry is translated by `t = (guide_offset + (A − C)).rotated(−c_node)`; (Phase 9 Round 6) when `guide_offset` is present, the joint anchor is whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is nearest the part's guide joint (`center − guide_offset`), replacing the per-side family choice + 180° flip heuristic for that case (fixing the lower-left-leg and lower-right-arm, which were mounted 180° off their bones) — old files without the key keep the family rules + flip heuristic as the fallback in the driver's bone frame (A = mount anchor incl. the 180° flip rule, C = raw bbox center, `c_node` = driver `RemoteTransform2D.global_rotation`), so the harness reproduces the editor's guide-relative placement 1:1; old files without the key keep the offset-0 behavior (head falls back to `HEAD_CHIN_DROP`). Each `Body/*` container's scale is reset to `(1,1)` / rotation `0` (position untouched). (Phase 9) also fits the head bone (`Head.position.y = -proportions.torso_length`) while mounting the head as **full geometry** — it clears the head's inline `@tool` circle script and mounts `.stk` head shapes as `Line2D`/`Polygon2D`, and zeroes the Head driver's local position so the chin sits on the neck joint; the head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px). `_mount_shapes()` also handles **v1.0/v1.1 single-shape** part dicts (wraps the part dict as one shape when it carries `points` but no `shapes` array), so older `.stk` files mount as visible geometry instead of being cleared. **Not used by the editor** — consumed by the runtime pipeline. | -| `res://scripts/stickman_factory.gd` | **Phase 9.** Runtime entry point (`class_name StickmanFactory`, `extends RefCounted`); a static factory that turns a `.stk` file into a live, rigged `master_rig.tscn` instance. `load_stk(path)` reads + parses the file (`{}` + `push_warning` on failure); `spawn_from_data(stk_data)` instantiates `res://master_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, and returns the rig root **typed as `StickmanRig`** (the rig now carries the `StickmanRig` root script); `spawn(path)` chains them (`null` on empty data). **Not used by the editor.** | -| `res://scripts/stickman_rig.gd` | **Phase 9 Task 4.** `class_name StickmanRig`, `extends Node2D`; the runtime owner of facing direction, per-joint bone bend, `Body/*` z-order, and (Phase 10/11) the **kinematic-to-ragdoll** state switch with instant handoff + stand-up recovery, attached to the `master_rig.tscn` root `Master`. Exports a `facing_profile` preset (`FacingProfile` LEFT/RIGHT/FORWARD, default FORWARD) and four `@export_enum("Normal","Inverted")` per-joint bend vars (`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend`), plus (Phase 11) `rest_timeout` (2.0 s) and `auto_recover` (true) exports. Non-`@tool`: resolves `Skeleton2D`/`Body`/bend joints at runtime, enables its own modification stack, and applies the profile (flag writes + `Body/*` reorder) in `_ready()` and setters. Signals `facing_profile_changed` / `bend_flag_changed` / `state_changed`; public API `set_facing_profile`/`get_facing_profile`, `set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, `get_bend_joint_global_position()`, plus the ragdoll API `set_ragdoll(enabled)`/`toggle_ragdoll()`/`is_in_ragdoll()`/`request_recovery()` with `state` / `enum RigState { ANIMATED, RAGDOLL, RECOVERING }`. The recovery cycle is **pre-capture → ragdoll pose capture (7-ray ground probe) → mirror-aware re-anchor → snap (entry-captured chain offsets, `(−π, π]`-normalized bones, ankle clamp) → 2.0 s tween to the pre-ragdoll pose (arms/head bone-driven with `_natural_joint_target` for elbows; legs IK-driven) → finish (profile + bend flags + LookAt restored)**, with every per-joint bend decision editable in the `RAGDOLL_JOINT_BAND_LIMITS` / `RECOVERY_LEG_SIDE_FORCE` / `RECOVERY_JOINT_PATH` tables (Joint bend configuration (EDIT ME)). Diagnostics are gated behind `DEBUG_RECOVERY := false`. **Phase 6** adds PLAY-only `begin_terrain_fall()` / `set_terrain_following_enabled()`, center/per-foot TerrainBlock ray fans with self-proxy filtering, layered leg base markers vs live IK targets, continuous stance/swing blending with a predictive foot guard, accumulated root elevation/pelvis compensation across ramps/steps, and bounded free-fall. (Phase 4b) `_update_walking()` latches `_walk_mode` once per walk (`LATCH_PROBE_MAX_FRAMES` 6), unifies arrival on the final target at `ARRIVE_DISTANCE` (snap-on-arrive), steers to the final target when close, and re-asserts standing markers one frame after stop — fixing the walk-waypoint arrival jitter. (Phase 4.5) builds ten top-level `AnimatableBody2D` collision proxies, explicitly synchronizes them to the rendered `Body/*` transforms each physics frame, derives the head circle center/radius from mounted geometry (90 px minimum), and applies bounded, contact-quadrant-aware bone flinches that decay to captured base rotations. `snap_to_standing()` and the stage's PLAY→DIRECT reset restore authored transforms and pose state. Null-guarded (`push_warning` + skip). **Not used by the editor.** | -| `res://scripts/create_animations.gd` | **Phase 11.** `@tool extends EditorScript`; a **standalone editor utility** (run manually with `master_rig.tscn` open; not auto-loaded or referenced at runtime) that supersedes the deleted `scripts/create_walk.gd`. `_run()` bakes `walk_left`/`walk_right` (same keyframes as the old script) and a one-shot `stand_up` (`POSE_DOWN` → `POSE_STANDING`, `STAND_UP_DURATION` 2.0, `loop_mode = LOOP_NONE`) into the open scene's default `AnimationLibrary`. The baked `stand_up` is an **authored reference only** — runtime recovery does not play it (`StickmanRig` tweens the IK targets directly from the captured ragdoll pose, since a fixed first keyframe can never match an arbitrary rest pose). `walk_right` is the **canonical walk** clip; `walk_left` remains baked in the library but is **not used at runtime** (facing is set explicitly and the rig root X-mirrors for LEFT). | -| `res://scripts/test_harness.gd` | **Phase 9.** Standalone staging scene (run via **F6** on `res://scenes/test_harness.tscn`, not wired into the editor) for debugging bone scales, vector-drawing offsets, and IK limits in isolation. Top UI bar: "Open .stk…" / quick-select buttons (`stickmen/break.stk`, `stickmen/basic.stk`, `stickmen/test.stk`), "Show Bones" / "Show IK Handles" toggles, loaded-filename label. `SubViewport` world + enabled `Camera2D` (middle-mouse pan, wheel zoom, recenter on spawn); each load frees the previous rig and spawns a fresh one via `StickmanFactory.spawn()`. A world-space debug overlay draws true bone segments (joint dots + parent→child lines, with limb leaf bones drawn out to their IK targets so wrist/ankle joints are visible; the **Head** leaf is the exception — its target is a LookAt aim point, not a joint, so it draws a ~90 px segment along the bone's own direction instead) and colored IK-target markers (hands green, feet blue, head yellow, torso magenta) plus a semi-transparent yellow head-aim line; the **6** `Marker2D` IK targets are click-draggable — the 4 limb targets flex limbs live via `SkeletonModificationStack2D` TwoBoneIK (the rig self-enables its stack), the Torso target translates the whole rig via its `RemoteTransform2D`, and the Head target drives the head's LookAt aim rotation (Phase 9 Round 7). | -| `res://scenes/test_harness.tscn` | **Phase 9.** Standalone staging scene backing `scripts/test_harness.gd` (run via **F6**; not wired into the editor). | -| `res://scripts/terrain_block.gd` | **Vector Terrain System.** `class_name TerrainBlock`, `extends StaticBody2D` — a reusable vector terrain component building `Polygon2D` (fill) + `Line2D` (border) + `CollisionPolygon2D` (`BUILD_SOLIDS`, supports concave) children in code. Has a `spawn_id: String` property (set by `StageSpawner`) so same-template terrain overlaps are detectable during drag-painting. | -| `res://scripts/terrain_utils.gd` | **Vector Terrain System.** `class_name TerrainUtils`, `extends RefCounted` — static `sanitize_points()` (grid snap → local `_simplify_polyline()` → clockwise enforcement) and a `spawn_block()` factory. | -| `res://scripts/physics_test_harness.gd` | **Vector Terrain System / Dynamic Vector Props.** Standalone staging scene root building flat/ramp/step terrain via `TerrainUtils`, instantiating `master_rig.tscn`, spawning props via **1/2/3** (`PropUtils`), and adding a rig collision proxy (run via **F6**; not wired into the editor). A toggle-mode button flips the rig's kinematic-to-ragdoll mode via `_rig.set_ragdoll()`, plus (Phase 11) a **Rest** `SpinBox` (writes `_rig.rest_timeout`) and **"Recover Now"** button (`_rig.request_recovery()`); the rig's `state_changed` signal removes the proxy on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`. | -| `res://scenes/physics_test_harness.tscn` | **Vector Terrain System / Dynamic Vector Props.** Standalone staging scene backing `scripts/physics_test_harness.gd` (run via **F6**; not wired into the editor). | -| `res://scripts/prop_block.gd` | **Dynamic Vector Props / Phase 4.5.** `class_name PropBlock`, `extends RigidBody2D` — a reusable physical prop building `Polygon2D` (fill) + `Line2D` (outline) + `CollisionPolygon2D`/`CollisionShape2D` (polygon/circle collision) children in code, with material presets (mass + friction/bounce), live-updating exports, contact monitoring, shape-cast CCD, and `collided(other, contact_normal, contact_position)` / `separated(other)` signals carrying world-space contact data. Also the single source of truth for the Prop Editor's inspector physics readouts. | -| `res://scripts/prop_editor.gd` | **Prop Editor (Phase 9).** `class_name PropEditor`, `extends Control` — standalone vector-prop authoring controller (run via **F6** on `res://scenes/prop_editor.tscn`; **not** the main scene). Owns the 📁 File (🆕 New / 💾 Save / 📄 Save As… / 📂 Load… / 🕑 Recent) and 👁️ View (🔍 Reset View) menus, the top-right Grid spinbox + Snap checkbox, the centered Arrange/Reshape mode toolbar, the material-preset + per-shape inspector (physics readouts from `PropBlock`; hides the circle Radius row + shows a "reshaped" note when a shape's `reshaped` flag is set), the status bar, the window title builder, the exit dialog (💾 Save / 🗑️ Discard / ❌ Cancel), recent-props persistence (`user://prop_editor_settings.json`, max 5), and the debounced autosave/recovery pipeline (`user://prop_editor_autosave.prop` via `PropFile.save_safe`). Saves export a cached 200×200 thumbnail. | -| `res://scripts/prop_canvas.gd` | **Prop Editor (Phase 9).** `class_name PropCanvas`, `extends Control` — multi-shape vector canvas: cursor-centered zoom/pan (wheel, pinch, middle-mouse), `EditMode { TRANSFORM, POINTS }` ("Arrange"/"Reshape") gating input + drawing only, rotation dot + corner scale crosses (Ctrl = 15° snap / aspect lock), color-matched vertex handles with Create/Remove Point (0.6 s flash), per-mode right-click menus, a local clipboard, mirror + z-order, the per-shape `reshaped` flag, sanitize-on-commit via `TerrainUtils`, and two-tone halo selection. | -| `res://scripts/prop_file.gd` | **Prop Editor (Phase 9).** `class_name PropFile`, `extends RefCounted` — static `.prop` v1.0 schema + converters: `to_file_dict`/`from_file_dict` (round-trip, version rejection, missing-key defaults), `preset_to_string`/`from_string`, `sanitize_shape`, `flatten_to_payload` (frontmost closed polygon → circle → first shape), `save`/`load`, and the atomic `save_safe` (tmp/`.bak` swap). Points must be coerced via `_coerce_points` when serializing (Round 5). | -| `res://scripts/prop_theme.gd` | **Prop Editor (Phase 9).** `class_name PropTheme`, `extends RefCounted` — validated loader for `res://prop_theme.json` (deep-merge, warn-once, clamped accessors); hierarchy theme file → `DEFAULT_CONFIG` → `PropBlock` constants. Physics is never themeable. Holds the Round 2–4 `ui` tokens (fonts, hit targets, colors). Applied through a **local** Theme only. | -| `res://scenes/prop_editor.tscn` | **Prop Editor (Phase 9).** Standalone scene backing `scripts/prop_editor.gd` (run via **F6**; not the main scene): minimal shell (`%MenuBar` in `%TopRow` beside `%TopRightControls`, `%TopBar`/`%PropNameEdit`, `%MainLayout`/`%CanvasPanel`/`%CanvasArea`/`%ModeBar`/`%PropCanvas`, `%InspectorColumn`/`%InspectorList`, status bar, dialogs); UI built in code. | -| `res://scripts/prop_utils.gd` | **Dynamic Vector Props.** `class_name PropUtils`, `extends RefCounted` — static `create_box()` / `create_ball()` / `create_plank()` / `create_triangle()` primitive generators and a `spawn_prop()` factory (sanitizes polygon points via `TerrainUtils`). `PropFile.flatten_to_payload()` produces the payload it consumes. | -| `res://scripts/sandbox_stage.gd` | **Sandbox Stage Builder / Phase 4.5.** `class_name SandboxStage`, `extends Node2D` — root controller: `enum StageMode { EDIT, DIRECT, PLAY }` state machine (freezes props with `FREEZE_MODE_KINEMATIC`; runs stickman queues + rags props/areas in PLAY), placement mode + terrain drag-painting, a grid spatial dictionary, camera pan/zoom, deletion, bottom status bar, mode badge/frame/cursors, the `res://sandbox_theme.json` loader, and signal fan-out (`mode_changed(mode: int)` / `object_placed` / `object_selected` / `object_deselected` / `object_deleted`). **Phase 3b** instantiates the `AssetSelector` popup + thumbnail renderers, owns the selector open/close flow and lazy per-frame thumbnail drain (§22). **Phase 3c** instantiates the queue/rule panels + editors + waypoint context, owns the unified `CaptureKind` target-capture system, the "Edit Queue…"/"Edit Rules…" + right-click context entry points, the shared confirmation dialog, and consequence-only rule editing (§23). **Phase 4.5** routes `PropBlock` physics contacts through proxy metadata, tracks per-part body IDs for pair debouncing, preserves rig→prop rule ordering, purges stale pairs on separation/deletion/ragdoll, and restores authored rig state when returning PLAY→DIRECT. Standalone staging scene run via **F6**; not wired into the editor. | -| `res://scripts/stage_spawner.gd` | **Sandbox Stage Builder.** `class_name StageSpawner`, `extends RefCounted` — registry-driven factory (`Array[Dictionary]`, no id `match`); reuses `TerrainUtils` / `PropUtils` / `StickmanFactory`; centers terrain on its origin. Exposes `is_terrain_id()` / `get_template_aabb()` and tags spawned terrain with a `spawn_id`. `get_template_aabb(id)` mirrors `_spawn_terrain()`'s sanitize pass (`TerrainUtils.sanitize_points` at `TERRAIN_GRID_SIZE` 16), so the returned extent matches the real placed footprint — e.g. the 200-px-wide Ground template returns a **192-px** stride — and drives the block-unit paint stride, ghost sizing, and cell rasterization. **Phase 3b:** registry ids `ground/ramp/step/prop/stickman/area` (separate `crate`/`ball` removed); holds `selected_stickman_path` / `selected_prop_id` session state + a per-path `_stickman_cache`; `prop`/`stickman` spawn the **selected** asset (§22). | -| `res://scripts/stickman_library.gd` | **Asset Library (Phase 3b).** `class_name StickmanLibrary`, `extends RefCounted` — scans `res://stickmen/*.stk` into `{path, name, data}` entries (corrupt/missing-`body_parts` skipped; name = `stickman_name` else filename basename); `make_entry(path)` for Browse-chosen paths. | -| `res://scripts/prop_library.gd` | **Asset Library (Phase 3b).** `class_name PropLibrary`, `extends RefCounted` — static registry of the 4 prop templates (Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard); `get_default_id()` = `"crate"`. | -| `res://scripts/asset_selector.gd` | **Asset Library (Phase 3b).** `class_name AssetSelector`, `extends PopupPanel` — grid popup controller (root of `scenes/asset_selector.tscn`): pagination 12/page, empty state, per-cell thumbnail + name + prop material badge, Browse/Refresh/Close, Esc. | -| `res://scripts/thumbnails/stickman_thumbnail.gd` | **Asset Library (Phase 3b).** `class_name StickmanThumbnail`, `extends Node` — renders a parsed `.stk` to a `Texture2D` via the real rig in an offscreen `SubViewport`. | -| `res://scripts/thumbnails/prop_thumbnail.gd` | **Asset Library (Phase 3b).** `class_name PropThumbnail`, `extends Node` — renders a prop template to a `Texture2D` (lightweight non-physics visual). | -| `res://scripts/thumbnails/thumbnail_cache.gd` | **Asset Library (Phase 3b).** `class_name ThumbnailCache`, `extends RefCounted` — disk PNG cache (`user://thumbnails/`) keyed by basename+mtime (stickmen) / `id_v` (props); load/save/stale cleanup. | -| `res://scenes/asset_selector.tscn` | **Asset Library (Phase 3b).** `PopupPanel` root + `asset_selector.gd` — minimal shell (title bar, empty grid, footer); dynamic cells built in code. | -| `res://scripts/action_registry.gd` | **Editor Tools (Phase 3c).** `class_name ActionRegistry`, `extends RefCounted` — const registry of the 5 action templates (`walk_to`/`speak`/`wait`/`ragdoll`/`recover`: label/icon/params); static `types()`/`has_type`/`label`/`icon`, `to_rule_action()`/`from_rule_action()`, `summarize()`. | -| `res://scripts/trigger_registry.gd` | **Editor Tools (Phase 3c).** `class_name TriggerRegistry`, `extends RefCounted` — const registry of the 5 trigger templates (label/icon/`target_type`); static `types()`/`has_type`/`label`/`icon`/`target_type`/`summarize()`. | -| `res://scripts/queue_panel.gd` | **Editor Tools (Phase 3c).** `class_name QueuePanel`, `extends PopupPanel` — Action Queue panel (one stickman's actions with ✎/✕/drag-reorder); mutates the rig queue via its API. | -| `res://scripts/rule_panel.gd` | **Editor Tools (Phase 3c).** `class_name RulePanel`, `extends PopupPanel` — rule list panel filtered by source stickman or waypoint, with ✎/✕/drag-reorder + Add Rule/Clear All. | -| `res://scripts/action_editor.gd` | **Editor Tools (Phase 3c).** `class_name ActionEditor`, `extends PopupPanel` — add/edit single-action editor; type dropdown + params generated from `ActionRegistry`; `walk_to` target captured on stage. | -| `res://scripts/rule_editor.gd` | **Editor Tools (Phase 3c).** `class_name RuleEditor`, `extends PopupPanel` — rule editor in **full** or **consequence-only** modes (trigger read-only); actions add/edit/remove. | -| `res://scripts/waypoint_context.gd` | **Editor Tools (Phase 3c).** `class_name WaypointContext`, `extends PopupMenu` — right-click waypoint menu: Edit/Delete Walk, Insert action before/after, Edit Trigger Rules. | -| `res://scenes/queue_panel.tscn` / `rule_panel.tscn` / `action_editor.tscn` / `rule_editor.tscn` | **Editor Tools (Phase 3c).** Minimal `PopupPanel` shells (bare root + root script); all UI is built in code at `_ready()`. | -| `res://scripts/stage_selection.gd` | **Sandbox Stage Builder.** `class_name StageSelection`, `extends RefCounted` — hover/click/box selection via geometric world-space AABB hit-testing (frontmost `World` child wins; `RagdollBodyContainer` subtree excluded); `hover_changed` / `selection_changed` signals. | -| `res://scripts/stage_gizmos.gd` | **Sandbox Stage Builder.** `class_name StageGizmos`, `extends Node2D` — hover highlight + selection outline + rotate ring via `_draw()` and distance-based hit-testing; objects are dragged directly (no move handle); drives `global_position` / `global_rotation`; emits `transform_committed`. | -| `res://scripts/stage_grid.gd` | **Sandbox Stage Builder.** `class_name StageGrid`, `extends Node2D` — optional world-space grid overlay (major line every 5 cells) that pans/zooms with the camera; `grid_size` / `enabled` set by `SandboxStage`. | -| `res://scripts/stage_placement_overlay.gd` | **Sandbox Stage Builder (Phase 4b).** `class_name StagePlacementOverlay`, `extends Node2D` — world-space overlay drawing the terrain drag-painting dashed guide line (`set_terrain_guide` / `clear_terrain_guide`) and the director action rubber-band trajectory + ghost marker (`set_action_trajectory` / `clear_action`); pure drawing, no hit-testing. | +| `res://scenes/main.tscn` | **Startup wrapper** configured as `run/main_scene`; hosts the Stickman Editor. | +| `res://scenes/editors/stickman/stickman_editor.tscn` | Stickman Editor layout, File/Edit/View menu bar, dialogs (`GridConfigDialog` + SpinBox), column containers (unique-name nodes). | +| `res://scenes/editors/stickman/body_part_panel.tscn` | Reusable single body-part editor panel (title, drawing area, context menu, `ColorPickerPopup`); expands vertically in its column. | +| `res://scripts/editors/stickman/stickman_editor.gd` | Editor controller — File/Edit/View menu actions, save/load/clear, JSON v1.5 serialization with multi-shape/rotation/scale, `part_order`, Phase 8 `proportions`/`pivot`/`length`, and Phase 9 Round 5 per-part `guide_offset` export, `settings.json` load/save, editor-wide shape clipboard (Copy/Paste across panels), broadcast of grid/snap settings to panels, Reset Views, populates panels, coordinates selection across panels. | +| `res://scripts/characters/stickman/stk_rig_adapter.gd` | **Phase 8, extended by Phase 9 (Rounds 4–6 bugfix).** Standalone runtime adapter (`class_name StkRigAdapter`, `static func apply(stk_data, rig)`): fits an instantiated `stickman_rig.tscn` to a loaded `.stk` by re-fitting the 8 limb bones (`Skeleton2D/Torso/...` `Bone2D` lengths + lower-bone origins), recalibrating the IK targets (`IK_Targets/Left|Right_Hand`, `Left|Right_Leg`), and mounting the `.stk` shapes onto the `Body/*` visual nodes (**one node per shape**: closed → single `Polygon2D` fill, open → single `Line2D` width 2). Shape mounting recomputes each part's bounding box at mount time (file `pivot`/`length` are no longer trusted) and derives a mount transform in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) via `_compute_mount_transform()`: the part's preview transform `E(P) = C + R(rot)·S·(P − C)` (rotation + scale about the bbox center — the editor's exact Whole-Stickman-preview transform) is composed **first**, then the anchor/alignment θ/bone-fit scale are computed on the **transformed geometry**; rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (e.g. the 180° torso shows its drawn neck end at the hip joint). Anchors (raw family rules): head/torso bottom-center `(cx, max_y)`, left horizontal limbs `(max_x, cy)`, right horizontal limbs `(min_x, cy)`, vertically drawn limbs top-center `(cx, min_y)`; alignment rotation θ maps the far end onto `+Y`; scaling is **anisotropic** — only the **auto-detected drawn long axis** (`width >= height`) scales to the bone length (`bone_length/extent`, guard `extent <= 0.0001` → `1.0`), cross-axis thickness stays 1:1. The `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. (Phase 9 Round 5) when a part dict carries `guide_offset`, the mounted geometry is translated by `t = (guide_offset + (A − C)).rotated(−c_node)`; (Phase 9 Round 6) when `guide_offset` is present, the joint anchor is whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is nearest the part's guide joint (`center − guide_offset`), replacing the per-side family choice + 180° flip heuristic for that case (fixing the lower-left-leg and lower-right-arm, which were mounted 180° off their bones) — old files without the key keep the family rules + flip heuristic as the fallback in the driver's bone frame (A = mount anchor incl. the 180° flip rule, C = raw bbox center, `c_node` = driver `RemoteTransform2D.global_rotation`), so the harness reproduces the editor's guide-relative placement 1:1; old files without the key keep the offset-0 behavior (head falls back to `HEAD_CHIN_DROP`). Each `Body/*` container's scale is reset to `(1,1)` / rotation `0` (position untouched). (Phase 9) also fits the head bone (`Head.position.y = -proportions.torso_length`) while mounting the head as **full geometry** — it clears the head's inline `@tool` circle script and mounts `.stk` head shapes as `Line2D`/`Polygon2D`, and zeroes the Head driver's local position so the chin sits on the neck joint; the head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px). `_mount_shapes()` also handles **v1.0/v1.1 single-shape** part dicts (wraps the part dict as one shape when it carries `points` but no `shapes` array), so older `.stk` files mount as visible geometry instead of being cleared. **Not used by the editor** — consumed by the runtime pipeline. | +| `res://scripts/characters/stickman/stickman_factory.gd` | **Phase 9.** Runtime entry point (`class_name StickmanFactory`, `extends RefCounted`); a static factory that turns a `.stk` file into a live, rigged `stickman_rig.tscn` instance. `load_stk(path)` reads + parses the file (`{}` + `push_warning` on failure); `spawn_from_data(stk_data)` instantiates `res://scenes/characters/stickman/stickman_rig.tscn`, calls `StkRigAdapter.apply(stk_data, rig)`, and returns the rig root **typed as `StickmanRig`** (the rig now carries the `StickmanRig` root script); `spawn(path)` chains them (`null` on empty data). **Not used by the editor.** | +| `res://scripts/characters/stickman/stickman_rig.gd` | **Phase 9 Task 4.** `class_name StickmanRig`, `extends Node2D`; the runtime owner of facing direction, per-joint bone bend, `Body/*` z-order, and (Phase 10/11) the **kinematic-to-ragdoll** state switch with instant handoff + stand-up recovery, attached to the `stickman_rig.tscn` root `Master`. Exports a `facing_profile` preset (`FacingProfile` LEFT/RIGHT/FORWARD, default FORWARD) and four `@export_enum("Normal","Inverted")` per-joint bend vars (`left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend`), plus (Phase 11) `rest_timeout` (2.0 s) and `auto_recover` (true) exports. Non-`@tool`: resolves `Skeleton2D`/`Body`/bend joints at runtime, enables its own modification stack, and applies the profile (flag writes + `Body/*` reorder) in `_ready()` and setters. Signals `facing_profile_changed` / `bend_flag_changed` / `state_changed`; public API `set_facing_profile`/`get_facing_profile`, `set_joint_bend_flipped`/`get_joint_bend_flipped`, `get_bend_joints()`, `get_bend_joint_global_position()`, plus the ragdoll API `set_ragdoll(enabled)`/`toggle_ragdoll()`/`is_in_ragdoll()`/`request_recovery()` with `state` / `enum RigState { ANIMATED, RAGDOLL, RECOVERING }`. The recovery cycle is **pre-capture → ragdoll pose capture (7-ray ground probe) → mirror-aware re-anchor → snap (entry-captured chain offsets, `(−π, π]`-normalized bones, ankle clamp) → 2.0 s tween to the pre-ragdoll pose (arms/head bone-driven with `_natural_joint_target` for elbows; legs IK-driven) → finish (profile + bend flags + LookAt restored)**, with every per-joint bend decision editable in the `RAGDOLL_JOINT_BAND_LIMITS` / `RECOVERY_LEG_SIDE_FORCE` / `RECOVERY_JOINT_PATH` tables (Joint bend configuration (EDIT ME)). Diagnostics are gated behind `DEBUG_RECOVERY := false`. **Phase 6** adds PLAY-only `begin_terrain_fall()` / `set_terrain_following_enabled()`, center/per-foot TerrainBlock ray fans with self-proxy filtering, layered leg base markers vs live IK targets, continuous stance/swing blending with a predictive foot guard, accumulated root elevation/pelvis compensation across ramps/steps, and bounded free-fall. (Phase 4b) `_update_walking()` latches `_walk_mode` once per walk (`LATCH_PROBE_MAX_FRAMES` 6), unifies arrival on the final target at `ARRIVE_DISTANCE` (snap-on-arrive), steers to the final target when close, and re-asserts standing markers one frame after stop — fixing the walk-waypoint arrival jitter. (Phase 4.5) builds ten top-level `AnimatableBody2D` collision proxies, explicitly synchronizes them to the rendered `Body/*` transforms each physics frame, derives the head circle center/radius from mounted geometry (90 px minimum), and applies bounded, contact-quadrant-aware bone flinches that decay to captured base rotations. `snap_to_standing()` and the stage's PLAY→DIRECT reset restore authored transforms and pose state. Null-guarded (`push_warning` + skip). **Not used by the editor.** | +| `res://scripts/tools/create_animations.gd` | **Phase 11.** `@tool extends EditorScript`; a **standalone editor utility** (run manually with `stickman_rig.tscn` open; not auto-loaded or referenced at runtime) that supersedes the deleted `scripts/create_walk.gd`. `_run()` bakes `walk_left`/`walk_right` (same keyframes as the old script) and a one-shot `stand_up` (`POSE_DOWN` → `POSE_STANDING`, `STAND_UP_DURATION` 2.0, `loop_mode = LOOP_NONE`) into the open scene's default `AnimationLibrary`. The baked `stand_up` is an **authored reference only** — runtime recovery does not play it (`StickmanRig` tweens the IK targets directly from the captured ragdoll pose, since a fixed first keyframe can never match an arbitrary rest pose). `walk_right` is the **canonical walk** clip; `walk_left` remains baked in the library but is **not used at runtime** (facing is set explicitly and the rig root X-mirrors for LEFT). | +| `res://scripts/tools/test_harness.gd` | **Phase 9.** Standalone staging scene (run via **F6** on `res://scenes/tools/test_harness.tscn`, not wired into the editor) for debugging bone scales, vector-drawing offsets, and IK limits in isolation. Top UI bar: "Open .stk…" / quick-select buttons (`content/stickmen/break.stk`, `content/stickmen/basic.stk`, `content/stickmen/test.stk`), "Show Bones" / "Show IK Handles" toggles, loaded-filename label. `SubViewport` world + enabled `Camera2D` (middle-mouse pan, wheel zoom, recenter on spawn); each load frees the previous rig and spawns a fresh one via `StickmanFactory.spawn()`. A world-space debug overlay draws true bone segments (joint dots + parent→child lines, with limb leaf bones drawn out to their IK targets so wrist/ankle joints are visible; the **Head** leaf is the exception — its target is a LookAt aim point, not a joint, so it draws a ~90 px segment along the bone's own direction instead) and colored IK-target markers (hands green, feet blue, head yellow, torso magenta) plus a semi-transparent yellow head-aim line; the **6** `Marker2D` IK targets are click-draggable — the 4 limb targets flex limbs live via `SkeletonModificationStack2D` TwoBoneIK (the rig self-enables its stack), the Torso target translates the whole rig via its `RemoteTransform2D`, and the Head target drives the head's LookAt aim rotation (Phase 9 Round 7). | +| `res://scenes/tools/test_harness.tscn` | **Phase 9.** Standalone staging scene backing `scripts/tools/test_harness.gd` (run via **F6**; not wired into the editor). | +| `res://scripts/terrain/terrain_block.gd` | **Vector Terrain System.** `class_name TerrainBlock`, `extends StaticBody2D` — a reusable vector terrain component building `Polygon2D` (fill) + `Line2D` (border) + `CollisionPolygon2D` (`BUILD_SOLIDS`, supports concave) children in code. Has a `spawn_id: String` property (set by `StageSpawner`) so same-template terrain overlaps are detectable during drag-painting. | +| `res://scripts/terrain/terrain_utils.gd` | **Vector Terrain System.** `class_name TerrainUtils`, `extends RefCounted` — static `sanitize_points()` (grid snap → local `_simplify_polyline()` → clockwise enforcement) and a `spawn_block()` factory. | +| `res://scripts/tools/physics_test_harness.gd` | **Vector Terrain System / Dynamic Vector Props.** Standalone staging scene root building flat/ramp/step terrain via `TerrainUtils`, instantiating `stickman_rig.tscn`, spawning props via **1/2/3** (`PropUtils`), and adding a rig collision proxy (run via **F6**; not wired into the editor). A toggle-mode button flips the rig's kinematic-to-ragdoll mode via `_rig.set_ragdoll()`, plus (Phase 11) a **Rest** `SpinBox` (writes `_rig.rest_timeout`) and **"Recover Now"** button (`_rig.request_recovery()`); the rig's `state_changed` signal removes the proxy on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`. | +| `res://scenes/tools/physics_test_harness.tscn` | **Vector Terrain System / Dynamic Vector Props.** Standalone staging scene backing `scripts/tools/physics_test_harness.gd` (run via **F6**; not wired into the editor). | +| `res://scripts/props/prop_block.gd` | **Dynamic Vector Props / Phase 4.5.** `class_name PropBlock`, `extends RigidBody2D` — a reusable physical prop building `Polygon2D` (fill) + `Line2D` (outline) + `CollisionPolygon2D`/`CollisionShape2D` (polygon/circle collision) children in code, with material presets (mass + friction/bounce), live-updating exports, contact monitoring, shape-cast CCD, and `collided(other, contact_normal, contact_position)` / `separated(other)` signals carrying world-space contact data. Also the single source of truth for the Prop Editor's inspector physics readouts. | +| `res://scripts/editors/prop/prop_editor.gd` | **Prop Editor (Phase 9).** `class_name PropEditor`, `extends Control` — standalone vector-prop authoring controller (run via **F6** on `res://scenes/editors/prop/prop_editor.tscn`; **not** the main scene). Owns the 📁 File (🆕 New / 💾 Save / 📄 Save As… / 📂 Load… / 🕑 Recent) and 👁️ View (🔍 Reset View) menus, the top-right Grid spinbox + Snap checkbox, the centered Arrange/Reshape mode toolbar, the material-preset + per-shape inspector (physics readouts from `PropBlock`; hides the circle Radius row + shows a "reshaped" note when a shape's `reshaped` flag is set), the status bar, the window title builder, the exit dialog (💾 Save / 🗑️ Discard / ❌ Cancel), recent-props persistence (`user://prop_editor_settings.json`, max 5), and the debounced autosave/recovery pipeline (`user://prop_editor_autosave.prop` via `PropFile.save_safe`). Saves export a cached 200×200 thumbnail. | +| `res://scripts/editors/prop/prop_canvas.gd` | **Prop Editor (Phase 9).** `class_name PropCanvas`, `extends Control` — multi-shape vector canvas: cursor-centered zoom/pan (wheel, pinch, middle-mouse), `EditMode { TRANSFORM, POINTS }` ("Arrange"/"Reshape") gating input + drawing only, rotation dot + corner scale crosses (Ctrl = 15° snap / aspect lock), color-matched vertex handles with Create/Remove Point (0.6 s flash), per-mode right-click menus, a local clipboard, mirror + z-order, the per-shape `reshaped` flag, sanitize-on-commit via `TerrainUtils`, and two-tone halo selection. | +| `res://scripts/editors/prop/prop_file.gd` | **Prop Editor (Phase 9).** `class_name PropFile`, `extends RefCounted` — static `.prop` v1.0 schema + converters: `to_file_dict`/`from_file_dict` (round-trip, version rejection, missing-key defaults), `preset_to_string`/`from_string`, `sanitize_shape`, `flatten_to_payload` (frontmost closed polygon → circle → first shape), `save`/`load`, and the atomic `save_safe` (tmp/`.bak` swap). Points must be coerced via `_coerce_points` when serializing (Round 5). | +| `res://scripts/editors/prop/prop_theme.gd` | **Prop Editor (Phase 9).** `class_name PropTheme`, `extends RefCounted` — validated loader for `res://prop_theme.json` (deep-merge, warn-once, clamped accessors); hierarchy theme file → `DEFAULT_CONFIG` → `PropBlock` constants. Physics is never themeable. Holds the Round 2–4 `ui` tokens (fonts, hit targets, colors). Applied through a **local** Theme only. | +| `res://scenes/editors/prop/prop_editor.tscn` | **Prop Editor (Phase 9).** Standalone scene backing `scripts/editors/prop/prop_editor.gd` (run via **F6**; not the main scene): minimal shell (`%MenuBar` in `%TopRow` beside `%TopRightControls`, `%TopBar`/`%PropNameEdit`, `%MainLayout`/`%CanvasPanel`/`%CanvasArea`/`%ModeBar`/`%PropCanvas`, `%InspectorColumn`/`%InspectorList`, status bar, dialogs); UI built in code. | +| `res://scripts/props/prop_utils.gd` | **Dynamic Vector Props.** `class_name PropUtils`, `extends RefCounted` — static `create_box()` / `create_ball()` / `create_plank()` / `create_triangle()` primitive generators and a `spawn_prop()` factory (sanitizes polygon points via `TerrainUtils`). `PropFile.flatten_to_payload()` produces the payload it consumes. | +| `res://scripts/editors/stage/sandbox_stage.gd` | **Sandbox Stage Builder / Phase 4.5.** `class_name SandboxStage`, `extends Node2D` — root controller: `enum StageMode { EDIT, DIRECT, PLAY }` state machine (freezes props with `FREEZE_MODE_KINEMATIC`; runs stickman queues + rags and props/areas in PLAY), placement mode + terrain drag-painting, a grid spatial dictionary, camera pan/zoom, deletion, bottom status bar, mode badge/frame/cursors, the `res://sandbox_theme.json` loader, and signal fan-out (`mode_changed(mode: int)` / `object_placed` / `object_selected` / `object_deselected` / `object_deleted`). **Phase 3b** instantiates the `AssetSelector` popup + thumbnail renderers, owns the selector open/close flow and lazy per-frame thumbnail drain (§22). **Phase 3c** instantiates the queue/rule panels + editors + waypoint context, owns the unified `CaptureKind` target-capture system, the "Edit Queue…"/"Edit Rules…" + right-click context entry points, the shared confirmation dialog, and consequence-only rule editing (§23). **Phase 4.5** routes `PropBlock` physics contacts through proxy metadata, tracks per-part body IDs for pair debouncing, preserves rig→prop rule ordering, purges stale pairs on separation/deletion/ragdoll, and restores authored rig state when returning PLAY→DIRECT. Standalone staging scene run via **F6**; not wired into the editor. | +| `res://scripts/stage/stage_spawner.gd` | **Sandbox Stage Builder.** `class_name StageSpawner`, `extends RefCounted` — registry-driven factory (`Array[Dictionary]`, no id `match`); reuses `TerrainUtils` / `PropUtils` / `StickmanFactory`; centers terrain on its origin. Exposes `is_terrain_id()` / `get_template_aabb()` and tags spawned terrain with a `spawn_id`. `get_template_aabb(id)` mirrors `_spawn_terrain()`'s sanitize pass (`TerrainUtils.sanitize_points` at `TERRAIN_GRID_SIZE` 16), so the returned extent matches the real placed footprint — e.g. the 200-px-wide Ground template returns a **192-px** stride — and drives the block-unit paint stride, ghost sizing, and cell rasterization. **Phase 3b:** registry ids `ground/ramp/step/prop/stickman/area` (separate `crate`/`ball` removed); holds `selected_stickman_path` / `selected_prop_id` session state + a per-path `_stickman_cache`; `prop`/`stickman` spawn the **selected** asset (§22). | +| `res://scripts/characters/stickman/stickman_library.gd` | **Asset Library (Phase 3b).** `class_name StickmanLibrary`, `extends RefCounted` — scans `res://content/stickmen/*.stk` into `{path, name, data}` entries (corrupt/missing-`body_parts` skipped; name = `stickman_name` else filename basename); `make_entry(path)` for Browse-chosen paths. | +| `res://scripts/props/prop_library.gd` | **Asset Library (Phase 3b).** `class_name PropLibrary`, `extends RefCounted` — static registry of the 4 prop templates (Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard); `get_default_id()` = `"crate"`. | +| `res://scripts/shared/ui/asset_selector.gd` | **Asset Library (Phase 3b).** `class_name AssetSelector`, `extends PopupPanel` — grid popup controller (root of `scenes/stage/asset_selector.tscn`): pagination 12/page, empty state, per-cell thumbnail + name + prop material badge, Browse/Refresh/Close, Esc. | +| `res://scripts/shared/thumbnails/stickman_thumbnail.gd` | **Asset Library (Phase 3b).** `class_name StickmanThumbnail`, `extends Node` — renders a parsed `.stk` to a `Texture2D` via the real rig in an offscreen `SubViewport`. | +| `res://scripts/shared/thumbnails/prop_thumbnail.gd` | **Asset Library (Phase 3b).** `class_name PropThumbnail`, `extends Node` — renders a prop template to a `Texture2D` (lightweight non-physics visual). | +| `res://scripts/shared/thumbnails/thumbnail_cache.gd` | **Asset Library (Phase 3b).** `class_name ThumbnailCache`, `extends RefCounted` — disk PNG cache (`user://thumbnails/`) keyed by basename+mtime (stickmen) / `id_v` (props); load/save/stale cleanup. | +| `res://scenes/stage/asset_selector.tscn` | **Asset Library (Phase 3b).** `PopupPanel` root + `asset_selector.gd` — minimal shell (title bar, empty grid, footer); dynamic cells built in code. | +| `res://scripts/stage/action_registry.gd` | **Editor Tools (Phase 3c).** `class_name ActionRegistry`, `extends RefCounted` — const registry of the 5 action templates (`walk_to`/`speak`/`wait`/`ragdoll`/`recover`: label/icon/params); static `types()`/`has_type`/`label`/`icon`, `to_rule_action()`/`from_rule_action()`, `summarize()`. | +| `res://scripts/stage/trigger_registry.gd` | **Editor Tools (Phase 3c).** `class_name TriggerRegistry`, `extends RefCounted` — const registry of the 5 trigger templates (label/icon/`target_type`); static `types()`/`has_type`/`label`/`icon`/`target_type`/`summarize()`. | +| `res://scripts/editors/stage/queue_panel.gd` | **Editor Tools (Phase 3c).** `class_name QueuePanel`, `extends PopupPanel` — Action Queue panel (one stickman's actions with ✎/✕/drag-reorder); mutates the rig queue via its API. | +| `res://scripts/editors/stage/rule_panel.gd` | **Editor Tools (Phase 3c).** `class_name RulePanel`, `extends PopupPanel` — rule list panel filtered by source stickman or waypoint, with ✎/✕/drag-reorder + Add Rule/Clear All. | +| `res://scripts/editors/stage/action_editor.gd` | **Editor Tools (Phase 3c).** `class_name ActionEditor`, `extends PopupPanel` — add/edit single-action editor; type dropdown + params generated from `ActionRegistry`; `walk_to` target captured on stage. | +| `res://scripts/editors/stage/rule_editor.gd` | **Editor Tools (Phase 3c).** `class_name RuleEditor`, `extends PopupPanel` — rule editor in **full** or **consequence-only** modes (trigger read-only); actions add/edit/remove. | +| `res://scripts/stage/waypoint_context.gd` | **Editor Tools (Phase 3c).** `class_name WaypointContext`, `extends PopupMenu` — right-click waypoint menu: Edit/Delete Walk, Insert action before/after, Edit Trigger Rules. | +| `res://scenes/editors/stage/queue_panel.tscn` / `rule_panel.tscn` / `action_editor.tscn` / `rule_editor.tscn` | **Editor Tools (Phase 3c).** Minimal `PopupPanel` shells (bare root + root script); all UI is built in code at `_ready()`. | +| `res://scripts/stage/stage_selection.gd` | **Sandbox Stage Builder.** `class_name StageSelection`, `extends RefCounted` — hover/click/box selection via geometric world-space AABB hit-testing (frontmost `World` child wins; `RagdollBodyContainer` subtree excluded); `hover_changed` / `selection_changed` signals. | +| `res://scripts/stage/stage_gizmos.gd` | **Sandbox Stage Builder.** `class_name StageGizmos`, `extends Node2D` — hover highlight + selection outline + rotate ring via `_draw()` and distance-based hit-testing; objects are dragged directly (no move handle); drives `global_position` / `global_rotation`; emits `transform_committed`. | +| `res://scripts/stage/stage_grid.gd` | **Sandbox Stage Builder.** `class_name StageGrid`, `extends Node2D` — optional world-space grid overlay (major line every 5 cells) that pans/zooms with the camera; `grid_size` / `enabled` set by `SandboxStage`. | +| `res://scripts/stage/stage_placement_overlay.gd` | **Sandbox Stage Builder (Phase 4b).** `class_name StagePlacementOverlay`, `extends Node2D` — world-space overlay drawing the terrain drag-painting dashed guide line (`set_terrain_guide` / `clear_terrain_guide`) and the director action rubber-band trajectory + ghost marker (`set_action_trajectory` / `clear_action`); pure drawing, no hit-testing. | | `res://sandbox_theme.json` | **Sandbox Stage Builder (Phase 4b).** Hand-editable styling defaults for the sandbox (font paths/sizes, grid snap default, mode accent + guide-line colors); loaded by `SandboxStage._load_theme()` with defaults on missing/malformed file. **Phase 3c** extends the `fonts` block (bold/italic variant paths, per-widget sizes/style flags, optional `action_popup` object form) and drives the panels'/editors' `apply_font(...)` (§21.3 / spec §14). | -| `res://scenes/sandbox_stage.tscn` | **Sandbox Stage Builder.** Standalone staging scene backing `scripts/sandbox_stage.gd` (run via **F6**; not wired into the editor): root `Node2D` + `Camera2D` + empty `World`; the gizmo layer, placement overlay, and CanvasLayer UI (mode switcher, toolbars, bottom status bar, badge, tooltip) are built in code. | -| `res://scripts/body_part_panel.gd` | Multi-shape creation, vertex editing, shape dragging, per-panel zoom & pan, grid drawing & snap-to-grid, ColorPicker, shape/vertex delete, Z-ordering (Send Back / Bring Forward), shape Copy/Paste, shape Mirror X/Y, drawing (fill + outline for closed shapes). | -| `res://scripts/whole_stickman_preview.gd` | Assembly preview, drag-to-reposition, part selection with white bounding box, rotation gizmo (circle below box) with Ctrl 15° snap, scale gizmo (corner crosses) with Ctrl aspect lock, part Z-ordering (Send Back / Bring Forward) via `part_order`, part Mirror X/Y (scale negation), zoom & pan, grid drawing & snap-to-grid, pose silhouette guide (Phase 7), part hit-bounds, labels, and (Phase 9 Round 5) `get_guide_joint_preview()` — the preview-space position of a guide joint, used by the editor to export per-part `guide_offset`. | +| `res://scenes/stage/sandbox_stage.tscn` | **Sandbox Stage Builder.** Standalone staging scene backing `scripts/editors/stage/sandbox_stage.gd` (run via **F6**; not wired into the editor): root `Node2D` + `Camera2D` + empty `World`; the gizmo layer, placement overlay, and CanvasLayer UI (mode switcher, toolbars, bottom status bar, badge, tooltip) are built in code. | +| `res://scripts/editors/stickman/body_part_panel.gd` | Multi-shape creation, vertex editing, shape dragging, per-panel zoom & pan, grid drawing & snap-to-grid, ColorPicker, shape/vertex delete, Z-ordering (Send Back / Bring Forward), shape Copy/Paste, shape Mirror X/Y, drawing (fill + outline for closed shapes). | +| `res://scripts/editors/stickman/whole_stickman_preview.gd` | Assembly preview, drag-to-reposition, part selection with white bounding box, rotation gizmo (circle below box) with Ctrl 15° snap, scale gizmo (corner crosses) with Ctrl aspect lock, part Z-ordering (Send Back / Bring Forward) via `part_order`, part Mirror X/Y (scale negation), zoom & pan, grid drawing & snap-to-grid, pose silhouette guide (Phase 7), part hit-bounds, labels, and (Phase 9 Round 5) `get_guide_joint_preview()` — the preview-space position of a guide joint, used by the editor to export per-part `guide_offset`. | | `res://addons/curved_lines_2d/` | Scalable Vector Shapes 2D addon (v2.27.7) — required dependency. | -| `res://stick.tscn` | **Legacy** rigged/animated stick figure scene (Skeleton2D + IK). Not used by the editor. | +| `res://scenes/legacy/scenes/legacy/stick.tscn` | **Legacy** rigged/animated stick figure scene (Skeleton2D + IK). Not used by the editor. | | `res://AGENTS.md` | Guidance for AI agents working in this codebase. | ### Menu bar structure @@ -1152,27 +1160,27 @@ BodyPartPanel.shape_selected() ---(bound to part_name)---> stickman_editor > **Phase 6 — Continuous Dynamic IK & Terrain-Following Locomotion (implemented and editor-verified):** PLAY enables a kinematic terrain controller. Unsupported rigs free-fall under bounded gravity; walking animation pauses while airborne and resumes on landing. The centerline uses a three-ray terrain fan and each lower-leg endpoint uses an independent three-ray fan; only upward-facing `TerrainBlock` hits are accepted, with self collision proxies excluded. `AnimationTargets/*_Base` markers hold the animation pose while live leg IK targets receive continuous stance/swing blending, world-space stance locks, a predictive foot guard, and centerline pelvis/root compensation. Runtime root elevation accumulates over ramps and steps. EDIT/DIRECT disable terrain following and restore authored transforms. The Sandbox navigation bake uses exposed upward-surface ribbons with short step bridges; `NavigationAgent2D` remains at `(0, +385)` and terrain owns vertical motion while arrival remains baseline-aligned. Trigger areas use swept visible bounds across physics frames to detect fast-fall entries. Reactive takeover batches preserve order, so `ragdoll` followed by `recover` executes both in sequence. `DEBUG_TERRAIN` is off by default. No foot/ankle bones were added. -> **Phase 7:** Adds the pose silhouette guide to the Whole Stickman preview — a semi-transparent, color-coded stick figure (left cyan-blue, right orange-red, central white) whose 13 joint anchors match the rest-pose pivots of the future `master_rig.tscn` rig, drawn above the grid and in front of user parts (ghosting over them, below the selection gizmos and drag highlight), centered in the preview at the default view. Toggleable via the dynamic **View → Hide/Show Pose Guide** label (on by default, persisted to `settings.json`). The guide is a view aid only: **no `.stk` format change** — `FILE_VERSION` stays `"1.3"`. +> **Phase 7:** Adds the pose silhouette guide to the Whole Stickman preview — a semi-transparent, color-coded stick figure (left cyan-blue, right orange-red, central white) whose 13 joint anchors match the rest-pose pivots of the future `stickman_rig.tscn` rig, drawn above the grid and in front of user parts (ghosting over them, below the selection gizmos and drag highlight), centered in the preview at the default view. Toggleable via the dynamic **View → Hide/Show Pose Guide** label (on by default, persisted to `settings.json`). The guide is a view aid only: **no `.stk` format change** — `FILE_VERSION` stays `"1.3"`. -> **Phase 8:** The `.stk` export evolved to **v1.4** with write-only metadata the editor never reads back. A top-level `proportions` object stores the 5 master-rig rest-pose bone lengths (`upper_arm_length` 168.0, `lower_arm_length` 200.0, `upper_leg_length` 200.0, `lower_leg_length` 200.0, `torso_length` 391.5) — hardcoded constants from `master_rig.tscn`, not measured from the user's shapes. Each `body_parts` entry gains `pivot` (local bounding-box center = rotation origin) and `length` (bbox extent along the segment axis: width for arms, height for torso/legs/head). v1.0–v1.3 files load unchanged and gain these keys on their next save. A new standalone `res://scripts/stk_rig_adapter.gd` (`class_name StkRigAdapter`) fits an instantiated `master_rig.tscn` to a loaded `.stk` (bone fitting + IK recalibration + visual shape mount); it is **not** used by the editor and is reserved for a future runtime pipeline. +> **Phase 8:** The `.stk` export evolved to **v1.4** with write-only metadata the editor never reads back. A top-level `proportions` object stores the 5 master-rig rest-pose bone lengths (`upper_arm_length` 168.0, `lower_arm_length` 200.0, `upper_leg_length` 200.0, `lower_leg_length` 200.0, `torso_length` 391.5) — hardcoded constants from `stickman_rig.tscn`, not measured from the user's shapes. Each `body_parts` entry gains `pivot` (local bounding-box center = rotation origin) and `length` (bbox extent along the segment axis: width for arms, height for torso/legs/head). v1.0–v1.3 files load unchanged and gain these keys on their next save. A new standalone `res://scripts/characters/stickman/stk_rig_adapter.gd` (`class_name StkRigAdapter`) fits an instantiated `stickman_rig.tscn` to a loaded `.stk` (bone fitting + IK recalibration + visual shape mount); it is **not** used by the editor and is reserved for a future runtime pipeline. -> **Phase 9:** Adds the **runtime pipeline** for turning a `.stk` file into a live, rigged `master_rig.tscn` instance, plus a standalone staging scene to debug it. A new `res://scripts/stickman_factory.gd` (`class_name StickmanFactory`) provides the runtime entry point: `load_stk(path)` reads/parses a `.stk` (`FileAccess` + `JSON.parse_string`, `{}` + `push_warning` on failure), `spawn_from_data(stk_data)` instantiates `master_rig.tscn` and applies `StkRigAdapter.apply(stk_data, rig)`, and `spawn(path)` chains them (`null` on empty data). `StkRigAdapter` is extended to also fit the head bone (`Head.position.y = -proportions.torso_length`) and to mount the head as **full geometry** like every other part — clearing its inline `@tool` circle script and mounting `.stk` head shapes as `Line2D`/`Polygon2D` (removed the dead `_mount_head_circle`, `_compute_shapes_bbox`, and `_first_shape_color` helpers). A new standalone scene/resource pair, `res://scenes/test_harness.tscn` + `res://scripts/test_harness.gd` (run via **F6**), loads `.stk` files (open dialog + quick-select for `stickmen/break.stk`, `stickmen/basic.stk`, `stickmen/test.stk`), toggles a world-space debug overlay (true bone segments — parent→child lines with joint dots, limb leaf bones drawn to their IK targets and the Head leaf along its own direction — plus IK-target markers), pans/zooms a `Camera2D`, and lets you click-drag the 4 limb IK targets with live TwoBoneIK flexing (`SkeletonModificationStack2D` enabled on spawn). Neither the factory nor the harness is wired into the editor. **No `.stk` format change** — `FILE_VERSION` stays `"1.4"`. +> **Phase 9:** Adds the **runtime pipeline** for turning a `.stk` file into a live, rigged `stickman_rig.tscn` instance, plus a standalone staging scene to debug it. A new `res://scripts/characters/stickman/stickman_factory.gd` (`class_name StickmanFactory`) provides the runtime entry point: `load_stk(path)` reads/parses a `.stk` (`FileAccess` + `JSON.parse_string`, `{}` + `push_warning` on failure), `spawn_from_data(stk_data)` instantiates `stickman_rig.tscn` and applies `StkRigAdapter.apply(stk_data, rig)`, and `spawn(path)` chains them (`null` on empty data). `StkRigAdapter` is extended to also fit the head bone (`Head.position.y = -proportions.torso_length`) and to mount the head as **full geometry** like every other part — clearing its inline `@tool` circle script and mounting `.stk` head shapes as `Line2D`/`Polygon2D` (removed the dead `_mount_head_circle`, `_compute_shapes_bbox`, and `_first_shape_color` helpers). A new standalone scene/resource pair, `res://scenes/tools/test_harness.tscn` + `res://scripts/tools/test_harness.gd` (run via **F6**), loads `.stk` files (open dialog + quick-select for `content/stickmen/break.stk`, `content/stickmen/basic.stk`, `content/stickmen/test.stk`), toggles a world-space debug overlay (true bone segments — parent→child lines with joint dots, limb leaf bones drawn to their IK targets and the Head leaf along its own direction — plus IK-target markers), pans/zooms a `Camera2D`, and lets you click-drag the 4 limb IK targets with live TwoBoneIK flexing (`SkeletonModificationStack2D` enabled on spawn). Neither the factory nor the harness is wired into the editor. **No `.stk` format change** — `FILE_VERSION` stays `"1.4"`. -> **Phase 9 Round 2 bugfix:** `StkRigAdapter._mount_shapes()` no longer trusts the file's `pivot`/`length`; it recomputes each part's bounding box at mount time and mounts geometry in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) with a per-family alignment rotation (head `0`, torso `π`, left limbs `−π/2`, right limbs `+π/2`, vertical-drawn limbs `0`). Anchors: head/torso bottom-center, left horizontal limbs `max_x`, right horizontal limbs `min_x`, vertical limbs top-center. Scaling is **anisotropic** — only the **auto-detected drawn long axis** (`width >= height`) scales to the bone length (`bone_length/extent`, guard `extent <= 0.0001` → `1.0`), cross-axis thickness stays 1:1. Driver rotation neutralization was **removed** — the 10 `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. `Body/*` container transforms are reset (scale `(1,1)`, rotation `0`, position untouched), the Head driver's local position is zeroed so the chin lands on the neck joint, and `DEFAULT_LINE_WIDTH` is 2.0 (editor's 2 px outline). The harness `_draw_bones()` was rewritten to draw true parent→child bone segments with leaf bones out to their IK targets (wrist/ankle joints now visible). Per docs/phase9_round2_bugfix_spec.md; verified with a 46-assertion headless smoke test. +> **Phase 9 Round 2 bugfix:** `StkRigAdapter._mount_shapes()` no longer trusts the file's `pivot`/`length`; it recomputes each part's bounding box at mount time and mounts geometry in the rig's **hanging convention** (joint anchor at the local origin, far end along local `+Y`) with a per-family alignment rotation (head `0`, torso `π`, left limbs `−π/2`, right limbs `+π/2`, vertical-drawn limbs `0`). Anchors: head/torso bottom-center, left horizontal limbs `max_x`, right horizontal limbs `min_x`, vertical limbs top-center. Scaling is **anisotropic** — only the **auto-detected drawn long axis** (`width >= height`) scales to the bone length (`bone_length/extent`, guard `extent <= 0.0001` → `1.0`), cross-axis thickness stays 1:1. Driver rotation neutralization was **removed** — the 10 `RemoteTransform2D` drivers keep `update_rotation = true`, so mounted shapes follow their bones under IK flexing. `Body/*` container transforms are reset (scale `(1,1)`, rotation `0`, position untouched), the Head driver's local position is zeroed so the chin lands on the neck joint, and `DEFAULT_LINE_WIDTH` is 2.0 (editor's 2 px outline). The harness `_draw_bones()` was rewritten to draw true parent→child bone segments with leaf bones out to their IK targets (wrist/ankle joints now visible). Per docs/specs/phase9_round2_bugfix_spec.md; verified with a 46-assertion headless smoke test. -> **Phase 9 Round 3 bugfix:** `StkRigAdapter._mount_shapes()` now reads each part's `rotation` (degrees, default 0.0) and `scale` (`{x,y}`, default `(1,1)`) and composes the preview's part transform `E(P) = C + R(rot)·S·(P − C)` (scale-then-rotate about the bbox center) **before** the hanging-convention mount — the anchor, alignment θ, and bone-fit scale are computed on the transformed geometry, so every rotation/scale the user applied in the Whole Stickman preview renders in the harness. Rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (the 180° torso shows its drawn neck end at the hip joint); the bone-fit scale `s = bone_length/|V|` is measured on the transformed extent so user-scaled parts are not double-fitted. The head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px), chin at the neck joint. `_mount_shape()` mounts **one node per shape** — closed → single `Polygon2D` (fill only), open → single `Line2D` (width 2) — removing the previous Polygon2D + Line2D outline duplication. The harness head-leaf overlay now draws along the bone's own direction (the LookAt aim point is not a joint) instead of a line to `IK_Targets/Head`, eliminating the bone sticking out of the head. Per docs/phase9_round3_bugfix_spec.md; verified with a 26-assertion headless smoke test. +> **Phase 9 Round 3 bugfix:** `StkRigAdapter._mount_shapes()` now reads each part's `rotation` (degrees, default 0.0) and `scale` (`{x,y}`, default `(1,1)`) and composes the preview's part transform `E(P) = C + R(rot)·S·(P − C)` (scale-then-rotate about the bbox center) **before** the hanging-convention mount — the anchor, alignment θ, and bone-fit scale are computed on the transformed geometry, so every rotation/scale the user applied in the Whole Stickman preview renders in the harness. Rotations near ±180° (`|wrapf(rot)| > 0.75π`) swap the attachment to the drawn far end so flips are visible (the 180° torso shows its drawn neck end at the hip joint); the bone-fit scale `s = bone_length/|V|` is measured on the transformed extent so user-scaled parts are not double-fitted. The head mounts upright (`θ = 0`, `s = 1`) but still applies the part scale via `E` (face ≈160 px), chin at the neck joint. `_mount_shape()` mounts **one node per shape** — closed → single `Polygon2D` (fill only), open → single `Line2D` (width 2) — removing the previous Polygon2D + Line2D outline duplication. The harness head-leaf overlay now draws along the bone's own direction (the LookAt aim point is not a joint) instead of a line to `IK_Targets/Head`, eliminating the bone sticking out of the head. Per docs/specs/phase9_round3_bugfix_spec.md; verified with a 26-assertion headless smoke test. -> **Phase 9 Round 4 bugfix:** the mounted **head** was too high in the harness — its chin (the local origin) landed on the neck joint at `y = −391.5`, floating above the torso. The editor's silhouette guide draws the head as a radius-100 circle centered at the Head joint `(0, −463.5)` (bottom at `−363.5`), so `StkRigAdapter` now applies a `+28 px` rig-space translation (`const HEAD_CHIN_DROP := 28.0`) to the mounted head points after the part transform and the `(θ = 0, s = 1)` transform. The chin lands at world ≈ `(0, −363.5)` — the guide circle's bottom — overlapping the torso (top at `−391.5`) by 28 px, matching the editor guide. The drop is flip-agnostic (a rig-space fixture applied after part scale/rotation). Per docs/phase9_round4_bugfix_spec.md; verified with a 15-assertion headless smoke test. +> **Phase 9 Round 4 bugfix:** the mounted **head** was too high in the harness — its chin (the local origin) landed on the neck joint at `y = −391.5`, floating above the torso. The editor's silhouette guide draws the head as a radius-100 circle centered at the Head joint `(0, −463.5)` (bottom at `−363.5`), so `StkRigAdapter` now applies a `+28 px` rig-space translation (`const HEAD_CHIN_DROP := 28.0`) to the mounted head points after the part transform and the `(θ = 0, s = 1)` transform. The chin lands at world ≈ `(0, −363.5)` — the guide circle's bottom — overlapping the torso (top at `−391.5`) by 28 px, matching the editor guide. The drop is flip-agnostic (a rig-space fixture applied after part scale/rotation). Per docs/specs/phase9_round4_bugfix_spec.md; verified with a 15-assertion headless smoke test. -> **Phase 9 Round 5:** the `.stk` export evolved to **v1.5** with per-part **`guide_offset`** — the part's bbox center minus its silhouette-guide joint (both in preview space, a pure master-space delta). `StkRigAdapter` applies it only when present as a node-frame translation `t = (guide_offset + (A − C)).rotated(−c_node)` (A = the mount anchor incl. the 180° flip rule, C = the raw bbox center, `c_node` = the part's driver `RemoteTransform2D.global_rotation` at apply time), so the harness reproduces the editor's guide-relative placement **1:1**; old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4 `HEAD_CHIN_DROP`. The head maps to the guide **Neck** joint — the head bone's rig attachment origin, **not** the circle center (which sits 72 px above the neck). `guide_offset` is **write-only** metadata like `pivot`/`length` — the load path ignores it, so v1.0–v1.4 files load unchanged and gain the key on their next save. **Note:** `.stk` files must be re-saved from the editor to populate the offsets. Per docs/phase9_round5_bugfix_spec.md. +> **Phase 9 Round 5:** the `.stk` export evolved to **v1.5** with per-part **`guide_offset`** — the part's bbox center minus its silhouette-guide joint (both in preview space, a pure master-space delta). `StkRigAdapter` applies it only when present as a node-frame translation `t = (guide_offset + (A − C)).rotated(−c_node)` (A = the mount anchor incl. the 180° flip rule, C = the raw bbox center, `c_node` = the part's driver `RemoteTransform2D.global_rotation` at apply time), so the harness reproduces the editor's guide-relative placement **1:1**; old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4 `HEAD_CHIN_DROP`. The head maps to the guide **Neck** joint — the head bone's rig attachment origin, **not** the circle center (which sits 72 px above the neck). `guide_offset` is **write-only** metadata like `pivot`/`length` — the load path ignores it, so v1.0–v1.4 files load unchanged and gain the key on their next save. **Note:** `.stk` files must be re-saved from the editor to populate the offsets. Per docs/specs/phase9_round5_bugfix_spec.md. -> **Phase 9 Round 6 bugfix:** `StkRigAdapter._compute_mount_transform()` now selects the joint anchor as whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is **nearest the part's stored guide joint** (`center − guide_offset`) when a part carries `guide_offset`. This replaces the per-side family choice and the 180° flip heuristic for that case, fixing the **lower left leg** and **lower right arm**, which were mounted 180° off their bones (the far end attached at the joint) because the user's drawn-side conventions are inconsistent across parts — the stored guide placement is the ground truth for which drawn end is the joint. The nearest-end rule naturally preserves the 180° flip behavior (a flipped part's far end lands nearest the joint), the head chin, and every previously-correct case. Old files without the key keep the family rules + flip heuristic exactly as before. `theta`, `s`, the Round 5 offset `t`, and the head `HEAD_CHIN_DROP` fallback are unchanged. Per docs/phase9_round6_bugfix_spec.md; verified with a 32-assertion headless smoke test. +> **Phase 9 Round 6 bugfix:** `StkRigAdapter._compute_mount_transform()` now selects the joint anchor as whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is **nearest the part's stored guide joint** (`center − guide_offset`) when a part carries `guide_offset`. This replaces the per-side family choice and the 180° flip heuristic for that case, fixing the **lower left leg** and **lower right arm**, which were mounted 180° off their bones (the far end attached at the joint) because the user's drawn-side conventions are inconsistent across parts — the stored guide placement is the ground truth for which drawn end is the joint. The nearest-end rule naturally preserves the 180° flip behavior (a flipped part's far end lands nearest the joint), the head chin, and every previously-correct case. Old files without the key keep the family rules + flip heuristic exactly as before. `theta`, `s`, the Round 5 offset `t`, and the head `HEAD_CHIN_DROP` fallback are unchanged. Per docs/specs/phase9_round6_bugfix_spec.md; verified with a 32-assertion headless smoke test. -> **Phase 9 Round 7:** the test harness (`scripts/test_harness.gd`) now exposes **6** draggable IK handles. `IK_HANDLE_PATHS` gains `"Head"` (`IK_Targets/Head`, the `SkeletonModification2DLookAt` aim point) and `"Torso"` (`IK_Targets/Torso`, whose child `RemoteTransform2D` moves the hip bone). Dragging the **Torso** handle moves **bones only** (no target following) — the marker's `RemoteTransform2D` translates the hip bone, and the whole skeleton + `Body/*` visuals follow rigidly, while the limb/head targets stay put so dragging the figure away from them stretches the limbs toward the stationary targets (per user decision). Dragging the **Head** handle drives the Head bone's LookAt rotation (the solver now uses a full-range, non-inverted band solved in global space — `constraint_angle_min = -180 / max = 180`, `constraint_invert = false`, `constraint_in_localspace = false` — so the head converges to the aim point with no per-frame mirror oscillation; the earlier authored ~55° clamp is gone); `Body/Head` follows. `_handle_color()` colors the head marker yellow (`HANDLE_COLOR_HEAD`) and the torso marker magenta (`HANDLE_COLOR_TORSO`); hands stay green, feet blue. The IK overlay additionally draws a null-guarded semi-transparent yellow aim line from the Head bone origin to the head marker (visual aid for the LookAt test). **No `.stk` format change.** Per docs/phase9_round7_feature_spec.md; verified with a 17-assertion headless test. +> **Phase 9 Round 7:** the test harness (`scripts/tools/test_harness.gd`) now exposes **6** draggable IK handles. `IK_HANDLE_PATHS` gains `"Head"` (`IK_Targets/Head`, the `SkeletonModification2DLookAt` aim point) and `"Torso"` (`IK_Targets/Torso`, whose child `RemoteTransform2D` moves the hip bone). Dragging the **Torso** handle moves **bones only** (no target following) — the marker's `RemoteTransform2D` translates the hip bone, and the whole skeleton + `Body/*` visuals follow rigidly, while the limb/head targets stay put so dragging the figure away from them stretches the limbs toward the stationary targets (per user decision). Dragging the **Head** handle drives the Head bone's LookAt rotation (the solver now uses a full-range, non-inverted band solved in global space — `constraint_angle_min = -180 / max = 180`, `constraint_invert = false`, `constraint_in_localspace = false` — so the head converges to the aim point with no per-frame mirror oscillation; the earlier authored ~55° clamp is gone); `Body/Head` follows. `_handle_color()` colors the head marker yellow (`HANDLE_COLOR_HEAD`) and the torso marker magenta (`HANDLE_COLOR_TORSO`); hands stay green, feet blue. The IK overlay additionally draws a null-guarded semi-transparent yellow aim line from the Head bone origin to the head marker (visual aid for the LookAt test). **No `.stk` format change.** Per docs/specs/phase9_round7_feature_spec.md; verified with a 17-assertion headless test. -> **Phase 10 (Kinematic-to-Ragdoll):** adds a reversible **kinematic-to-ragdoll** state switch to the runtime rig. `StickmanRig` gains `enum RigState { ANIMATED, RAGDOLL }`, `var state: RigState`, `signal state_changed(new_state)`, and the `set_ragdoll(enabled)` / `toggle_ragdoll()` / `is_in_ragdoll()` API. In `RAGDOLL` mode the IK modification stack is disabled, the `AnimationPlayer` stopped, and the `Body/*` visuals hidden; a procedural network of **10** `RigidBody2D` (torso `CapsuleShape2D` mass 8.0, head `CircleShape2D` radius 100, limb capsules radius 8) + **9** `PinJoint2D` (elbow/knee fold-only ±bands, shoulder/hip ±160°, neck free) is built in code and reparented into a `"RagdollBodyContainer"` under the rig's **parent** (world root), layer 1/mask 1 so it collides with terrain and props. The rig root's momentum (tracked in `_physics_process`) is applied to the ragdoll Torso body for a seamless handoff. Exiting frees the ragdoll, re-shows `Body/*`, re-enables IK, and stops the animation. The physics harness toggles via its **Stickman ↔ Ragdoll** button, removing the `RigCollisionProxy` on entry and re-adding it (idempotently) on exit. `master_rig.tscn` is **not** modified. +> **Phase 10 (Kinematic-to-Ragdoll):** adds a reversible **kinematic-to-ragdoll** state switch to the runtime rig. `StickmanRig` gains `enum RigState { ANIMATED, RAGDOLL }`, `var state: RigState`, `signal state_changed(new_state)`, and the `set_ragdoll(enabled)` / `toggle_ragdoll()` / `is_in_ragdoll()` API. In `RAGDOLL` mode the IK modification stack is disabled, the `AnimationPlayer` stopped, and the `Body/*` visuals hidden; a procedural network of **10** `RigidBody2D` (torso `CapsuleShape2D` mass 8.0, head `CircleShape2D` radius 100, limb capsules radius 8) + **9** `PinJoint2D` (elbow/knee fold-only ±bands, shoulder/hip ±160°, neck free) is built in code and reparented into a `"RagdollBodyContainer"` under the rig's **parent** (world root), layer 1/mask 1 so it collides with terrain and props. The rig root's momentum (tracked in `_physics_process`) is applied to the ragdoll Torso body for a seamless handoff. Exiting frees the ragdoll, re-shows `Body/*`, re-enables IK, and stops the animation. The physics harness toggles via its **Stickman ↔ Ragdoll** button, removing the `RigCollisionProxy` on entry and re-adding it (idempotently) on exit. `stickman_rig.tscn` is **not** modified. > **Phase 11 (Instant Handoff & Recovery):** replaces the hard ragdoll entry/exit with an **instant handoff** and adds a **stand-up recovery** path. `StickmanRig` gains `enum RigState { ANIMATED, RAGDOLL, RECOVERING }` plus exports `rest_timeout` (2.0 s) and `auto_recover` (true). On entering `RAGDOLL` the ragdoll is built from the **current solved bone positions** (the player is stopped with `keep_state`), then `Body/*` is hidden and the IK stack disabled in the same call — no crossfade, since the ragdoll spawns at exactly the same pose and a fade would only read as ghosting (an earlier `transition_duration` blend was removed on director feedback). Rest detection reads the Torso body — sleeping, or linear ≤ `REST_LINEAR_THRESHOLD` (5.0 px/s, tuned up from the plan's 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s) and angular ≤ 0.1 rad/s — then after `rest_timeout` + `STABILIZATION_DELAY` (0.1 s) with `auto_recover` on, calls `_start_recovery()`. Recovery first calls `_capture_pre_ragdoll_state()` (6 IK markers, 8 limb + head bone rotations, facing profile, 4 bend flags, head LookAt state, per-leg `leg_sides`, and entry chain offsets `torso_chain_offset`/`head_chain_offset`) as the END target, then `_capture_ragdoll_pose()` records the 10 bodies' rig-local pose **and** probes the terrain via `_probe_ground_y()` (a 7-ray fan with a tolerance band, excluding the ragdoll RIDs), re-anchors the rig root so the standing figure's **feet** sit on the ground at the landing X (`_reanchor_root_to_landing()`, `new_root = feet + FOOT_OFFSET`, with a mirror-aware local shift `local_shift = to_local(shift) − to_local(Vector2.ZERO)`), destroys the ragdoll, sets `state = RECOVERING` + emits, snap-solves via `_snap_skeleton_to_pose()` — the 6 IK targets plus direct arm/head bone rotations (`rot_u = chain(u) − θ_t − bone_angle_u`, `rot_l = chain(l) − chain(u) + bone_angle_u − bone_angle_l`, head via `head_chain_offset`), all normalized into `(−π, π]`, with the ankles clamped onto the terrain and the two leg IK mods enabled with the captured knee side — and finally `_play_stand_up()` runs a **2.0 s** sine-in-out tween of the markers → their pre-ragdoll values and the arm/head bones to the snap-computed `_recovery_bones` targets (elbows via `_natural_joint_target`), legs IK-driven. `_on_stand_up_finished()` restores the facing profile + bend flags + LookAt state, re-enables the arm mods, and returns to `ANIMATED`. The per-joint bend decisions live in three editable tables — `RAGDOLL_JOINT_BAND_LIMITS`, `RECOVERY_LEG_SIDE_FORCE`, `RECOVERY_JOINT_PATH` (Joint bend configuration (EDIT ME)); diagnostics are gated behind `DEBUG_RECOVERY := false`. - Torso bone's `bone_angle` (copying the body rotation directly would slam the skeleton −90° and lay it flat). `request_recovery()` is public (no-op unless in `RAGDOLL`); `set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll, `set_ragdoll(false)` during `RAGDOLL` routes through recovery, and calls are otherwise idempotent. `is_in_ragdoll()` stays `state == RAGDOLL` (so `RECOVERING` reads as "Stickman"). A new `res://scripts/create_animations.gd` editor script (superseding the deleted `create_walk.gd`) bakes `walk_left`/`walk_right`/the one-shot `stand_up` into `master_rig.tscn`'s `AnimationLibrary` (the baked `stand_up` is an authored reference only — recovery does not play it) — **no `.stk` format change**; `master_rig.tscn` scene nodes are unchanged (only its baked animations are added). The physics harness gains a **Rest** `SpinBox` (0.1–10 s, writes `_rig.rest_timeout`), a **"Recover Now"** button (`request_recovery()`), and a `state_changed` hook that removes the `RigCollisionProxy` on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`. + Torso bone's `bone_angle` (copying the body rotation directly would slam the skeleton −90° and lay it flat). `request_recovery()` is public (no-op unless in `RAGDOLL`); `set_ragdoll(true)` during `RECOVERING` kills the stand-up tween and rebuilds the ragdoll, `set_ragdoll(false)` during `RAGDOLL` routes through recovery, and calls are otherwise idempotent. `is_in_ragdoll()` stays `state == RAGDOLL` (so `RECOVERING` reads as "Stickman"). A new `res://scripts/tools/create_animations.gd` editor script (superseding the deleted `create_walk.gd`) bakes `walk_left`/`walk_right`/the one-shot `stand_up` into `stickman_rig.tscn`'s `AnimationLibrary` (the baked `stand_up` is an authored reference only — recovery does not play it) — **no `.stk` format change**; `stickman_rig.tscn` scene nodes are unchanged (only its baked animations are added). The physics harness gains a **Rest** `SpinBox` (0.1–10 s, writes `_rig.rest_timeout`), a **"Recover Now"** button (`request_recovery()`), and a `state_changed` hook that removes the `RigCollisionProxy` on `RAGDOLL` and re-adds it (idempotently) on `ANIMATED`/`RECOVERING`. -> **Phase 9 Prop Editor (Rounds 1–7):** a standalone, kid-friendly vector-prop authoring tool — the authoring counterpart to the stickman editor — run via **F6** on `res://scenes/prop_editor.tscn` (not the main scene). It adds a new `.prop` v1.0 JSON format (`res://props/*.prop`, multi-shape stack + deterministic `PropFile.flatten_to_payload()` conversion), a dedicated `PropCanvas` multi-shape canvas, a validated `PropTheme` (`res://prop_theme.json`) with a Round 2–4 `ui` token section, and a save-side 200×200 thumbnail pipeline reusing `PropThumbnail` + a mtime-keyed `ThumbnailCache.prop_file_png`. Round highlights: **R2** — two explicit canvas modes ("Arrange"/"Reshape"), the stickman-style bottom rotation dot + corner scale crosses, color-matched 3 px vertex handles, and cursor-projection "Create Point" with a 0.6 s flash; **R3** — POINTS-mode right-click is point-editing only (segment proximity via `hit_test_segment`), centered mode toolbar with emoji, window title `Untitled — file.prop *`, exit dialog (💾/🗑️/❌), and a debounced autosave/recovery prompt; **R4** — selected-mode is clearly outlined (`FOCUS_NONE` + accent-border `pressed` stylebox), bigger theme-driven defaults, the Grid spinbox + Snap checkbox moved to the menu row (Edit menu removed), File → New with a save prompt, and **Ball removed** from the add menus; **R5** — File → 🕑 Recent (last 5, persisted) and a critical serialization fix (typed `PackedVector2Array` assignments from `{x,y}` dicts silently zeroed every coordinate — `_serialize_shape` now routes through `_coerce_points`); **R6** — Recent updates on save, a per-shape **`reshaped`** flag gates the circle Radius row, and fill-adaptive selection contrast; **R7** — two-tone halo selection (dark backing + bright core) so outlines/boxes/dots/crosses/handles read on any background. Several Prop Editor rows are logged in `docs/tech_debt_and_optimizations.md` (#29–#35). +> **Phase 9 Prop Editor (Rounds 1–7):** a standalone, kid-friendly vector-prop authoring tool — the authoring counterpart to the stickman editor — run via **F6** on `res://scenes/editors/prop/prop_editor.tscn` (not the main scene). It adds a new `.prop` v1.0 JSON format (`res://content/props/*.prop`, multi-shape stack + deterministic `PropFile.flatten_to_payload()` conversion), a dedicated `PropCanvas` multi-shape canvas, a validated `PropTheme` (`res://prop_theme.json`) with a Round 2–4 `ui` token section, and a save-side 200×200 thumbnail pipeline reusing `PropThumbnail` + a mtime-keyed `ThumbnailCache.prop_file_png`. Round highlights: **R2** — two explicit canvas modes ("Arrange"/"Reshape"), the stickman-style bottom rotation dot + corner scale crosses, color-matched 3 px vertex handles, and cursor-projection "Create Point" with a 0.6 s flash; **R3** — POINTS-mode right-click is point-editing only (segment proximity via `hit_test_segment`), centered mode toolbar with emoji, window title `Untitled — file.prop *`, exit dialog (💾/🗑️/❌), and a debounced autosave/recovery prompt; **R4** — selected-mode is clearly outlined (`FOCUS_NONE` + accent-border `pressed` stylebox), bigger theme-driven defaults, the Grid spinbox + Snap checkbox moved to the menu row (Edit menu removed), File → New with a save prompt, and **Ball removed** from the add menus; **R5** — File → 🕑 Recent (last 5, persisted) and a critical serialization fix (typed `PackedVector2Array` assignments from `{x,y}` dicts silently zeroed every coordinate — `_serialize_shape` now routes through `_coerce_points`); **R6** — Recent updates on save, a per-shape **`reshaped`** flag gates the circle Radius row, and fill-adaptive selection contrast; **R7** — two-tone halo selection (dark backing + bright core) so outlines/boxes/dots/crosses/handles read on any background. Several Prop Editor rows are logged in `docs/architecture/tech_debt_and_optimizations.md` (#29–#35). diff --git a/props/baseball.prop b/content/props/baseball.prop similarity index 100% rename from props/baseball.prop rename to content/props/baseball.prop diff --git a/props/box.prop b/content/props/box.prop similarity index 100% rename from props/box.prop rename to content/props/box.prop diff --git a/props/briefcase.prop b/content/props/briefcase.prop similarity index 100% rename from props/briefcase.prop rename to content/props/briefcase.prop diff --git a/props/eye.prop b/content/props/eye.prop similarity index 100% rename from props/eye.prop rename to content/props/eye.prop diff --git a/game_scenes/bend.scene b/content/stages/bend.scene similarity index 98% rename from game_scenes/bend.scene rename to content/stages/bend.scene index abe1f6e..a7905a6 100644 --- a/game_scenes/bend.scene +++ b/content/stages/bend.scene @@ -17,7 +17,7 @@ "y": -972.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/manwalk.scene b/content/stages/manwalk.scene similarity index 98% rename from game_scenes/manwalk.scene rename to content/stages/manwalk.scene index 790adbf..c602506 100644 --- a/game_scenes/manwalk.scene +++ b/content/stages/manwalk.scene @@ -227,7 +227,7 @@ "y": -312.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { @@ -272,7 +272,7 @@ "y": -72.0 }, "rotation": 0.0, - "asset_id": "res://props/briefcase.prop", + "asset_id": "res://content/props/briefcase.prop", "stage_object_id": "prop_1", "scale": { "x": 1.0, @@ -417,7 +417,7 @@ "y": -1273.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/test.stk", + "stk_path": "res://content/stickmen/test.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/ramp.scene b/content/stages/ramp.scene similarity index 99% rename from game_scenes/ramp.scene rename to content/stages/ramp.scene index 1eb5155..9f7b278 100644 --- a/game_scenes/ramp.scene +++ b/content/stages/ramp.scene @@ -199,7 +199,7 @@ "y": 132.287109375 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/speech.scene b/content/stages/speech.scene similarity index 97% rename from game_scenes/speech.scene rename to content/stages/speech.scene index 10290bc..1f0ae8e 100644 --- a/game_scenes/speech.scene +++ b/content/stages/speech.scene @@ -17,7 +17,7 @@ "y": -685.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/talk.scene b/content/stages/talk.scene similarity index 98% rename from game_scenes/talk.scene rename to content/stages/talk.scene index 4c924fb..8808c47 100644 --- a/game_scenes/talk.scene +++ b/content/stages/talk.scene @@ -17,7 +17,7 @@ "y": -673.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/test_fall.scene b/content/stages/test_fall.scene similarity index 99% rename from game_scenes/test_fall.scene rename to content/stages/test_fall.scene index 20af3ed..f7ceefb 100644 --- a/game_scenes/test_fall.scene +++ b/content/stages/test_fall.scene @@ -143,7 +143,7 @@ "y": -959.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/test_scene.scene b/content/stages/test_scene.scene similarity index 98% rename from game_scenes/test_scene.scene rename to content/stages/test_scene.scene index c558b61..9d611eb 100644 --- a/game_scenes/test_scene.scene +++ b/content/stages/test_scene.scene @@ -325,7 +325,7 @@ "y": 255.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { @@ -465,7 +465,7 @@ "y": -2177.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/bad.stk", + "stk_path": "res://content/stickmen/bad.stk", "facing_profile": "FORWARD", "action_queue": [] } diff --git a/game_scenes/test_scene2.scene b/content/stages/test_scene2.scene similarity index 99% rename from game_scenes/test_scene2.scene rename to content/stages/test_scene2.scene index 3e6b14c..8fe593a 100644 --- a/game_scenes/test_scene2.scene +++ b/content/stages/test_scene2.scene @@ -325,7 +325,7 @@ "y": 255.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/test_scene3.scene b/content/stages/test_scene3.scene similarity index 99% rename from game_scenes/test_scene3.scene rename to content/stages/test_scene3.scene index 42f47b3..7c54c4e 100644 --- a/game_scenes/test_scene3.scene +++ b/content/stages/test_scene3.scene @@ -325,7 +325,7 @@ "y": 255.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/game_scenes/throw_case.scene b/content/stages/throw_case.scene similarity index 98% rename from game_scenes/throw_case.scene rename to content/stages/throw_case.scene index b344cfc..738ba9a 100644 --- a/game_scenes/throw_case.scene +++ b/content/stages/throw_case.scene @@ -227,7 +227,7 @@ "y": -312.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/break.stk", + "stk_path": "res://content/stickmen/break.stk", "facing_profile": "FORWARD", "action_queue": [ { @@ -272,7 +272,7 @@ "y": -72.0 }, "rotation": 0.0, - "asset_id": "res://props/briefcase.prop", + "asset_id": "res://content/props/briefcase.prop", "stage_object_id": "prop_1", "scale": { "x": 1.0, @@ -417,7 +417,7 @@ "y": -1273.0 }, "rotation": 0.0, - "stk_path": "res://stickmen/test.stk", + "stk_path": "res://content/stickmen/test.stk", "facing_profile": "FORWARD", "action_queue": [ { diff --git a/stickmen/bad.stk b/content/stickmen/bad.stk similarity index 100% rename from stickmen/bad.stk rename to content/stickmen/bad.stk diff --git a/stickmen/basic.stk b/content/stickmen/basic.stk similarity index 100% rename from stickmen/basic.stk rename to content/stickmen/basic.stk diff --git a/stickmen/break.stk b/content/stickmen/break.stk similarity index 100% rename from stickmen/break.stk rename to content/stickmen/break.stk diff --git a/stickmen/test.stk b/content/stickmen/test.stk similarity index 100% rename from stickmen/test.stk rename to content/stickmen/test.stk diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1ce40d1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +# Documentation + +- `guides/` contains user guides and practical project workflows. +- `architecture/` describes implemented systems and maintained technical references. +- `specs/` contains feature specifications, including completed phase specs. +- `plans/` is for active and future implementation plans. +- `archive/` preserves older project references and completed plan drafts. +- `known_issues.md` tracks current bugs and deferred fixes. + +Keep `README.md` and `AGENTS.md` at the repository root for project onboarding and implementation rules. + +## Architecture references + +- [Repository organization](architecture/repository_organization.md) documents the feature-based folder layout, legacy content-path mapping, and serialized file-format compatibility contract. diff --git a/docs/SANDBOX_THEME.md b/docs/architecture/SANDBOX_THEME.md similarity index 99% rename from docs/SANDBOX_THEME.md rename to docs/architecture/SANDBOX_THEME.md index 8ec2569..3b017c5 100644 --- a/docs/SANDBOX_THEME.md +++ b/docs/architecture/SANDBOX_THEME.md @@ -1,9 +1,9 @@ # Sandbox theme reference `res://sandbox_theme.json` is the hand-editable presentation configuration for -the Sandbox Stage (`res://scenes/sandbox_stage.tscn`). It is loaded by +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/sandbox_theme.gd` (`class_name SandboxTheme`). +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. diff --git a/docs/architecture/repository_organization.md b/docs/architecture/repository_organization.md new file mode 100644 index 0000000..714dc1a --- /dev/null +++ b/docs/architecture/repository_organization.md @@ -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/.stk` | `res://content/stickmen/.stk` | +| `res://props/.prop` | `res://content/props/.prop` | +| `res://game_scenes/.scene` | `res://content/stages/.scene` | + +Equivalent project-relative inputs (for example, `stickmen/.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. diff --git a/docs/speech_system_as_built.md b/docs/architecture/speech_system_as_built.md similarity index 71% rename from docs/speech_system_as_built.md rename to docs/architecture/speech_system_as_built.md index 19b57ae..70e4826 100644 --- a/docs/speech_system_as_built.md +++ b/docs/architecture/speech_system_as_built.md @@ -40,24 +40,24 @@ The editor preview and runtime playback share `SpeechBubble`. `StickmanActionRun | File | Responsibility | | --- | --- | -| `scripts/speech_composer.gd`, `scenes/speech_composer.tscn` | Draft editing, bubble management, Message/Emoji UI, recovery, style context, and preview state. | -| `scripts/speech_style_toolbar.gd` | Bubble-scoped stage controls and attached choosers. | -| `scripts/speech_show_toolbar.gd` | Whole-sequence controls, layout presets, preview/done/cancel, and More settings. | -| `scripts/speech_payload.gd` | Payload defaults, validation, migration, word spans, and duration calculation. | -| `scripts/stickman_speech_bubble.gd` | Shared preview/playback renderer, bubble measurement and geometry, word targeting, order badges, and bubble hit testing. | -| `scripts/sandbox_stage.gd` | Session lifecycle, stage input routing, toolbar placement, badge avoidance, and queue/rule commit. | -| `scripts/stickman_action_runner.gd` | Runtime speech clocks and bubble ownership. | -| `scripts/stage_scene_io.gd`, `scripts/action_registry.gd` | Scene and rule serialization adapters. | -| `scripts/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. | +| `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](../specs/speech_whole_sequence_show.md) -- [Speech Style toolbar](../specs/speech_style_toolbar.md), [live editing](../specs/speech_style_toolbar_live_edit.md), and [Message/Emoji/Timing](../specs/speech_message_emoji_timing.md) -- [Bubble layout presets](../specs/speech_bubble_layout_presets.md) and [bubble visuals](../specs/speech_bubble_visuals.md) -- [Badge stability, grouping, and toolbar avoidance](../specs/speech_badge_stability.md) -- [Phase 13 payload and playback details](../specs/phase_13_speech_bubbles.md) +- [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 `game_scenes/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. +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. diff --git a/docs/tech_debt_and_optimizations.md b/docs/architecture/tech_debt_and_optimizations.md similarity index 64% rename from docs/tech_debt_and_optimizations.md rename to docs/architecture/tech_debt_and_optimizations.md index d6f6b77..a83213f 100644 --- a/docs/tech_debt_and_optimizations.md +++ b/docs/architecture/tech_debt_and_optimizations.md @@ -18,43 +18,43 @@ This document tracks known technical debt, optimization opportunities, and minor | 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/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) | +| 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/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) | +| 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 props/other stickmen are registered as obstacles. (2026‑08‑29) | +| 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 `plans/PHASE_4b_SPEC.md` §3. (2026-09-02) | -| 16 | **No grid spatial dictionary** — terrain placement (`sandbox_stage.gd` `_place_at`) is single-click with no occupancy tracking; the 3-state "empty/same-type/blocked" drag-paint query and any spatial broadphase need a cell→nodes index. | Low | ✅ Resolved | Phase 4b adds an **advisory** grid spatial dictionary `_grid_cells` (cell `Vector2i` @ `TERRAIN_GRID_SIZE` 16 → `Array[Node2D]`) plus `_rasterize_aabb_to_cells()` to index every world AABB's covered cells; it drives the terrain drag-painting 3-state ghost query and the director target-validity test (skipping cells whose nodes include a `TerrainBlock`). It is populated on place, rebuilt on move/rotate/delete, and is **never authoritative** (the `World` tree is). It is **not yet wired into the Phase 4 event engine** — #14 stays Open for that. See `plans/PHASE_4b_SPEC.md` §2.6. (2026-09-02) | +| 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`. | 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/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/phase_3c_editor_spec.md` §2 decision 5, not as a debt row. (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 `specs/phase_9_prop_editor.md` §6. Custom `.prop` thumbnails are mtime-keyed (`ThumbnailCache.prop_file_key`/`prop_file_png`, `_.png`), so re-edits invalidate the cached PNG; built-in template PNGs stay `id_v`-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 (`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 (`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 (`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 `props/eye.prop` and `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 (`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 (`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 `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) | +| 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`, `_.png`), so re-edits invalidate the cached PNG; built-in template PNGs stay `id_v`-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 `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://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 `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 `specs/phase_10_prop_integration.md`. (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 `specs/phase_10a_prop_scaling.md`. (2026-09-21) | +| 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) | @@ -62,13 +62,13 @@ This document tracks known technical debt, optimization opportunities, and minor | 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 `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 `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 `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 `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 `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 `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 `specs/speech_stage_only_editor.md`. (2026-09-30) | +| 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) | --- @@ -99,51 +99,51 @@ This document tracks known technical debt, optimization opportunities, and minor | ---------- | -------------------------------------------------------------- | | 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/sandbox_stage.gd` + `stage_spawner.gd` / `stage_selection.gd` / `stage_gizmos.gd` + `scenes/sandbox_stage.tscn`. Logged #11 (no unified freeze abstraction over the mixed `StaticBody2D` / `RigidBody2D` / `Node2D` population) and #12 (selection hit‑testing uses world‑space AABBs rather than point‑in‑polygon). | -| 2026-08-29 | Phase 3a (Core Director Functionality) spec written (`docs/phase_3a_spec.md`). Logged #13 (slope-aware walking physics + dynamic obstacle avoidance deferred — the nav mesh itself is built in 3a). Notable non-debt decisions recorded in the spec: Play mode now runs the director script instead of auto-ragdolling stickmen; `walk_to(target)` treats `target` as a feet/ground destination via `FOOT_OFFSET`, with the `NavigationAgent2D` child placed at the feet `(0,+385)` so it paths on the ground-level nav mesh. | -| 2026-08-29 | **`walk_to` stops-after-a-few-px bug fixed.** `_update_walking` (Phase 3a) now defers nav reads until `NavigationServer2D.map_get_iteration_id(...) != 0` (map-sync guard) and forces the path query via `get_next_path_position()` before any empty-path/finished check. **Follow-up (same day):** the initial "warn + finish in place" unreachable-target policy was itself reported as "stickman stands still with a waypoint" and was replaced by **hybrid nav/direct steering** — an on-mesh target follows the nav path (`_walk_mode = "nav"`), an off-mesh/unreachable target walks **straight to the clicked waypoint** (`_walk_mode = "direct"`, root target = waypoint + `FOOT_OFFSET`), with **no** `push_warning`; `_walk_path_grace` removed (map-sync guard + forced path query replace it); the debug trace now carries `mode=nav|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 `BUGS.md`; verified with a 44-assertion headless regression suite. | -| 2026-08-30 | Phase 4 (Triggers & Event System) implemented: `scripts/trigger_area.gd` (NEW placeable sensor), `sandbox_stage.gd` rule system (`_event_rules`, geometric event engine `_update_area_entry` / `_update_stickman_prop_collision`, rule-builder UI state machine, `_cleanup_rules_for_nodes`), `stickman_rig.gd` (`arrived` gains a `target` payload; new `enqueue_reactive`), `prop_block.gd` (`collided` physics signal), `stage_director_visuals.gd` rule visualization, `stage_spawner.gd` / `stage_selection.gd` `"area"` palette + duck-typed `get_area_rect`. Logged #14 (all-pairs event engine scales O(rigs×props + areas×movables); spatial hash suggested). | -| 2026-09-02 | Phase 4b (Polish) spec written (`plans/PHASE_4b_SPEC.md`). Logged #15 (walk-arrival jitter: `_walk_mode` re-evaluated per frame + arrival-radius mismatch) and #16 (no grid spatial dictionary; Phase 4b adds one for terrain drag-painting + event-engine broadphase). | -| 2026-09-02 | Phase 4b (Polish) implemented. Resolved **#15** (walk-mode latch once per walk, unified `ARRIVE_DISTANCE` arrival with snap-on-arrive, steer-to-final-when-close, one-frame marker re-assert) and **#16** (grid spatial dictionary `_grid_cells` @ `TERRAIN_GRID_SIZE` for the drag-painting 3-state query + director target-validity). #14 remains **Open** — the dictionary is not yet wired into the Phase 4 event engine (note updated). Added `res://sandbox_theme.json`, `scripts/stage_placement_overlay.gd`, the 3-segment mode switcher / status bar / badge / frame / cursors, and director tooltip/trajectory/reticle UX. | -| 2026-09-03 | Phase 3b (Asset Library) implemented (`docs/phase_3b_asset_grid_spec.md`): `scripts/stickman_library.gd` / `prop_library.gd`, `scripts/thumbnails/thumbnail_cache.gd` / `stickman_thumbnail.gd` / `prop_thumbnail.gd`, `scripts/asset_selector.gd` + `scenes/asset_selector.tscn`. The Stickman/Prop palette buttons now open visual selector grids (pagination 12/page, session-only selection, Browse/Refresh); `StageSpawner` registry becomes `ground/ramp/step/prop/stickman/area` (`crate`/`ball` removed) with `selected_*` session state + per-path stickman cache; `SandboxStage` owns the selector open/close flow, Esc priority, and the one-per-frame lazy thumbnail drain. Logged #17 (thumbnail cache growth / headless placeholder) and #18 (session-only selection). Verified with the new headless suite `tests/test_phase3b_library.gd` plus the updated `tests/test_phase4b1_fixes.gd`. | -| 2026-09-03 | **Selector UI bugfix round** (5 bugs, documented via `tests/test_phase3b_ui_fixes.gd` + docs in `docs/phase_3b_asset_grid_spec.md` / `README.md` / `AGENTS.md`): `AssetSelector.open()` drops its selection params (`selected_path`/`selected_id`) — no cell is pre-highlighted on open (selected stylebox removed); the selector re-centers on window resize (`size_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-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/action_registry.gd` / `trigger_registry.gd` (extensible action/trigger templates), `scripts/action_editor.gd` / `rule_editor.gd` / `queue_panel.gd` / `rule_panel.gd` + `scenes/*.tscn` shells, `scripts/waypoint_context.gd`, plus integration in `sandbox_stage.gd` (unified `CaptureKind` target-capture, "Edit Queue…/Edit Rules…" + right-click stickman/waypoint context menus, confirmation dialog, consequence-only rule editor) and `stage_director_visuals.gd` (`hit_test_waypoint_action`, waypoint edit highlight). Logged #19 (ad-hoc popup host-visibility discipline), #20 ("Add Rule" has no source in waypoint-filtered panels), #21 (drag-reorder nearest-row-center), #22 (`--check-only --script` needs a prior `--editor` rescan), and later the same day #23 (Phase 3c confirmation dialog popped over an exclusive panel — the delete/clear-all confirm paths do not hide the `QueuePanel`/`RulePanel` first). Documented the plan-§11.2 deviation (paramless `ActionEditor` pre-fill instead of a separate ragdoll/recover confirmation dialog) in `docs/phase_3c_editor_spec.md` §2 decision 5 — a design decision, so no debt row. Static-verified via headless `--editor --quit` + per-script `--check-only`; runtime F6 flows (queue/rule panel edit-delete-reorder, waypoint context menu, visual walk edit, full vs consequence-only rule editor) still need manual verification. | -| 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 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-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 (`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 (`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 (`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 (`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 (`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 (`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 (`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/prop_editor_user_guide.md`; focused tests and the Prop Editor startup smoke test passed. | +| 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 (`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 (`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 (`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 (`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 (`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 (`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-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 (`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 | 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. `game_scenes/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 (`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 (`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 (`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-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 (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-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 (`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 (`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 (`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 (`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-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_ diff --git a/PROJECT.md b/docs/archive/PROJECT.md similarity index 100% rename from PROJECT.md rename to docs/archive/PROJECT.md diff --git a/ROADMAP.md b/docs/archive/ROADMAP.md similarity index 100% rename from ROADMAP.md rename to docs/archive/ROADMAP.md diff --git a/plans/COMMAND_LINE_SCENE.md b/docs/archive/plans/COMMAND_LINE_SCENE.md similarity index 100% rename from plans/COMMAND_LINE_SCENE.md rename to docs/archive/plans/COMMAND_LINE_SCENE.md diff --git a/plans/DYNAMIC_VECTOR_PROPS.md b/docs/archive/plans/DYNAMIC_VECTOR_PROPS.md similarity index 100% rename from plans/DYNAMIC_VECTOR_PROPS.md rename to docs/archive/plans/DYNAMIC_VECTOR_PROPS.md diff --git a/plans/KINEMATIC_BLENDING_AND_RECOVERY.md b/docs/archive/plans/KINEMATIC_BLENDING_AND_RECOVERY.md similarity index 100% rename from plans/KINEMATIC_BLENDING_AND_RECOVERY.md rename to docs/archive/plans/KINEMATIC_BLENDING_AND_RECOVERY.md diff --git a/plans/KINEMATIC_TO_RAGDOLL.md b/docs/archive/plans/KINEMATIC_TO_RAGDOLL.md similarity index 100% rename from plans/KINEMATIC_TO_RAGDOLL.md rename to docs/archive/plans/KINEMATIC_TO_RAGDOLL.md diff --git a/plans/KINEMATIC_TO_RAGDOLL_SPEC.md b/docs/archive/plans/KINEMATIC_TO_RAGDOLL_SPEC.md similarity index 100% rename from plans/KINEMATIC_TO_RAGDOLL_SPEC.md rename to docs/archive/plans/KINEMATIC_TO_RAGDOLL_SPEC.md diff --git a/plans/PHASE_10_PROP_INTEGRATION.md b/docs/archive/plans/PHASE_10_PROP_INTEGRATION.md similarity index 100% rename from plans/PHASE_10_PROP_INTEGRATION.md rename to docs/archive/plans/PHASE_10_PROP_INTEGRATION.md diff --git a/plans/PHASE_10a_PROP_SCALING.md b/docs/archive/plans/PHASE_10a_PROP_SCALING.md similarity index 100% rename from plans/PHASE_10a_PROP_SCALING.md rename to docs/archive/plans/PHASE_10a_PROP_SCALING.md diff --git a/plans/PHASE_11_PROP_INTERACTIONS.md b/docs/archive/plans/PHASE_11_PROP_INTERACTIONS.md similarity index 100% rename from plans/PHASE_11_PROP_INTERACTIONS.md rename to docs/archive/plans/PHASE_11_PROP_INTERACTIONS.md diff --git a/plans/PHASE_12_LIVE_DIRECTOR.md b/docs/archive/plans/PHASE_12_LIVE_DIRECTOR.md similarity index 100% rename from plans/PHASE_12_LIVE_DIRECTOR.md rename to docs/archive/plans/PHASE_12_LIVE_DIRECTOR.md diff --git a/plans/PHASE_13_SPEECH_BUBBLES.md b/docs/archive/plans/PHASE_13_SPEECH_BUBBLES.md similarity index 100% rename from plans/PHASE_13_SPEECH_BUBBLES.md rename to docs/archive/plans/PHASE_13_SPEECH_BUBBLES.md diff --git a/plans/PHASE_3a_CORE_DIRECTOR.md b/docs/archive/plans/PHASE_3a_CORE_DIRECTOR.md similarity index 100% rename from plans/PHASE_3a_CORE_DIRECTOR.md rename to docs/archive/plans/PHASE_3a_CORE_DIRECTOR.md diff --git a/plans/PHASE_3b_ASSET_GRID.md b/docs/archive/plans/PHASE_3b_ASSET_GRID.md similarity index 100% rename from plans/PHASE_3b_ASSET_GRID.md rename to docs/archive/plans/PHASE_3b_ASSET_GRID.md diff --git a/plans/PHASE_3c_EDITOR.md b/docs/archive/plans/PHASE_3c_EDITOR.md similarity index 100% rename from plans/PHASE_3c_EDITOR.md rename to docs/archive/plans/PHASE_3c_EDITOR.md diff --git a/plans/PHASE_3c_SPEC.md b/docs/archive/plans/PHASE_3c_SPEC.md similarity index 100% rename from plans/PHASE_3c_SPEC.md rename to docs/archive/plans/PHASE_3c_SPEC.md diff --git a/plans/PHASE_4.5_COLLISION_BODIES.md b/docs/archive/plans/PHASE_4.5_COLLISION_BODIES.md similarity index 100% rename from plans/PHASE_4.5_COLLISION_BODIES.md rename to docs/archive/plans/PHASE_4.5_COLLISION_BODIES.md diff --git a/plans/PHASE_4_FINAL_BUGS.md b/docs/archive/plans/PHASE_4_FINAL_BUGS.md similarity index 100% rename from plans/PHASE_4_FINAL_BUGS.md rename to docs/archive/plans/PHASE_4_FINAL_BUGS.md diff --git a/plans/PHASE_4_TRIGGER_EVENTS.md b/docs/archive/plans/PHASE_4_TRIGGER_EVENTS.md similarity index 100% rename from plans/PHASE_4_TRIGGER_EVENTS.md rename to docs/archive/plans/PHASE_4_TRIGGER_EVENTS.md diff --git a/plans/PHASE_4b_POLISH.md b/docs/archive/plans/PHASE_4b_POLISH.md similarity index 100% rename from plans/PHASE_4b_POLISH.md rename to docs/archive/plans/PHASE_4b_POLISH.md diff --git a/plans/PHASE_4b_POLISH.md.bugs.md b/docs/archive/plans/PHASE_4b_POLISH.md.bugs.md similarity index 100% rename from plans/PHASE_4b_POLISH.md.bugs.md rename to docs/archive/plans/PHASE_4b_POLISH.md.bugs.md diff --git a/plans/PHASE_4b_SPEC.md b/docs/archive/plans/PHASE_4b_SPEC.md similarity index 100% rename from plans/PHASE_4b_SPEC.md rename to docs/archive/plans/PHASE_4b_SPEC.md diff --git a/plans/PHASE_4c_AREA.md b/docs/archive/plans/PHASE_4c_AREA.md similarity index 100% rename from plans/PHASE_4c_AREA.md rename to docs/archive/plans/PHASE_4c_AREA.md diff --git a/plans/PHASE_5_FILE_FEATURES.md b/docs/archive/plans/PHASE_5_FILE_FEATURES.md similarity index 100% rename from plans/PHASE_5_FILE_FEATURES.md rename to docs/archive/plans/PHASE_5_FILE_FEATURES.md diff --git a/plans/PHASE_6_GROUND_SNAPPING.md b/docs/archive/plans/PHASE_6_GROUND_SNAPPING.md similarity index 100% rename from plans/PHASE_6_GROUND_SNAPPING.md rename to docs/archive/plans/PHASE_6_GROUND_SNAPPING.md diff --git a/plans/PHASE_6a_AIRBORNE_LANDING.md b/docs/archive/plans/PHASE_6a_AIRBORNE_LANDING.md similarity index 100% rename from plans/PHASE_6a_AIRBORNE_LANDING.md rename to docs/archive/plans/PHASE_6a_AIRBORNE_LANDING.md diff --git a/plans/PHASE_7_UI_POLISH.md b/docs/archive/plans/PHASE_7_UI_POLISH.md similarity index 100% rename from plans/PHASE_7_UI_POLISH.md rename to docs/archive/plans/PHASE_7_UI_POLISH.md diff --git a/plans/PHASE_8_RIG_MODULARIZATION.md b/docs/archive/plans/PHASE_8_RIG_MODULARIZATION.md similarity index 100% rename from plans/PHASE_8_RIG_MODULARIZATION.md rename to docs/archive/plans/PHASE_8_RIG_MODULARIZATION.md diff --git a/plans/PHASE_9_PROP_EDITOR.md b/docs/archive/plans/PHASE_9_PROP_EDITOR.md similarity index 100% rename from plans/PHASE_9_PROP_EDITOR.md rename to docs/archive/plans/PHASE_9_PROP_EDITOR.md diff --git a/plans/PHASE_9a_PROP_ATTACH.md b/docs/archive/plans/PHASE_9a_PROP_ATTACH.md similarity index 100% rename from plans/PHASE_9a_PROP_ATTACH.md rename to docs/archive/plans/PHASE_9a_PROP_ATTACH.md diff --git a/plans/RAGDOLL_CLEANUP.md b/docs/archive/plans/RAGDOLL_CLEANUP.md similarity index 100% rename from plans/RAGDOLL_CLEANUP.md rename to docs/archive/plans/RAGDOLL_CLEANUP.md diff --git a/plans/SANDBOX_STAGE_BUILDER.md b/docs/archive/plans/SANDBOX_STAGE_BUILDER.md similarity index 100% rename from plans/SANDBOX_STAGE_BUILDER.md rename to docs/archive/plans/SANDBOX_STAGE_BUILDER.md diff --git a/plans/VECTOR_TERRAIN_SYSTEM.md b/docs/archive/plans/VECTOR_TERRAIN_SYSTEM.md similarity index 100% rename from plans/VECTOR_TERRAIN_SYSTEM.md rename to docs/archive/plans/VECTOR_TERRAIN_SYSTEM.md diff --git a/RIGGING.md b/docs/guides/RIGGING.md similarity index 88% rename from RIGGING.md rename to docs/guides/RIGGING.md index be0376c..7508587 100644 --- a/RIGGING.md +++ b/docs/guides/RIGGING.md @@ -51,7 +51,7 @@ Changing the profile WILL change the context menu item for each individual joint ## Task 2: Body part z-order -**Implemented (Phase 9 Task 2)** — see `docs/phase9_task2_zorder_spec.md`; the harness reorders the rig's `Body/*` children from the Facing profile (test harness top-bar "Facing" menu). +**Implemented (Phase 9 Task 2)** — see `docs/specs/phase9_task2_zorder_spec.md`; the harness reorders the rig's `Body/*` children from the Facing profile (test harness top-bar "Facing" menu). When the stickman is facing certain directions, body parts need to be drawn in a certain order: @@ -63,7 +63,7 @@ When the stickman is facing certain directions, body parts need to be drawn in a ## Task 3: Coordinates display -**Implemented (Phase 9 Task 3)** — see `docs/phase9_task3_coords_spec.md`; the harness adds a +**Implemented (Phase 9 Task 3)** — see `docs/specs/phase9_task3_coords_spec.md`; the harness adds a "Show Coords" checkbox (top bar, default ON) and a code-built top-right readout panel (`_coords_panel` + monospace `_coords_label`) showing, per frame, the Skeleton2D position and rotation, all 10 bone positions and rotations, and the 6 IK target positions (world-space @@ -98,8 +98,8 @@ The display should be easy to read to the right side of the screen. There should ## Task 4: Refactor facing direction / bone bend -**Implemented (Phase 9 Task 4)** — see `docs/phase9_task4_refactor_spec.md`; a new -`scripts/stickman_rig.gd` (`class_name StickmanRig`, attached to the `master_rig.tscn` root) +**Implemented (Phase 9 Task 4)** — see `docs/specs/phase9_task4_refactor_spec.md`; a new +`scripts/characters/stickman/stickman_rig.gd` (`class_name StickmanRig`, attached to the `master_rig.tscn` root) now owns the facing preset, the four per-joint TwoBoneIK bend flags, and the `Body/*` z-order as exported controls; the test harness became a thin driver that reads/writes the rig script. @@ -108,7 +108,7 @@ So rather than keeping that data in the harness, we need it in the master rig. T ## Task 5: Rig animation -**Implemented (Phase 9 Task 5)** — see `docs/phase9_task5_animation_spec.md`; the harness adds a dropdown, play/pause/resume, stop, and loop controls that drive the rig's `AnimationPlayer` directly. +**Implemented (Phase 9 Task 5)** — see `docs/specs/phase9_task5_animation_spec.md`; the harness adds a dropdown, play/pause/resume, stop, and loop controls that drive the rig's `AnimationPlayer` directly. Both AnimationPlayer and AnimationTree has been added to the master_rig.tscn. Animations should be selectable and playable in the harness. diff --git a/docs/prop_editor_user_guide.md b/docs/guides/prop_editor_user_guide.md similarity index 99% rename from docs/prop_editor_user_guide.md rename to docs/guides/prop_editor_user_guide.md index ea4749e..d63191a 100644 --- a/docs/prop_editor_user_guide.md +++ b/docs/guides/prop_editor_user_guide.md @@ -7,7 +7,7 @@ checklist. ## Start the editor -1. Open `res://scenes/prop_editor.tscn` in the Godot editor. +1. Open `res://scenes/editors/prop/prop_editor.tscn` in the Godot editor. 2. Press **F6** to run the current scene. The Prop Editor is not the project's configured main scene. If F6 runs a @@ -60,7 +60,7 @@ scroll it rather than expecting every section to fit at once. Save dialogs use the `.prop` filter. Save As normalizes a filename so it ends in `.prop` even if that extension was omitted. Keeping authored props under -`res://props/` is recommended. +`res://content/props/` is recommended. ### `👁️ View` @@ -482,7 +482,7 @@ Load dialogs report malformed or unsupported files through the Error dialog. ### Launch and layout -- [ ] Open `res://scenes/prop_editor.tscn` and press F6. +- [ ] Open `res://scenes/editors/prop/prop_editor.tscn` and press F6. - [ ] Confirm the File/View menus, prop-name field, mode buttons, canvas, Inspector, and status bar are visible. - [ ] Drag the main canvas/Inspector splitter left and right. diff --git a/BUGS.md b/docs/known_issues.md similarity index 82% rename from BUGS.md rename to docs/known_issues.md index 8f123eb..d7460bb 100644 --- a/BUGS.md +++ b/docs/known_issues.md @@ -69,11 +69,11 @@ The guide and joints should slightly 'ghost' in front of the objects so that the ## Stickman editor (Phase 9 Round 1) -> FIXED — 2026 Round 1: implemented per docs/phase9_round1_bugfix_spec.md; anisotropic scaling, joint-based anchors, and node/driver transform neutralization; verified with a 37-assertion headless smoke test. +> FIXED — 2026 Round 1: implemented per docs/specs/phase9_round1_bugfix_spec.md; anisotropic scaling, joint-based anchors, and node/driver transform neutralization; verified with a 37-assertion headless smoke test. ## Stickman editor (Phase 9 Round 2) -> FIXED — 2026 Round 2: implemented per docs/phase9_round2_bugfix_spec.md; restored the `RemoteTransform2D` driver rotations (`update_rotation = true` again) so mounted shapes follow their bones under IK flexing, switched the mount to the rig's hanging convention (joint anchor at the local origin, far end along +Y, per-family alignment rotation), auto-detected the drawn long axis so only that axis scales to the bone length (cross-axis thickness 1:1), zeroed the Head driver's local position so the chin lands on the neck joint, dropped `DEFAULT_LINE_WIDTH` to 2.0 (editor's outline width), and rewrote the harness bone overlay (true parent→child segments + leaf bones drawn to their IK targets). Verified with a 46-assertion headless smoke test. +> FIXED — 2026 Round 2: implemented per docs/specs/phase9_round2_bugfix_spec.md; restored the `RemoteTransform2D` driver rotations (`update_rotation = true` again) so mounted shapes follow their bones under IK flexing, switched the mount to the rig's hanging convention (joint anchor at the local origin, far end along +Y, per-family alignment rotation), auto-detected the drawn long axis so only that axis scales to the bone length (cross-axis thickness 1:1), zeroed the Head driver's local position so the chin lands on the neck joint, dropped `DEFAULT_LINE_WIDTH` to 2.0 (editor's outline width), and rewrote the harness bone overlay (true parent→child segments + leaf bones drawn to their IK targets). Verified with a 46-assertion headless smoke test. 1. **Lower-limb bones/joints invisible in "Show Bones".** The harness `_draw_bones()` drew parent-origin → bone-origin lines, so for lower bones the segment duplicated the upper segment and wrist/ankle joints (bone ends, not origins) were never drawn. Fixed: the overlay now draws a joint dot per bone, parent→child bone segments, and leaf bones out to their IK targets. 2. **IK only moves lower-arm shapes; upper-arm shapes static.** Round 1's `_neutralize_driver_rotations()` set `update_rotation = false` on the 10 `RemoteTransform2D` drivers, so `Body/*` visuals sat unrotated at fixed joints. Fixed: driver rotation neutralization removed — the drivers keep their defaults and the hanging-convention mount rotates every part to follow its bone in any pose. @@ -82,7 +82,7 @@ The guide and joints should slightly 'ghost' in front of the objects so that the ## Stickman editor (Phase 9 Round 3) -> FIXED — 2026 Round 3: implemented per docs/phase9_round3_bugfix_spec.md; one node per shape (closed → single Polygon2D, open → single Line2D), the part's preview rotation/scale composed into the mount before the hanging-convention fit (anchor/θ/bone-fit scale computed on the transformed geometry; ±180° flips swap the attachment to the drawn far end so flips render), and the harness head-leaf overlay drawn along the bone's own direction instead of to the LookAt aim point. Verified with a 26-assertion headless smoke test. +> FIXED — 2026 Round 3: implemented per docs/specs/phase9_round3_bugfix_spec.md; one node per shape (closed → single Polygon2D, open → single Line2D), the part's preview rotation/scale composed into the mount before the hanging-convention fit (anchor/θ/bone-fit scale computed on the transformed geometry; ±180° flips swap the attachment to the drawn far end so flips render), and the harness head-leaf overlay drawn along the bone's own direction instead of to the LookAt aim point. Verified with a 26-assertion headless smoke test. 1. **"The head bone has another bone extending out of it."** The harness bone overlay drew the Head leaf bone as a segment to `IK_Targets/Head` — the `SkeletonModification2DLookAt` aim point 232 px above the neck — a long line sticking out of the head. Fixed: the Head leaf now draws a ~90 px segment along the bone's own direction (`Vector2(length, 0)` rotated by `bone_angle` then `global_rotation`), so the head bone runs neck→head-top. Limb leaf bones (LeftLowerArm, RightLowerArm, LeftLowerLeg, RightLowerLeg) still draw to their IK targets. 2. **Duplicate Polygon2D + Line2D pairs per closed shape.** Closed shapes mounted as a `Polygon2D` fill **plus** a `Line2D` outline, producing two nodes per shape (e.g. `@Polygon2D@165` + `@Line2D@166`). Fixed: one node per shape — closed shapes mount as a single `Polygon2D`, open shapes as a single `Line2D` (width 2). @@ -96,24 +96,24 @@ When applying .stk v1.4 data to master*rig.tscn, shapes are severely distorted:G ## Stickman editor (Phase 9 Round 4) -> FIXED — 2026 Round 4: implemented per docs/phase9_round4_bugfix_spec.md; the mounted head geometry is dropped +28 px (`HEAD_CHIN_DROP`) in rig space so the chin lands 28 px below the neck, aligned with the editor silhouette guide's head circle bottom and overlapping the torso. Verified with a 15-assertion headless smoke test. +> FIXED — 2026 Round 4: implemented per docs/specs/phase9_round4_bugfix_spec.md; the mounted head geometry is dropped +28 px (`HEAD_CHIN_DROP`) in rig space so the chin lands 28 px below the neck, aligned with the editor silhouette guide's head circle bottom and overlapping the torso. Verified with a 15-assertion headless smoke test. 1. **Head too high in the harness.** The head bone origin (neck) is at `y = −391.5`, and the mounted head's chin (its local origin) previously landed on that neck joint, so the head floated above the torso with no overlap. The editor's silhouette guide draws the head as a radius-100 circle centered at the Head joint `(0, −463.5)`, so the circle's bottom is `−363.5` — 28 px below the neck, overlapping the torso's top region. Fixed: the mounted head points now receive a `Vector2(0.0, 28.0)` rig-space translation after the part transform and the (θ = 0, s = 1) transform, so the chin lands at world ≈ `(0, −363.5)` — the guide circle's bottom — overlapping the torso by 28 px, matching the editor guide. The drop is a rig-space fixture position (applied after part scale/rotation), so a flipped/rotated head drops identically. node hierarchy does not multiply the geometry scaling a second time.Deliverable:Please update StkRigAdapter.gd to implement these corrected coordinate transform calculations and return the full updated GDScript. ## Stickman editor (Phase 9 Round 5) -> ADDED — 2026 Round 5: implemented per docs/phase9_round5_bugfix_spec.md; the harness now reproduces the editor's guide-relative part placement **1:1**. The editor exports per-part `guide_offset` (bbox center − guide joint, preview space; a pure master-space delta) as **write-only** metadata on save (`FILE_VERSION` bumped to `"1.5"`; the load path ignores the key). The head maps to the guide **Neck** joint — the head bone's rig attachment origin `(0, −391.5)` — **not** the circle center, which sits 72 px above the neck. `StkRigAdapter` applies the offset as a node-frame translation `t = (guide_offset + (A − C)).rotated(−c_node)` only when the key is present (A = the mount anchor incl. the 180° flip rule, C = the raw bbox center, `c_node` = the part's driver `RemoteTransform2D.global_rotation` at apply time); old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4 `HEAD_CHIN_DROP`. **Note:** `.stk` files must be re-saved from the editor to populate the offsets — only files carrying `guide_offset` pick up the guide-relative placement in the harness. +> ADDED — 2026 Round 5: implemented per docs/specs/phase9_round5_bugfix_spec.md; the harness now reproduces the editor's guide-relative part placement **1:1**. The editor exports per-part `guide_offset` (bbox center − guide joint, preview space; a pure master-space delta) as **write-only** metadata on save (`FILE_VERSION` bumped to `"1.5"`; the load path ignores the key). The head maps to the guide **Neck** joint — the head bone's rig attachment origin `(0, −391.5)` — **not** the circle center, which sits 72 px above the neck. `StkRigAdapter` applies the offset as a node-frame translation `t = (guide_offset + (A − C)).rotated(−c_node)` only when the key is present (A = the mount anchor incl. the 180° flip rule, C = the raw bbox center, `c_node` = the part's driver `RemoteTransform2D.global_rotation` at apply time); old files without the key keep the previous offset-0 behavior and the head falls back to the Round 4 `HEAD_CHIN_DROP`. **Note:** `.stk` files must be re-saved from the editor to populate the offsets — only files carrying `guide_offset` pick up the guide-relative placement in the harness. ## Stickman editor (Phase 9 Round 6) -> FIXED — 2026 Round 6: implemented per docs/phase9_round6_bugfix_spec.md; the mount anchor is now the transformed end nearest the part's stored guide joint when `guide_offset` is present. Verified with a 32-assertion headless smoke test. +> FIXED — 2026 Round 6: implemented per docs/specs/phase9_round6_bugfix_spec.md; the mount anchor is now the transformed end nearest the part's stored guide joint when `guide_offset` is present. Verified with a 32-assertion headless smoke test. 1. **Lower-left-leg mounted 180° off its bone.** The leg's drawn knee was at the `min_x` end, but the per-side family rule anchored it at the `max_x` end, so the ankle attached at the joint and the shin hung backward. Root cause: the adapter picked the joint end with fixed per-side family rules (left limbs → `max_x`, right limbs → `min_x`) plus a 180° flip heuristic, but the user's drawn-side conventions are **inconsistent across parts** — no fixed rule can know which drawn end is the knee. Fix: when a part carries `guide_offset`, the anchor is whichever transformed end (`E(J_raw)` or `E(F_pt_raw)`) is **nearest `center − guide_offset`** (the part's guide joint) — the knee is now at the joint. Old files without the key keep the previous family rules. 2. **Lower-right-arm mounted 180° off its bone.** Same root cause mirrored on the right: the drawn elbow was at the `max_x` end, but the family rule anchored the arm at the `min_x` end, so the wrist attached at the joint and the forearm hung backward. Fix: the same nearest-end-to-guide-joint anchor selection puts the elbow at the joint. Old files without the key keep the previous rules. ## Stickman editor (Phase 9 Round 7) -> ADDED — 2026 Round 7: implemented per docs/phase9_round7_feature_spec.md; the test harness now exposes **six** draggable IK handles. `IK_HANDLE_PATHS` gains `"Head"` (`IK_Targets/Head`, the `SkeletonModification2DLookAt` aim point) and `"Torso"` (`IK_Targets/Torso`, whose child `RemoteTransform2D` moves the hip bone). Dragging the **Torso** handle moves **bones only** — the marker's `RemoteTransform2D` translates the hip bone and the whole skeleton + `Body/*` visuals follow rigidly, while the limb/head targets stay put (dragging the figure away from them stretches the limbs toward the stationary targets, per user decision). Dragging the **Head** handle drives the Head bone's LookAt rotation (clamped at the authored ~55° constraint); `Body/Head` follows. `_handle_color()` colors the head marker yellow (`HANDLE_COLOR_HEAD`) and the torso marker magenta (`HANDLE_COLOR_TORSO`); hands stay green, feet blue. The IK overlay additionally draws a null-guarded semi-transparent yellow aim line from the Head bone origin to the head marker (visual aid for the LookAt test). Verified with a 17-assertion headless test (Torso moved by (60, −40) → `Skeleton2D/Torso` and `Body/*` translate by exactly (60, −40); Head marker moved → Head bone + `Body/Head` rotate). +> ADDED — 2026 Round 7: implemented per docs/specs/phase9_round7_feature_spec.md; the test harness now exposes **six** draggable IK handles. `IK_HANDLE_PATHS` gains `"Head"` (`IK_Targets/Head`, the `SkeletonModification2DLookAt` aim point) and `"Torso"` (`IK_Targets/Torso`, whose child `RemoteTransform2D` moves the hip bone). Dragging the **Torso** handle moves **bones only** — the marker's `RemoteTransform2D` translates the hip bone and the whole skeleton + `Body/*` visuals follow rigidly, while the limb/head targets stay put (dragging the figure away from them stretches the limbs toward the stationary targets, per user decision). Dragging the **Head** handle drives the Head bone's LookAt rotation (clamped at the authored ~55° constraint); `Body/Head` follows. `_handle_color()` colors the head marker yellow (`HANDLE_COLOR_HEAD`) and the torso marker magenta (`HANDLE_COLOR_TORSO`); hands stay green, feet blue. The IK overlay additionally draws a null-guarded semi-transparent yellow aim line from the Head bone origin to the head marker (visual aid for the LookAt test). Verified with a 17-assertion headless test (Torso moved by (60, −40) → `Skeleton2D/Torso` and `Body/*` translate by exactly (60, −40); Head marker moved → Head bone + `Body/Head` rotate). ## Sandbox Stage — Director Tool (Phase 3a) @@ -126,7 +126,7 @@ When applying .stk v1.4 data to master*rig.tscn, shapes are severely distorted:G ## Sandbox Stage (Phase 3c) -> FIXED — 2026-09-04: all five bugs fixed (developer stage). Bugs 1 and 4 (implementation defects — panel rows never parented into the list container) fixed by adding the row to the list container; bugs 2 and 3 (spec/design defects — unconditional entry-point items) fixed by gating popup items behind an `about_to_popup` refresh; bug 5 (spec/design defect — theme schema) fixed by extending `sandbox_theme.json`'s `fonts` block + adding `apply_font(...)` to the Phase 3c panels/editors. See per-bug notes below. Spec amendments: `docs/phase_3c_editor_spec.md` §2 decision 8, §5.3, §11, and §14. +> FIXED — 2026-09-04: all five bugs fixed (developer stage). Bugs 1 and 4 (implementation defects — panel rows never parented into the list container) fixed by adding the row to the list container; bugs 2 and 3 (spec/design defects — unconditional entry-point items) fixed by gating popup items behind an `about_to_popup` refresh; bug 5 (spec/design defect — theme schema) fixed by extending `sandbox_theme.json`'s `fonts` block + adding `apply_font(...)` to the Phase 3c panels/editors. See per-bug notes below. Spec amendments: `docs/specs/phase_3c_editor_spec.md` §2 decision 8, §5.3, §11, and §14. 1. **"Edit Queue" shows no actions (implementation defect).** `queue_panel.gd` `refresh()` builds each row via `_make_row()` into `_rows` but never `_list.add_child(...)`s it, so rows are tracked for reorder but never rendered. Fix: add `_list.add_child(panel)` in `_make_row()` (or in `refresh()`). - **FIXED (2026-09-04):** rows are now parented into the list container (`_list.add_child(panel)` in `_make_row()`), so the queue rows render. @@ -141,7 +141,7 @@ When applying .stk v1.4 data to master*rig.tscn, shapes are severely distorted:G ## Stickman rig (head mirror + recovery re-anchor) -> FIXED — 2026-09-04: two runtime bugs in `scripts/stickman_rig.gd` fixed (developer stage), covered by a new 76-assertion headless regression suite `tests/test_phase3c_walk_recovery.gd`. See per-bug notes below. No spec changes; no `.stk` / scene / `.tscn` file touched. +> FIXED — 2026-09-04: two runtime bugs in `scripts/characters/stickman/stickman_rig.gd` fixed (developer stage), covered by a new 76-assertion headless regression suite `tests/test_phase3c_walk_recovery.gd`. See per-bug notes below. No spec changes; no `.stk` / scene / `.tscn` file touched. 1. **Head does not mirror when the rig faces LEFT (implementation defect).** `_apply_head_flip()` reflected the mirror through the Head **Pivot** node's negative-determinant scale, which Godot re-decomposed into a per-frame Y-scale flip as the Head bone rotated under `LookAt` (the driver only forwards `update_rotation = true`, so the negative scale leaked into the rotation channel). Root cause: folding the reflection into a driver whose transform is applied through the rotation channel. Fix: keep the Pivot's transform identity for **all** profiles (scale `(1,1)`, rotation `0`) and mirror the mounted geometry directly on `Body/Head.scale.x` (`-1` for LEFT, `(1,1)` for RIGHT/FORWARD); the driver's `update_scale = false` never clobbers this manual scale. Note: the driver may canonicalize the mirror to `(1,-1)` — the X-mirror semantics are unchanged. - **FIXED (2026-09-04):** `_apply_head_flip()` now resets the Pivot transform to identity for every profile and applies the LEFT mirror as `Body/Head.scale.x = -1` (RIGHT/FORWARD → `(1,1)`). @@ -152,5 +152,5 @@ When applying .stk v1.4 data to master*rig.tscn, shapes are severely distorted:G ## Stickman rig (whole-rig Y-axis mirror — design change) -> **DESIGN CHANGE (proposed by user, 2026-09-05):** replace the per-part/head mirroring for `FacingProfile` LEFT/RIGHT with mirroring the **entire stickman** over the Y-axis (`Master.scale.x = -1` for LEFT, `(1,1)` otherwise), so the head AND body mirror together and face the correct direction. `_apply_head_flip()` and the `Body/Head.scale.x` mirror are **removed**; per-joint `flip_bend_direction` flags (`PROFILE_FLAGS`) and `Z_ORDER_BY_PROFILE` are **kept provisionally** (unchanged). **Walk-clip mapping — Option A (single canonical clip):** `walk_right` is the canonical walk; for `FacingProfile.LEFT` the rig root is X-mirrored and the **same `walk_right`** clip plays mirrored (`walk_left` becomes unused at runtime; the animation `.:facing_profile` tracks are neutralized/removed and facing is set explicitly by `set_facing_profile()`/`walk_to()`). See `docs/phase9_task4_refactor_spec.md` §9a and the `AGENTS.md` stickman_rig section. +> **DESIGN CHANGE (proposed by user, 2026-09-05):** replace the per-part/head mirroring for `FacingProfile` LEFT/RIGHT with mirroring the **entire stickman** over the Y-axis (`Master.scale.x = -1` for LEFT, `(1,1)` otherwise), so the head AND body mirror together and face the correct direction. `_apply_head_flip()` and the `Body/Head.scale.x` mirror are **removed**; per-joint `flip_bend_direction` flags (`PROFILE_FLAGS`) and `Z_ORDER_BY_PROFILE` are **kept provisionally** (unchanged). **Walk-clip mapping — Option A (single canonical clip):** `walk_right` is the canonical walk; for `FacingProfile.LEFT` the rig root is X-mirrored and the **same `walk_right`** clip plays mirrored (`walk_left` becomes unused at runtime; the animation `.:facing_profile` tracks are neutralized/removed and facing is set explicitly by `set_facing_profile()`/`walk_to()`). See `docs/specs/phase9_task4_refactor_spec.md` §9a and the `AGENTS.md` stickman_rig section. > **Follow-up (2026-09-05):** under the LEFT root mirror the head was displaced/flipped — (a) the head `RemoteTransform2D` (`Skeleton2D/Torso/Head/Pivot`) set `update_scale = false`, a partial-channel push that re-canonicalized `Body/Head.scale` under the mirrored root (per-frame Y-flips/wrap-jumps), and (b) the head `SkeletonModification2DLookAt` is **not mirror-invariant**, writing a bone rotation 180° off the FORWARD aim that flips the head to hang below the neck. Fixed: the head driver now pushes the **full transform** like every other `Body` driver (`update_scale` no longer `false`), and `_apply_head_lookat_mirror_mode()` **disables** the LookAt when facing LEFT, pinning the head bone to the FORWARD canonical aim (π) with `_pin_mirrored_head_rotation()` re-asserting the pin each `_physics_process` frame while ANIMATED/RECOVERING (RIGHT/FORWARD re-enable the LookAt). Interactive head-aiming is intentionally static while facing LEFT. diff --git a/docs/plans/README.md b/docs/plans/README.md new file mode 100644 index 0000000..eb15476 --- /dev/null +++ b/docs/plans/README.md @@ -0,0 +1,3 @@ +# Active plans + +Place future implementation plans in this directory. Move completed or superseded plan drafts to `../archive/plans/`. diff --git a/specs/phase-4.5-collision-bodies.md b/docs/specs/phase-4.5-collision-bodies.md similarity index 98% rename from specs/phase-4.5-collision-bodies.md rename to docs/specs/phase-4.5-collision-bodies.md index 52b1531..b0a5f0b 100644 --- a/specs/phase-4.5-collision-bodies.md +++ b/docs/specs/phase-4.5-collision-bodies.md @@ -1,11 +1,11 @@ # Phase 4.5a — Collision Bodies, Physics Events, and Hit Reactions -Source plan: `plans/PHASE_4.5_COLLISION_BODIES.md` +Source plan: `docs/archive/plans/PHASE_4.5_COLLISION_BODIES.md` ## Scope and verified corrections -Implement the feature in `scripts/stickman_rig.gd`, `scripts/prop_block.gd`, and -`scripts/sandbox_stage.gd`. Add a focused headless regression script, but no scene, `.stk`, +Implement the feature in `scripts/characters/stickman/stickman_rig.gd`, `scripts/props/prop_block.gd`, and +`scripts/editors/stage/sandbox_stage.gd`. Add a focused headless regression script, but no scene, `.stk`, registry, rule-payload, layout, or theme changes. The implementation must correct these plan/code mismatches: @@ -354,7 +354,7 @@ capsule/circle proxies rather than per-shape polygons and keeps reaction timing post-head-pin physics slot. After editor verification, append these deferred trade-offs to -`docs/tech_debt_and_optimizations.md`: fixed proxy radii do not follow arbitrary mounted widths; +`docs/architecture/tech_debt_and_optimizations.md`: fixed proxy radii do not follow arbitrary mounted widths; the head radius is fixed although its center follows geometry; signal-time direct state may use the documented fallback on rare timing edges; contact speed is total prop speed rather than normal relative speed/impulse; and the post-IK reaction slot remains engine-order sensitive. diff --git a/docs/phase2_spec.md b/docs/specs/phase2_spec.md similarity index 91% rename from docs/phase2_spec.md rename to docs/specs/phase2_spec.md index 4829f75..a977ad1 100644 --- a/docs/phase2_spec.md +++ b/docs/specs/phase2_spec.md @@ -120,7 +120,7 @@ Menu id: 0=Line, 1=Rectangle, 2=Circle, 3=Create Point (only shown in vertex con 4. Append `1` to `vertex_flags` at the corresponding position. 5. `queue_redraw()` and `emit shape_changed`. -*Note: PROJECT.md says "a small hollow rectangle will appear in the outline where the user clicked — this is not a point(vertex)". I interpret this to mean: the vertex is placed on the outline at the nearest edge midpoint. The "not a point" language means it's a vertex, just drawn differently (hollow rectangle).* +*Note: docs/archive/PROJECT.md says "a small hollow rectangle will appear in the outline where the user clicked — this is not a point(vertex)". I interpret this to mean: the vertex is placed on the outline at the nearest edge midpoint. The "not a point" language means it's a vertex, just drawn differently (hollow rectangle).* ### 2g. Vertex Dragging @@ -207,12 +207,12 @@ Add a `deselect()` method to `BodyPartPanel` that sets `_selected=false` and `qu | File | Changes | |---|---| -| `scripts/body_part_panel.gd` | Major: vertex editing state, selection, zoom, context menu refactor, rendering with closed flag & vertex_flags, circle→12 vertices, mouse handling expanded | -| `scripts/stickman_editor.gd` | Version bump to 1.1, save format includes closed/vertex_flags, load handles 1.0+1.1, selection coordination signal | -| `scripts/whole_stickman_preview.gd` | Zoom support, closed-based rendering instead of shape_type-based, input coordinate transform | -| `scenes/body_part_panel.tscn` | Minimal — ContextMenu items may be reduced or left as-is (dynamic repopulation in script). *Consider adding a 4th static item "Create Point" hidden by default.* | +| `scripts/editors/stickman/body_part_panel.gd` | Major: vertex editing state, selection, zoom, context menu refactor, rendering with closed flag & vertex_flags, circle→12 vertices, mouse handling expanded | +| `scripts/editors/stickman/stickman_editor.gd` | Version bump to 1.1, save format includes closed/vertex_flags, load handles 1.0+1.1, selection coordination signal | +| `scripts/editors/stickman/whole_stickman_preview.gd` | Zoom support, closed-based rendering instead of shape_type-based, input coordinate transform | +| `scenes/editors/stickman/body_part_panel.tscn` | Minimal — ContextMenu items may be reduced or left as-is (dynamic repopulation in script). *Consider adding a 4th static item "Create Point" hidden by default.* | | `README.md` | Document Phase 2 features, updated .stk format, vertex editing, zoom | -| `docs/phase2_spec.md` | This file | +| `docs/specs/phase2_spec.md` | This file | --- diff --git a/docs/phase3_spec.md b/docs/specs/phase3_spec.md similarity index 95% rename from docs/phase3_spec.md rename to docs/specs/phase3_spec.md index 613d16c..36cd19a 100644 --- a/docs/phase3_spec.md +++ b/docs/specs/phase3_spec.md @@ -475,7 +475,7 @@ func _on_view_menu_id_pressed(id: int) -> void: ## 5. Scene File Changes -### 5a. `scenes/stickman_editor.tscn` +### 5a. `scenes/editors/stickman/stickman_editor.tscn` Add new nodes under the root `StickmanEditor`: @@ -502,7 +502,7 @@ step = 1.0 rounded = true ``` -### 5b. `scenes/body_part_panel.tscn` +### 5b. `scenes/editors/stickman/body_part_panel.tscn` Add a ColorPicker popup: @@ -538,12 +538,12 @@ text = "OK" | File | Changes | |---|---| -| `scripts/body_part_panel.gd` | Major: panning (middle mouse), grid drawing, snap-to-grid in vertex drag, ColorPicker integration, "Delete" shape, "Remove Point" vertex delete, context menu refactor, reset_view(), set_grid_size(), set_snap_enabled() | -| `scripts/stickman_editor.gd` | Modular: Edit/View menus, GridConfigDialog, settings.json load/save, broadcast grid/snap settings, Reset Views handler | -| `scripts/whole_stickman_preview.gd` | Major: panning, grid drawing, snap-to-grid in part drag, reset_view(), set_grid_size(), set_snap_enabled() | -| `scenes/stickman_editor.tscn` | Add GridConfigDialog + children | -| `scenes/body_part_panel.tscn` | Add ColorPickerPopup + children | -| `docs/phase3_spec.md` | This file | +| `scripts/editors/stickman/body_part_panel.gd` | Major: panning (middle mouse), grid drawing, snap-to-grid in vertex drag, ColorPicker integration, "Delete" shape, "Remove Point" vertex delete, context menu refactor, reset_view(), set_grid_size(), set_snap_enabled() | +| `scripts/editors/stickman/stickman_editor.gd` | Modular: Edit/View menus, GridConfigDialog, settings.json load/save, broadcast grid/snap settings, Reset Views handler | +| `scripts/editors/stickman/whole_stickman_preview.gd` | Major: panning, grid drawing, snap-to-grid in part drag, reset_view(), set_grid_size(), set_snap_enabled() | +| `scenes/editors/stickman/stickman_editor.tscn` | Add GridConfigDialog + children | +| `scenes/editors/stickman/body_part_panel.tscn` | Add ColorPickerPopup + children | +| `docs/specs/phase3_spec.md` | This file | --- diff --git a/docs/phase7_round1_spec.md b/docs/specs/phase7_round1_spec.md similarity index 91% rename from docs/phase7_round1_spec.md rename to docs/specs/phase7_round1_spec.md index 2b42032..cc5ddff 100644 --- a/docs/phase7_round1_spec.md +++ b/docs/specs/phase7_round1_spec.md @@ -2,13 +2,13 @@ ## Overview -This round fixes three user-reported bugs in the **pose silhouette guide** introduced in Phase 7 (see `BUGS.md`, "Stickman editor (Phase 7 Round 1)"): +This round fixes three user-reported bugs in the **pose silhouette guide** introduced in Phase 7 (see `docs/known_issues.md`, "Stickman editor (Phase 7 Round 1)"): 1. **Silhouette starting position** — on startup the guide is anchored at a fixed world position (`GUIDE_OFFSET = (170, 580)`) that puts the figure up-left of the preview, forcing the user to pan over to it. The guide should start **centered** in the Whole Stickman view. 2. **Menu label never changes** — View → "Show Pose Guide" toggles visibility but its text stays "Show Pose Guide" regardless of state. It must read **"Hide Pose Guide"** when visible and **"Show Pose Guide"** when hidden. 3. **Guide renders behind parts** — the guide is drawn *below* user parts, so it cannot be used to judge how well parts align to the joints/limbs. It should "ghost" *in front of* the parts. -All three are confined to two scripts (`scripts/whole_stickman_preview.gd`, `scripts/stickman_editor.gd`) and touch no `.stk` format and no `settings.json` schema (the `show_pose_guide` key already exists). **No `.tscn` changes are required.** +All three are confined to two scripts (`scripts/editors/stickman/whole_stickman_preview.gd`, `scripts/editors/stickman/stickman_editor.gd`) and touch no `.stk` format and no `settings.json` schema (the `show_pose_guide` key already exists). **No `.tscn` changes are required.** Key behavior that is **preserved** (must not regress): @@ -35,7 +35,7 @@ Why this approach: - **Minimal** — a one-line change to `_guide_to_preview()` plus a renamed constant; no new state, no camera/timing machinery. - **Preserves world-fixture semantics** — the guide is *not* screen-locked; it still moves with pan/zoom. "Centered" is true at the default view and after `Reset Views`, and the user can still pan/zoom the guide anywhere afterward. -### 1c. Code changes (`scripts/whole_stickman_preview.gd`) +### 1c. Code changes (`scripts/editors/stickman/whole_stickman_preview.gd`) Replace the constant (line 48): @@ -104,7 +104,7 @@ Make the label reflect the **action** the click will perform, per the user's exa **User decision: drop the checkmark.** The item is text-only; the action wording itself communicates the current state. Remove all `set_item_checked` usage for this item (setup + refresh). -### 2c. Code changes (`scripts/stickman_editor.gd`) +### 2c. Code changes (`scripts/editors/stickman/stickman_editor.gd`) Replace `_guide_menu_label()` (lines 271–272): @@ -184,7 +184,7 @@ Move `_draw_silhouette_guide()` to draw **after** the part loop but **before** t - **Drag highlight above the guide** — the yellow drag rect (drawn during `Interaction.TRANSLATE`) is also an affordance; keep it above the guide for the same reason. (Draw guide at step 5, before the highlight at step 6, so highlight + gizmos remain on top.) - **Labels** — part labels (`H`, `T`, `LUA`, …) are drawn inside the part loop (step 4), so the guide may overlap them slightly. This is acceptable: labels are small, and the guide is semi-transparent. (Splitting labels into a later pass is not worth the complexity.) -### 3c. Code changes (`scripts/whole_stickman_preview.gd`) +### 3c. Code changes (`scripts/editors/stickman/whole_stickman_preview.gd`) Delete line 322 (`_draw_silhouette_guide()` from its current position) and insert it after the part loop, immediately before the "Highlight dragged part" block (i.e., between line 384 and line 386): @@ -211,9 +211,9 @@ The guide is pure `draw_*` calls and participates in **no** input logic. Hit-tes | File | Changes | |---|---| -| `scripts/whole_stickman_preview.gd` | Replace `GUIDE_OFFSET` const with `GUIDE_FIGURE_CENTER`; re-center in `_guide_to_preview()`; move `_draw_silhouette_guide()` call after the part loop (before the drag highlight). | -| `scripts/stickman_editor.gd` | Make `_guide_menu_label()` dynamic; refresh text in `_update_guide_menu_item()` and drop the checkmark (`set_item_checked` calls removed); add `_update_guide_menu_item()` call in `_load_settings()`. | -| `docs/phase7_round1_spec.md` | This file. | +| `scripts/editors/stickman/whole_stickman_preview.gd` | Replace `GUIDE_OFFSET` const with `GUIDE_FIGURE_CENTER`; re-center in `_guide_to_preview()`; move `_draw_silhouette_guide()` call after the part loop (before the drag highlight). | +| `scripts/editors/stickman/stickman_editor.gd` | Make `_guide_menu_label()` dynamic; refresh text in `_update_guide_menu_item()` and drop the checkmark (`set_item_checked` calls removed); add `_update_guide_menu_item()` call in `_load_settings()`. | +| `docs/specs/phase7_round1_spec.md` | This file. | | `README.md` | Update §5 "Pose Silhouette Guide" (centering behavior, "ghosts in front of parts", dynamic menu label) and the menu-bar structure diagram (§ "Menu bar structure"). | | `AGENTS.md` | Update the Phase 7 notes: `GUIDE_OFFSET (170, 580)` → dynamic centering via `GUIDE_FIGURE_CENTER`; "renders above the grid but below user parts" → "above the grid and above user parts, below selection gizmos"; "Show Pose Guide" → dynamic "Hide/Show Pose Guide" label. | @@ -264,8 +264,8 @@ There is no automated test suite; verification is manual in the editor plus the ## 8. Recommended Implementation Order -1. `scripts/whole_stickman_preview.gd` — Bug 1 centering (`GUIDE_FIGURE_CENTER` + `_guide_to_preview`). -2. `scripts/whole_stickman_preview.gd` — Bug 3 reorder (`_draw_silhouette_guide()` move). -3. `scripts/stickman_editor.gd` — Bug 2 dynamic label + `_update_guide_menu_item()` + `_load_settings()` sync. +1. `scripts/editors/stickman/whole_stickman_preview.gd` — Bug 1 centering (`GUIDE_FIGURE_CENTER` + `_guide_to_preview`). +2. `scripts/editors/stickman/whole_stickman_preview.gd` — Bug 3 reorder (`_draw_silhouette_guide()` move). +3. `scripts/editors/stickman/stickman_editor.gd` — Bug 2 dynamic label + `_update_guide_menu_item()` + `_load_settings()` sync. 4. Manual verification (§6) + `--headless --check-only --quit`. 5. Doc updates (`README.md`, `AGENTS.md`). diff --git a/docs/phase7_spec.md b/docs/specs/phase7_spec.md similarity index 95% rename from docs/phase7_spec.md rename to docs/specs/phase7_spec.md index 3282942..7498634 100644 --- a/docs/phase7_spec.md +++ b/docs/specs/phase7_spec.md @@ -60,7 +60,7 @@ The rest transforms are static authored data; reading them at runtime buys nothi ## 2. WholeStickmanPreview — Rendering -All changes live in `scripts/whole_stickman_preview.gd`. No new scene/node is required (the guide is a `_draw()` overlay, mirroring how `_draw_grid()` is already inlined). +All changes live in `scripts/editors/stickman/whole_stickman_preview.gd`. No new scene/node is required (the guide is a `_draw()` overlay, mirroring how `_draw_grid()` is already inlined). ### 2a. New constants @@ -116,7 +116,7 @@ const GUIDE_BONES: Array = [ ] ``` -> The 13 joint names in `GUIDE_JOINTS` are exactly the key pivot locations required by PROJECT.md. `GUIDE_SCALE` and `GUIDE_OFFSET` are the only tunables mapping master-rig coordinates into preview world space (see §3 for the chosen values). +> The 13 joint names in `GUIDE_JOINTS` are exactly the key pivot locations required by docs/archive/PROJECT.md. `GUIDE_SCALE` and `GUIDE_OFFSET` are the only tunables mapping master-rig coordinates into preview world space (see §3 for the chosen values). ### 2b. New state + public API @@ -351,9 +351,9 @@ Because `_broadcast_settings()` is already called in `_ready()` (after `_load_se | File | Changes | |---|---| -| `scripts/whole_stickman_preview.gd` | Add guide constants, `_show_guide` state, `set_show_guide()`, `_draw_silhouette_guide()` + `_guide_to_preview()`/`_guide_color()`/`_guide_joint_color()` helpers; call guide draw between grid and parts in `_on_preview_draw()`. | -| `scripts/stickman_editor.gd` | Add `_show_guide` state, `_view_menu` member, "Show Pose Guide" checkable View menu item (id 1), `_on_view_menu_about_to_popup`/`_update_guide_menu_item`, settings key `show_pose_guide`, broadcast to preview. | -| `docs/phase7_spec.md` | This file. | +| `scripts/editors/stickman/whole_stickman_preview.gd` | Add guide constants, `_show_guide` state, `set_show_guide()`, `_draw_silhouette_guide()` + `_guide_to_preview()`/`_guide_color()`/`_guide_joint_color()` helpers; call guide draw between grid and parts in `_on_preview_draw()`. | +| `scripts/editors/stickman/stickman_editor.gd` | Add `_show_guide` state, `_view_menu` member, "Show Pose Guide" checkable View menu item (id 1), `_on_view_menu_about_to_popup`/`_update_guide_menu_item`, settings key `show_pose_guide`, broadcast to preview. | +| `docs/specs/phase7_spec.md` | This file. | | `README.md` | Document the guide, the View menu item, and the settings key (see §7). | No `.tscn` changes are required. @@ -376,7 +376,7 @@ No `.tscn` changes are required. ## 7. Testing / Verification -There is no automated test suite in the repo (only a sample figure `stickmen/test.stk`). Verification is manual in the Godot editor, plus the project parse check: +There is no automated test suite in the repo (only a sample figure `content/stickmen/test.stk`). Verification is manual in the Godot editor, plus the project parse check: 1. **Load check** — run the project; no parse errors; main scene opens. 2. **Toggle** — View → Show Pose Guide is checked by default and the guide is visible; clicking it unchecks it and hides the guide; clicking again re-shows it. diff --git a/docs/phase8_spec.md b/docs/specs/phase8_spec.md similarity index 88% rename from docs/phase8_spec.md rename to docs/specs/phase8_spec.md index 792e46d..fd6e4cd 100644 --- a/docs/phase8_spec.md +++ b/docs/specs/phase8_spec.md @@ -14,9 +14,9 @@ Scope is intentionally tight: Key facts established from exploration (cited throughout): -- The Phase 7 silhouette guide already hardcodes the 13 rest-pose joint coordinates of `master_rig.tscn` as `GUIDE_JOINTS` in `scripts/whole_stickman_preview.gd:59-73`. These are the **same pivots** the adapter's bone-fitting targets. +- The Phase 7 silhouette guide already hardcodes the 13 rest-pose joint coordinates of `master_rig.tscn` as `GUIDE_JOINTS` in `scripts/editors/stickman/whole_stickman_preview.gd:59-73`. These are the **same pivots** the adapter's bone-fitting targets. - `master_rig.tscn`'s bone names/paths match the Phase 8 requirement text exactly (`Skeleton2D/Torso/LeftUpperArm`, `LeftLowerArm`, `RightUpperArm`, `RightLowerArm`, `LeftUpperLeg`, `LeftLowerLeg`, `RightUpperLeg`, `RightLowerLeg`, `IK_Targets/Left_Leg`, `IK_Targets/Right_Leg`, `IK_Targets/Left_Hand`, `IK_Targets/Right_Hand`). -- The editor already serializes per-part `shapes`/`position`/`rotation`/`scale` in `scripts/stickman_editor.gd:363-394`. +- The editor already serializes per-part `shapes`/`position`/`rotation`/`scale` in `scripts/editors/stickman/stickman_editor.gd:363-394`. --- @@ -111,7 +111,7 @@ Notes (reproducible): - **Bone length vs. joint distance.** `upper_arm_length` is the *bone length* `168.0`, not the literal `|shoulder - elbow| = sqrt(168² + 8²) ≈ 168.19`. The 8 px vertical offset comes from the arm bone's `rotation = 0.0477` (`master_rig.tscn:170`), which the adapter does **not** touch. Writing `168.0` reproduces the rest pose exactly when the adapter sets `LeftUpperArm.length = 168.0` and leaves `rotation` alone. (Writing `168.19` would over-extend the bone.) The requirement's "distance between shoulder and elbow" is treated as the bone length. - **`torso_length`** has no corresponding single bone (the spine is the `Head` bone authored at `y = -391.5`); it is informational in `proportions` (no adapter bone op consumes it). -- **`RightUpperLeg.length = 90.0` inconsistency.** `master_rig.tscn:245` authors the right upper leg bone at `90.0` while the left is `200.0` (`:222`) and `master_rig2.tscn:262` fixes the right to `200.0`. Use **200.0** (the symmetric/corrected value). The adapter overwrites both legs anyway, so the discrepancy is moot after fitting — but it is flagged in §5f. +- **`RightUpperLeg.length = 90.0` inconsistency.** `master_rig.tscn:245` authors the right upper leg bone at `90.0` while the left is `200.0` (`:222`) and `scenes/legacy/master_rig2.tscn:262` fixes the right to `200.0`. Use **200.0** (the symmetric/corrected value). The adapter overwrites both legs anyway, so the discrepancy is moot after fitting — but it is flagged in §5f. --- @@ -150,7 +150,7 @@ Empty part (no shapes, or all shapes with <2 points) → `pivot {0,0}`, `length --- -## 4. Editor changes (`scripts/stickman_editor.gd`) +## 4. Editor changes (`scripts/editors/stickman/stickman_editor.gd`) ### 4a. New constants @@ -240,7 +240,7 @@ No change to `_on_save_file_selected` (`:287-300`) beyond what the above touches ### 5a. Class shape & API -New file `res://scripts/stk_rig_adapter.gd`: +New file `res://scripts/characters/stickman/stk_rig_adapter.gd`: ```gdscript class_name StkRigAdapter @@ -346,15 +346,15 @@ For each of the 10 part keys, mount the part's `.stk` shapes into the correspond > The coordinate mapping here is the most under-specified piece of the requirement. §5e defines a concrete, deterministic v1 (translate `pivot`→origin, scale along the segment axis to the bone length). The fine-grained fidelity (matching the user's in-preview `rotation`/`scale`/`position` exactly) is intentionally left to the runtime consumer — see §10 Q3. -### 5f. `master_rig.tscn` vs `master_rig2.tscn` +### 5f. `master_rig.tscn` vs `scenes/legacy/master_rig2.tscn` - The adapter targets **`master_rig.tscn`** (the clean scene Phase 7 derived `GUIDE_JOINTS` from). It has **no** `metadata/_local_pose_override_enabled_` on its bones. -- `master_rig2.tscn` is a variant that (a) adds `metadata/_local_pose_override_enabled_ = true` to every bone, (b) fixes `RightUpperLeg.length` from `90.0` → `200.0` (`master_rig2.tscn:262`), and (c) explicitly sets `enabled = true` on the modification stack. `clear_pose.gd` (an `EditorScript`) exists to strip the pose-override metadata and reset bone scales to `Vector2.ONE`/`rest`. +- `scenes/legacy/master_rig2.tscn` is a variant that (a) adds `metadata/_local_pose_override_enabled_ = true` to every bone, (b) fixes `RightUpperLeg.length` from `90.0` → `200.0` (`scenes/legacy/master_rig2.tscn:262`), and (c) explicitly sets `enabled = true` on the modification stack. `clear_pose.gd` (an `EditorScript`) exists to strip the pose-override metadata and reset bone scales to `Vector2.ONE`/`rest`. - **Implication:** the adapter is written against `master_rig.tscn` node names (identical in both) and overwrites lengths anyway, so it works with either — but the `90.0` right-leg value in `master_rig.tscn` is a latent bug the adapter must not rely on (it writes `RightUpperLeg.length` from `proportions`, §5c). ### 5g. Editor does **not** verify/import the adapter -The editor only *produces* data the adapter *consumes*. There is no editor→adapter reference, no `preload("res://scripts/stk_rig_adapter.gd")`, no scene that imports it. "Verification" is a standalone concern: the adapter is syntax-checked with the project parse check and (optionally) a future headless smoke test (§8). This keeps the editor decoupled from the runtime pipeline. +The editor only *produces* data the adapter *consumes*. There is no editor→adapter reference, no `preload("res://scripts/characters/stickman/stk_rig_adapter.gd")`, no scene that imports it. "Verification" is a standalone concern: the adapter is syntax-checked with the project parse check and (optionally) a future headless smoke test (§8). This keeps the editor decoupled from the runtime pipeline. --- @@ -362,9 +362,9 @@ The editor only *produces* data the adapter *consumes*. There is no editor→ada | File | Changes | |---|---| -| `scripts/stickman_editor.gd` | `FILE_VERSION` → `"1.4"`; add `"1.4"` to `SUPPORTED_VERSIONS`; add `PROPORTIONS` const + `X_AXIS_PARTS` const; add `_compute_part_pivot_length()`; extend `_collect_all_shape_data()` with `pivot`/`length`; add `proportions` to `_build_json_data()`. | -| `scripts/stk_rig_adapter.gd` | **New.** `StkRigAdapter` (`RefCounted`) with `static func apply(stk_data, rig)` + `_fit_bones` / `_recalibrate_ik` / `_mount_shapes` + node-path consts (§5). | -| `docs/phase8_spec.md` | This file. | +| `scripts/editors/stickman/stickman_editor.gd` | `FILE_VERSION` → `"1.4"`; add `"1.4"` to `SUPPORTED_VERSIONS`; add `PROPORTIONS` const + `X_AXIS_PARTS` const; add `_compute_part_pivot_length()`; extend `_collect_all_shape_data()` with `pivot`/`length`; add `proportions` to `_build_json_data()`. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | **New.** `StkRigAdapter` (`RefCounted`) with `static func apply(stk_data, rig)` + `_fit_bones` / `_recalibrate_ik` / `_mount_shapes` + node-path consts (§5). | +| `docs/specs/phase8_spec.md` | This file. | | `README.md` | Document the `.stk` v1.4 `proportions`/`pivot`/`length` keys, the `StkRigAdapter.gd` script, and the runtime-pipeline note (§11). | | `AGENTS.md` | Add a Phase 8 note (v1.4 export + `StkRigAdapter.gd`). | @@ -392,10 +392,10 @@ There is no automated test suite in the repo (no `test/` directory, no GUT addon ``` ..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit ``` - > The plain `--check-only` form hangs on renderer init in 4.7.x; use `--headless --check-only --quit` (as established in `docs/phase7_round1_spec.md` §6). The user's stated command `..\Godot_v4.7.1-stable_win64_console.exe . --check-only` is equivalent in intent but should include `--headless --quit`. + > The plain `--check-only` form hangs on renderer init in 4.7.x; use `--headless --check-only --quit` (as established in `docs/specs/phase7_round1_spec.md` §6). The user's stated command `..\Godot_v4.7.1-stable_win64_console.exe . --check-only` is equivalent in intent but should include `--headless --quit`. 2. **Save emits v1.4** — File → Save; inspect the `.stk`: `version == "1.4"`, a top-level `proportions` with the 5 values (`168.0/200.0/200.0/200.0/391.5`), and every `body_parts.*` entry has `pivot {x,y}` + `length` (arm parts' `length` = bbox width; leg/torso/head = bbox height). 3. **Empty part** — a part with no shapes writes `pivot {0,0}` / `length 0.0`; no crash. -4. **Backward-compat load** — load `stickmen/basic.stk` (v1.0) and `stickmen/test.stk` (v1.1); no error; save; result is v1.4 with computed pivot/length/proportions. +4. **Backward-compat load** — load `content/stickmen/basic.stk` (v1.0) and `content/stickmen/test.stk` (v1.1); no error; save; result is v1.4 with computed pivot/length/proportions. 5. **Adapter smoke test (standalone, optional/headless)** — a small `--headless` SceneTree script that instantiates `master_rig.tscn`, calls `StkRigAdapter.apply(sample_stk, rig)`, and asserts: `LeftUpperArm.length == 168.0`, `LeftLowerArm.position.x == -168.0`, `LeftUpperLeg.length == 200.0`, `IK_Targets/Left_Leg.position.y == 400.0`, `IK_Targets/Left_Hand.position == (-168.0, -456.0)`. (Add under `test/` later if GUT is introduced; out of scope for this phase.) --- @@ -408,7 +408,7 @@ There is no automated test suite in the repo (no `test/` directory, no GUT addon | D2 | `pivot` = local bbox center; `length` = axis-specific extent (arms→Δx, legs/torso/head→Δy) (§3) | Matches the editor's actual rotation pivot and the adapter's `position.x` vs `position.y` bone convention. | | D3 | Version `"1.3" → "1.4"`, write-only metadata, no load migration (§1) | `pivot`/`length`/`proportions` are recomputed on save; old files load unchanged and gain keys on next save. | | D4 | `StkRigAdapter` = `RefCounted` static `apply()`, standalone script (§5a) | No scene/autoload dependency; consumed by a future runtime pipeline, never by the editor. | -| D5 | Adapter targets `master_rig.tscn` (not `master_rig2.tscn`) (§5f) | It's the clean scene Phase 7 derived `GUIDE_JOINTS` from; `master_rig2` is a pose-override variant with a fixed right-leg length. | +| D5 | Adapter targets `master_rig.tscn` (not `scenes/legacy/master_rig2.tscn`) (§5f) | It's the clean scene Phase 7 derived `GUIDE_JOINTS` from; `master_rig2` is a pose-override variant with a fixed right-leg length. | | D6 | Visual mount = update existing `Body/*` nodes in place (§5e) | Preserves `RemoteTransform2D` driving and node names; `.stk` shape → `Line2D`/`Polygon2D` with pivot→origin + length-normalization. | | D7 | Editor never imports/verifies the adapter (§5g) | Keeps the editor decoupled; the adapter is verified standalone (parse check + optional headless smoke test). | | D8 | Bone fitting adds lower-bone `.length` writes beyond the requirement's literal text (§5c) | Without it the lower limbs don't scale; the addition is a safe no-op at default proportions (flagged Q2). | @@ -428,16 +428,16 @@ There is no automated test suite in the repo (no `test/` directory, no GUT addon ## 11. README / AGENTS Updates - **README §"File format (`.stk`)"** — bump the example to `"1.4"`; add the top-level `proportions` table and the per-part `pivot`/`length` rows to the Part object table; update the migration note (`v1.0–v1.3` auto-migrate; pivot/length/proportions recomputed on save). -- **README new subsection (or §"Project structure")** — document `scripts/stk_rig_adapter.gd`: its `static apply()` API, that it fits an instantiated `master_rig.tscn` to a loaded `.stk`, and that it is a runtime-pipeline utility (not used by the editor). -- **AGENTS.md** — add a Phase 8 note: `FILE_VERSION "1.4"`; top-level `proportions` + per-part `pivot`/`length` computed on save in `stickman_editor.gd`; `scripts/stk_rig_adapter.gd` (`class_name StkRigAdapter`) as a standalone runtime adapter targeting `master_rig.tscn`. +- **README new subsection (or §"Project structure")** — document `scripts/characters/stickman/stk_rig_adapter.gd`: its `static apply()` API, that it fits an instantiated `master_rig.tscn` to a loaded `.stk`, and that it is a runtime-pipeline utility (not used by the editor). +- **AGENTS.md** — add a Phase 8 note: `FILE_VERSION "1.4"`; top-level `proportions` + per-part `pivot`/`length` computed on save in `stickman_editor.gd`; `scripts/characters/stickman/stk_rig_adapter.gd` (`class_name StkRigAdapter`) as a standalone runtime adapter targeting `master_rig.tscn`. --- ## 12. Recommended Implementation Order -1. `scripts/stickman_editor.gd` — constants (`FILE_VERSION`, `SUPPORTED_VERSIONS`, `PROPORTIONS`, `X_AXIS_PARTS`) + `_compute_part_pivot_length()`. -2. `scripts/stickman_editor.gd` — wire `pivot`/`length` into `_collect_all_shape_data()` and `proportions` into `_build_json_data()`. -3. `scripts/stk_rig_adapter.gd` — `apply()` + `_fit_bones()` + `_recalibrate_ik()` + `_mount_shapes()` with §5b paths. +1. `scripts/editors/stickman/stickman_editor.gd` — constants (`FILE_VERSION`, `SUPPORTED_VERSIONS`, `PROPORTIONS`, `X_AXIS_PARTS`) + `_compute_part_pivot_length()`. +2. `scripts/editors/stickman/stickman_editor.gd` — wire `pivot`/`length` into `_collect_all_shape_data()` and `proportions` into `_build_json_data()`. +3. `scripts/characters/stickman/stk_rig_adapter.gd` — `apply()` + `_fit_bones()` + `_recalibrate_ik()` + `_mount_shapes()` with §5b paths. 4. Manual verification (§8) + `--headless --check-only --quit`. 5. Optional headless adapter smoke test. 6. Doc updates (`README.md`, `AGENTS.md`). diff --git a/docs/phase9_round1_bugfix_spec.md b/docs/specs/phase9_round1_bugfix_spec.md similarity index 90% rename from docs/phase9_round1_bugfix_spec.md rename to docs/specs/phase9_round1_bugfix_spec.md index 7edfdc0..30597e1 100644 --- a/docs/phase9_round1_bugfix_spec.md +++ b/docs/specs/phase9_round1_bugfix_spec.md @@ -2,7 +2,7 @@ ## Overview -The runtime adapter `scripts/stk_rig_adapter.gd` mounts `.stk` v1.4 vector shapes onto +The runtime adapter `scripts/characters/stickman/stk_rig_adapter.gd` mounts `.stk` v1.4 vector shapes onto `master_rig.tscn`'s `Body/*` visual nodes. Two defects make the mounted result unusable: 1. **Giant Polygon Explosions (legs)** — `_mount_shapes()` computes a single **uniform** @@ -13,7 +13,7 @@ The runtime adapter `scripts/stk_rig_adapter.gd` mounts `.stk` v1.4 vector shape is the bounding-box **center** `(min+max)/2`, not the joint connection (neck base / hip). Shapes therefore rotate around their geometric center instead of the bone joint. -The fix is confined to `scripts/stk_rig_adapter.gd` (the file the bug's "ActorFactory.gd" +The fix is confined to `scripts/characters/stickman/stk_rig_adapter.gd` (the file the bug's "ActorFactory.gd" reference actually means; the factory `stickman_factory.gd` only calls it). It introduces **anisotropic scaling** (scale only the bone's primary axis), **joint-based anchors** (computed from the part bbox at mount time, not the file `pivot`), and a **node-transform reset** on the @@ -21,7 +21,7 @@ from the part bbox at mount time, not the file `pivot`), and a **node-transform ### Scope -- **Changed:** `scripts/stk_rig_adapter.gd` (the mount pipeline only — `_mount_shapes`, +- **Changed:** `scripts/characters/stickman/stk_rig_adapter.gd` (the mount pipeline only — `_mount_shapes`, `_mount_shape`, `_transform_points`, plus new bbox/anchor/scale helpers). - **Unchanged:** `_fit_bones()`, `_recalibrate_ik()`, `_bone_length_for()`, all node-path constants, `stickman_factory.gd`, `test_harness.gd`, `master_rig.tscn`. @@ -93,7 +93,7 @@ torso-facing end — see §8 Q3.) --- -## 3. Function-level change list (`scripts/stk_rig_adapter.gd`) +## 3. Function-level change list (`scripts/characters/stickman/stk_rig_adapter.gd`) ### 3a. `_mount_shapes()` (line 166) — rewrite the per-part loop @@ -222,8 +222,8 @@ aspirational and was never added as a file). Verification is manual + parse chec 1. **Parse check** (per project convention, from `C:\Godot4\stickman`): `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit` -2. **Harness visual check** (F6 on `res://scenes/test_harness.tscn`), load each of - `stickmen/basic.stk`, `stickmen/test.stk`, `stickmen/break.stk`: +2. **Harness visual check** (F6 on `res://scenes/tools/test_harness.tscn`), load each of + `content/stickmen/basic.stk`, `content/stickmen/test.stk`, `content/stickmen/break.stk`: - **Legs** are thin (no block explosion) — cross-axis thickness is preserved at drawn size; only the long axis stretches to the bone length. - **Head** is unscaled (identical to drawn size), anchored at the neck base. @@ -294,29 +294,29 @@ harness and resolve Q1 if limbs render rotated. | File | Change | |---|---| -| `scripts/stk_rig_adapter.gd` | Rewrite `_mount_shapes()` per §3a; add `_compute_part_bbox()`, `_compute_anchor()`, `_compute_part_length()`, `_compute_scale()`, `_reset_node_transform()`, `_neutralize_driver_rotations()`; change `_mount_shape()` and `_transform_points()` signatures to `anchor: Vector2, scale: Vector2`; add `X_AXIS_PARTS` const; call `_neutralize_driver_rotations()` from `apply()`. | -| `docs/phase9_round1_bugfix_spec.md` | This file. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | Rewrite `_mount_shapes()` per §3a; add `_compute_part_bbox()`, `_compute_anchor()`, `_compute_part_length()`, `_compute_scale()`, `_reset_node_transform()`, `_neutralize_driver_rotations()`; change `_mount_shape()` and `_transform_points()` signatures to `anchor: Vector2, scale: Vector2`; add `X_AXIS_PARTS` const; call `_neutralize_driver_rotations()` from `apply()`. | +| `docs/specs/phase9_round1_bugfix_spec.md` | This file. | No changes to `stickman_factory.gd`, `test_harness.gd`, `stickman_editor.gd`, `master_rig.tscn`, -`master_rig2.tscn`, `clear_pose.gd`, `master_rig_builder.gd`, or the `.stk` files. +`scenes/legacy/master_rig2.tscn`, `clear_pose.gd`, `master_rig_builder.gd`, or the `.stk` files. **Adjacent files checked, no impact:** -- `master_rig2.tscn` — node-for-node mirror of `master_rig.tscn` (same `Body/*` + `RemoteTransform2D` - layout, `master_rig2.tscn:72-286`); adapter targets `master_rig.tscn`, and the fix applies to +- `scenes/legacy/master_rig2.tscn` — node-for-node mirror of `master_rig.tscn` (same `Body/*` + `RemoteTransform2D` + layout, `scenes/legacy/master_rig2.tscn:72-286`); adapter targets `master_rig.tscn`, and the fix applies to either since node names/paths are identical. - `clear_pose.gd` — an `EditorScript` that resets bone `scale`/`rest` on the *edited* scene; not consumed by the runtime adapter. -- `scripts/master_rig_builder.gd` — builds a **different** rig (`Sticky/Stickman/.../Hip` naming), +- `scripts/tools/master_rig_builder.gd` — builds a **different** rig (`Sticky/Stickman/.../Hip` naming), unrelated to `master_rig.tscn`'s `Master/Body/Skeleton2D/Torso` naming; not an adapter target. --- ## 10. Recommended implementation order -1. `scripts/stk_rig_adapter.gd` — add `X_AXIS_PARTS` const + `_compute_part_bbox()`. -2. `scripts/stk_rig_adapter.gd` — add `_compute_anchor()`, `_compute_part_length()`, +1. `scripts/characters/stickman/stk_rig_adapter.gd` — add `X_AXIS_PARTS` const + `_compute_part_bbox()`. +2. `scripts/characters/stickman/stk_rig_adapter.gd` — add `_compute_anchor()`, `_compute_part_length()`, `_compute_scale()`, `_reset_node_transform()`, `_neutralize_driver_rotations()`. -3. `scripts/stk_rig_adapter.gd` — rewrite `_mount_shapes()`; change `_mount_shape()` / +3. `scripts/characters/stickman/stk_rig_adapter.gd` — rewrite `_mount_shapes()`; change `_mount_shape()` / `_transform_points()` signatures and point math; wire `_neutralize_driver_rotations()` into `apply()`. 4. Parse check + harness visual verification (§5); flip the torso anchor if it renders upside-down. diff --git a/docs/phase9_round2_bugfix_spec.md b/docs/specs/phase9_round2_bugfix_spec.md similarity index 94% rename from docs/phase9_round2_bugfix_spec.md rename to docs/specs/phase9_round2_bugfix_spec.md index 33afcda..e8b99c7 100644 --- a/docs/phase9_round2_bugfix_spec.md +++ b/docs/specs/phase9_round2_bugfix_spec.md @@ -3,7 +3,7 @@ ## Overview Testing `break.stk` in the test harness revealed four defects. This round fixes all four with -changes confined to `scripts/stk_rig_adapter.gd` (mount pipeline) and `scripts/test_harness.gd` +changes confined to `scripts/characters/stickman/stk_rig_adapter.gd` (mount pipeline) and `scripts/tools/test_harness.gd` (debug bone overlay). Root causes were established with a headless probe of the live rig (spawned via `StickmanFactory` with the modification stack enabled, 2 frames). @@ -127,9 +127,9 @@ dots + target joints — lower limbs clearly visible. | File | Changes | |---|---| -| `scripts/stk_rig_adapter.gd` | Remove `_neutralize_driver_rotations()` + call; new anchor/scale/rotation table in `_mount_shapes()` + `_compute_anchor`/`_compute_scale`/rotation helpers; `DEFAULT_LINE_WIDTH = 2.0`; zero the Head driver's local position in `_fit_bones()`; update doc comments. | -| `scripts/test_harness.gd` | Rewrite `_draw_bones()` (child-direction segments + leaf→IK-target segments + dots). | -| `docs/phase9_round2_bugfix_spec.md` | This file. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | Remove `_neutralize_driver_rotations()` + call; new anchor/scale/rotation table in `_mount_shapes()` + `_compute_anchor`/`_compute_scale`/rotation helpers; `DEFAULT_LINE_WIDTH = 2.0`; zero the Head driver's local position in `_fit_bones()`; update doc comments. | +| `scripts/tools/test_harness.gd` | Rewrite `_draw_bones()` (child-direction segments + leaf→IK-target segments + dots). | +| `docs/specs/phase9_round2_bugfix_spec.md` | This file. | ## 5. Edge cases @@ -188,4 +188,4 @@ dots + target joints — lower limbs clearly visible. `DEFAULT_LINE_WIDTH = 2.0`; zero Head driver position. 3. `test_harness.gd` — rewrite `_draw_bones()`. 4. Headless smoke test + parse check. -5. Docs (`BUGS.md` Round 2 note, `AGENTS.md`, `README.md`). +5. Docs (`docs/known_issues.md` Round 2 note, `AGENTS.md`, `README.md`). diff --git a/docs/phase9_round3_bugfix_spec.md b/docs/specs/phase9_round3_bugfix_spec.md similarity index 92% rename from docs/phase9_round3_bugfix_spec.md rename to docs/specs/phase9_round3_bugfix_spec.md index 55e2261..b88ff66 100644 --- a/docs/phase9_round3_bugfix_spec.md +++ b/docs/specs/phase9_round3_bugfix_spec.md @@ -21,7 +21,7 @@ Three defects remain after Round 2 (user report, testing `break.stk` in the harn ## 1. Root-cause analysis -### 1a. Overlay head segment (`scripts/test_harness.gd`) +### 1a. Overlay head segment (`scripts/tools/test_harness.gd`) `_draw_bones()` treats every leaf bone identically: draw to its IK target. For the four limbs the targets ARE the wrist/ankle joints (correct). For the Head the target is a LookAt aim point @@ -32,13 +32,13 @@ the targets ARE the wrist/ankle joints (correct). For the Head the target is a L **Fix:** special-case the Head leaf — draw along the bone's own direction; keep the limb leaf bones drawing to their IK targets. -### 1b. Duplicate nodes (`scripts/stk_rig_adapter.gd` `_mount_shape`) +### 1b. Duplicate nodes (`scripts/characters/stickman/stk_rig_adapter.gd` `_mount_shape`) Closed shapes mount `Polygon2D` + closed `Line2D` outline (Round 1 mirrored the editor's fill+outline rendering). The user wants one node per shape: closed → `Polygon2D` only; open → `Line2D` only. `DEFAULT_LINE_WIDTH = 2.0` still applies to open lines. -### 1c. Part rotation/scale (`scripts/stk_rig_adapter.gd` mount pipeline) +### 1c. Part rotation/scale (`scripts/characters/stickman/stk_rig_adapter.gd` mount pipeline) The `.stk` part dict carries `rotation` (degrees, about the part bbox center) and `scale` (about the bbox center) from the Whole Stickman preview @@ -84,7 +84,7 @@ double-rotates. The correct composition (per user direction) is: ## 2. Fix specification -### 2a. `scripts/test_harness.gd` — head leaf segment +### 2a. `scripts/tools/test_harness.gd` — head leaf segment In `_draw_bones()`, for the leaf bone `Head` (no Bone2D child): draw `origin → origin + Vector2(0, -length).rotated(global_rotation)` instead of the IK-target @@ -96,12 +96,12 @@ already draws `origin + R(global_rotation)·(length, 0)` — change it to use th for the Head (`bone_angle = −90`) that yields `(0, −90)` rotated by the pose — the desired segment; for other bones `bone_angle` is 0 → `(length, 0)` — same as today). -### 2b. `scripts/stk_rig_adapter.gd` — one node per shape +### 2b. `scripts/characters/stickman/stk_rig_adapter.gd` — one node per shape `_mount_shape()`: closed → `Polygon2D` only (polygon + color; **no** outline Line2D); open → `Line2D` (width `DEFAULT_LINE_WIDTH`). Delete the outline branch. -### 2c. `scripts/stk_rig_adapter.gd` — part transform composition +### 2c. `scripts/characters/stickman/stk_rig_adapter.gd` — part transform composition Read per-part `rotation` (float, **degrees**, default 0.0) and `scale` (`{x, y}`, default `(1, 1)`) from the part dict. Apply in the mount pipeline: @@ -146,9 +146,9 @@ constants, null guards. | File | Changes | |---|---| -| `scripts/stk_rig_adapter.gd` | `_mount_shapes()` reads part `rotation`/`scale`; mount pipeline composes `E_full` + fitted anchor/θ/fit-scale + flip heuristic; `_mount_shape()` mounts one node per shape (closed → Polygon2D only). | -| `scripts/test_harness.gd` | `_draw_bones()` leaf fallback uses the bone's `bone_angle` direction (Head draws a 90 px segment inside the head; limbs unchanged). | -| `docs/phase9_round3_bugfix_spec.md` | This file. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | `_mount_shapes()` reads part `rotation`/`scale`; mount pipeline composes `E_full` + fitted anchor/θ/fit-scale + flip heuristic; `_mount_shape()` mounts one node per shape (closed → Polygon2D only). | +| `scripts/tools/test_harness.gd` | `_draw_bones()` leaf fallback uses the bone's `bone_angle` direction (Head draws a 90 px segment inside the head; limbs unchanged). | +| `docs/specs/phase9_round3_bugfix_spec.md` | This file. | ## 4. Open question — RESOLVED (user directive) @@ -210,4 +210,4 @@ constants, null guards. 2. `stk_rig_adapter.gd` — `_mount_shape()` single-node-per-shape. 3. `test_harness.gd` — head-leaf overlay segment. 4. Headless smoke test + parse check. -5. Docs (`BUGS.md` Round 3 note, `AGENTS.md`, `README.md`). +5. Docs (`docs/known_issues.md` Round 3 note, `AGENTS.md`, `README.md`). diff --git a/docs/phase9_round4_bugfix_spec.md b/docs/specs/phase9_round4_bugfix_spec.md similarity index 91% rename from docs/phase9_round4_bugfix_spec.md rename to docs/specs/phase9_round4_bugfix_spec.md index 38bad55..105d613 100644 --- a/docs/phase9_round4_bugfix_spec.md +++ b/docs/specs/phase9_round4_bugfix_spec.md @@ -6,7 +6,7 @@ User report: the head is **too high** in the harness. Reference: the editor's si head is a circle; the bottom of the mounted head shape must line up with the bottom of that circle relative to the torso bone, so the head **overlaps the torso** some. -### Guide geometry (source of truth, `scripts/whole_stickman_preview.gd`) +### Guide geometry (source of truth, `scripts/editors/stickman/whole_stickman_preview.gd`) | Constant | Value | |---|---| @@ -29,7 +29,7 @@ The mounted head's bottom (chin) must land at `y = −363.5` — i.e. the mounte gets a `+28 px` local Y offset so the chin sits 28 px below the neck joint, matching the guide circle's bottom and overlapping the torso. -## Fix specification — `scripts/stk_rig_adapter.gd` +## Fix specification — `scripts/characters/stickman/stk_rig_adapter.gd` 1. Add `const HEAD_CHIN_DROP := 28.0` with a doc comment deriving the value (guide head circle bottom `−363.5` − neck `−391.5` = 28; the guide circle has radius 100 @@ -48,8 +48,8 @@ fixture position, not part geometry), so a 180°-rotated head drops by the same | File | Changes | |---|---| -| `scripts/stk_rig_adapter.gd` | `HEAD_CHIN_DROP` const + head-points translation. | -| `docs/phase9_round4_bugfix_spec.md` | This file. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | `HEAD_CHIN_DROP` const + head-points translation. | +| `docs/specs/phase9_round4_bugfix_spec.md` | This file. | ## Test plan diff --git a/docs/phase9_round5_bugfix_spec.md b/docs/specs/phase9_round5_bugfix_spec.md similarity index 90% rename from docs/phase9_round5_bugfix_spec.md rename to docs/specs/phase9_round5_bugfix_spec.md index 09c3d8b..266c71b 100644 --- a/docs/phase9_round5_bugfix_spec.md +++ b/docs/specs/phase9_round5_bugfix_spec.md @@ -18,7 +18,7 @@ size-independent delta in preview pixels = master pixels, since `GUIDE_SCALE = 1 writes it as per-part write-only metadata. The adapter converts it to the joint-end offset and translates the mounted geometry accordingly, in the bone's frame. -## 1. Guide reference geometry (`scripts/whole_stickman_preview.gd`) +## 1. Guide reference geometry (`scripts/editors/stickman/whole_stickman_preview.gd`) `GUIDE_JOINTS` (`:59-73`) gives every joint in master space. The part→joint mapping: @@ -37,7 +37,7 @@ translates the mounted geometry accordingly, in the bone's frame. ## 2. Editor changes -### 2a. `scripts/whole_stickman_preview.gd` +### 2a. `scripts/editors/stickman/whole_stickman_preview.gd` Add a public method: @@ -49,7 +49,7 @@ func get_guide_joint_preview(joint_name: String) -> Vector2: return _guide_to_preview(GUIDE_JOINTS[joint_name]) ``` -### 2b. `scripts/stickman_editor.gd` +### 2b. `scripts/editors/stickman/stickman_editor.gd` 1. Add `const GUIDE_JOINT_FOR_PART: Dictionary` (the 10-row mapping above). 2. Bump `FILE_VERSION` `"1.4"` → `"1.5"`; add `"1.5"` to `SUPPORTED_VERSIONS`; update the @@ -68,7 +68,7 @@ func get_guide_joint_preview(joint_name: String) -> Vector2: (`_apply_json_data` unchanged); v1.0–v1.4 files load unchanged and gain the key on their next save. -## 3. Adapter changes (`scripts/stk_rig_adapter.gd`) +## 3. Adapter changes (`scripts/characters/stickman/stk_rig_adapter.gd`) 1. Read `guide_offset` (`{x, y}`) from the part dict (default `null`/absent). 2. **Only when present** (old files keep the current offset-0 behavior), compute the anchor @@ -98,10 +98,10 @@ the Hips joint, reproducing the editor placement. | File | Changes | |---|---| -| `scripts/whole_stickman_preview.gd` | `get_guide_joint_preview()` public method. | -| `scripts/stickman_editor.gd` | `GUIDE_JOINT_FOR_PART` const; `FILE_VERSION "1.5"` (+ `SUPPORTED_VERSIONS` + error text); `guide_offset` computed per non-head part in `_collect_all_shape_data()`. | -| `scripts/stk_rig_adapter.gd` | Read + apply `guide_offset` (node-frame translation, only when present). | -| `docs/phase9_round5_bugfix_spec.md` | This file. | +| `scripts/editors/stickman/whole_stickman_preview.gd` | `get_guide_joint_preview()` public method. | +| `scripts/editors/stickman/stickman_editor.gd` | `GUIDE_JOINT_FOR_PART` const; `FILE_VERSION "1.5"` (+ `SUPPORTED_VERSIONS` + error text); `guide_offset` computed per non-head part in `_collect_all_shape_data()`. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | Read + apply `guide_offset` (node-frame translation, only when present). | +| `docs/specs/phase9_round5_bugfix_spec.md` | This file. | ## 5. Edge cases @@ -156,4 +156,4 @@ the Hips joint, reproducing the editor placement. 2. `stickman_editor.gd` — const map + version bump + `guide_offset` in `_collect_all_shape_data()`. 3. `stk_rig_adapter.gd` — read/apply `guide_offset`. 4. Headless smoke test + parse check. -5. Docs (BUGS.md Round 5 note, AGENTS.md, README.md). +5. Docs (docs/known_issues.md Round 5 note, AGENTS.md, README.md). diff --git a/docs/phase9_round6_bugfix_spec.md b/docs/specs/phase9_round6_bugfix_spec.md similarity index 94% rename from docs/phase9_round6_bugfix_spec.md rename to docs/specs/phase9_round6_bugfix_spec.md index 88ffcd8..8094aee 100644 --- a/docs/phase9_round6_bugfix_spec.md +++ b/docs/specs/phase9_round6_bugfix_spec.md @@ -29,7 +29,7 @@ the joint is the **user's placement**, which Round 5 now stores as `guide_offset part space). This fixes both buggy parts, preserves every correct case, and naturally reproduces the 180° flip behavior (a flipped part's far end lands near the joint). -## 2. Fix specification — `scripts/stk_rig_adapter.gd` +## 2. Fix specification — `scripts/characters/stickman/stk_rig_adapter.gd` In `_compute_mount_transform()` (the anchor/flip section, ~lines 397–407): @@ -56,8 +56,8 @@ record which end is the joint), the stored guide placement decides the pivot sid | File | Changes | |---|---| -| `scripts/stk_rig_adapter.gd` | Nearest-end anchor selection when `guide_offset` is present (replaces the family-side + flip choice for that case). | -| `docs/phase9_round6_bugfix_spec.md` | This file. | +| `scripts/characters/stickman/stk_rig_adapter.gd` | Nearest-end anchor selection when `guide_offset` is present (replaces the family-side + flip choice for that case). | +| `docs/specs/phase9_round6_bugfix_spec.md` | This file. | ## 4. Edge cases @@ -100,4 +100,4 @@ record which end is the joint), the stored guide placement decides the pivot sid 1. `stk_rig_adapter.gd` — nearest-end anchor selection (guarded by `has_guide_offset`). 2. Headless smoke test + parse check. -3. Docs (BUGS.md Round 6 note, AGENTS.md, README.md). +3. Docs (docs/known_issues.md Round 6 note, AGENTS.md, README.md). diff --git a/docs/phase9_round7_feature_spec.md b/docs/specs/phase9_round7_feature_spec.md similarity index 92% rename from docs/phase9_round7_feature_spec.md rename to docs/specs/phase9_round7_feature_spec.md index d5c05c3..af7a279 100644 --- a/docs/phase9_round7_feature_spec.md +++ b/docs/specs/phase9_round7_feature_spec.md @@ -13,12 +13,12 @@ The rig already provides both targets: - `IK_Targets/Head` (Marker2D at `(0, −624)`) — the `SkeletonModification2DLookAt` aim point for the Head bone (`bone_index = 1`, constrained, `constraint_in_localspace = true`). -The harness (`scripts/test_harness.gd`) already has generic handle dragging +The harness (`scripts/tools/test_harness.gd`) already has generic handle dragging (`IK_HANDLE_PATHS` → `_hit_test_handle` → `_dragging_handle` → `_handle_mouse_motion` sets `global_position`; `_draw_ik_handles` draws markers). Only the two new handles need wiring, plus torso-follow semantics. -## 1. Fix specification — `scripts/test_harness.gd` +## 1. Fix specification — `scripts/tools/test_harness.gd` ### 1a. Register the new handles @@ -56,8 +56,8 @@ existing overlay draw style, e.g. width 1.5/zoom, alpha ~0.5.) | File | Changes | |---|---| -| `scripts/test_harness.gd` | `IK_HANDLE_PATHS` + 2 entries (Head, Torso); head/torso marker colors; head aim line. | -| `docs/phase9_round7_feature_spec.md` | This file. | +| `scripts/tools/test_harness.gd` | `IK_HANDLE_PATHS` + 2 entries (Head, Torso); head/torso marker colors; head aim line. | +| `docs/specs/phase9_round7_feature_spec.md` | This file. | ## 3. Edge cases @@ -100,4 +100,4 @@ existing overlay draw style, e.g. width 1.5/zoom, alpha ~0.5.) 1. `test_harness.gd` — new handles + colors + aim line + torso-follow. 2. Headless verification + parse check. -3. Docs (BUGS.md Round 7 note, AGENTS.md, README.md). +3. Docs (docs/known_issues.md Round 7 note, AGENTS.md, README.md). diff --git a/docs/phase9_task2_zorder_spec.md b/docs/specs/phase9_task2_zorder_spec.md similarity index 91% rename from docs/phase9_task2_zorder_spec.md rename to docs/specs/phase9_task2_zorder_spec.md index 7e0e841..16ab032 100644 --- a/docs/phase9_task2_zorder_spec.md +++ b/docs/specs/phase9_task2_zorder_spec.md @@ -2,7 +2,7 @@ ## Overview -RIGGING.md Task 2: when the stickman faces different directions, body parts must be drawn in +docs/guides/RIGGING.md Task 2: when the stickman faces different directions, body parts must be drawn in the right order relative to the torso: 1. **Facing left** → left upper/lower arm **and** left upper/lower leg draw **behind** the @@ -57,7 +57,7 @@ with node names from `master_rig.tscn` (`"Head"`, `"Body"`, `"LeftUpperArm"`, it (requirement 2). - The head is frontmost in all three (requirement 4). -## 3. Implementation — `scripts/test_harness.gd` +## 3. Implementation — `scripts/tools/test_harness.gd` ### 3a. Constants @@ -93,11 +93,11 @@ the back. | File | Changes | |---|---| -| `scripts/test_harness.gd` | `BODY_CONTAINER_PATH`, `Z_ORDER_BY_PROFILE`, `_body_container`, resolve + clear + `_apply_body_z_order()`, hook in `_apply_facing_profile()`. | -| `docs/phase9_task2_zorder_spec.md` | This file. | +| `scripts/tools/test_harness.gd` | `BODY_CONTAINER_PATH`, `Z_ORDER_BY_PROFILE`, `_body_container`, resolve + clear + `_apply_body_z_order()`, hook in `_apply_facing_profile()`. | +| `docs/specs/phase9_task2_zorder_spec.md` | This file. | | `AGENTS.md` | Test-harness section: "Phase 9 Task 2 body-part z-order" bullet. | | `README.md` | Test-harness bullet: facing profiles now also reorder `Body/*` draw order. | -| `RIGGING.md` | Mark Task 2 implemented (Task 1 was documented in the same way). | +| `docs/guides/RIGGING.md` | Mark Task 2 implemented (Task 1 was documented in the same way). | ## 5. Edge cases @@ -121,7 +121,7 @@ the back. 1. Parse check: `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit` (user-requested; no errors). 2. Temporary headless SceneTree script (pattern from the Round 7 spec; deleted afterwards): - - Instantiate the harness scene, `add_child` it, call `_load_and_spawn("res://stickmen/basic.stk")`. + - Instantiate the harness scene, `add_child` it, call `_load_and_spawn("res://content/stickmen/basic.stk")`. - Assert `Body` children order equals `Z_ORDER_BY_PROFILE[FacingProfile.FORWARD]`. - Call `_apply_facing_profile(FacingProfile.LEFT)` → assert left limb pairs precede the torso node (left arm pair before left leg pair) and right pairs follow it (right leg @@ -132,6 +132,6 @@ the back. ## 8. Implementation order -1. `scripts/test_harness.gd` — constants, state, resolution, reorder + hook. +1. `scripts/tools/test_harness.gd` — constants, state, resolution, reorder + hook. 2. Parse check + headless verification script (temp, then removed). -3. Docs: `AGENTS.md`, `README.md`, `RIGGING.md`. +3. Docs: `AGENTS.md`, `README.md`, `docs/guides/RIGGING.md`. diff --git a/docs/phase9_task3_coords_spec.md b/docs/specs/phase9_task3_coords_spec.md similarity index 91% rename from docs/phase9_task3_coords_spec.md rename to docs/specs/phase9_task3_coords_spec.md index 3e0954b..8758e3b 100644 --- a/docs/phase9_task3_coords_spec.md +++ b/docs/specs/phase9_task3_coords_spec.md @@ -2,7 +2,7 @@ ## Overview -RIGGING.md Task 3: the test harness needs a show/hide readout on the right side of the screen +docs/guides/RIGGING.md Task 3: the test harness needs a show/hide readout on the right side of the screen displaying the current skeleton pose so bone/IK work can be inspected numerically: 1. **Skeleton2D** position and rotation. @@ -28,7 +28,7 @@ Values are read live each frame in `_process(delta)`, so the panel always mirror pose. All values are **world-space** (`global_position` / `global_rotation`). Rotation is reported in degrees (`rad_to_deg`); positions are `Vector2` formatted to 1 decimal place. -## 2. Implementation — `scripts/test_harness.gd` +## 2. Implementation — `scripts/tools/test_harness.gd` ### 2a. Constants @@ -105,10 +105,10 @@ calls `_update_coords_display()` each frame. `_update_coords_display()`: | File | Changes | |---|---| -| `scripts/test_harness.gd` | `COORDS_PANEL_WIDTH`, `COORD_BONE_PATHS`, `_coords_panel`, `_coords_label`, `_coord_bones`, `_show_coords`, `_build_coords_panel()`, `_on_show_coords_toggled()`, `_process()`, `_update_coords_display()`, `_resolve_coord_bones()`, `_fmt_vec2()`, `_fmt_deg()`; "Show Coords" checkbox. | -| `docs/phase9_task3_coords_spec.md` | This file. | +| `scripts/tools/test_harness.gd` | `COORDS_PANEL_WIDTH`, `COORD_BONE_PATHS`, `_coords_panel`, `_coords_label`, `_coord_bones`, `_show_coords`, `_build_coords_panel()`, `_on_show_coords_toggled()`, `_process()`, `_update_coords_display()`, `_resolve_coord_bones()`, `_fmt_vec2()`, `_fmt_deg()`; "Show Coords" checkbox. | +| `docs/specs/phase9_task3_coords_spec.md` | This file. | | `AGENTS.md` | Test-harness section: "Phase 9 Task 3 coordinates display" bullet; `test_harness.tscn` scene description now lists the "Show Coords" toggle. | -| `RIGGING.md` | Mark Task 3 implemented. | +| `docs/guides/RIGGING.md` | Mark Task 3 implemented. | ## 4. Edge cases @@ -139,7 +139,7 @@ calls `_update_coords_display()` each frame. `_update_coords_display()`: (user-requested; no errors). 2. Manual F6 check: - Launch harness; readout visible on the top-right with `"No rig loaded"`. - - Load `stickmen/basic.stk`; Skeleton2D, 10 Bones, and 6 IK Target rows populate with + - Load `content/stickmen/basic.stk`; Skeleton2D, 10 Bones, and 6 IK Target rows populate with numeric values. - Drag an IK handle → position/rotation values update live each frame. - Uncheck "Show Coords" → panel hides; recheck → reappears with current values. diff --git a/docs/phase9_task4_refactor_spec.md b/docs/specs/phase9_task4_refactor_spec.md similarity index 92% rename from docs/phase9_task4_refactor_spec.md rename to docs/specs/phase9_task4_refactor_spec.md index da19ae4..1d02e05 100644 --- a/docs/phase9_task4_refactor_spec.md +++ b/docs/specs/phase9_task4_refactor_spec.md @@ -2,7 +2,7 @@ ## Overview -RIGGING.md Task 4: facing direction and per-joint bone bend must become **part of +docs/guides/RIGGING.md Task 4: facing direction and per-joint bone bend must become **part of `master_rig.tscn`**, not the test harness. A rigged stickman instance should carry its own facing profile and bend flags so different instances can face different directions / bend differently at the same time. These must be **exported controls** on the rig so they are easy to access from the @@ -21,7 +21,7 @@ Today the harness owns all of this: Task 4 moves that state + logic into a new script attached to the `master_rig.tscn` root, and the harness becomes a thin driver that reads/writes the rig's exported properties. -## 1. New rig script — `scripts/stickman_rig.gd` +## 1. New rig script — `scripts/characters/stickman/stickman_rig.gd` A new `class_name StickmanRig` script, `extends Node2D`, attached to the `Master` root node of `master_rig.tscn` (the root currently has **no** script). It is the single owner of facing profile @@ -238,7 +238,7 @@ Matches repo style throughout (`get_node_or_null`, `push_warning`, skip): ## 2. `master_rig.tscn` changes 1. **Attach the script** to the root `Master` node: `script = ExtResource("…")` for - `res://scripts/stickman_rig.gd`. The exported defaults get serialized (`facing_profile = 2`, + `res://scripts/characters/stickman/stickman_rig.gd`. The exported defaults get serialized (`facing_profile = 2`, `left_arm_bend = 0`, `right_arm_bend = 1`, `left_leg_bend = 1`, `right_leg_bend = 0`). 2. **Align authored state to the FORWARD default** (Q1 **resolved: yes**). So the @@ -259,7 +259,7 @@ Matches repo style throughout (`get_node_or_null`, `push_warning`, skip): FORWARD on `_ready`), so this only affects the as-authored appearance and makes the "default leaves the scene as-authored" verification true. -## 3. Harness changes — `scripts/test_harness.gd` +## 3. Harness changes — `scripts/tools/test_harness.gd` The harness stops owning bend/facing state and drives the rig script. It keeps a thin UI mirror for menu labels + re-application across respawns (the rig remains the authority over the actual flags). @@ -355,10 +355,10 @@ menu still refreshes on `about_to_popup` as the primary mechanism.) the tree), so mounted shape children are already present when the rig reorders `Body/*` — moving a part node moves its whole shape group, exactly as today. -## 5. `master_rig_builder.gd`, `master_rig2.tscn` +## 5. `master_rig_builder.gd`, `scenes/legacy/master_rig2.tscn` -Out of scope and untouched (per `docs/phase9_round1_bugfix_spec.md`): `master_rig_builder.gd` builds -a **different** rig (`Sticky/Stickman/.../Hip` naming), and `master_rig2.tscn` is a pose-override +Out of scope and untouched (per `docs/specs/phase9_round1_bugfix_spec.md`): `master_rig_builder.gd` builds +a **different** rig (`Sticky/Stickman/.../Hip` naming), and `scenes/legacy/master_rig2.tscn` is a pose-override variant not referenced by the factory. The new `StickmanRig` script targets `master_rig.tscn` only. ## 6. Defaults & backward compatibility @@ -381,14 +381,14 @@ variant not referenced by the factory. The new `StickmanRig` script targets `mas | File | Changes | |---|---| -| `scripts/stickman_rig.gd` | **New.** `class_name StickmanRig extends Node2D` — enums, constants, exported properties, signals, methods, runtime resolution/apply. | +| `scripts/characters/stickman/stickman_rig.gd` | **New.** `class_name StickmanRig extends Node2D` — enums, constants, exported properties, signals, methods, runtime resolution/apply. | | `master_rig.tscn` | Attach `StickmanRig` to root `Master`; (recommended) align authored TwoBoneIK flags + `Body/*` order to FORWARD (§2). | -| `scripts/test_harness.gd` | Delete bend/facing ownership (§3a); add `_rig_script` + repurposed `_facing_profile`; rewire spawn, facing menu, right-click toggle (§3c–3f). | -| `scripts/stickman_factory.gd` | Narrow `spawn_from_data`/`spawn` return type to `StickmanRig` (Q6). | -| `docs/phase9_task4_refactor_spec.md` | This file. | -| `AGENTS.md` | Add `scripts/stickman_rig.gd` bullet; update `test_harness.gd` bullet (facing/bend now drive the rig); note `master_rig.tscn` root script. | +| `scripts/tools/test_harness.gd` | Delete bend/facing ownership (§3a); add `_rig_script` + repurposed `_facing_profile`; rewire spawn, facing menu, right-click toggle (§3c–3f). | +| `scripts/characters/stickman/stickman_factory.gd` | Narrow `spawn_from_data`/`spawn` return type to `StickmanRig` (Q6). | +| `docs/specs/phase9_task4_refactor_spec.md` | This file. | +| `AGENTS.md` | Add `scripts/characters/stickman/stickman_rig.gd` bullet; update `test_harness.gd` bullet (facing/bend now drive the rig); note `master_rig.tscn` root script. | | `README.md` | Project-structure table row for `stickman_rig.gd`; update test-harness + factory bullets. | -| `RIGGING.md` | Mark Task 4 implemented. | +| `docs/guides/RIGGING.md` | Mark Task 4 implemented. | ## 8. Edge cases @@ -411,7 +411,7 @@ variant not referenced by the factory. The new `StickmanRig` script targets `mas |---|---|---| | D1 | New `class_name StickmanRig extends Node2D` on the `master_rig.tscn` root | The rig is the natural owner of per-instance facing/bend; a `class_name` script makes it a typed, reusable API for the harness and future consumers. | | D2 | Enums live in the rig script (`StickmanRig.FacingProfile` / `StickmanRig.BendDirection`) | No autoload/singleton; idiomatic `class_name` enum access; single source of truth both sides can reference. | -| D3 | `facing_profile` is a preset; the four `*_bend` enum exports are the per-joint source of truth | Matches the harness's existing preset-then-override behavior (RIGGING.md: "user is free to change bend manually"); the enum dropdowns give readable "Normal"/"Inverted" inspector labels (the task's "per-joint bend enums" hint). | +| D3 | `facing_profile` is a preset; the four `*_bend` enum exports are the per-joint source of truth | Matches the harness's existing preset-then-override behavior (docs/guides/RIGGING.md: "user is free to change bend manually"); the enum dropdowns give readable "Normal"/"Inverted" inspector labels (the task's "per-joint bend enums" hint). | | D4 | Public bool-form methods (`set_joint_bend_flipped`/`get_joint_bend_flipped`) alongside the enum exports | `flip_bend_direction` is a bool; the bool form is the engine-accurate contract and keeps the harness context-menu label logic unchanged. | | D5 | TwoBoneIK resolved by `joint_two_bone2d_node` NodePath matching, not stack index | Preserves the existing (Task 1) resolution approach; robust to stack reordering. | | D6 | Non-`@tool` script; apply only at runtime | IK doesn't solve in-editor; mutating `Body/*` order / mod flags in-editor would dirty the scene; runtime-only is the task's permitted minimum. | @@ -455,7 +455,7 @@ head-aiming while facing LEFT is intentionally static. 1. **Parse check** (same as prior tasks): `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit` — no errors. 2. **Headless SceneTree verification** (temporary script, deleted after; pattern from Round 7): - - `StickmanFactory.spawn("res://stickmen/basic.stk")` → cast root to `StickmanRig` (non-null); + - `StickmanFactory.spawn("res://content/stickmen/basic.stk")` → cast root to `StickmanRig` (non-null); `add_child` it; assert `get_facing_profile() == FacingProfile.FORWARD` and the 4 mods' flags equal the FORWARD set; assert `Body` child order == `Z_ORDER_BY_PROFILE[FORWARD]` and `Body/Head` is last. @@ -468,7 +468,7 @@ head-aiming while facing LEFT is intentionally static. 3. **Harness code review**: facing menu ids use `StickmanRig.FacingProfile`; spawn connects signals before `add_child` and re-applies `_facing_profile`; right-click toggle reads/writes the rig script, not a mod directly; `_resolve_rig_nodes()` no longer touches bend/z-order state. -4. **Manual F6** (`res://scenes/test_harness.tscn`): +4. **Manual F6** (`res://scenes/tools/test_harness.tscn`): - Load `basic.stk`; figure starts FORWARD (unchanged from before). - Facing menu → Left/Right: limbs tuck behind the torso correctly, head stays frontmost; `[√] ` moves; context menu labels reflect the new per-joint flags. @@ -479,12 +479,12 @@ head-aiming while facing LEFT is intentionally static. ## 11. Implementation order -1. `scripts/stickman_rig.gd` — enums, constants, exports, signals, methods, runtime apply. +1. `scripts/characters/stickman/stickman_rig.gd` — enums, constants, exports, signals, methods, runtime apply. 2. `master_rig.tscn` — attach script; align authored flags + `Body/*` order to FORWARD (Q1). -3. `scripts/test_harness.gd` — remove ownership, add `_rig_script`, rewire spawn/menus/toggle. -4. `scripts/stickman_factory.gd` — (optional) narrow return type. +3. `scripts/tools/test_harness.gd` — remove ownership, add `_rig_script`, rewire spawn/menus/toggle. +4. `scripts/characters/stickman/stickman_factory.gd` — (optional) narrow return type. 5. Parse check + headless verification (temp, then removed). -6. Docs: `AGENTS.md`, `README.md`, `RIGGING.md`, this spec. +6. Docs: `AGENTS.md`, `README.md`, `docs/guides/RIGGING.md`, this spec. ## 12. Open questions — RESOLVED (user approval) diff --git a/docs/phase9_task5_animation_spec.md b/docs/specs/phase9_task5_animation_spec.md similarity index 88% rename from docs/phase9_task5_animation_spec.md rename to docs/specs/phase9_task5_animation_spec.md index a0f7982..bd6d637 100644 --- a/docs/phase9_task5_animation_spec.md +++ b/docs/specs/phase9_task5_animation_spec.md @@ -2,20 +2,20 @@ ## Overview -RIGGING.md Task 5: `master_rig.tscn` already ships an `AnimationPlayer` and an `AnimationTree`. +docs/guides/RIGGING.md Task 5: `master_rig.tscn` already ships an `AnimationPlayer` and an `AnimationTree`. The test harness must let the user **select** an animation from a dropdown, **play / pause / resume / stop** it, and **toggle loop vs. play-once**. Concretely: 1. A **dropdown** listing the rig's animations (from `AnimationPlayer.get_animation_list()`). 2. A **play/pause/resume** button (single button whose label reflects playback state) plus a - **Stop** button. Per RIGGING.md, **Play always restarts from the beginning**; **Stop resets to + **Stop** button. Per docs/guides/RIGGING.md, **Play always restarts from the beginning**; **Stop resets to the start** so the next Play restarts. 3. A **Loop** checkbox that makes the selected animation loop or play just once. This is a **harness-only** change. `master_rig.tscn` is **not** modified (the two animation nodes -already exist and are sufficient). `scripts/stickman_rig.gd` is **not** modified (the harness +already exist and are sufficient). `scripts/characters/stickman/stickman_rig.gd` is **not** modified (the harness resolves the `AnimationPlayer` directly by node path, exactly as it already resolves `Skeleton2D` -and the IK handles). Only `scripts/test_harness.gd` changes. +and the IK handles). Only `scripts/tools/test_harness.gd` changes. ## 1. Findings — `master_rig.tscn` animation nodes @@ -32,7 +32,7 @@ rig root is the `Master` node (`_rig`, a `StickmanRig`), and `AnimationPlayer` i ### 1b. Animation library — two animations (not one) -The `AnimationLibrary_t75yq._data` dictionary holds **two** animations (RIGGING.md says "currently +The `AnimationLibrary_t75yq._data` dictionary holds **two** animations (docs/guides/RIGGING.md says "currently just 'walk_right'", but there is also a pose-reset helper): | Name | Length | `loop_mode` | Tracks | @@ -114,7 +114,7 @@ before the "Open .stk…" button** (keeps the two rig-behavior control clusters Controls are **always enabled** (matching the harness's existing "Facing" menu / checkbox style); each handler no-op-guards on a missing `AnimationPlayer` instead of disabling the control. -## 4. Implementation — `scripts/test_harness.gd` +## 4. Implementation — `scripts/tools/test_harness.gd` ### 4a. Constants @@ -122,7 +122,7 @@ each handler no-op-guards on a missing `AnimationPlayer` instead of disabling th ## AnimationPlayer node path (relative to rig root). const ANIMATION_PLAYER_PATH := "AnimationPlayer" -## Initially-selected animation in the dropdown (RIGGING.md default). +## Initially-selected animation in the dropdown (docs/guides/RIGGING.md default). const DEFAULT_ANIMATION := "walk_right" ``` @@ -314,14 +314,14 @@ func _update_play_button() -> void: | File | Changes | |---|---| -| `scripts/test_harness.gd` | `ANIMATION_PLAYER_PATH`, `DEFAULT_ANIMATION`, `PlaybackState`, `_anim_dropdown`/`_play_button`/`_stop_button`/`_loop_check`, `_anim_player`/`_selected_animation`/`_playback_state`/`_loop`, UI block in `_build_ui()`, `_resolve_anim_player()`, `_populate_animation_dropdown()`, `_on_anim_dropdown_selected()`, `_on_play_pressed()`, `_on_stop_pressed()`, `_on_loop_toggled()`, `_on_animation_finished()`, `_apply_loop_mode()`, `_update_play_button()`; lifecycle hooks in `_resolve_rig_nodes()` + `_free_current_rig()`. | -| `docs/phase9_task5_animation_spec.md` | This file. | +| `scripts/tools/test_harness.gd` | `ANIMATION_PLAYER_PATH`, `DEFAULT_ANIMATION`, `PlaybackState`, `_anim_dropdown`/`_play_button`/`_stop_button`/`_loop_check`, `_anim_player`/`_selected_animation`/`_playback_state`/`_loop`, UI block in `_build_ui()`, `_resolve_anim_player()`, `_populate_animation_dropdown()`, `_on_anim_dropdown_selected()`, `_on_play_pressed()`, `_on_stop_pressed()`, `_on_loop_toggled()`, `_on_animation_finished()`, `_apply_loop_mode()`, `_update_play_button()`; lifecycle hooks in `_resolve_rig_nodes()` + `_free_current_rig()`. | +| `docs/specs/phase9_task5_animation_spec.md` | This file. | | `AGENTS.md` | Test-harness section: "Phase 9 Task 5 rig animation" bullet (dropdown + play/pause/stop + loop, drives `AnimationPlayer` directly). | | `README.md` | Test-harness bullet: animation select/play/pause/stop/loop controls. | -| `RIGGING.md` | Mark Task 5 implemented. | +| `docs/guides/RIGGING.md` | Mark Task 5 implemented. | -**Not modified:** `master_rig.tscn`, `scripts/stickman_rig.gd`, `scripts/stickman_factory.gd`, -`scripts/stk_rig_adapter.gd`. +**Not modified:** `master_rig.tscn`, `scripts/characters/stickman/stickman_rig.gd`, `scripts/characters/stickman/stickman_factory.gd`, +`scripts/characters/stickman/stk_rig_adapter.gd`. ## 6. Edge cases @@ -350,18 +350,18 @@ func _update_play_button() -> void: | # | Decision | Justification | |---|---|---| | D1 | Drive `AnimationPlayer` directly; `AnimationTree` out of scope | `AnimationTree` is an unconfigured placeholder (`active = false`, empty state machine). Select/play/pause/stop/loop are all first-class `AnimationPlayer` APIs; wiring a blend tree would require editing `master_rig.tscn` for no gain here. | -| D2 | Single play/pause/resume button + separate Stop button | Matches RIGGING.md "pause/resume" (toggling) vs "stop (restart on play)" as distinct states; the label swap is the harness's existing dynamic-text pattern (cf. the editor's snap/guide menus). | +| D2 | Single play/pause/resume button + separate Stop button | Matches docs/guides/RIGGING.md "pause/resume" (toggling) vs "stop (restart on play)" as distinct states; the label swap is the harness's existing dynamic-text pattern (cf. the editor's snap/guide menus). | | D3 | Track playback state via `_playback_state` + `animation_finished`, not `is_playing()` polling | The harness is the sole driver, so state is deterministic. `animation_finished` reliably fires for non-looping end-of-play and never fires on `pause()`/`stop()`; the `_loop` guard makes the looping-wrap ambiguity moot. Avoids adding per-frame polling to `_process`. | | D4 | Loop = write `Animation.loop_mode` on the selected `Animation` before playing | Loop is an `Animation`-resource property, not a `play()` argument; this is the only way to override the authored value. Re-applied on every play so the harness `_loop` toggle is authoritative regardless of authored `loop_mode`. | | D5 | Loop default **ON** | Matches the authored `walk_right` (`loop_mode = 1`), and a walk cycle is the natural looping case. | | D6 | Dropdown populated dynamically per spawn from `get_animation_list()` | The list comes from the rig's own library, so future animations appear automatically; no hardcoded list. | -| D7 | Prefer `walk_right` as initial selection (`DEFAULT_ANIMATION`) | Matches RIGGING.md's "currently just 'walk_right'" default even though the library also contains `RESET`. | +| D7 | Prefer `walk_right` as initial selection (`DEFAULT_ANIMATION`) | Matches docs/guides/RIGGING.md's "currently just 'walk_right'" default even though the library also contains `RESET`. | | D8 | No `stickman_rig.gd` change | The harness already resolves rig children by node path (`SKELETON_PATH`, `IK_HANDLE_PATHS`); `ANIMATION_PLAYER_PATH` follows that established pattern. A rig-level accessor is unnecessary. | | D9 | Controls never disabled; handlers no-op-guard | Matches the existing harness style (the "Facing" menu and checkboxes are always enabled). | ## 8. Verification -Only `scripts/test_harness.gd` changes, so the syntax checks target that script. +Only `scripts/tools/test_harness.gd` changes, so the syntax checks target that script. 1. **Whole-project parse check** (established form used by every prior phase — the plain `--check-only` form hangs on renderer init in 4.7.x, so use the `--headless --check-only --quit` @@ -374,11 +374,11 @@ Only `scripts/test_harness.gd` changes, so the syntax checks target that script. 2. **Single-script check** (user-specified form; run from anywhere): ``` - & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\Godot4\stickman" --check-only --script "res://scripts/test_harness.gd" + & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\Godot4\stickman" --check-only --script "res://scripts/tools/test_harness.gd" ``` -3. **Manual F6 check** (`res://scenes/test_harness.tscn`): - - Load `stickmen/basic.stk` → dropdown lists `RESET` and `walk_right`, `walk_right` selected, +3. **Manual F6 check** (`res://scenes/tools/test_harness.tscn`): + - Load `content/stickmen/basic.stk` → dropdown lists `RESET` and `walk_right`, `walk_right` selected, play button shows `"Play"`, Loop checked. - Press **Play** → button `"Pause"`; the figure walks (IK targets animate, legs/arms swing, facing menu flips to `[√] Right`). Coordinates readout updates live. @@ -392,7 +392,7 @@ Only `scripts/test_harness.gd` changes, so the syntax checks target that script. ## 9. Implementation order -1. `scripts/test_harness.gd` — constants, enum, node refs, state, `_build_ui()` block, handlers, +1. `scripts/tools/test_harness.gd` — constants, enum, node refs, state, `_build_ui()` block, handlers, helpers, lifecycle hooks. 2. Parse checks (§8 items 1–2) + manual F6 (§8 item 3). -3. Docs: `AGENTS.md`, `README.md`, `RIGGING.md`, this spec. +3. Docs: `AGENTS.md`, `README.md`, `docs/guides/RIGGING.md`, this spec. diff --git a/specs/phase_10_prop_integration.md b/docs/specs/phase_10_prop_integration.md similarity index 93% rename from specs/phase_10_prop_integration.md rename to docs/specs/phase_10_prop_integration.md index ebecf75..7188268 100644 --- a/specs/phase_10_prop_integration.md +++ b/docs/specs/phase_10_prop_integration.md @@ -1,6 +1,6 @@ # Phase 10 — Custom Prop Library and Sandbox Integration -**Status:** Approved for implementation (2026-09-21). Implements the approved choices from `plans/PHASE_10_PROP_INTEGRATION.md`; this document specifies work and does not itself change runtime behavior. +**Status:** Approved for implementation (2026-09-21). Implements the approved choices from `docs/archive/plans/PHASE_10_PROP_INTEGRATION.md`; this document specifies work and does not itself change runtime behavior. ## 1. Scope and existing seams @@ -13,7 +13,7 @@ Runtime attachment, grip/emitter roles, prop editing inside the Sandbox, and `.p ## 2. Architecture and contracts ```text -res://props/*.prop ──scan──> PropLibrary entry ──id/path──> AssetSelector +res://content/props/*.prop ──scan──> PropLibrary entry ──id/path──> AssetSelector │ │ ├── parsed shapes ──> thumbnail queue ──> PropThumbnail │ └── ThumbnailCache @@ -53,7 +53,7 @@ The canonical custom ID is the normalized source path, including its scheme wher ### 2.2 Shared visual and collision conversion -Add `scripts/prop_visuals.gd` (`class_name PropVisuals extends RefCounted`) as a stateless helper used by both runtime and thumbnail rendering: +Add `scripts/props/prop_visuals.gd` (`class_name PropVisuals extends RefCounted`) as a stateless helper used by both runtime and thumbnail rendering: ```gdscript static func build(parent: Node2D, shapes: Array[Dictionary]) -> Rect2 @@ -101,7 +101,7 @@ For custom props, changing `material_preset` applies physics properties but **do ### 2.5 Scene reload and missing custom assets -Add `scripts/missing_prop_placeholder.gd` (`class_name MissingPropPlaceholder extends Node2D`) with typed `original_entry: Dictionary`, `missing_prop_path: String`, `configure(entry: Dictionary) -> void`, and `get_area_rect() -> Rect2`. It displays an inert missing-asset marker and retains a deep copy of the original prop scene entry. The loader creates it **only for a missing/invalid path-valued custom prop ID**; unknown legacy template IDs continue to warn and skip. It applies the saved transform and scale and adds the placeholder to the World in the original object order. +Add `scripts/props/missing_prop_placeholder.gd` (`class_name MissingPropPlaceholder extends Node2D`) with typed `original_entry: Dictionary`, `missing_prop_path: String`, `configure(entry: Dictionary) -> void`, and `get_area_rect() -> Rect2`. It displays an inert missing-asset marker and retains a deep copy of the original prop scene entry. The loader creates it **only for a missing/invalid path-valued custom prop ID**; unknown legacy template IDs continue to warn and skip. It applies the saved transform and scale and adds the placeholder to the World in the original object order. The placeholder is intentionally entered into `file_to_runtime` as an available object ID, so `StageSceneIO.decode_rules()` maps existing rule references to its instance ID. It does **not** belong in the `unavailable` set that currently makes rules disappear for missing stickmen. Its `Node2D` type prevents `PropBlock` physics/contact handling; stage trigger/action execution must treat it as inert. `_collect_scene_data()` recognizes `MissingPropPlaceholder` as a `prop`, begins from `original_entry.duplicate(true)`, updates the current file ID and transform/scale, and serializes its original `asset_id` and material. The normal runtime-to-file map includes its ID so `StageSceneIO.encode_rules()` writes the preserved references. Deleting a placeholder follows normal object/rule cleanup. Reopening the scene retries the path and replaces it with a real prop when available. @@ -140,7 +140,7 @@ Add a focused `tests/test_phase10_prop_integration.gd` headless suite and update 5. **Scene persistence:** save/reload retains a custom prop's path ID, material, position, rotation, scale, and full visuals. With its source temporarily absent, load creates a placeholder carrying the original entry; save/reload preserves that entry and a rule referring to it. Restoring the file and reopening resolves a real prop and the rule reference. Existing built-in prop and missing-stickman behavior stays covered by `tests/test_phase5_file_features.gd`. 6. **Lifecycle:** in EDIT/DIRECT the custom `RigidBody2D` is frozen; in PLAY it simulates and reports contacts; after PLAY→EDIT and PLAY→DIRECT it returns to the authored transform with zero velocities. Use a physics-frame runtime test and a brief interactive Sandbox check for full-layer artwork, collision, selection, Browse, and reload. -Run the affected headless suites plus Godot 4.7.1 `--headless --check-only` on changed scripts. Test thumbnail pixels interactively because headless `SubViewport` capture can be blank. The target scene remains `res://scenes/sandbox_stage.tscn`; no new UI scene is required beyond the existing selector's code-controlled visibility/text. +Run the affected headless suites plus Godot 4.7.1 `--headless --check-only` on changed scripts. Test thumbnail pixels interactively because headless `SubViewport` capture can be blank. The target scene remains `res://scenes/stage/sandbox_stage.tscn`; no new UI scene is required beyond the existing selector's code-controlled visibility/text. ## 5. Architectural trade-offs diff --git a/specs/phase_10a_prop_scaling.md b/docs/specs/phase_10a_prop_scaling.md similarity index 89% rename from specs/phase_10a_prop_scaling.md rename to docs/specs/phase_10a_prop_scaling.md index ad8b7f9..e5203fc 100644 --- a/specs/phase_10a_prop_scaling.md +++ b/docs/specs/phase_10a_prop_scaling.md @@ -2,7 +2,7 @@ Status: **implemented and user verified** (amended and verified 2026-09-22). -Source: `plans/PHASE_10a_PROP_SCALING.md`. Phase 10a adds two distinct operations: +Source: `docs/archive/plans/PHASE_10a_PROP_SCALING.md`. Phase 10a adds two distinct operations: permanent size changes to a `.prop` document in the Prop Editor, and reversible uniform instance scaling of one `PropBlock` in Sandbox EDIT mode. The final UI uses origin-based Normalize Size and direct whole-prop Arrange gestures; it does not expose @@ -41,12 +41,12 @@ or uniformly resizes the complete document. It does not change Sandbox scaling. | Class / file | Required contract | | --- | --- | -| `class_name PropCanvas extends Control` (`scripts/prop_canvas.gd`) | Keep `can_scale_all_shapes()` / `scale_all_shapes()`, and add equivalent validated document helpers `can_translate_all_shapes(delta: Vector2) -> bool`, `translate_all_shapes(delta: Vector2) -> bool`, `can_rotate_all_shapes(angle_rad: float) -> bool`, and `rotate_all_shapes(angle_rad: float) -> bool`. Successful commit helpers mutate every shape and emit `shape_changed(shapes: Array[Dictionary])` exactly once; preflight/no-op/failure never mutates or emits. Add the transient whole-prop tool API in §3.4. `get_shapes_bounds() -> Rect2` remains authoritative for its union gizmo. | -| `class_name PropAnchorData extends RefCounted` (`scripts/prop_anchor_data.gd`) | Keep pure `try_scale_non_origin()`, and add pure `try_translate_non_origin(anchors: Dictionary, delta: Vector2) -> Dictionary` and `try_rotate_non_origin(anchors: Dictionary, angle_rad: float) -> Dictionary`, each returning `{ "ok": bool, "anchors": Dictionary }`. Translation changes editable positions only. Rotation rotates editable positions about `(0,0)` and adds the angle to their wrapped `rotation_deg`. Every helper regenerates canonical `origin` unchanged and rejects invalid results without partially modifying input. Existing `translated_non_origin()` may delegate to the validated helper. | -| `class_name PropEditor extends Control` (`scripts/prop_editor.gd`) | Own three atomic document transactions: `_translate_document(delta: Vector2) -> bool`, `_rotate_document(angle_rad: float) -> bool`, and existing `_scale_document(factor: float) -> bool`. Each prevalidates shapes and anchors, applies the anchor copy, commits canvas geometry once, rolls anchors back on an unexpected canvas failure, and refreshes inspector/preview/buttons through the existing shape-change path. Connect the typed request signals in §3.4. Persist only guide visibility in editor settings. | -| `class_name StageGizmos extends Node2D` (`scripts/stage_gizmos.gd`) | Extend `enum Handle` with `SCALE`. Draw and hit-test four corner crosses only when `_targets.size() == 1` and `_targets[0] is PropBlock`; keep hit radii and strokes in screen pixels via camera zoom. Capture start `scale: Vector2`, world origin, selected corner, and cursor at gesture start. Use one positive uniform multiplier through the drag; emit the existing transform signals. | -| `class_name SandboxStage extends Node2D` (`scripts/sandbox_stage.gd`) | Keep the current EDIT pointer routing and existing transform-signal handlers. Route scale gestures through the same physics-sync and authored-state commit path as move/rotate. No save/load or PLAY→EDIT reset rewrite is needed unless verification exposes a defect. | -| `class_name StageSelection extends RefCounted` (`scripts/stage_selection.gd`) | Its existing `get_world_aabb(node: Node2D) -> Rect2` transforms visual points to world space. Verify scaled/rotated built-in and custom props; change the method only if a specific failing case appears. | +| `class_name PropCanvas extends Control` (`scripts/editors/prop/prop_canvas.gd`) | Keep `can_scale_all_shapes()` / `scale_all_shapes()`, and add equivalent validated document helpers `can_translate_all_shapes(delta: Vector2) -> bool`, `translate_all_shapes(delta: Vector2) -> bool`, `can_rotate_all_shapes(angle_rad: float) -> bool`, and `rotate_all_shapes(angle_rad: float) -> bool`. Successful commit helpers mutate every shape and emit `shape_changed(shapes: Array[Dictionary])` exactly once; preflight/no-op/failure never mutates or emits. Add the transient whole-prop tool API in §3.4. `get_shapes_bounds() -> Rect2` remains authoritative for its union gizmo. | +| `class_name PropAnchorData extends RefCounted` (`scripts/props/prop_anchor_data.gd`) | Keep pure `try_scale_non_origin()`, and add pure `try_translate_non_origin(anchors: Dictionary, delta: Vector2) -> Dictionary` and `try_rotate_non_origin(anchors: Dictionary, angle_rad: float) -> Dictionary`, each returning `{ "ok": bool, "anchors": Dictionary }`. Translation changes editable positions only. Rotation rotates editable positions about `(0,0)` and adds the angle to their wrapped `rotation_deg`. Every helper regenerates canonical `origin` unchanged and rejects invalid results without partially modifying input. Existing `translated_non_origin()` may delegate to the validated helper. | +| `class_name PropEditor extends Control` (`scripts/editors/prop/prop_editor.gd`) | Own three atomic document transactions: `_translate_document(delta: Vector2) -> bool`, `_rotate_document(angle_rad: float) -> bool`, and existing `_scale_document(factor: float) -> bool`. Each prevalidates shapes and anchors, applies the anchor copy, commits canvas geometry once, rolls anchors back on an unexpected canvas failure, and refreshes inspector/preview/buttons through the existing shape-change path. Connect the typed request signals in §3.4. Persist only guide visibility in editor settings. | +| `class_name StageGizmos extends Node2D` (`scripts/stage/stage_gizmos.gd`) | Extend `enum Handle` with `SCALE`. Draw and hit-test four corner crosses only when `_targets.size() == 1` and `_targets[0] is PropBlock`; keep hit radii and strokes in screen pixels via camera zoom. Capture start `scale: Vector2`, world origin, selected corner, and cursor at gesture start. Use one positive uniform multiplier through the drag; emit the existing transform signals. | +| `class_name SandboxStage extends Node2D` (`scripts/editors/stage/sandbox_stage.gd`) | Keep the current EDIT pointer routing and existing transform-signal handlers. Route scale gestures through the same physics-sync and authored-state commit path as move/rotate. No save/load or PLAY→EDIT reset rewrite is needed unless verification exposes a defect. | +| `class_name StageSelection extends RefCounted` (`scripts/stage/stage_selection.gd`) | Its existing `get_world_aabb(node: Node2D) -> Rect2` transforms visual points to world space. Verify scaled/rotated built-in and custom props; change the method only if a specific failing case appears. | No new persistent shape fields are required. The whole-prop tool adds three typed commit-request signals. Use static type hints for new fields, parameters, @@ -410,7 +410,7 @@ Example persisted values remain additive to existing formats: ``` ```json -{"type":"prop","file_id":"prop_1","asset_id":"res://props/ball.prop","position":{"x":100,"y":200},"rotation":0,"scale":{"x":1.5,"y":1.5},"material_preset":2} +{"type":"prop","file_id":"prop_1","asset_id":"res://content/props/ball.prop","position":{"x":100,"y":200},"rotation":0,"scale":{"x":1.5,"y":1.5},"material_preset":2} ``` The JSON fragments show only the fields relevant to scaling; ordinary diff --git a/specs/phase_11_prop_interactions.md b/docs/specs/phase_11_prop_interactions.md similarity index 99% rename from specs/phase_11_prop_interactions.md rename to docs/specs/phase_11_prop_interactions.md index 235a735..348e8e7 100644 --- a/specs/phase_11_prop_interactions.md +++ b/docs/specs/phase_11_prop_interactions.md @@ -8,7 +8,7 @@ Throw landing-target amendment: **Approved for implementation (2026-09-25).** Queue-aware throw-preview amendment: **Approved for implementation (2026-09-25).** -Source: `plans/PHASE_11_PROP_INTERACTIONS.md`. This spec keeps the requested +Source: `docs/archive/plans/PHASE_11_PROP_INTERACTIONS.md`. This spec keeps the requested pickup, drop, throw, ragdoll handoff, and “When…” features while adapting them to the current Sandbox Stage architecture and closing the design gaps identified during review. diff --git a/specs/phase_12_live_director.md b/docs/specs/phase_12_live_director.md similarity index 82% rename from specs/phase_12_live_director.md rename to docs/specs/phase_12_live_director.md index 47fb047..8ebfc8e 100644 --- a/specs/phase_12_live_director.md +++ b/docs/specs/phase_12_live_director.md @@ -1,6 +1,6 @@ # Phase 12 — Live Director -Status: **specification for user sign-off**. Source: `plans/PHASE_12_LIVE_DIRECTOR.md` and the Phase 1 clarification. Implementation begins only after this spec is approved. +Status: **specification for user sign-off**. Source: `docs/archive/plans/PHASE_12_LIVE_DIRECTOR.md` and the Phase 1 clarification. Implementation begins only after this spec is approved. ## 1. Scope and decisions @@ -36,14 +36,14 @@ SandboxStage (Node2D; mode, session clock, event rules) | Class | Required API and ownership | | --- | --- | -| `class_name SandboxStage extends Node2D` (`scripts/sandbox_stage.gd`) | Own `enum RunState { STOPPED, RUNNING, PAUSED }`, `_run_state: RunState`, `_elapsed_seconds: float`, `_prop_pause_states: Dictionary` keyed by instance ID, and a paused-edit autosave debounce. Keep `StageMode { EDIT, DIRECT, PLAY }` for UI. Add `_start_run() -> void`, `_pause_run() -> void`, `_resume_run() -> void`, `_reset_run() -> void`, `_set_timestamp(seconds: float) -> void`, `_capture_prop_pause_state(body: RigidBody2D) -> Dictionary`, and a silent event-edge baseline refresh. `set_mode(mode: StageMode) -> void` dispatches by current mode and run state. `_handle_event()` accepts events only in RUNNING after the resume baseline is ready. | -| `class_name StickmanRig extends Node2D` (`scripts/stickman_rig.gd`) | Facade: `pause_simulation() -> void`, `resume_simulation() -> void`, `reset_simulation_pause() -> void`, `get_director_anchor() -> Dictionary`, `get_queue_view() -> Array[Dictionary]`, and typed queue edit wrappers (`replace_action`, `move_action`, existing add/insert/remove/clear). Its physics tick returns while paused; it delegates physics/tween handling to components. Capture whether its locomotion `AnimationPlayer` was playing, pause it, and resume only playback that Phase 12 paused; preserve a prior airborne/landing pause. Route whole `action_queue` property assignment through the runner's validated setter. | -| `class_name StickmanRagdoll extends Node` (`scripts/stickman_ragdoll.gd`) | Own `pause_simulation() -> void`, `resume_simulation() -> void`, `clear_pause_snapshot() -> void`, `get_world_body_bounds() -> Rect2`, `get_body_rids() -> Array[RID]`, and typed access to its owned body nodes/container. Snapshot each existing body’s `global_transform`, `linear_velocity`, `angular_velocity`, `sleeping`, prior `freeze`, and prior `freeze_mode` by body instance ID. Pause `_stand_up_tween` when valid; resume it only if it was playing when paused. Gate `_process` pose correction while paused. Expose the live ragdoll bounds and ground-probe origin through the rig facade. | -| `class_name StickmanActionRunner extends Node` (`scripts/stickman_action_runner.gd`) | Own session-only stable authored action IDs, an independent completed/interrupted ledger, and transient reactive runtime entries. Add `set_authored_queue(actions: Array[Dictionary]) -> bool`, `replace_action(index: int, action: Dictionary) -> bool`, `move_action(from_index: int, to_index: int) -> bool`, `get_queue_view() -> Array[Dictionary]`, `pause_simulation() -> void`, `resume_simulation() -> void`, and `clear_pause_state() -> void`. Existing add/insert/remove/clear reconcile the live pending list when paused. Whole-queue replacement is accepted only while STOPPED for setup/load and rebuilds IDs; it is rejected in RUNNING and PAUSED. Reconciled queue APIs are the only run-session edit path. | -| `class_name StageSelection extends RefCounted` (`scripts/stage_selection.gd`) | `hit_test(world_pos: Vector2) -> Node2D` maps hits on ragdoll bone `CollisionShape2D` children to their `stickman_rig` metadata. Return the rig, never the body container or an individual limb. Exclude all ragdoll containers by ownership metadata/group, not an exact name; keep frontmost selection behavior for other objects. | -| `class_name StageDirectorVisuals extends Node2D` (`scripts/stage_director_visuals.gd`) | Add `set_live_session(paused: bool) -> void`. Draw queue views with completed/interrupted/active/pending statuses and start pending paths at `StickmanRig.get_director_anchor()`. `hit_test_waypoint_action()` returns `action_id`, current authored index, and status; consumed waypoints open delete-only controls. The same computed anchors feed both rendering and hit testing. | -| `class_name QueuePanel extends PopupPanel` (`scripts/queue_panel.gd`) | Render the rig's queue view and status. During a paused run, allow drag reorder only among PENDING actions; keep the ACTIVE action fixed. COMPLETED and INTERRUPTED actions cannot be edited or moved, but retain Delete. Delegate reorder through a single rig `move_action()` call, not remove+insert. Capture action IDs for delayed edits/deletes and emit the existing add/edit/delete/clear signals for stage routing. | -| `class_name WaypointContext extends PopupMenu` (`scripts/waypoint_context.gd`) | Accept the waypoint's queue status at popup time; disable Edit/Insert for COMPLETED or INTERRUPTED waypoints while retaining Delete and existing trigger-rule inspection. The stage stores the `action_id` and resolves its current index when a delayed menu choice or capture commits. | +| `class_name SandboxStage extends Node2D` (`scripts/editors/stage/sandbox_stage.gd`) | Own `enum RunState { STOPPED, RUNNING, PAUSED }`, `_run_state: RunState`, `_elapsed_seconds: float`, `_prop_pause_states: Dictionary` keyed by instance ID, and a paused-edit autosave debounce. Keep `StageMode { EDIT, DIRECT, PLAY }` for UI. Add `_start_run() -> void`, `_pause_run() -> void`, `_resume_run() -> void`, `_reset_run() -> void`, `_set_timestamp(seconds: float) -> void`, `_capture_prop_pause_state(body: RigidBody2D) -> Dictionary`, and a silent event-edge baseline refresh. `set_mode(mode: StageMode) -> void` dispatches by current mode and run state. `_handle_event()` accepts events only in RUNNING after the resume baseline is ready. | +| `class_name StickmanRig extends Node2D` (`scripts/characters/stickman/stickman_rig.gd`) | Facade: `pause_simulation() -> void`, `resume_simulation() -> void`, `reset_simulation_pause() -> void`, `get_director_anchor() -> Dictionary`, `get_queue_view() -> Array[Dictionary]`, and typed queue edit wrappers (`replace_action`, `move_action`, existing add/insert/remove/clear). Its physics tick returns while paused; it delegates physics/tween handling to components. Capture whether its locomotion `AnimationPlayer` was playing, pause it, and resume only playback that Phase 12 paused; preserve a prior airborne/landing pause. Route whole `action_queue` property assignment through the runner's validated setter. | +| `class_name StickmanRagdoll extends Node` (`scripts/characters/stickman/stickman_ragdoll.gd`) | Own `pause_simulation() -> void`, `resume_simulation() -> void`, `clear_pause_snapshot() -> void`, `get_world_body_bounds() -> Rect2`, `get_body_rids() -> Array[RID]`, and typed access to its owned body nodes/container. Snapshot each existing body’s `global_transform`, `linear_velocity`, `angular_velocity`, `sleeping`, prior `freeze`, and prior `freeze_mode` by body instance ID. Pause `_stand_up_tween` when valid; resume it only if it was playing when paused. Gate `_process` pose correction while paused. Expose the live ragdoll bounds and ground-probe origin through the rig facade. | +| `class_name StickmanActionRunner extends Node` (`scripts/characters/stickman/stickman_action_runner.gd`) | Own session-only stable authored action IDs, an independent completed/interrupted ledger, and transient reactive runtime entries. Add `set_authored_queue(actions: Array[Dictionary]) -> bool`, `replace_action(index: int, action: Dictionary) -> bool`, `move_action(from_index: int, to_index: int) -> bool`, `get_queue_view() -> Array[Dictionary]`, `pause_simulation() -> void`, `resume_simulation() -> void`, and `clear_pause_state() -> void`. Existing add/insert/remove/clear reconcile the live pending list when paused. Whole-queue replacement is accepted only while STOPPED for setup/load and rebuilds IDs; it is rejected in RUNNING and PAUSED. Reconciled queue APIs are the only run-session edit path. | +| `class_name StageSelection extends RefCounted` (`scripts/stage/stage_selection.gd`) | `hit_test(world_pos: Vector2) -> Node2D` maps hits on ragdoll bone `CollisionShape2D` children to their `stickman_rig` metadata. Return the rig, never the body container or an individual limb. Exclude all ragdoll containers by ownership metadata/group, not an exact name; keep frontmost selection behavior for other objects. | +| `class_name StageDirectorVisuals extends Node2D` (`scripts/stage/stage_director_visuals.gd`) | Add `set_live_session(paused: bool) -> void`. Draw queue views with completed/interrupted/active/pending statuses and start pending paths at `StickmanRig.get_director_anchor()`. `hit_test_waypoint_action()` returns `action_id`, current authored index, and status; consumed waypoints open delete-only controls. The same computed anchors feed both rendering and hit testing. | +| `class_name QueuePanel extends PopupPanel` (`scripts/editors/stage/queue_panel.gd`) | Render the rig's queue view and status. During a paused run, allow drag reorder only among PENDING actions; keep the ACTIVE action fixed. COMPLETED and INTERRUPTED actions cannot be edited or moved, but retain Delete. Delegate reorder through a single rig `move_action()` call, not remove+insert. Capture action IDs for delayed edits/deletes and emit the existing add/edit/delete/clear signals for stage routing. | +| `class_name WaypointContext extends PopupMenu` (`scripts/stage/waypoint_context.gd`) | Accept the waypoint's queue status at popup time; disable Edit/Insert for COMPLETED or INTERRUPTED waypoints while retaining Delete and existing trigger-rule inspection. The stage stores the `action_id` and resolves its current index when a delayed menu choice or capture commits. | Existing `action_started(action: Dictionary, index: int)`, `action_finished(action: Dictionary, index: int)`, `queue_changed`, `mode_changed(mode: int)`, `StageSelection.selection_changed(nodes: Array[Node2D])`, and `ActionEditor.committed(action: Dictionary)` signals remain. Add `simulation_paused(elapsed_seconds: float)`, `simulation_resumed(elapsed_seconds: float)`, and `simulation_reset()` on `SandboxStage` for timestamp/chrome refresh and future consumers. Emit each only after its transition has completed. A runtime queue view is an `Array[Dictionary]`; each authored entry has `{ "action": Dictionary, "authored_index": int, "status": int, "action_id": int }`. `status` uses a typed runner enum `QueueStatus { NOT_STARTED, COMPLETED, INTERRUPTED, ACTIVE, PENDING }`. Reactive consequences carry `authored_index = -1` in separate transient runtime entries and have no editable authored ID. No session ID is serialized. diff --git a/specs/phase_13_speech_bubbles.md b/docs/specs/phase_13_speech_bubbles.md similarity index 94% rename from specs/phase_13_speech_bubbles.md rename to docs/specs/phase_13_speech_bubbles.md index 923a26a..af938f3 100644 --- a/specs/phase_13_speech_bubbles.md +++ b/docs/specs/phase_13_speech_bubbles.md @@ -3,7 +3,7 @@ > **2026-10-01 behavior amendment:** Comic chain is a third Show bubbles presentation mode. It reveals bubbles cumulatively and joins each new bubble to the previous one with a broad attachment. Stack and Same spot remain placement actions. See [Comic Chain Speech Mode](speech_comic_chain_layout.md) for the current behavior and legacy payload migration. Status: **Approved follow-up implemented; saved `talk.scene` load verified (2026-09-28)**. The base feature, first composer refinement, and approved §9 follow-up are implemented. The reported scene now loads through the Sandbox Stage CLI without a skipped-speech warning; visual GUI verification remains with the user. User deferred bundled font files; previews use available system fonts until a later font-bundling task. -Source: `plans/PHASE_13_SPEECH_BUBBLES.md`, the Phase 0 UI proposal, the user's Phase 1 decisions, and the subsequent composer review. +Source: `docs/archive/plans/PHASE_13_SPEECH_BUBBLES.md`, the Phase 0 UI proposal, the user's Phase 1 decisions, and the subsequent composer review. ## 1. Product decisions and scope @@ -44,9 +44,9 @@ The stage owns the preview lifecycle. It is an unsaved child with no queue or ru | Target | Contract | | --- | --- | -| `class_name SpeechPayload extends RefCounted` (new `scripts/speech_payload.gd`) | Pure, statically typed helpers: `static func normalize(raw: Dictionary) -> Dictionary`, `static func validate(raw: Variant) -> String`, `static func duration_for(raw: Dictionary) -> float`, `static func bubble_intervals(raw: Dictionary) -> PackedFloat32Array`, `static func plain_summary(raw: Dictionary) -> String`, `static func word_span_at(text: String, index: int) -> Vector2i`, and `static func rich_text_runs(text: String, word_styles: Array[Dictionary]) -> Array[Dictionary]`. `word_span_at` returns `Vector2i(-1, -1)` for non-words. No scene-tree access. Constants enumerate valid style/font/size/reveal/effect IDs and caps. Returns deep copies, never aliases mutable action arrays. The run compiler accepts only validated whole-word spans and trusted font/color/effect IDs. | -| `class_name SpeechComposer extends Control` (`scripts/speech_composer.gd` and `scenes/speech_composer.tscn`) | Retain `open_for(actor: StickmanRig, initial: Dictionary, reactive_options: bool) -> void`, `get_draft() -> Dictionary`, `draft_changed(action: Dictionary)`, `committed(action: Dictionary)`, and `cancelled()`. Show one active `TextEdit` bound to the selected numbered tab; inactive bubble text lives in the staged draft. Keep one transient whole-word target `{bubble_index: int, start: int, end: int, text: String}` and a graphical, input-transparent word cue. Own a preview state `STATIC / PLAYING / BLANK_HOLD`; expose visibility and elapsed time to the stage without touching runner playback. | -| `class_name SpeechBubble extends Node2D` (`scripts/stickman_speech_bubble.gd`) | Keep the shared, input-transparent `RichTextLabel` renderer. Factor frame geometry so runtime frames and small frame-choice thumbnails use the same shape rules. Given the rig's live head geometry, draw one filled triangular tail from the nearest visible lower frame boundary to just outside the head rim; never draw a fallback connector line through the character. SFX has no line or tail. Derive each card's Bounce in entrance from that card's local elapsed time; the true bounce overshoots and settles, while Simply appear has no entrance scale animation. | +| `class_name SpeechPayload extends RefCounted` (new `scripts/speech/speech_payload.gd`) | Pure, statically typed helpers: `static func normalize(raw: Dictionary) -> Dictionary`, `static func validate(raw: Variant) -> String`, `static func duration_for(raw: Dictionary) -> float`, `static func bubble_intervals(raw: Dictionary) -> PackedFloat32Array`, `static func plain_summary(raw: Dictionary) -> String`, `static func word_span_at(text: String, index: int) -> Vector2i`, and `static func rich_text_runs(text: String, word_styles: Array[Dictionary]) -> Array[Dictionary]`. `word_span_at` returns `Vector2i(-1, -1)` for non-words. No scene-tree access. Constants enumerate valid style/font/size/reveal/effect IDs and caps. Returns deep copies, never aliases mutable action arrays. The run compiler accepts only validated whole-word spans and trusted font/color/effect IDs. | +| `class_name SpeechComposer extends Control` (`scripts/editors/stage/speech_composer.gd` and `scenes/editors/stage/speech_composer.tscn`) | Retain `open_for(actor: StickmanRig, initial: Dictionary, reactive_options: bool) -> void`, `get_draft() -> Dictionary`, `draft_changed(action: Dictionary)`, `committed(action: Dictionary)`, and `cancelled()`. Show one active `TextEdit` bound to the selected numbered tab; inactive bubble text lives in the staged draft. Keep one transient whole-word target `{bubble_index: int, start: int, end: int, text: String}` and a graphical, input-transparent word cue. Own a preview state `STATIC / PLAYING / BLANK_HOLD`; expose visibility and elapsed time to the stage without touching runner playback. | +| `class_name SpeechBubble extends Node2D` (`scripts/speech/stickman_speech_bubble.gd`) | Keep the shared, input-transparent `RichTextLabel` renderer. Factor frame geometry so runtime frames and small frame-choice thumbnails use the same shape rules. Given the rig's live head geometry, draw one filled triangular tail from the nearest visible lower frame boundary to just outside the head rim; never draw a fallback connector line through the character. SFX has no line or tail. Derive each card's Bounce in entrance from that card's local elapsed time; the true bounce overshoots and settles, while Simply appear has no entrance scale animation. | | `class_name StickmanActionRunner extends Node` | Keep primary and overlay playback records `{action: Dictionary, elapsed: float, duration: float}`. Add `speak_action(action: Dictionary) -> void`, `speak_overlay_action(action: Dictionary) -> void`, and `set_speech_preview_suppressed(value: bool) -> void`; retain `speak(text: String, duration: float)` and `speak_overlay(text: String, duration: float)` as adapters. Tick only the visible overlay while it has priority, pause the primary speech clock beneath it, and render the winning record. Emit `speech_finished` exactly once when each completed group ends. | | `class_name StickmanRig extends Node2D` | Keep `speak_action(action: Dictionary) -> void`, `set_speech_preview_suppressed(value: bool) -> void`, and legacy `speak(text: String, duration: float)`. Expose live head center and radius from the mounted `Body/Head` circle, with a ragdoll-head or safe rig fallback when that visual is unavailable. Preview and playback use the same geometry after facing/camera transforms. | | `class_name SandboxStage extends Node2D` | Continue routing every Speak entry point through one composer session: actor, origin context, action ID/target, draft, and return target. Manage temporary renderer, stage placement capture, actor-first capture for rule-add, and final commit through existing queue/rule methods. Preserve the recovery payload on non-explicit dismissal, validate origin identity on Restore, and clear recovery on successful Save speech or explicit Cancel. The preview never calls `rig.speak()` or changes an authored action before Save speech. | diff --git a/docs/phase_3a_spec.md b/docs/specs/phase_3a_spec.md similarity index 97% rename from docs/phase_3a_spec.md rename to docs/specs/phase_3a_spec.md index ab7e419..46b97f7 100644 --- a/docs/phase_3a_spec.md +++ b/docs/specs/phase_3a_spec.md @@ -1,14 +1,14 @@ # Phase 3a — Core Director Functionality (Implementation Specification) > **Status:** Draft for review — Architect deliverable (research + spec only; no code written). -> **Source plan:** `plans/PHASE_3a_CORE_DIRECTOR.md` +> **Source plan:** `docs/archive/plans/PHASE_3a_CORE_DIRECTOR.md` > **Scope:** Director Tool, waypoint visuals, per-stickman action queues, and an action runner executed on Play. --- ## 1. Overview & Goals -Phase 3a turns the Sandbox Stage Builder (`scripts/sandbox_stage.gd`) into a mini +Phase 3a turns the Sandbox Stage Builder (`scripts/editors/stage/sandbox_stage.gd`) into a mini "director" tool: click a stickman, choose actions from a popup, see the script as waypoints/badges in Edit mode, then press Play to run every stickman's queue. @@ -143,7 +143,7 @@ in memory and are lost on scene reload. Save/Load is a later phase (ROADMAP). ## 4. File-by-file changes -### 4.1 `scripts/stickman_rig.gd` (primary changes) +### 4.1 `scripts/characters/stickman/stickman_rig.gd` (primary changes) Add navigation/walking, speech, action queue, and runner. All public, strictly typed, null-guarded, `push_warning` prefixed `"StickmanRig: "` (matching existing style). @@ -353,7 +353,7 @@ _update_speech(delta) _update_runner(delta) ``` -### 4.2 `scripts/stickman_speech_bubble.gd` (NEW) +### 4.2 `scripts/speech/stickman_speech_bubble.gd` (NEW) `class_name SpeechBubble extends Node2D` — a world-space bubble drawn in `_draw()`. @@ -379,7 +379,7 @@ HORIZONTAL_ALIGNMENT_LEFT, MAX_WIDTH, FONT_SIZE)`, draws a rounded-rect backgrou downward tail triangle centered on the rig-local origin (the origin is the anchor above the head), then `draw_string(...)` the text. `visible = false` by default. No hit-testing. -### 4.3 `scripts/stage_director_visuals.gd` (NEW) +### 4.3 `scripts/stage/stage_director_visuals.gd` (NEW) `class_name StageDirectorVisuals extends Node2D` — Edit-mode director overlay. @@ -425,11 +425,11 @@ Drawing rules (per rig, in queue order `i = 0..n-1`): - All sizes divided by `_zoom()` so markers stay screen-constant while panning/zooming (matches `StageGizmos`). -### 4.4 `scripts/sandbox_stage.gd` (changes) +### 4.4 `scripts/editors/stage/sandbox_stage.gd` (changes) **New preload:** ```gdscript -const STAGE_DIRECTOR_VISUALS := preload("res://scripts/stage_director_visuals.gd") +const STAGE_DIRECTOR_VISUALS := preload("res://scripts/stage/stage_director_visuals.gd") ``` **New constants (popup item ids):** @@ -579,11 +579,11 @@ if _direct_mode: `_director_visuals.set_enabled(...)`. - Add `_direct_button.visible` to `_set_build_controls_visible(...)`. -### 4.5 `scripts/stage_gizmos.gd` +### 4.5 `scripts/stage/stage_gizmos.gd` **No changes.** Director visuals live in `StageDirectorVisuals` (D7). Gizmos remain the selection/hover/rotate layer. -### 4.6 `scenes/sandbox_stage.tscn` +### 4.6 `scenes/stage/sandbox_stage.tscn` **No change required.** All stage UI is built in code (`_build_ui`), so the "Direct" button is added programmatically (consistent with the existing palette/grid/snap controls). The scene already has only `SandboxStage`, `Camera2D`, and `World` — the visuals layer and popup @@ -740,7 +740,7 @@ do follow Edit-mode drags live. legacy `curved_lines_2d` addon — it is not present on disk). No `res://test/` directory. - **No CLI test command.** `project.godot` has no test autoload; `run/main_scene` is the editor. - **Established pattern: headless scripted smoke tests** — previous phases (per `docs/phase9_*` - specs and `BUGS.md`) verified work with **temporary headless `SceneTree` scripts** run + specs and `docs/known_issues.md`) verified work with **temporary headless `SceneTree` scripts** run against the console build, e.g.: ``` ..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit @@ -754,7 +754,7 @@ do follow Edit-mode drags live. 1. **Parse check** (all changed scripts): `..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit` 2. **Headless queue/runner smoke test** (temporary `SceneTree` script, deleted after): - - `StickmanFactory.spawn_from_data(load_stk("res://stickmen/test.stk"))`, `add_child`, + - `StickmanFactory.spawn_from_data(load_stk("res://content/stickmen/test.stk"))`, `add_child`, await a few frames. - Queue API: `queue_action` ×3 → `queue_size()==3`, `get_queue()` order, `insert_action`, `remove_action`, `clear_queue`. @@ -775,7 +775,7 @@ do follow Edit-mode drags live. `StageDirectorVisuals` with a rig that has a queue; assert `_collect_rigs()` and that `_draw()` runs without errors (drawing can't be pixel-asserted headlessly). -### 10.3 Manual checklist (F6 on `res://scenes/sandbox_stage.tscn`) +### 10.3 Manual checklist (F6 on `res://scenes/stage/sandbox_stage.tscn`) 1. Place a Ground + a Stickman (feet on ground). 2. Select **Direct** → click the stickman → popup appears. @@ -803,8 +803,8 @@ Two debug instrumentation gates are shipped, **off by default** (flip the const | Gate | Location | Traces | | ---- | -------- | ------ | -| `const DEBUG_WALK := false` | `scripts/stickman_rig.gd` | Per-frame walk trace (`[walk] frame=… mode=nav\|direct finished=… reachable=… map_iter=…`) plus one-shot `walk_to` / `_finish_walk` / `_cancel_walking` / runner prints. | -| `const DEBUG_STAGE := false` | `scripts/sandbox_stage.gd` | `[stage] walk_to captured target=…`, `[nav] baked verts=… polys=…`, `[stage] PLAY rigs=…`. | +| `const DEBUG_WALK := false` | `scripts/characters/stickman/stickman_rig.gd` | Per-frame walk trace (`[walk] frame=… mode=nav\|direct finished=… reachable=… map_iter=…`) plus one-shot `walk_to` / `_finish_walk` / `_cancel_walking` / runner prints. | +| `const DEBUG_STAGE := false` | `scripts/editors/stage/sandbox_stage.gd` | `[stage] walk_to captured target=…`, `[nav] baked verts=… polys=…`, `[stage] PLAY rigs=…`. | These are diagnostic-only and are never enabled in production. diff --git a/docs/phase_3b_asset_grid_spec.md b/docs/specs/phase_3b_asset_grid_spec.md similarity index 89% rename from docs/phase_3b_asset_grid_spec.md rename to docs/specs/phase_3b_asset_grid_spec.md index bea70ef..608ed44 100644 --- a/docs/phase_3b_asset_grid_spec.md +++ b/docs/specs/phase_3b_asset_grid_spec.md @@ -1,7 +1,7 @@ # Phase 3b — Asset Library: Stickman + Prop Selector Grids (Spec) Status: IMPLEMENTED (revised by selector bugfix round, 2026-09-03) -Related plan: `plans/PHASE_3b_ASSET_GRID.md` +Related plan: `docs/archive/plans/PHASE_3b_ASSET_GRID.md` Target: Godot **4.7** (`project.godot:19` declares `config/features=PackedStringArray("4.7", ...)`; the test runner comment in `tests/test_phase4b_stage.gd:12` names `Godot_v4.7.1`). --- @@ -11,7 +11,7 @@ Target: Godot **4.7** (`project.godot:19` declares `config/features=PackedString Phase 3b replaces the Sandbox Stage Builder's hard-coded single-stickman and single-prop palette buttons with **visual selector grids**: -- Clicking **"Stickman"** opens a grid of every `.stk` file in `res://stickmen/`, each with a +- Clicking **"Stickman"** opens a grid of every `.stk` file in `res://content/stickmen/`, each with a rendered thumbnail. - Clicking **"Prop"** opens a grid of 4 built-in prop templates (Crate/Ball/Plank/Triangle), each with a thumbnail + material badge. @@ -32,12 +32,12 @@ palette buttons with **visual selector grids**: ### Out of scope / untouched (must not regress) - **Terrain palette buttons** `Ground`/`Ramp`/`Step` and the **`Area`** button remain direct - placement buttons, unchanged (`scripts/stage_spawner.gd:178-220` registry, `scripts/sandbox_stage.gd:1151-1157`). + placement buttons, unchanged (`scripts/stage/stage_spawner.gd:178-220` registry, `scripts/editors/stage/sandbox_stage.gd:1151-1157`). - **Terrain drag-painting** (`_begin_terrain_drag` / `_update_terrain_drag` / - `_commit_terrain_drag`, `scripts/sandbox_stage.gd:712-869`) — untouched; `prop`/`stickman` are + `_commit_terrain_drag`, `scripts/editors/stage/sandbox_stage.gd:712-869`) — untouched; `prop`/`stickman` are non-terrain ids so they keep single-placement via `_place_at`. - **Ghost system** (`_spawn_ghost`, `_configure_ghost_collision`, `_update_ghost_position`, - `scripts/sandbox_stage.gd:614-668`) — untouched; the ghost automatically reflects the selected + `scripts/editors/stage/sandbox_stage.gd:614-668`) — untouched; the ghost automatically reflects the selected asset once `selected_stickman_path`/`selected_prop_id` are honored by `spawn()`. - **Director / rule-builder** flows (Phase 3a/4) — untouched. - No `.stk` format changes; no editor changes. @@ -53,8 +53,8 @@ palette buttons with **visual selector grids**: `StickmanFactory.spawn_from_data(stk_data)` in an offscreen `SubViewport` — identical to what gets placed on stage (not a re-implementation of `WholeStickmanPreview`). 3. **Prop set: ALL 4 TEMPLATES** — Crate/Wood, Ball/Rubber, Plank/Metal, Triangle/Cardboard. -4. **Selector build: SEPARATE `.tscn` SCENE** — `res://scenes/asset_selector.tscn` with root - script `scripts/asset_selector.gd`. The `.tscn` is a minimal **shell/layout skeleton only** +4. **Selector build: SEPARATE `.tscn` SCENE** — `res://scenes/stage/asset_selector.tscn` with root + script `scripts/shared/ui/asset_selector.gd`. The `.tscn` is a minimal **shell/layout skeleton only** (title bar, empty grid placeholder, footer controls as authored nodes); all dynamic cell content is still built in code at runtime. `sandbox_stage.gd` instantiates the scene and applies its `_ui_font`/`_emoji_font`/theme overrides after instantiation. @@ -65,15 +65,15 @@ palette buttons with **visual selector grids**: | File | `class_name` / extends | Responsibility | |---|---|---| -| `res://scripts/stickman_library.gd` | `StickmanLibrary` / `RefCounted` | Scan `res://stickmen/*.stk` → entry models; display-name fallback; corrupt-file skip; ad-hoc Browse entries | -| `res://scripts/prop_library.gd` | `PropLibrary` / `RefCounted` | Static registry of the 4 prop templates (id → payload + material preset + label) | -| `res://scripts/thumbnails/thumbnail_cache.gd` | `ThumbnailCache` / `RefCounted` | Disk PNG cache: key computation, load/save, invalidation, stale cleanup | -| `res://scripts/thumbnails/stickman_thumbnail.gd` | `StickmanThumbnail` / `Node` | Render a parsed `.stk` → `Texture2D` via real rig in offscreen `SubViewport` | -| `res://scripts/thumbnails/prop_thumbnail.gd` | `PropThumbnail` / `Node` | Render a prop template → `Texture2D` (lightweight visual, no physics) | -| `res://scripts/asset_selector.gd` | `AssetSelector` / `PopupPanel` | Grid UI controller (root of the `.tscn` shell) | -| `res://scenes/asset_selector.tscn` | `PopupPanel` root + `asset_selector.gd` | Shell: title bar, empty `GridContainer`, footer buttons | +| `res://scripts/characters/stickman/stickman_library.gd` | `StickmanLibrary` / `RefCounted` | Scan `res://content/stickmen/*.stk` → entry models; display-name fallback; corrupt-file skip; ad-hoc Browse entries | +| `res://scripts/props/prop_library.gd` | `PropLibrary` / `RefCounted` | Static registry of the 4 prop templates (id → payload + material preset + label) | +| `res://scripts/shared/thumbnails/thumbnail_cache.gd` | `ThumbnailCache` / `RefCounted` | Disk PNG cache: key computation, load/save, invalidation, stale cleanup | +| `res://scripts/shared/thumbnails/stickman_thumbnail.gd` | `StickmanThumbnail` / `Node` | Render a parsed `.stk` → `Texture2D` via real rig in offscreen `SubViewport` | +| `res://scripts/shared/thumbnails/prop_thumbnail.gd` | `PropThumbnail` / `Node` | Render a prop template → `Texture2D` (lightweight visual, no physics) | +| `res://scripts/shared/ui/asset_selector.gd` | `AssetSelector` / `PopupPanel` | Grid UI controller (root of the `.tscn` shell) | +| `res://scenes/stage/asset_selector.tscn` | `PopupPanel` root + `asset_selector.gd` | Shell: title bar, empty `GridContainer`, footer buttons | -Directory `res://scripts/thumbnails/` does not exist yet — create it. +The shared thumbnail scripts now live under `res://scripts/shared/thumbnails/`. --- @@ -83,7 +83,7 @@ Directory `res://scripts/thumbnails/` does not exist yet — create it. ```gdscript { - "path": String, # "res://stickmen/bob.stk" (or a Browse-chosen arbitrary path) + "path": String, # "res://content/stickmen/bob.stk" (or a Browse-chosen arbitrary path) "name": String, # display name: stickman_name if non-empty, else filename basename "data": Dictionary, # parsed JSON root (parse failures skip the entry entirely — never {}) } @@ -113,19 +113,19 @@ Default materials (per decision 3): | `plank` | Plank | `PropUtils.create_plank()` | `PropBlock.MaterialPreset.METAL` | Metal | | `triangle` | Triangle | `PropUtils.create_triangle()` | `PropBlock.MaterialPreset.CARDBOARD` | Cardboard | -The generator color themes already match these presets (`scripts/prop_utils.gd:23-30`). +The generator color themes already match these presets (`scripts/props/prop_utils.gd:23-30`). --- ## 5. Public API Signatures (GDScript) -### 5.1 `StickmanLibrary` (`res://scripts/stickman_library.gd`) +### 5.1 `StickmanLibrary` (`res://scripts/characters/stickman/stickman_library.gd`) ```gdscript class_name StickmanLibrary extends RefCounted -const STICKMEN_DIR := "res://stickmen" +const STICKMEN_DIR := "res://content/stickmen" var entries: Array[Dictionary] = [] # last scan result (empty until scan()) @@ -142,7 +142,7 @@ func find_by_path(path: String) -> Dictionary # {} if absent func make_entry(path: String) -> Dictionary # ad-hoc (Browse): load_stk + name fallback; {} on failure ``` -### 5.2 `PropLibrary` (`res://scripts/prop_library.gd`) +### 5.2 `PropLibrary` (`res://scripts/props/prop_library.gd`) ```gdscript class_name PropLibrary @@ -159,7 +159,7 @@ static func get_default_id() -> String # "crate" Note: `PropUtils.create_*()` are `static func` — they **cannot** run in a `const` initializer, so `_templates` is a `static var` built on first access. -### 5.3 `ThumbnailCache` (`res://scripts/thumbnails/thumbnail_cache.gd`) +### 5.3 `ThumbnailCache` (`res://scripts/shared/thumbnails/thumbnail_cache.gd`) ```gdscript class_name ThumbnailCache @@ -180,7 +180,7 @@ func ensure_dir(dir: String) -> void # DirAccess.make_dir_recursive_abso func clean_stale_stickmen(valid_keys: Dictionary) -> void # optional: delete basename_* not in valid set ``` -### 5.4 `StickmanThumbnail` (`res://scripts/thumbnails/stickman_thumbnail.gd`) +### 5.4 `StickmanThumbnail` (`res://scripts/shared/thumbnails/stickman_thumbnail.gd`) ```gdscript class_name StickmanThumbnail @@ -193,7 +193,7 @@ func render(stk_data: Dictionary) -> Texture2D # Returns a placeholder ImageTexture on blank/failed capture (headless degrade). ``` -### 5.5 `PropThumbnail` (`res://scripts/thumbnails/prop_thumbnail.gd`) +### 5.5 `PropThumbnail` (`res://scripts/shared/thumbnails/prop_thumbnail.gd`) ```gdscript class_name PropThumbnail @@ -206,7 +206,7 @@ func render(payload: Dictionary, material_preset: int) -> Texture2D # tinted via PropBlock.tint_for(material_preset) when preset != NONE; frames + captures. ``` -### 5.6 `AssetSelector` (`res://scripts/asset_selector.gd`, root of `.tscn`) +### 5.6 `AssetSelector` (`res://scripts/shared/ui/asset_selector.gd`, root of `.tscn`) ```gdscript class_name AssetSelector @@ -283,7 +283,7 @@ func _frame_camera(bbox: Rect2) -> void: ### 6.2 Prop (lightweight visual — no physics) Mirror `PropBlock._apply_polygon_geometry()` / `_apply_circle_geometry()` -(`scripts/prop_block.gd:169-194`) into a plain `Node2D` with a `Polygon2D` (fill) and a `Line2D` +(`scripts/props/prop_block.gd:169-194`) into a plain `Node2D` with a `Polygon2D` (fill) and a `Line2D` (outline, closed loop, round joints/caps). **Do not instantiate `PropBlock`** — it is a `RigidBody2D` and would fall under gravity inside the SubViewport. Apply `PropBlock.tint_for(material_preset)` for non-`NONE` presets, then the same `_frame_camera` + @@ -313,10 +313,10 @@ double-`frame_post_draw` capture as above. ## 7. `AssetSelector` — `.tscn`-Based Design -### 7.1 Scene structure (`res://scenes/asset_selector.tscn`) +### 7.1 Scene structure (`res://scenes/stage/asset_selector.tscn`) ``` -AssetSelector (PopupPanel, root, script = res://scripts/asset_selector.gd) +AssetSelector (PopupPanel, root, script = res://scripts/shared/ui/asset_selector.gd) ├── MarginContainer │ └── VBoxContainer │ ├── TitleBar (HBoxContainer) @@ -364,7 +364,7 @@ relied on). ```gdscript # preload at top: -const ASSET_SELECTOR := preload("res://scenes/asset_selector.tscn") +const ASSET_SELECTOR := preload("res://scenes/stage/asset_selector.tscn") # in _ready(), after _build_ui(): _selector = ASSET_SELECTOR.instantiate() as AssetSelector @@ -380,11 +380,11 @@ _selector.refresh_requested.connect(_on_refresh_requested) If a dedicated `AssetSelector.apply_font(ui_font: Font, emoji_font: Font) -> void` helper is preferred (walking the authored controls and adding font overrides), the stage calls it right -after `add_child` using `_ui_font`/`_emoji_font` (loaded in `_load_theme`, `scripts/sandbox_stage.gd:2164-2212`). +after `add_child` using `_ui_font`/`_emoji_font` (loaded in `_load_theme`, `scripts/editors/stage/sandbox_stage.gd:2164-2212`). --- -## 8. `StageSpawner` Precise Edits (`scripts/stage_spawner.gd`) +## 8. `StageSpawner` Precise Edits (`scripts/stage/stage_spawner.gd`) ### 8.1 New state (replace the single `_stickman_data`) @@ -461,7 +461,7 @@ func get_selected_prop_id() -> String --- -## 9. `SandboxStage` Precise Edits (`scripts/sandbox_stage.gd`) +## 9. `SandboxStage` Precise Edits (`scripts/editors/stage/sandbox_stage.gd`) ### 9.1 New state @@ -493,7 +493,7 @@ add_child(_prop_thumb) ``` Build the selector in `_build_ui()` (see §7.3) or immediately after it; build `_browse_dialog` -mirroring `scripts/test_harness.gd:286-292`: +mirroring `scripts/tools/test_harness.gd:286-292`: ```gdscript _browse_dialog = FileDialog.new() @@ -558,7 +558,7 @@ func _on_selector_cancelled() -> void: set_placement_mode("") # un-press the palette button ``` -`set_placement_mode` (`scripts/sandbox_stage.gd:566-576`) already loops `_palette_buttons` to sync +`set_placement_mode` (`scripts/editors/stage/sandbox_stage.gd:566-576`) already loops `_palette_buttons` to sync pressed state, so it correctly presses the `stickman`/`prop` button on selection and un-presses on cancel. @@ -609,7 +609,7 @@ selection. ### 9.8 Guards -While `_selector_open`, `_handle_world_click` (`scripts/sandbox_stage.gd:360-409`) and +While `_selector_open`, `_handle_world_click` (`scripts/editors/stage/sandbox_stage.gd:360-409`) and `_handle_mouse_motion` (lines 412-426) early-return at the top (the modal `PopupPanel` already blocks most input, but the stage uses `_input` for mouse — the explicit flag is belt-and-suspenders). @@ -627,7 +627,7 @@ blocks most input, but the stage uses `_input` for mouse — the explicit flag i | Modified `.stk` | New cache key (mtime-embedded) → regenerate thumbnail | | Thumbnail render fails (headless / blank) | Placeholder texture; retried next open | | Selected file deleted | On open, if `selected_stickman_path` not in scan → fall back to first entry (or empty state) | -| Browse-selected file (outside `stickmen/`) | Spawnable by path via `make_entry`; not rescanned; re-validated next open | +| Browse-selected file (outside `content/stickmen/`) | Spawnable by path via `make_entry`; not rescanned; re-validated next open | | Duplicate display names | Allowed; selection is by path/id, not name | | Grid resize | `GridContainer` reflows; page bounds recomputed on `resized` | | Window resize | Selector re-centers (`popup_centered()` re-run on the root's `size_changed` while visible); stage behind the grid is dimmed by `_selector_dim` while open | @@ -641,7 +641,7 @@ blocks most input, but the stage uses `_input` for mouse — the explicit flag i ### 11.1 Stickman selector - [ ] "Stickman" opens the grid (does not immediately spawn `test.stk`). -- [ ] Grid lists all `.stk` in `res://stickmen/` (test/basic/break). +- [ ] Grid lists all `.stk` in `res://content/stickmen/` (test/basic/break). - [ ] Each cell shows a rig-rendered thumbnail. - [ ] Name = `stickman_name`, else filename (`test`/`break` fall back; `Basic` shows "Basic"). - [ ] Click → select + close + enter placement (ghost appears). @@ -699,7 +699,7 @@ Cover (no pixel assertions — headless renders blank): (no `crate`/`ball`). 6. `_spawn_prop` honors `selected_prop_id`: default spawns a `PropBlock` (crate); set `selected_prop_id = "ball"` → spawns a circle prop (`shape_type == ShapeType.CIRCLE`). -7. `_spawn_stickman` honors `selected_stickman_path`: set it to `res://stickmen/basic.stk` → spawns +7. `_spawn_stickman` honors `selected_stickman_path`: set it to `res://content/stickmen/basic.stk` → spawns a `StickmanRig` whose mounted geometry differs from `test.stk` (or at minimum spawns non-null). 8. `ThumbnailCache`: `stickman_key(path)` changes when `FileAccess.get_modified_time` changes (mock by passing a path + expected mtime); `stickman_png`/`prop_png` path formatting; @@ -722,7 +722,7 @@ Cover (no pixel assertions — headless renders blank): ### 12.4 Manual (F6) -`res://scenes/sandbox_stage.tscn`: visual grid, thumbnails, pagination, Browse, Refresh, +`res://scenes/stage/sandbox_stage.tscn`: visual grid, thumbnails, pagination, Browse, Refresh, ghost/placement, mode-toggle persistence. --- diff --git a/docs/phase_3c_editor_spec.md b/docs/specs/phase_3c_editor_spec.md similarity index 92% rename from docs/phase_3c_editor_spec.md rename to docs/specs/phase_3c_editor_spec.md index 12f1254..742b14c 100644 --- a/docs/phase_3c_editor_spec.md +++ b/docs/specs/phase_3c_editor_spec.md @@ -1,7 +1,7 @@ # Phase 3c — Editor Tools: Action & Rule Editing (Implementation Spec) Status: IMPLEMENTED + TESTED (12 headless suites, 667 assertions; `tests/test_phase3c_editor.gd` alone: 253 assertions) -Related plan: `plans/PHASE_3c_EDITOR.md` +Related plan: `docs/archive/plans/PHASE_3c_EDITOR.md` Target: Godot **4.7** (`project.godot:19` declares `config/features=PackedStringArray("4.7", ...)`; the Phase 3c test header names `Godot_v4.7.1-stable_win64_console.exe`). --- @@ -26,16 +26,16 @@ one-entry registry append (plan §4/§10) — no other code changes. ### In scope -- `scripts/action_registry.gd` / `trigger_registry.gd` — extensible template registries. -- `scripts/queue_panel.gd` + `scenes/queue_panel.tscn` — Action Queue panel. -- `scripts/rule_panel.gd` + `scenes/rule_panel.tscn` — Rule list panel. -- `scripts/action_editor.gd` + `scenes/action_editor.tscn` — single-action property editor. -- `scripts/rule_editor.gd` + `scenes/rule_editor.tscn` — full / consequence-only rule editor. -- `scripts/waypoint_context.gd` — waypoint right-click menu. -- `scripts/sandbox_stage.gd` — Phase 3c wiring: "Edit Queue…"/"Edit Rules…" entry points, +- `scripts/stage/action_registry.gd` / `trigger_registry.gd` — extensible template registries. +- `scripts/editors/stage/queue_panel.gd` + `scenes/editors/stage/queue_panel.tscn` — Action Queue panel. +- `scripts/editors/stage/rule_panel.gd` + `scenes/editors/stage/rule_panel.tscn` — Rule list panel. +- `scripts/editors/stage/action_editor.gd` + `scenes/editors/stage/action_editor.tscn` — single-action property editor. +- `scripts/editors/stage/rule_editor.gd` + `scenes/editors/stage/rule_editor.tscn` — full / consequence-only rule editor. +- `scripts/stage/waypoint_context.gd` — waypoint right-click menu. +- `scripts/editors/stage/sandbox_stage.gd` — Phase 3c wiring: "Edit Queue…"/"Edit Rules…" entry points, right-click context menus, the **unified target-capture system** (`CaptureKind`), the shared confirmation dialog, and consequence-only rule editing on rule-label click. -- `scripts/stage_director_visuals.gd` — `hit_test_waypoint_action()`, `set/clear_edit_waypoint` +- `scripts/stage/stage_director_visuals.gd` — `hit_test_waypoint_action()`, `set/clear_edit_waypoint` with the pulsing edit highlight, and rule-label → editor routing. - `tests/test_phase3c_editor.gd` — 253-assertion headless suite. @@ -95,20 +95,20 @@ one-entry registry append (plan §4/§10) — no other code changes. | File | `class_name` / extends | Responsibility | |---|---|---| -| `res://scripts/action_registry.gd` | `ActionRegistry` / `RefCounted` | `ACTION_TEMPLATES` (5 actions), static accessors + queue⇄rule conversions + summaries | -| `res://scripts/trigger_registry.gd` | `TriggerRegistry` / `RefCounted` | `TRIGGER_TEMPLATES` (5 triggers), static accessors + summaries | -| `res://scripts/queue_panel.gd` | `QueuePanel` / `PopupPanel` | Action Queue editor popup (root of `queue_panel.tscn`) | -| `res://scripts/rule_panel.gd` | `RulePanel` / `PopupPanel` | Rule list editor popup (root of `rule_panel.tscn`) | -| `res://scripts/action_editor.gd` | `ActionEditor` / `PopupPanel` | Single-action add/edit editor (root of `action_editor.tscn`) | -| `res://scripts/rule_editor.gd` | `RuleEditor` / `PopupPanel` | Full + consequence-only rule editor (root of `rule_editor.tscn`) | -| `res://scripts/waypoint_context.gd` | `WaypointContext` / `PopupMenu` | Waypoint right-click menu | -| `res://scenes/queue_panel.tscn` / `rule_panel.tscn` / `action_editor.tscn` / `rule_editor.tscn` | `PopupPanel` roots | Minimal shells; UI built in code | +| `res://scripts/stage/action_registry.gd` | `ActionRegistry` / `RefCounted` | `ACTION_TEMPLATES` (5 actions), static accessors + queue⇄rule conversions + summaries | +| `res://scripts/stage/trigger_registry.gd` | `TriggerRegistry` / `RefCounted` | `TRIGGER_TEMPLATES` (5 triggers), static accessors + summaries | +| `res://scripts/editors/stage/queue_panel.gd` | `QueuePanel` / `PopupPanel` | Action Queue editor popup (root of `queue_panel.tscn`) | +| `res://scripts/editors/stage/rule_panel.gd` | `RulePanel` / `PopupPanel` | Rule list editor popup (root of `rule_panel.tscn`) | +| `res://scripts/editors/stage/action_editor.gd` | `ActionEditor` / `PopupPanel` | Single-action add/edit editor (root of `action_editor.tscn`) | +| `res://scripts/editors/stage/rule_editor.gd` | `RuleEditor` / `PopupPanel` | Full + consequence-only rule editor (root of `rule_editor.tscn`) | +| `res://scripts/stage/waypoint_context.gd` | `WaypointContext` / `PopupMenu` | Waypoint right-click menu | +| `res://scenes/editors/stage/queue_panel.tscn` / `rule_panel.tscn` / `action_editor.tscn` / `rule_editor.tscn` | `PopupPanel` roots | Minimal shells; UI built in code | --- ## 4. Public API Signatures (GDScript) -### 4.1 `ActionRegistry` (`res://scripts/action_registry.gd`) +### 4.1 `ActionRegistry` (`res://scripts/stage/action_registry.gd`) ```gdscript class_name ActionRegistry @@ -140,7 +140,7 @@ static func summarize(action: Dictionary) -> String # both the flat and rule-action key layouts via get() fallbacks. ``` -### 4.2 `TriggerRegistry` (`res://scripts/trigger_registry.gd`) +### 4.2 `TriggerRegistry` (`res://scripts/stage/trigger_registry.gd`) ```gdscript class_name TriggerRegistry @@ -162,7 +162,7 @@ static func target_type(type: String) -> String # "waypoint" | "action_type" | static func summarize(trigger: Dictionary) -> String # "