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:
+332
-9
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user