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