242 lines
7.2 KiB
GDScript
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()
|