- 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.
113 lines
12 KiB
Markdown
113 lines
12 KiB
Markdown
# 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.
|