Add prop interaction system and throw mechanics specification

- 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.
This commit is contained in:
2026-09-25 23:42:26 -04:00
parent 730a9a966b
commit 6a1359f244
22 changed files with 3433 additions and 34 deletions
+332 -9
View File
@@ -52,7 +52,7 @@ enum RunState { STOPPED, RUNNING, PAUSED }
enum RuleStep { IDLE, SELECT_TRIGGER, TRIGGER_TARGET, SELECT_ACTION, ACTION_TARGET, ACTION_POSITION, PARAMS }
## Phase 3c target-capture kinds (what the next stage click resolves to).
enum CaptureKind { NONE, WAYPOINT, AREA, PROP, STICKMAN, POSITION }
enum CaptureKind { NONE, WAYPOINT, AREA, PROP, STICKMAN, POSITION, THROW_POSITION }
enum PendingFileAction { NONE, NEW_SCENE, OPEN_DIALOG, OPEN_PATH, QUIT }
# ---------------------------------------------------------------------------
@@ -106,6 +106,9 @@ const TRIG_SPEECH_FINISHED := 2
const TRIG_ENTERED_AREA := 3
const TRIG_COLLIDED := 4
const TRIG_BACK := 5
const TRIG_PROP_PICKED_UP := 6
const TRIG_PROP_DROPPED := 7
const TRIG_PROP_THROWN := 8
## Phase 4 "add another / done" popup item ids.
const RULE_MORE_ADD := 0
@@ -118,6 +121,9 @@ const RULE_ACTION_DONE := 6
## Phase 3c action-popup item ids ("Edit Queue…" / "Edit Rules…").
const ACT_EDIT_QUEUE := 7
const ACT_EDIT_RULES := 8
const ACT_PICK_UP_PROP := 9
const ACT_DROP_PROP := 10
const ACT_THROW_PROP := 11
## Phase 3c stickman right-click context-menu item ids.
const RIG_CTX_EDIT_QUEUE := 0
@@ -276,6 +282,7 @@ var _edit_physics_settle_frames: int = 0
var _action_popup: PopupMenu = null
var _context_rig: StickmanRig = null
var _pending_walk_target: bool = false
var _next_stage_prop_serial: int = 0
var _speak_dialog: AcceptDialog = null
var _speak_edit: LineEdit = null
var _speak_stop_current_check: CheckBox = null
@@ -399,6 +406,9 @@ var _capture_kind: CaptureKind = CaptureKind.NONE
var _capture_hint: String = ""
var _capture_callback: Callable = Callable()
var _capture_cancel: Callable = Callable()
var _capture_throw_preview_rig: StickmanRig = null
var _capture_throw_preview_prefix: Array[Dictionary] = []
var _throw_preview_cache: Dictionary = {}
## Walk-edit state (Phase 3c.3): re-placing an existing walk_to target.
var _walk_edit_rig: StickmanRig = null
@@ -492,6 +502,7 @@ func _process(delta: float) -> void:
func _physics_process(delta: float) -> void:
_update_throw_preview_cache()
if _run_state == RunState.RUNNING:
_elapsed_seconds += delta
_set_timestamp(_elapsed_seconds)
@@ -507,6 +518,19 @@ func _physics_process(delta: float) -> void:
if current_mode == StageMode.PLAY and _event_dispatch_ready and _run_state == RunState.RUNNING:
_update_area_entry()
func _update_throw_preview_cache() -> void:
if _capture_kind != CaptureKind.THROW_POSITION or _capture_throw_preview_rig == null or not is_instance_valid(_capture_throw_preview_rig):
_throw_preview_cache.clear()
return
if _camera == null:
_throw_preview_cache.clear()
return
var target := _camera.get_global_mouse_position()
if _snap_enabled:
target = _snap_to_grid(target)
_throw_preview_cache = _capture_throw_preview_rig.preview_prop_throw(target, _capture_throw_preview_prefix)
# ---------------------------------------------------------------------------
# Input
# ---------------------------------------------------------------------------
@@ -924,6 +948,7 @@ func _reset_run() -> void:
for node: Node2D in _world_children_selectable():
if node is STICKMAN_RIG:
var rig := node as StickmanRig
rig.reset_prop_interactions_for_scene_exit()
rig.stop_queue()
rig.set_terrain_following_enabled(false)
rig.snap_to_standing()
@@ -1118,6 +1143,11 @@ func _place_at(world_pos: Vector2) -> void:
## Common post-spawn wiring: freeze kinematic props in EDIT, save authored state,
## and connect the Phase 3a/4 signals for stickmen/props.
func _finalize_placed_node(node: Node2D) -> void:
if node is PropBlock:
var placed_prop := node as PropBlock
if placed_prop.stage_object_id.is_empty():
placed_prop.stage_object_id = _new_stage_prop_id()
placed_prop.ensure_default_grip()
if current_mode == StageMode.EDIT and node is RigidBody2D:
var body := node as RigidBody2D
body.freeze_mode = RigidBody2D.FREEZE_MODE_KINEMATIC
@@ -1125,11 +1155,13 @@ func _finalize_placed_node(node: Node2D) -> void:
_save_object_state(node)
if node is STICKMAN_RIG:
var rig := node as STICKMAN_RIG
rig.set_prop_resolver(Callable(self, "_resolve_prop_by_stage_id"))
rig.queue_changed.connect(_on_rig_queue_changed)
rig.arrived.connect(_on_rig_arrived.bind(rig))
rig.action_finished.connect(_on_rig_action_finished.bind(rig))
rig.speech_finished.connect(_on_rig_speech_finished.bind(rig))
rig.state_changed.connect(_on_rig_state_changed.bind(rig))
rig.prop_interaction_event.connect(_on_rig_prop_interaction_event.bind(rig))
_director_visuals.mark_dirty()
if node is PropBlock:
var prop := node as PropBlock
@@ -1137,6 +1169,39 @@ func _finalize_placed_node(node: Node2D) -> void:
prop.separated.connect(_on_prop_separated.bind(prop))
func _new_stage_prop_id() -> String:
while true:
_next_stage_prop_serial += 1
var candidate := "prop_%d_%d_%d" % [Time.get_ticks_usec(), randi(), _next_stage_prop_serial]
if not _stage_prop_id_in_use(candidate):
return candidate
return ""
func _stage_prop_id_in_use(stage_id: String) -> bool:
for child: Node in _world.get_children():
if child is PropBlock and (child as PropBlock).stage_object_id == stage_id:
return true
if child.get_script() == MISSING_PROP_PLACEHOLDER and String(child.get("stage_object_id")) == stage_id:
return true
return false
func _resolve_prop_by_stage_id(stage_id: String) -> PropBlock:
if stage_id.is_empty():
return null
for child: Node in _world.get_children():
if child is PropBlock and (child as PropBlock).stage_object_id == stage_id:
return child as PropBlock
return null
func _on_rig_prop_interaction_event(event_type: String, prop: PropBlock, reason: String, rig: StickmanRig) -> void:
if prop == null or not is_instance_valid(prop) or rig == null or not is_instance_valid(rig):
return
_handle_event({ "type": event_type, "source": rig, "target": prop, "position": prop.global_position, "reason": reason, "action": {} })
func _spawn_ghost() -> void:
_free_ghost()
if _placement_id == "":
@@ -1559,9 +1624,11 @@ func _collect_scene_data(is_autosave: bool = false) -> Dictionary:
elif type == "prop" and n.get_script() == MISSING_PROP_PLACEHOLDER:
entry = (n.get("original_entry") as Dictionary).duplicate(true)
entry["file_id"] = file_id; entry["type"] = "prop"; entry["position"] = STAGE_SCENE_IO.vector_to_data(pos); entry["rotation"] = rot; entry["scale"] = STAGE_SCENE_IO.vector_to_data(n.scale)
entry["stage_object_id"] = String(n.get("stage_object_id"))
elif type == "prop":
var prop := n as PropBlock
entry["asset_id"] = prop.spawn_id
entry["stage_object_id"] = prop.stage_object_id
var prop_scale: Vector2 = authored.get("scale", prop.scale)
entry["scale"] = STAGE_SCENE_IO.vector_to_data(prop_scale)
entry["material_preset"] = prop.material_preset
@@ -1662,6 +1729,7 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
continue
(node as TriggerArea).size = area_size
if node is PropBlock:
(node as PropBlock).stage_object_id = String(entry.get("stage_object_id", ""))
var preset_names: Array = PropBlock.MaterialPreset.keys()
var raw_preset: Variant = entry.get("material_preset", PropBlock.MaterialPreset.NONE)
var index := -1
@@ -2333,6 +2401,9 @@ func _build_ui() -> void:
_action_popup.add_item("⏳ Wait", ACT_WAIT)
_action_popup.add_item("💥 Ragdoll", ACT_RAGDOLL)
_action_popup.add_item("🔄 Recover", ACT_RECOVER)
_action_popup.add_item("🫴 Pick Up Prop…", ACT_PICK_UP_PROP)
_action_popup.add_item("⬇️ Drop Prop", ACT_DROP_PROP)
_action_popup.add_item("🏹 Throw Prop…", ACT_THROW_PROP)
_action_popup.add_separator()
_action_popup.add_item("⚡ When...", ACT_WHEN)
_action_popup.add_separator()
@@ -2348,6 +2419,9 @@ func _build_ui() -> void:
_trigger_popup.add_item("💬 Finishes speaking", TRIG_SPEECH_FINISHED)
_trigger_popup.add_item("🎯 Enters trigger area", TRIG_ENTERED_AREA)
_trigger_popup.add_item("💥 Collides with something", TRIG_COLLIDED)
_trigger_popup.add_item("🫴 Picks up a prop", TRIG_PROP_PICKED_UP)
_trigger_popup.add_item("⬇️ Drops a prop", TRIG_PROP_DROPPED)
_trigger_popup.add_item("🏹 Throws a prop", TRIG_PROP_THROWN)
_trigger_popup.add_separator()
_trigger_popup.add_item("⬅ Back to actions", TRIG_BACK)
_trigger_popup.id_pressed.connect(_on_trigger_popup_id_pressed)
@@ -2359,6 +2433,9 @@ func _build_ui() -> void:
_rule_action_popup.add_item("⏳ Wait", ACT_WAIT)
_rule_action_popup.add_item("💥 Ragdoll", ACT_RAGDOLL)
_rule_action_popup.add_item("🔄 Recover", ACT_RECOVER)
_rule_action_popup.add_item("🫴 Pick Up Prop…", ACT_PICK_UP_PROP)
_rule_action_popup.add_item("⬇️ Drop Prop", ACT_DROP_PROP)
_rule_action_popup.add_item("🏹 Throw Prop…", ACT_THROW_PROP)
_rule_action_popup.id_pressed.connect(_on_rule_action_popup_id_pressed)
ui.add_child(_rule_action_popup)
@@ -3110,6 +3187,18 @@ func _on_action_popup_id_pressed(id: int) -> void:
_context_rig.queue_action({ "type": "ragdoll" })
ACT_RECOVER:
_context_rig.queue_action({ "type": "recover" })
ACT_PICK_UP_PROP:
_begin_capture(CaptureKind.PROP, "🫴 Click the prop to pick up (Esc or RMB to cancel)", _cb_direct_pickup_prop, _cb_direct_capture_cancel)
ACT_DROP_PROP:
_context_rig.queue_action({ "type": "drop_prop" })
ACT_THROW_PROP:
_begin_throw_position_capture(
_context_rig,
"🏹 Click where to throw (Esc or RMB to cancel)",
_cb_direct_throw_target,
_cb_direct_capture_cancel,
_throw_preview_prefix_for(_context_rig)
)
ACT_WHEN:
_begin_rule_build(_context_rig.get_instance_id())
ACT_EDIT_QUEUE:
@@ -3118,6 +3207,26 @@ func _on_action_popup_id_pressed(id: int) -> void:
_open_rules_panel_for_rig(_context_rig)
func _cb_direct_pickup_prop(value: Node2D) -> void:
if _context_rig != null and is_instance_valid(_context_rig) and value is PropBlock:
var prop := value as PropBlock
if not prop.stage_object_id.is_empty():
_context_rig.queue_action({ "type": "pick_up_prop", "target_prop_id": prop.stage_object_id, "hand": "auto" })
else:
_show_toast("That prop is missing its scene ID")
_context_rig = null
func _cb_direct_throw_target(value: Vector2) -> void:
if _context_rig != null and is_instance_valid(_context_rig):
_context_rig.queue_action({ "type": "throw_prop", "target_position": value })
_context_rig = null
func _cb_direct_capture_cancel() -> void:
_context_rig = null
# ---------------------------------------------------------------------------
# Director UX (Phase 4b): cursor-attached tooltip, trajectory, ghost marker
# ---------------------------------------------------------------------------
@@ -3136,6 +3245,12 @@ func _action_hint_text() -> String:
if _pending_walk_target:
return "🚩 Click to set walk target"
if _capture_kind != CaptureKind.NONE:
if _capture_kind == CaptureKind.THROW_POSITION and not _throw_preview_cache.is_empty():
if not bool(_throw_preview_cache.get("preview_available", true)):
return "ℹ %s Click to set the throw target." % String(_throw_preview_cache.get("preview_reason", "No throw preview is available."))
var preview_hint := String(_throw_preview_cache.get("preview_hint", ""))
if not preview_hint.is_empty():
return "%s %s" % [_capture_hint, preview_hint]
return _capture_hint
match _rule_step:
RuleStep.TRIGGER_TARGET:
@@ -3157,6 +3272,10 @@ func _action_hint_text() -> String:
func _action_origin() -> Vector2:
if _capture_kind == CaptureKind.THROW_POSITION and _capture_throw_preview_rig != null and is_instance_valid(_capture_throw_preview_rig):
if not _throw_preview_cache.is_empty():
return _throw_preview_cache.get("start_position", _capture_throw_preview_rig.global_position)
return _capture_throw_preview_rig.global_position
if _pending_walk_target:
if _context_rig != null and is_instance_valid(_context_rig):
return _context_rig.global_position - StickmanLocomotion.FOOT_OFFSET
@@ -3191,9 +3310,37 @@ func _update_action_overlay() -> void:
if awaiting:
var origin := _action_origin()
var target := _camera.get_global_mouse_position()
if _snap_enabled and _rule_step == RuleStep.ACTION_POSITION:
if _snap_enabled and (_rule_step == RuleStep.ACTION_POSITION or _capture_kind == CaptureKind.THROW_POSITION):
target = _snap_to_grid(target)
_placement_overlay.set_action_trajectory(origin, target, _is_target_valid(target))
if _capture_kind == CaptureKind.THROW_POSITION:
if _capture_throw_preview_rig == null or not is_instance_valid(_capture_throw_preview_rig):
_placement_overlay.set_throw_preview_unavailable(target)
else:
var throw_preview := _throw_preview_cache
if throw_preview.is_empty():
_placement_overlay.set_throw_preview_unavailable(target)
elif not bool(throw_preview.get("preview_available", true)):
_placement_overlay.set_throw_preview_unavailable(target)
else:
var trajectory_points: PackedVector2Array = throw_preview.get("trajectory_points", PackedVector2Array())
var effective_target: Vector2 = throw_preview.get("effective_target", target)
var collision_position: Vector2 = throw_preview.get("collision_position", Vector2.INF)
var target_reachable := bool(throw_preview.get("target_reachable", true))
var preview_planned := bool(throw_preview.get("preview_planned", false))
var preview_uncertain := bool(throw_preview.get("preview_uncertain", false))
_placement_overlay.set_throw_trajectory(
throw_preview["start_position"],
target,
throw_preview["landing_position"],
target_reachable,
trajectory_points,
effective_target,
collision_position,
preview_planned,
preview_uncertain
)
else:
_placement_overlay.set_action_trajectory(origin, target, _is_target_valid(target))
else:
_placement_overlay.clear_action()
if _tooltip != null:
@@ -3317,7 +3464,7 @@ func _on_trigger_popup_id_pressed(id: int) -> void:
var trigger: Dictionary = _rule_builder.get("trigger", {})
trigger["type"] = _trigger_type_string(id)
_rule_builder["trigger"] = trigger
if id == TRIG_ARRIVED or id == TRIG_ENTERED_AREA or id == TRIG_COLLIDED:
if id == TRIG_ARRIVED or id == TRIG_ENTERED_AREA or id == TRIG_COLLIDED or id == TRIG_PROP_PICKED_UP or id == TRIG_PROP_DROPPED or id == TRIG_PROP_THROWN:
_rule_step = RuleStep.TRIGGER_TARGET
match id:
TRIG_ARRIVED:
@@ -3326,6 +3473,8 @@ func _on_trigger_popup_id_pressed(id: int) -> void:
_rule_hint = "Click the trigger area to watch (Esc or RMB to cancel)"
TRIG_COLLIDED:
_rule_hint = "Click the prop that will be collided with (Esc or RMB to cancel)"
TRIG_PROP_PICKED_UP, TRIG_PROP_DROPPED, TRIG_PROP_THROWN:
_rule_hint = "Click the prop to watch (Esc or RMB to cancel)"
_refresh_status()
else:
_rule_step = RuleStep.SELECT_ACTION
@@ -3366,6 +3515,12 @@ func _trigger_type_string(id: int) -> String:
return "entered_area"
TRIG_COLLIDED:
return "collided"
TRIG_PROP_PICKED_UP:
return "prop_picked_up"
TRIG_PROP_DROPPED:
return "prop_dropped"
TRIG_PROP_THROWN:
return "prop_thrown"
_:
return ""
@@ -3382,6 +3537,12 @@ func _action_type_string(id: int) -> String:
return "ragdoll"
ACT_RECOVER:
return "recover"
ACT_PICK_UP_PROP:
return "pick_up_prop"
ACT_DROP_PROP:
return "drop_prop"
ACT_THROW_PROP:
return "throw_prop"
_:
return ""
@@ -3398,6 +3559,12 @@ func _action_verb(id: int) -> String:
return "ragdoll"
ACT_RECOVER:
return "recover"
ACT_PICK_UP_PROP:
return "pick up a prop"
ACT_DROP_PROP:
return "drop a prop"
ACT_THROW_PROP:
return "throw a prop"
_:
return "act"
@@ -3436,7 +3603,7 @@ func _handle_trigger_target_click(world_pos: Vector2) -> void:
else:
_rule_hint = "Click a trigger area (Esc or RMB to cancel)"
_refresh_status()
"collided":
"collided", "prop_picked_up", "prop_dropped", "prop_thrown":
var hit2 := _selection.hit_test(world_pos)
if hit2 is PropBlock:
trigger["target"] = hit2.get_instance_id()
@@ -3476,8 +3643,12 @@ func _handle_action_target_click(world_pos: Vector2) -> void:
_rule_step = RuleStep.PARAMS
_wait_spin.value = 1.0
_wait_dialog.popup_centered()
"pick_up_prop":
_begin_capture(CaptureKind.PROP, "🫴 Click the prop to pick up (Esc or RMB to cancel)", _cb_rule_action_prop_target, _cb_rule_action_capture_cancel)
"throw_prop":
_begin_throw_position_capture(hit as StickmanRig, "🏹 Click where to throw (Esc or RMB to cancel)", _cb_rule_action_throw_target, _cb_rule_action_capture_cancel)
_:
# ragdoll / recover need no params.
# Ragdoll, recover, and drop need no extra parameters.
_open_rule_more_popup()
@@ -3496,6 +3667,45 @@ func _handle_action_position_click(world_pos: Vector2) -> void:
_open_rule_more_popup()
func _cb_rule_action_prop_target(value: Node2D) -> void:
if not value is PropBlock:
return
var prop := value as PropBlock
if prop.stage_object_id.is_empty():
_show_toast("That prop is missing its scene ID")
return
var actions: Array = _rule_builder.get("actions", [])
if actions.is_empty():
return
var action: Dictionary = actions.back()
var params: Dictionary = action.get("params", {})
params["target_prop_id"] = prop.stage_object_id
params["hand"] = String(params.get("hand", "auto"))
action["params"] = params
actions[actions.size() - 1] = action
_rule_builder["actions"] = actions
_open_rule_more_popup()
func _cb_rule_action_throw_target(value: Vector2) -> void:
var actions: Array = _rule_builder.get("actions", [])
if actions.is_empty():
return
var action: Dictionary = actions.back()
var params: Dictionary = action.get("params", {})
params["target_position"] = value
action["params"] = params
actions[actions.size() - 1] = action
_rule_builder["actions"] = actions
_open_rule_more_popup()
func _cb_rule_action_capture_cancel() -> void:
_rule_step = RuleStep.ACTION_TARGET
_rule_hint = "Click the stickman who will act (Esc or RMB to cancel)"
_refresh_status()
## Writes dialog-confirmed params onto the last action in the builder.
func _set_rule_action_params(params: Dictionary) -> void:
var actions: Array = _rule_builder.get("actions", [])
@@ -3737,6 +3947,8 @@ func _rule_matches(rule: Dictionary, event: Dictionary) -> bool:
return _ids_match(source_id, source) and _ids_match(target_id, target)
"collided":
return _ids_match(source_id, source) and _ids_match(target_id, target)
"prop_picked_up", "prop_dropped", "prop_thrown":
return _ids_match(source_id, source) and _ids_match(target_id, target)
_:
return false
@@ -3776,6 +3988,11 @@ func _action_for_rig(a: Dictionary) -> Dictionary:
out["stop_current"] = bool(params.get("stop_current", false))
"wait":
out["duration"] = float(params.get("duration", 0.0))
"pick_up_prop":
out["target_prop_id"] = String(params.get("target_prop_id", ""))
out["hand"] = String(params.get("hand", "auto"))
"throw_prop":
out["target_position"] = params.get("target_position", Vector2.ZERO)
return out
@@ -3952,19 +4169,58 @@ func _begin_capture(kind: CaptureKind, hint: String, on_resolve: Callable, on_ca
_capture_hint = hint
_capture_callback = on_resolve
_capture_cancel = on_cancel
_capture_throw_preview_rig = null
_capture_throw_preview_prefix.clear()
_throw_preview_cache.clear()
_apply_cursor()
_refresh_status()
func _begin_throw_position_capture(rig: StickmanRig, hint: String, on_resolve: Callable, on_cancel: Callable = Callable(), queue_prefix: Array[Dictionary] = []) -> void:
_begin_capture(CaptureKind.THROW_POSITION, hint, on_resolve, on_cancel)
_capture_throw_preview_rig = rig if rig != null and is_instance_valid(rig) else null
_capture_throw_preview_prefix = queue_prefix.duplicate(true)
_throw_preview_cache.clear()
func _end_capture() -> void:
_capture_kind = CaptureKind.NONE
_capture_hint = ""
_capture_callback = Callable()
_capture_cancel = Callable()
_capture_throw_preview_rig = null
_capture_throw_preview_prefix.clear()
_throw_preview_cache.clear()
_apply_cursor()
_refresh_status()
func _throw_preview_prefix_for(rig: StickmanRig, before_action_id: int = -1) -> Array[Dictionary]:
var result: Array[Dictionary] = []
if rig == null or not is_instance_valid(rig):
return result
var queue_view: Array[Dictionary] = rig.get_queue_view()
var limit := queue_view.size()
if before_action_id >= 0:
limit = rig.index_for_action_id(before_action_id)
if limit < 0:
return result
for index: int in mini(limit, queue_view.size()):
var entry: Dictionary = queue_view[index]
var status := int(entry.get("status", StickmanActionRunner.QueueStatus.NOT_STARTED))
if status == StickmanActionRunner.QueueStatus.COMPLETED or status == StickmanActionRunner.QueueStatus.INTERRUPTED:
continue
var action: Dictionary = entry.get("action", {})
if action.is_empty():
continue
result.append({
"action": action.duplicate(true),
"step": index + 1,
"active": status == StickmanActionRunner.QueueStatus.ACTIVE,
})
return result
func _cancel_capture() -> void:
var cb := _capture_cancel
_end_capture()
@@ -3993,6 +4249,8 @@ func _resolve_capture(world_pos: Vector2) -> void:
value = hit3
CaptureKind.POSITION:
value = _snap_to_grid(world_pos) if _snap_enabled else world_pos
CaptureKind.THROW_POSITION:
value = _snap_to_grid(world_pos) if _snap_enabled else world_pos
if value == null:
_refresh_status()
return
@@ -4412,8 +4670,12 @@ func _on_action_editor_committed(flat: Dictionary) -> void:
else:
_show_toast("That action can no longer be used as an insert point")
"rule_add":
_pending_rule_action_flat = flat
_begin_capture(CaptureKind.STICKMAN, "🎯 Click the stickman who will act (Esc or RMB to cancel)", _cb_rule_actor, _cb_rule_actor_cancel)
if _action_editor_actor_id >= 0:
if _rule_editor != null:
_rule_editor.set_action(-1, ACTION_REGISTRY.to_rule_action(flat, _action_editor_actor_id))
else:
_pending_rule_action_flat = flat
_begin_capture(CaptureKind.STICKMAN, "🎯 Click the stickman who will act (Esc or RMB to cancel)", _cb_rule_actor, _cb_rule_actor_cancel)
return
"rule_edit":
if _rule_editor != null:
@@ -4435,7 +4697,33 @@ func _on_action_editor_cancelled() -> void:
func _on_action_editor_target_requested() -> void:
_action_editor.hide()
_begin_capture(CaptureKind.POSITION, "🚩 Click where to walk (Esc or RMB to cancel)", _cb_editor_walk_target, _cb_editor_walk_target_cancel)
var action_type := _action_editor.current_action_type()
if _action_editor_kind == "rule_add" and _action_editor_actor_id < 0 and (action_type == "pick_up_prop" or action_type == "throw_prop"):
_begin_capture(CaptureKind.STICKMAN, "🎯 Click the stickman who will act (Esc or RMB to cancel)", _cb_editor_actor_for_target, _cb_editor_actor_capture_cancel)
return
_begin_editor_action_target_capture()
func _begin_editor_action_target_capture() -> void:
match _action_editor.current_action_type():
"pick_up_prop":
_begin_capture(CaptureKind.PROP, "🫴 Click the prop to pick up (Esc or RMB to cancel)", _cb_editor_prop_target, _cb_editor_walk_target_cancel)
"throw_prop":
var preview_rig := _action_editor_preview_rig()
var queue_prefix: Array[Dictionary] = []
if _action_editor_kind == "queue_edit":
queue_prefix = _throw_preview_prefix_for(preview_rig, _action_editor_action_id)
elif _action_editor_kind == "queue_add":
queue_prefix = _throw_preview_prefix_for(preview_rig)
_begin_throw_position_capture(
preview_rig,
"🏹 Click where to throw (Esc or RMB to cancel)",
_cb_editor_throw_target,
_cb_editor_walk_target_cancel,
queue_prefix
)
_:
_begin_capture(CaptureKind.POSITION, "🚩 Click where to walk (Esc or RMB to cancel)", _cb_editor_walk_target, _cb_editor_walk_target_cancel)
func _cb_editor_walk_target(value: Vector2) -> void:
@@ -4443,6 +4731,41 @@ func _cb_editor_walk_target(value: Vector2) -> void:
_action_editor.set_walk_target(value)
func _cb_editor_prop_target(value: Node2D) -> void:
if _action_editor != null and value is PropBlock:
var prop := value as PropBlock
if not prop.stage_object_id.is_empty():
_action_editor.set_prop_target(prop.stage_object_id)
else:
_action_editor.popup_centered()
func _cb_editor_throw_target(value: Vector2) -> void:
if _action_editor != null:
_action_editor.set_throw_target(value)
func _cb_editor_actor_for_target(value: Node2D) -> void:
if not value is StickmanRig:
if _action_editor != null:
_action_editor.popup_centered()
return
_action_editor_actor_id = int(value.get_instance_id())
_begin_editor_action_target_capture()
func _cb_editor_actor_capture_cancel() -> void:
if _action_editor != null:
_action_editor.popup_centered()
func _action_editor_preview_rig() -> StickmanRig:
if _action_editor_rig != null and is_instance_valid(_action_editor_rig):
return _action_editor_rig
var actor := instance_from_id(_action_editor_actor_id) as StickmanRig
return actor if actor != null and is_instance_valid(actor) else null
func _cb_editor_walk_target_cancel() -> void:
if _action_editor != null:
_action_editor.popup_centered()