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