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)