class_name CardGrid extends Control ## CardGrid - Virtualized scrolling grid for displaying cards in the deck builder signal card_selected(card: CardDatabase.CardData) signal card_double_clicked(card: CardDatabase.CardData) const CARD_WIDTH: float = 140.0 const CARD_HEIGHT: float = 196.0 const CARD_GAP: float = 8.0 const COLUMNS: int = 5 const VISIBLE_ROWS_BUFFER: int = 2 var filtered_cards: Array = [] # Array of CardData var card_cells: Array[Control] = [] var scroll_container: ScrollContainer var grid_content: Control var visible_start_row: int = 0 var total_rows: int = 0 var last_click_time: float = 0.0 var last_clicked_card: CardDatabase.CardData = null # Loading indicator var loading_label: Label func _ready() -> void: _create_ui() func _create_ui() -> void: # Main scroll container scroll_container = ScrollContainer.new() scroll_container.set_anchors_preset(Control.PRESET_FULL_RECT) scroll_container.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll_container.get_v_scroll_bar().value_changed.connect(_on_scroll_changed) add_child(scroll_container) # Grid content container (sized to fit all cards) grid_content = Control.new() grid_content.mouse_filter = Control.MOUSE_FILTER_IGNORE scroll_container.add_child(grid_content) # Loading label loading_label = Label.new() loading_label.text = "Loading cards..." loading_label.set_anchors_preset(Control.PRESET_CENTER) loading_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER loading_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5)) add_child(loading_label) # Pre-create card cell pool _create_cell_pool() func _create_cell_pool() -> void: # Calculate max visible cells needed var viewport_height = get_viewport_rect().size.y if get_viewport() else 900.0 var max_visible_rows = ceili(viewport_height / (CARD_HEIGHT + CARD_GAP)) + VISIBLE_ROWS_BUFFER * 2 var pool_size = max_visible_rows * COLUMNS for i in range(pool_size): var cell = _create_card_cell() cell.visible = false grid_content.add_child(cell) card_cells.append(cell) func _create_card_cell() -> Control: var cell = Panel.new() cell.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT) cell.size = Vector2(CARD_WIDTH, CARD_HEIGHT) cell.mouse_filter = Control.MOUSE_FILTER_STOP var style = StyleBoxFlat.new() style.bg_color = Color(0.15, 0.15, 0.2, 0.8) style.border_color = Color(0.3, 0.3, 0.35) style.set_border_width_all(1) style.set_corner_radius_all(3) cell.add_theme_stylebox_override("panel", style) # Card image var tex_rect = TextureRect.new() tex_rect.name = "TextureRect" tex_rect.set_anchors_preset(Control.PRESET_FULL_RECT) tex_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED cell.add_child(tex_rect) # Fallback color rect var fallback = ColorRect.new() fallback.name = "Fallback" fallback.set_anchors_preset(Control.PRESET_FULL_RECT) fallback.visible = false cell.add_child(fallback) # Card name label (shown on fallback) var name_label = Label.new() name_label.name = "NameLabel" name_label.set_anchors_preset(Control.PRESET_FULL_RECT) name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.autowrap_mode = TextServer.AUTOWRAP_WORD name_label.add_theme_font_size_override("font_size", 10) name_label.visible = false cell.add_child(name_label) # Hover highlight var highlight = ColorRect.new() highlight.name = "Highlight" highlight.set_anchors_preset(Control.PRESET_FULL_RECT) highlight.color = Color(1.0, 1.0, 1.0, 0.0) highlight.mouse_filter = Control.MOUSE_FILTER_IGNORE cell.add_child(highlight) # Input handling cell.gui_input.connect(_on_cell_input.bind(cell)) cell.mouse_entered.connect(_on_cell_hover.bind(cell, true)) cell.mouse_exited.connect(_on_cell_hover.bind(cell, false)) return cell func _on_cell_input(event: InputEvent, cell: Control) -> void: if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: var card = cell.get_meta("card", null) if card: var current_time = Time.get_ticks_msec() / 1000.0 if card == last_clicked_card and current_time - last_click_time < 0.4: # Double click card_double_clicked.emit(card) last_clicked_card = null else: # Single click card_selected.emit(card) last_clicked_card = card last_click_time = current_time func _on_cell_hover(cell: Control, entered: bool) -> void: var highlight = cell.get_node("Highlight") as ColorRect if highlight: highlight.color.a = 0.15 if entered else 0.0 func set_cards(cards: Array) -> void: filtered_cards = cards total_rows = ceili(float(cards.size()) / COLUMNS) if cards.size() > 0 else 0 # Update content size var content_width = COLUMNS * (CARD_WIDTH + CARD_GAP) - CARD_GAP var content_height = total_rows * (CARD_HEIGHT + CARD_GAP) grid_content.custom_minimum_size = Vector2(content_width, content_height) loading_label.visible = cards.is_empty() # Reset scroll and update visible cells scroll_container.scroll_vertical = 0 _update_visible_cells() func _on_scroll_changed(_value: float) -> void: _update_visible_cells() func _update_visible_cells() -> void: if filtered_cards.is_empty(): for cell in card_cells: cell.visible = false return var scroll_y = scroll_container.scroll_vertical var viewport_height = scroll_container.size.y # Calculate visible row range var first_visible_row = int(scroll_y / (CARD_HEIGHT + CARD_GAP)) var last_visible_row = ceili((scroll_y + viewport_height) / (CARD_HEIGHT + CARD_GAP)) # Add buffer first_visible_row = maxi(0, first_visible_row - VISIBLE_ROWS_BUFFER) last_visible_row = mini(total_rows - 1, last_visible_row + VISIBLE_ROWS_BUFFER) # Update cells var cell_index = 0 for row in range(first_visible_row, last_visible_row + 1): for col in range(COLUMNS): var card_index = row * COLUMNS + col if card_index >= filtered_cards.size(): break if cell_index < card_cells.size(): var cell = card_cells[cell_index] var card = filtered_cards[card_index] # Position cell cell.position = Vector2( col * (CARD_WIDTH + CARD_GAP), row * (CARD_HEIGHT + CARD_GAP) ) # Update cell content _update_cell_content(cell, card) cell.visible = true cell_index += 1 # Hide unused cells for i in range(cell_index, card_cells.size()): card_cells[i].visible = false func _update_cell_content(cell: Control, card: CardDatabase.CardData) -> void: var current_id = cell.get_meta("card_id", "") if current_id == card.id: return # Already showing this card cell.set_meta("card_id", card.id) cell.set_meta("card", card) var tex_rect = cell.get_node("TextureRect") as TextureRect var fallback = cell.get_node("Fallback") as ColorRect var name_label = cell.get_node("NameLabel") as Label # Load texture var texture = CardDatabase.get_card_texture(card) if texture: tex_rect.texture = texture tex_rect.visible = true fallback.visible = false name_label.visible = false else: tex_rect.visible = false fallback.visible = true fallback.color = Enums.element_to_color(card.get_primary_element()).darkened(0.3) name_label.visible = true name_label.text = card.name ## Get currently displayed card count func get_card_count() -> int: return filtered_cards.size()