253 lines
7.5 KiB
GDScript
253 lines
7.5 KiB
GDScript
class_name ActionEditor
|
|
extends PopupPanel
|
|
## ActionEditor - Single-action property editor popup (Phase 3c).
|
|
##
|
|
## Used for adding a new action and for editing an existing one (queue or rule).
|
|
## The type dropdown and the param fields are generated from ActionRegistry, so
|
|
## new action types appear automatically. For `walk_to`, the target position is
|
|
## captured on the stage: the editor emits `target_requested()` and the stage
|
|
## hides the editor, captures a click, then calls `set_walk_target()`.
|
|
|
|
const ACTION_REGISTRY := preload("res://scripts/action_registry.gd")
|
|
|
|
signal committed(action: Dictionary)
|
|
signal cancelled()
|
|
signal target_requested()
|
|
|
|
var _mode: String = "new"
|
|
var _initial: Dictionary = {}
|
|
var _type_option: OptionButton = null
|
|
var _params_box: VBoxContainer = null
|
|
var _text_edit: LineEdit = null
|
|
var _duration_spin: SpinBox = null
|
|
var _target_label: Label = null
|
|
var _set_target_btn: Button = null
|
|
var _has_target: bool = false
|
|
var _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.
|
|
var _ui_font: Font = null
|
|
var _emoji_font: Font = null
|
|
var _base_font_size: int = 0
|
|
|
|
|
|
func _ready() -> void:
|
|
exclusive = true
|
|
popup_window = true
|
|
_build_ui()
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
if (event as InputEventKey).keycode == KEY_ESCAPE:
|
|
cancelled.emit()
|
|
|
|
|
|
## Opens the editor in "add" mode (empty, default type walk_to).
|
|
func open_new() -> void:
|
|
_mode = "new"
|
|
_initial = {}
|
|
_has_target = false
|
|
_target_pos = Vector2.ZERO
|
|
_select_type("walk_to")
|
|
_rebuild_params("walk_to")
|
|
title = "Add Action"
|
|
popup_centered()
|
|
|
|
|
|
## Opens the editor pre-filled from a flat queue action.
|
|
func open_edit(action: Dictionary) -> void:
|
|
_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
|
|
_select_type(type)
|
|
_rebuild_params(type)
|
|
title = "Edit Action"
|
|
popup_centered()
|
|
|
|
|
|
## Called by the stage after a walk-target capture click.
|
|
func set_walk_target(pos: Vector2) -> void:
|
|
_has_target = true
|
|
_target_pos = pos
|
|
_update_walk_target_label()
|
|
popup_centered()
|
|
|
|
|
|
func _build_ui() -> void:
|
|
var margin := MarginContainer.new()
|
|
margin.add_theme_constant_override("margin_left", 12)
|
|
margin.add_theme_constant_override("margin_right", 12)
|
|
margin.add_theme_constant_override("margin_top", 12)
|
|
margin.add_theme_constant_override("margin_bottom", 12)
|
|
add_child(margin)
|
|
|
|
var vbox := VBoxContainer.new()
|
|
vbox.add_theme_constant_override("separation", 8)
|
|
margin.add_child(vbox)
|
|
|
|
var type_row := HBoxContainer.new()
|
|
vbox.add_child(type_row)
|
|
var type_label := Label.new()
|
|
type_label.text = "Type:"
|
|
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():
|
|
_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)
|
|
|
|
_params_box = VBoxContainer.new()
|
|
_params_box.add_theme_constant_override("separation", 6)
|
|
vbox.add_child(_params_box)
|
|
|
|
var buttons := HBoxContainer.new()
|
|
buttons.alignment = BoxContainer.ALIGNMENT_END
|
|
buttons.add_theme_constant_override("separation", 8)
|
|
vbox.add_child(buttons)
|
|
|
|
var cancel := Button.new()
|
|
cancel.text = "Cancel"
|
|
cancel.pressed.connect(func() -> void: cancelled.emit())
|
|
buttons.add_child(cancel)
|
|
|
|
var ok := Button.new()
|
|
ok.text = "OK"
|
|
ok.pressed.connect(_on_ok_pressed)
|
|
buttons.add_child(ok)
|
|
|
|
min_size = Vector2i(340, 200)
|
|
|
|
|
|
## Applies theme font/size overrides (mirrors AssetSelector.apply_font). Called by
|
|
## the stage after add_child so the editor's UI is already built.
|
|
func apply_font(ui_font: Font, emoji_font: Font, sizes: Dictionary) -> void:
|
|
_ui_font = ui_font
|
|
_emoji_font = emoji_font
|
|
_base_font_size = int(sizes.get("action_editor", 18))
|
|
_apply_font_recursive(self, _base_font_size)
|
|
|
|
|
|
func _apply_font_recursive(node: Node, size: int) -> void:
|
|
for child: Node in node.get_children():
|
|
if child is Control:
|
|
_apply_font_to(child as Control, size)
|
|
_apply_font_recursive(child, size)
|
|
|
|
|
|
func _apply_font_to(c: Control, size: int) -> void:
|
|
if c == null:
|
|
return
|
|
if _ui_font != null:
|
|
c.add_theme_font_override("font", _ui_font)
|
|
elif _emoji_font != null:
|
|
c.add_theme_font_override("font", _emoji_font)
|
|
if size > 0:
|
|
c.add_theme_font_size_override("font_size", size)
|
|
|
|
|
|
func _select_type(type: String) -> void:
|
|
var idx := ACTION_REGISTRY.types().find(type)
|
|
if idx < 0:
|
|
idx = 0
|
|
_type_option.select(idx)
|
|
|
|
|
|
func _current_type() -> String:
|
|
var types := ACTION_REGISTRY.types()
|
|
var idx := _type_option.selected
|
|
if idx < 0 or idx >= types.size():
|
|
return "walk_to"
|
|
return types[idx]
|
|
|
|
|
|
func _on_type_selected(_index: int) -> void:
|
|
_rebuild_params(_current_type())
|
|
|
|
|
|
## Rebuilds the param controls for the given action type.
|
|
func _rebuild_params(type: String) -> void:
|
|
for child: Node in _params_box.get_children():
|
|
child.queue_free()
|
|
_text_edit = null
|
|
_duration_spin = null
|
|
_target_label = null
|
|
_set_target_btn = null
|
|
|
|
match type:
|
|
"walk_to":
|
|
var trow := HBoxContainer.new()
|
|
_params_box.add_child(trow)
|
|
_target_label = Label.new()
|
|
_target_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
trow.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())
|
|
trow.add_child(_set_target_btn)
|
|
_update_walk_target_label()
|
|
"speak":
|
|
var tlabel := Label.new()
|
|
tlabel.text = "Text:"
|
|
_params_box.add_child(tlabel)
|
|
_text_edit = LineEdit.new()
|
|
_text_edit.placeholder_text = "Say something…"
|
|
_text_edit.text = String(_initial.get("text", ""))
|
|
_params_box.add_child(_text_edit)
|
|
var dlabel := Label.new()
|
|
dlabel.text = "Duration (s):"
|
|
_params_box.add_child(dlabel)
|
|
_duration_spin = _make_duration_spin(float(_initial.get("duration", 2.0)))
|
|
_params_box.add_child(_duration_spin)
|
|
"wait":
|
|
var wlabel := Label.new()
|
|
wlabel.text = "Duration (s):"
|
|
_params_box.add_child(wlabel)
|
|
_duration_spin = _make_duration_spin(float(_initial.get("duration", 1.0)))
|
|
_params_box.add_child(_duration_spin)
|
|
_:
|
|
# ragdoll / recover (and any future no-param action) need no fields.
|
|
pass
|
|
_apply_font_recursive(_params_box, _base_font_size)
|
|
|
|
|
|
func _make_duration_spin(value: float) -> SpinBox:
|
|
var spin := SpinBox.new()
|
|
spin.min_value = 0.1
|
|
spin.max_value = 3600.0
|
|
spin.step = 0.1
|
|
spin.value = value
|
|
spin.custom_minimum_size = Vector2(120.0, 0.0)
|
|
return spin
|
|
|
|
|
|
func _update_walk_target_label() -> void:
|
|
if _target_label == null:
|
|
return
|
|
if _has_target:
|
|
_target_label.text = "Target: (%d, %d)" % [int(roundf(_target_pos.x)), int(roundf(_target_pos.y))]
|
|
else:
|
|
_target_label.text = "Target: not set"
|
|
|
|
|
|
func _on_ok_pressed() -> void:
|
|
var type := _current_type()
|
|
var action: Dictionary = { "type": type }
|
|
match type:
|
|
"walk_to":
|
|
if not _has_target:
|
|
# Ask the stage to capture the target before committing.
|
|
target_requested.emit()
|
|
return
|
|
action["target"] = _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
|
|
"wait":
|
|
action["duration"] = _duration_spin.value if _duration_spin != null else 1.0
|
|
committed.emit(action)
|