Implement Phase 17a Prop Roles UI/UX Specification and associated functionality
- Added UI/UX specification for prop roles and set down mechanics in phase 17a. - Created `PropStageRoles` script to manage normalized behavior for props, including depth, motion, and collision settings. - Introduced `StagePhysicsPolicy` for shared physics configurations related to props and actors. - Developed comprehensive tests for prop roles, including default settings, legacy migration, serialization, and runtime behavior. - Ensured proper handling of set down actions, including clearance and orientation preservation.
This commit is contained in:
@@ -71,6 +71,11 @@ This document tracks known technical debt, optimization opportunities, and minor
|
||||
| 59 | **Stage-only speech edits remain drafts until session completion** — a recoverable draft can differ from its queue/rule target while the child is styling. | Medium | Planned | Done speech is the single validated in-memory commit boundary. A Scene Save request during composition is deferred until the user explicitly completes speech; it never commits implicitly or writes the stale queue/rule target. Cancel speech confirms before discarding an edited draft. This avoids premature persistence of blank bubbles and stale scene data. Recovery remains draft-only. See `docs/specs/speech_stage_only_editor.md`. (2026-09-30) |
|
||||
| 60 | **Run contact timing and baked IK poses share authored gait facts** — clip duration/contact phases must agree with locomotion's stance weights and fixed travel pace. | Medium | Planned | Phase 16 keeps one locomotion controller and an authored Run clip. Tune the speed, cycle duration, foot travel, and contact windows together; flight windows must release both foot locks. The generator and runtime each need phase data, so a future data-driven gait asset may be useful if more styles are added. See `docs/specs/phase_16_running_stickman.md`. (2026-10-03) |
|
||||
| 61 | **Move To migration crosses action and trigger identities** — an old `walk_to` completion filter can silently stop matching after an action becomes `move_to`. | High | Planned | Phase 16 canonicalizes old queue/rule movement actions and `action_finished.params.action_type` filters before validation, while preserving actor IDs, waypoint positions, action order, and rule IDs. Verify old scenes at the boundary and retain the `StickmanRig.walk_to` scripting facade. See `docs/specs/phase_16_running_stickman.md`. (2026-10-03) |
|
||||
| 62 | **Grounded movement currently writes the stickman root transform directly** — limb proxy contacts alone cannot prevent walking through a prop marked Blocks stickmen. | High | Planned | Phase 17a adds a swept body check to grounded movement and arrival snaps, and reports a blocked approach without false arrival. Keep ordinary ground and navigation tied to terrain; use the same actor-blocking profile for walking, airborne sweeps, ragdoll bodies, and contact events. See `docs/specs/phase_17a_prop_roles.md`. (2026-10-05) |
|
||||
| 63 | **Per-placement prop contact permissions need pair reconciliation** — layer/mask changes on one body alone can leave contacts from the other body's scan. | Medium | Planned | Phase 17a centralizes authored-start/held/released profiles on `PropBlock`, uses named physics bits, and adds symmetric pair exceptions on discrete role/scene transitions. Reconcile affected pairs on role edits, pickup/release, load, and reset; avoid per-frame pair work. See `docs/specs/phase_17a_prop_roles.md`. (2026-10-05) |
|
||||
| 64 | **Legacy scene draw order can interleave props and characters** — the new two-choice depth control cannot exactly encode an older prop between two characters. | Low | Planned | Preserve `World` order with a persisted `legacy_draw_order` marker and z-index zero until the child explicitly picks Behind or In front. Show an Old scene order cue in the card; selecting a depth applies that choice relative to every character. See `docs/specs/phase_17a_prop_roles.md`. (2026-10-05) |
|
||||
| 65 | **Large custom prop switches need state parity with native buttons** — a drawn 64 × 34 px switch inside a full-row `Button` replaces the stock small `CheckButton` icon. | Low | Planned | The proposed `PropSettingsToggleRow` should keep native focus, keyboard activation, disabled state, and one `toggled` emission per press; its child labels must ignore pointer input. A compact depth cue lives inside each choice button, while the stage remains the live preview. See `docs/specs/prop_settings_card_refinement.md`. (2026-10-09) |
|
||||
| 66 | **Stage selection panels duplicate layout and visual rules** — the prop card owns its own shell sizing, choice styles, and `stage_prop_settings` tokens, making later inspector cards prone to drift. | Low | Planned | Promote the current prop card to a small shared Stage inspector shell, choice group, and toggle row with `stage_panels` theme tokens. Keep domain handlers in `SandboxStage`; preserve the existing prop role schema. During migration, resolve old prop tokens as fallback only temporarily and remove duplicate reads once the prop card uses the shared controls. See `docs/specs/stage_panel_ui_system.md`. (2026-10-09) |
|
||||
|
||||
---
|
||||
|
||||
@@ -149,4 +154,8 @@ This document tracks known technical debt, optimization opportunities, and minor
|
||||
| 2026-10-03 | Speech badge clusters now also form when bubble frames intersect. For overlapping bubbles, the sorted badge row anchors above the largest frame, preventing a smaller bubble's badge from appearing through the larger bubble's text. |
|
||||
| 2026-10-03 | Phase 16 Move To/Run architecture specification written for user sign-off (`docs/specs/phase_16_running_stickman.md`). Approved canonical `move_to` plus Walk/Run style, migration of old movement and completion-filter types, a short arrival settle, and visual verification across Ground, Ramp, Step, seams, edges, and existing fall/landing behavior. Logged #60 for duplicated gait phase facts and #61 for the migration boundary. No implementation code changed. |
|
||||
|
||||
| 2026-10-05 | Phase 17a prop roles and Set Down architecture specification written for final user sign-off (`docs/specs/phase_17a_prop_roles.md`). Retain `PropBlock` as a stable `RigidBody2D` with authored-start/held/released profiles; make Set Down a world release target with terrain-grounded reach and physics settling; use explicit contact-pair policy and a grounded walking sweep. Legacy scenes migrate with dynamic prop behavior and a saved draw-order marker. Logged #62–#64. No implementation code changed. |
|
||||
| 2026-10-09 | Proposed Prop settings card refinement written for user approval (`docs/specs/prop_settings_card_refinement.md`): full-width 64 × 34 px switches, explicit selected choice styling, plain-language copy, nearby help, compact scrollable card, and small front/behind cues. Logged #65 for custom control state parity. No implementation code changed. |
|
||||
| 2026-10-09 | Proposed Stage selection panel UI system drafted for user approval (`docs/specs/stage_panel_ui_system.md`). The current prop card is the reference; a shared shell, choice group, toggle row, and theme tokens support later Stage inspectors. Logged #66 for duplicated panel presentation. No implementation code changed. |
|
||||
|
||||
_End of Log_
|
||||
|
||||
@@ -8,6 +8,18 @@ Priorities are recommendations for discussion, not a committed release schedule.
|
||||
|
||||
1. **Move To with selectable movement styles** — **Status:** Verified · **Suggested priority:** High. **Intent:** Give a stickman one destination action with a per-action style choice, starting with Walk (default) and Run; leave room for future styles like Fly. Use a fixed pace for each style initially, matched to its animation. **Rationale:** Children can direct the same route at different paces without choosing duplicate action types. The first run cycle should read as a believable human gait on the existing stickman rig, with clear foot contact and push-off, a flight phase, alternating arm swing, and cadence matched to travel speed. **Connections:** [Phase 16 design brief](../plans/phase_16_running_stickman.md), `scripts/stage/action_registry.gd`, `scripts/editors/stage/action_editor.gd`, `scripts/stage/stage_scene_io.gd`, `scripts/characters/stickman/stickman_locomotion.gd`, `scripts/tools/create_animations.gd`; existing scenes and rules currently store `walk_to` and should continue as Move To · Walk.
|
||||
|
||||
2. **Prop depth and behavior settings** — **Status:** Planned · **Suggested priority:** Medium. **Intent:** Let placed props draw behind or in front of characters, and keep every prop selectable in EDIT even when covered. Dynamic sorting with characters is deferred until the Stage has a clear front/back movement convention. **Rationale:** Children can build layered sets with independently configurable visual depth, physics/support behavior, and interaction. Some props can provide surfaces that support other props while remaining pass-through for characters; others can block characters, move under physics, be picked up, or react to events. These roles should work across prop types rather than depend on one table-and-pot example. New placements start still and use a selected-prop settings card. **Set Down** targets a clicked stage-world release point, alongside free Drop and Throw; physics determines the subsequent landing on existing collision geometry, with no support-surface authoring. Door open/close behavior and scene travel stay in the later doorway feature. This is **Phase 17a** in the [Phase 17 — Props for Storytelling plan](../plans/phase_17_props_for_storytelling.md), with an approved [17a UI specification](../specs/phase_17a_prop_roles_ui.md). **Connections:** `scripts/props/prop_block.gd`, `scripts/stage/stage_scene_io.gd`, `scripts/stage/stage_spawner.gd`, `scripts/stage/stage_selection.gd`, `scripts/editors/stage/sandbox_stage.gd`, `scripts/characters/stickman/stickman_prop_interactor.gd`, [Phase 11 prop interactions](../specs/phase_11_prop_interactions.md).
|
||||
|
||||
3. **Functional doorways and doors** — **Status:** Idea · **Suggested priority:** Medium. **Intent:** Let placed door artwork serve as scenery, an open passage, or a closed wall door that blocks characters until opened; an open passage may optionally link to another scene and arrival point. **Rationale:** Children can reuse door art for different stage roles. Door open/close actions and scene travel add state and depend on the basic prop blocking model and dynamic scene loading; keep this as later work. Whether a doorway activates automatically or through a directed action remains open. **Connections:** `scripts/stage/stage_scene_io.gd`, `scripts/stage/trigger_area.gd`, `scripts/stage/trigger_registry.gd`, `scripts/editors/stage/sandbox_stage.gd`.
|
||||
|
||||
4. **Vector stage backgrounds and wall finishes** — **Status:** Idea · **Suggested priority:** Medium. **Intent:** Let children build zoomable backdrop scenery such as brick or wooden room walls, with no collision. **Rationale:** A broad background wall should be quick to paint and sit behind the action; its material/look is independent from whether a wall is physically solid. Walls that block characters fit static structure/terrain behavior, while loose bricks and planks remain movable props. The current Stage has vector terrain and props but no saved background-art system. **Connections:** `scripts/terrain/terrain_block.gd`, `scripts/stage/stage_scene_io.gd`, `scripts/editors/stage/sandbox_stage.gd`.
|
||||
|
||||
5. **Use spots on props** — **Status:** Planned · **Suggested priority:** Medium. **Intent:** Let children mark a handle, button, or lever on a placed prop and direct a stickman to use it. **Rationale:** A Use spot is distinct from a grip anchor: the prop stays in place while the stickman reaches to it. Initial direction is an instance-level spot that produces a **Used prop** event after the stickman reaches and completes the interaction. Rules can respond to use of the whole prop or filter for one specific spot; the event identifies the stickman, prop, and spot. Built-in effects such as toggling a light are deferred. The Stage needs a clear standing/reach target, visible EDIT feedback, and an understandable unreachable cue. This is **Phase 17b** in the [Phase 17 — Props for Storytelling plan](../plans/phase_17_props_for_storytelling.md). **Connections:** `scripts/props/prop_anchor_data.gd`, `scripts/props/prop_block.gd`, `scripts/characters/stickman/stickman_prop_interactor.gd`, `scripts/stage/action_registry.gd`, `scripts/stage/trigger_registry.gd`.
|
||||
|
||||
6. **Prop impact reactions** — **Status:** Planned · **Suggested priority:** Medium. **Intent:** Let a prop wobble, topple, or break when struck or thrown. **Rationale:** Adds slapstick and cause-and-effect stories using the existing collision events. Initial direction: physics-derived hit strength drives a short controlled wobble, with no child-facing force slider; PLAY reset restores the authored state. Topple and break remain later options. This is **Phase 17c** in the [Phase 17 — Props for Storytelling plan](../plans/phase_17_props_for_storytelling.md). **Connections:** `scripts/props/prop_block.gd`, `scripts/editors/stage/sandbox_stage.gd`, `scripts/stage/trigger_registry.gd`.
|
||||
|
||||
7. **Group props into set pieces** — **Status:** Planned · **Suggested priority:** Low. **Intent:** Let children group objects for organization and moving together within one scene. **Rationale:** Scene-only groups make arranging furnished sets faster. In PLAY, each item keeps its own physics and interaction; picking up one prop does not move the rest, and stopping PLAY returns the grouped items to their authored arrangement. Reusable set-piece assets are deferred. This is **Phase 17d** in the [Phase 17 — Props for Storytelling plan](../plans/phase_17_props_for_storytelling.md). **Connections:** `scripts/stage/stage_selection.gd`, `scripts/editors/stage/sandbox_stage.gd`, `scripts/stage/stage_scene_io.gd`.
|
||||
|
||||
## Entry format
|
||||
|
||||
For each feature, record:
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Phase 17 — Props for Storytelling
|
||||
|
||||
> **Status:** Accepted product brief. User approval: Phase 17a implementation and UI accepted. Phases 17b–17d remain deferred.
|
||||
> **Audience:** UI/UX, architect, and implementation team.
|
||||
> **Purpose:** Product and design brief; technical contracts belong in later specifications.
|
||||
|
||||
## Goal and audience
|
||||
|
||||
Help children build layered sets and direct clear cause-and-effect stories with props. A table can sit behind a character and hold a flower pot without blocking the character. The character can pick up the pot, set it down deliberately, drop or throw it, press a button on another prop, and make a struck prop wobble. Children can also keep related set pieces together while arranging a scene.
|
||||
|
||||
The audience is a child working in the Sandbox Stage's EDIT, DIRECT, and PLAY modes. The controls should describe visible outcomes, show the relevant target on the stage, and let the child correct a misplaced object or action without rebuilding the scene.
|
||||
|
||||
## Desired user outcome and delivery order
|
||||
|
||||
Phase 17 is one product milestone with four ordered parts. Each part should be independently reviewable; later parts build on the placed-prop behavior established in 17a.
|
||||
|
||||
| Part | Child-facing outcome | Priority | Status |
|
||||
| --- | --- | --- |
|
||||
| **17a — Prop roles and placement** | Arrange props behind or in front of characters; choose independent physical and interaction capabilities; build pass-through support props; select covered props in EDIT; use **Set Down** to release a held prop at a chosen stage spot. | First and highest | Implemented and user-approved |
|
||||
| **17b — Use spots** | Mark a handle, button, or lever on a placed prop, direct a stickman to use it, and respond through a rule. | Next | Deferred |
|
||||
| **17c — Impact wobble** | Make a prop briefly wobble after a meaningful hit. | Next | Deferred |
|
||||
| **17d — Scene groups** | Organize and move several placed props together while editing a scene. | Last; lower priority | Deferred |
|
||||
|
||||
17a's role model, saved-scene migration, fixed depth tiers, covered-object chooser, collision handling, Set Down action, and UI are implemented and user-approved. Automated test run: no execution result is documented for `tests/test_phase17a_prop_roles.gd`. The success criteria below remain regression guidance. Depth is currently a fixed behind/in-front choice; dynamic Y sorting remains deferred.
|
||||
|
||||
## Core child flow and feedback
|
||||
|
||||
### 17a — Prop roles and placement
|
||||
|
||||
The child places a prop, selects it in EDIT, and chooses its visual depth separately from what it does in PLAY. The initial depth choices are **Behind characters** and **In front of characters**. Props keep a clear selection outline and a way to select or edit them even when another object covers them. The stage should make the chosen depth and behavior legible without exposing physics terminology as the primary labels.
|
||||
|
||||
Behavior is expressed through independent capabilities, because a prop may support other props while characters pass through it, and a pickable prop may be nonblocking at the start of PLAY but collide after release. Newly placed props start still by default. A table can remain fixed, sit behind a stickman, and support a flower pot without blocking that stickman. The pot stays at its authored starting position until picked up. While held, it follows the hand without collisions; after Set Down, Drop, or Throw, it interacts with configured surfaces, stickmen, and other props as appropriate. It remains available for another pickup. Stopping PLAY restores the authored arrangement and behavior for a repeatable replay.
|
||||
|
||||
**Set Down** complements the existing free **Drop** and **Throw** actions. The child directs a stickman holding a prop, chooses Set Down, then clicks any stage-world spot, including open air. The stickman approaches from a reachable grounded position and moves the held prop to the requested release point. If that point embeds the prop in a support surface, the stage raises the release point just enough to clear its collision shape. The arm reaches smoothly; low placements bend the torso and knees. At release, the object becomes physical and falls onto the crate, table, terrain, or other surface below. The stage previews the requested point, corrected release origin when needed, actor approach, and reach, and gives a clear, recoverable cue for an unreachable or blocked release point. A support prop uses its collision geometry; the child does not mark a special support surface.
|
||||
|
||||
### 17b — Use spots
|
||||
|
||||
In EDIT, the child adds visible Use spots to a **placed prop instance** and moves each marker onto a handle, button, or lever. A Use spot differs from a pickup grip: the stickman reaches to the spot while the target prop stays in place. In DIRECT, the child chooses **Use**, selects the prop and intended spot, and sees where the stickman will approach and reach. If the spot cannot be reached, the stage explains the problem at the target rather than silently skipping the action.
|
||||
|
||||
A successfully completed interaction emits a child-facing **Used prop** event that identifies the stickman, prop, and spot. A rule can respond to use of the whole prop or only one specific spot. The spot gets a brief, visible completion cue. Built-in results such as opening a door or toggling a light are later work; the rule system supplies story consequences in this phase.
|
||||
|
||||
### 17c — Impact wobble
|
||||
|
||||
The child can choose a simple impact response for a placed prop: **None** or **Wobble**. A noticeable physical hit produces one short, controlled wobble that settles cleanly. Ordinary resting contact should not make a prop shake repeatedly. The stage's existing collision rules remain available for more elaborate consequences. Stopping PLAY cancels any active wobble and restores the authored prop appearance and placement.
|
||||
|
||||
### 17d — Scene groups
|
||||
|
||||
In EDIT, the child selects related placed props such as a table, cloth, and pot, chooses **Group**, and gets a visible shared selection that can be moved together. The child can open the group to adjust one item and can ungroup without losing the items. A group organizes this scene; it does not turn its members into one physical object. In PLAY, each member keeps its own depth, collision, pickup, and event behavior. Picking up the pot moves only the pot. Stopping PLAY returns the authored arrangement, with the group still available in EDIT.
|
||||
|
||||
## Story and directing value
|
||||
|
||||
Depth lets children stage a character behind a table or in front of scenery. Independent support and collision roles make an apparently simple set behave as expected. Set Down gives deliberate blocking and placement moments, while Drop and Throw keep playful physical outcomes. Use spots let a character visibly press or handle a story object; rules let that moment trigger dialogue or another action. Wobble adds an immediate, readable response to a bump. Groups reduce repeated arranging as a set becomes more detailed.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- A child can save, reload, and replay a scene in which a fixed table is visually behind a stickman, supports a pot, and lets the stickman pass through. The pot can be picked up, set down onto the table with a smooth reach, released collision-free until that moment, and then fall onto it through physics. It can also be dropped or thrown and picked up again. PLAY reset restores the authored setup.
|
||||
- Visual depth and physical/interaction capabilities remain independent on placed copies of the same prop asset. A covered prop remains discoverable and editable in EDIT.
|
||||
- A child can place more than one Use spot on a prop instance, direct a stickman to a chosen spot, and make a rule fire for either the whole prop or that specific spot only after the interaction completes. Failed reach attempts give useful feedback.
|
||||
- A meaningful collision produces one readable wobble when enabled; resting contact does not produce continuous wobbling, and stopping PLAY clears the effect.
|
||||
- A saved scene group can be moved together in EDIT, opened to edit a member, and ungrouped. Members behave independently in PLAY and return to their authored arrangement on reset.
|
||||
- Older scenes and reusable `.prop` assets continue to open with sensible behavior. A child does not have to re-author a scene to use the new controls.
|
||||
|
||||
## Scope and exclusions
|
||||
|
||||
Included: per-placement depth and independent capabilities; support for props that need not block characters; predictable start, held, and released prop behavior; Set Down by clicking a stage-world release point, followed by a physical fall; EDIT selection of obscured props; instance Use spots and Used prop rules; controlled impact wobble; and persistent scene-only groups. No support-surface authoring control is part of this phase.
|
||||
|
||||
Deferred: dynamic Y sorting by character and prop ground points; built-in Use effects; topple and break reactions; reusable set-piece assets; group physics or jointed assemblies; door opening/closing, functional doorways, and scene travel. Large vector background walls belong to the stage backgrounds work, while fixed actor-blocking walls are structure/terrain concerns. Loose bricks or planks remain movable props.
|
||||
|
||||
## As-built touchpoints and remaining risks
|
||||
|
||||
- `scripts/props/prop_block.gd` and `scripts/props/prop_stage_roles.gd` now hold per-placement role settings and EDIT, start-still, start-physics, held, and released profiles. `scripts/stage/stage_scene_io.gd` writes version 1.2 and migrates older scenes; the `.prop` asset schema is unchanged. Keep save/load compatibility and PLAY reset in the regression checklist.
|
||||
- `scripts/characters/stickman/stickman_prop_interactor.gd` handles pickup, Drop, Throw, and Set Down. It clears collisions while a prop is held and applies its authored release profile afterward. Set Down previews and runtime checks share the target solver; complex queued/rule sequences and changed runtime conditions remain useful regression cases.
|
||||
- `scripts/characters/stickman/stickman_locomotion.gd` and `scripts/stage/stage_physics_policy.gd` include actor-blocking prop collision bits and movement sweeps. Props set to block stickmen are not walkable platforms; terrain remains the movement surface. Keep obstruction, pause/reset, and ragdoll interactions in the regression checklist.
|
||||
- `scripts/stage/stage_selection.gd` provides `hit_test_all()` and the stage shows an **Objects here** chooser for overlapping candidates. The current hit test uses world-space AABBs rather than exact vector-shape containment, so the chooser may include an object when the pointer is inside its bounds but outside its artwork.
|
||||
- Props use fixed behind/in-front draw tiers. Dynamic ground-point Y sorting remains deferred. `scripts/props/prop_anchor_data.gd` continues to describe reusable grip anchors; 17b Use spots will be placed-instance data and must remain distinct from those grips.
|
||||
- Stage collision events remain contact based. Phase 17c still needs a dependable hit-strength rule for a moving stickman striking a stationary prop without causing resting-contact jitter or requiring a child-facing force slider.
|
||||
- Stage multiselection can move several objects together in one edit, but it does not save persistent group membership. Phase 17d must preserve independent member identities and prop-targeted actions when adding saved groups.
|
||||
- Relevant prior context: [Phase 11 prop interactions](../specs/phase_11_prop_interactions.md) and the live [feature backlog](../ideas/feature_backlog.md).
|
||||
|
||||
## Regression checks and future design questions
|
||||
|
||||
17a's settings controls, overlap chooser, Set Down release-point behavior, legacy scene defaults, implementation, and UI are user-approved. Keep the table/pot support and pass-through case, blocking movement, open-air and corrected Set Down targets, invalid-target recovery, covered-object selection, repeat PLAY reset, and old-scene replay as regression guidance.
|
||||
|
||||
The following design questions remain for the deferred parts of Phase 17:
|
||||
|
||||
- **17b — Use spots:** How should EDIT show and target multiple instance spots without confusing them with reusable grip anchors, and how should DIRECT explain an unreachable spot?
|
||||
- **17c — Impact wobble:** What impact measure and visual treatment make a wobble consistent across stationary props, moving props, and stickmen without repeated jitter?
|
||||
- **17d — Scene groups:** What EDIT gesture makes it clear whether the child is selecting a whole group or one member? How should group membership be shown without covering the set artwork?
|
||||
@@ -0,0 +1,156 @@
|
||||
# Phase 17a — Prop roles and Set Down: architecture specification
|
||||
|
||||
> **Status:** User-approved and accepted: Phase 17a implementation and architecture.
|
||||
> **Product contract:** [Phase 17 plan](../plans/phase_17_props_for_storytelling.md) and [approved 17a UI/UX specification](phase_17a_prop_roles_ui.md).
|
||||
> **Scope:** Placed prop depth and behavior, covered-object selection, and Set Down in the Sandbox Stage. Use spots, wobble, groups, and doorways belong to later work.
|
||||
|
||||
## Decisions and behavior
|
||||
|
||||
- A placed `PropBlock` stays a `RigidBody2D`. An authored role profile determines its PLAY-start state; pickup temporarily makes it held and collision-free; every intentional release makes it a dynamic body with the authored after-release collision profile. This avoids replacing nodes and preserves `stage_object_id`, queue references, signals, and scale handling.
|
||||
- New placements start still. Existing scene props without 17a fields retain their legacy dynamic, collidable behavior. The chosen default for **After letting go** is **Bumps into stickmen: On** and **Bumps into other props: On**. Explicitly turning the latter Off means the released prop passes through other props, including support props; terrain still catches it.
|
||||
- **Holds other props** advertises a prop's existing physical shape as a support. It creates no support line. A prop can support another prop and let characters pass through. A released prop can rest on a support only if its **Bumps into other props** setting is On. When both **Holds other props** and a released prop's **Bumps into other props** are Off, the Off setting wins for that released body's own prop contacts.
|
||||
- **Implemented behavior:** A newly configured **Blocks stickmen** prop blocks the character's body and movement route but does not become a walkable terrain platform. Terrain supplies normal ground and navigation. Legacy scene behavior, including recovery probes that previously found props, is preserved. Keep obstruction and recovery cases in the regression checklist.
|
||||
|
||||
## Implementation snapshot
|
||||
|
||||
- `PropStageRoles`, `PropBlock`, and `StagePhysicsPolicy` now hold per-placement roles and the EDIT, start-still, start-physics, held, and released collision profiles. Stage files use version 1.2; 1.0 and 1.1 migrate, missing role data receives legacy behavior, and invalid present role values are rejected before the scene is replaced. The reusable `.prop` format remains unchanged.
|
||||
- `StageSelection.hit_test_all()` returns geometric candidates in draw order. EDIT opens an **Objects here** chooser for overlapping objects, including covered props.
|
||||
- The prop settings card is built from the shared `scripts/shared/stage_ui/` controls and `stage_panels` theme tokens. See the [Stage panel UI system](stage_panel_ui_system.md) and the [17a UI specification](phase_17a_prop_roles_ui.md).
|
||||
- `set_down_prop` is wired through the queue, rules, editor, save/load, target capture, and `StickmanPropInteractor`. The implementation provides a conditional preview when the held prop cannot be inferred, checks release clearance and reach, keeps the held prop non-collidable through approach/reach, restores the pickup orientation, and releases it with the configured profile.
|
||||
- Depth currently uses fixed behind/in-front draw tiers. There is no ground-point Y sorting. Use spots, impact wobble, scene groups, and functional doorways remain outside 17a.
|
||||
|
||||
`tests/test_phase17a_prop_roles.gd` has been added for role defaults/migration, serialization, runtime profiles, support contact, Set Down clearance/release, orientation, and low reach. Automated test run: no execution result is documented.
|
||||
|
||||
## Pre-17a baseline and integration points
|
||||
|
||||
| Existing code | Consequence for 17a |
|
||||
| --- | --- |
|
||||
| Before 17a, `PropBlock` at `scripts/props/prop_block.gd` was one vector `RigidBody2D` with generated collision shape and a stable `stage_object_id`. | Instance settings are stored on this node, independent of the reusable `.prop` asset; shape generation and material physics remain in place. |
|
||||
| Before 17a, `SandboxStage._start_run()` unfreezed every `RigidBody2D`; `_reset_run()` froze them and `_restore_authored_state()` restored transforms. | PLAY now applies each prop's authored start profile, and reset restores role state, masks, depth, velocity, and transform after held props are detached. |
|
||||
| Before 17a, `StickmanPropInteractor._commit_pickup()` saved freeze, masks, and z order before clearing collisions, and `_release_held()` restored that snapshot. | Drop, Throw, and Set Down now use the authored *released* profile; scene reset restores the authored start profile. |
|
||||
| Before 17a, `StickmanLocomotion._update_walking()` wrote `rig.global_position` directly, and animated body proxies and terrain shared collision bit 1. | Named physics bits and movement sweeps now let **Blocks stickmen** stop grounded movement; changing a prop mask alone would not stop direct root movement. |
|
||||
| Before 17a, `StageSelection.hit_test()` considered one world AABB winner, with ragdoll hit priority. | `hit_test_all()` now returns ordered candidates for the **Objects here** chooser. |
|
||||
| Before 17a, `StageSceneIO.FILE_VERSION` was 1.1; stage save wrote prop ID, asset, scale, and material. | Stage files now write 1.2 with validated optional per-placement roles and migration defaults; `.prop` files and queue target IDs remain unchanged. |
|
||||
|
||||
## Static contracts and ownership
|
||||
|
||||
Add a small `class_name PropStageRoles extends RefCounted` in `scripts/props/prop_stage_roles.gd`. It owns constants, allowed strings, default/migration profiles, validation, and the pure contact predicate. It is not a node or an event bus. The stage and interactor call it through `PropBlock` methods. Example contract:
|
||||
|
||||
```gdscript
|
||||
class_name PropStageRoles
|
||||
extends RefCounted
|
||||
|
||||
const DEPTH_BEHIND: String = "behind"
|
||||
const DEPTH_FRONT: String = "front"
|
||||
const START_STILL: String = "still"
|
||||
const START_PHYSICS: String = "physics"
|
||||
|
||||
static func new_placement() -> Dictionary: ...
|
||||
static func legacy_placement() -> Dictionary: ...
|
||||
static func normalize(raw: Variant, fallback: Dictionary) -> Dictionary: ...
|
||||
static func props_should_collide(a_roles: Dictionary, a_state: int, b_roles: Dictionary, b_state: int) -> bool: ...
|
||||
```
|
||||
|
||||
`PropBlock` owns an explicit, typed API rather than letting UI code assign physics flags:
|
||||
|
||||
```gdscript
|
||||
enum RuntimeRole { EDIT, START_STILL, START_PHYSICS, HELD, RELEASED }
|
||||
var stage_roles: Dictionary = {} # normalized per-placement profile
|
||||
var runtime_role: RuntimeRole = RuntimeRole.EDIT
|
||||
|
||||
func set_stage_roles(roles: Dictionary) -> void: ...
|
||||
func apply_runtime_role(role: RuntimeRole) -> void: ...
|
||||
func can_be_picked_up() -> bool: ...
|
||||
func is_actor_blocking() -> bool: ...
|
||||
func can_receive_prop_contact() -> bool: ...
|
||||
```
|
||||
|
||||
The saved per-prop entry gains one `roles` dictionary. The values below are JSON-safe and remain independent; a copied placement receives a new `stage_object_id` and a deep copy of the roles.
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "prop_2",
|
||||
"type": "prop",
|
||||
"stage_object_id": "existing-stable-id",
|
||||
"roles": {
|
||||
"depth": "behind",
|
||||
"start_motion": "still",
|
||||
"holds_other_props": true,
|
||||
"blocks_stickmen": false,
|
||||
"can_pick_up": false,
|
||||
"released_blocks_stickmen": true,
|
||||
"released_bumps_other_props": true,
|
||||
"legacy_draw_order": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Set `StageSceneIO.FILE_VERSION` to `"1.2"` and accept/migrate `"1.0"`, `"1.1"`, and `"1.2"`. Apply the existing 1.0 stable-ID migration first, then add 17a role fields to old props as `legacy_placement()`. A 1.2 prop with no `roles` also receives that legacy profile, so an incomplete older export is safe. For a present `roles` dictionary, missing known keys take `new_placement()` defaults; present known keys must have valid strings or strict booleans, otherwise reject the document in `StageSceneIO` before `_apply_scene_data()` destroys the current scene. `legacy_draw_order` is a persisted boolean inside `roles`; it is true only for migrated props or a saved legacy placement, and becomes false when the child explicitly chooses a depth. New placement uses `new_placement()`: behind characters, starts still, does not hold others, does not block stickmen, pickup On for assets with a usable grip, both after-release bumps On, and `legacy_draw_order=false`. For assets without a compatible grip, `can_pick_up` defaults Off and is unavailable until the grip exists. Do not infer settings from an asset filename. Keep `ContentPaths.canonicalize()` for path-bearing entries; roles contain no paths. Existing `stage_object_id` remains the action reference. Do not alter `.prop` version 1.0. Preserve missing-prop placeholder `original_entry`, including unknown role data, when round-tripping.
|
||||
|
||||
An older prop was previously dynamic at PLAY start, collided with other props and characters, and was pickable if it had a grip. Legacy defaults reflect those facts. Old files can contain props interleaved with characters in `World` child order; two new depth choices cannot exactly represent every old interleaving. Preserve object order and keep migrated props at `z_index == 0` while `legacy_draw_order=true`. Derive the displayed Behind/In front choice from the number of characters before versus after the prop in saved order (ties choose Behind); show a short **Old scene order** cue on the card because a mixed old arrangement may sit between characters. An explicit depth click sets `legacy_draw_order=false` and moves the prop to its chosen tier relative to every character. Re-save the marker and object order so reload does not silently move the prop. New props use fixed behind/front tiers, without Y sorting.
|
||||
|
||||
## Physics and collision policy
|
||||
|
||||
Define named physics bit constants in one stage physics policy module; keep terrain's existing bit 1 so navigation, ground rays, and old content retain it. Reserve distinct bits for animated/ragdoll actors, general props, and props that currently block actors. Configure animated collision proxies, ragdoll bodies, props, and physics queries from these constants. Update `StickmanLocomotion` ground/landing probes and airborne sweeps deliberately: normal terrain following still accepts `TerrainBlock` only, recovery keeps its previous legacy prop behavior, and the airborne horizontal sweep also sees props with the current actor-blocking bit.
|
||||
|
||||
Do not rely on a one-sided mask to make an unwanted contact disappear. Godot masks describe what each body scans, so a second body's scan can still create a contact. The policy sets both sides' relevant bits and maintains symmetric `PhysicsBody2D.add_collision_exception_with()` / `remove_collision_exception_with()` for prop pairs whose instance settings forbid contact. Reconcile exceptions when a prop is placed, deleted, edited, picked up, released, reset, or loaded; update affected pairs, not every pair every frame. Held props clear both layer and mask and are excluded from overlap queries. For a modest scene, pair reconciliation on these discrete transitions is simpler than body replacement or one physics layer per placed prop.
|
||||
|
||||
The intended matrix is:
|
||||
|
||||
| Pair | Contact rule |
|
||||
| --- | --- |
|
||||
| Any unheld prop / terrain | Yes; terrain still catches released props. |
|
||||
| Prop / animated or ragdoll stickman | The prop's current `blocks_stickmen` choice at PLAY start, or `released_blocks_stickmen` after release. Held: No. |
|
||||
| Start-still prop / moving prop | Yes only when still prop **Holds other props** and moving prop accepts prop contact. |
|
||||
| Two moving props | Yes when both accept prop contact; an explicit after-release **Bumps into other props: Off** vetoes the pair. |
|
||||
| Two start-still props | No physical response is needed; authored placements remain independently selectable. |
|
||||
|
||||
Starting dynamic props accept contact with configured supports and other dynamic props; the initial card has no separate outgoing prop-bump switch. A start-still prop advertises prop support only when `holds_other_props` is On. A released prop with `released_bumps_other_props` Off passes through every prop, even a support. If a released prop itself has `holds_other_props` On, it can support another prop only while its own prop-bump setting is On. Collision events must be emitted only for actual allowed contacts. On reset, clear contact-pair suppression/baselines to avoid fake collision events on the next PLAY. Avoid changing material mass, bounce, or shape geometry to express these roles.
|
||||
|
||||
```text
|
||||
Authored EDIT ──PLAY──> START_STILL (frozen) or START_PHYSICS (dynamic)
|
||||
^ │ │
|
||||
│ successful pickup successful pickup
|
||||
│ └─────────┬───────────────┘
|
||||
│ v
|
||||
│ HELD (no collisions)
|
||||
│ │ Set Down / Drop / Throw
|
||||
│ v
|
||||
└────────────STOP/RESET──────── RELEASED (dynamic)
|
||||
```
|
||||
|
||||
In EDIT, props remain frozen and physically quiescent but geometrically selectable. On PLAY start, `START_STILL` uses `freeze = true` with the existing generated collider active; `START_PHYSICS` uses `freeze = false`. On pickup, verify `can_pick_up` and a valid grip, reserve the prop, save only the temporary pose state needed for cancellation, freeze it, and clear collisions. On Set Down, Drop, or Throw, apply `RELEASED`, wake the body, and apply its authored release masks regardless of its start motion. Set Down gives zero launch velocity and spin, so it falls under gravity; Drop retains existing loose-release behavior unless the revised implementation makes a small compatibility correction. Throw retains its computed impulse. After release, it remains pickable when `can_pick_up` is On. Ragdoll forced release uses the same released profile. Scene exit/reset detaches held props, clears their owner/reservation, restores the authored role, depth, transform, and zero velocity before another PLAY. Pause/resume preserves each prop's current runtime role and frozen state rather than turning start-still props dynamic.
|
||||
|
||||
Draw order is per placement: new behind props render beneath characters and new front props above them, independent of physics. Use stable `CanvasItem.z_index` tiers for unheld props and the existing held-hand offset while held; restore authored depth on release. Selection outlines/gizmos draw in a separate overlay above stage artwork. Sorting and selection compare effective z tier, then sibling order; AABB size is only a tie-breaker for objects with equal draw rank.
|
||||
|
||||
## Character movement must honor blocking props
|
||||
|
||||
`StickmanLocomotion._update_walking()` currently assigns the root transform and can cross a physical body even when its limb proxies collide. Add a horizontal shape sweep of the animated body's relevant torso/head proxy shapes before every grounded movement step and before the final arrival snap, excluding the actor's own proxy RIDs and the held prop. Query terrain and the actor-blocking prop bit; retain the largest safe fraction that does not penetrate a blocking prop. On obstruction, stop advancing, keep grounded terrain following, and fail/finish the affected Move To or Set Down approach with a visible reason after a short no-progress interval; do not report arrival at an unreachable point. Use the same actor-blocking predicate for preview route checks. Continue to use the existing terrain navigation for walkable ground. When a blocking prop moves into the route during PLAY, recheck from the current pose; do not tunnel through it.
|
||||
|
||||
This collision sweep also needs to cover direct steering, close-target fast paths in `move_to()`, and any other root-position shortcut that can cross a prop. Airborne horizontal motion already casts shapes; update its mask to include actor-blocking props. Ragdoll bodies receive compatible masks and must not collide with a prop set to pass through stickmen. Existing prop collision reaction/event logic still receives allowed contacts.
|
||||
|
||||
## Set Down action and reach
|
||||
|
||||
Use the flat action `{ "type": "set_down_prop", "target_position": Vector2 }`. The corresponding saved queue value is `{ "type": "set_down_prop", "target_position": {"x": 420.0, "y": -260.0} }`; rule actions use `ActionRegistry.to_rule_action()` / `from_rule_action()` and the existing `StageSceneIO` rule encoder, with the same parameter inside `params`. `target_position` is the requested prop origin at release, not a landing target, grip position, table attachment, or support ID. It must be finite. Runtime may raise this point only when needed to clear the collision geometry of a support beneath it; the authored request stays unchanged. The queue runner waits until the approach, reach, and smooth body recovery complete or fail, and a failed action completes safely so later actions can proceed.
|
||||
|
||||
Add `set_down_prop` to `ActionRegistry`, `ActionEditor`, stickman action popup, stage target-capture enum, cursor overlay, queue summaries, and `StageSceneIO` encode/decode. The Action Editor uses **Choose release place** / **Change release place**, Done only after a finite stage point has been chosen, and the existing popup hide/restore pattern for stage capture. Both direct queue and rule authoring reach the same capture function. Esc, right-click, or visible Cancel restores the prior draft point when editing and discards an unsaved new candidate. For a known prop, a green click records the coordinate and a red click leaves capture open. When exact prop/reach/occupancy cannot be known from earlier actions, a finite click records a **conditional** target with the neutral marker and **Prop depends on earlier actions**; it is never colored green or presented as verified. PLAY performs the full check and gives a recoverable failure cue if needed. Snap, if enabled, changes only the clicked world point. `ContentPaths` is unaffected.
|
||||
|
||||
The interactor adds SET_DOWN_APPROACHING and SET_DOWN_REACHING (or equivalent explicit substates), with one shared nonmutating solver for authoring preview and PLAY validation. It reuses the queued held-prop preview prefix from Throw to determine the prop, grip, hand, and predicted actor pose. The prefix evaluator must also consume earlier Set Down actions as release actions and account for Drop/Throw; it may return `known=false` when a rule or earlier action makes the held prop uncertain. Do not move or reserve a live prop during preview. When unknown, show the neutral marker and **Prop depends on earlier actions**, and assess grounded approach/reach where possible without pretending to know exact occupancy. As with the existing Throw preview, cache physics shape checks in `_physics_process()` and draw the latest result in `_process()`; do not perform fresh direct-space queries from every pointer draw.
|
||||
|
||||
For a known prop, retain its current held orientation and authored/play scale. Treat `target_position` as the requested release origin, then check the prospective collision shape against terrain and released-profile actor/prop contacts; exclude the held prop's own RID but check the acting rig's torso/head and other solid parts when released actor bumps are On. If the shape overlaps terrain or a prop whose **Holds other props** setting is On, raise the prospective origin vertically just enough to clear the overlapping support geometry. Do not shift it sideways or select a named surface. Keep the requested point unchanged in the saved action. If the shape overlaps the acting character or a non-support prop, reject it with **No room to let go here**. Do not add an implicit owner-collision grace period or permit release inside the torso. An open-air point remains exactly where clicked. Compute the grip position from the resolved transform and asset anchor, then solve a grounded approach on terrain to either side and check wrist/IK reach. Do not teleport the actor or clamp the point to make it reachable. Preview the requested point and corrected release origin when they differ. Re-resolve at runtime because actors and props may have moved since authoring. If the route is blocked or the point remains occupied, keep the prop held, restore its hand pose, show **Too far to reach** or **No room to let go here**, finish the action safely, and let the child edit the target. If no prop is held, show a short cue on that actor and finish safely without creating or dropping a prop.
|
||||
|
||||
During the reach, move the held prop smoothly with the hand marker toward the resolved release transform. If the point is low, bend the torso and knees during the reach, then release the prop and ease the arm and body back to their resting pose. At release, use the resolved origin and apply `RELEASED` with zero launch velocity so gravity carries the prop onto the surface. The support prop's collision geometry and gravity determine where the object settles. While held, the prop has no collision layer or mask; released collision settings take effect at the release moment. A pause freezes the animation and prop state. Ordinary action cancellation before release keeps the prop held and restores the marker; ragdoll aborts Set Down and uses the existing forced-release path with the authored released profile. STOP restores authored placement. Preserve the existing grip attachment logic and scale conversion rather than assuming an unscaled local anchor.
|
||||
|
||||
## Stage UI, selection, and theme
|
||||
|
||||
Create the `PanelContainer` Prop settings card in the stage UI, with `MarginContainer`, scrollable `VBoxContainer`, header thumbnail/name, a large Close control, the exact copy and controls in the [UI spec](phase_17a_prop_roles_ui.md). Bind it only to one selected `PropBlock`; selecting a stickman/terrain hides it, and multiple selected nodes show **Select one prop to change its settings** rather than editing several silently. Choice changes normalize roles, update EDIT depth immediately, mark the scene dirty, and refresh the card. Close preserves selection and settings. Existing thumbnail cache can provide the prop header image; a type icon is acceptable until an asset thumbnail is ready.
|
||||
|
||||
Expose `StageSelection.hit_test_all(world_pos: Vector2) -> Array[Node2D]`: gather selectable `World` children geometrically even when occluded, sort by effective canvas z and sibling order, and return the top candidate first. Keep ragdoll-to-rig mapping. Normal click selects the top visible candidate; when several candidates overlap, show the **Objects here** button near the pointer. The chooser lists every candidate with at least 44 px rows, thumbnail/type icon, name, depth cue, and current-selection check. Choosing a row calls `select_only()` with that exact node, so a fully covered prop stays editable. The chooser has no effect on shift multiselect or box selection; dismissal leaves current selection intact. A selection overlay remains above all prop/actor art regardless of depth.
|
||||
|
||||
Use `SandboxTheme.control_theme()` on the card and popup. Read dimensions, fonts, accents, selected, valid, and invalid colors from `SandboxTheme` / `sandbox_theme.json`, adding narrowly scoped fallback tokens for the Prop card and Set Down preview. Every interactive target is at least 44 × 44 logical pixels and no shorter than the theme's button minimum; the card scrolls at narrow widths. Set Down overlay includes amber DIRECT framing, **Click where to let go**, a release cross, known-prop ghost or neutral marker, route/reach guide, green **Ready to set down** or the specific red failure cue, and a visible Cancel control. The ghost forecasts release pose only, not final landing. Selection and state use outlines/checks and text as well as color.
|
||||
|
||||
## Regression checklist
|
||||
|
||||
For future regression runs, use `tests/test_phase17a_prop_roles.gd` for role defaults/migration, queue and rule Set Down serialization, unknown-prop preview, released-vs-held transitions, and support contact, followed by Godot headless syntax validation. A small runtime scene can check that a support-only table catches a released pot while a stickman passes through, that switching **Can bump into other props** Off makes the pot fall through the table but still land on terrain, and that **Blocks stickmen** stops walking instead of letting the root tunnel through. Reopen saved scenes, copy a placement, select a fully occluded prop, and check correction of a bad Set Down target. Repeat PLAY after STOP and confirm identical authored arrangement, role settings, and usable stable IDs. Check pause/resume during holding and after release, plus ragdoll release.
|
||||
|
||||
The approved table/pot, open-air Set Down, invalid-target feedback, overlap chooser, card readability, old-scene replay, and movement-blocking behavior are useful visual/runtime regression scenarios.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Phase 17a — Prop Roles and Set Down: UI/UX Specification
|
||||
|
||||
> **Status:** User-approved and accepted: Phase 17a UI.
|
||||
> **Source:** [Phase 17 — Props for Storytelling](../plans/phase_17_props_for_storytelling.md).
|
||||
> **Scope:** Sandbox Stage EDIT and DIRECT controls for placed props. Phase 17b Use spots, 17c wobble, and 17d scene groups follow separately.
|
||||
|
||||
## Child flow
|
||||
|
||||
In EDIT, the child places a prop and selects it. A settings card opens near the right edge of the stage, leaves the artwork visible, and identifies the selected placed prop by icon and name. The header shows the selected prop's name without a second **Prop settings** title. The child chooses how it draws, how it begins PLAY, and what it collides with. Changes are visible on the stage immediately and belong to that placement, so another copy of the same asset can behave differently.
|
||||
|
||||
In DIRECT, the child selects a stickman and adds **Set Down** beside **Pick Up Prop**, **Drop Prop**, and **Throw Prop**. A cursor guide invites a stage click. The click records the requested release point, including a point in open air. If that point embeds the prop inside terrain or a support prop, the runtime moves the release origin straight up enough to clear the collision shape. The stickman approaches from the ground and releases the held prop there; the prop then falls under physics. The preview does not promise a final landing spot.
|
||||
|
||||
## Control layout and node structure
|
||||
|
||||
- Keep the Stage's current top mode switcher, left asset palette, and bottom status bar. Place the Prop settings card in a `PanelContainer` over the right side of the EDIT viewport, inset from the screen edge and below mode/run controls. Use a `MarginContainer` and scrollable `VBoxContainer` so longer labels and raised font sizes do not clip. The card may shrink with the window, but must leave a usable central stage view.
|
||||
- The card header contains a prop icon, the placed prop's display name, and a 44×44 or larger **✕ Close** button. Closing the card leaves the prop selected and exposes a **Settings** reopen control; it stays closed while that same prop remains selected. Selecting a different prop updates and reopens the card. Selecting terrain or a stickman hides the prop card. A scene with multiple selected objects does not silently change all their settings: show **Select one prop to change its settings** and a visible way to return to the stage.
|
||||
- Under **Where it appears**, use two full-width, mutually exclusive large buttons: **Behind stickmen** and **In front of stickmen**, each with a small depth illustration. Show the chosen button with the EDIT accent. The stage redraws the prop immediately so the result can be seen among characters; no ground-point sorting control appears in 17a.
|
||||
- Under **When PLAY starts**, use two mutually exclusive buttons: **Stays in place** and **Can fall and move**. New placements select **Stays in place**. The child can change the setting for each placement. Existing saved scenes should display their migrated legacy behavior instead of silently adopting the new default.
|
||||
- Below those buttons, use three independent, at least 44 px high switch rows: **Props can sit on top**, **Blocks stickmen**, and **Stickmen can pick it up**. Add a short plain-language line beneath a row only when its outcome might surprise the child. Under **Props can sit on top**, show **Other props can land on this one.** When **Blocks stickmen** is on, show **Stickmen cannot walk through it.** For **Stays in place**, show **Stays here while PLAY runs.** when pickup is off and **Stays here until picked up.** when pickup is on. **Blocks stickmen** describes the prop before pickup or release and does not force either of the other capabilities on.
|
||||
- If **Stickmen can pick it up** is on, reveal an **After letting go** section with the intro **After Set Down, Drop, or Throw:** and independent **Can bump into stickmen** and **Can bump into other props** switches. These settings apply after Set Down, Drop, or Throw. They remain separately editable from the initial **Blocks stickmen** and **Props can sit on top** settings. A prop can therefore let stickmen walk through at the start and collide with them after release. Hide this section when pickup is off; keep its values if the child toggles pickup off and back on during the same edit.
|
||||
- For new placements, both **After letting go** switches default to On. **Can bump into other props** lets a released object land on a support prop such as a table. If the child turns it Off, the released object passes through props, including support props; terrain such as the floor can still catch it.
|
||||
- Do not expose collision layers, mass, z-index, or numeric force in the card. Use the existing prop's selected outline and a small stage badge when a setting changes so the child can see which object changed. Use a brief, nonblocking confirmation such as **This prop is behind characters** when a depth change is visually obscured.
|
||||
- Card choices apply as soon as the child presses them and mark the scene as changed. **✕ Close** dismisses the card and keeps the chosen settings; it is not a rollback button. The child can reopen the card and change a choice again. PLAY reset restores the authored settings and placement.
|
||||
- Add an **Objects here** button near the cursor when an EDIT click covers multiple selectable objects. It opens a themed popup with a vertical list of the objects under that point, each row containing a thumbnail or recognizable type icon, name, and depth cue. Every row is at least 44 px high. The currently selected object is marked. Choosing a row selects that exact object and opens its card; the menu remains available when the chosen object is completely covered. Normal click still selects the visible object; the chooser makes covered props reachable without relying on a hidden repeated-click or keyboard gesture. The chooser must not interfere with box selection or shift multiselection.
|
||||
- In DIRECT, add **Set Down…** to the same action entry points as Drop and Throw, including the stickman action popup and Action Editor. The Action Editor has a **Choose release place** button and the existing visible **Cancel** / **Done** pattern. A saved Set Down action can reopen its target and use **Change release place**.
|
||||
|
||||
## Theme, size, and accessibility
|
||||
|
||||
- Use `SandboxTheme.control_theme()` for the card and all popups. Read fonts, selected states, neutral surfaces, borders, EDIT cyan, DIRECT amber, and valid/invalid preview colors from `SandboxTheme` and its JSON contract. Any new visual tokens belong in that contract with fallback values; avoid fixed UI colors and font sizes in implementation.
|
||||
- Interactive card rows, segmented buttons, popup rows, and the DIRECT Cancel button have a minimum pointer target of 44×44 logical pixels and a height no smaller than the applicable `button_min_height` theme token. Containers wrap or scroll at narrow widths and larger font settings. Labels should remain readable without hover tooltips.
|
||||
- Selection uses both an outline and a name or icon cue, so color alone never identifies a prop. Selected switches show a check as well as accent color. Popup and dialog windows receive `theme.control_theme()` explicitly.
|
||||
- Each active cursor workflow has an obvious **✕ Cancel** button close to the pointer or action card, and Esc/right-click may also cancel. The child does not need a keyboard shortcut to finish or leave Set Down target capture.
|
||||
|
||||
## On-canvas and cursor feedback
|
||||
|
||||
- In EDIT, maintain a high-contrast selection outline above stage artwork even when the selected prop draws behind characters. The **Objects here** chooser uses geometric candidates from the clicked position, including obscured props; draw order cannot make a selected prop uneditable. The Prop settings card stays outside the prop's immediate drag area where space permits.
|
||||
- During Set Down target capture, use the DIRECT amber mode frame and a cursor-attached pill: **Click where to let go**. Draw a small cross at the candidate world point, a translucent ghost of the known held prop centered at the intended release point, and a visible guide from the stickman's grounded approach/reach pose to that point. The guide follows the cursor continuously. It is a release preview, not a forecast of where the prop will settle.
|
||||
- A reachable point with room to release shows a green ghost/guide and **Ready to set down**. An unreachable point or a point blocked by the character or a non-support prop shows red and a short cause near the cursor, such as **Too far to reach** or **No room to let go here**. Open air can be valid. When a point is inside a support surface, keep the requested click visible with an amber cross and show the corrected ghost above it, joined by a vertical guide. This is collision clearance, not surface snapping or a special support marker; no landing surface is required.
|
||||
- When the queued actions make the held prop known, preview its shape. When the held prop cannot be known during authoring, show a neutral object marker and **Prop depends on earlier actions**; do not invent a specific silhouette or landing path. The stage can still assess the actor's route and reach where possible. If no prop is held when Set Down executes, give a concise cue at the stickman and continue safely according to the architecture spec.
|
||||
- Invalid targets do not close capture or erase the draft action. A valid click records the release point and returns to the action's editor or queue flow, with a visible **Change release place** path.
|
||||
- **✕ Cancel** during target capture discards the new candidate point and returns to the previous action editing state. When changing an existing Set Down action, cancellation keeps its earlier release point. **Done** commits a valid draft action; a draft without a chosen point cannot be committed. If the actor or scene changes after authoring, PLAY should show a recoverable failure cue rather than silently placing the prop elsewhere.
|
||||
|
||||
## Copy map
|
||||
|
||||
| UI location | Exact child-facing copy |
|
||||
| --- | --- |
|
||||
| Card header | Selected prop name with type icon; no separate **Prop settings** title |
|
||||
| Depth heading/options | **Where it appears** · **Behind stickmen** · **In front of stickmen** |
|
||||
| Initial behavior heading/options | **When PLAY starts** · **Stays in place** · **Can fall and move** |
|
||||
| Initial switches | **Props can sit on top** · **Blocks stickmen** · **Stickmen can pick it up** |
|
||||
| Release section/switches | **After letting go** · **Can bump into stickmen** · **Can bump into other props** |
|
||||
| Overlap chooser | **Objects here** · **Choose an object** |
|
||||
| DIRECT action | **Set Down…** · **Choose release place** · **Change release place** |
|
||||
| Cursor and target feedback | **Click where to let go** · **Ready to set down** · **Too far to reach** · **No room to let go here** · **Prop depends on earlier actions** |
|
||||
| Exit/action buttons | **Settings** · **✕ Cancel** · **Done** · **✕ Close** |
|
||||
|
||||
## UI boundaries for architecture
|
||||
|
||||
- Depth is a per-placement visual choice and remains independent of collision, pickup, and starting behavior. A fixed prop can hold another prop while stickmen pass through it. A staged pickup prop can begin nonblocking and gain chosen collisions after release.
|
||||
- **Holds other props** is a capability backed by the prop's existing collision geometry. There is no authored support line, suggested surface, surface snapping, or special table targeting in this UI.
|
||||
- Set Down's clicked point is the requested release point; if its collision shape would overlap terrain or a prop configured to hold other props, runtime raises the release origin vertically until it clears. Actor and reach checks still apply, and physics determines the subsequent fall and landing. There is no named support surface, sideways snapping, or requirement for a landing surface.
|
||||
- The 17b Use spot controls may later extend the selected prop card; 17c can add a simple impact response row; 17d can add group organization controls. Their detailed UI is outside this 17a spec.
|
||||
|
||||
## Implementation and verification status
|
||||
|
||||
- The card uses `StageInspectorPanel`, `StagePanelChoiceGroup`, and `StagePanelToggleRow` from `scripts/shared/stage_ui/`, styled from `stage_panels` theme tokens. Future in-stage inspectors should follow the [shared Stage panel UI system](stage_panel_ui_system.md).
|
||||
- The overlap chooser lists candidate names and depth/type cues in 44 px rows. It uses type icons rather than prop thumbnails. The prop settings header likewise currently uses a type icon and the selected prop name.
|
||||
- Set Down target capture, visible Cancel, conditional unknown-prop messaging, release ghost, approach/reach guide, and valid/invalid feedback are implemented and accepted. The accepted as-built UI shows feedback text in the stage status line and places DIRECT Cancel in the top-right action area; it does not include a cursor-attached hint or a separate depth-change badge/toast.
|
||||
- Automated test run: no execution result is documented. Card sizing, overlap selection, Set Down feedback, and old-scene presentation remain useful visual regression guidance.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Prop settings card refinement — proposal for approval
|
||||
|
||||
> **Status:** Proposed; approval requested before implementation.
|
||||
> **Scope:** The Sandbox Stage EDIT card for one selected placed prop. This refines the card portions of [Phase 17a UI](phase_17a_prop_roles_ui.md). It changes presentation and input targets only; the saved `PropStageRoles` keys and PLAY behavior retain their current meanings.
|
||||
|
||||
## Child-facing result
|
||||
|
||||
The card names the selected prop, then presents two clear choices about drawing order, two choices about starting motion, and large on/off rows for what the prop can do. Every choice has a visible selected state. A child can press anywhere on a toggle row, including its words, help line, and switch. When there is room, the card ends after its content instead of filling the entire right side of the stage.
|
||||
|
||||
```text
|
||||
╭─ 📦 Prop settings ───────────── ✕ Close ─╮
|
||||
│ Crate │
|
||||
│ Where it appears │
|
||||
│ [✓ ▣ Behind stickmen ] │
|
||||
│ [ ▣ In front of stickmen ] │
|
||||
│ When PLAY starts │
|
||||
│ [✓ Stays in place ] │
|
||||
│ Stays here until picked up. │
|
||||
│ [ Can fall and move ] │
|
||||
│ │
|
||||
│ Props can sit on top [ ✓ ON ] │
|
||||
│ Other props can land on this one. │
|
||||
│ Blocks stickmen [ OFF ] │
|
||||
│ Stickmen cannot walk through it. │
|
||||
│ Stickmen can pick it up [ OFF ] │
|
||||
╰──────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
The two `▣` marks above stand for small overlap pictures, not the final icon art. Each picture shows a stickman and a prop in the selected front/behind order. The stage remains the live preview after a choice changes. There is no separate preview pane.
|
||||
|
||||
## Copy and meaning
|
||||
|
||||
| Current control | Proposed label | Associated help / behavior |
|
||||
| --- | --- | --- |
|
||||
| Behind characters / In front of characters | **Behind stickmen** / **In front of stickmen** | The picture shows only draw order. The chosen button gets a leading check, EDIT cyan border, and distinct fill. |
|
||||
| Starts still | **Stays in place** | When pickup is enabled: **Stays here until picked up.** Otherwise: **Stays here while PLAY runs.** A start-still prop remains frozen until an explicit pickup; a bump alone does not start motion. |
|
||||
| Moves with physics | **Can fall and move** | **Gravity and bumps can move it.** This avoids promising that a prop resting on terrain will always fall. |
|
||||
| Holds other props | **Props can sit on top** | **Other props can land on this one.** Help sits inside this row, directly under its label, when On. |
|
||||
| Blocks stickmen | **Blocks stickmen** | **Stickmen cannot walk through it.** Help sits inside this row when On; this setting describes its starting state. |
|
||||
| Can be picked up | **Stickmen can pick it up** | When unavailable because there is no usable grip: **Add a hand hold in Prop Editor first.** Row is visibly disabled. |
|
||||
| After letting go | **After letting go** | Appears only when pickup is enabled. A one-line intro says **After Set Down, Drop, or Throw:** |
|
||||
| Bumps into stickmen | **Can bump into stickmen** | The release state is independent of **Blocks stickmen** at PLAY start. |
|
||||
| Bumps into other props | **Can bump into other props** | When On, a released prop can land on a support prop; when Off, it passes through props and can still land on terrain. |
|
||||
|
||||
Copy is derived from roles during refresh, never stored in the scene. For a migrated scene with `legacy_draw_order: true`, show **Old scene order** and leave both front/behind buttons visually unselected. The child makes a new depth choice by pressing one; that action clears `legacy_draw_order` and selects the chosen button. The old scene's drawing order remains visible until then.
|
||||
|
||||
## Control and layout contract
|
||||
|
||||
| Item | Proposed logical size / behavior |
|
||||
| --- | --- |
|
||||
| Card | Preferred width 380 px; inset 12 px from the right edge. Clamp to viewport width minus 24 px. Fit height to visible content plus padding, capped between the top controls and status bar; scroll only when content exceeds that available height. |
|
||||
| Depth and motion choice | Full card width, minimum 56 px high. A selected button shows **✓** and EDIT cyan fill/border; unselected buttons retain a distinct neutral surface. Pressed hover and focus must preserve the selected border. |
|
||||
| Toggle row | Full card width, minimum 72 px high with one-line help; minimum 64 px without help. The entire row is one focusable/toggleable `Button`. The complete label and help area are clickable. |
|
||||
| Switch drawing | At least **64 × 34 px** visible track with **26 px** knob and 12 px gap from text. Show **ON / OFF** inside or beside the track as well as a check on On. Disabled state retains readable text and a clear reason. |
|
||||
| Type | Body 18–20 px, helper at least 16 px, headings 18–20 px, title 22–24 px. Wrap long labels; never clip them to keep the switch on one line. |
|
||||
| Spacing | 12 px between groups, 6–8 px between options; at least 12 px inside row edges. Close remains at least 44 × 44 px. |
|
||||
|
||||
The existing card is anchored from a fixed top to a fixed bottom, creating a tall blank area. Size it from the content after the selection and conditional release controls refresh. Refit on viewport resize, font/theme change, and pickup toggle; clamp its height before enabling the `ScrollContainer`. Keep the selected prop visible where possible by leaving the central stage unobstructed. Reset scroll to top when selecting another prop; preserve scroll while toggling the current prop.
|
||||
|
||||
## Godot 4 implementation contract
|
||||
|
||||
```text
|
||||
CanvasLayer
|
||||
└─ PropSettingsCard : PanelContainer
|
||||
└─ MarginContainer
|
||||
└─ VBoxContainer
|
||||
├─ Header : HBoxContainer (title, Close; remains visible)
|
||||
└─ ScrollContainer
|
||||
└─ VBoxContainer
|
||||
├─ PropName : Label
|
||||
├─ DepthChoices : VBoxContainer (two ButtonGroup buttons + overlap cues)
|
||||
├─ MotionChoices : VBoxContainer (two ButtonGroup buttons + selected help)
|
||||
├─ InitialRoles : VBoxContainer (three PropSettingsToggleRow buttons)
|
||||
├─ AfterLettingGo : VBoxContainer (two PropSettingsToggleRow buttons)
|
||||
└─ LegacyOrderCue : Label
|
||||
```
|
||||
|
||||
- `SandboxStage` (`scripts/editors/stage/sandbox_stage.gd`, `class_name SandboxStage extends Node2D`) retains selection ownership and `PropBlock` role writes. `_on_prop_role_choice(key: String, value: String) -> void` and `_on_prop_role_toggle(enabled: bool, key: String) -> void` remain the only mutation entry points; they normalize roles through `PropBlock.set_stage_roles`, save the placement, reconcile contacts, mark the scene dirty, and refresh the card. `_refresh_prop_settings_card() -> void` sets all pressed states without emitting signals, applies selected styles/copy, and requests one deferred size fit.
|
||||
- A small reusable `scripts/editors/stage/prop_settings_toggle_row.gd` class, `class_name PropSettingsToggleRow extends Button`, owns label/help presentation and vector switch drawing. Public methods: `configure(caption: String, help_text: String) -> void`, `set_help_text(value: String) -> void`, and `set_checked_no_signal(value: bool) -> void`. It uses inherited typed `toggled(toggled_on: bool)` and `disabled`. `toggle_mode = true`; all child labels use `MOUSE_FILTER_IGNORE`, so mouse, touch, and keyboard activation pass through the same button. Its `button_pressed` and `disabled` states drive `queue_redraw()` for the switch; there is no separate clickable switch node.
|
||||
- Keep depth/motion choices as `ButtonGroup` members with stable role values (`PropStageRoles.DEPTH_BEHIND`, `DEPTH_FRONT`, `START_STILL`, `START_PHYSICS`). When `legacy_draw_order` is true, refresh leaves both depth buttons unpressed even though the normalized role dictionary carries a fallback `depth` value; pressing a depth button clears the legacy marker through the existing handler. Once the child has chosen a depth, refresh always restores exactly one pressed depth button. A compact vector overlap cue can be a private drawn `Control` inside each depth button; it has `MOUSE_FILTER_IGNORE` and no role logic. A choice's check, accent, and focus styling are refreshed from its `button_pressed` state, including after `set_pressed_no_signal()`.
|
||||
- `SandboxTheme` (`scripts/stage/sandbox_theme.gd`, `class_name SandboxTheme extends RefCounted`) adds fallback values under `stage_prop_settings` for card width, choice height, toggle row height, switch width/height/knob size, row padding, helper size, and selected/neutral/disabled surfaces. Mirror them in `sandbox_theme.json`. Use `mode_colors.edit` for selected borders; use `global.text_color` and `global.muted_text_color` for label/help. Do not mutate the shared `Theme` returned by `control_theme()`; apply local stylebox overrides to this card's buttons. The switch and depth cues draw from resolved theme colors.
|
||||
- `PropStageRoles` and `StageSceneIO` need no schema changes. The role values remain the same. The card must refresh when selection changes or `can_pick_up` reveals/hides release controls.
|
||||
|
||||
### Signal flow and example
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Tap[Press entire toggle row] --> Signal[Button.toggled enabled]
|
||||
Signal --> Stage[SandboxStage._on_prop_role_toggle]
|
||||
Stage --> Roles[PropBlock.set_stage_roles]
|
||||
Stage --> Save[Save placement + mark dirty]
|
||||
Stage --> Refresh[Refresh card + fit content height]
|
||||
Refresh --> UI[Check, larger switch, helper, stage result]
|
||||
```
|
||||
|
||||
For a crate with `{ "start_motion": "still", "holds_other_props": true, "blocks_stickmen": false, "can_pick_up": false }`, the card shows **Stays in place**, **Props can sit on top: ON**, **Blocks stickmen: OFF**, and **Stickmen can pick it up: OFF**. The crate holds a falling prop while the stickman can walk through it. Turning pickup On changes the still-state help to **Stays here until picked up** and reveals the two release collision choices; no change to the initial blocking setting is implied.
|
||||
|
||||
## Trade-offs and boundaries
|
||||
|
||||
- A dedicated drawn switch gives the requested 64 × 34 px visual size; increasing a stock `CheckButton`'s minimum row height only enlarges its click area and leaves its theme icon small. The custom row adds one modest UI class and must maintain focus, disabled, and keyboard states.
|
||||
- The overlap cue is a small explanation of front/behind. The actual stage gives immediate visual feedback, so a separate preview area would consume useful stage space and repeat information.
|
||||
- The role text is deliberately plain. **Can fall and move** describes what physics permits; it does not imply the prop always moves. **Stays in place** still allows explicit pickup, so its helper changes with pickup permission.
|
||||
- This refinement covers the card only. It does not change prop physics, role persistence, Set Down, the overlap chooser, or future Use spot and impact controls.
|
||||
|
||||
## Acceptance for implementation
|
||||
|
||||
1. With a 4× scaled crate selected, all five role switches that can appear have visible tracks at least 64 × 34 px; clicking the label, helper, or switch changes exactly the intended role once.
|
||||
2. New placements and scenes with an explicit depth show exactly one selected depth button. A migrated **Old scene order** placement shows the cue with neither front/behind button selected until the child makes a choice; then exactly one is selected and the cue disappears. The motion choices always show exactly one selected state. Checks and selected borders remain visible under mouse hover and keyboard focus. Front/behind cues agree with the selected depth, and the stage visibly updates.
|
||||
3. A prop set to **Stays in place** remains still until a pickup action occurs; its helper says **Stays here until picked up** only when pickup is enabled. **Can fall and move** can remain at rest on terrain without contradictory wording.
|
||||
4. **Props can sit on top** shows its help directly below that row, including when the release section appears. The two release choices remain independent and retain their values when pickup is toggled off and on.
|
||||
5. On a tall viewport, the card ends near its final visible row. On a shorter viewport or with larger fonts, content scrolls, all controls remain reachable, and the Close button stays usable. Disabled pickup explains the missing hand hold.
|
||||
6. Loading a scene with legacy draw order preserves its cue until the child chooses front or behind. Save/reopen preserves role values, and closing the card does not undo them.
|
||||
@@ -0,0 +1,146 @@
|
||||
# Stage selection panel UI system — architecture specification
|
||||
|
||||
> **Status:** Approved for implementation. **Scope:** In-stage selection and inspector cards, beginning with the placed-prop settings card. Modal dialogs, the Prop Editor, and the Stickman Editor are later candidates, outside this change.
|
||||
|
||||
## Purpose and current reference
|
||||
|
||||
The placed-prop card is the visual reference for future Stage selection panels. Its **selected prop name is the header** beside an icon and Close button; there is no separate “Prop settings” title. Its depth and motion options show selection through a colored surface and border, with **no checkmark in the label**. Large full-row switches show ON/OFF and keep the switch in the same position when help appears. The header stays fixed while the content scrolls.
|
||||
|
||||
Today `SandboxStage` builds and styles that card in `sandbox_stage.gd`. `PropSettingsToggleRow` and its drawn switch implement one reusable control, but carry prop-specific names. `SandboxTheme` has `stage_prop_settings` tokens. `QueuePanel`, `RulePanel`, and action dialogs also create local style overrides, but they are popup workflows and do not join this first migration.
|
||||
|
||||
The goal is one shared look and interaction contract for the next Stage inspector, without moving prop rules, selection, or saved data into UI widgets.
|
||||
|
||||
## Child-facing contract
|
||||
|
||||
- A selection card has a recognizable icon, the selected object's name, and a large Close button. Long names wrap to at most two lines and expose their full name in a tooltip; Close never overlaps the title.
|
||||
- Content uses the same heading, choice, switch, helper, spacing, corners, focus, hover, selected, and disabled styles. Each setting can still supply its own words, optional illustration, and reason for being unavailable.
|
||||
- Choice labels stay stable when selected. Selection is shown by fill and border; text does not gain a checkmark or change width.
|
||||
- A switch is clickable anywhere in its row. Showing or hiding help may change the row height, but the switch and ON/OFF label stay anchored at the top, so a second click at the same pointer position hits the same control.
|
||||
- A card fits its visible content on a tall stage. On a short stage, only the body scrolls; the title and Close remain reachable. Focus and selected states remain distinct from hover.
|
||||
- After Close, a compact **Settings** button appears at the card's usual top-right position for the selected object. It reopens the card without requiring the child to deselect and reselect the object.
|
||||
- Keyboard users can Tab to choices, switches, and Close; Space/Enter activates the focused control. Disabled rows explain why. Text and colored states both convey ON/OFF; color alone is not the only cue.
|
||||
|
||||
## Component hierarchy
|
||||
|
||||
```text
|
||||
CanvasLayer (owned by SandboxStage)
|
||||
└─ StageInspectorPanel : PanelContainer
|
||||
└─ MarginContainer
|
||||
└─ VBoxContainer
|
||||
├─ Header : HBoxContainer
|
||||
│ ├─ Icon : Label
|
||||
│ ├─ Title : Label (selected item name)
|
||||
│ └─ Close : Button
|
||||
└─ ScrollContainer (vertical only)
|
||||
└─ Body : VBoxContainer (caller adds sections and controls)
|
||||
├─ Section heading : Label
|
||||
├─ StagePanelChoiceGroup : VBoxContainer
|
||||
│ └─ Choice : Button (+ optional decorative cue)
|
||||
└─ StagePanelToggleRow : Button
|
||||
└─ caption/help, vector switch, ON/OFF
|
||||
```
|
||||
|
||||
The shell owns layout and presentation. The caller owns the body composition and all domain values. A section heading remains a styled `Label` made by a small helper; it does not need its own subclass. Illustrations remain optional `Control` children with `MOUSE_FILTER_IGNORE`.
|
||||
|
||||
## Typed Godot 4 contracts
|
||||
|
||||
| Target | Public contract | Responsibility |
|
||||
| --- | --- | --- |
|
||||
| `scripts/shared/stage_ui/stage_inspector_panel.gd`, `class_name StageInspectorPanel extends PanelContainer` | `signal close_requested`; `func apply_theme(stage_theme: SandboxTheme) -> void`; `func set_header(title: String, icon_text: String = "") -> void`; `func body() -> VBoxContainer`; `func set_safe_rect(rect: Rect2) -> void`; `func request_fit() -> void`; `func reset_scroll() -> void` | Fixed header, scroll body, viewport fit, local theme. Emits Close; does not mutate selection or hide itself. `request_fit()` coalesces repeated calls into one deferred fit. |
|
||||
| `scripts/shared/stage_ui/stage_panel_choice_group.gd`, `class_name StagePanelChoiceGroup extends VBoxContainer` | `signal choice_selected(id: StringName)`; `func add_choice(id: StringName, caption: String, cue: Control = null) -> Button`; `func select_no_signal(id: StringName) -> void`; `func clear_no_signal() -> void`; `func apply_theme(stage_theme: SandboxTheme) -> void` | Exclusive choices with stable captions and native focus/activation. `clear_no_signal()` supports migrated prop draw order. Repressing the selected option leaves it selected. An optional cue is decorative, not clickable. |
|
||||
| `scripts/shared/stage_ui/stage_panel_toggle_row.gd`, `class_name StagePanelToggleRow extends Button` | `signal preferred_height_changed`; inherited `signal toggled(toggled_on: bool)`; `func configure(caption: String) -> void`; `func set_help_text(text: String) -> void`; `func set_disabled_state(value: bool, reason: String = "") -> void`; `func set_checked_no_signal(value: bool) -> void`; `func apply_theme(stage_theme: SandboxTheme) -> void` | One focusable full-row toggle, large drawn switch, ON/OFF label, help layout. Stores `_normal_help` and `_disabled_reason` separately; displayed help derives from `disabled`. Re-enabling restores normal help and never leaves a stale disabled reason. Child labels and switch ignore pointer events. |
|
||||
| `scripts/shared/stage_ui/stage_panel_switch_visual.gd`, `class_name StagePanelSwitchVisual extends Control` | `func set_state(enabled: bool, unavailable: bool) -> void`; `func apply_theme(stage_theme: SandboxTheme) -> void` | Vector switch drawing only. No input or business signals. Reuses the current 64 × 34 track and 26 px knob. |
|
||||
| `scripts/stage/sandbox_theme.gd`, `class_name SandboxTheme extends RefCounted` | Existing `integer()`, `color()`, `font()`, `stylebox()`, `control_theme()` accessors; add typed panel-style helpers only if needed by more than one control. | Validated shared token source. Return styles or local overrides; never mutate the cached `control_theme()` for one panel. |
|
||||
| `scripts/editors/stage/sandbox_stage.gd`, `class_name SandboxStage extends Node2D` | Existing prop role handlers and refresh path; use the shared controls' typed methods/signals. | Selection, visible panel decision, prop role writes, dirty state, contact reconciliation, and `CanvasLayer` ownership. Supplies safe rect from Stage chrome. |
|
||||
|
||||
The new shared scripts live under `scripts/shared/stage_ui/` because Stage-specific inspectors can be used by future stage controllers. `PropSettingsDepthCue` stays prop-specific under `scripts/editors/stage/`: the overlap drawing explains a prop role, not a general choice. A separate `Resource` schema or global signal bus is unnecessary for this small, synchronous UI.
|
||||
|
||||
### Signal and refresh flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input[Choice or toggle pressed] --> Widget[Shared widget signal]
|
||||
Widget --> Stage[SandboxStage prop handler]
|
||||
Stage --> Role[PropBlock.set_stage_roles]
|
||||
Stage --> Save[Save placement, reconcile, mark dirty]
|
||||
Stage --> Refresh[Refresh controls without signals]
|
||||
Refresh --> Fit[Panel request_fit, deferred once]
|
||||
Close[Close pressed] --> CloseSignal[StageInspectorPanel.close_requested]
|
||||
CloseSignal --> Stage
|
||||
```
|
||||
|
||||
Signal payloads are `StringName` for a choice ID and `bool` for a toggle. The group ID is UI-local and maps explicitly to the existing prop role string; no UI object or dictionary is serialized. Refresh calls `select_no_signal()` / `set_checked_no_signal()`, avoiding another mutation signal. The panel emits `close_requested`; `SandboxStage` records the currently selected object as dismissed and keeps its card hidden through unrelated refreshes. `SandboxStage` shows a compact Settings button at the panel anchor while dismissed. A different selection or pressing Settings clears that dismissal. Closing does not undo a saved setting.
|
||||
|
||||
### Example prop adapter
|
||||
|
||||
```gdscript
|
||||
var depth_group: StagePanelChoiceGroup = StagePanelChoiceGroup.new()
|
||||
depth_group.add_choice(&"behind", "Behind stickmen", behind_cue)
|
||||
depth_group.add_choice(&"front", "In front of stickmen", front_cue)
|
||||
depth_group.choice_selected.connect(_on_prop_depth_selected)
|
||||
|
||||
func _refresh_depth(roles: Dictionary) -> void:
|
||||
if bool(roles.get("legacy_draw_order", false)):
|
||||
depth_group.clear_no_signal()
|
||||
else:
|
||||
depth_group.select_no_signal(StringName(roles.get("depth", "behind")))
|
||||
|
||||
func _on_prop_depth_selected(id: StringName) -> void:
|
||||
_on_prop_role_choice("depth", String(id))
|
||||
```
|
||||
|
||||
The example omits the prop-specific depth cue construction. Existing `PropStageRoles` values and `StageSceneIO` serialization remain authoritative. No stage scene file format changes.
|
||||
|
||||
## Shared theme and layout tokens
|
||||
|
||||
Move reusable `stage_prop_settings` values into `stage_panels` in both `SandboxTheme.DEFAULT_CONFIG` and `sandbox_theme.json`. Resolve colors through `SandboxTheme` and apply per-node stylebox overrides without editing the shared `Theme` instance. Keep prop cue colors under `stage_prop_settings` or a prop-specific `stage_panels.prop` subtree.
|
||||
|
||||
| Token group | Shared values | Reference |
|
||||
| --- | --- | --- |
|
||||
| `stage_panels.shell` | preferred width, edge inset, padding, header gap, corner radius, surface/border | 380 px width; 12 px inset/padding |
|
||||
| `stage_panels.type` | title, section, body, helper font sizes and text colors | 24 / 20 / 20 / 16 px |
|
||||
| `stage_panels.choice` | minimum height, row gap, neutral/hover/selected/disabled surfaces, border, focus | 56 px height; 6 px gap |
|
||||
| `stage_panels.toggle` | minimum height, help height, row padding, switch dimensions, track/knob colors, row surfaces | 64 / 72 px row; 64 × 34 px switch |
|
||||
| `stage_panels.action` | minimum button target and Close width | at least 44 px target; Close at least 88 × 44 px |
|
||||
|
||||
Use `mode_colors.edit` as the current prop panel's accent, passed to shared controls through a small typed `set_accent(color: Color)` API if another inspector needs a different mode. Prefer existing `global.text_color` and `global.muted_text_color` as token fallbacks. Existing `stage_prop_settings` keys can be read as fallback for one migration release; afterward remove duplicated reads. `SandboxTheme` still owns validation and fallback behavior.
|
||||
|
||||
The shell's `set_safe_rect()` receives the unobstructed CanvasLayer area from `SandboxStage` after the top and status bars and any reserved right-side chrome. It clamps width to `rect.size.x`, calculates natural content height, caps height at `rect.size.y`, and enables body scrolling as needed. It refits when the viewport or theme changes, conditional content appears, or a row's preferred height changes. Scroll returns to the top only when the selected object changes; toggling a setting preserves position.
|
||||
|
||||
## Interaction and accessibility rules
|
||||
|
||||
1. A `Button` or `ButtonGroup` supplies pointer, touch, and keyboard semantics. All decorative children use `MOUSE_FILTER_IGNORE`. Do not place a second active button inside a toggle row.
|
||||
2. Switch and ON/OFF remain at a fixed top alignment while help grows below the caption. Control height comes from wrapped content and a minimum, never from vertically centering the switch in a changing row.
|
||||
3. Selected, hovered, focused, and disabled styles each have a visible state. Focus remains visible over selected fill. Disabled rows retain readable text and show their reason in the row. If no reason exists, the caller must provide one before disabling a child-facing setting.
|
||||
4. Choice state is apparent through fill and border; its label stays unchanged. The toggle also has ON/OFF text and knob position, so it does not depend on color alone.
|
||||
5. The title can wrap to two lines; Close reserves its width. Full title is available as tooltip. The shell recomputes height after wrapping and keeps Close outside the scroll area.
|
||||
6. The card consumes pointer events inside its rectangle so a settings click cannot select a stage object behind it. Escape follows the Stage's existing panel dismissal convention. Focus moves into the card when opened from keyboard and returns to the originating Stage control on Close when one exists.
|
||||
|
||||
## Migration sequence
|
||||
|
||||
1. Add shared theme tokens and small controls. Copy the current prop card's appearance and interaction rules as the reference. Keep current prop cue drawing and role handlers.
|
||||
2. Rebuild only the placed-prop card using `StageInspectorPanel`, `StagePanelChoiceGroup`, and `StagePanelToggleRow`. Replace `PropSettingsToggleRow` / its switch visual with the shared classes or leave temporary thin compatibility aliases while existing preloads are changed. Avoid two independent implementations of switch behavior.
|
||||
3. Delete prop-specific style and fit helpers from `SandboxStage` once the card uses the shell. Leave prop wording and role refresh in `SandboxStage` or a small prop-card adapter; do not put `PropBlock` dependencies in the shared controls.
|
||||
4. Use the components for the next Stage selection inspector. Migrate the queue/rule popup panels or dialogs only in a later, separately scoped pass because their modal focus and layout differ from an in-stage inspector.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. The prop card retains the current selected-name header, fixed Close, no text checkmarks, front/behind cues, role semantics, and saved scene format. Legacy draw order still shows no selected depth until a child chooses one.
|
||||
2. At the same viewport and theme, the migrated prop card's width, spacing, typography, colors, switch size, and selected states match the current card. Long prop names and wrapped labels cannot collide with Close or the switch.
|
||||
3. Switching a row On and Off at the same pointer position works twice without moving the switch. Help appears beneath the caption. A disabled reason disappears on re-enable and normal help returns.
|
||||
4. On short viewports, every body control is reachable by scroll while header and Close stay visible. Scroll position remains stable during a setting change and resets for a different selected prop. After Close, the card stays hidden while that same object remains selected, including after role or viewport refreshes; a compact Settings button remains available at the card anchor to reopen it. Changing selection shows the new selection's card.
|
||||
5. Pointer, touch, and keyboard activation each change a setting once. Disabled controls cannot mutate a role, and focus remains visible on both selected and neutral choices.
|
||||
6. The next Stage inspector can create a shell, a choice group, and a toggle row using only `SandboxTheme` and shared APIs, with no copied panel geometry or stylebox code.
|
||||
|
||||
## Trade-offs and decisions
|
||||
|
||||
- **Small composable controls:** A shell, choice group, and toggle row remove repeated UI mechanics while keeping panel content explicit. A data-driven panel schema would hide conditional prop rules in dictionaries and complicate authoring.
|
||||
- **Stage-owned state:** Shared controls emit intent and display values. `SandboxStage` remains the source of selection and prop mutations, preventing a generic inspector from depending on prop physics or serialization.
|
||||
- **Theme tokens first:** Keeping token values in `SandboxTheme` makes new panels match by default. Local StyleBox instances prevent one panel's accent or focus override from changing every control that shares the cached `Theme`.
|
||||
- **Incremental migration:** The prop card is the pilot; modal popup workflows have different focus and close semantics and are deferred.
|
||||
|
||||
## Decision and next adopter
|
||||
|
||||
1. **Panel dismissal — decided:** Closing a selection card keeps it dismissed while the same object remains selected. Changing selection or pressing the compact Settings button shows it again. The implementation tracks this in `SandboxStage`, separate from the shared shell.
|
||||
2. **Next adopter — recommended:** Apply the shared kit to the next in-stage selected-object inspector as it is built. Existing queue/rule popups have different focus and sizing behavior and can be evaluated in a later pass.
|
||||
|
||||
Reference in New Issue
Block a user