Files
stickman/docs/specs/prop_settings_card_refinement.md
T
ryan 1d4a71525a 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.
2026-10-09 23:11:34 -04:00

12 KiB
Raw Blame History

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. 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.

╭─ 📦 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

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

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.