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:
@@ -22,10 +22,16 @@ var _text_edit: LineEdit = null
|
||||
var _duration_spin: SpinBox = null
|
||||
var _target_label: Label = null
|
||||
var _set_target_btn: Button = null
|
||||
var _prop_target_label: Label = null
|
||||
var _set_prop_target_btn: Button = null
|
||||
var _hand_option: OptionButton = null
|
||||
var _stop_current_check: CheckBox = null
|
||||
var _show_reactive_options: bool = false
|
||||
var _has_target: bool = false
|
||||
var _target_pos: Vector2 = Vector2.ZERO
|
||||
var _prop_target_id: String = ""
|
||||
var _has_throw_target: bool = false
|
||||
var _throw_target_pos: Vector2 = Vector2.ZERO
|
||||
|
||||
## Theme font overrides (set via apply_font from sandbox_theme.json). Size 0 = no
|
||||
## override (engine default); Font null = no override.
|
||||
@@ -53,6 +59,9 @@ func open_new(show_reactive_options: bool = false) -> void:
|
||||
_initial = {}
|
||||
_has_target = false
|
||||
_target_pos = Vector2.ZERO
|
||||
_prop_target_id = ""
|
||||
_has_throw_target = false
|
||||
_throw_target_pos = Vector2.ZERO
|
||||
_select_type("walk_to")
|
||||
_rebuild_params("walk_to")
|
||||
title = "Add Action"
|
||||
@@ -67,6 +76,9 @@ func open_edit(action: Dictionary, show_reactive_options: bool = false) -> void:
|
||||
var type := String(action.get("type", "walk_to"))
|
||||
_has_target = action.has("target") and action["target"] is Vector2
|
||||
_target_pos = action.get("target", Vector2.ZERO) if _has_target else Vector2.ZERO
|
||||
_prop_target_id = String(action.get("target_prop_id", ""))
|
||||
_has_throw_target = action.has("target_position") and action["target_position"] is Vector2
|
||||
_throw_target_pos = action.get("target_position", Vector2.ZERO) if _has_throw_target else Vector2.ZERO
|
||||
_select_type(type)
|
||||
_rebuild_params(type)
|
||||
title = "Edit Action"
|
||||
@@ -81,6 +93,23 @@ func set_walk_target(pos: Vector2) -> void:
|
||||
popup_centered()
|
||||
|
||||
|
||||
func set_prop_target(prop_id: String) -> void:
|
||||
_prop_target_id = prop_id
|
||||
_update_prop_target_label()
|
||||
popup_centered()
|
||||
|
||||
|
||||
func set_throw_target(pos: Vector2) -> void:
|
||||
_has_throw_target = true
|
||||
_throw_target_pos = pos
|
||||
_update_throw_target_label()
|
||||
popup_centered()
|
||||
|
||||
|
||||
func current_action_type() -> String:
|
||||
return _current_type()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 12)
|
||||
@@ -184,6 +213,9 @@ func _rebuild_params(type: String) -> void:
|
||||
_duration_spin = null
|
||||
_target_label = null
|
||||
_set_target_btn = null
|
||||
_prop_target_label = null
|
||||
_set_prop_target_btn = null
|
||||
_hand_option = null
|
||||
_stop_current_check = null
|
||||
|
||||
match type:
|
||||
@@ -198,6 +230,39 @@ func _rebuild_params(type: String) -> void:
|
||||
_set_target_btn.pressed.connect(func() -> void: target_requested.emit())
|
||||
trow.add_child(_set_target_btn)
|
||||
_update_walk_target_label()
|
||||
"pick_up_prop":
|
||||
var prop_row := HBoxContainer.new()
|
||||
_params_box.add_child(prop_row)
|
||||
_prop_target_label = Label.new()
|
||||
_prop_target_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
prop_row.add_child(_prop_target_label)
|
||||
_set_prop_target_btn = Button.new()
|
||||
_set_prop_target_btn.text = "🎯 Click prop…"
|
||||
_set_prop_target_btn.pressed.connect(func() -> void: target_requested.emit())
|
||||
prop_row.add_child(_set_prop_target_btn)
|
||||
_update_prop_target_label()
|
||||
var hand_row := HBoxContainer.new()
|
||||
_params_box.add_child(hand_row)
|
||||
var hand_label := Label.new()
|
||||
hand_label.text = "Hand:"
|
||||
hand_row.add_child(hand_label)
|
||||
_hand_option = OptionButton.new()
|
||||
for label: String in ["Auto", "Left", "Right"]:
|
||||
_hand_option.add_item(label)
|
||||
var hand_idx := ["auto", "left", "right"].find(String(_initial.get("hand", "auto")))
|
||||
_hand_option.select(maxi(hand_idx, 0))
|
||||
hand_row.add_child(_hand_option)
|
||||
"throw_prop":
|
||||
var throw_row := HBoxContainer.new()
|
||||
_params_box.add_child(throw_row)
|
||||
_target_label = Label.new()
|
||||
_target_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
throw_row.add_child(_target_label)
|
||||
_set_target_btn = Button.new()
|
||||
_set_target_btn.text = "🎯 Click target…"
|
||||
_set_target_btn.pressed.connect(func() -> void: target_requested.emit())
|
||||
throw_row.add_child(_set_target_btn)
|
||||
_update_throw_target_label()
|
||||
"speak":
|
||||
var tlabel := Label.new()
|
||||
tlabel.text = "Text:"
|
||||
@@ -247,6 +312,20 @@ func _update_walk_target_label() -> void:
|
||||
_target_label.text = "Target: not set"
|
||||
|
||||
|
||||
func _update_prop_target_label() -> void:
|
||||
if _prop_target_label != null:
|
||||
_prop_target_label.text = "Prop: %s" % (_prop_target_id if not _prop_target_id.is_empty() else "not set")
|
||||
|
||||
|
||||
func _update_throw_target_label() -> void:
|
||||
if _target_label == null:
|
||||
return
|
||||
if _has_throw_target:
|
||||
_target_label.text = "Target: (%d, %d)" % [int(roundf(_throw_target_pos.x)), int(roundf(_throw_target_pos.y))]
|
||||
else:
|
||||
_target_label.text = "Target: not set"
|
||||
|
||||
|
||||
func _on_ok_pressed() -> void:
|
||||
var type := _current_type()
|
||||
var action: Dictionary = { "type": type }
|
||||
@@ -257,6 +336,18 @@ func _on_ok_pressed() -> void:
|
||||
target_requested.emit()
|
||||
return
|
||||
action["target"] = _target_pos
|
||||
"pick_up_prop":
|
||||
if _prop_target_id.is_empty():
|
||||
target_requested.emit()
|
||||
return
|
||||
var hands: Array[String] = ["auto", "left", "right"]
|
||||
action["target_prop_id"] = _prop_target_id
|
||||
action["hand"] = hands[_hand_option.selected] if _hand_option != null and _hand_option.selected >= 0 else "auto"
|
||||
"throw_prop":
|
||||
if not _has_throw_target:
|
||||
target_requested.emit()
|
||||
return
|
||||
action["target_position"] = _throw_target_pos
|
||||
"speak":
|
||||
action["text"] = _text_edit.text if _text_edit != null else ""
|
||||
action["duration"] = _duration_spin.value if _duration_spin != null else 2.0
|
||||
|
||||
@@ -47,6 +47,29 @@ const ACTION_TEMPLATES := {
|
||||
"params": [],
|
||||
"reactive_role": ReactiveRole.TAKEOVER, "overlay_eligible": false,
|
||||
},
|
||||
"pick_up_prop": {
|
||||
"label": "Pick Up Prop",
|
||||
"icon": "🫴",
|
||||
"params": [
|
||||
{ "key": "target_prop_id", "type": "prop", "required": true },
|
||||
{ "key": "hand", "type": "choice", "default": "auto", "choices": ["auto", "left", "right"] },
|
||||
],
|
||||
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
|
||||
},
|
||||
"drop_prop": {
|
||||
"label": "Drop Prop",
|
||||
"icon": "⬇️",
|
||||
"params": [],
|
||||
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
|
||||
},
|
||||
"throw_prop": {
|
||||
"label": "Throw Prop",
|
||||
"icon": "🏹",
|
||||
"params": [
|
||||
{ "key": "target_position", "type": "position", "required": true },
|
||||
],
|
||||
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +125,11 @@ static func to_rule_action(action: Dictionary, target_id: int) -> Dictionary:
|
||||
params["stop_current"] = true
|
||||
"wait":
|
||||
params["duration"] = float(action.get("duration", 0.0))
|
||||
"pick_up_prop":
|
||||
params["target_prop_id"] = String(action.get("target_prop_id", ""))
|
||||
params["hand"] = String(action.get("hand", "auto"))
|
||||
"throw_prop":
|
||||
params["target_position"] = action.get("target_position", Vector2.ZERO)
|
||||
return {
|
||||
"type": String(action.get("type", "")),
|
||||
"target": target_id,
|
||||
@@ -126,6 +154,12 @@ static func from_rule_action(rule_action: Dictionary) -> Dictionary:
|
||||
return speak
|
||||
"wait":
|
||||
return { "type": "wait", "duration": float(params.get("duration", 0.0)) }
|
||||
"pick_up_prop":
|
||||
return { "type": "pick_up_prop", "target_prop_id": String(params.get("target_prop_id", "")), "hand": String(params.get("hand", "auto")) }
|
||||
"drop_prop":
|
||||
return { "type": "drop_prop" }
|
||||
"throw_prop":
|
||||
return { "type": "throw_prop", "target_position": params.get("target_position", Vector2.ZERO) }
|
||||
_:
|
||||
return { "type": String(rule_action.get("type", "")) }
|
||||
|
||||
@@ -155,6 +189,14 @@ static func summarize(action: Dictionary) -> String:
|
||||
return "Ragdoll"
|
||||
"recover":
|
||||
return "Recover"
|
||||
"pick_up_prop":
|
||||
return "Pick Up Prop (%s)" % String(action.get("target_prop_id", params.get("target_prop_id", "")))
|
||||
"drop_prop":
|
||||
return "Drop Prop"
|
||||
"throw_prop":
|
||||
var raw_position: Variant = action.get("target_position", params.get("target_position", Vector2.ZERO))
|
||||
var target_position: Vector2 = raw_position as Vector2 if raw_position is Vector2 else Vector2.ZERO
|
||||
return "Throw Prop (%d, %d)" % [int(roundf(target_position.x)), int(roundf(target_position.y))]
|
||||
_:
|
||||
return type
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ extends Node2D
|
||||
|
||||
var original_entry: Dictionary = {}
|
||||
var missing_prop_path: String = ""
|
||||
var stage_object_id: String = ""
|
||||
const MARKER_SIZE := Vector2(180.0, 54.0)
|
||||
|
||||
func configure(entry: Dictionary) -> void:
|
||||
original_entry = entry.duplicate(true)
|
||||
missing_prop_path = String(entry.get("asset_id", ""))
|
||||
stage_object_id = String(entry.get("stage_object_id", ""))
|
||||
queue_redraw()
|
||||
|
||||
func get_area_rect() -> Rect2:
|
||||
|
||||
@@ -11,6 +11,7 @@ extends RigidBody2D
|
||||
## friction/bounce (PhysicsMaterial), and themed visuals.
|
||||
|
||||
const PropVisuals := preload("res://scripts/prop_visuals.gd")
|
||||
const PropAnchorDataScript := preload("res://scripts/prop_anchor_data.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
@@ -23,6 +24,13 @@ enum MaterialPreset { NONE, WOOD, RUBBER, CARDBOARD, METAL }
|
||||
## Stable library provenance used by Sandbox Stage scene files.
|
||||
var spawn_id: String = ""
|
||||
|
||||
## Stable identity of this placed prop in one Sandbox Stage scene. Unlike
|
||||
## spawn_id, this value is unique per instance and survives scene save/load.
|
||||
var stage_object_id: String = ""
|
||||
var interaction_anchors: Dictionary = {}
|
||||
var interaction_reserved_by: int = 0
|
||||
var interaction_owner_id: int = 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Child node names
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -126,6 +134,33 @@ func _ready() -> void:
|
||||
body_exited.connect(_on_body_exited)
|
||||
|
||||
|
||||
func set_interaction_anchors(value: Dictionary) -> void:
|
||||
interaction_anchors = PropAnchorDataScript.normalize_anchors(value)
|
||||
|
||||
|
||||
func get_interaction_anchors() -> Dictionary:
|
||||
return PropAnchorDataScript.normalize_anchors(interaction_anchors, false)
|
||||
|
||||
|
||||
func ensure_default_grip() -> void:
|
||||
var normalized := get_interaction_anchors()
|
||||
if not PropAnchorDataScript.ids_for_role(normalized, "grip").is_empty():
|
||||
interaction_anchors = normalized
|
||||
return
|
||||
var bounds := _visual_local_bounds
|
||||
if bounds.size == Vector2.ZERO:
|
||||
if shape_type == ShapeType.CIRCLE:
|
||||
bounds = Rect2(Vector2(-radius, -radius), Vector2(radius * 2.0, radius * 2.0))
|
||||
elif polygon_points.size() >= 3:
|
||||
var min_point := polygon_points[0]
|
||||
var max_point := polygon_points[0]
|
||||
for point: Vector2 in polygon_points:
|
||||
min_point = min_point.min(point)
|
||||
max_point = max_point.max(point)
|
||||
bounds = Rect2(min_point, max_point - min_point)
|
||||
interaction_anchors = PropAnchorDataScript.preset_layout("one_handed", bounds)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
var center: Vector2 = (body as Node2D).global_position if body is Node2D else global_position
|
||||
var normal: Vector2 = (center - global_position).normalized()
|
||||
@@ -277,6 +312,10 @@ func has_custom_shapes() -> bool:
|
||||
func get_visual_local_bounds() -> Rect2:
|
||||
return _visual_local_bounds
|
||||
|
||||
|
||||
func get_interaction_scale() -> Vector2:
|
||||
return _play_instance_scale if _play_scale_active else scale.abs()
|
||||
|
||||
## RigidBody2D transforms do not reliably retain scale during simulation. Move
|
||||
## the authored uniform scale into child visuals and collision geometry for
|
||||
## PLAY, then restore the root scale on return to EDIT for scene persistence.
|
||||
|
||||
@@ -75,7 +75,7 @@ static func make_custom_entry(path: String) -> Dictionary:
|
||||
var name := String(parsed.get("prop_name", "")).strip_edges()
|
||||
if name.is_empty(): name = normalized_path.get_file().get_basename()
|
||||
var preset: int = int(parsed.get("material_preset", PROP_BLOCK.MaterialPreset.NONE))
|
||||
return {"id": normalized_path, "path": normalized_path, "name": name, "material_preset": preset, "material_label": PROP_FILE.preset_to_string(preset).capitalize(), "is_custom": true, "payload": PROP_FILE.flatten_to_payload(shapes), "shapes": shapes, "collision_hull": hull}
|
||||
return {"id": normalized_path, "path": normalized_path, "name": name, "material_preset": preset, "material_label": PROP_FILE.preset_to_string(preset).capitalize(), "is_custom": true, "payload": PROP_FILE.flatten_to_payload(shapes), "shapes": shapes, "collision_hull": hull, "anchors": parsed.get("anchors", {})}
|
||||
|
||||
|
||||
static func is_custom_path(path: String) -> bool:
|
||||
|
||||
+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()
|
||||
|
||||
@@ -19,7 +19,16 @@ var guide_line_color: Color = Color("#22c6ff")
|
||||
var action_visible: bool = false
|
||||
var action_origin: Vector2 = Vector2.ZERO
|
||||
var action_target: Vector2 = Vector2.ZERO
|
||||
var action_curve_points: PackedVector2Array = PackedVector2Array()
|
||||
var action_valid: bool = true
|
||||
var action_requested_target: Vector2 = Vector2.ZERO
|
||||
var action_show_requested_target: bool = false
|
||||
var action_effective_target: Vector2 = Vector2.ZERO
|
||||
var action_collision_position: Vector2 = Vector2.INF
|
||||
var action_show_collision: bool = false
|
||||
var action_preview_unavailable: bool = false
|
||||
var action_preview_planned: bool = false
|
||||
var action_preview_uncertain: bool = false
|
||||
var valid_color: Color = Color(0.3, 0.9, 0.4)
|
||||
var invalid_color: Color = Color(1.0, 0.3, 0.3)
|
||||
var line_width_px: float = 2.0
|
||||
@@ -62,11 +71,53 @@ func set_action_trajectory(origin: Vector2, target: Vector2, valid: bool) -> voi
|
||||
action_visible = true
|
||||
action_origin = origin
|
||||
action_target = target
|
||||
action_curve_points.clear()
|
||||
action_valid = valid
|
||||
action_show_requested_target = false
|
||||
action_show_collision = false
|
||||
action_preview_unavailable = false
|
||||
action_preview_planned = false
|
||||
action_preview_uncertain = false
|
||||
|
||||
|
||||
func set_throw_trajectory(origin: Vector2, requested_target: Vector2, landing_target: Vector2, valid: bool, trajectory_points: PackedVector2Array = PackedVector2Array(), effective_target: Vector2 = Vector2.INF, collision_position: Vector2 = Vector2.INF, planned: bool = false, uncertain: bool = false) -> void:
|
||||
action_visible = true
|
||||
action_origin = origin
|
||||
action_target = landing_target
|
||||
action_curve_points = trajectory_points.duplicate()
|
||||
action_valid = valid
|
||||
action_requested_target = requested_target
|
||||
action_show_requested_target = true
|
||||
action_effective_target = effective_target if effective_target.is_finite() else landing_target
|
||||
action_collision_position = collision_position
|
||||
action_show_collision = collision_position.is_finite()
|
||||
action_preview_unavailable = false
|
||||
action_preview_planned = planned
|
||||
action_preview_uncertain = uncertain
|
||||
|
||||
|
||||
func set_throw_preview_unavailable(requested_target: Vector2) -> void:
|
||||
action_visible = true
|
||||
action_origin = requested_target
|
||||
action_target = requested_target
|
||||
action_curve_points.clear()
|
||||
action_valid = false
|
||||
action_requested_target = requested_target
|
||||
action_show_requested_target = false
|
||||
action_show_collision = false
|
||||
action_preview_unavailable = true
|
||||
action_preview_planned = false
|
||||
action_preview_uncertain = false
|
||||
|
||||
|
||||
func clear_action() -> void:
|
||||
action_visible = false
|
||||
action_curve_points.clear()
|
||||
action_show_requested_target = false
|
||||
action_show_collision = false
|
||||
action_preview_unavailable = false
|
||||
action_preview_planned = false
|
||||
action_preview_uncertain = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -76,9 +127,29 @@ func _draw() -> void:
|
||||
_draw_cell_marker(terrain_anchor, guide_line_color)
|
||||
_draw_cell_marker(terrain_target, guide_line_color)
|
||||
if action_visible:
|
||||
var color := valid_color if action_valid else invalid_color
|
||||
_draw_dashed(action_origin, action_target, color)
|
||||
_draw_ghost_marker(action_target, color)
|
||||
if action_preview_unavailable:
|
||||
var zoom := _zoom()
|
||||
var unavailable_color := Color(0.74, 0.79, 0.85, 0.9)
|
||||
draw_arc(action_target, MARKER_RADIUS_PX / zoom, 0.0, TAU, 24, unavailable_color, LINE_WIDTH_PX / zoom, true)
|
||||
else:
|
||||
var color := valid_color if action_valid else invalid_color
|
||||
if action_valid and action_preview_planned:
|
||||
color = Color(0.35, 0.8, 1.0, 0.95)
|
||||
elif action_valid and action_preview_uncertain:
|
||||
color = Color(0.8, 0.68, 1.0, 0.95)
|
||||
if action_curve_points.size() >= 2:
|
||||
draw_polyline(action_curve_points, color, line_width_px / _zoom(), true)
|
||||
else:
|
||||
_draw_dashed(action_origin, action_target, color)
|
||||
if action_show_requested_target:
|
||||
_draw_ghost_marker(action_effective_target, color)
|
||||
var request_color := Color(1.0, 0.85, 0.25, 0.9)
|
||||
var r := 7.0 / _zoom()
|
||||
draw_arc(action_requested_target, r, 0.0, TAU, 20, request_color, LINE_WIDTH_PX / _zoom(), true)
|
||||
if action_show_collision:
|
||||
_draw_collision_marker(action_collision_position)
|
||||
else:
|
||||
_draw_ghost_marker(action_target, color)
|
||||
|
||||
|
||||
func _draw_dashed(from: Vector2, to: Vector2, color: Color) -> void:
|
||||
@@ -113,6 +184,15 @@ func _draw_ghost_marker(pos: Vector2, color: Color) -> void:
|
||||
draw_line(pos + Vector2(0.0, -r), pos + Vector2(0.0, r), color, LINE_WIDTH_PX / zoom, true)
|
||||
|
||||
|
||||
func _draw_collision_marker(pos: Vector2) -> void:
|
||||
var zoom := _zoom()
|
||||
var r := 8.0 / zoom
|
||||
var color := Color(1.0, 0.25, 0.15, 0.95)
|
||||
draw_arc(pos, r, 0.0, TAU, 24, color, LINE_WIDTH_PX / zoom, true)
|
||||
draw_line(pos + Vector2(-r * 0.55, -r * 0.55), pos + Vector2(r * 0.55, r * 0.55), color, LINE_WIDTH_PX / zoom, true)
|
||||
draw_line(pos + Vector2(-r * 0.55, r * 0.55), pos + Vector2(r * 0.55, -r * 0.55), color, LINE_WIDTH_PX / zoom, true)
|
||||
|
||||
|
||||
func _zoom() -> float:
|
||||
if camera != null and is_instance_valid(camera):
|
||||
return maxf(camera.zoom.x, 0.0001)
|
||||
|
||||
@@ -3,7 +3,7 @@ extends RefCounted
|
||||
## JSON boundary for Sandbox Stage documents. This class deliberately has no
|
||||
## scene-tree dependencies, which makes rejected documents non-destructive.
|
||||
|
||||
const FILE_VERSION: String = "1.0"
|
||||
const FILE_VERSION: String = "1.1"
|
||||
const ACTION_REGISTRY := preload("res://scripts/action_registry.gd")
|
||||
const TRIGGER_REGISTRY := preload("res://scripts/trigger_registry.gd")
|
||||
|
||||
@@ -149,17 +149,38 @@ static func write_file_safe(path: String, data: Dictionary) -> Dictionary:
|
||||
return {"ok": true, "error": ""}
|
||||
|
||||
static func _migrate_to_current(data: Dictionary) -> Dictionary:
|
||||
if String(data.get("version", "")) != FILE_VERSION: return {}
|
||||
return data
|
||||
var version := String(data.get("version", ""))
|
||||
if version == FILE_VERSION:
|
||||
return data
|
||||
if version != "1.0":
|
||||
return {}
|
||||
var migrated: Dictionary = data.duplicate(true)
|
||||
for raw: Variant in migrated.get("objects", []):
|
||||
if not raw is Dictionary:
|
||||
continue
|
||||
var object: Dictionary = raw
|
||||
if String(object.get("type", "")) == "prop":
|
||||
object["stage_object_id"] = String(object.get("file_id", ""))
|
||||
migrated["version"] = FILE_VERSION
|
||||
return migrated
|
||||
|
||||
static func _validate_root(data: Dictionary) -> Dictionary:
|
||||
var seen: Dictionary = {}
|
||||
var seen_stage_ids: Dictionary = {}
|
||||
for raw: Variant in data["objects"]:
|
||||
if not raw is Dictionary: continue
|
||||
var entry: Dictionary = raw
|
||||
var file_id := String(entry.get("file_id", ""))
|
||||
if file_id.is_empty() or seen.has(file_id): return {"ok": false, "error": "Object file IDs must be unique and non-empty."}
|
||||
seen[file_id] = true
|
||||
if String(entry.get("type", "")) == "prop":
|
||||
var raw_stage_id: Variant = entry.get("stage_object_id", "")
|
||||
if not raw_stage_id is String:
|
||||
return {"ok": false, "error": "Prop stage IDs must be unique and non-empty strings."}
|
||||
var stage_id := String(raw_stage_id)
|
||||
if stage_id.strip_edges().is_empty() or seen_stage_ids.has(stage_id):
|
||||
return {"ok": false, "error": "Prop stage IDs must be unique and non-empty."}
|
||||
seen_stage_ids[stage_id] = true
|
||||
return {"ok": true, "error": ""}
|
||||
|
||||
static func _recover_backup(path: String) -> void:
|
||||
@@ -180,6 +201,17 @@ static func _encode_flat_action(action: Dictionary) -> Dictionary:
|
||||
return {"type": type, "target": vector_to_data(action["target"])}
|
||||
"speak": return {"type": type, "text": String(action.get("text", "")), "duration": float(action.get("duration", 2.0)), "stop_current": bool(action.get("stop_current", false))} if _finite_number(action.get("duration", 2.0)) else {}
|
||||
"wait": return {"type": type, "duration": float(action.get("duration", 0.0))} if _finite_number(action.get("duration", 0.0)) else {}
|
||||
"pick_up_prop":
|
||||
var prop_id := String(action.get("target_prop_id", ""))
|
||||
var hand := String(action.get("hand", "auto"))
|
||||
if prop_id.is_empty() or not ["auto", "left", "right"].has(hand): return {}
|
||||
return {"type": type, "target_prop_id": prop_id, "hand": hand}
|
||||
"drop_prop": return {"type": type}
|
||||
"throw_prop":
|
||||
if not action.get("target_position") is Vector2: return {}
|
||||
var target_position: Vector2 = action["target_position"]
|
||||
if not target_position.is_finite(): return {}
|
||||
return {"type": type, "target_position": vector_to_data(target_position)}
|
||||
_: return {"type": type}
|
||||
|
||||
static func _decode_flat_action(action: Dictionary) -> Dictionary:
|
||||
@@ -195,6 +227,15 @@ static func _decode_flat_action(action: Dictionary) -> Dictionary:
|
||||
if bool(action.get("stop_current", false)): out["stop_current"] = true
|
||||
return out
|
||||
"wait": return {"type": type, "duration": float(action.get("duration", 0.0))} if _finite_number(action.get("duration", 0.0)) else {}
|
||||
"pick_up_prop":
|
||||
var prop_id := String(action.get("target_prop_id", ""))
|
||||
var hand := String(action.get("hand", "auto"))
|
||||
if prop_id.is_empty() or not ["auto", "left", "right"].has(hand): return {}
|
||||
return {"type": type, "target_prop_id": prop_id, "hand": hand}
|
||||
"drop_prop": return {"type": type}
|
||||
"throw_prop":
|
||||
if not is_vector_data(action.get("target_position")): return {}
|
||||
return {"type": type, "target_position": data_to_vector(action["target_position"], Vector2.ZERO)}
|
||||
_: return {"type": type}
|
||||
|
||||
static func _map_ref(value: Variant, map: Dictionary) -> Variant:
|
||||
|
||||
@@ -176,6 +176,8 @@ func spawn_prop_asset(asset_id: String, object_position: Vector2) -> PropBlock:
|
||||
else:
|
||||
prop = PROP_UTILS.spawn_prop(_world, object_position, template["payload"], int(template["material_preset"]), Vector2.ZERO)
|
||||
prop.spawn_id = String(template.get("id", asset_id))
|
||||
prop.set_interaction_anchors(template.get("anchors", {}) as Dictionary)
|
||||
prop.ensure_default_grip()
|
||||
return prop
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const SPEECH_BUBBLE_SCRIPT := preload("res://scripts/stickman_speech_bubble.gd")
|
||||
const SPEECH_BUBBLE_OFFSET := Vector2(0.0, -640.0)
|
||||
|
||||
enum RunnerState { IDLE, EXECUTING }
|
||||
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING }
|
||||
enum ActionPhase { NONE, WALKING, SPEAKING, WAITING, RAGDOLLING, RECOVERING, PROP_INTERACTION }
|
||||
enum QueueStatus { NOT_STARTED, COMPLETED, INTERRUPTED, ACTIVE, PENDING }
|
||||
|
||||
signal action_started(action: Dictionary, index: int)
|
||||
@@ -29,6 +29,7 @@ var action_queue: Array[Dictionary]:
|
||||
set_authored_queue(value)
|
||||
var _rig: StickmanRig = null
|
||||
var _locomotion: StickmanLocomotion = null
|
||||
var _prop_interactor: Node = null
|
||||
var _runner_state: RunnerState = RunnerState.IDLE
|
||||
var _action_phase: ActionPhase = ActionPhase.NONE
|
||||
var _current_index: int = -1
|
||||
@@ -46,11 +47,15 @@ var _completed_ids: Dictionary = {}
|
||||
var _interrupted_ids: Dictionary = {}
|
||||
var _paused: bool = false
|
||||
var _session_started: bool = false
|
||||
var _prop_action_finished: bool = false
|
||||
|
||||
|
||||
func configure(rig: StickmanRig, locomotion: StickmanLocomotion) -> void:
|
||||
func configure(rig: StickmanRig, locomotion: StickmanLocomotion, prop_interactor: Node = null) -> void:
|
||||
_rig = rig
|
||||
_locomotion = locomotion
|
||||
_prop_interactor = prop_interactor
|
||||
if _prop_interactor != null and not _prop_interactor.is_connected("action_completed", _on_prop_action_completed):
|
||||
_prop_interactor.connect("action_completed", _on_prop_action_completed)
|
||||
|
||||
|
||||
func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
|
||||
@@ -78,6 +83,10 @@ func tick(delta: float, rig_state: int, ragdoll_at_rest: bool) -> void:
|
||||
ActionPhase.RECOVERING:
|
||||
if rig_state == StickmanRig.RigState.ANIMATED:
|
||||
_finish_action()
|
||||
ActionPhase.PROP_INTERACTION:
|
||||
if _prop_action_finished:
|
||||
_prop_action_finished = false
|
||||
_finish_action()
|
||||
|
||||
|
||||
func speak(text: String, duration: float) -> void:
|
||||
@@ -358,7 +367,9 @@ func _advance_to_next_action() -> void:
|
||||
_current_action_id = int(_current_run_item.get("action_id", -1))
|
||||
_current_index = _index_for_action_id(_current_action_id) if _current_action_id >= 0 else -1
|
||||
var action: Dictionary = _current_run_item.get("action", {})
|
||||
if String(action.get("type", "")) == "walk_to" and _rig != null:
|
||||
var action_type := String(action.get("type", ""))
|
||||
var requires_animated := action_type == "walk_to" or action_type == "pick_up_prop" or action_type == "drop_prop" or action_type == "throw_prop"
|
||||
if requires_animated and _rig != null:
|
||||
if _rig.state == StickmanRig.RigState.RECOVERING:
|
||||
_run_queue.insert(_run_cursor, _current_run_item.duplicate(true))
|
||||
_current_run_item.clear()
|
||||
@@ -394,6 +405,12 @@ func _begin_action(action: Dictionary) -> void:
|
||||
"recover":
|
||||
_action_phase = ActionPhase.RECOVERING
|
||||
recovery_requested.emit()
|
||||
"pick_up_prop", "drop_prop", "throw_prop":
|
||||
if _rig == null or not _rig.start_prop_action(action):
|
||||
_finish_action()
|
||||
else:
|
||||
_prop_action_finished = false
|
||||
_action_phase = ActionPhase.PROP_INTERACTION
|
||||
_:
|
||||
push_warning("StickmanActionRunner: unknown action type '%s'; skipped." % String(action.get("type", "")))
|
||||
_finish_action()
|
||||
@@ -420,9 +437,12 @@ func _cancel_current_primary(notify: bool = false) -> void:
|
||||
active_action_cancelled.emit(action.duplicate(true))
|
||||
if _locomotion != null:
|
||||
_locomotion.cancel_walk(not _is_blocked())
|
||||
if _rig != null:
|
||||
_rig.cancel_prop_action()
|
||||
_primary_speech.clear()
|
||||
_phase_timer = 0.0
|
||||
_action_phase = ActionPhase.NONE
|
||||
_prop_action_finished = false
|
||||
_current_run_item.clear()
|
||||
_current_index = -1
|
||||
_current_action_id = -1
|
||||
@@ -430,6 +450,10 @@ func _cancel_current_primary(notify: bool = false) -> void:
|
||||
_speech_bubble.hide_bubble()
|
||||
|
||||
|
||||
func _on_prop_action_completed() -> void:
|
||||
_prop_action_finished = true
|
||||
|
||||
|
||||
func _allocate_action_id() -> int:
|
||||
var result: int = _next_action_id
|
||||
_next_action_id += 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://bsdnwby48vl5
|
||||
+42
-1
@@ -179,6 +179,7 @@ signal action_finished(action: Dictionary, index: int)
|
||||
signal queue_finished # queue ran to completion (not on stop)
|
||||
signal queue_changed # any queue mutation
|
||||
signal speech_finished # bubble auto-hid after speak()
|
||||
signal prop_interaction_event(event_type: String, prop: PropBlock, reason: String)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
@@ -225,6 +226,7 @@ var source_stk_path: String = ""
|
||||
var _locomotion_component: StickmanLocomotion = null
|
||||
var _ragdoll_component: StickmanRagdoll = null
|
||||
var _action_runner_component: StickmanActionRunner = null
|
||||
var _prop_interactor_component: Node = null
|
||||
var _simulation_paused: bool = false
|
||||
var _phase12_paused_animation: bool = false
|
||||
var _phase12_paused_animation_name: StringName = &""
|
||||
@@ -238,12 +240,15 @@ func _ready() -> void:
|
||||
_locomotion_component = get_node_or_null(NodePath("RigComponents/Locomotion")) as StickmanLocomotion
|
||||
_ragdoll_component = get_node_or_null(NodePath("RigComponents/Ragdoll")) as StickmanRagdoll
|
||||
_action_runner_component = get_node_or_null(NodePath("RigComponents/ActionRunner")) as StickmanActionRunner
|
||||
_prop_interactor_component = get_node_or_null(NodePath("RigComponents/PropInteractor"))
|
||||
if _locomotion_component == null:
|
||||
push_warning("StickmanRig: missing RigComponents/Locomotion; locomotion boundary unavailable.")
|
||||
if _ragdoll_component == null:
|
||||
push_warning("StickmanRig: missing RigComponents/Ragdoll; ragdoll boundary unavailable.")
|
||||
if _action_runner_component == null:
|
||||
push_warning("StickmanRig: missing RigComponents/ActionRunner; action boundary unavailable.")
|
||||
if _prop_interactor_component == null:
|
||||
push_warning("StickmanRig: missing RigComponents/PropInteractor; prop interactions unavailable.")
|
||||
if _locomotion_component != null:
|
||||
_locomotion_component.configure(self)
|
||||
_locomotion_component.arrived.connect(func(target: Vector2) -> void: arrived.emit(target))
|
||||
@@ -251,8 +256,11 @@ func _ready() -> void:
|
||||
_ragdoll_component.configure(self, _locomotion_component)
|
||||
_ragdoll_component.recovery_requested.connect(func() -> void: request_recovery())
|
||||
_ragdoll_component.state_transition_requested.connect(_set_state)
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("configure", self, _locomotion_component, Callable())
|
||||
_prop_interactor_component.connect("prop_event", _on_prop_interaction_event)
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.configure(self, _locomotion_component)
|
||||
_action_runner_component.configure(self, _locomotion_component, _prop_interactor_component)
|
||||
_action_runner_component.action_queue = _pending_action_queue
|
||||
_action_runner_component.action_started.connect(func(action: Dictionary, index: int) -> void: action_started.emit(action, index))
|
||||
_action_runner_component.action_finished.connect(func(action: Dictionary, index: int) -> void: action_finished.emit(action, index))
|
||||
@@ -315,6 +323,8 @@ func _physics_process(delta: float) -> void:
|
||||
_ragdoll_component.tick_pre_movement(delta, int(state), rest_timeout, auto_recover)
|
||||
if _locomotion_component != null:
|
||||
_locomotion_component.tick_movement(delta, int(state))
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("tick", delta)
|
||||
if _action_runner_component != null:
|
||||
_action_runner_component.tick(delta, int(state), is_ragdoll_at_rest())
|
||||
_pin_mirrored_head_rotation()
|
||||
@@ -457,6 +467,8 @@ func _set_state(next_state: int) -> void:
|
||||
|
||||
func set_ragdoll(enabled: bool) -> void:
|
||||
if enabled:
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("disarm_for_ragdoll")
|
||||
if _locomotion_component != null:
|
||||
_locomotion_component.reset_for_ragdoll_handoff()
|
||||
match state:
|
||||
@@ -480,6 +492,35 @@ func toggle_ragdoll() -> void:
|
||||
set_ragdoll(not is_in_ragdoll())
|
||||
|
||||
|
||||
func set_prop_resolver(resolver: Callable) -> void:
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("configure", self, _locomotion_component, resolver)
|
||||
|
||||
|
||||
func start_prop_action(action: Dictionary) -> bool:
|
||||
return bool(_prop_interactor_component.call("start_action", action)) if _prop_interactor_component != null else false
|
||||
|
||||
|
||||
func cancel_prop_action() -> void:
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("cancel_action")
|
||||
|
||||
|
||||
func preview_prop_throw(target: Vector2, queue_prefix: Array[Dictionary] = []) -> Dictionary:
|
||||
if _prop_interactor_component == null:
|
||||
return {}
|
||||
return _prop_interactor_component.call("preview_throw", target, queue_prefix)
|
||||
|
||||
|
||||
func reset_prop_interactions_for_scene_exit() -> void:
|
||||
if _prop_interactor_component != null:
|
||||
_prop_interactor_component.call("reset_for_scene_exit")
|
||||
|
||||
|
||||
func _on_prop_interaction_event(event_type: String, prop: PropBlock, reason: String) -> void:
|
||||
prop_interaction_event.emit(event_type, prop, reason)
|
||||
|
||||
|
||||
## Public stand-up request. No-op unless the rig is in RAGDOLL.
|
||||
func request_recovery() -> void:
|
||||
if state == RigState.RAGDOLL and _ragdoll_component != null:
|
||||
|
||||
@@ -32,6 +32,21 @@ const TRIGGER_TEMPLATES := {
|
||||
"icon": "💥",
|
||||
"target_type": "prop",
|
||||
},
|
||||
"prop_picked_up": {
|
||||
"label": "Picks up a prop",
|
||||
"icon": "🫴",
|
||||
"target_type": "prop",
|
||||
},
|
||||
"prop_dropped": {
|
||||
"label": "Drops a prop",
|
||||
"icon": "⬇️",
|
||||
"target_type": "prop",
|
||||
},
|
||||
"prop_thrown": {
|
||||
"label": "Throws a prop",
|
||||
"icon": "🏹",
|
||||
"target_type": "prop",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user