Add unique identifier for test_phase3c_walk_recovery.gd

This commit is contained in:
2026-09-06 13:09:37 -04:00
parent 1f91f3d2e5
commit 80595a0273
53 changed files with 6806 additions and 471 deletions
+252
View File
@@ -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)