Files
stickman/scripts/action_registry.gd
T
ryan 6666173c4f Implement speech style toolbar live-edit refinements and whole speech show controls
- Added a new specification for the speech style toolbar live-edit, detailing immediate style application, scoped effect demonstration, and hover feedback.
- Introduced refinements to the speech style toolbar, including improved chooser sizing, exclusive stage editing during speech composition, and removal of redundant controls.
- Documented new stage controls for managing how the whole speech shows, including options for bubble arrangement, word appearance, and bubble motion.
- Created a new log file for speech padding syntax to track engine versioning.
2026-10-03 14:49:22 -04:00

212 lines
8.1 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_id", "type": "prop", "required": true },
{ "key": "hand", "type": "choice", "default": "auto", "choices": ["auto", "left", "right"] },
],
"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_position", "type": "position", "required": true },
],
"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":
if params.has("keep_acting") or action.has("keep_acting"):
return ReactiveRole.OVERLAY if bool(params.get("keep_acting", action.get("keep_acting", false))) else ReactiveRole.SEQUENTIAL
if 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":
for key: String in ["text", "duration", "stop_current", "keep_acting", "visual", "arrangement", "bubble_layout", "connect_bubbles", "reveal", "pop_in", "duration_mode", "placement", "bubbles"]:
if action.has(key):
params[key] = action[key].duplicate(true) if action[key] is Dictionary or action[key] is Array else action[key]
"wait":
params["duration"] = float(action.get("duration", 0.0))
"pick_up_prop":
params["target_prop_id"] = String(action.get("target_prop_id", ""))
params["hand"] = String(action.get("hand", "auto"))
"throw_prop":
params["target_position"] = action.get("target_position", Vector2.ZERO)
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"}
for key: String in ["text", "duration", "stop_current", "keep_acting", "visual", "arrangement", "bubble_layout", "connect_bubbles", "reveal", "pop_in", "duration_mode", "placement", "bubbles"]:
if params.has(key):
speak[key] = params[key].duplicate(true) if params[key] is Dictionary or params[key] is Array else params[key]
if not speak.has("keep_acting"):
speak["keep_acting"] = not bool(params.get("stop_current", false))
return speak
"wait":
return { "type": "wait", "duration": float(params.get("duration", 0.0)) }
"pick_up_prop":
return { "type": "pick_up_prop", "target_prop_id": String(params.get("target_prop_id", "")), "hand": String(params.get("hand", "auto")) }
"drop_prop":
return { "type": "drop_prop" }
"throw_prop":
return { "type": "throw_prop", "target_position": params.get("target_position", Vector2.ZERO) }
_:
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 has_keep_acting: bool = params.has("keep_acting") or action.has("keep_acting")
var keep_acting: bool = bool(params.get("keep_acting", action.get("keep_acting", false)))
var legacy_stop_current: bool = bool(params.get("stop_current", action.get("stop_current", false)))
var suffix: String = " — keeps acting" if has_keep_acting and keep_acting else (" — stops current" if not has_keep_acting and legacy_stop_current 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 (%s)" % String(action.get("target_prop_id", params.get("target_prop_id", "")))
"drop_prop":
return "Drop Prop"
"throw_prop":
var raw_position: Variant = action.get("target_position", params.get("target_position", Vector2.ZERO))
var target_position: Vector2 = raw_position as Vector2 if raw_position is Vector2 else Vector2.ZERO
return "Throw Prop (%d, %d)" % [int(roundf(target_position.x)), int(roundf(target_position.y))]
_:
return type
static func _fmt_duration(v: float) -> String:
if v == roundf(v):
return str(int(v))
return str(v)