Implement interactive prop handling for StickmanRig

- Added PropTrajectory class for deterministic ballistic calculations.
- Introduced StickmanPropInteraction component to manage prop interactions (pickup, drop, throw).
- Implemented grip resolution and attachment logic for props.
- Added signals for interaction events (prop_picked_up, prop_dropped, prop_thrown).
- Defined specifications for prop interaction lifecycle and event rules.
- Ensured compatibility with existing prop systems and added new capabilities for prop pickup.
This commit is contained in:
2026-09-23 21:26:13 -04:00
parent 0a7f97cec7
commit 043f114083
22 changed files with 1444 additions and 37 deletions
+92 -5
View File
@@ -13,6 +13,7 @@ const ACTION_REGISTRY := preload("res://scripts/action_registry.gd")
signal committed(action: Dictionary)
signal cancelled()
signal target_requested()
signal prop_requested()
var _mode: String = "new"
var _initial: Dictionary = {}
@@ -21,11 +22,17 @@ var _params_box: VBoxContainer = null
var _text_edit: LineEdit = null
var _duration_spin: SpinBox = null
var _target_label: Label = null
var _prop_target_label: Label = null
var _target_prop_display_name: String = ""
var _set_target_btn: Button = null
var _stop_current_check: CheckBox = null
var _show_reactive_options: bool = false
var _allow_prop_actions: bool = true
var _has_target: bool = false
var _target_pos: Vector2 = Vector2.ZERO
var _target_prop_id: int = -1
var _flight_time_spin: SpinBox = null
var _hand_option: OptionButton = null
## Theme font overrides (set via apply_font from sandbox_theme.json). Size 0 = no
## override (engine default); Font null = no override.
@@ -49,10 +56,14 @@ func _unhandled_input(event: InputEvent) -> void:
## Opens the editor in "add" mode (empty, default type walk_to).
func open_new(show_reactive_options: bool = false) -> void:
_show_reactive_options = show_reactive_options
_allow_prop_actions = not show_reactive_options
_rebuild_type_options()
_mode = "new"
_initial = {}
_has_target = false
_target_pos = Vector2.ZERO
_target_prop_id = -1
_target_prop_display_name = ""
_select_type("walk_to")
_rebuild_params("walk_to")
title = "Add Action"
@@ -62,11 +73,15 @@ func open_new(show_reactive_options: bool = false) -> void:
## Opens the editor pre-filled from a flat queue action.
func open_edit(action: Dictionary, show_reactive_options: bool = false) -> void:
_show_reactive_options = show_reactive_options
_allow_prop_actions = not show_reactive_options
_rebuild_type_options()
_mode = "edit"
_initial = action.duplicate(true)
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
_has_target = (action.has("target") and action["target"] is Vector2) or (action.has("target_position") and action["target_position"] is Vector2)
_target_pos = action.get("target_position", action.get("target", Vector2.ZERO)) if _has_target else Vector2.ZERO
_target_prop_id = int(action.get("target_prop", -1))
_target_prop_display_name = ""
_select_type(type)
_rebuild_params(type)
title = "Edit Action"
@@ -80,6 +95,18 @@ func set_walk_target(pos: Vector2) -> void:
_update_walk_target_label()
popup_centered()
func set_prop_target(prop: PropBlock, display_name: String = "") -> void:
_target_prop_id = prop.get_instance_id() if prop != null and is_instance_valid(prop) else -1
_target_prop_display_name = display_name
_update_prop_target_label()
popup_centered()
func set_throw_target(pos: Vector2) -> void:
_has_target = true
_target_pos = pos
_update_walk_target_label()
popup_centered()
func _build_ui() -> void:
var margin := MarginContainer.new()
@@ -100,7 +127,7 @@ func _build_ui() -> void:
type_row.add_child(type_label)
_type_option = OptionButton.new()
_type_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
for type: String in ACTION_REGISTRY.types():
for type: String in _available_types():
_type_option.add_item("%s %s" % [ACTION_REGISTRY.icon(type), ACTION_REGISTRY.label(type)])
_type_option.item_selected.connect(_on_type_selected)
type_row.add_child(_type_option)
@@ -158,19 +185,42 @@ func _apply_font_to(c: Control, size: int) -> void:
func _select_type(type: String) -> void:
var idx := ACTION_REGISTRY.types().find(type)
var idx := _available_types().find(type)
if idx < 0:
idx = 0
_type_option.select(idx)
func _current_type() -> String:
var types := ACTION_REGISTRY.types()
var types := _available_types()
var idx := _type_option.selected
if idx < 0 or idx >= types.size():
return "walk_to"
return types[idx]
func get_action_type() -> String:
return _current_type()
func _available_types() -> Array[String]:
var types: Array[String] = ACTION_REGISTRY.types()
if not _allow_prop_actions:
types.erase("pick_up_prop")
types.erase("drop_prop")
types.erase("throw_prop")
return types
func _rebuild_type_options() -> void:
if _type_option == null: return
_type_option.clear()
for type: String in _available_types():
_type_option.add_item("%s %s" % [ACTION_REGISTRY.icon(type), ACTION_REGISTRY.label(type)])
func get_target_prop_id() -> int:
return _target_prop_id
func get_flight_time() -> float:
return _flight_time_spin.value if _flight_time_spin != null else 0.9
func _on_type_selected(_index: int) -> void:
_rebuild_params(_current_type())
@@ -183,8 +233,11 @@ func _rebuild_params(type: String) -> void:
_text_edit = null
_duration_spin = null
_target_label = null
_prop_target_label = null
_set_target_btn = null
_stop_current_check = null
_flight_time_spin = null
_hand_option = null
match type:
"walk_to":
@@ -222,6 +275,24 @@ func _rebuild_params(type: String) -> void:
_params_box.add_child(wlabel)
_duration_spin = _make_duration_spin(float(_initial.get("duration", 1.0)))
_params_box.add_child(_duration_spin)
"pick_up_prop", "throw_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)
var prop_button := Button.new(); prop_button.text = "🧱 Click prop…"; prop_button.pressed.connect(func() -> void: prop_requested.emit()); prop_row.add_child(prop_button)
_update_prop_target_label()
if type == "pick_up_prop":
var hand_label := Label.new(); hand_label.text = "Hand:"; _params_box.add_child(hand_label)
_hand_option = OptionButton.new()
for hand: String in ["auto", "left", "right"]: _hand_option.add_item(hand.capitalize())
_hand_option.select(maxi(0, ["auto", "left", "right"].find(String(_initial.get("hand", "auto")))))
_params_box.add_child(_hand_option)
if type == "throw_prop":
var target_row := HBoxContainer.new(); _params_box.add_child(target_row)
_target_label = Label.new(); _target_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL; target_row.add_child(_target_label)
var target_button := Button.new(); target_button.text = "🎯 Click landing point…"; target_button.pressed.connect(func() -> void: target_requested.emit()); target_row.add_child(target_button)
_update_walk_target_label()
var flight_label := Label.new(); flight_label.text = "Flight (s):"; _params_box.add_child(flight_label)
_flight_time_spin = _make_duration_spin(float(_initial.get("flight_time", 0.9))); _flight_time_spin.min_value = 0.35; _flight_time_spin.max_value = 1.5; _params_box.add_child(_flight_time_spin)
_:
# ragdoll / recover (and any future no-param action) need no fields.
pass
@@ -246,6 +317,15 @@ func _update_walk_target_label() -> void:
else:
_target_label.text = "Target: not set"
func _update_prop_target_label() -> void:
if _prop_target_label != null:
if _target_prop_id < 0:
_prop_target_label.text = "Prop: not set"
elif not _target_prop_display_name.is_empty():
_prop_target_label.text = "Prop: %s" % _target_prop_display_name
else:
_prop_target_label.text = "Prop: selected"
func _on_ok_pressed() -> void:
var type := _current_type()
@@ -264,4 +344,11 @@ func _on_ok_pressed() -> void:
action["stop_current"] = _stop_current_check.button_pressed
"wait":
action["duration"] = _duration_spin.value if _duration_spin != null else 1.0
"pick_up_prop":
if _target_prop_id < 0: prop_requested.emit(); return
action["target_prop"] = _target_prop_id; action["hand"] = ["auto", "left", "right"][_hand_option.selected] if _hand_option != null else "auto"
"throw_prop":
if _target_prop_id < 0: prop_requested.emit(); return
if not _has_target: target_requested.emit(); return
action["target_prop"] = _target_prop_id; action["target_position"] = _target_pos; action["flight_time"] = _flight_time_spin.value if _flight_time_spin != null else 0.9
committed.emit(action)