Add headless regression tests for Phase 4b features
- Implement test for popup anchor behavior in rule-builder menus to ensure consistent anchor positioning during menu transitions. - Create tests for stage logic, including mode transitions, toolbar visibility, and status bar updates. - Add terrain drag-painting tests to verify correct block placement behavior and conflict handling. - Introduce walk waypoint tests to check for arrival conditions and position stability after navigation.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
class_name AssetSelector
|
||||
extends PopupPanel
|
||||
|
||||
signal item_selected(entry: Dictionary)
|
||||
signal cancelled()
|
||||
signal browse_requested()
|
||||
signal refresh_requested()
|
||||
|
||||
const COLUMNS := 4
|
||||
const ROWS := 3
|
||||
const PAGE_SIZE := COLUMNS * ROWS
|
||||
|
||||
const CELL_MIN_SIZE := Vector2(140.0, 168.0)
|
||||
const THUMB_SIZE := Vector2(120.0, 120.0)
|
||||
|
||||
var kind: String = ""
|
||||
|
||||
var _entries: Array[Dictionary] = []
|
||||
var _page: int = 0
|
||||
var _thumbnails: Dictionary = {}
|
||||
var _cell_texrects: Dictionary = {}
|
||||
var _placeholder_tex: ImageTexture = null
|
||||
var _ui_font: Font = null
|
||||
var _emoji_font: Font = null
|
||||
|
||||
@onready var _title_label: Label = %TitleLabel
|
||||
@onready var _grid: GridContainer = %GridContainer
|
||||
@onready var _empty_label: Label = %EmptyLabel
|
||||
@onready var _page_label: Label = %PageLabel
|
||||
@onready var _prev_button: Button = %PrevButton
|
||||
@onready var _next_button: Button = %NextButton
|
||||
@onready var _browse_button: Button = %BrowseButton
|
||||
@onready var _refresh_button: Button = %RefreshButton
|
||||
@onready var _close_button: Button = %CloseButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
exclusive = true
|
||||
_prev_button.pressed.connect(_on_prev)
|
||||
_next_button.pressed.connect(_on_next)
|
||||
_browse_button.pressed.connect(func() -> void: browse_requested.emit())
|
||||
_refresh_button.pressed.connect(func() -> void: refresh_requested.emit())
|
||||
_close_button.pressed.connect(func() -> void: cancelled.emit())
|
||||
get_tree().root.size_changed.connect(_on_root_size_changed)
|
||||
|
||||
|
||||
func _on_root_size_changed() -> void:
|
||||
if visible:
|
||||
popup_centered()
|
||||
|
||||
|
||||
func open(p_kind: String, entries: Array[Dictionary]) -> void:
|
||||
kind = p_kind
|
||||
_title_label.text = "Choose Your Stickman" if kind == "stickman" else "Choose a Prop"
|
||||
_browse_button.visible = kind == "stickman"
|
||||
_refresh_button.visible = kind == "stickman"
|
||||
set_entries(entries)
|
||||
popup_centered()
|
||||
|
||||
|
||||
func set_entries(entries: Array[Dictionary]) -> void:
|
||||
_entries = entries
|
||||
_thumbnails.clear()
|
||||
_cell_texrects.clear()
|
||||
_page = 0
|
||||
_rebuild()
|
||||
|
||||
|
||||
func set_thumbnail(entry: Dictionary, tex: Texture2D) -> void:
|
||||
var key := _entry_key(entry)
|
||||
_thumbnails[key] = tex
|
||||
if _cell_texrects.has(key):
|
||||
(_cell_texrects[key] as TextureRect).texture = tex
|
||||
|
||||
|
||||
func close() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func apply_font(ui_font: Font, emoji_font: Font) -> void:
|
||||
_ui_font = ui_font
|
||||
_emoji_font = emoji_font
|
||||
var controls: Array = [
|
||||
_title_label, _empty_label, _page_label,
|
||||
_prev_button, _next_button, _browse_button, _refresh_button, _close_button,
|
||||
]
|
||||
for c: Control in controls:
|
||||
_apply_font_to(c)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if visible and event is InputEventKey:
|
||||
var key := event as InputEventKey
|
||||
if key.pressed and not key.echo and key.keycode == KEY_ESCAPE:
|
||||
get_viewport().set_input_as_handled()
|
||||
cancelled.emit()
|
||||
|
||||
|
||||
static func page_bounds(total: int, page: int, page_size: int = PAGE_SIZE) -> Dictionary:
|
||||
var page_count := maxi(1, int(ceil(float(total) / float(page_size))))
|
||||
var start := page * page_size
|
||||
if start >= total:
|
||||
start = 0
|
||||
var end := mini(start + page_size, total)
|
||||
return { "start": start, "end": end, "total": total, "page_count": page_count }
|
||||
|
||||
|
||||
func _entry_key(entry: Dictionary) -> String:
|
||||
if kind == "stickman":
|
||||
return String(entry.get("path", ""))
|
||||
return String(entry.get("id", ""))
|
||||
|
||||
|
||||
func _on_prev() -> void:
|
||||
if _page > 0:
|
||||
_page -= 1
|
||||
_rebuild()
|
||||
|
||||
|
||||
func _on_next() -> void:
|
||||
if (_page + 1) * PAGE_SIZE < _entries.size():
|
||||
_page += 1
|
||||
_rebuild()
|
||||
|
||||
|
||||
func _page_count() -> int:
|
||||
if _entries.is_empty():
|
||||
return 1
|
||||
return int(ceil(float(_entries.size()) / float(PAGE_SIZE)))
|
||||
|
||||
|
||||
func _rebuild() -> void:
|
||||
for c: Node in _grid.get_children():
|
||||
c.queue_free()
|
||||
_cell_texrects.clear()
|
||||
_empty_label.visible = _entries.is_empty()
|
||||
_grid.visible = not _entries.is_empty()
|
||||
var total := _page_count()
|
||||
var single := total <= 1
|
||||
_prev_button.visible = not single
|
||||
_next_button.visible = not single
|
||||
_page_label.visible = not single
|
||||
_prev_button.disabled = _page <= 0
|
||||
_next_button.disabled = (_page + 1) * PAGE_SIZE >= _entries.size()
|
||||
_page_label.text = "Page %d/%d" % [_page + 1, total]
|
||||
if _entries.is_empty():
|
||||
return
|
||||
var bounds := page_bounds(_entries.size(), _page)
|
||||
for i: int in range(int(bounds["start"]), int(bounds["end"])):
|
||||
_grid.add_child(_build_cell(_entries[i]))
|
||||
|
||||
|
||||
func _build_cell(entry: Dictionary) -> Control:
|
||||
var key := _entry_key(entry)
|
||||
var cell := Button.new()
|
||||
cell.custom_minimum_size = CELL_MIN_SIZE
|
||||
cell.toggle_mode = false
|
||||
cell.focus_mode = Control.FOCUS_NONE
|
||||
cell.pressed.connect(_on_cell_pressed.bind(entry))
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vbox.add_theme_constant_override("separation", 2)
|
||||
cell.add_child(vbox)
|
||||
|
||||
var tex_rect := TextureRect.new()
|
||||
tex_rect.custom_minimum_size = THUMB_SIZE
|
||||
tex_rect.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
tex_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var tex: Texture2D = _thumbnails.get(key, null)
|
||||
tex_rect.texture = tex if tex != null else _get_placeholder()
|
||||
vbox.add_child(tex_rect)
|
||||
_cell_texrects[key] = tex_rect
|
||||
|
||||
var name_label := Label.new()
|
||||
name_label.text = String(entry.get("name", ""))
|
||||
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
name_label.add_theme_font_size_override("font_size", 14)
|
||||
_apply_font_to(name_label)
|
||||
vbox.add_child(name_label)
|
||||
|
||||
if kind == "prop":
|
||||
var badge := Label.new()
|
||||
badge.text = String(entry.get("material_label", ""))
|
||||
badge.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
badge.add_theme_font_size_override("font_size", 12)
|
||||
badge.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1.0))
|
||||
_apply_font_to(badge)
|
||||
vbox.add_child(badge)
|
||||
|
||||
return cell
|
||||
|
||||
|
||||
func _on_cell_pressed(entry: Dictionary) -> void:
|
||||
item_selected.emit(entry)
|
||||
|
||||
|
||||
func _apply_font_to(c: Control) -> 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)
|
||||
|
||||
|
||||
func _get_placeholder() -> ImageTexture:
|
||||
if _placeholder_tex == null:
|
||||
var img := Image.create(THUMB_SIZE.x, THUMB_SIZE.y, false, Image.FORMAT_RGBA8)
|
||||
img.fill(Color(0.16, 0.16, 0.16, 1.0))
|
||||
_placeholder_tex = ImageTexture.create_from_image(img)
|
||||
return _placeholder_tex
|
||||
Reference in New Issue
Block a user