Add unique identifier for test_phase3c_walk_recovery.gd
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user