- 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.
179 lines
7.0 KiB
GDScript
179 lines
7.0 KiB
GDScript
class_name ActionRegistry
|
|
extends RefCounted
|
|
## ActionRegistry - Registry of director action templates (Phase 3c).
|
|
##
|
|
## Single source of truth for the action types the Director Tool and the event
|
|
## system understand. Adding a new action type is just appending an entry here;
|
|
## the ActionEditor and the panels generate their UI from this registry, so no
|
|
## other code changes are required.
|
|
|
|
enum ReactiveRole { SEQUENTIAL, OVERLAY, TAKEOVER }
|
|
|
|
const ACTION_TEMPLATES := {
|
|
"walk_to": {
|
|
"label": "Walk To",
|
|
"icon": "🚶",
|
|
"params": [
|
|
{ "key": "target", "type": "position", "required": true },
|
|
],
|
|
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
|
|
},
|
|
"speak": {
|
|
"label": "Speak",
|
|
"icon": "💬",
|
|
"params": [
|
|
{ "key": "text", "type": "text", "required": true },
|
|
{ "key": "duration", "type": "float", "default": 2.0 },
|
|
],
|
|
"reactive_role": ReactiveRole.OVERLAY, "overlay_eligible": true,
|
|
},
|
|
"wait": {
|
|
"label": "Wait",
|
|
"icon": "⏳",
|
|
"params": [
|
|
{ "key": "duration", "type": "float", "required": true },
|
|
],
|
|
"reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false,
|
|
},
|
|
"ragdoll": {
|
|
"label": "Ragdoll",
|
|
"icon": "💥",
|
|
"params": [],
|
|
"reactive_role": ReactiveRole.TAKEOVER, "overlay_eligible": false,
|
|
},
|
|
"recover": {
|
|
"label": "Recover",
|
|
"icon": "🔄",
|
|
"params": [],
|
|
"reactive_role": ReactiveRole.TAKEOVER, "overlay_eligible": false,
|
|
},
|
|
"pick_up_prop": { "label": "Pick Up Prop", "icon": "🖐️", "params": [{"key": "target_prop", "type": "prop", "required": true}, {"key": "hand", "type": "hand", "default": "auto"}], "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_prop", "type": "prop", "required": true}, {"key": "target_position", "type": "position", "required": true}, {"key": "flight_time", "type": "float", "default": 0.9}], "reactive_role": ReactiveRole.SEQUENTIAL, "overlay_eligible": false },
|
|
}
|
|
|
|
|
|
static func types() -> Array[String]:
|
|
# Dictionary.keys() returns an untyped Array at runtime; build a genuinely
|
|
# typed Array[String] so callers can store it in typed locals (see
|
|
# ActionEditor._current_type / RuleEditor._select_action_type).
|
|
var out: Array[String] = []
|
|
out.assign(ACTION_TEMPLATES.keys())
|
|
return out
|
|
|
|
|
|
static func has_type(type: String) -> bool:
|
|
return ACTION_TEMPLATES.has(type)
|
|
|
|
|
|
static func label(type: String) -> String:
|
|
var tpl: Dictionary = ACTION_TEMPLATES.get(type, {})
|
|
return String(tpl.get("label", type))
|
|
|
|
|
|
static func icon(type: String) -> String:
|
|
var tpl: Dictionary = ACTION_TEMPLATES.get(type, {})
|
|
return String(tpl.get("icon", ""))
|
|
|
|
|
|
static func is_overlay_eligible(type: String) -> bool:
|
|
var tpl: Dictionary = ACTION_TEMPLATES.get(type, {})
|
|
return bool(tpl.get("overlay_eligible", false))
|
|
|
|
|
|
static func reactive_role(action: Dictionary) -> ReactiveRole:
|
|
var type: String = String(action.get("type", ""))
|
|
var tpl: Dictionary = ACTION_TEMPLATES.get(type, {})
|
|
var role: ReactiveRole = int(tpl.get("reactive_role", ReactiveRole.SEQUENTIAL))
|
|
var params: Dictionary = action.get("params", {})
|
|
if type == "speak" and bool(params.get("stop_current", action.get("stop_current", false))):
|
|
return ReactiveRole.SEQUENTIAL
|
|
return role
|
|
|
|
|
|
## Converts a flat queue action into a rule-action shape (adds `target` + nests
|
|
## params). `target_id` is the acting stickman's instance id.
|
|
static func to_rule_action(action: Dictionary, target_id: int) -> Dictionary:
|
|
var params: Dictionary = {}
|
|
match String(action.get("type", "")):
|
|
"walk_to":
|
|
params["target"] = action.get("target", Vector2.ZERO)
|
|
"speak":
|
|
params["text"] = String(action.get("text", ""))
|
|
params["duration"] = float(action.get("duration", 2.0))
|
|
if bool(action.get("stop_current", false)):
|
|
params["stop_current"] = true
|
|
"wait":
|
|
params["duration"] = float(action.get("duration", 0.0))
|
|
"pick_up_prop":
|
|
params["target_prop"] = int(action.get("target_prop", -1)); params["hand"] = String(action.get("hand", "auto"))
|
|
"throw_prop":
|
|
params["target_prop"] = int(action.get("target_prop", -1)); params["target_position"] = action.get("target_position", Vector2.ZERO); params["flight_time"] = float(action.get("flight_time", 0.9))
|
|
return {
|
|
"type": String(action.get("type", "")),
|
|
"target": target_id,
|
|
"params": params,
|
|
}
|
|
|
|
|
|
## Converts a rule-action shape back into a flat queue action (drops `target`).
|
|
static func from_rule_action(rule_action: Dictionary) -> Dictionary:
|
|
var params: Dictionary = rule_action.get("params", {})
|
|
match String(rule_action.get("type", "")):
|
|
"walk_to":
|
|
return { "type": "walk_to", "target": params.get("target", Vector2.ZERO) }
|
|
"speak":
|
|
var speak: Dictionary = {
|
|
"type": "speak",
|
|
"text": String(params.get("text", "")),
|
|
"duration": float(params.get("duration", 2.0)),
|
|
}
|
|
if bool(params.get("stop_current", false)):
|
|
speak["stop_current"] = true
|
|
return speak
|
|
"wait":
|
|
return { "type": "wait", "duration": float(params.get("duration", 0.0)) }
|
|
"pick_up_prop": return {"type": "pick_up_prop", "target_prop": int(params.get("target_prop", -1)), "hand": String(params.get("hand", "auto"))}
|
|
"drop_prop": return {"type": "drop_prop"}
|
|
"throw_prop": return {"type": "throw_prop", "target_prop": int(params.get("target_prop", -1)), "target_position": params.get("target_position", Vector2.ZERO), "flight_time": float(params.get("flight_time", 0.9))}
|
|
_:
|
|
return { "type": String(rule_action.get("type", "")) }
|
|
|
|
|
|
## Human-readable one-line summary for both flat queue actions and rule-action
|
|
## shapes (the get() fallbacks tolerate either key layout).
|
|
static func summarize(action: Dictionary) -> String:
|
|
var type := String(action.get("type", ""))
|
|
var params: Dictionary = action.get("params", {})
|
|
match type:
|
|
"walk_to":
|
|
# Rule actions use top-level `target` for the actor instance id and
|
|
# nested `params.target` for the waypoint. Prefer the nested value and
|
|
# type-guard legacy/malformed payloads so summaries never crash.
|
|
var raw_target: Variant = params.get("target", action.get("target", Vector2.ZERO))
|
|
var t: Vector2 = raw_target as Vector2 if raw_target is Vector2 else Vector2.ZERO
|
|
return "Walk To (%d, %d)" % [int(roundf(t.x)), int(roundf(t.y))]
|
|
"speak":
|
|
var text := String(action.get("text", params.get("text", "")))
|
|
var dur := float(action.get("duration", params.get("duration", 2.0)))
|
|
var suffix: String = " — stops current" if bool(params.get("stop_current", action.get("stop_current", false))) else ""
|
|
return "Speak \"%s\" (%ss)%s" % [text, _fmt_duration(dur), suffix]
|
|
"wait":
|
|
var wd := float(action.get("duration", params.get("duration", 0.0)))
|
|
return "Wait %ss" % _fmt_duration(wd)
|
|
"ragdoll":
|
|
return "Ragdoll"
|
|
"recover":
|
|
return "Recover"
|
|
"pick_up_prop": return "Pick Up Prop"
|
|
"drop_prop": return "Drop Prop"
|
|
"throw_prop": return "Throw Prop"
|
|
_:
|
|
return type
|
|
|
|
|
|
static func _fmt_duration(v: float) -> String:
|
|
if v == roundf(v):
|
|
return str(int(v))
|
|
return str(v)
|