- Introduced `stickman_prop_interactor.gd` to manage prop interactions. - Created `phase_11_prop_interactions.md` detailing the design and implementation of prop pickup, drop, and throw mechanics. - Defined interaction states, action lifecycles, and event triggers for prop handling. - Specified constraints for throw actions, including target positioning and velocity calculations. - Established acceptance criteria for prop interactions and lifecycle management.
28 KiB
Phase 11 — Prop Interactions and Event Triggers
Status: Approved for implementation (2026-09-24).
Throw UX amendment: Approved for implementation (2026-09-25).
Throw landing-target amendment: Approved for implementation (2026-09-25).
Queue-aware throw-preview amendment: Approved for implementation (2026-09-25).
Source: plans/PHASE_11_PROP_INTERACTIONS.md. This spec keeps the requested
pickup, drop, throw, ragdoll handoff, and “When…” features while adapting them to
the current Sandbox Stage architecture and closing the design gaps identified
during review.
1. Approved design decisions
- A throw action stores a fixed stage-world target position. At run time the trajectory starts at the actor’s current held-prop transform and aims at that saved point. Actor-relative throws are deferred.
- Phase 11 supports one prop held in one compatible hand per stickman. Two-handed grips and holding multiple props are deferred.
- Each placed prop has a persistent per-instance stage ID. The ID is
separate from the asset’s
spawn_idand from Godot’s session-only instance ID. Duplicates of the same library prop therefore remain distinguishable.
1.1 Previously approved throw UX amendment (2026-09-25; superseded by §1.2)
This amendment refines the approved throw action and does not add fields to its
saved payload. It supersedes the exact-landing interpretation of decision 1
and §3.3 while keeping target_position as a fixed stage-world point.
- Cursor meaning: the saved point is an aim-and-power handle. At release, the vector from the predicted release point to that handle sets the launch bearing; its length sets launch speed. The prop is allowed to land short of or beyond the cursor as gravity and collisions dictate.
- Power curve: map cursor distance monotonically and smoothly to launch speed, clamp at a maximum, and keep the limits in one set of tunable constants. Initial tuning recommendation: 300 px/s at minimum input, rising to a 1,250 px/s cap at 700 px of cursor distance. Distances beyond that stay at the cap. Retain the existing near-zero-distance no-op guard.
- Prediction: preview and execution use the same release point, cursor vector, power mapping, velocity, gravity, and prop gravity scale. Draw the sampled ballistic path and show the cursor separately from the predicted impact point. Use the held prop's collision shape to estimate its first world collision along the path, ignoring the actor and held prop; if no collision occurs within the existing 2.0-second maximum horizon, mark the horizon endpoint. This remains an estimate for moving bodies.
- Overhand motion: replace the short backward/upward wrist offset with a head-relative IK arc. Lift the held prop behind and beside the head, carry it above the crown, then sweep forward over the head. Release during the forward sweep and finish with a follow-through. Derive points from the live rig pose and facing, and clamp them within the selected arm's reachable length so the IK solver does not snap or overextend. A small torso lean may support the motion; do not reposition the legs.
- Release origin: calculate the preview from the prop's predicted
attachment transform at the release waypoint, not its pre-windup transform.
Recompute the same origin at the release frame before applying the bounded
launch velocity. Keep the action payload as
{ type, target_position }; power is derived at runtime, so existing saved queues remain readable.
Acceptance checks for the amendment: launch speed increases monotonically with cursor distance until the cap; launch bearing follows the cursor vector; preview points agree with the chosen release pose and solver; the overhand hand path clears the head and remains within arm reach; release happens during the forward sweep; the predicted impact marker is distinct from the cursor.
1.2 Approved throw landing-target amendment (2026-09-25)
This approved amendment supersedes §1.1's aim-and-power mapping. It keeps the
saved { type, target_position } payload and treats its fixed stage-world point
as the requested landing/aim point.
- Range cap: cap the raw cursor point to a 3,600 px radius from the stickman's root position at preview time and again when the throw action starts. If it is farther away, project it onto that radius along the root-to-cursor vector. The root is the intuitive range reference; the actual ballistic path still starts at the prop's predicted hand-release transform. A cursor within the existing 4 px no-op threshold of the root skips the throw and leaves the prop held. Keep the raw saved world point unchanged, so queued actions remain readable. When a queued actor has moved by the time the throw starts, recompute only the effective capped point from its current root position.
- Target solver: in unobstructed flight, solve the launch velocity so the
prop reaches the effective target on the descending part of its arc. Continue
using project gravity and the prop's
gravity_scale. Search flight times from 0.35 s to the 2.0 s horizon and select the shortest valid time whose launch speed does not exceed 2,100 px/s. This favors a fast trajectory while preserving the selected endpoint. If no valid candidate exists, show the target as unreachable and skip the throw without releasing the prop. - Preview and physical collisions: preview and execution use the same effective target, release transform, selected flight time, velocity, gravity, and prop gravity scale. Shape-cast the held prop along the path, excluding the thrower's collision bodies and the prop itself. A world collision before the target ends the guide at its first safe contact point. If no collision occurs before the requested endpoint, the guide reaches that endpoint. If the point is in open air, the prop continues under normal physics after passing it; the guide marks the requested point rather than the rest of that continuing flight. Moving-body impacts remain an estimate.
- Target markers: show the raw cursor separately from the effective capped target. When a collision truncates the guide before that target, show the collision point separately as well. Thus a cursor at 1,200 px from the root shows an effective target at 3,600 px, while retaining the cursor marker at 1,200 px.
- Prop weight/material: keep one shared range and speed policy in this
change. Physical mass does not change gravitational acceleration. A future
balance pass may add an explicit gameplay throwability factor derived from
prop weight or material; it must not silently change the physics mass or
existing
.propschema.
The previously approved overhand IK path, its reach clamp, forward-sweep release, follow-through, and release-origin prediction remain in force.
Acceptance checks for this amendment: a target within 3,600 px remains exact; farther targets clamp to a 3,600 px radius from the current stickman root; the cursor and effective target markers remain distinct; the solver chooses the shortest valid descending arc under the speed cap; earlier collisions truncate the guide; unreachable targets release no prop; and queued throws preserve the raw fixed world point while recomputing a bound when necessary.
1.3 Approved queue-aware throw-planning preview (2026-09-25)
At queue-edit time, a throw may follow a queued pickup that has not executed yet. The preview must model that planned held-prop state without mutating the live rig, prop, queue, reservations, or physics. This is a planning estimate; runtime continues to solve from the actual held prop and release transform.
- Queue prefix: for an edited throw, inspect authored actions before its
stable action ID. For a newly appended queue or Direct throw, inspect the
current queue as the prefix. Start from the rig's current held-prop state,
then fold only pending actions in order: a valid
pick_up_propselects its stage prop ID and requested hand;drop_proporthrow_propclears the virtual held prop. Completed actions are already reflected in live state and are not replayed. A missing or incompatible prop leaves the preview unavailable. - Predicted pickup pose: use the same grip-selection and pickup stand-off
calculations as runtime. For an out-of-reach pickup, predict the approach
root position from the selected grip and
APPROACH_GAP; also account for preceding authoredwalk_totargets. This predicts the ordinary queue case without starting locomotion or attaching the prop. If an earlier action makes the future actor or prop transform indeterminate (for example, an active ragdoll or a prior physical throw), do not present the path as exact; show a planning/uncertain status and use the best available pose estimate. - Pure preview context: pass the resolved prop, chosen grip/hand, and
predicted root transform as explicit preview inputs to the existing
gravity-aware solver and collision shape cast. Do not temporarily set the
interactor's
_held_prop, reserve the prop, change the hand target, or move either scene object to make the preview work. - Feedback: show a plan-preview hint such as “Assumes Step 2 pickup succeeds.” Keep raw cursor, capped target, and collision markers. Reserve red for a genuinely unreachable solved target; if no planned or live prop can be resolved, show a neutral explanation instead of a misleading straight red trajectory. Preview status must update as the queue prefix or target changes.
- Scope: queue edit, queue append, and Direct throw captures have a known authored prefix and use this model. Rule-authored throws have no fixed action prefix, so they use the live held-prop state and report preview unavailable when no prop is currently held.
Acceptance checks for this amendment: at t = 0, editing a pending throw after
a pending pickup displays a gravity/collision-aware planning guide; the preview
does not change prop ownership, transforms, reservations, the queue, or live
physics; a preceding drop/throw clears the inferred held prop; the predicted
pickup approach moves the guide origin; and actual execution recomputes the
trajectory from the real release transform. A missing, invalid, or ambiguous
prefix never masquerades as a solver-unreachable red trajectory.
2. Existing contracts and scope
StickmanRigis the public runtime facade. It already composes separate Locomotion, Ragdoll, and ActionRunner components underRigComponents.StickmanActionRunnerowns authored queue IDs and emits one start/finish pair per authored action. Walking is currently a normal queue action. Pickup’s internal approach must not be inserted into the authored queue: pickup remains one authored action, with one action ID and one completion.StickmanLocomotion.walk_to()drives the existing navigation/terrain behavior, but its final root displacement is horizontal. Prop approach must choose a grounded stand point and verify hand reach; it cannot assume arbitrary 2D walking.PropAnchorDataalready normalizes grip roles, allowed hands, local transforms, and attachment-transform math.PropFilealready serializes anchors, butPropLibrarycurrently drops them andStageSpawnerdoes not pass them intoPropBlock. Phase 11 must close that runtime data path.StickmanRiguses animated IK target markers. A prop hold/throw pose needs a single explicit owner for the active hand target after animation updates and before the IK solve. Do not let an AnimationPlayer track and the interaction code fight over the same target.- Rule/action templates live in
TriggerRegistryandActionRegistry.SandboxStagehandles rule matching and event dispatch; there is no currentStageEventRegistry. Extend these existing paths. PropBlock.begin_play_instance_scale()moves authored instance scale into its visuals/collision during PLAY. Grip alignment must use that effective scale, not assume the RigidBody root retains the EDIT scale.
In scope: one-hand pickup, drop, and targeted throw; automatic approach; prop/rig state handoff; stage-scene persistence for prop identity and queue payloads; Direct and queue/rule authoring; and three edge-triggered prop rules.
Out of scope: two-hand or multiple-prop holding; torso/leg bending to improve pickup reach; prop-to-prop mounting; melee hitboxes; moving-target tracking; actor-relative throws; a general-purpose event bus rewrite; and document-wide undo/redo.
3. Runtime ownership and action lifecycle
Add a StickmanPropInteractor component under each rig’s RigComponents.
It owns the selected hand, held prop reference, chosen grip ID, reservation,
saved collision/freeze settings, active interaction phase, and recent held
transform samples. StickmanRig exposes a narrow facade to the ActionRunner
and calls the interactor’s ragdoll/reset hooks. The ActionRunner remains the
owner of authored queue progression; it delegates prop work to the interactor
and waits in a prop-interaction phase until that action succeeds, fails, or is
cancelled.
The interaction state is:
| State | Contract |
|---|---|
| Idle | No prop is held and no pickup is reserved. |
| Approaching | The pickup action owns one prop reservation and uses locomotion to reach a grounded stand point. It is still one active authored action. |
| Reaching | The chosen arm target blends to the chosen grip; the prop is not yet reported as held. |
| Held | Exactly one prop is aligned to the solved wrist and cannot be claimed by another rig. |
| Throwing | The arm performs windup/follow-through; release is committed once at the release boundary. |
Cancellation before pickup attachment stops the approach/arm blend and releases the prop reservation. Cancellation of a throw before its release boundary leaves the prop held; cancellation after release never pulls it back and only ends the remaining follow-through.
Action payloads:
| Type | Runtime payload | Behavior |
|---|---|---|
pick_up_prop |
`{ type, target_prop_id: String, hand: "auto" | "left" |
drop_prop |
{ type } |
Release the currently held prop. If none is held, complete as a skipped no-op. |
throw_prop |
{ type, target_position: Vector2 } |
Throw the held prop toward the saved stage-world point. If none is held, complete as a skipped no-op. |
Skipped means the queue item reaches its existing completed state with a
no-op outcome; it does not add a new persistent queue status. The existing
action_finished signal continues to mean that a queue item ended. The
dedicated prop_* triggers below are the success signals for actual prop
state changes.
3.1 Pickup selection and auto-approach
At action start, resolve target_prop_id to a live PropBlock in this stage.
Reject a missing ID, a placeholder, an already-held/reserved prop, a rig
already holding a prop, or a hand/anchor combination with no compatible grip.
These failures finish the queue item as skipped and emit no pickup event.
Use the prop’s normalized authored anchors. For hand: "auto", consider
compatible grip/hand pairs, prefer primary_grip, then choose the reachable
pair with the shortest wrist-to-grip distance. An explicit left/right request
must use that hand; do not silently switch hands when its selected grip forbids
it. If no editable grip exists, use a generated one-hand fallback grip at the
center of the prop’s visual bounds. Built-in props use that fallback unless
their template provides an explicit grip.
Measure reach from the selected solved wrist to the transformed grip, not from the rig root to the prop origin. If the grip is out of reach, compute a grounded stand point on a reachable side of the prop and ask existing locomotion to walk there. The stand point must account for current terrain height and the chosen hand. After arrival, re-resolve the prop, recompute its grip transform, and confirm actual reach before starting the arm blend. A moving prop may cause at most one approach replan; if it is still unreachable, release the reservation and skip the action. No unbounded “chase the prop” loop is allowed.
Reservations are claimed synchronously when pickup begins so two actors cannot
both approach and attach the same prop. Release the reservation on success,
failure, cancellation, prop deletion, or actor deletion. Do not emit a
walk_to completion or waypoint-arrival event for the internal approach.
3.2 Attachment and hold
Interpolate the selected hand’s IK target to the grip over a short reach blend.
Once the arm is solved, align the prop with
PropAnchorData.solve_attachment_transform(), using the solved wrist
transform, the grip’s local transform, effective PLAY scale, and the
left-facing mirror convention. Apply the anchor’s z-index offset relative to
the rig’s draw order.
Keep the PropBlock under the stage World. Do not reparent it to the animated
rig or use reparenting as the attachment mechanism. During hold, use
RigidBody2D.FREEZE_MODE_KINEMATIC, update the prop transform from the solved
wrist in the interaction update, and temporarily disable its collision layer
and mask so it cannot snag terrain, its owner’s collision proxies, or generate
ordinary contact events while held. Save and restore all modified physics
settings on release. The hold transform must be updated only after the wrist
pose has been solved for that frame.
Sample the held prop transform each physics frame. Those samples provide
release linear/angular velocity for drops and ragdoll disarm; do not use
_last_grounded_walk_velocity_x as full actor momentum. It is a grounded,
horizontal locomotion value and does not describe airborne or wrist motion.
3.3 Drop and throw
A manual drop detaches at the current prop transform, restores its collision settings and dynamic state, and applies the most recent finite sampled attachment velocity. A throw uses the same release handoff, then overrides linear velocity with the trajectory solver result and adds a small deterministic spin. Release and its event are committed exactly once.
The approved baseline uses a bounded ballistic solver and no-op guards. The approved §1.2 amendment changes its target handling as follows:
- Keep the saved absolute
target_position. At preview and throw start, compute the effective point by clamping its radial distance from the current stickman root toMAX_THROW_RANGE(3,600 px). Use the prop's actual predicted attachment transform at the release waypoint as the ballistic origin. - For a candidate flight time
t, compute the required initial velocity as(effective_target - release_position - 0.5 * gravity * t * t) / t. Search[0.35 s, 2.0 s]and choose the shortest finite candidate that reaches the effective point on descent and stays at or underMAX_THROW_SPEED(2,100 px/s). The time interval is the preferred tuning seam if playtesting shows that the default arc needs to be more or less lofted. - If no candidate can reach the target within those bounds, mark the preview invalid and leave the prop held when the action runs. Never substitute an undershot point while presenting it as the requested landing target.
The preview uses the same effective target, release transform, flight time, velocity, gravity, and prop gravity scale as execution. Shape-cast the prop along the path up to the effective target, excluding the thrower's collision bodies and the held prop. An earlier world collision truncates the guide at first safe contact; otherwise the guide reaches the effective target. In open air, the target is an aim point and the physical prop continues until a later collision. Show the raw cursor, effective capped target, and an earlier collision point as separate markers.
A queue-authored throw may use the queue-aware planning preview in §1.3. Its inferred prop, grip/hand, and estimated root pose are immutable solver inputs; the preview must not simulate pickup by changing live ownership or transforms. Runtime release always recomputes from actual state.
A cursor within the 4 px zero-distance threshold of the stickman root is a
no-op and leaves the prop held. Godot's positive-Y-down gravity vector must be used consistently. Set
linear_velocity from the solved velocity; do not pass a velocity vector to
apply_central_impulse(). Never pass a NaN or infinite vector into physics.
Prop mass does not change the solver result; prop-specific throwability remains
future tuning work.
The rig faces toward the saved target before throw windup when the horizontal offset is outside a small dead zone; it retains that direction afterward. Use a scripted arm IK pose for windup, release, and follow-through. Do not directly rotate the torso or reposition the legs in this phase.
4. Ragdoll and Sandbox lifecycle
Before StickmanRig.set_ragdoll(true) resets locomotion or begins the ragdoll
handoff, ask the interactor to detach any held prop. Restore its collision and
physics settings and apply the latest sampled attachment velocity. This path is
idempotent and works for direct ragdoll, collision-triggered ragdoll, and a
queued ragdoll action.
Ragdoll disarm emits one prop_dropped event with reason ragdoll; it does
not run the manual drop animation. A manual drop emits the same event with
reason manual. A scene reset or leaving PLAY detaches silently, clears
reservations/pose overrides, then lets the existing authored-state restore
return props to their starting transforms. It must not create a gameplay rule
event during cleanup.
Pause/resume preserves a held attachment and its sampled transform state. Reset/restart restores the unheld authored prop state. Prop interaction actions require an animated rig; if queued while ragdolled, use the existing recovery barrier behavior before pickup/throw. A no-held drop/throw after recovery remains a skipped no-op.
5. Events and authoring
Add ActionRegistry entries and labels/icons for pick_up_prop, drop_prop,
and throw_prop. Extend ActionEditor/QueuePanel serialization and the Direct
action popup so actions can be added and edited. Direct popup labels should
read 🫴 Pick Up Prop…, ⬇️ Drop Prop, and 🏹 Throw Prop…. Pick Up
captures a prop; Throw captures a stage-world point; Drop queues immediately.
The When/Then action editor must support those same payloads, including the
second stage capture needed to choose the prop or throw point after the acting
stickman is selected.
Add TriggerRegistry entries for:
| Type | Source | Target | Fire condition |
|---|---|---|---|
prop_picked_up |
Acting stickman | Picked-up prop | After grip alignment and held-state ownership commit. |
prop_dropped |
Acting stickman | Released prop | After manual or ragdoll release commits; event payload includes the reason. |
prop_thrown |
Acting stickman | Thrown prop | At the exact release boundary, after physics velocity is assigned. |
Prop trigger rules select a stickman source and prop target. Use the existing ordered, all-matching rule evaluation and normal reactive action queues. Emit events from the interactor’s successful state transitions, not merely because an ActionRunner item finished. Do not emit while a prop is being continuously held, after a failed action, or during scene cleanup. Rule actions can target a different stickman and can refer to their own prop ID/throw point.
User-authored rules may form a real cycle (for example, pick up → drop → pick up). Do not synthesize rules or dispatch prop events inline from a failed attempt. Keep transition events edge-triggered and preserve the existing stage-run/event-dispatch guards.
6. Stage identity, anchors, and saved data
Add a non-empty, opaque stage_object_id to every spawned PropBlock.
Generate it for each placement/instance; never copy it from the asset or from a
duplicated prop. Keep spawn_id exclusively as library/template provenance.
The stage owns resolution and uniqueness checks. Missing custom-prop
placeholders preserve the same ID so replacing a missing asset with its real
asset restores queue/rule references.
Bump StageSceneIO to version 1.1. Keep reading 1.0 scenes; migration assigns
each prop’s current unique file_id as its initial stage_object_id.
Write stage_object_id on each prop entry thereafter. Reject duplicate or
empty IDs in a 1.1 file before applying it, preserving the current scene on
load failure. Existing per-save file_id mappings continue to serialize
runtime rule references; do not use Godot instance IDs as saved references.
StageSceneIO queue encode/decode and rule-action conversion must preserve
target_prop_id as a stable string and target_position as finite vector
data. A queued action whose prop ID no longer resolves stays in the queue and
finishes as skipped when run. A missing-prop placeholder is inert and cannot
be picked up. A rule action with a missing prop target is skipped at execution;
it must not crash or silently retarget another prop.
Carry anchors from PropLibrary.make_custom_entry() through StageSpawner
into PropBlock. Built-in props get a generated centered one-hand grip from
their visual bounds unless their template already defines one. Preserve the
existing .prop anchor schema; do not add runtime-only hand state to .prop
assets.
7. Implementation sequence
- Identity and data path: persistent stage IDs, 1.0→1.1 scene migration, action payload serialization, and custom/built-in grip availability.
- Pickup/drop vertical slice: PropInteractor, one-hand state ownership, approach/revalidation, hold pose, drop, action cancellation, and Direct / queue editing.
- Throw: fixed world-target capture, root-relative 3,600 px range clamp, shortest-valid bounded landing solver, facing/overhand arm arc, release velocity, collision-aware trajectory preview, and queue-prefix planning context for pending throw targets.
- Lifecycle: ragdoll disarm, PLAY pause/resume/reset, deletion, and recovery-barrier integration.
- Rules: three prop triggers, source/target capture, When/Then prop actions, persistence, and missing-target behavior.
Each step should be headless-verifiable with pure serialization/solver cases and followed by editor/runtime verification in the Sandbox Stage.
8. Acceptance criteria
- A distant pickup approaches using one active queue item, then attaches one compatible grip to one hand without emitting a synthetic walk event.
- Left/right facing, authored anchor position/rotation, prop scale, and z-index offset produce stable grip alignment. Built-in props remain pickable.
- Missing, deleted, reserved, incompatible, or unreachable pickup targets fail safely, release their reservation, and emit no successful prop event.
- Drop restores physics/collision properties and transfers sampled release motion. Drop and throw with no held prop complete as skipped no-ops.
- A throw queued before the actor moves keeps its saved stage-world point; if the range cap binds at runtime, the effective point is clamped from the actor's then-current root. Preview and runtime use the same gravity and target solver.
- A throw within range reaches its selected point on the descending arc when unobstructed; targets beyond range show the 3,600 px effective point separately from the cursor. Earlier collisions stop the guide at the collision point.
- At
t = 0, a pending throw after a pending pickup has a non-mutating, queue-aware planning guide; no held prop or invalid prefix shows a neutral explanation instead of a false red ballistic path. - Ragdoll releases a held prop before hiding the animated rig, transfers sampled motion, and emits exactly one ragdoll-reason drop event.
- Pause/resume preserves the hold; PLAY reset/exit releases silently and restores authored state.
- Prop pickup/drop/throw triggers fire exactly once after their respective committed transitions and execute matching rules in existing rule order.
- Prop IDs and new queue/rule action payloads survive save/load, missing custom prop placeholders, and object reordering without binding to the wrong copy.
Final user sign-off on §1.3 was received 2026-09-25. Implementation and editor verification are complete (2026-09-25).