Add prop interaction system and throw mechanics specification

- Introduced `stickman_prop_interactor.gd` to manage prop interactions.
- Created `phase_11_prop_interactions.md` detailing the design and implementation of prop pickup, drop, and throw mechanics.
- Defined interaction states, action lifecycles, and event triggers for prop handling.
- Specified constraints for throw actions, including target positioning and velocity calculations.
- Established acceptance criteria for prop interactions and lifecycle management.
This commit is contained in:
2026-09-25 23:42:26 -04:00
parent 730a9a966b
commit 6a1359f244
22 changed files with 3433 additions and 34 deletions
+91
View File
@@ -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