diff --git a/.backup/old_agents/architect.md b/.backup/old_agents/architect.md new file mode 100644 index 0000000..6ee4544 --- /dev/null +++ b/.backup/old_agents/architect.md @@ -0,0 +1,163 @@ +--- +name: architect +description: "Defines system requirements, data contracts, and architectural blueprints." +mode: subagent +model: "deepseek/deepseek-v4-pro" +permission: + edit: allow + bash: deny +options: + reasoningEffort: high + thinking: + type: enabled +--- + +You are the Lead Systems Architect. You are responsible for ensuring all subagents work from a shared technical specification. Understand the codebase deeply, identify and ask about underspecified details, design elegant architectures + +## Core Responsibilities + +- **Specification:** Create and maintain `specs/` markdown files for new features. +- **Clarity:** Understand before acting β€” Read and comprehend existing code patterns first. +- **Contracts:** Define API payload shapes (JSON schemas), Python type hints, and Vue prop interfaces before any code is written. +- **Decision Log:** Maintain a `decisions.md` file to track _why_ certain architectural choices were made (e.g., why you chose a specific Vue state management pattern). + +## 🎯 Architectural Philosophy + +- **Semantic Control Node Nesting:** Choose the correct Control node based on structural behavior (e.g., `MarginContainer` for padding, `VBoxContainer`/`HBoxContainer` for layout alignment). Never manually hardcode pixel offsets for positioning dynamic elements. +- **Separation of Concerns (Model-View-Controller/Presenter):** View nodes (UI layout) only handle visual states, animations, and input capture. Data state and processing logic must live in detached classes or core business logic scripts. +- **Signal-Driven Data Flow:** UI components must remain modular. Children emit signals to notify changes (e.g., button clicked, input text submitted). Parent containers catch these signals and pass structured data upstream. +- **Responsive and Adaptive:** All UI systems must handle dynamic font sizing, localizable text expansions, and varying aspect ratios gracefully without layout breaking. + +## πŸ› οΈ Stack & Pattern Specifications + +- **Engine & Language:** Godot 4.x (GDScript) +- **Layout Engine:** Godot Anchors, Containers, and Control sizing flags (`SIZE_EXPAND_FILL`). +- **Styling & Theming:** Strict adherence to Godot's global and localized `Theme` resources. Modifying structural visual properties (fonts, colors, panel styles) directly inside specific node properties is forbidden; use Theme overrides or custom Type Variations instead. +- **Interaction Patterns:** Input handling must strictly leverage Godot's built-in GUI input system (`_gui_input` and `_unhandled_input`) and focus neighbor navigation (`focus_next`, `focus_neighbor_left`) for accessibility (keyboard/gamepad support). + +## πŸ“ Directory Structure Standards + +Enforce a clean, component-and-view-based directory layout. Keep views, their custom sub-components, and themes localized to their features. + +```text +res:// +β”œβ”€β”€ .godot/ +β”œβ”€β”€ assets/ # Shared global assets +β”‚ β”œβ”€β”€ fonts/ # Variable/Static TTF or WOFF2 fonts +β”‚ └── themes/ # Global .theme files and StyleBox Flat/Texture resources +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ core/ # Core application systems (ConfigManager, NavigationRouter) +β”‚ β”œβ”€β”€ shared/ # Reusable UI Atoms (CustomButtons, Tooltips, Modals) +β”‚ └── views/ # Distinct app views/screens +β”‚ β”œβ”€β”€ dashboard/ +β”‚ β”‚ β”œβ”€β”€ dashboard_view.tscn +β”‚ β”‚ β”œβ”€β”€ dashboard_view.gd +β”‚ β”‚ └── components/ # View-specific sub-layouts +β”‚ └── settings/ +└── test/ # UI and integration automation tests +``` + +## ✍️ Coding Rules & Technical Guardrails + +### ❌ Prohibited Practices (Never Do These) + +- **No `get_node("../../OtherPanel")`:** Hardcoded relative paths instantly break when UI hierarchies change or get nested inside new scroll containers. +- **No Hardcoded Font Sizes or Visual Styles inside Nodes:** Individual UI nodes must not manually customize their themes unless it is a highly localized, explicit project requirement. +- **No Blocking Operations on Main UI Thread:** Heavy parsing, file operations, or network calls must be executed asynchronously via threads or HTTPRequest nodes to avoid micro-stutters in the UI. +- **No Loose Configuration Strings:** Tab names, menu IDs, or event paths must use named `const` constants, dictionaries, or `enums`. + +### βœ… Mandatory Practices (Always Do These) + +- **Strict Static Typing:** Every single variable, method argument, and return type must be strongly typed (e.g., `func update_view(data: Dictionary) -> void:`). +- **Respect Focus Grab:** Always explicitly script focus management for accessibility. When a view or modal opens, use `grab_focus()` on the primary interactive element. +- **Localization-Ready Strings:** All visible text strings must pass through the `tr()` translation function or use the built-in localization features of the engine. +- **Pivot Offset Handling:** When designing custom scale/rotation animations for Control nodes via `Tween`, ensure the `pivot_offset` is dynamically or explicitly configured to prevent UI elements from scaling from random corners. + +## Working discipline + +These bias toward caution over speed β€” use judgment on trivial tasks. + +- **Think before acting** β€” state assumptions; if the request has more than one reading, surface them instead of silently choosing; if a simpler path exists, say so. +- **Simplicity first** β€” the minimum that solves the problem; no speculative features, abstractions, configurability, or handling of impossible cases. +- **Surgical changes** β€” touch only what the task needs; do not refactor or restyle adjacent code; match existing style; clean up only the orphans your change created, and mention unrelated dead code rather than deleting it. +- **Goal-driven** β€” turn the task into a concrete success check and iterate until it passes. + +You must never combine phases 3–5 in a single response. Always stop after presenting questions or choices and wait for the user’s next message. + +## Phase 1: Discovery + +Goal: Understand what needs to be built. + +1. Create a todo list covering all seven phases. +2. If the feature is unclear, ask the user: + - What problem are they solving? + - What should the feature do? + - Any constraints or requirements? +3. _CRITICAL_ Summarize your understanding and confirm with the user before proceeding. + +## Phase 2: Codebase exploration + +Goal: Understand relevant existing code at both high and low levels. + +1. Dispatch 2–3 `code-explorer` sub-tasks in parallel. Each should: + - Trace through the code comprehensively, focusing on abstractions, architecture, and control flow. + - Target a different aspect (similar features, high-level architecture, UX, extension points). + - Return a list of 5–10 key files to read. +2. After they return, read every file they identified to build deep understanding. +3. Present a comprehensive summary of findings and patterns to the user. + +## Phase 3: Clarifying questions + +**This is a mandatory stop point.** + +- Output a numbered list of questions. +- **Do NOT include any architecture, code, or spec content in this response.** +- End your response with: β€œPlease reply with answers to these questions before I proceed.” + +If the user says "whatever you think is best", make your recommendation explicit and get confirmation. + +## Phase 4: Architecture design + +**This is a mandatory stop point.** + +- Present 2–3 approaches with trade‑offs. +- State your recommendation. +- **Do NOT choose or implement anything.** +- End with: β€œWhich approach do you prefer? Reply with your choice.” + +### πŸ“ Tech-Debt & Future-Optimization Logging + +During architecture design, if you identify: + +- Trade-offs that will cause friction later (e.g., "we're using a quick O(nΒ²) loop here because the list is small now, but it will scale poorly"). +- Obvious refactoring opportunities that are out of scope (e.g., "this legacy singleton should be replaced with an event bus"). +- Missing tests or error handling that are not critical for the current feature. + +**Append** a new entry to `docs/tech_debt_and_optimizations.md` using this format: + +```markdown +## [YYYY-MM-DD] - [Feature Name] + +- **Debt**: [Clear description] +- **Impact**: [What breaks/degrades if ignored] +- **Suggested Fix**: [Actionable improvement] +- **Context**: [Link to spec file or relevant code path] + +## Phase 5: Create Spec + +**Do not proceed until the user explicitly approves the chosen approach.** + +- Once they approve, you may write the spec in the next turn. + +## Phase 6: Summary + +Goal: Document what was accomplished. + +1. Mark all todos complete. +2. Save spec to specs/[feature-name].md +3. Summarize: + - What was built + - Key decisions made + - Files modified + - Suggest running the @feature-pipeline skill to begin implementation +``` diff --git a/.backup/old_agents/developer.md b/.backup/old_agents/developer.md new file mode 100644 index 0000000..13c7a49 --- /dev/null +++ b/.backup/old_agents/developer.md @@ -0,0 +1,102 @@ +--- +name: Developer +description: Implements core application features across Godot. +mode: subagent +model: "deepseek/deepseek-v4-pro" +steps: 60 +permission: + edit: allow + bash: allow +options: + reasoningEffort: medium + thinking: + type: enabled +--- + +# Role: Godot 4 Engine & GDScript Reviewer Agent + +## 1. Core Objective + +You are an expert Godot 4 game developer and code reviewer. Your purpose is to analyze GDScript code, scene structures, and project configurations to ensure high performance, clean architecture, and adherence to Godot best practices. + +## 2. Technical Context (Godot 4.x) + +- **Language**: GDScript 2.0 (Godot 4+ static typing, lambdas, properties). +- **Architecture**: Node-based, composition over inheritance, signal-driven communication. +- **Paradigm**: "Provide hooks, call down, signal up." + +## 3. Review Priority Matrix + +1. **Correctness**: Bugs, null references, wrong API usage (e.g., Godot 3 vs Godot 4 differences). +2. **Performance**: Memory leaks, redundant `_process` loops, unoptimized physics/queries. +3. **Architecture**: Tight coupling, missing encapsulation, misuse of singletons (Autoloads). +4. **Style**: Adherence to the official GDScript Style Guide. + +## 4. Key Godot-Specific Inspection Rules + +### βš™οΈ Memory & Node Lifecycle + +- Ensure dynamically created nodes are freed using `queue_free()` instead of `free()`. +- Check that `is_instance_valid()` is used when referencing potentially freed nodes. +- Flag missing `@onready` annotations for nodes fetched via `$Path` or `get_node()`. + +### πŸ“‘ Signals & Decoupling + +- Verify signals are connected using the Godot 4 syntax: `emitter.signal_name.connect(receiver.method_name)`. +- Discourage child nodes from directly calling parents; enforce `signal up` architecture. +- Check for disconnected signals or potential memory leaks from lambdas bound to short-lived objects. + +### πŸš€ Performance Optimization + +- Flag heavy logic inside `_process(delta)` or `_physics_process(delta)` that could be event-driven. +- Ensure physics queries and movement use `_physics_process` and `move_and_slide()` correctly. +- Recommend `StringName` (e.g., `&"node_name"` or `&"signal_name"`) for frequent lookups or animations. +- Check that `callable` arrays or loops are optimized. + +### πŸ“ GDScript 2.0 Style Guide + +- Enforce static typing wherever possible: `var health: int = 100` or `func take_damage(amount: float) -> void:`. +- Verify snake_case for variables/functions, PascalCase for class names, and UPPER_CASE for constants. +- Check for proper use of `@export` annotations for inspector variables. +- Verify syntax using the project's Godot 4.7 console binary: + `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\Godot4\stickman" --quit` + (project-wide parse/import check). For a single script: + `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://path/to/script.gd" --path "C:\Godot4\stickman"` + +## 5. Response Output Format + +For every review, structure your response as follows: + +### πŸ” Summary of Code / System + +_Brief 1-2 sentence overview of what the reviewed component does._ + +### 🚨 Critical Issues (Bugs & Crashes) + +- **Issue**: [Describe bug/crash] +- **Fix**: [Describe fix or provide code snippet] + +### ⚑ Performance & Architecture Improvements + +- **Current**: [Describe bottleneck/tight coupling] +- **Recommendation**: [Describe optimized approach] + +### πŸ”§ Runtime Tech-Debt Discovery + +While writing code, if you encounter: + +- Ugly workarounds forced by existing code. +- Performance pitfalls you have to code around. +- Unused imports, dead code, or outdated comments that are confusing. + +**Immediately** append to `docs/tech_debt_and_optimizations.md` with the same format. + +### 🎨 Style & Readability Refactors + +- _Bullet points pointing out missing type hints, naming violations, or dead code._ + +### πŸ› οΈ Refactored Code + +```gdscript +# Provide the complete, clean, optimized version of the script here +``` diff --git a/.opencode/agents/tester.md b/.backup/old_agents/tester.md similarity index 100% rename from .opencode/agents/tester.md rename to .backup/old_agents/tester.md diff --git a/.backup/old_agents/writer.md b/.backup/old_agents/writer.md new file mode 100644 index 0000000..e0dae2c --- /dev/null +++ b/.backup/old_agents/writer.md @@ -0,0 +1,24 @@ +--- +description: "Drafts and updates technical documentation, architecture guides, and API specs." +mode: "subagent" +model: "deepseek/deepseek-v4-flash" +permission: + edit: allow + bash: deny +options: + thinking: + type: disabled +--- + +You are a technical writer who communicates complex software architectures with pinpoint precision. + +### Deliverables + +- Clear, architectural READMEs, system setup guides, and internal team runbooks. +- Clean API documentation maps outlining payload shapes, status codes, and endpoint routing. + +### Style Guide + +1. Keep prose technical, precise, and highly scannable. +2. Avoid generic corporate or marketing phrases. Lead with the technical details immediately. +3. Maximize the use of Markdown tables, bulleted structural lists, and code blocks for readability. diff --git a/.opencode/skills/bugfix/SKILL.md b/.backup/skills/bugfix/SKILL.md similarity index 100% rename from .opencode/skills/bugfix/SKILL.md rename to .backup/skills/bugfix/SKILL.md diff --git a/.opencode/skills/e2e-repair/SKILL.md b/.backup/skills/e2e-repair/SKILL.md similarity index 100% rename from .opencode/skills/e2e-repair/SKILL.md rename to .backup/skills/e2e-repair/SKILL.md diff --git a/.opencode/skills/feature-pipeline/SKILL.md b/.backup/skills/feature-pipeline/SKILL.md similarity index 100% rename from .opencode/skills/feature-pipeline/SKILL.md rename to .backup/skills/feature-pipeline/SKILL.md diff --git a/.opencode/skills/fix-pipeline/SKILL.md b/.backup/skills/fix-pipeline/SKILL.md similarity index 100% rename from .opencode/skills/fix-pipeline/SKILL.md rename to .backup/skills/fix-pipeline/SKILL.md diff --git a/.backup/skills/spec-pipeline/SKILL.md b/.backup/skills/spec-pipeline/SKILL.md new file mode 100644 index 0000000..3f6eed4 --- /dev/null +++ b/.backup/skills/spec-pipeline/SKILL.md @@ -0,0 +1,25 @@ +--- +name: spec-pipeline +description: "Executes the full dev-to-docs pipeline: Developer -> Tester -> Writer." +--- + +## What I do + +I orchestrate a sequential feature implementation and verification pipeline: + +1. **Architect**: Explores codebase, asks clarifying questions, and drafts the spec. *Waits for user approval before handoff.* +2. **Developer**: Implements the feature based on the spec. +3. **Tester**: Runs full unit test suites; repairs failures if found. +4. **Writer**: After updating README and API docs, review `docs/tech_debt_and_optimizations.md` for formatting consistency and ensure no duplicate entries exist. + +## Execution Rules + +- **Architect Gate**: Stop after Phase 3 and wait for user approval on the spec before calling `@developer`. +- **Tester Repair Limit**: Allow `@tester` a maximum of 2 auto-repair attempts for failing test suites. If tests still fail after 2 attempts, hand the error context back to `@developer` to fix the underlying implementation. +- Stop and ask the user for clarification if any step fails or is ambiguous. +- Use `@` mentions to trigger subagents sequentially. +- Pass context from each completed stage to the next stage. + +## When to use me + +Invoke me when you are ready to begin a new feature/fix or when the Architect has finished a specification. \ No newline at end of file diff --git a/.opencode/agents/architect.md b/.opencode/agents/architect.md index 6ee4544..fedeb6b 100644 --- a/.opencode/agents/architect.md +++ b/.opencode/agents/architect.md @@ -1,6 +1,6 @@ --- name: architect -description: "Defines system requirements, data contracts, and architectural blueprints." +description: "Defines system requirements and technical specifications for Godot features." mode: subagent model: "deepseek/deepseek-v4-pro" permission: @@ -8,156 +8,21 @@ permission: bash: deny options: reasoningEffort: high - thinking: - type: enabled --- -You are the Lead Systems Architect. You are responsible for ensuring all subagents work from a shared technical specification. Understand the codebase deeply, identify and ask about underspecified details, design elegant architectures +You are the Lead Systems Architect. Refer to `AGENTS.md` for project directory standards and coding rules. -## Core Responsibilities +## Responsibilities -- **Specification:** Create and maintain `specs/` markdown files for new features. -- **Clarity:** Understand before acting β€” Read and comprehend existing code patterns first. -- **Contracts:** Define API payload shapes (JSON schemas), Python type hints, and Vue prop interfaces before any code is written. -- **Decision Log:** Maintain a `decisions.md` file to track _why_ certain architectural choices were made (e.g., why you chose a specific Vue state management pattern). +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`. -## 🎯 Architectural Philosophy +## Workflow -- **Semantic Control Node Nesting:** Choose the correct Control node based on structural behavior (e.g., `MarginContainer` for padding, `VBoxContainer`/`HBoxContainer` for layout alignment). Never manually hardcode pixel offsets for positioning dynamic elements. -- **Separation of Concerns (Model-View-Controller/Presenter):** View nodes (UI layout) only handle visual states, animations, and input capture. Data state and processing logic must live in detached classes or core business logic scripts. -- **Signal-Driven Data Flow:** UI components must remain modular. Children emit signals to notify changes (e.g., button clicked, input text submitted). Parent containers catch these signals and pass structured data upstream. -- **Responsive and Adaptive:** All UI systems must handle dynamic font sizing, localizable text expansions, and varying aspect ratios gracefully without layout breaking. - -## πŸ› οΈ Stack & Pattern Specifications - -- **Engine & Language:** Godot 4.x (GDScript) -- **Layout Engine:** Godot Anchors, Containers, and Control sizing flags (`SIZE_EXPAND_FILL`). -- **Styling & Theming:** Strict adherence to Godot's global and localized `Theme` resources. Modifying structural visual properties (fonts, colors, panel styles) directly inside specific node properties is forbidden; use Theme overrides or custom Type Variations instead. -- **Interaction Patterns:** Input handling must strictly leverage Godot's built-in GUI input system (`_gui_input` and `_unhandled_input`) and focus neighbor navigation (`focus_next`, `focus_neighbor_left`) for accessibility (keyboard/gamepad support). - -## πŸ“ Directory Structure Standards - -Enforce a clean, component-and-view-based directory layout. Keep views, their custom sub-components, and themes localized to their features. - -```text -res:// -β”œβ”€β”€ .godot/ -β”œβ”€β”€ assets/ # Shared global assets -β”‚ β”œβ”€β”€ fonts/ # Variable/Static TTF or WOFF2 fonts -β”‚ └── themes/ # Global .theme files and StyleBox Flat/Texture resources -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ core/ # Core application systems (ConfigManager, NavigationRouter) -β”‚ β”œβ”€β”€ shared/ # Reusable UI Atoms (CustomButtons, Tooltips, Modals) -β”‚ └── views/ # Distinct app views/screens -β”‚ β”œβ”€β”€ dashboard/ -β”‚ β”‚ β”œβ”€β”€ dashboard_view.tscn -β”‚ β”‚ β”œβ”€β”€ dashboard_view.gd -β”‚ β”‚ └── components/ # View-specific sub-layouts -β”‚ └── settings/ -└── test/ # UI and integration automation tests -``` - -## ✍️ Coding Rules & Technical Guardrails - -### ❌ Prohibited Practices (Never Do These) - -- **No `get_node("../../OtherPanel")`:** Hardcoded relative paths instantly break when UI hierarchies change or get nested inside new scroll containers. -- **No Hardcoded Font Sizes or Visual Styles inside Nodes:** Individual UI nodes must not manually customize their themes unless it is a highly localized, explicit project requirement. -- **No Blocking Operations on Main UI Thread:** Heavy parsing, file operations, or network calls must be executed asynchronously via threads or HTTPRequest nodes to avoid micro-stutters in the UI. -- **No Loose Configuration Strings:** Tab names, menu IDs, or event paths must use named `const` constants, dictionaries, or `enums`. - -### βœ… Mandatory Practices (Always Do These) - -- **Strict Static Typing:** Every single variable, method argument, and return type must be strongly typed (e.g., `func update_view(data: Dictionary) -> void:`). -- **Respect Focus Grab:** Always explicitly script focus management for accessibility. When a view or modal opens, use `grab_focus()` on the primary interactive element. -- **Localization-Ready Strings:** All visible text strings must pass through the `tr()` translation function or use the built-in localization features of the engine. -- **Pivot Offset Handling:** When designing custom scale/rotation animations for Control nodes via `Tween`, ensure the `pivot_offset` is dynamically or explicitly configured to prevent UI elements from scaling from random corners. - -## Working discipline - -These bias toward caution over speed β€” use judgment on trivial tasks. - -- **Think before acting** β€” state assumptions; if the request has more than one reading, surface them instead of silently choosing; if a simpler path exists, say so. -- **Simplicity first** β€” the minimum that solves the problem; no speculative features, abstractions, configurability, or handling of impossible cases. -- **Surgical changes** β€” touch only what the task needs; do not refactor or restyle adjacent code; match existing style; clean up only the orphans your change created, and mention unrelated dead code rather than deleting it. -- **Goal-driven** β€” turn the task into a concrete success check and iterate until it passes. - -You must never combine phases 3–5 in a single response. Always stop after presenting questions or choices and wait for the user’s next message. - -## Phase 1: Discovery - -Goal: Understand what needs to be built. - -1. Create a todo list covering all seven phases. -2. If the feature is unclear, ask the user: - - What problem are they solving? - - What should the feature do? - - Any constraints or requirements? -3. _CRITICAL_ Summarize your understanding and confirm with the user before proceeding. - -## Phase 2: Codebase exploration - -Goal: Understand relevant existing code at both high and low levels. - -1. Dispatch 2–3 `code-explorer` sub-tasks in parallel. Each should: - - Trace through the code comprehensively, focusing on abstractions, architecture, and control flow. - - Target a different aspect (similar features, high-level architecture, UX, extension points). - - Return a list of 5–10 key files to read. -2. After they return, read every file they identified to build deep understanding. -3. Present a comprehensive summary of findings and patterns to the user. - -## Phase 3: Clarifying questions - -**This is a mandatory stop point.** - -- Output a numbered list of questions. -- **Do NOT include any architecture, code, or spec content in this response.** -- End your response with: β€œPlease reply with answers to these questions before I proceed.” - -If the user says "whatever you think is best", make your recommendation explicit and get confirmation. - -## Phase 4: Architecture design - -**This is a mandatory stop point.** - -- Present 2–3 approaches with trade‑offs. -- State your recommendation. -- **Do NOT choose or implement anything.** -- End with: β€œWhich approach do you prefer? Reply with your choice.” - -### πŸ“ Tech-Debt & Future-Optimization Logging - -During architecture design, if you identify: - -- Trade-offs that will cause friction later (e.g., "we're using a quick O(nΒ²) loop here because the list is small now, but it will scale poorly"). -- Obvious refactoring opportunities that are out of scope (e.g., "this legacy singleton should be replaced with an event bus"). -- Missing tests or error handling that are not critical for the current feature. - -**Append** a new entry to `docs/tech_debt_and_optimizations.md` using this format: - -```markdown -## [YYYY-MM-DD] - [Feature Name] - -- **Debt**: [Clear description] -- **Impact**: [What breaks/degrades if ignored] -- **Suggested Fix**: [Actionable improvement] -- **Context**: [Link to spec file or relevant code path] - -## Phase 5: Create Spec - -**Do not proceed until the user explicitly approves the chosen approach.** - -- Once they approve, you may write the spec in the next turn. - -## Phase 6: Summary - -Goal: Document what was accomplished. - -1. Mark all todos complete. -2. Save spec to specs/[feature-name].md -3. Summarize: - - What was built - - Key decisions made - - Files modified - - Suggest running the @feature-pipeline skill to begin implementation -``` +1. **Scope & Clarify:** Ask up to 3 high-impact questions if requirements are ambiguous. +2. **Spec Creation:** Output a minimal specification containing: + - 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. diff --git a/.opencode/agents/developer.md b/.opencode/agents/developer.md index 13c7a49..e35edff 100644 --- a/.opencode/agents/developer.md +++ b/.opencode/agents/developer.md @@ -1,102 +1,26 @@ --- -name: Developer -description: Implements core application features across Godot. +name: developer +description: Implements GDScript logic and scene adjustments. mode: subagent model: "deepseek/deepseek-v4-pro" -steps: 60 permission: edit: allow bash: allow options: reasoningEffort: medium - thinking: - type: enabled --- -# Role: Godot 4 Engine & GDScript Reviewer Agent +You are a Godot 4 / GDScript 2.0 implementation developer. Adhere to coding standards in `AGENTS.md`. -## 1. Core Objective +## Rules -You are an expert Godot 4 game developer and code reviewer. Your purpose is to analyze GDScript code, scene structures, and project configurations to ensure high performance, clean architecture, and adherence to Godot best practices. - -## 2. Technical Context (Godot 4.x) - -- **Language**: GDScript 2.0 (Godot 4+ static typing, lambdas, properties). -- **Architecture**: Node-based, composition over inheritance, signal-driven communication. -- **Paradigm**: "Provide hooks, call down, signal up." - -## 3. Review Priority Matrix - -1. **Correctness**: Bugs, null references, wrong API usage (e.g., Godot 3 vs Godot 4 differences). -2. **Performance**: Memory leaks, redundant `_process` loops, unoptimized physics/queries. -3. **Architecture**: Tight coupling, missing encapsulation, misuse of singletons (Autoloads). -4. **Style**: Adherence to the official GDScript Style Guide. - -## 4. Key Godot-Specific Inspection Rules - -### βš™οΈ Memory & Node Lifecycle - -- Ensure dynamically created nodes are freed using `queue_free()` instead of `free()`. -- Check that `is_instance_valid()` is used when referencing potentially freed nodes. -- Flag missing `@onready` annotations for nodes fetched via `$Path` or `get_node()`. - -### πŸ“‘ Signals & Decoupling - -- Verify signals are connected using the Godot 4 syntax: `emitter.signal_name.connect(receiver.method_name)`. -- Discourage child nodes from directly calling parents; enforce `signal up` architecture. -- Check for disconnected signals or potential memory leaks from lambdas bound to short-lived objects. - -### πŸš€ Performance Optimization - -- Flag heavy logic inside `_process(delta)` or `_physics_process(delta)` that could be event-driven. -- Ensure physics queries and movement use `_physics_process` and `move_and_slide()` correctly. -- Recommend `StringName` (e.g., `&"node_name"` or `&"signal_name"`) for frequent lookups or animations. -- Check that `callable` arrays or loops are optimized. - -### πŸ“ GDScript 2.0 Style Guide - -- Enforce static typing wherever possible: `var health: int = 100` or `func take_damage(amount: float) -> void:`. -- Verify snake_case for variables/functions, PascalCase for class names, and UPPER_CASE for constants. -- Check for proper use of `@export` annotations for inspector variables. -- Verify syntax using the project's Godot 4.7 console binary: - `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\Godot4\stickman" --quit` - (project-wide parse/import check). For a single script: +- **Targeted Edits Only:** Do NOT output full unchanged files. Output only modified functions, concise diffs, or specific node configurations. +- **Static Typing:** Enforce explicit type hints for all signatures and variables. +- **Decoupling:** Follow "call down, signal up". +- **Validation:** Run syntax verification command when finished: `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://path/to/script.gd" --path "C:\Godot4\stickman"` -## 5. Response Output Format +## Output Format -For every review, structure your response as follows: - -### πŸ” Summary of Code / System - -_Brief 1-2 sentence overview of what the reviewed component does._ - -### 🚨 Critical Issues (Bugs & Crashes) - -- **Issue**: [Describe bug/crash] -- **Fix**: [Describe fix or provide code snippet] - -### ⚑ Performance & Architecture Improvements - -- **Current**: [Describe bottleneck/tight coupling] -- **Recommendation**: [Describe optimized approach] - -### πŸ”§ Runtime Tech-Debt Discovery - -While writing code, if you encounter: - -- Ugly workarounds forced by existing code. -- Performance pitfalls you have to code around. -- Unused imports, dead code, or outdated comments that are confusing. - -**Immediately** append to `docs/tech_debt_and_optimizations.md` with the same format. - -### 🎨 Style & Readability Refactors - -- _Bullet points pointing out missing type hints, naming violations, or dead code._ - -### πŸ› οΈ Refactored Code - -```gdscript -# Provide the complete, clean, optimized version of the script here -``` +- **Summary:** 1-2 lines on changes made. +- **Code Edits:** Show only modified snippet/function blocks with surrounding context lines. diff --git a/.opencode/agents/writer.md b/.opencode/agents/writer.md index e0dae2c..6ef94ba 100644 --- a/.opencode/agents/writer.md +++ b/.opencode/agents/writer.md @@ -1,5 +1,5 @@ --- -description: "Drafts and updates technical documentation, architecture guides, and API specs." +description: "Drafts and updates technical documentation, GDScript APIs, and architecture specs." mode: "subagent" model: "deepseek/deepseek-v4-flash" permission: @@ -10,15 +10,15 @@ options: type: disabled --- -You are a technical writer who communicates complex software architectures with pinpoint precision. +You are a technical writer for Godot 4 / GDScript projects. You document game architectures, class APIs, and node systems with pinpoint precision. ### Deliverables -- Clear, architectural READMEs, system setup guides, and internal team runbooks. -- Clean API documentation maps outlining payload shapes, status codes, and endpoint routing. +- **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`. -### Style Guide +### Execution Rules -1. Keep prose technical, precise, and highly scannable. -2. Avoid generic corporate or marketing phrases. Lead with the technical details immediately. -3. Maximize the use of Markdown tables, bulleted structural lists, and code blocks for readability. +1. **Targeted Edits Only:** Touch only the specific markdown sections affected by recent changes. Never output full unchanged files. +2. **Scannable & Technical:** Lead directly with code blocks, tables, and structured lists. Eliminate marketing fluff or generic introductions. diff --git a/.opencode/skills/spec-pipeline/SKILL.md b/.opencode/skills/spec-pipeline/SKILL.md index 3f6eed4..9561436 100644 --- a/.opencode/skills/spec-pipeline/SKILL.md +++ b/.opencode/skills/spec-pipeline/SKILL.md @@ -1,25 +1,23 @@ --- name: spec-pipeline -description: "Executes the full dev-to-docs pipeline: Developer -> Tester -> Writer." +description: "Lean implementation loop: Architect -> Developer -> User Verification." --- -## What I do +## Execution Logic -I orchestrate a sequential feature implementation and verification pipeline: +1. **Phase 1: Architecture (Optional for small edits)** + - `@architect` defines signatures, node structures, or spec updates in `specs/[feature-name].md`. + - **STOP:** Wait for user approval on design choices before invoking developer. -1. **Architect**: Explores codebase, asks clarifying questions, and drafts the spec. *Waits for user approval before handoff.* -2. **Developer**: Implements the feature based on the spec. -3. **Tester**: Runs full unit test suites; repairs failures if found. -4. **Writer**: After updating README and API docs, review `docs/tech_debt_and_optimizations.md` for formatting consistency and ensure no duplicate entries exist. +2. **Phase 2: Implementation & Engine Validation** + - `@developer` updates code and scenes using targeted diffs/snippets. + - `@developer` runs headless syntax verification: + `& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --check-only --script "res://path/to/script.gd" --path "C:\Godot4\project"` + - If static check fails, `@developer` fixes the code immediately. -## Execution Rules +3. **Phase 3: User Hand-off (Stop)** + - Present the changes to the user to test inside the Godot Editor. + - Do NOT update READMEs or documentation yet. -- **Architect Gate**: Stop after Phase 3 and wait for user approval on the spec before calling `@developer`. -- **Tester Repair Limit**: Allow `@tester` a maximum of 2 auto-repair attempts for failing test suites. If tests still fail after 2 attempts, hand the error context back to `@developer` to fix the underlying implementation. -- Stop and ask the user for clarification if any step fails or is ambiguous. -- Use `@` mentions to trigger subagents sequentially. -- Pass context from each completed stage to the next stage. - -## When to use me - -Invoke me when you are ready to begin a new feature/fix or when the Architect has finished a specification. \ No newline at end of file +4. **Phase 4: Documentation (Triggered manually by user)** + - Only call `@writer` after the user confirms: _"Feature verified and working."_ diff --git a/AGENTS.md b/AGENTS.md index 3fbadb5..3ab197a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -228,7 +228,8 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and `BEND_JOINT_BONE_PATHS` (each joint β†’ its lower `Bone2D` NodePath relative to `Skeleton2D`), `PROFILE_FLAGS` (per-profile `flip_bend_direction` sets), `Z_ORDER_BY_PROFILE` (per-profile `Body/*` draw-order tables, back-to-front). Exports: `facing_profile: FacingProfile` (default - `FORWARD`, a preset whose setter writes the four per-joint vars + reorders `Body/*`) and an + `FORWARD`, a preset whose setter writes the four per-joint vars, reorders `Body/*`, and + applies a whole-rig Y-axis mirror for LEFT β€” `Master.scale.x = -1`; RIGHT/FORWARD `(1,1)`) and an `@export_group("Bend Direction")` of four `@export_enum("Normal","Inverted")` vars `left_arm_bend`/`right_arm_bend`/`left_leg_bend`/`right_leg_bend` (defaults NORMAL/INVERTED/INVERTED/NORMAL = FORWARD). Recovery exports: `rest_timeout` (2.0 s), @@ -245,6 +246,25 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and `_nodes_ready`; a `_nodes_ready` guard makes pre-`_ready` setters store-only (robust against setter timing during `PackedScene.instantiate()`). Null-guards + `push_warning` prefixed `"StickmanRig: "` throughout; never crashes. + - **Whole-rig Y-axis mirror (replaces `_apply_head_flip()`, 2026 design change):** Facing LEFT + is applied as a **whole-rig Y-axis mirror** β€” `Master.scale.x = -1` (RIGHT/FORWARD β†’ `(1,1)`) β€” + so the head AND body mirror together and face the correct direction. The old `_apply_head_flip()` + and the `Body/Head.scale.x` mirror are **removed** (the Head Pivot node's driver transform is + untouched). Per-joint `flip_bend_direction` flags (`PROFILE_FLAGS`) and `Z_ORDER_BY_PROFILE` are + **kept provisionally** (unchanged): the mirror reflects the whole skeleton + `IK_Targets` + + 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`) + 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 + `_apply_head_lookat_mirror_mode()` (called from `_apply_profile()`) **disables** the head + `SkeletonModification2DLookAt` when facing LEFT and pins the head bone rotation to the FORWARD + canonical aim (Ο€); `_pin_mirrored_head_rotation()` re-asserts that pin each `_physics_process` + frame while ANIMATED/RECOVERING so recovery's stack re-arm can't let LookAt flip the bone. + RIGHT/FORWARD re-enable the LookAt. Consequence: interactive head-aiming while facing LEFT is + intentionally static. - **Phase 10 ragdoll state system:** `StickmanRig` owns a reversible `ANIMATED ⇄ RAGDOLL` physics mode switch plus a `RECOVERING` stand-up state. `enum RigState { ANIMATED, RAGDOLL, RECOVERING }`, `var state: RigState` (default `ANIMATED`), `signal state_changed(new_state: @@ -280,18 +300,31 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and `rest_timeout` (and a `STABILIZATION_DELAY` hold) with `auto_recover` on it calls `_start_recovery()`. `_start_recovery()` (also `request_recovery()`, no-op unless in RAGDOLL) captures the 10 bodies' rig-local `{pos, rot, half}` into `_captured_pose` (`half` = - each capsule's half-length from build-time metadata), `_destroy_ragdoll()`s, + each capsule's half-length from build-time metadata) plus a landing anchor + (`_captured_landing_center` = the ragdoll torso's world center, `_captured_ground_y` = + `torso.center.y + RAGDOLL_TORSO_RADIUS`), `_reanchor_root_to_landing()`s (translates the rig + root so the standing figure's **feet** sit on the ground at the landing X β€” `feet = + (_captured_landing_center.x, _captured_ground_y)`, `new_root = feet + FOOT_OFFSET` β€” and + re-bases the captured rig-local positions by the root shift; the figure stands up **in place, + on the ground**, where the ragdoll landed, since the ragdoll bodies live under a world sibling + and the root never moved while it fell), `_destroy_ragdoll()`s, sets `state = RECOVERING` + emits, then `_snap_skeleton_to_pose()` β€” a **marker-driven** snap writing `IK_Targets/Torso.position`/`.rotation`, `IK_Targets/Head.position`, and the 4 limb markers (never the slaved `Torso` Bone2D), re-showing - `Body/*` before re-enabling the IK stack so TwoBoneIK solves toward the end-effectors. Snap + `Body/*`, then calling `_rearm_ik_stack()` β€” a defensive re-setup (re-sets up the + `SkeletonModificationStack2D` when it reports `!get_is_setup()`, re-asserts `enabled = true` + and `Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker's + `RemoteTransform2D` update-position/rotation/scale flags) so the stack reliably resumes solving + toward the end-effectors after a disableβ†’enable toggle. Runtime diagnosis of the re-arm is + gated behind `const DEBUG_RECOVERY := false` (off by default; `_recovery_dbg()` traces stack + enabled/setup/internal + bone-following state). Snap geometry: the ragdoll capsules span joint originβ†’tip along their +X, so the **hip** is derived as `torso.pos βˆ’ spine_dirΒ·half` and the wrist/ankle targets as `lower_body.pos + dirΒ·half`; the Torso marker rotation subtracts the Torso Bone2D's `bone_angle` (bone world angle = marker rotation + bone_angle β€” copying the body rotation directly would slam the skeleton βˆ’90Β° and lay it flat). `_play_stand_up()` then tweens the 6 markers **directly** from their captured values to - `STAND_POSE` over `STAND_UP_DURATION` (sine ease-in-out, `_tween_markers_to()`); the baked + `STAND_POSE` over `STAND_UP_DURATION` (2.0 s, sine ease-in-out, `_tween_markers_to()`); the baked `"stand_up"` animation is **not** played (a fixed first keyframe can never match an arbitrary ragdoll rest pose, so recovery starts from wherever the snap left the markers). On tween finish `_on_stand_up_finished()` re-enables IK, re-shows `Body/*`, sets `state = ANIMATED`, @@ -335,8 +368,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and unreachable warning**; the debug trace now carries a `mode=nav|direct` field. Off-by-default diagnostics: `DEBUG_WALK` + `_walk_dbg()` (rig) and `DEBUG_STAGE` + `_stage_dbg()` (sandbox_stage). Walking is kinematic - (`global_position.move_toward`); movement composes with the `walk_left`/`walk_right` in-place limb - animations (each has a discrete `.:facing_profile` track). + (`global_position.move_toward`); movement composes with the canonical `walk_right` in-place limb + animation, played for every direction β€” X-mirrored by the rig root for LEFT (facing is set + explicitly by `walk_to()`/`set_facing_profile()`; `walk_left` is no longer used at runtime and + the animation `.:facing_profile` tracks are neutralized/removed). - **Phase 4 triggers:** the `arrived` signal gained a `target: Vector2` payload (emits `_walk_target_feet`) so the stage can match waypoints for `arrived_at_waypoint` rules; new `enqueue_reactive(actions: Array[Dictionary]) -> void` appends reactive actions to the action @@ -356,7 +391,7 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and (run manually with `master_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` = 0.8, `loop_mode = LOOP_NONE`) into the + (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. @@ -476,8 +511,10 @@ assembled in a "Whole Stickman" preview that supports translation, rotation, and `PlaybackState {STOPPED, PLAYING, PAUSED}` via the button handlers + the `animation_finished` signal (guarded by `_loop`) β€” no polling in `_process`. Changing the dropdown selection stops playback; `_free_current_rig()` clears `_anim_player`, the dropdown, `_selected_animation`, - and state. Playing `walk_right` also sets the rig's `facing_profile` via its animation track β†’ - export setter β†’ the existing `_on_facing_profile_changed` handling (menu `[√] ` + redraw). No + and state. The animation `.:facing_profile` tracks are **neutralized/removed** β€” facing is now + set explicitly via `set_facing_profile()` / `walk_to()` (which sets LEFT/RIGHT from the walk + direction and root-mirrors for LEFT), so playing `walk_right` no longer flips the rig's profile + through an animation track. No 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()`). diff --git a/BUGS.md b/BUGS.md index 9b1d4f6..8f123eb 100644 --- a/BUGS.md +++ b/BUGS.md @@ -123,3 +123,34 @@ When applying .stk v1.4 data to master*rig.tscn, shapes are severely distorted:G - **Symptom:** in Play, the stickman advances one `move_toward` step then stops; the walk never reaches its target. - **Root cause:** `_update_walking` checked `is_navigation_finished()` before the map had synchronized. An unsynced `NavigationAgent2D` (map iteration id `0`) reports an empty, already-finished path, so the walk was ended after a single step. Additionally, `get_current_navigation_path()` alone never triggers a path computation β€” only `get_next_path_position()` forces the agent's internal `_update_navigation()` to re-query the map for a fresh target, so the empty-path guard would misjudge an off-mesh target as "unreachable" immediately. - **Fix:** (1) defer all nav reads until `map_get_iteration_id(...) != 0`; (2) call `get_next_path_position()` **before** the empty-path / finished checks so the path is actually computed; (3) make walking **hybrid** β€” an on-mesh target follows the nav path (`_walk_mode = "nav"`), while an off-mesh/unreachable target (path empty **or** `is_target_reachable()` false) switches to **direct straight-line steering** toward the clicked waypoint (`_walk_mode = "direct"`, root target = waypoint + `FOOT_OFFSET`), so the stickman always reaches the waypoint the user clicked; (4) **no** `push_warning` for an off-mesh waypoint (a supported case β€” logged only via `_walk_dbg`); the original "warn + finish in place" policy was itself reported as "stickman stands still with a waypoint" and was superseded by this DIRECT branch; (5) `_finish_walk(reason: String)` internal param for the debug trace. The `_walk_path_grace` counter is **removed** β€” the map-sync guard + forced path query replace it. + +## 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. + +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. +2. **"Edit Queue…" should not appear when the stickman has no actions (spec/design).** The Direct action popup (`sandbox_stage.gd:1389`) and the stickman right-click menu (`:1475`) add "πŸ“‹ Edit Queue…" unconditionally; no gating is specified in the plan/spec. Fix: hide/disable the item when `rig.get_queue().is_empty()`, refreshed on `about_to_popup`. + - **FIXED (2026-09-04):** the popup items are now gated via an `about_to_popup` refresh β€” "πŸ“‹ Edit Queue…" is hidden/disabled when `rig.get_queue().is_empty()`. +3. **"Edit Rules…" should not appear when an object has no rules (spec/design).** Same unconditional add (`sandbox_stage.gd:1390`, `:1476`); no gating specified. Fix: hide/disable the item when no rule has `trigger.source == rig.get_instance_id()`. + - **FIXED (2026-09-04):** "⚑ Edit Rules…" is now gated via the same `about_to_popup` refresh β€” hidden/disabled when no rule has `trigger.source == rig.get_instance_id()`. +4. **"Edit Rules" shows no rules (implementation defect).** `rule_panel.gd` `refresh()` has the identical missing `_list.add_child(panel)` as bug 1. Fix: add `_list.add_child(panel)` in `_make_row()`. + - **FIXED (2026-09-04):** rows are now parented into the list container (`_list.add_child(panel)` in `_make_row()`), so the rule rows render. +5. **Font styles/sizes not configurable in `sandbox_theme.json` (spec/design).** The `fonts` block only carries sizes + font paths; no style (bold/italic) and no keys for the Phase 3c panels/editors (which currently get no font override at all). `action_popup_emoji_size` is a dead key. Fix: extend the schema (per-widget `{size, bold, italic}` + Phase 3c sizes/style flags) and add `apply_font(...)` to `QueuePanel`/`RulePanel`/`ActionEditor`/`RuleEditor`; consume `action_popup_emoji_size`. + - **FIXED (2026-09-04):** the `fonts` schema is extended (per-widget `{size, bold, italic}` object form + Phase 3c size/style flags); the Phase 3c panels/editors now accept `apply_font(...)`; `set_item_font_size` is replaced with `add_theme_font_size_override`; and stale bold/italic are reset on theme reload. + +## 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. + +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)`). +2. **Figure slides back to its pre-ragdoll position when it stands up after a fall (implementation defect).** The ragdoll bodies spawn under a world sibling (the rig's parent), so the rig root never moves while the figure falls; recovery captured only rig-local body poses and re-solved them against the root's original world position, so the stand-up tween dragged the standing figure back to where it was **before** the ragdoll instead of where it **landed**. Root cause: recovery never translated the rig root to the ragdoll's landing spot. Fix: `_capture_ragdoll_pose()` additionally records `_captured_hip_world` (the ragdoll torso's world-space hip β€” `torso.global_position βˆ’ spine_dirΒ·half`); `_start_recovery()` calls a new `_reanchor_root_to_landing()` which translates the rig root so `STAND_POSE`'s hip lands on that captured world hip, then re-bases the captured rig-local positions by the root shift β€” the figure stands up **in place** where the ragdoll landed. + - **FIXED (2026-09-04):** `_capture_ragdoll_pose()` records `_captured_hip_world`; `_start_recovery()` calls `_reanchor_root_to_landing()` before the snap/tween. +3. **Recovery buries the standing figure when the ragdoll lands lying flat (implementation defect).** The 2026-09-04 re-anchor fix anchored `STAND_POSE`'s hip onto a **spine-direction hip** (`torso.global_position βˆ’ spine_dirΒ·half`), which is only correct while the torso is upright. When the ragdoll lies flat, `spine_dir` is horizontal, so the derived hip sits at ground level (torso center.y + capsule radius β‰ˆ ground), and `_reanchor_root_to_landing()` places the standing hip at ground level β€” burying the standing feet ~363 px into the ground and making the stand-up tween read as a ground-level pivot instead of a lyingβ†’standing rise. Root cause: the landing anchor assumed an upright torso. Fix: replace `_captured_hip_world` with a **landing-center + ground-contact anchor** β€” `_captured_landing_center` = the ragdoll torso's world center, `_captured_ground_y` = `torso.center.y + RAGDOLL_TORSO_RADIUS`; `_reanchor_root_to_landing()` then sets `new_root = (landing_center.x, ground_y) + FOOT_OFFSET` so the standing figure's **feet** sit on the ground at the landing X. + - Also per user request, `STAND_UP_DURATION` is bumped **0.8 s β†’ 2.0 s** so the lyingβ†’standing stand-up tween is clearly visible before returning to `ANIMATED`. (Docs updated: README Β§17, AGENTS.md Phase 11.) + +## 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. +> **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/README.md b/README.md index b67e3cc..86a34ad 100644 --- a/README.md +++ b/README.md @@ -260,10 +260,10 @@ The factory is the intended runtime API: `StickmanFactory.spawn("res://stickmen/ - **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. - **IK drag** β€” click and drag any of the **6** `Marker2D` IK handles (`IK_Targets/Left_Hand`, `Right_Hand`, `Left_Leg`, `Right_Leg` flex the limb via TwoBoneIK; `IK_Targets/Torso` translates the whole rig rigidly via its `RemoteTransform2D`; `IK_Targets/Head` drives the head's `SkeletonModification2DLookAt` aim rotation) (Phase 9 Round 7). The rig self-enables its modification stack in `_ready()`. -- **Facing** β€” a `MenuButton` (leftmost in the top bar) applying a preset to the rig's **`StickmanRig`** exported `facing_profile`, which sets the rig's TwoBoneIK **Flip Bend Direction** flags: **Left** (arms normal, legs inverted), **Right** (arms inverted, legs normal), **Forward** (RightArm / LeftLeg inverted β€” the rig's default). The current profile is prefixed `[√] ` on the menu labels and persists across rig loads (Phase 9 Task 1, Task 4). +- **Facing** β€” a `MenuButton` (leftmost in the top bar) applying a preset to the rig's **`StickmanRig`** exported `facing_profile`, which sets the rig's TwoBoneIK **Flip Bend Direction** flags: **Left** (arms normal, legs inverted), **Right** (arms inverted, legs normal), **Forward** (RightArm / LeftLeg inverted β€” the rig's default). Facing **Left** now applies a **whole-rig Y-axis mirror** β€” `Master.scale.x = -1` (RIGHT/FORWARD β†’ `(1,1)`) β€” so the head **and** body mirror together and face the correct direction (this replaces the old head-only `Body/Head.scale.x = -1` mirror; `_apply_head_flip()` is removed). Two head-related fixes make the mirror stable: the head `RemoteTransform2D` (`Skeleton2D/Torso/Head/Pivot`) pushes the **full transform** (no `update_scale = false`), 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 when facing LEFT `_apply_head_lookat_mirror_mode()` **disables** the head `SkeletonModification2DLookAt` and pins the head bone to the FORWARD canonical aim (Ο€), re-asserted each physics frame by `_pin_mirrored_head_rotation()` while ANIMATED/RECOVERING (RIGHT/FORWARD re-enable the LookAt) β€” so interactive head-aiming is intentionally static while facing LEFT. The current profile is prefixed `[√] ` on the menu labels and persists across rig loads (Phase 9 Task 1, Task 4). - **Bend-direction toggle** β€” right-click an elbow or knee joint in the viewport to pop a context menu that inverts that joint's TwoBoneIK bend direction ("Invert Bend" β†’ "Normal Bend" and back). Only the 4 elbows/knees are targets (Phase 9 Task 1). -- **Body-part z-order** β€” the Facing profile also reorders the rig's `Body/*` visual part nodes (tree order = draw order), now owned by the rig's `StickmanRig._apply_body_z_order()`: **Forward** draws all limbs in front of the torso, **Left** tucks the left arm/leg pairs behind the torso (right pairs in front), **Right** tucks the right pairs behind; upper limbs sit behind lower limbs, far-side (behind-torso) arms draw behind the legs while near-side arms draw in front of them, and the **head is always frontmost** (Phase 9 Task 2, Task 4). -- **Animation** β€” a dropdown (populated per spawn from the rig's `AnimationPlayer.get_animation_list()`, `walk_right` pre-selected) plus **Play/Pause/Resume** (label swaps with playback state), **Stop**, and **Loop** (default ON, persists across loads) controls. The harness drives the rig's `AnimationPlayer` directly by node path (`ANIMATION_PLAYER_PATH`); loop writes `Animation.loop_mode` before play, and playback state is tracked via the button handlers + the `animation_finished` signal (no polling). The `AnimationTree` node remains an untouched placeholder. Playing `walk_right` also flips the rig's facing profile to Right via the animation's `facing_profile` track (Phase 9 Task 5). +- **Body-part z-order** β€” the Facing profile also reorders the rig's `Body/*` visual part nodes (tree order = draw order), now owned by the rig's `StickmanRig._apply_body_z_order()`: **Forward** draws all limbs in front of the torso, **Left** tucks the left arm/leg pairs behind the torso (right pairs in front), **Right** tucks the right pairs behind; upper limbs sit behind lower limbs, far-side (behind-torso) arms draw behind the legs while near-side arms draw in front of them, and the **head is always frontmost** (Phase 9 Task 2, Task 4). The per-profile z-order tables are **unchanged** and kept provisionally β€” an X-mirror does not affect depth (draw order). +- **Animation** β€” a dropdown (populated per spawn from the rig's `AnimationPlayer.get_animation_list()`, `walk_right` pre-selected) plus **Play/Pause/Resume** (label swaps with playback state), **Stop**, and **Loop** (default ON, persists across loads) controls. The harness drives the rig's `AnimationPlayer` directly by node path (`ANIMATION_PLAYER_PATH`); loop writes `Animation.loop_mode` before play, and playback state is tracked via the button handlers + the `animation_finished` signal (no polling). The `AnimationTree` node remains an untouched placeholder. The animation `.:facing_profile` tracks are **neutralized/removed** β€” facing is now set explicitly by `walk_to()`/`set_facing_profile()` (LEFT root-mirrors the rig and plays the same canonical `walk_right` clip mirrored; `walk_left` is no longer used at runtime), so playing `walk_right` no longer flips the profile through an animation track (Phase 9 Task 5). Debug overlay (a world-space `Node2D` `_draw()`): true bone **segments** drawn between each `Bone2D` origin and its Bone2D children (color-coded left cyan / right orange / central white, with a joint dot per bone), with limb leaf bones drawn out to their IK targets so the forearm/shin segments and wrist/ankle joints are visible (Phase 9 Round 2) and the **Head** leaf drawn along the bone's own direction (~90 px, since its IK target is a LookAt aim point, not a joint) (Phase 9 Round 3), when **Show Bones** is on; colored markers at the six IK targets β€” hands green, feet blue, head **yellow**, torso **magenta** (Phase 9 Round 7) β€” plus a semi-transparent yellow aim line from the Head bone to the head marker, when **Show IK Handles** is on. Each load frees the previous rig and spawns a fresh one. @@ -387,9 +387,9 @@ Ragdoll bodies spawn fully visible β€” the entry handoff is instant (the ragdoll **Rest detection (auto-recovery):** while `state == RAGDOLL`, `_update_rest_detection()` reads the **Torso** `RigidBody2D`. When it is sleeping **or** its linear velocity ≀ `REST_LINEAR_THRESHOLD` (**5.0 px/s** β€” tuned up from the original 0.1 because a soft-pinned ragdoll micro-jitters around ~0.5 px/s even when settled) and angular velocity ≀ `REST_ANGULAR_THRESHOLD` (0.1 rad/s), a `_rest_timer` accumulates; after `rest_timeout` (exported, default **2.0 s**) plus a `STABILIZATION_DELAY` (0.1 s) hold, and with `auto_recover` (exported, default **true**) enabled, recovery is triggered. `rest_timeout` and `auto_recover` are runtime-adjustable exports. -**Recovery (`_start_recovery()`, also `request_recovery()`):** the 10 bodies' rig-local `{pos, rot, half}` are captured into `_captured_pose` (`half` = each capsule's half-length, stored as build-time metadata), the ragdoll is destroyed, and `state = RECOVERING` is set + emitted. `_snap_skeleton_to_pose()` writes the `IK_Targets/Torso` position + rotation, `IK_Targets/Head`, and the 4 limb markers (never the slaved Torso `Bone2D`), re-shows `Body/*`, then re-enables the IK stack so TwoBoneIK solves toward the end-effectors. Snap geometry: the ragdoll capsules span joint originβ†’tip along their +X, so the **hip** is derived as `torso.pos βˆ’ spine_dirΒ·half` and the wrist/ankle targets as `lower_body.pos + dirΒ·half`; the Torso marker rotation subtracts the Torso `Bone2D`'s `bone_angle` (bone world angle = marker rotation + bone_angle β€” copying the body rotation directly would slam the skeleton βˆ’90Β° and lay it flat). The snap therefore reproduces the ragdoll's exact final pose (a "sitting" rest stays sitting). `_play_stand_up()` then tweens the 6 markers **directly** from their captured values to `STAND_POSE` over `STAND_UP_DURATION` (**0.8 s**, sine ease-in-out, `_tween_markers_to()`). The baked `stand_up` animation is **not** played β€” a fixed first keyframe can never match an arbitrary ragdoll rest pose (the earlier bridge-into-the-animation approach caused a visible jump from the captured pose to the animation's first frame), so the tween starts from wherever the snap left the markers. On tween finish, `_on_stand_up_finished()` re-enables IK, re-shows `Body/*`, sets `state = ANIMATED`, and emits. +**Recovery (`_start_recovery()`, also `request_recovery()`):** `_capture_ragdoll_pose()` records the 10 bodies' rig-local `{pos, rot, half}` into `_captured_pose` (`half` = each capsule's half-length, stored as build-time metadata) **plus** a landing anchor β€” `_captured_landing_center` (the ragdoll torso's world center) and `_captured_ground_y` (the torso's ground-contact line, `center.y + RAGDOLL_TORSO_RADIUS`). Because the ragdoll bodies spawn under a world sibling, the rig root never moves while the figure falls, so before destroying the ragdoll `_start_recovery()` calls `_reanchor_root_to_landing()`, which translates the rig root so the standing figure's **feet** sit on the ground at the landing X β€” `feet = (_captured_landing_center.x, _captured_ground_y)`, `new_root = feet + FOOT_OFFSET` β€” and re-bases the captured rig-local positions by that root shift. The figure stands up **in place, on the ground**, where the ragdoll landed β€” not anchoring its hip to the lying hip (a spine-direction hip sits at ground level when the torso lies flat, which would bury the standing feet ~363 px into the ground), and not back at its pre-ragdoll position. The ragdoll is then destroyed and `state = RECOVERING` is set + emitted. `_snap_skeleton_to_pose()` writes the `IK_Targets/Torso` position + rotation, `IK_Targets/Head`, and the 4 limb markers (never the slaved Torso `Bone2D`), re-shows `Body/*`, then calls `_rearm_ik_stack()` β€” a defensive re-setup (re-runs `setup()` when the stack reports `!get_is_setup()`, re-asserts `enabled = true` and `Skeleton2D.set_process_internal(true)`, and re-asserts the Torso marker `RemoteTransform2D`'s update-position/rotation/scale flags) so TwoBoneIK reliably resumes solving toward the end-effectors after a disableβ†’enable toggle; its runtime diagnosis is gated behind `const DEBUG_RECOVERY := false` (`_recovery_dbg()`, off by default). Snap geometry: the ragdoll capsules span joint originβ†’tip along their +X, so the **hip** is derived as `torso.pos βˆ’ spine_dirΒ·half` and the wrist/ankle targets as `lower_body.pos + dirΒ·half`; the Torso marker rotation subtracts the Torso `Bone2D`'s `bone_angle` (bone world angle = marker rotation + bone_angle β€” copying the body rotation directly would slam the skeleton βˆ’90Β° and lay it flat). The snap therefore reproduces the ragdoll's exact final pose (a "sitting" rest stays sitting). `_play_stand_up()` then tweens the 6 markers **directly** from their captured values to `STAND_POSE` over `STAND_UP_DURATION` (**2.0 s**, sine ease-in-out, `_tween_markers_to()`). The baked `stand_up` animation is **not** played β€” a fixed first keyframe can never match an arbitrary ragdoll rest pose (the earlier bridge-into-the-animation approach caused a visible jump from the captured pose to the animation's first frame), so the tween starts from wherever the snap left the markers. On tween finish, `_on_stand_up_finished()` re-enables IK, re-shows `Body/*`, sets `state = ANIMATED`, and emits. -**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). +**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). ### 18. Sandbox Stage Builder @@ -400,7 +400,7 @@ The stage is intentionally **extendable**: the spawn palette is registry-driven | 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). | +| `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). | @@ -413,7 +413,7 @@ The stage is intentionally **extendable**: the spawn palette is registry-driven | `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://sandbox_theme.json` | **Phase 4b** hand-editable styling defaults (font paths/sizes, grid snap default, mode accent colors). Loaded at `_ready()`; missing/malformed falls back to built-in constants. | +| `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`: @@ -466,7 +466,7 @@ The **Director Tool** (Phase 3a) turns the Sandbox Stage into a mini director's |---|---| | `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. | +| `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. | **Direct tool workflow (Edit):** @@ -638,13 +638,25 @@ A single committed, hand-editable JSON config drives sandbox font/size/color/gri "fonts": { "ui_font": "", "emoji_font": "", + "ui_font_bold": "", + "ui_font_italic": "", "action_popup_font_size": 24, "action_popup_emoji_size": 22, "assignment_badge_font_size": 20, "assignment_badge_radius": 9, "rule_label_font_size": 16, "status_pill_font_size": 16, - "tooltip_font_size": 18 + "tooltip_font_size": 18, + "queue_panel_font_size": 18, + "rule_panel_font_size": 18, + "action_editor_font_size": 18, + "rule_editor_font_size": 18, + "panel_row_font_size": 16, + "panel_title_font_size": 18, + "panel_title_bold": true, + "rule_label_bold": false, + "badge_bold": true, + "action_popup": { "size": 24, "bold": false, "italic": false } }, "grid": { "snap_size": 15.0 @@ -661,16 +673,23 @@ A single committed, hand-editable JSON config drives sandbox font/size/color/gri | Key | Default | Consumed by | |---|---|---| | `fonts.ui_font` / `fonts.emoji_font` | `""` (fallback font) | `res://` font paths; empty/missing β†’ `ThemeDB.fallback_font`. `emoji_font` is also pushed to `StageDirectorVisuals.emoji_font` and the director popups. | -| `fonts.action_popup_font_size` / `action_popup_emoji_size` | 24 / 22 | Font size override on the director action/trigger/rule popups. | +| `fonts.ui_font_bold` / `fonts.ui_font_italic` | `""` (fallback to `ui_font`) | **Phase 3c** style-variant font paths for bold/italic; empty β†’ `ui_font`. Bold/italic are realised via a dedicated `FontVariation` (e.g. `variation_embolden`, OpenType slant) when no separate file is configured. | +| `fonts.action_popup_font_size` / `action_popup_emoji_size` | 24 / 22 | Font size override on the director action/trigger/rule popups. **Phase 3c:** `action_popup_emoji_size` is now **consumed** β€” applied as the popup menu's emoji-glyph font size when a popup font is configured (`_apply_popup_theme()`). | +| `fonts.action_popup` (optional object) | `{ "size": 24, "bold": false, "italic": false }` | **Phase 3c** per-widget object form for the popups; when present, overrides the flat `action_popup_font_size` / bold keys for that widget. | | `fonts.assignment_badge_font_size` / `assignment_badge_radius` | 20 / 9 | Replaces `StageDirectorVisuals` `ICON_SIZE_PX` / `RULE_BADGE_RADIUS_PX` (and order-number size) via `set_style(cfg)`. | -| `fonts.rule_label_font_size` | 16 | Replaces `StageDirectorVisuals.RULE_LABEL_FONT_SIZE_PX`. | +| `fonts.rule_label_font_size` | 16 | Replaces `StageDirectorVisuals.RULE_LABEL_FONT_SIZE_PX`. **Phase 3c** `fonts.rule_label_bold` (default `false`) adds the bold flag; `fonts.badge_bold` (default `true`) bolds the action/trigger badges. | | `fonts.status_pill_font_size` / `tooltip_font_size` | 16 / 18 | The mode badge pill and the cursor-attached action tooltip. | +| `fonts.queue_panel_font_size` / `rule_panel_font_size` | 18 / 18 | **Phase 3c** font sizes for the Queue / Rule panel bodies; fall back to `action_popup_font_size`. | +| `fonts.action_editor_font_size` / `rule_editor_font_size` | 18 / 18 | **Phase 3c** font sizes for the Action / Rule editors; fall back to `action_popup_font_size`. | +| `fonts.panel_row_font_size` / `panel_title_font_size` | 16 / 18 | **Phase 3c** per-row summary/number label and panel title label sizes; `fonts.panel_title_bold` (default `true`) bolds panel titles. | | `grid.snap_size` | 15.0 | Initial default grid size for the Size spinbox (first run). | | `mode_colors.edit_accent` / `direct_accent` / `play_accent` | `#22c6ff` / `#ffb300` / `#33dd77` | Mode badge pill bg, the Direct viewfinder frame, the active mode-segment text, and tooltip border. | | `mode_colors.guide_line` | `#22c6ff` | The terrain drag-painting dashed guide line (`StagePlacementOverlay.guide_line_color`). | `StageDirectorVisuals.set_style(cfg)` applies the `fonts` keys onto instance vars (`badge_icon_size`, `badge_number_size`, `badge_radius`, `rule_label_font_size`) whose defaults equal the old constants, so behavior is unchanged when no theme is present. +**Phase 3c theming (font styles/sizes):** the Phase 3c widgets now accept an `apply_font(ui_font, emoji_font, sizes)` call β€” `QueuePanel`, `RulePanel`, `ActionEditor`, and `RuleEditor` (see Β§23) each expose it (mirroring `AssetSelector.apply_font`), and `SandboxStage._build_ui()` invokes it **after** `add_child(...)`. Per-widget sizes come from the `*_panel_font_size` / `*_editor_font_size` / `panel_row_font_size` / `panel_title_font_size` keys; bold/italic style flags (`panel_title_bold`, `rule_label_bold`, `badge_bold`) and a widget's optional `{size, bold, italic}` object form are applied via the `ui_font_bold` / `ui_font_italic` `FontVariation`-derived font. The `PopupMenu`s (rig/waypoint context) keep `_apply_popup_theme()`, extended to honour `action_popup_emoji_size` and the `action_popup` object form. Authoritative schema: `docs/phase_3c_editor_spec.md` Β§14. + **Director-context rule-connector refresh (bugfix):** `SandboxStage._on_transform_committed()` now calls `StageDirectorVisuals.mark_dirty()` after a move/rotate, so **translating a `TriggerArea` (or any rule-anchoring object) moves its dashed connector and ⚑/β†’ badges** to the new position on drag end. (Rule anchors were already computed live each `_draw()`; the missing `mark_dirty()` was leaving them stale because a `_draw()` never ran.) Deleting a referenced area already triggers `_cleanup_rules_for_nodes β†’ set_rules β†’ mark_dirty`. #### 21.4 Walk-waypoint arrival jitter fix (`StickmanRig`) @@ -726,6 +745,54 @@ The result: `mode` stays constant for the whole walk, exactly one `arrived` fire **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`. +### 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.** + +**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). | + +**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. + +**Entry points:** + +| Entry point | Gesture | Opens | +|---|---|---| +| Edit Queue | **Direct** action popup β†’ "πŸ“‹ Edit Queue…" **or** right-click a stickman β†’ "πŸ“‹ Edit Queue…" | `QueuePanel` for that rig | +| Edit Rules | **Direct** action popup β†’ "⚑ Edit Rules…" **or** right-click a stickman β†’ "⚑ Edit Rules…" | `RulePanel` filtered to that stickman as trigger source | +| Edit Trigger Rules | Right-click a waypoint β†’ "⚑ Edit Trigger Rules" | `RulePanel` filtered to rules whose `arrived_at_waypoint` trigger targets that waypoint | +| Edit a walk | Right-click a waypoint β†’ "✎ Edit this Walk", or a walk action's ✎ in the Queue panel | **Visual walk edit** β€” enters a `POSITION` target capture with the waypoint highlighted by a pulsing amber ring; the next stage click moves the target | +| Edit a rule | Click a rule's dashed **label** on stage β†’ consequence-only editor; or a rule's ✎ in the Rule panel β†’ **full** editor | `RuleEditor` (consequence / full) | +| Delete / reorder / add | βœ• / drag ≑ / "βž• Add Action|Rule" in the panels; "Add Rule" only when a source stickman panel is open | Confirmation dialog (delete / clear) then mutation | + +**Unified target capture (`CaptureKind`):** the ad-hoc "pending target" flows from Phase 3a/4 (walk target, rule trigger target, rule action actor) are unified into a single stage-click capture system on `SandboxStage`: `enum CaptureKind { NONE, WAYPOINT, AREA, PROP, STICKMAN, POSITION }`. `_begin_capture(kind, hint, on_resolve, on_cancel)` sets the current capture kind + status hint + cursor, routes the next left-click through `_resolve_capture()` (which hit-tests against the kind's expected target β€” a waypoint dot, `TriggerArea`, `PropBlock`, `StickmanRig`, or a snapped free position), then invokes the resolve callback; `Esc` runs the cancel callback (which re-pops the calling editor/panel). This replaces the previously scattered per-flow pending states. + +**Data flow (typical edit-a-queue-action):** Queue panel row ✎ β†’ stage `_on_queue_panel_edit_requested(index)` β†’ for a non-walk action opens `ActionEditor.open_edit(action)` (pre-filled) with the `QueuePanel` hidden; on OK `committed(action)` β†’ stage rewrites the rig queue via `remove_action`+`insert_action` β†’ `queue_changed` β†’ director visuals `mark_dirty()` β†’ waypoints/badges redraw; `_restore_queue_panel()` re-pops the (refreshed) panel. For a `walk_to`, editing routes through the visual **walk-edit** capture instead. Rule editing follows the same hide-editor β†’ capture (trigger target / actor) β†’ re-pop pattern. + +**Registry-driven extensibility (per plan Β§4/Β§10):** adding a new action type = appending one entry to `ActionRegistry.ACTION_TEMPLATES` (with a `params` spec); the `ActionEditor` type dropdown, param fields, and the panels' summaries all appear automatically. Adding a new trigger type = one entry to `TriggerRegistry.TRIGGER_TEMPLATES` (with a `target_type`); the `RuleEditor` trigger dropdown and the `RulePanel` trigger summaries pick it up. The action data model stays a `Dictionary`, so new keys are free (the editor shows editable fields for known `params` keys and ignores unknown ones gracefully). + +**Backward compatibility:** editing preserves each action's `type`/params and each rule's `id`; pre-existing (Phase 3a/4-authored) queues and rules load and display through the new panels unchanged. No queue/rule persistence changes β€” both remain in-memory across `EDIT ⇄ DIRECT ⇄ PLAY` toggles and reset on scene reload. + +**Verification:** new headless suite `tests/test_phase3c_editor.gd` (`extends SceneTree`, **253 assertions**) covering both registries (lookups, summaries, unknown-type handling), scene-shell instantiation, `ActionEditor` open_new/open_edit pre-fill + signal flow, `RuleEditor` full vs consequence modes + `get_action()` + id preservation, `QueuePanel`/`RulePanel` render + add/edit/delete/clear/reorder flows (mutating the rig queue via its API), `WaypointContext` item ids + trigger-rules enable/count, `StageDirectorVisuals.hit_test_waypoint_action()`, the `CaptureKind` begin/cancel/resolve + Esc priority, and backward compatibility of pre-existing queues/rules. Run: + +``` +& "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`. + ## File format (`.stk`) Files are UTF-8 JSON, pretty-printed with tab indentation. The format is versioned and designed to remain **backward/forward compatible** β€” new fields can be added without breaking older files. @@ -865,7 +932,7 @@ Behavior: | `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 }`. (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. 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` 0.8, `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). | +| `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. | @@ -874,7 +941,7 @@ Behavior: | `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.** `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) and live-updating exports. | | `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`). | -| `res://scripts/sandbox_stage.gd` | **Sandbox Stage Builder.** `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). Standalone staging scene run via **F6**; not wired into the editor. | +| `res://scripts/sandbox_stage.gd` | **Sandbox Stage Builder.** `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). 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"`. | @@ -883,11 +950,19 @@ Behavior: | `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://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. | +| `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`. | @@ -988,4 +1063,4 @@ BodyPartPanel.shape_selected() ---(bound to part_name)---> stickman_editor > **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 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 captures the 10 bodies' rig-local pose, destroys the ragdoll, sets `state = RECOVERING` + emits, snap-solves the skeleton via the **6** IK targets (`IK_Targets/Torso` pos+rot, `IK_Targets/Head`, 4 limb markers β€” never the slaved Torso `Bone2D`), deriving the **hip** from the torso capsule's bottom end (`pos βˆ’ dirΒ·half`) and the wrist/ankle targets from the lower-limb capsules' far ends (`pos + dirΒ·half`), with the Torso marker rotation subtracting the Torso bone's `bone_angle` (copying the body rotation directly would slam the skeleton βˆ’90Β° and lay it flat), then `_play_stand_up()` tweens the markers **directly** from their captured values to `STAND_POSE` over `STAND_UP_DURATION` (0.8 s, sine ease-in-out) β€” the baked `stand_up` animation is **not** played, because a fixed first keyframe can never match an arbitrary ragdoll rest pose (the earlier bridge-into-the-animation approach caused a visible jump); `_on_stand_up_finished()` then returns the rig to `ANIMATED`. `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`. +> **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 captures the 10 bodies' rig-local pose **plus a landing anchor** (`_captured_landing_center` = the ragdoll torso's world center, `_captured_ground_y` = torso `center.y + RAGDOLL_TORSO_RADIUS`), 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`) so the figure stands up **in place, on the ground**, where the ragdoll landed rather than sliding back to its pre-ragdoll root position (the earlier spine-direction-hip anchor sat at ground level for a lying torso and buried the standing feet), destroys the ragdoll, sets `state = RECOVERING` + emits, snap-solves the skeleton via the **6** IK targets (`IK_Targets/Torso` pos+rot, `IK_Targets/Head`, 4 limb markers β€” never the slaved Torso `Bone2D`), deriving the **hip** from the torso capsule's bottom end (`pos βˆ’ dirΒ·half`) and the wrist/ankle targets from the lower-limb capsules' far ends (`pos + dirΒ·half`), with the Torso marker rotation subtracting the Torso bone's `bone_angle` (copying the body rotation directly would slam the skeleton βˆ’90Β° and lay it flat), then `_play_stand_up()` tweens the markers **directly** from their captured values to `STAND_POSE` over `STAND_UP_DURATION` (2.0 s, sine ease-in-out) β€” the baked `stand_up` animation is **not** played, because a fixed first keyframe can never match an arbitrary ragdoll rest pose (the earlier bridge-into-the-animation approach caused a visible jump); `_on_stand_up_finished()` then returns the rig to `ANIMATED`. `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`. diff --git a/docs/phase9_task4_refactor_spec.md b/docs/phase9_task4_refactor_spec.md index 688790c..da19ae4 100644 --- a/docs/phase9_task4_refactor_spec.md +++ b/docs/phase9_task4_refactor_spec.md @@ -419,6 +419,37 @@ variant not referenced by the factory. The new `StickmanRig` script targets `mas | D8 | Rig enables its own mod stack in `_ready()` | Facing/bend are meaningless until the stack is live; the rig should self-enable at runtime (single consumer today always enables it). | | D9 | Rig exposes `get_bend_joint_global_position()` | Lets the harness drop `BEND_JOINT_BONE_PATHS`/`_bend_joint_bones` entirely; the rig owns the whole bend domain. | +## 9a. Round N β€” whole-rig Y-axis mirror (2026-09-05 design change) + +**Decision (user-approved):** facing LEFT is now rendered as a **whole-rig Y-axis mirror** β€” +`Master.scale.x = -1` (RIGHT/FORWARD β†’ `(1,1)`) β€” replacing the per-part/head mirroring. This flips +the head **and** body together so the figure faces the correct direction. + +- `_apply_head_flip()` and the `Body/Head.scale.x` mirror are **removed** (the Head Pivot node's + driver transform is untouched). +- `PROFILE_FLAGS` (per-joint `flip_bend_direction`) and `Z_ORDER_BY_PROFILE` are **kept + provisionally** (unchanged). The mirror reflects the whole skeleton + `IK_Targets` + mounted + `Body/*` geometry, but does **not** affect depth (draw order); whether the bend flags can be + collapsed to a single canonical set must still be verified empirically (a root mirror is not + provably reflection-invariant for TwoBoneIK's `flip_bend_direction` sign). +- **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` is no longer used at runtime. The animation `.:facing_profile` tracks are + neutralized/removed β€” facing is set explicitly by `set_facing_profile()` / `walk_to()`. + +**Follow-up (implemented, tested):** two head-related fixes were required to make the LEFT root +mirror render the head correctly. (1) The 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). (2) The `SkeletonModification2DLookAt` +that aims the Head bone is **not mirror-invariant**: under the LEFT root mirror it writes a bone +rotation 180Β° off the FORWARD aim, flipping the head to hang below the neck. New +`_apply_head_lookat_mirror_mode()` (called from `_apply_profile()`) disables the LookAt and pins the +head bone to the FORWARD canonical aim (Ο€) when facing LEFT; `_pin_mirrored_head_rotation()` +re-asserts the pin each `_physics_process` frame while ANIMATED/RECOVERING so recovery's stack +re-arm can't let LookAt flip the bone. RIGHT/FORWARD re-enable the LookAt. Consequence: interactive +head-aiming while facing LEFT is intentionally static. + ## 10. Test plan 1. **Parse check** (same as prior tasks): diff --git a/docs/phase_3a_spec.md b/docs/phase_3a_spec.md index 32ad9ae..ab7e419 100644 --- a/docs/phase_3a_spec.md +++ b/docs/phase_3a_spec.md @@ -230,9 +230,9 @@ func is_walking() -> bool 1. Guard `state == RigState.ANIMATED`. 2. `_walk_target_feet = target`. 3. `_nav_agent.target_position = target` (global ground point β€” see D1). -4. Set facing from horizontal delta (`dx < -0.5` β†’ `FacingProfile.LEFT` + play - `walk_left`; `dx > 0.5` β†’ `FacingProfile.RIGHT` + play `walk_right`; vertical-only β†’ - keep facing, play `walk_right`). +4. Set facing from horizontal delta (`dx < -0.5` β†’ `FacingProfile.LEFT`; `dx > 0.5` β†’ + `FacingProfile.RIGHT`; vertical-only β†’ keep facing), then play the canonical `walk_right` + clip (root-mirrored via `Master.scale.x = -1` for LEFT; `walk_left` is not used at runtime). 5. `_anim_player.play(name)` (walk anims are authored `LOOP_LINEAR`, so they loop). 6. `_walking = true`, `_walk_done = false`. @@ -301,9 +301,10 @@ ragdolled rig has no stale walk/path state. The agent is a passive helper node ( body) β€” it does not interfere with the ragdoll network, and `_update_walking`'s `state != ANIMATED` guard prevents it being read while ragdolled. -> The `walk_left`/`walk_right` animations key the IK targets in-place and carry a discrete -> `.:facing_profile` track, so playing the matching clip both swings limbs and (re)sets the -> facing profile/z-order/head-flip. Root translation composes with the in-place limb +> The canonical `walk_right` animation keys the IK targets in-place; `walk_to()` sets the facing +> profile explicitly (`LEFT` root-mirrors the rig via `Master.scale.x = -1` and plays the same +> `walk_right` clip mirrored; `RIGHT`/`FORWARD` play it unmirrored β€” `walk_left` is no longer used +> at runtime). Root translation composes with the in-place limb > animation. Movement is **kinematic** (`global_position.move_toward` in `_physics_process`; > the rig is a plain `Node2D`, no `CharacterBody2D`), so no `NavigationAgent2D.velocity` / > `velocity_computed` RVO handling is used (avoidance is disabled). diff --git a/docs/phase_3c_editor_spec.md b/docs/phase_3c_editor_spec.md new file mode 100644 index 0000000..12f1254 --- /dev/null +++ b/docs/phase_3c_editor_spec.md @@ -0,0 +1,700 @@ +# 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` +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`). + +--- + +## 1. Overview & Scope + +Phase 3c adds **full editing** for the Sandbox Stage's two director-facing authoring +artifacts that previously had no in-place editing: + +- **Action queues** (per-`StickmanRig`, Phase 3a): actions could only be *appended*. Phase 3c + adds a **Queue Panel** (view / edit / delete / drag-reorder / add / clear-all) and a + **waypoint context menu** with visual walk re-placement and insert-before/after. +- **Event rules** (`_event_rules` on `SandboxStage`, Phase 4): rules could only be + *created or deleted*. Phase 3c adds a **Rule Panel** (view / edit / delete / drag-reorder / + add / clear-all), a **full rule editor** (trigger + target + actions) and a + **consequence-only rule editor** (trigger read-only; actions editable). + +Everything is **registry-driven**: two const registries (`ActionRegistry`, +`TriggerRegistry`) are the single source of truth for the action/trigger templates, and the +new editors + panels generate their UI from them. Adding a new action or trigger type is a +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, + 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` + with the pulsing edit highlight, and rule-label β†’ editor routing. +- `tests/test_phase3c_editor.gd` β€” 253-assertion headless suite. + +### Out of scope / untouched (must not regress) + +- **`.stk` format / the editor** β€” no changes. +- **Queue / rule disk persistence** β€” still in-memory across `EDIT ⇄ DIRECT ⇄ PLAY` toggles, + reset on scene reload (matches Phase 3a/4 scope). +- **The queue runner** (`StickmanRig._process_queue`, Phase 3a) and the **event engine** + (Phase 4) β€” read only; they already consume the same action/rule dict shapes. +- **Rule-builder flows** (Phase 4 `RuleStep` state machine) β€” unchanged; Phase 3c's editors are + additive alongside them. The panel "Add Rule" reuses `_begin_rule_build()`. + +--- + +## 2. Recorded User / Implementation Decisions + +1. **Registries are const dictionaries with static accessors.** Each action/trigger template is + one const dict entry. Because `Dictionary.keys()` is untyped at runtime, the registries expose + `static func types() -> Array[String]` (a genuinely typed array) rather than exposing `.keys()` + directly, so callers can store the type list in typed locals (`ActionEditor._current_type`, + `RuleEditor._select_action_type`). +2. **Panels/editors are `PopupPanel`s with `exclusive = true` + `popup_window = true`** built in + code. The four `.tscn` files are **minimal shells** (bare `PopupPanel` + root script); all UI + is constructed in `_ready()` (consistent with the Phase 3b `asset_selector.tscn` pattern). + Because they are exclusive, 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 (see Known Limitations, + Β§10 #19). +3. **Rule-action shape ↔ flat queue-action conversions live on the registry.** A rule action + nests params under `params` and adds `target` (the actor's instance id); a flat queue action + (Phase 3a) inlines them. `ActionRegistry.to_rule_action()` / `from_rule_action()` convert, and + `ActionRegistry.summarize()` / `TriggerRegistry.summarize()` produce the one-line labels used + by both the panels and the editors (they `get()` with fallbacks tolerant of either key layout). +4. **Waypoint "Edit this Walk" is a visual stage edit** (a `POSITION` capture), not a numeric + dialog. The target waypoint is highlighted with a **pulsing amber ring** drawn by + `StageDirectorVisuals` while the capture is pending. +5. **`ragdoll` / `recover` "Edit" = a paramless `ActionEditor` pre-fill.** Plan Β§11.2 originally + specified a separate *confirmation dialog* for editing these no-parameter actions; the + implementation instead reuses the generic `ActionEditor` (open, then OK) β€” fewer special + cases, and the type is unchanged unless the user changes it. This is a deliberate deviation + from the plan, not a defect (recorded in the tech-debt Change Log only; no debt row). +6. **Confirmations use one shared `ConfirmationDialog`** (`_ask_confirm(title, message, cb)`). + Queue delete / clear-all and Rule delete / clear-all confirm before mutating. +7. **Reordering is nearest-row-center drop semantics** (see Known Limitations, Β§10 #21): a drag + targets the row whose vertical center is nearest the pointer. +8. **"Edit Queue…" / "Edit Rules…" entry points are content-gated.** In the Direct action popup + and the stickman right-click menu, "πŸ“‹ Edit Queue…" is hidden (or disabled) when the stickman's + queue is empty, and "⚑ Edit Rules…" is hidden (or disabled) when the stickman is the source of + no rule in `_event_rules`. The waypoint menu's "⚑ Edit Trigger Rules" entry already follows this + pattern β€” `WaypointContext.popup_for(rect, count)` enables it (and shows the count) only when + `count > 0`. Rationale: a menu item that opens an empty panel is noise; gating it signals that + there is nothing to edit. + +--- + +## 3. New Files + +| 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 | + +--- + +## 4. Public API Signatures (GDScript) + +### 4.1 `ActionRegistry` (`res://scripts/action_registry.gd`) + +```gdscript +class_name ActionRegistry +extends RefCounted + +const ACTION_TEMPLATES := { + "walk_to": { "label": "Walk To", "icon": "🚢", + "params": [ { "key": "target", "type": "position", "required": true } ] }, + "speak": { "label": "Speak", "icon": "πŸ’¬", + "params": [ { "key": "text", "type": "text", "required": true }, + { "key": "duration", "type": "float", "default": 2.0 } ] }, + "wait": { "label": "Wait", "icon": "⏳", + "params": [ { "key": "duration", "type": "float", "required": true } ] }, + "ragdoll": { "label": "Ragdoll", "icon": "πŸ’₯", "params": [] }, + "recover": { "label": "Recover", "icon": "πŸ”„", "params": [] }, +} + +static func types() -> Array[String] # genuinely typed (Dictionary.keys() is untyped) +static func has_type(type: String) -> bool +static func label(type: String) -> String # template label, else the type itself +static func icon(type: String) -> String # template icon, else "" +static func to_rule_action(action: Dictionary, target_id: int) -> Dictionary + # flat queue action -> rule-action shape: { "type", "target": target_id, + # "params": { target | text+duration | duration } } (walk_to/speak/wait only; others -> no params) +static func from_rule_action(rule_action: Dictionary) -> Dictionary + # rule-action shape -> flat queue action (drops target, inlines params) +static func summarize(action: Dictionary) -> String + # one-line label, e.g. 'Walk To (123, 456)', 'Speak "Hello" (2s)', 'Wait 1s'; tolerates + # both the flat and rule-action key layouts via get() fallbacks. +``` + +### 4.2 `TriggerRegistry` (`res://scripts/trigger_registry.gd`) + +```gdscript +class_name TriggerRegistry +extends RefCounted + +const TRIGGER_TEMPLATES := { + "arrived_at_waypoint": { "label": "Arrives at waypoint", "icon": "πŸ“", "target_type": "waypoint" }, + "action_finished": { "label": "Completes any action", "icon": "βœ…", "target_type": "action_type" }, + "speech_finished": { "label": "Finishes speaking", "icon": "πŸ’¬", "target_type": "none" }, + "entered_area": { "label": "Enters trigger area", "icon": "🎯", "target_type": "area" }, + "collided": { "label": "Collides with something", "icon": "πŸ’₯", "target_type": "prop" }, +} + +static func types() -> Array[String] +static func has_type(type: String) -> bool +static func label(type: String) -> String +static func icon(type: String) -> String +static func target_type(type: String) -> String # "waypoint" | "action_type" | "none" | "area" | "prop" +static func summarize(trigger: Dictionary) -> String # "