Files
stickman/scripts/shared/ui/asset_selector.gd
T

323 lines
12 KiB
GDScript

class_name AssetSelector
extends PopupPanel
signal item_selected(entry: Dictionary)
signal cancelled()
signal browse_requested()
signal refresh_requested()
signal make_artwork_requested()
signal edit_scenery_requested()
const COLUMNS := 4
const ROWS := 3
const PAGE_SIZE := COLUMNS * ROWS
const CELL_MIN_SIZE := Vector2(190.0, 220.0)
const THUMB_SIZE := Vector2(156.0, 156.0)
const ASSET_CARD := preload("res://scripts/shared/ui/asset_card.gd")
const BACKGROUND_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_background_card_preview.gd")
const STARTER_CARD_PREVIEW := preload("res://scripts/stage/scenery/stage_starter_card_preview.gd")
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
var _sandbox_theme: Variant = null
var _gallery_tab: String = "backgrounds"
var _tag_id: String = "all"
var _current_sky_id: String = ""
var _sky_card_group: ButtonGroup
@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
@onready var _tabs: HBoxContainer = %GalleryTabs
@onready var _backgrounds_tab: Button = %BackgroundsTab
@onready var _artwork_tab: Button = %ArtworkTab
@onready var _tag_filter: OptionButton = %TagFilter
@onready var _make_artwork_button: Button = %MakeArtworkButton
@onready var _edit_scenery_button: Button = %EditSceneryButton
func _ready() -> void:
exclusive = true
if _tag_filter.item_count == 0:
for label: String in ["All artwork", "Nature", "Buildings", "Walls", "Roads", "Objects"]:
_tag_filter.add_item(label)
_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())
_backgrounds_tab.pressed.connect(func() -> void: _set_gallery_tab("backgrounds"))
_artwork_tab.pressed.connect(func() -> void: _set_gallery_tab("artwork"))
_tag_filter.item_selected.connect(_on_tag_filter_selected)
_make_artwork_button.pressed.connect(func() -> void: make_artwork_requested.emit())
_edit_scenery_button.pressed.connect(func() -> void: edit_scenery_requested.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], current_sky_id: String = "") -> void:
kind = p_kind
_current_sky_id = current_sky_id if kind == "scenery" else ""
_title_label.text = "Choose Your Stickman" if kind == "stickman" else ("Choose Scenery" if kind == "scenery" else ("Choose a starter scene" if kind == "starters" else "Choose a Prop"))
_tabs.visible = kind == "scenery"
_tag_filter.visible = false
_make_artwork_button.visible = false
_edit_scenery_button.visible = false
_browse_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery"
_refresh_button.visible = kind == "stickman" or kind == "prop" or kind == "scenery"
_empty_label.text = "No scenery found. Choose Make artwork to draw a new piece." if kind == "scenery" else ("No props found. Browse for a .prop file." if kind == "prop" else ("No starter scenes are available." if kind == "starters" else "No stickmen found."))
_gallery_tab = "backgrounds"
_tag_id = "all"
_backgrounds_tab.set_pressed_no_signal(true)
_artwork_tab.set_pressed_no_signal(false)
_tag_filter.select(0)
set_entries(entries)
if kind == "scenery":
_set_gallery_tab("backgrounds")
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 set_current_sky(sky_id: StringName) -> void:
var next_id: String = String(sky_id)
if kind != "scenery" or _current_sky_id == next_id:
return
_current_sky_id = next_id
if _gallery_tab == "backgrounds":
_rebuild()
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 apply_theme(theme: Variant) -> void:
_sandbox_theme = theme
if theme != null:
self.theme = theme.control_theme()
add_theme_stylebox_override("panel", theme.stylebox("popups.asset_selector"))
var cell_size: Variant = theme.value("popups.asset_selector.cell_size", CELL_MIN_SIZE)
if cell_size is Array and (cell_size as Array).size() == 2:
_cell_min_size = Vector2(float(cell_size[0]), float(cell_size[1]))
var thumbnail_size: Variant = theme.value("popups.asset_selector.thumbnail_size", THUMB_SIZE)
if thumbnail_size is Array and (thumbnail_size as Array).size() == 2:
_thumbnail_size = Vector2(float(thumbnail_size[0]), float(thumbnail_size[1]))
var _cell_min_size: Vector2 = CELL_MIN_SIZE
var _thumbnail_size: Vector2 = THUMB_SIZE
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_tag_filter_selected(index: int) -> void:
var ids: Array[String] = ["all", "nature", "buildings", "walls", "roads", "objects"]
_tag_id = ids[clampi(index, 0, ids.size() - 1)]
_page = 0
_rebuild()
func _set_gallery_tab(tab: String) -> void:
_gallery_tab = tab
_backgrounds_tab.set_pressed_no_signal(tab == "backgrounds")
_artwork_tab.set_pressed_no_signal(tab == "artwork")
_tag_filter.visible = kind == "scenery" and tab == "artwork"
_make_artwork_button.visible = kind == "scenery" and tab == "artwork"
_edit_scenery_button.visible = kind == "scenery"
_browse_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork")
_refresh_button.visible = kind == "stickman" or kind == "prop" or (kind == "scenery" and tab == "artwork")
_empty_label.text = "No artwork in this group yet." if tab == "artwork" else "Choose a Sky look or wall finish."
_page = 0
_rebuild()
func _filtered_entries() -> Array[Dictionary]:
if kind != "scenery":
return _entries
var filtered: Array[Dictionary] = []
for entry: Dictionary in _entries:
if String(entry.get("gallery_tab", "artwork")) != _gallery_tab:
continue
if _gallery_tab == "artwork" and _tag_id != "all":
var tags: Array = entry.get("tags", []) as Array
if not tags.has(_tag_id):
continue
filtered.append(entry)
return filtered
func _on_prev() -> void:
if _page > 0:
_page -= 1
_rebuild()
func _on_next() -> void:
if (_page + 1) * PAGE_SIZE < _filtered_entries().size():
_page += 1
_rebuild()
func _page_count() -> int:
var entries: Array[Dictionary] = _filtered_entries()
if entries.is_empty():
return 1
return int(ceil(float(entries.size()) / float(PAGE_SIZE)))
func _rebuild() -> void:
_sky_card_group = ButtonGroup.new()
for c: Node in _grid.get_children():
c.queue_free()
_cell_texrects.clear()
var filtered: Array[Dictionary] = _filtered_entries()
_empty_label.visible = filtered.is_empty()
_grid.visible = not filtered.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 >= filtered.size()
_page_label.text = "Page %d/%d" % [_page + 1, total]
if filtered.is_empty():
return
var bounds := page_bounds(filtered.size(), _page)
for i: int in range(int(bounds["start"]), int(bounds["end"])):
_grid.add_child(_build_cell(filtered[i]))
func _build_cell(entry: Dictionary) -> Control:
var key := _entry_key(entry)
var cell: Variant = ASSET_CARD.new()
# Selector cells inherit the popup theme. Avoid a per-card state override so
# opening a selector never implies that one asset is pre-selected.
var categories: Array = entry.get("tags", []) as Array
var category: String = ", ".join(PackedStringArray(categories)) if not categories.is_empty() else String(entry.get("category", entry.get("material_label", "")))
var card_size: Vector2 = _cell_min_size
var thumb_size: Vector2 = _thumbnail_size
if kind == "starters":
card_size.y = maxf(card_size.y, 248.0)
thumb_size = Vector2(128.0, 128.0)
var card_payload: Dictionary = {"placement_id": _entry_key(entry), "label": String(entry.get("name", entry.get("id", ""))), "icon": String(entry.get("icon", "")), "category": category, "description": String(entry.get("description", "")), "thumbnail_size": thumb_size}
if entry.has("background_kind"):
card_payload["background_kind"] = entry["background_kind"]
card_payload["finish_id"] = entry.get("finish_id", "")
card_payload["id"] = entry.get("id", "")
var is_sky_choice: bool = String(entry.get("background_kind", "")) == "sky"
var is_current_sky: bool = is_sky_choice and String(entry.get("id", "")) == _current_sky_id
if is_current_sky:
card_payload["status"] = "Current Sky"
card_payload["status_color"] = _theme_color("mode_colors.edit", Color("#22C6FF"))
cell.configure(card_payload, _thumbnails.get(key, null), null)
cell.custom_minimum_size = card_size
cell.focus_mode = Control.FOCUS_ALL
cell.activated.connect(func(_payload: Dictionary) -> void: _on_cell_pressed(entry))
if is_sky_choice:
cell.button_group = _sky_card_group
cell.set_selected(is_current_sky, _theme_color("stage_panels.choice.selected_surface", Color("#12394A")), _theme_color("mode_colors.edit", Color("#22C6FF")))
var tex: Texture2D = _thumbnails.get(key, null)
if kind == "starters":
var preview: Control = STARTER_CARD_PREVIEW.new() as Control
preview.call("configure", entry)
cell.set_preview_control(preview)
elif entry.has("background_kind"):
var preview: StageBackgroundCardPreview = BACKGROUND_CARD_PREVIEW.new() as StageBackgroundCardPreview
preview.configure(entry)
cell.set_preview_control(preview)
else:
cell.set_thumbnail(tex if tex != null else _get_placeholder())
_cell_texrects[key] = cell.thumbnail_rect()
return cell
func _theme_color(path: String, fallback: Color) -> Color:
if _sandbox_theme != null:
return _sandbox_theme.color(path, fallback)
return fallback
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(int(_thumbnail_size.x), int(_thumbnail_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