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
+286
View File
@@ -0,0 +1,286 @@
class_name QueuePanel
extends PopupPanel
## QueuePanel - Action Queue editor popup (Phase 3c.2).
##
## Shows all queued actions for one stickman, in order, with edit / delete /
## drag-reorder controls. Mutations go through the StickmanRig queue API
## (queue_action / remove_action / insert_action / clear_queue), which already
## emits `queue_changed` and redraws the director overlay. Editing and adding
## delegate to the stage via signals so this panel stays decoupled.
const ACTION_REGISTRY := preload("res://scripts/action_registry.gd")
const STICKMAN_RIG := preload("res://scripts/stickman_rig.gd")
signal edit_requested(index: int)
signal delete_requested(index: int)
signal add_requested()
signal clear_requested()
var rig: StickmanRig = null
var _title_label: Label = null
var _list: VBoxContainer = null
var _empty_label: Label = null
var _rows: Array[PanelContainer] = []
## Drag-reorder state.
var _drag_index: int = -1
var _drag_target: int = -1
## 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
var _row_font_size: int = 0
var _title_font_size: int = 0
var _title_font: Font = null
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:
hide()
## Attaches a rig and rebuilds the row list.
func setup(r: StickmanRig) -> void:
rig = r
refresh()
func refresh() -> void:
if _list == null:
return
for child: Node in _list.get_children():
if child != _empty_label:
child.queue_free()
_rows.clear()
_drag_index = -1
_drag_target = -1
var queue: Array[Dictionary] = []
if rig != null and is_instance_valid(rig):
queue = rig.get_queue()
_title_label.text = "Stickman: %s" % (String(rig.name) if rig != null and is_instance_valid(rig) else "?")
_empty_label.visible = queue.is_empty()
for i: int in queue.size():
_rows.append(_make_row(i, queue[i]))
func _build_ui() -> void:
title = "Action Queue"
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 title_bar := HBoxContainer.new()
vbox.add_child(title_bar)
_title_label = Label.new()
_title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_title_label.text = "Stickman: ?"
title_bar.add_child(_title_label)
var close_btn := Button.new()
close_btn.text = "× Close"
close_btn.pressed.connect(hide)
title_bar.add_child(close_btn)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.custom_minimum_size = Vector2(0.0, 260.0)
vbox.add_child(scroll)
_list = VBoxContainer.new()
_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_list.add_theme_constant_override("separation", 4)
scroll.add_child(_list)
_empty_label = Label.new()
_empty_label.text = "Queue is empty."
_empty_label.modulate = Color(1.0, 1.0, 1.0, 0.5)
_list.add_child(_empty_label)
var footer := HBoxContainer.new()
footer.add_theme_constant_override("separation", 8)
vbox.add_child(footer)
var add_btn := Button.new()
add_btn.text = " Add Action"
add_btn.pressed.connect(func() -> void: add_requested.emit())
footer.add_child(add_btn)
var clear_btn := Button.new()
clear_btn.text = "🗑 Clear All"
clear_btn.pressed.connect(func() -> void: clear_requested.emit())
footer.add_child(clear_btn)
min_size = Vector2i(460, 360)
func _make_row(index: int, action: Dictionary) -> PanelContainer:
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _row_style(Color(0.0, 0.0, 0.0, 0.0)))
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
panel.add_child(row)
var number := Label.new()
number.text = str(index + 1)
row.add_child(number)
var summary := Label.new()
summary.size_flags_horizontal = Control.SIZE_EXPAND_FILL
summary.text = "%s %s" % [ACTION_REGISTRY.icon(String(action.get("type", ""))), ACTION_REGISTRY.summarize(action)]
row.add_child(summary)
var edit := Button.new()
edit.text = ""
edit.tooltip_text = "Edit"
edit.pressed.connect(func() -> void: edit_requested.emit(index))
row.add_child(edit)
var remove := Button.new()
remove.text = ""
remove.tooltip_text = "Delete"
remove.pressed.connect(func() -> void: delete_requested.emit(index))
row.add_child(remove)
var drag := Button.new()
drag.text = ""
drag.tooltip_text = "Drag to reorder"
drag.mouse_default_cursor_shape = Control.CURSOR_MOVE
drag.gui_input.connect(_on_drag_handle_gui_input.bind(index))
row.add_child(drag)
# Parent the row into the list and apply the theme font/size overrides. Without
# add_child the row never renders and drag-reorder has no geometry to work with.
_list.add_child(panel)
_apply_font_recursive(panel, _row_font_size)
return panel
## Applies theme font/size overrides (mirrors AssetSelector.apply_font). Called by
## the stage after add_child so the panel's UI is already built. `sizes` carries the
## parsed per-widget size / title / row sizes, the panel-title bold flag, and the
## resolved bold/italic Font variants.
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("queue_panel", 18))
_row_font_size = int(sizes.get("panel_row", 16))
_title_font_size = int(sizes.get("panel_title", 18))
var title_bold := bool(sizes.get("panel_title_bold", true))
var bold_font: Font = sizes.get("bold_font", null)
_title_font = bold_font if title_bold and bold_font != null else ui_font
_apply_font_recursive(self, _base_font_size)
if _title_label != null:
_apply_font_to(_title_label, _title_font_size, _title_font)
if _empty_label != null:
_apply_font_to(_empty_label, _row_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, font: Font = null) -> void:
if c == null:
return
if font != null:
c.add_theme_font_override("font", font)
elif _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 _row_style(bg: Color) -> StyleBoxFlat:
var sb := StyleBoxFlat.new()
sb.bg_color = bg
sb.set_corner_radius_all(4)
sb.content_margin_left = 6.0
sb.content_margin_right = 6.0
sb.content_margin_top = 2.0
sb.content_margin_bottom = 2.0
return sb
func _on_drag_handle_gui_input(event: InputEvent, index: int) -> void:
if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT:
if (event as InputEventMouseButton).pressed:
_drag_index = index
_drag_target = index
_refresh_drag_highlight()
else:
_commit_drag()
elif event is InputEventMouseMotion and _drag_index >= 0:
_update_drag_target()
func _update_drag_target() -> void:
var mouse_y := _list.get_local_mouse_position().y
var best := _drag_target
var best_d := INF
for i: int in _rows.size():
var row := _rows[i]
var d := absf((row.position.y + row.size.y * 0.5) - mouse_y)
if d < best_d:
best_d = d
best = i
if best != _drag_target:
_drag_target = best
_refresh_drag_highlight()
func _refresh_drag_highlight() -> void:
for i: int in _rows.size():
var bg := Color(0.0, 0.0, 0.0, 0.0)
if _drag_index >= 0 and i == _drag_target:
bg = Color(0.15, 0.4, 0.9, 0.4)
(_rows[i] as PanelContainer).add_theme_stylebox_override("panel", _row_style(bg))
func _commit_drag() -> void:
var from := _drag_index
var to := _drag_target
_drag_index = -1
_drag_target = -1
_refresh_drag_highlight()
if from < 0 or to < 0 or from == to:
return
_move_action(from, to)
refresh()
## Moves the action at `from` to `to` in the rig's queue (indices in the
## pre-removal space), using the existing remove/insert API.
func _move_action(from: int, to: int) -> void:
if rig == null or not is_instance_valid(rig):
return
var queue := rig.get_queue()
if from < 0 or from >= queue.size() or to < 0 or to >= queue.size():
return
var action: Dictionary = queue[from]
rig.remove_action(from)
var target := to
if to > from:
target -= 1
rig.insert_action(target, action)