feat(stage): complete phase 20b action plans
This commit is contained in:
@@ -137,9 +137,9 @@ Add `set_down_prop` to `ActionRegistry`, `ActionEditor`, stickman action popup,
|
||||
|
||||
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.
|
||||
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**. 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.
|
||||
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. For a pickup, when the actor reserves its target, add an owner-specific contact exception between that target and the reserving rig's animated and ragdoll collision bodies. Keep it while `interaction_reserved_by` names that rig, regardless of distance or elapsed time; the owner's movement sweeps also skip only that target. On successful pickup, the `HELD` role clears the exception with the collision profile. On cancel or failure, release the reservation but retain an exit grace until the actor's collision shapes clear the prop by a 24 px query margin for two consecutive physics frames. For Set Down, add the same owner-specific exception after release and keep it until those shapes clear by that margin. Neither behavior changes authored collision flags or contact with the support, other props, or other stickmen; normal contacts resume after clearance. These protections are owner-specific and clearance-based, not inherited from the support; pickup protection lasts for the full reservation so it cannot expire while the actor is far away. Drop, Throw, and ragdoll forced release keep their authored contact behavior. A pause freezes the animation and prop state. Ordinary action cancellation before release keeps the prop held and restores the marker; STOP clears temporary exceptions and restores authored placement. Preserve the existing grip attachment logic and scale conversion rather than assuming an unscaled local anchor.
|
||||
|
||||
## Stage UI, selection, and theme
|
||||
|
||||
@@ -151,6 +151,6 @@ Use `SandboxTheme.control_theme()` on the card and popup. Read dimensions, fonts
|
||||
|
||||
## 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.
|
||||
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, support contact, Set Down owner-collision grace, and reserved-pickup protection, followed by Godot headless syntax validation. A runtime Set Down followed immediately by Move should confirm that the releasing stickman can clear the prop without knocking it away, the temporary exception is removed after clearance, a different stickman can still contact it, and the prop still rests on its support. A reserved pickup should stay protected through a long approach, clear on successful pickup, and transition to exit grace on cancellation/failure. Also verify 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,226 @@
|
||||
# Phase 20b — Action plans and DIRECT authoring
|
||||
|
||||
> **Status:** Implementation delivered; the user verified manual scenarios 1–7. The broader acceptance matrix still has unrecorded production-interaction and UI/UX-review evidence. The branch remains unmerged.
|
||||
> **Approval:** User approved the specifications in this conversation.
|
||||
> **Branch:** `feature/phase-20-action-reactions` in `C:\Godot4\stickman\.worktrees\phase-20-action-reactions`.
|
||||
> **Scope:** The [Phase 20 roadmap](../plans/phase_20_action_plans_and_reactions.md) defines 20b as ordered, optionally grouped character plans, explicit place/prop Move targets, DIRECT route authoring, and stable action identity. [Phase 20c–20e](../plans/phase_20_action_plans_and_reactions.md) retain reaction authoring/runtime and prop event handling.
|
||||
|
||||
## Sources and current boundary
|
||||
|
||||
Use the accepted [action/reaction design brief](../plans/action_reactions_design_brief.md), [Phase 20a identity contract](phase_20a_stage_identities_and_names.md), [Stage panel UI system](stage_panel_ui_system.md), [editor visual style guide](../guides/editor_visual_style_guide.md), and branch-local [Step Stack](../plans/action_reactions_mockups/phase_20b_step_stack.png) and [Add together](../plans/action_reactions_mockups/phase_20b_add_together_compatibility.png) mockups. The mockups show the selected direction; this spec settles data and interaction behavior where a static image cannot.
|
||||
|
||||
Today `.scene` v1.8 saves a flat `action_queue` per stickman. `StickmanActionRunner` runs one primary queue action at a time, with a replaceable speech overlay; its `int` action IDs are regenerated by `set_authored_queue()` and at PLAY start. `QueuePanel` and `ActionEditor` are separate popup workflows. `StageDirectorVisuals` draws and hits flat queue positions separately. `StickmanRig` and `PropBlock` now have persistent `stage_object_id` and scene-unique `stage_name`; `file_id` remains only the save-local rule remap. This spec replaces authored plan identity and scheduling without changing those Phase 20a identities or the existing generic rule system in 20b.
|
||||
|
||||
## Approved product decisions
|
||||
|
||||
1. The selected-character DIRECT inspector uses the **Step Stack**: numbered steps in order; all actions in a step begin together; the next step waits until every action in the current step has ended.
|
||||
2. The fixed **Move**, **Say**, and **Wait** shortcuts always append a *new* step, even when a card is selected. **+ Add together** is the explicit way to group.
|
||||
3. A 20b concurrent step contains one supported primary action plus at most one **Say**. Say can accompany every other registered action type, including Move, Wait, prop actions, Ragdoll, and Recover. Two primary actions, Say+Say, and any third action stay incompatible. The group picker shows only compatible choices; when the selected step is Say, it offers every other action type.
|
||||
4. A named-prop Move keeps the exact `stage_object_id` and a world-aligned approach offset. It pursues that prop for a bounded time during this Move, reaches only when both current approach point and prop are within reach, and fails safely if the prop vanishes, route blocks, or time expires. Rotation of a thrown/spinning prop never rotates the saved offset.
|
||||
5. In paused DIRECT, grouping and step reorder affect future steps only. Existing active-action replace/delete behavior remains. Completed/interrupted actions cannot be edited or moved, but their action rows still offer confirmed **Erase action** for the next run, as in [Phase 12](phase_12_live_director.md).
|
||||
6. Phase 20b creates no reaction editor or reaction execution. It persists action IDs for future links and exposes completion outcomes so a later event can distinguish a reached endpoint from failure.
|
||||
|
||||
## Child-facing Step Stack
|
||||
|
||||
```text
|
||||
Selected Milo in DIRECT stage route
|
||||
┌───────────────────────────────────┐ Milo ── Walk ── 🚩 1 Blue Door
|
||||
│ Milo │ 💬 1
|
||||
│ [ Move ] [ Say ] [ Wait ] │
|
||||
│ Plan [+ Add step] │
|
||||
│ ┌ Step 1 Together ─────── ↑ ↓ ┐ │
|
||||
│ │ Walk to Blue Door │ │
|
||||
│ │ Say “Let's go!” │ │
|
||||
│ │ [+ Add together] │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ ┌ Step 2 ───────────────── ↑ ↓ ┐ │
|
||||
│ │ Wait 2 seconds │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
└───────────────────────────────────┘
|
||||
```
|
||||
|
||||
The inspector is anchored on the right for **one selected character** in DIRECT. Its right edge aligns with the clock above it, using the shared 12 px viewport inset so the orange DIRECT frame remains visible. Its width is capped at 380 px and shrinks only when the safe viewport is narrower. It uses the selected character's current stage name and icon as its header. Clicking empty stage space dismisses it and clears selection. There is no Close button, the explicit exception already accepted in the design brief. DIRECT selection does not rename the character; the separate EDIT naming inspector retains its own controls. A character switch resets plan scroll and expanded action; an in-place edit preserves the anchor and scroll as far as possible. No character selected means no Director inspector.
|
||||
|
||||
`StageInspectorPanel` provides the shell, fixed header, safe rectangle, focus, and scroll behavior. Extend it with a caller-owned **pinned content slot** between header and scroll body for the three quick actions and plan heading/add control; include the slot in its height budget so only the plan list scrolls. Keep the existing `body()` API and other inspector behavior. Use `set_close_visible(false)` and explicit outside-click handling in the Stage owner; do not add a duplicate shell. `StagePanelChoiceGroup` suits exclusive Walk/Run and Prop/Place choices. `StagePanelToggleRow` and `StagePanelSwitchVisual` are not needed because 20b has no on/off setting. `StageUseTargetCaptureBar` should be generalized or reused for destination capture; keep its prompt and visible Cancel together below the top toolbar/mode pill. `StageDialogStyler` styles action deletion confirmations. Anchored prop and overlap choices use `popups.anchored_chooser` and the 20a **Objects here** behavior.
|
||||
|
||||
The scroll body shows **Plan**, **+ Add step**, and ordered **Step 1**, **Step 2**, etc. Numbers derive from order and update after reorder. Each step title is plain, stationary text in a focusable card. Clicking anywhere on a card, including its title or controls, selects that step; a child control still performs its own action. The card's DIRECT amber border marks selection. Tab focus on an unselected card is shown by a separate white inset outline; the selected card uses its amber border as the focus cue. Enter or Space selects the focused card. The title has no tooltip or hover style. Selecting a different step clears the previous action focus and closes its Action options. Selecting an action summary selects its owning step and opens only that action's **Action options** inline. Long Say summaries stay on one line and end with an ellipsis; hovering the Say row reveals the full utterance in its tooltip, and **Edit action** opens the full speech composer. A step with two actions says **Together**. Actions with editable settings offer vertically stacked, full-width **Edit action**, **Change action** when a different type is valid, and **Erase action** controls. Parameter-free actions such as Ragdoll, Recover, and Drop Prop omit **Edit action**. **Edit action** changes that action's settings; its type is fixed in the plan editor. **Change action** opens the existing slide-out action picker, omits the current type, and shows a **Replace: [current summary]** context line. A type choice opens its normal editor; changes apply only on **Done** (or **Done speech** for Say). Choosing a parameter-free type adds it immediately, or replaces the selected action immediately while preserving its ID, with no empty editor dialog. Cancel/Escape/outside click in the picker closes without changing the plan, and Cancel/Escape in the editor leaves the plan unchanged. The replacement keeps its stable action ID and remains selected. For a Together action, choices are filtered against the other action so the resulting pair stays valid; when no different type can fit, hide **Change action**. Move destination changes are made through **Edit action**, which opens the place/prop destination editor. In paused DIRECT, a completed or interrupted action's options retain **Erase action** with confirmation while **Edit action** is unavailable. There is no **Make someone react to this…** control until 20c. Bold solid up/down arrows reorder a step earlier or later inside 48 × 48 px targets; each has a descriptive tooltip and is disabled at the first/last position or when runtime state forbids reordering. Moving one step swaps only the two neighboring cards with a brief ~200 ms ease-out slide. The moved card remains selected and is drawn in front while they move; the same stable action ID preserves selection/spotlight through the list rebuild. Reorder buttons are disabled during the slide, and the prior scroll position and keyboard focus are restored afterward. Drag reorder is optional and cannot be the only route. Empty state: **Choose an action to start Milo’s plan.**
|
||||
|
||||
**+ Add step** opens a left-side **Choose an action** drawer with every supported action shown as a 48 px icon-and-label choice and a visible **Cancel** button pinned below the list. Its top aligns with the inspector, and it grows below the inspector to show all choices when the safe viewport allows. On shorter viewports, only the action list scrolls; Cancel stays visible and the drawer stays above the Stage status bar. Choosing an action with settings opens that action's editor; Say opens the speech composer. Parameter-free actions (Ragdoll, Recover, and Drop Prop) are added immediately after selection. The selected action type stays fixed in the new plan editor. A step is appended only after its complete draft is committed. **+ Add together** appears only when the selected step has a compatible action to add, and its picker offers only compatible choices: a one-action Say step offers every other action, while a different one-action step offers Say. The same two-action limit prevents Say+Say and primary-action pairs. Move setup asks **How should Milo move?** (Walk/Run) and **Where should Milo go?** (**Choose a prop**/**Choose a place**). The prop roster uses current stage names, a preview/highlight, and optional **Pick on stage**. Overlap picking reuses the 20a geometry-filtered chooser, including preview, Cancel, Escape, and no click-through. During plan place/prop capture, show one cursor-following instruction, a fixed **✕ Cancel** button, the cursor crosshair, and only the grounded route preview with a **Walk**/**Run** label; hide the duplicate prompt panel and generic diagonal trajectory. Place capture asks **Choose a place for Milo.**; prop capture asks **Choose the prop Milo will walk to.** Editing a target asks **Choose a new place or prop for Milo.** Capture is complete only after valid target selection and **Done** where a draft needs confirmation. **Cancel**/Escape discards the draft, creates no empty step, and causes no dirty mark. An invalid landing point says **Choose open ground for Milo.** For other target capture, prop/actor/area/waypoint selection shows only the cursor marker; direct Move To and rule Move previews stay horizontal at the actor's ground level. Throw retains its ballistic arc, and Set Down retains its prop/reach preview.
|
||||
|
||||
The existing speech composer creates exactly one Say action in the chosen step. Commit is atomic at **Done speech**; cancel does not create a step or replace the old Say. Existing bubble styling and content stay inside that action. **Erase action** asks **Erase this action?** via the shared confirmation card; Cancel preserves it. Removing the last action removes its step and renumbers later cards. A type replacement through **Change action** keeps its action ID because it explicitly replaces that selected action; later reaction cleanup rules belong to 20c.
|
||||
|
||||
### Stage preview and capture
|
||||
|
||||
- Keep the stage canvas quiet outside directing. In EDIT, hide authored action-plan routes, flags, Walk/Run badges, Say/Wait markers, and DIRECT actor badges. Keep placement feedback, the grid, selection outlines, and transform handles. When one or more named visible stickmen or props are selected, show a rounded, screen-sized badge for each selected object using its `stage_name`; use the EDIT cyan outline, position from the object's visual bounds, and prefer above the object. If a badge would collide with another badge, a transform handle, Stage chrome, an inspector, or the viewport edge, move it to the nearest clear adjacent position and add a short tether when displaced. Never omit a selected object's badge. Long names elide at the theme maximum width; the full name remains available through the single-object inspector or by selecting that object alone. Badges are read-only and input-transparent; Rename remains in the inspector. While **Objects here** previews a candidate, hide these persistent EDIT badges so the chooser's existing preview-name pill stays distinct.
|
||||
- DIRECT uses **Step Spotlight** only when exactly one character is selected. With no actor selected or with a multi-selection, hide authored action-plan routes, flags, movement badges, and Say/Wait markers; keep the amber DIRECT frame and the existing direction hint. With one character selected, show that actor's stage name in an amber rounded, screen-sized badge centered above the head. Show the focused step's full route and its action markers; hide routes for other actors. If the plan has no steps, keep the name badge and show no route. On first selection, focus the first pending step, or the active/next pending step while paused. A character switch applies the same default.
|
||||
- Movement steps outside the focus remain as faint numbered destination flags without route lines or action labels. These ghost flags keep their 44 px hit areas. Selecting a step card or ghost flag moves the focus without changing or dirtying the plan. A flag hit still resolves through the existing `action_id`, scrolls its step into view, and opens its Action options. Flags are not draggable; **Edit action** is the deliberate retarget entry and opens the destination editor.
|
||||
- Draw the focused route with its authored **Walk** or **Run** label. Use a bright 4 px cyan dashed line with a 6 px dark contrast edge, 12 px dashes, and 7 px gaps. Place the rounded, bold action badge near the first third of the route, away from the center crossing. Keep the destination label separate: an endpoint linked to a prop shows that prop's current `stage_name`, such as **Blue Door**; a free destination uses only its numbered flag, without a generic **Place 2** label or an inferred nearby prop name.
|
||||
- Round Move action badges have a dark surface, 2 px action-tinted border, bold 18–20 px light text, and generous padding. The actor-name pill uses a dark surface and 2 px DIRECT amber border, bold 20 px text, 12 px corners, and a 260 px maximum width; long names elide while the inspector retains the full stage name. Badges are visual only and do not intercept stage input.
|
||||
- A focused Move+Say step shows two stacked action pills near the first third of the grounded route: the movement glyph and **Walk**/**Run** label above a speech glyph and **Say** label. Use the Move cyan and Say yellow action colors; this pair replaces the separate Say marker for that step. A focused standalone Say shows a yellow-bordered pill with a speech glyph and **Say**, without a step number or leader. A focused standalone Wait shows a purple-bordered pill with an hourglass glyph and **Wait**, also without a step number or leader; place it just above the action’s predicted ground start point with the same safe-viewport placement as Say. A focused Wait+Say step shows both pills stacked near that point, with Wait above Say. In Step Spotlight, show each primary-action+Say Together step as two stacked, icon-bearing pills with the primary action above plain **Say**; fit the pair as one group against the name badge, inspector, and safe viewport. Place Move+Say near the first third of the grounded route; Wait, Ragdoll, Recover, Drop Prop, Set Down Prop, and Use near the predicted start; Throw Prop beside the character’s hands or upper torso. Keep Pick Up over its target prop and place its Say cue by the character so the target stays clear. Keep dialogue text in the inspector. In the plan overview, paired Move+Say keeps its route stack and other Say pairings remain compact. Markers from nonfocused steps are hidden. When prior steps contain unpredictable physics, show a conditional/uncertain anchor using existing preview semantics rather than pretending an exact simulated future position.
|
||||
- During retarget capture keep the old route/flag faint, even if selection is temporarily cleared. Draw a candidate route and flag with a validity state and relevant label at the pointer or exact picked prop. The old destination remains the authored value until a valid commit; Cancel/Escape restores its exact prior kind, ID, offset, and coordinate and preserves selection.
|
||||
- Factor the route/marker layout into one preview projection used by both drawing and hit testing. Its entries carry `action_id`, `step_index`, world points, label, status, and hit shape. This prevents a drawn flag from selecting a different flat queue index after reorder.
|
||||
|
||||
Use `SandboxTheme` as the sole Stage visual source: `stage_panels` for shell/type/action/focus sizes, `mode_colors.direct` for selection, `popups.anchored_chooser` for rosters, `popups.confirmation` for deletion, and a small `director_plan` subtree for card/badge/route/flag/speech/old-target/candidate states. Theme both `SandboxTheme.DEFAULT_CONFIG` and `sandbox_theme.json`; use local stylebox overrides instead of mutating its cached `Theme`. Button targets are at least 44 × 44 logical pixels, preferably 48 px. Fit at wide and approximately 920×560 viewports below Stage chrome; the header and quick actions remain visible while eight or more steps scroll. Long stage names wrap or elide with full tooltip and do not cover actions. Focus/hover/selected/disabled/invalid states are visually distinct and described with text or icon as well as color. In-place picker growth keeps its anchor and only nudges within the safe viewport.
|
||||
|
||||
## Canonical document model and identity
|
||||
|
||||
One placed character owns an ordered `action_plan: Array[Dictionary]`. Each step is `{ "actions": Array[Dictionary] }`, with one or two nonempty actions; no persisted step ID or number. Each action has an opaque nonempty **string** `action_id` plus the action's canonical `type` and parameters. IDs are unique across authored plans in one `.scene`, independent of actor name, step/order, destination, and action type. They survive save/load, stage rename, edit, regroup, and step reorder. A duplicated *placed* character receives a new `stage_object_id` (20a) and fresh action IDs for all copied plan actions. Deletion retires IDs; a new action never reuses them in that scene. A replacement through Edit action keeps the selected ID; **+ Add step** or **+ Add together** allocates a new ID only on valid commit. Cancel never consumes a persisted ID or mutates the plan. Scene names are presentation values only and never link keys.
|
||||
|
||||
Use a scene-scoped `StageActionIdentity` (`RefCounted`, not autoload) to allocate/check IDs against real and missing character entries. A stable ID may be an opaque `action_<random/counter>` string; its textual shape is not part of the UI or event contract. Legacy v1.8 migration derives deterministic IDs from `stage_object_id` plus original flat index, with deterministic collision suffixes. A current-format file with duplicate/blank IDs is rejected before World clear, not silently repaired. A missing `.stk` placeholder preserves the entire plan and IDs in its original entry through save and asset repair.
|
||||
|
||||
### Target payloads
|
||||
|
||||
The canonical Move action uses one discriminated `destination` dictionary. Its position values are authored world coordinates, encoded with the existing `{x,y}` vector helper. The runtime `target: Vector2` used by present locomotion is derived, not a second authoritative serialized target.
|
||||
|
||||
```json
|
||||
{
|
||||
"action_id": "action_opaque_17",
|
||||
"type": "move_to",
|
||||
"style": "walk",
|
||||
"destination": {
|
||||
"kind": "prop",
|
||||
"target_prop_id": "stage_opaque_4",
|
||||
"approach_offset": { "x": -42.0, "y": 0.0 },
|
||||
"last_known_approach": { "x": 310.0, "y": 420.0 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action_id": "action_opaque_18",
|
||||
"type": "move_to",
|
||||
"style": "run",
|
||||
"destination": { "kind": "place", "position": { "x": 510.0, "y": 420.0 } }
|
||||
}
|
||||
```
|
||||
|
||||
`target_prop_id` is the exact 20a `PropBlock.stage_object_id`, including missing-prop placeholders; `approach_offset` is a **world-aligned translation** from prop origin to the selected approach point. It does not rotate with the prop. `last_known_approach` is only an authored editor fallback for an unresolved/deleted prop link; PLAY motion does not rewrite it on save. It never turns the action into a free place or implies successful arrival. Prop rename changes displayed text but not this payload. A deleted target remains a repairable unresolved link labeled **Missing prop** and fails at PLAY if not retargeted. `kind: "place"` has no prop ID, even when its coordinate overlaps or is near a prop. Changing place commits a new destination dictionary on the same action ID. Validate finite vectors, known kinds, nonblank string prop IDs, and Walk/Run style; validate referenced prop IDs when present, while allowing an unresolved ID to preserve repairable authored intent. Never infer a prop link from coordinates. `ContentPaths.canonicalize()` remains on existing asset path fields; these identity and coordinate fields are not paths.
|
||||
|
||||
### Scene v1.9 boundary
|
||||
|
||||
`StageSceneIO.FILE_VERSION` becomes `"1.9"`. Current v1.9 character entries save `action_plan` and do not save a second authoritative `action_queue`. Add `encode_plan()`/`decode_plan()` or equivalent so IDs and destinations cannot be stripped by today's flat `_encode_flat_action()`/`_decode_flat_action()`. Preserve full speech payload normalization. Existing `event_rules`, their save-local `file_id` remap, background fields, and 20a stage identities/names stay intact. `StageSceneAdapter.collect()` writes the rig's canonical plan; `apply()` restores it after object spawn and identity assignment. Missing-character placeholders keep the canonical plan in `original_entry` so a save does not lose it.
|
||||
|
||||
For v1.0–v1.7, run the existing historical migrations, including the 20a identity/name pass, then migrate flat actions. For v1.8, retain its validated identities/names and migrate each valid flat `action_queue` entry to an ordered singleton step with a deterministic action ID; old `move_to.target` becomes a `place` destination. Existing malformed queue entries keep the present skip-and-warn behavior, while the current-format v1.9 shape is strictly validated. Preserve the precise old Sky/Plain and wall-tint version conditions: v1.8 must not receive older background defaults or wall rewrites. A migrated file saves as v1.9. `read_file()` migrates and validates the whole document before `StageSceneAdapter.apply()` clears World, including cross-actor action ID uniqueness, nonempty steps, compatibility, and destination shape.
|
||||
|
||||
The old `speak.keep_acting` overlay is a compatibility seam. Migration pairs a v1.8 overlay Say with its immediately following Move/Wait when that creates a compatible step, retaining order and approximate concurrent intent. Otherwise it makes a singleton Say with an explicit legacy-only nonblocking runtime flag to preserve old playback. The legacy flag is an exception to the new step barrier; new 20b authoring never creates it. Do not silently drop speech or reinterpret it as a prop/place target. Record paired and unpaired cases in migration tests. The new authored Say in a group remains active until its own bubble/line duration finishes, so its barrier is real.
|
||||
|
||||
## Runtime step execution
|
||||
|
||||
One runner owns the actor's canonical plan and an ephemeral execution cursor. At step start, snapshot both actions, mark both active, emit each `action_started` in plan order in the same physics tick, and start the primary track (Move/Wait) and optional Say track. The Say track's completion is the actual speech duration, not the instant its overlay is shown. Each action independently enters `PENDING`, `ACTIVE`, `SUCCEEDED`, `FAILED`, or `CANCELLED`; a step ends when all its actions have ended, then starts the next step on the next runner tick. Failure is an ended state and advances the plan; it is never reported as a reached endpoint. An empty plan completes immediately. STOP resets runtime status but does not change saved IDs or steps. Pausing freezes timers/pursuit and resumes them without consuming the bounded pursuit budget.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Pending
|
||||
Pending --> Active: step starts
|
||||
Active --> Succeeded: action succeeds
|
||||
Active --> Failed: missing target / blocked / timeout
|
||||
Active --> Cancelled: edit, delete, or STOP
|
||||
Succeeded --> [*]
|
||||
Failed --> [*]
|
||||
Cancelled --> [*]
|
||||
```
|
||||
|
||||
Keep `StickmanRig` as the public façade. Add typed plan APIs and typed event signals while adapting existing flat/index callers deliberately; do not persist or regenerate the old session-only `int` IDs as link keys. `ActionRegistry` owns an explicit authored-step compatibility predicate, separate from `ReactiveRole`/`overlay_eligible`, which still govern existing generic rule injection. Current generic rules and reactive actions remain operational, but are not converted into reaction plans. For 20b, preserve their present priority behavior and do not design 20d interruption/resume here.
|
||||
|
||||
| Owner | Proposed typed contract and work |
|
||||
| --- | --- |
|
||||
| `StageActionPlan extends RefCounted` (`scripts/stage/stage_action_plan.gd`) | `static func validate_steps(steps: Array[Dictionary]) -> String`; `static func can_add_together(step: Dictionary, action_type: String) -> bool`; pure deep-copy edit/reorder helpers returning a new plan or error. It owns structure/compatibility, no nodes or UI. |
|
||||
| `StageActionIdentity extends RefCounted` (`scripts/stage/stage_action_identity.gd`) | `func allocate(world: Node2D) -> String`; `func validate_scene_ids(objects: Array) -> String`; scan current scene and placeholders rather than maintaining a second mutable registry. |
|
||||
| `StickmanRig extends Node2D` | `func get_action_plan() -> Array[Dictionary]`; `func set_action_plan(steps: Array[Dictionary]) -> bool`; `func find_action(action_id: String) -> Dictionary`; `signal planned_action_started(action_id: String, action: Dictionary, step_index: int)`; `signal planned_action_ended(action_id: String, outcome: StringName, step_index: int)`. Existing `action_started/action_finished`, `arrived`, `get_queue()` and queue mutation callers need a tested compatibility path during migration. |
|
||||
| `StickmanActionRunner extends Node` | Execute current step with primary + Say tracks; track statuses by string action ID; expose `get_plan_view() -> Array[Dictionary]` with `step_index`, `action_id`, `action`, `status`; preserve `pause_simulation()`/`resume_simulation()` and active replace/delete semantics. A direct `get_queue_view()` can be a derived flat projection for legacy consumers during migration, never the persistence model. |
|
||||
| `StickmanLocomotion extends Node` | Add a lightweight `retarget_walk(target: Vector2) -> bool` for the current active Move: update `_walk_target_feet`/`NavigationAgent2D.target_position` without resetting gait, facing, pause, or settle history. Report reached versus blocked explicitly. |
|
||||
| `StageSceneIO extends RefCounted` / `StageSceneAdapter extends RefCounted` | v1.9 encode/decode/migrate/validate plan, actor IDs, prop links, and missing placeholders before live apply; preserve old rule remap and path canonicalization. |
|
||||
| `StageActionQueueController extends RefCounted` and `StageDirectorController extends Node` | Coordinate atomic draft/commit by string action ID, capture, step edits, pausable history guard, and plan UI intent. Convert delayed editor/capture references from index/session `int` to stable `String`; re-resolve against the live plan on commit. |
|
||||
| `StageDirectorVisuals extends Node2D` | Produce one route/marker projection shared by draw and hit. Hit result uses `action_id: String`, not a stale index. Keep rule visuals independent. |
|
||||
| `QueuePanel extends PopupPanel` / `ActionEditor extends PopupPanel` | Reuse action-type editing/composer behavior where it fits. The selected-character Step Stack becomes the primary DIRECT plan view; retire or adapt the flat queue popup without a second conflicting authoring source. Dialogs need their own focus/capture layout and should not be forced into `StageInspectorPanel`. |
|
||||
|
||||
Suggested event flow:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UI[Step Stack / flag hit] --> Intent[StageActionQueueController by action_id]
|
||||
Intent --> Draft[Editor or destination capture]
|
||||
Draft -->|Done and valid| Plan[StageActionPlan commit]
|
||||
Draft -->|Cancel| UI
|
||||
Plan --> Rig[StickmanRig plan facade]
|
||||
Rig --> Draw[StageDirectorVisuals projection]
|
||||
Rig --> IO[StageSceneAdapter -> StageSceneIO v1.9]
|
||||
Rig --> Runner[StickmanActionRunner]
|
||||
Runner -->|started / ended with outcome| Stage[SandboxStage listeners]
|
||||
```
|
||||
|
||||
All UI and visual pointers re-resolve action ID at commit/hit; index is only a derived display position. Selection and opening Action options are read-only. `queue_changed`/plan-changed refresh happens after one valid transaction; mark the Stage document dirty exactly once per commit. Cancel, invalid input, selection, and hover never mark dirty.
|
||||
|
||||
### Bounded pursuit for named props
|
||||
|
||||
At this Move's start, resolve `target_prop_id` to the exact live `PropBlock`, including a missing placeholder check. Project `prop.global_position + approach_offset` onto a reachable terrain approach point, retaining the offset in world axes even while the rigid body spins. While the Move is active, sample the prop's translation before locomotion's movement tick and retarget only after both a meaningful target shift and a minimum interval (initial tuning: at least 16 world units **and** 0.1 seconds since the last retarget) so a rolling ball does not restart the walk animation or repath every tiny physics move. The implementation may tune thresholds from rendered/behavior checks but must retain a finite budget (initial candidate: 12 seconds of unpaused active pursuit). A settled prop simply leaves the latest destination in place; if it moves again before arrival, retarget while budget remains.
|
||||
|
||||
Success requires the actor to finish arrival at the **current** ground-projected approach point *and* the live prop to be within the interaction reach used for an ordinary grounded actor/prop approach. Arrival at a stale point, or directly under an airborne out-of-reach prop, is not success: continue waiting/retargeting until reachable or timeout. A vanished/unresolvable prop, blocked/unreachable path, invalid ground projection, or expired budget ends the Move as `FAILED`, advances the step when its other action ends, and emits no successful endpoint moment. The failure may show brief child-facing feedback and an action status, without a blocking popup during PLAY. Freeze the budget during pause; STOP cancels it. Ensure `StickmanRig.arrived`/legacy waypoint rules do not receive a false intermediate arrival from a retargeted prop Move; later action-specific endpoint events must derive from `planned_action_ended(..., SUCCEEDED)` for that exact ID. A free-place Move keeps the existing finite walk target and arrival behavior.
|
||||
|
||||
This Move is finite pursuit of *one prop for one destination action*. It ends when reached or failed. The future 20c **Follow** action is a different authored action with its own explicit stop condition and continues to track after catch-up; do not turn every prop-linked Move into an indefinite Follow.
|
||||
|
||||
### Paused DIRECT invariants
|
||||
|
||||
Step reorder and Add together are allowed only on wholly future steps. A step containing any `ACTIVE`, `SUCCEEDED`, `FAILED`, `CANCELLED`, or legacy interrupted action cannot be regrouped or reordered; disable those structural controls with a concise reason. Completed/interrupted action **contents** cannot be edited, retargeted, or moved during this run, but their rows retain **Erase action** with the normal confirmation, matching Phase 12. That deletion removes the authored action for the next run while leaving consumed runtime history and already-emitted outcomes untouched; if it empties a step, remove that step from the saved plan without replaying or renumbering the live execution cursor. Preserve the existing ability to replace/delete the active action while paused: cancel that action's live track, keep the unaffected concurrent action/status in the same step, re-resolve by stable ID, then run the replacement or finish the step as appropriate. Editing a pending action preserves its ID and cannot alter an already-started Say event. Future-step edits rebuild only future runtime snapshots. Resume uses the modified future plan; STOP/PLAY starts from the authored beginning.
|
||||
|
||||
## Acceptance and verification plan
|
||||
|
||||
The roadmap requires automated behavior/persistence assertions, production-path interaction, and rendered UIUX review against the style guide/mockups. The checks below define the acceptance scope; completion status is recorded above.
|
||||
|
||||
### 1. Automated behavior and persistence
|
||||
|
||||
1. Validate singleton steps and Say paired with every other registered action type; reject primary+primary, Say+Say, a third action, empty steps, duplicate/blank action IDs, malformed destination variants, nonfinite vectors, and unknown style. Confirm **+ Add together** appears on a standalone Say step and offers all other action types, while every other singleton offers Say. Confirm the picker omits choices that would create an invalid pair. Confirm all edit/reorder/regroup/retarget operations retain IDs, duplicate actor regenerates every copied action ID, and invalid/cancel creates no saved mutation.
|
||||
2. Load v1.0–v1.8 fixtures; assert Phase 20a IDs/names, background/wall migrations, deterministic legacy action IDs, fixed Move targets becoming places, and current `.scene` v1.9 save/reopen preserving every action, speech payload, prop target ID/offset, and existing generic rules. Reject malformed current v1.9 plans before the live World is cleared. Save/reload missing actor and missing/deleted prop references without silently turning them into coordinates.
|
||||
3. Run a small repeated step timing scenario: a plan with `Step 1 = Walk 180 units + Say 3 seconds`, `Step 2 = Wait 0.5 seconds`. Record physics timestamps for both Step 1 starts, Walk end, Say end, and Wait start. Both starts occur in the same runner tick; Wait starts only after `max(Walk end, Say end)`. Repeat with Wait+Say and Recover+Say, STOP/PLAY, and pause/resume. A failed Move still lets its Say finish before the next step.
|
||||
4. Run a repeatable moving-prop scenario on simple Ground: actor Move targets a named rigid ball with a fixed world-left offset; move and spin the ball during the action. Confirm destination updates with translation but not rotation, gait does not restart on retarget, and success occurs only after the actor reaches the latest approach and the ball is within reach. Repeat with an airborne ball over the actor (no early success), a removed target, a blocking prop/path, timeout, and pause (budget frozen). Assert no reached-endpoint event on failure or stale intermediate arrival.
|
||||
5. Check existing generic rule/queue callers and old `keep_acting` migration cases. A new grouped Say remains active to its real end. Existing rule `file_id` remaps, prop Use/Pick Up IDs, and `ContentPaths` behavior survive.
|
||||
|
||||
### 2. Production-path interaction
|
||||
|
||||
In the real Sandbox Stage, verify EDIT with no selection has no plan markers; selecting a stickman and a prop individually shows one cyan stage-name badge each; mixed multi-selection shows every selected named object badge without overlap or input interception; and the **Objects here** candidate preview suppresses persistent badges. Keep each selected object's badge anchor stable through translate/rotate/scale, release, camera pan, and zoom. Reposition only if that anchor becomes obstructed by the actor, another badge, a transform handle, Stage chrome, an inspector, or the safe viewport; keep unaffected multi-selection badges anchored. Plan routes and action markers stay hidden throughout EDIT. In DIRECT, verify an empty stage with no selection has no plan markers, and selecting exactly one actor restores Step Spotlight. Place the actor-name badge at least 24 screen pixels above the actual visible head shape, including headwear; when the top safe area prevents that placement, move the badge beside the head with a short tether and keep it inside the safe viewport. Select an empty-plan Milo, see the empty message and fixed quick actions, then dismiss on empty stage and reselect. Add Move by Walk/Run and exact prop/place selection; add Say to the selected step via **+ Add together**, then append Wait with its quick action. Select an action in Step 2, then click Step 1's plain title; verify the old action options close and its amber highlight clears. Verify that clicking blank card space and each child control selects its owning card while preserving that control's action. Hover over a step title and verify the text remains stationary with no tooltip. Tab to a card, verify the white inset focus outline, and use Enter/Space to select it. Verify **↑**/**↓** buttons reorder future steps, preserve action IDs, and remain disabled at boundaries or when runtime state forbids reordering. Observe Step 1 **Together** and Step 2 Wait, play and observe concurrent Walk+Say with the barrier before Wait. On a standalone Say step, open **+ Add together** and verify the drawer offers every other registered action; on any other singleton, verify it offers Say. Confirm the picker never offers Say+Say or primary+primary and the two-action card cap stays enforced. Use the reorder arrows and verify numbering, routes, action IDs, and saved order.
|
||||
|
||||
Select Ragdoll, Recover, or Drop Prop from the add/change picker and verify each commits immediately without opening an editor; replacement keeps the action ID, and parameter-free cards have no **Edit action** button.
|
||||
|
||||
Place two instances of the same prop asset, select one by current distinct stage name/visual preview, rename it in EDIT, and verify the Move card/flag update while the saved target ID stays fixed. Place a free destination beside that prop and verify it remains **Place N**. In DIRECT, select a multi-step character and verify only the focused route is bright, other actors' routes are hidden, and other movement steps retain faint numbered flags; click a step card and a ghost flag to switch focus without dirtying the scene. Check the rounded name/action badges, long-name elision, route contrast over sky/building/grass, and that marker labels stay away from route crossings. Tap one numbered flag: the correct existing action opens with no dirty mark or move. Open **Edit action**, inspect the old faint and candidate routes while retargeting, Cancel/Escape, then retarget to the other prop and to a free place; verify the same action ID and saved destination variant each time. Open **Change action** on a standalone Move, verify the slide-in picker lists other types, then Cancel and confirm the plan stays clean. Choose Wait, Cancel its editor, and confirm the Move and ID remain; repeat and commit, then verify Wait replaced Move under the same ID. In a Move+Say Together step, verify changing Move offers all other primary actions except Move, while Say has no **Change action** control; in Wait+Say, changing Wait offers all other primary actions except Wait. Check that Pick Up stays anchored to its prop while Say stays by the actor, and that Recover+Say pills stack without overlap. Check invalid terrain, overlap chooser Cancel, keyboard-only target selection, and speech composer Cancel. Pause during a grouped step: active replace/delete works; grouping/reorder offers only future steps. After one action completes or is interrupted, verify its options offer confirmed Erase but not Edit action; delete it, resume without replaying history, then Reset and confirm the deletion affects the next run.
|
||||
|
||||
### 3. Rendered captures and UIUX review
|
||||
|
||||
Capture the actual GUI states reached by those interactions, not an isolated screenshot generator: EDIT empty selection, selected actor/prop names, mixed multi-selection labels, badge position during a real drag, and overlap chooser preview; DIRECT empty selection, focused Step 1, clear name-to-head gap, and top-edge tether fallback; the Choose an action picker with scrolling and a pinned Cancel button; the Together picker for a standalone Say showing every other action and for another singleton offering Say, without compatibility helper copy; Walk+Say stacked action pills and a later Wait; a prop action+Say with Pick Up over its prop and Say by the actor; Recover+Say with distinct stacked action pills; exact prop roster/preview and 20a overlap chooser; free place near a prop; selected flag/options with stacked Edit, Change, and Erase controls; the Change action drawer for a standalone action and for a Together pair with only compatible replacement choices; Cancel from the drawer/editor and a committed replacement preserving the action ID; Move retarget old/candidate/invalid/Cancel through Edit action; focused standalone Say and Wait pills, stacked Wait+Say pills, and compact overview markers; paused active/future controls; and playback start/barrier frames. Include wide and approximately 920×560 viewports with a long character name and eight or more steps, visible header/quick buttons, body scrolling, bold 48×48 px reorder arrows, an animated adjacent-card swap with the moved card in front, persistent selected-card/action identity, preserved scroll/focus, plain stationary step titles with no tooltip, blank-card and child-control selection, keyboard focus outline, and safe capture bar placement. Compare against the branch-local mockups and `docs/guides/editor_visual_style_guide.md`; record any intentional deviation. An automated assertion, log line, or image file alone is not proof that the production interaction and visual state were reached.
|
||||
|
||||
## Implementation and verification record
|
||||
|
||||
### Delivered
|
||||
|
||||
- **Plan data and identity:** Scene v1.9 stores ordered `action_plan` steps with stable string `action_id` values. Move destinations explicitly identify a free `place` or a prop by its stable stage object ID and world-aligned approach offset. Legacy flat queues are migrated to plans.
|
||||
- **Step execution:** A step may contain one primary action and one Say. They start together; the next step waits until both finish. The runner records per-action outcomes, and a failed Move does not count as reaching its endpoint. Named-prop pursuit follows translation without rotating the saved offset, pauses its time budget with the simulation, and fails on a missing target, blocked or unreachable route, or expired pursuit budget.
|
||||
- **DIRECT authoring:** The inspector provides the ordered Step Stack, quick actions, compatible Add together choices, action editing/replacement, cancellation, and step reordering. Selection follows the same action through reorder; route spotlight and on-stage action markers follow the focused step.
|
||||
- **Target and prop feedback:** Stage target capture uses a visible Cancel and action-specific preview. Pickup selection uses friendly prop names and eligible-target feedback; Set Down highlights only valid support surfaces. Prop interaction safety protects an intended pickup target from its approaching actor and gives the releasing actor collision grace after Set Down.
|
||||
|
||||
### Verification evidence
|
||||
|
||||
- The automated action-plan suite recorded **45 checks, 0 failures** in [the final log](../../tests/phase20b_action_plan_test_final.log). Its checks cover plan validation and serialization, stable IDs, grouped-action timing/barriers, pause behavior, moving-prop retargeting, and missing-target failure. The test source is [test_phase20b_action_plan.gd](../../tests/test_phase20b_action_plan.gd).
|
||||
- Focused production-GUI runs recorded passing results for selected slices, including grouped authoring, action drawer, step spotlight/reorder, flag selection, and Move editing. Examples include [grouping](../../tests/phase20b_grouping_success_check.log), [Step Spotlight](../../tests/phase20b_step_spotlight_final.log), and [action drawer](../../tests/phase20b_action_drawer_full_height.log). These are targeted slice results, not a single run of every item in the broader GUI checklist above.
|
||||
- The user reported completing and passing the following seven manual scenarios:
|
||||
1. **Airborne target:** pursuing a prop above the actor does not count as arrival before it is reachable.
|
||||
2. **Blocked route:** a blocked Move does not pass through the obstacle or report success, and eventually fails.
|
||||
3. **Timeout and missing target:** both cases fail without an arrival result or a stalled plan.
|
||||
4. **Pause during pursuit:** pausing longer than the pursuit limit does not consume the budget; pursuit resumes toward the prop's current position. Fixture: [Test 4](../../content/stages/phase20b_test4_pause_during_pursuit.scene).
|
||||
5. **Save, reload, rename, and delete:** a prop Move retains its identity through rename and remains repairable after deletion; a nearby free destination remains a place target. Fixture: [Test 5](../../content/stages/phase20b_test5_target_identity.scene).
|
||||
6. **DIRECT editing and focus:** Move+Say grouping, adding Wait, selection/focus changes, reorder identity, and Change Action cancellation behave as expected. Fixture: [Test 6](../../content/stages/phase20b_test6_direct_editing_focus.scene).
|
||||
7. **Target picking and cancel:** Escape cancels place, Pick Up, and Set Down capture without committing the draft; the inspector and target feedback recover. Fixture: [Test 7](../../content/stages/phase20b_test7_target_picking_cancel.scene).
|
||||
|
||||
### Remaining boundary
|
||||
|
||||
The user-verified seven-case matrix and the cited automated/GUI slices do not establish that every detailed item in the broad acceptance matrix was independently exercised and captured. In particular, the complete production-interaction matrix and a recorded UI/UX review remain unrecorded as a single sign-off. Phase 20b does not include event-specific reaction authoring or reaction runtime; those remain in Phases 20c–20e.
|
||||
|
||||
## Decisions and trade-offs
|
||||
|
||||
- **Nested steps and string IDs:** A true step array makes order and barrier semantics explicit. Persistent opaque strings cost more migration and adaptation than flat session indices but keep later event links stable under edits and saves. Step numbers remain derived, avoiding unnecessary step identity.
|
||||
- **Explicit destination kind:** The discriminated payload prevents proximity inference and keeps prop rename separate from identity. A cached last-known approach supports repairable missing targets; it never acts as an implicit fallback success location.
|
||||
- **Finite live pursuit:** Retargeting by world-aligned offset supports moving/spinning props without creating a perpetual Follow. Throttling avoids gait resets/repath churn; a bounded budget means a continually moving prop eventually fails, so the plan cannot stall forever.
|
||||
- **One primary plus one Say:** A user update broadened the rule to every currently registered primary action type. The explicit pair preserves the runner's existing primary/speech split, prevents ambiguous primary+primary and Say+Say steps, and still defers a general parallel action scheduler until later phases need evidence for one.
|
||||
- **Shared Stage shell, specialized plan body:** Extend `StageInspectorPanel` for pinned quick actions and use shared theme/capture/dialog patterns. The plan card and route preview remain domain-specific; modal action editing keeps its own focus model.
|
||||
- **Legacy behavior:** Migrating flat queues is deterministic, while old nonblocking `keep_acting` may need a narrow compatibility flag. It must be visibly tested and must not leak into newly authored grouped steps.
|
||||
|
||||
> Historical note: The user approved this spec before implementation. The implementation and verification record above documents the current delivery and evidence status.
|
||||
Reference in New Issue
Block a user