Files
FFCardGame/scripts/ui/CardGrid.gd

242 lines
7.2 KiB
GDScript

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()