new features, play menu, deck builder, deck selection

This commit is contained in:
2026-01-28 20:22:09 -05:00
parent f4c7bab6b0
commit bf9aa3fa23
80 changed files with 4501 additions and 58 deletions

View File

@@ -0,0 +1,355 @@
class_name CardDetailViewer
extends Control
## CardDetailViewer - Left panel showing enlarged card with details and add-to-deck controls
signal add_to_deck_requested(card: CardDatabase.CardData, quantity: int)
signal card_info_requested(card: CardDatabase.CardData)
const CARD_WIDTH: float = 405.0
const CARD_HEIGHT: float = 567.0
const PANEL_WIDTH: float = 450.0
var current_card: CardDatabase.CardData = null
var current_deck_count: int = 0
var quantity_to_add: int = 1
# UI elements
var card_image: TextureRect
var fallback_rect: ColorRect
var fallback_label: Label
var name_label: Label
var type_label: Label
var element_label: Label
var cost_label: Label
var power_label: Label
var job_label: Label
var category_label: Label
var abilities_label: Label
var quantity_label: Label
var decrease_btn: Button
var increase_btn: Button
var add_button: Button
var no_card_label: Label
func _ready() -> void:
custom_minimum_size = Vector2(PANEL_WIDTH, 0)
_create_ui()
func _create_ui() -> void:
# Main panel
var panel = PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_theme_stylebox_override("panel", _create_panel_style())
add_child(panel)
var main_vbox = VBoxContainer.new()
main_vbox.add_theme_constant_override("separation", 12)
panel.add_child(main_vbox)
# Card image container
var image_container = Control.new()
image_container.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT)
main_vbox.add_child(image_container)
# Actual card image
card_image = TextureRect.new()
card_image.set_anchors_preset(Control.PRESET_FULL_RECT)
card_image.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
card_image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
image_container.add_child(card_image)
# Fallback colored rect (when no image)
fallback_rect = ColorRect.new()
fallback_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
fallback_rect.visible = false
image_container.add_child(fallback_rect)
fallback_label = Label.new()
fallback_label.set_anchors_preset(Control.PRESET_CENTER)
fallback_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
fallback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
fallback_label.add_theme_font_size_override("font_size", 18)
fallback_label.visible = false
image_container.add_child(fallback_label)
# No card selected label
no_card_label = Label.new()
no_card_label.text = "Select a card to view details"
no_card_label.set_anchors_preset(Control.PRESET_CENTER)
no_card_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
no_card_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
image_container.add_child(no_card_label)
# Card info section
var info_vbox = VBoxContainer.new()
info_vbox.add_theme_constant_override("separation", 4)
main_vbox.add_child(info_vbox)
name_label = _create_info_label("", 20, Color(1.0, 0.95, 0.8))
info_vbox.add_child(name_label)
var details_grid = GridContainer.new()
details_grid.columns = 2
details_grid.add_theme_constant_override("h_separation", 12)
details_grid.add_theme_constant_override("v_separation", 4)
info_vbox.add_child(details_grid)
type_label = _create_info_label("")
cost_label = _create_info_label("")
element_label = _create_info_label("")
power_label = _create_info_label("")
job_label = _create_info_label("")
category_label = _create_info_label("")
details_grid.add_child(_create_info_label("Type:", 14, Color(0.6, 0.6, 0.6)))
details_grid.add_child(type_label)
details_grid.add_child(_create_info_label("Cost:", 14, Color(0.6, 0.6, 0.6)))
details_grid.add_child(cost_label)
details_grid.add_child(_create_info_label("Element:", 14, Color(0.6, 0.6, 0.6)))
details_grid.add_child(element_label)
details_grid.add_child(_create_info_label("Power:", 14, Color(0.6, 0.6, 0.6)))
details_grid.add_child(power_label)
details_grid.add_child(_create_info_label("Job:", 14, Color(0.6, 0.6, 0.6)))
details_grid.add_child(job_label)
details_grid.add_child(_create_info_label("Category:", 14, Color(0.6, 0.6, 0.6)))
details_grid.add_child(category_label)
# Abilities section
var abilities_header = _create_info_label("Abilities", 16, Color(0.8, 0.75, 0.6))
info_vbox.add_child(abilities_header)
var abilities_scroll = ScrollContainer.new()
abilities_scroll.custom_minimum_size = Vector2(0, 80)
abilities_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
info_vbox.add_child(abilities_scroll)
abilities_label = Label.new()
abilities_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
abilities_label.add_theme_font_size_override("font_size", 12)
abilities_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
abilities_scroll.add_child(abilities_label)
# Quantity selector
var qty_hbox = HBoxContainer.new()
qty_hbox.add_theme_constant_override("separation", 10)
qty_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
main_vbox.add_child(qty_hbox)
quantity_label = Label.new()
quantity_label.text = "In deck: 0/3"
quantity_label.add_theme_font_size_override("font_size", 14)
qty_hbox.add_child(quantity_label)
decrease_btn = _create_quantity_button("-")
decrease_btn.pressed.connect(_on_decrease_quantity)
qty_hbox.add_child(decrease_btn)
var qty_display = Label.new()
qty_display.text = "1"
qty_display.name = "QtyDisplay"
qty_display.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
qty_display.custom_minimum_size = Vector2(30, 0)
qty_hbox.add_child(qty_display)
increase_btn = _create_quantity_button("+")
increase_btn.pressed.connect(_on_increase_quantity)
qty_hbox.add_child(increase_btn)
# Add to deck button
add_button = _create_styled_button("Add to Deck")
add_button.pressed.connect(_on_add_to_deck)
main_vbox.add_child(add_button)
# Initial state
_update_ui_state()
func _create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.08, 0.12, 0.95)
style.border_color = Color(0.5, 0.4, 0.2)
style.set_border_width_all(2)
style.set_corner_radius_all(6)
style.content_margin_left = 15
style.content_margin_right = 15
style.content_margin_top = 15
style.content_margin_bottom = 15
return style
func _create_info_label(text: String, font_size: int = 14, color: Color = Color(0.9, 0.9, 0.9)) -> Label:
var label = Label.new()
label.text = text
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
return label
func _create_quantity_button(text: String) -> Button:
var button = Button.new()
button.text = text
button.custom_minimum_size = Vector2(36, 36)
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color(0.2, 0.2, 0.25)
style_normal.border_color = Color(0.4, 0.4, 0.5)
style_normal.set_border_width_all(1)
style_normal.set_corner_radius_all(4)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = Color(0.3, 0.3, 0.4)
style_hover.border_color = Color(0.6, 0.5, 0.3)
style_hover.set_border_width_all(1)
style_hover.set_corner_radius_all(4)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_font_size_override("font_size", 18)
return button
func _create_styled_button(text: String) -> Button:
var button = Button.new()
button.text = text
button.custom_minimum_size = Vector2(0, 44)
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color(0.25, 0.25, 0.3)
style_normal.border_color = Color(0.5, 0.5, 0.6)
style_normal.set_border_width_all(1)
style_normal.set_corner_radius_all(5)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = Color(0.35, 0.35, 0.45)
style_hover.border_color = Color(0.7, 0.6, 0.3)
style_hover.set_border_width_all(2)
style_hover.set_corner_radius_all(5)
button.add_theme_stylebox_override("hover", style_hover)
var style_disabled = StyleBoxFlat.new()
style_disabled.bg_color = Color(0.15, 0.15, 0.18)
style_disabled.border_color = Color(0.3, 0.3, 0.35)
style_disabled.set_border_width_all(1)
style_disabled.set_corner_radius_all(5)
button.add_theme_stylebox_override("disabled", style_disabled)
button.add_theme_font_size_override("font_size", 16)
return button
## Show a card in the detail viewer
func show_card(card: CardDatabase.CardData, deck_count: int = 0) -> void:
current_card = card
current_deck_count = deck_count
quantity_to_add = 1
_update_ui_state()
## Update deck count for current card
func update_deck_count(count: int) -> void:
current_deck_count = count
_update_ui_state()
## Clear the detail viewer
func clear() -> void:
current_card = null
current_deck_count = 0
_update_ui_state()
func _update_ui_state() -> void:
var has_card = current_card != null
no_card_label.visible = not has_card
card_image.visible = has_card
add_button.disabled = not has_card
if not has_card:
name_label.text = ""
type_label.text = ""
cost_label.text = ""
element_label.text = ""
power_label.text = ""
job_label.text = ""
category_label.text = ""
abilities_label.text = ""
quantity_label.text = "In deck: 0/3"
fallback_rect.visible = false
fallback_label.visible = false
return
# Load card image
var texture = CardDatabase.get_card_texture(current_card)
if texture:
card_image.texture = texture
card_image.visible = true
fallback_rect.visible = false
fallback_label.visible = false
else:
card_image.visible = false
fallback_rect.visible = true
fallback_label.visible = true
fallback_rect.color = Enums.element_to_color(current_card.get_primary_element())
fallback_label.text = current_card.name
# Update info labels
name_label.text = current_card.name
type_label.text = Enums.card_type_to_string(current_card.type)
cost_label.text = str(current_card.cost)
var elements_str = ""
for i in range(current_card.elements.size()):
if i > 0:
elements_str += " / "
elements_str += Enums.element_to_string(current_card.elements[i])
element_label.text = elements_str
power_label.text = str(current_card.power) if current_card.power > 0 else "-"
job_label.text = current_card.job if not current_card.job.is_empty() else "-"
category_label.text = current_card.category if not current_card.category.is_empty() else "-"
# Update abilities
var abilities_text = ""
for ability in current_card.abilities:
if not abilities_text.is_empty():
abilities_text += "\n\n"
var ability_type = Enums.ability_type_to_string(ability.type)
abilities_text += "[%s]" % ability_type
if not ability.trigger.is_empty():
abilities_text += " %s:" % ability.trigger
abilities_text += " %s" % ability.effect
abilities_label.text = abilities_text if not abilities_text.is_empty() else "No abilities"
# Update quantity display
var max_addable = Deck.MAX_COPIES - current_deck_count
quantity_label.text = "In deck: %d/%d" % [current_deck_count, Deck.MAX_COPIES]
var qty_display = get_node_or_null("PanelContainer/VBoxContainer/HBoxContainer/QtyDisplay")
if qty_display:
qty_display.text = str(quantity_to_add)
decrease_btn.disabled = quantity_to_add <= 1
increase_btn.disabled = quantity_to_add >= max_addable or max_addable <= 0
add_button.disabled = max_addable <= 0
func _on_decrease_quantity() -> void:
if quantity_to_add > 1:
quantity_to_add -= 1
_update_ui_state()
func _on_increase_quantity() -> void:
var max_addable = Deck.MAX_COPIES - current_deck_count
if quantity_to_add < max_addable:
quantity_to_add += 1
_update_ui_state()
func _on_add_to_deck() -> void:
if current_card:
add_to_deck_requested.emit(current_card, quantity_to_add)

432
scripts/ui/CardFilterBar.gd Normal file
View File

@@ -0,0 +1,432 @@
class_name CardFilterBar
extends Control
## CardFilterBar - Filter controls for the deck builder card grid
signal filters_changed(filters: Dictionary)
const FILTER_BAR_HEIGHT: float = 120.0
const EXPANDED_HEIGHT: float = 200.0
var current_filters: Dictionary = {}
var is_expanded: bool = false
# UI elements
var search_field: LineEdit
var element_buttons: Dictionary = {} # Enums.Element -> Button
var type_dropdown: OptionButton
var cost_slider: HSlider
var cost_label: Label
var expand_button: Button
var expanded_container: Control
# Expanded filter elements
var job_dropdown: OptionButton
var category_dropdown: OptionButton
var power_min_spin: SpinBox
var power_max_spin: SpinBox
var ex_burst_check: CheckBox
var set_dropdown: OptionButton
func _ready() -> void:
custom_minimum_size = Vector2(0, FILTER_BAR_HEIGHT)
_create_ui()
_populate_dropdowns()
func _create_ui() -> void:
var panel = PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_theme_stylebox_override("panel", _create_panel_style())
add_child(panel)
var main_vbox = VBoxContainer.new()
main_vbox.add_theme_constant_override("separation", 8)
panel.add_child(main_vbox)
# Row 1: Search and type
var row1 = HBoxContainer.new()
row1.add_theme_constant_override("separation", 12)
main_vbox.add_child(row1)
# Search field
var search_label = Label.new()
search_label.text = "Search:"
search_label.add_theme_font_size_override("font_size", 12)
row1.add_child(search_label)
search_field = LineEdit.new()
search_field.placeholder_text = "Card name..."
search_field.custom_minimum_size = Vector2(180, 0)
search_field.text_changed.connect(_on_search_changed)
row1.add_child(search_field)
# Type dropdown
var type_label = Label.new()
type_label.text = "Type:"
type_label.add_theme_font_size_override("font_size", 12)
row1.add_child(type_label)
type_dropdown = OptionButton.new()
type_dropdown.custom_minimum_size = Vector2(100, 0)
type_dropdown.add_item("All", -1)
type_dropdown.add_item("Forward", Enums.CardType.FORWARD)
type_dropdown.add_item("Backup", Enums.CardType.BACKUP)
type_dropdown.add_item("Summon", Enums.CardType.SUMMON)
type_dropdown.add_item("Monster", Enums.CardType.MONSTER)
type_dropdown.item_selected.connect(_on_type_selected)
row1.add_child(type_dropdown)
# Cost slider
var cost_container = HBoxContainer.new()
cost_container.add_theme_constant_override("separation", 8)
row1.add_child(cost_container)
var cost_title = Label.new()
cost_title.text = "Max Cost:"
cost_title.add_theme_font_size_override("font_size", 12)
cost_container.add_child(cost_title)
cost_slider = HSlider.new()
cost_slider.min_value = 1
cost_slider.max_value = 14
cost_slider.value = 14
cost_slider.step = 1
cost_slider.custom_minimum_size = Vector2(100, 0)
cost_slider.value_changed.connect(_on_cost_changed)
cost_container.add_child(cost_slider)
cost_label = Label.new()
cost_label.text = "14"
cost_label.custom_minimum_size = Vector2(24, 0)
cost_label.add_theme_font_size_override("font_size", 12)
cost_container.add_child(cost_label)
# Expand/collapse button
expand_button = Button.new()
expand_button.text = "More Filters"
expand_button.custom_minimum_size = Vector2(100, 0)
expand_button.pressed.connect(_toggle_expanded)
_apply_button_style(expand_button)
row1.add_child(expand_button)
# Clear filters button
var clear_btn = Button.new()
clear_btn.text = "Clear"
clear_btn.custom_minimum_size = Vector2(60, 0)
clear_btn.pressed.connect(_clear_filters)
_apply_button_style(clear_btn)
row1.add_child(clear_btn)
# Row 2: Element buttons
var row2 = HBoxContainer.new()
row2.add_theme_constant_override("separation", 6)
main_vbox.add_child(row2)
var elem_label = Label.new()
elem_label.text = "Elements:"
elem_label.add_theme_font_size_override("font_size", 12)
row2.add_child(elem_label)
for element in Enums.Element.values():
var btn = Button.new()
btn.text = Enums.element_to_string(element).substr(0, 3).to_upper()
btn.custom_minimum_size = Vector2(44, 28)
btn.toggle_mode = true
btn.button_pressed = false
btn.pressed.connect(_on_element_toggled.bind(element))
_apply_element_button_style(btn, element)
row2.add_child(btn)
element_buttons[element] = btn
# Expanded filters container (hidden by default)
expanded_container = VBoxContainer.new()
expanded_container.visible = false
expanded_container.add_theme_constant_override("separation", 8)
main_vbox.add_child(expanded_container)
var expanded_row = HBoxContainer.new()
expanded_row.add_theme_constant_override("separation", 16)
expanded_container.add_child(expanded_row)
# Job dropdown
var job_container = HBoxContainer.new()
job_container.add_theme_constant_override("separation", 4)
expanded_row.add_child(job_container)
var job_label_el = Label.new()
job_label_el.text = "Job:"
job_label_el.add_theme_font_size_override("font_size", 12)
job_container.add_child(job_label_el)
job_dropdown = OptionButton.new()
job_dropdown.custom_minimum_size = Vector2(120, 0)
job_dropdown.item_selected.connect(_on_job_selected)
job_container.add_child(job_dropdown)
# Category dropdown
var cat_container = HBoxContainer.new()
cat_container.add_theme_constant_override("separation", 4)
expanded_row.add_child(cat_container)
var cat_label = Label.new()
cat_label.text = "Category:"
cat_label.add_theme_font_size_override("font_size", 12)
cat_container.add_child(cat_label)
category_dropdown = OptionButton.new()
category_dropdown.custom_minimum_size = Vector2(80, 0)
category_dropdown.item_selected.connect(_on_category_selected)
cat_container.add_child(category_dropdown)
# Power range
var power_container = HBoxContainer.new()
power_container.add_theme_constant_override("separation", 4)
expanded_row.add_child(power_container)
var power_label_el = Label.new()
power_label_el.text = "Power:"
power_label_el.add_theme_font_size_override("font_size", 12)
power_container.add_child(power_label_el)
power_min_spin = SpinBox.new()
power_min_spin.min_value = 0
power_min_spin.max_value = 20000
power_min_spin.step = 1000
power_min_spin.value = 0
power_min_spin.custom_minimum_size = Vector2(70, 0)
power_min_spin.value_changed.connect(_on_power_min_changed)
power_container.add_child(power_min_spin)
var dash = Label.new()
dash.text = "-"
power_container.add_child(dash)
power_max_spin = SpinBox.new()
power_max_spin.min_value = 0
power_max_spin.max_value = 20000
power_max_spin.step = 1000
power_max_spin.value = 20000
power_max_spin.custom_minimum_size = Vector2(70, 0)
power_max_spin.value_changed.connect(_on_power_max_changed)
power_container.add_child(power_max_spin)
# EX Burst checkbox
ex_burst_check = CheckBox.new()
ex_burst_check.text = "EX Burst only"
ex_burst_check.add_theme_font_size_override("font_size", 12)
ex_burst_check.toggled.connect(_on_ex_burst_toggled)
expanded_row.add_child(ex_burst_check)
# Set/Opus dropdown
var set_container = HBoxContainer.new()
set_container.add_theme_constant_override("separation", 4)
expanded_row.add_child(set_container)
var set_label = Label.new()
set_label.text = "Set:"
set_label.add_theme_font_size_override("font_size", 12)
set_container.add_child(set_label)
set_dropdown = OptionButton.new()
set_dropdown.custom_minimum_size = Vector2(70, 0)
set_dropdown.item_selected.connect(_on_set_selected)
set_container.add_child(set_dropdown)
func _populate_dropdowns() -> void:
# Populate job dropdown
job_dropdown.add_item("All", -1)
for job in CardDatabase.get_unique_jobs():
job_dropdown.add_item(job)
# Populate category dropdown
category_dropdown.add_item("All", -1)
for category in CardDatabase.get_unique_categories():
category_dropdown.add_item(category)
# Populate set dropdown
set_dropdown.add_item("All", -1)
for set_id in CardDatabase.get_card_sets():
set_dropdown.add_item("Opus " + set_id, set_dropdown.item_count)
func _create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.1, 0.1, 0.14, 0.95)
style.border_color = Color(0.4, 0.35, 0.25)
style.set_border_width_all(1)
style.set_corner_radius_all(4)
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 8
style.content_margin_bottom = 8
return style
func _apply_button_style(button: Button) -> void:
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color(0.2, 0.2, 0.25)
style_normal.border_color = Color(0.4, 0.4, 0.5)
style_normal.set_border_width_all(1)
style_normal.set_corner_radius_all(3)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = Color(0.28, 0.28, 0.35)
style_hover.border_color = Color(0.5, 0.45, 0.3)
style_hover.set_border_width_all(1)
style_hover.set_corner_radius_all(3)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_font_size_override("font_size", 12)
func _apply_element_button_style(button: Button, element: Enums.Element) -> void:
var element_color = Enums.element_to_color(element)
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = element_color.darkened(0.6)
style_normal.border_color = element_color.darkened(0.3)
style_normal.set_border_width_all(1)
style_normal.set_corner_radius_all(3)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = element_color.darkened(0.4)
style_hover.border_color = element_color
style_hover.set_border_width_all(2)
style_hover.set_corner_radius_all(3)
button.add_theme_stylebox_override("hover", style_hover)
var style_pressed = StyleBoxFlat.new()
style_pressed.bg_color = element_color.darkened(0.2)
style_pressed.border_color = Color.WHITE
style_pressed.set_border_width_all(2)
style_pressed.set_corner_radius_all(3)
button.add_theme_stylebox_override("pressed", style_pressed)
button.add_theme_font_size_override("font_size", 10)
func _toggle_expanded() -> void:
is_expanded = not is_expanded
expanded_container.visible = is_expanded
expand_button.text = "Less Filters" if is_expanded else "More Filters"
custom_minimum_size.y = EXPANDED_HEIGHT if is_expanded else FILTER_BAR_HEIGHT
func _clear_filters() -> void:
search_field.text = ""
type_dropdown.select(0)
cost_slider.value = 14
for btn in element_buttons.values():
btn.button_pressed = false
job_dropdown.select(0)
category_dropdown.select(0)
power_min_spin.value = 0
power_max_spin.value = 20000
ex_burst_check.button_pressed = false
set_dropdown.select(0)
current_filters.clear()
filters_changed.emit(current_filters)
func _emit_filters() -> void:
filters_changed.emit(current_filters)
func _on_search_changed(text: String) -> void:
if text.is_empty():
current_filters.erase("name")
else:
current_filters["name"] = text
_emit_filters()
func _on_type_selected(index: int) -> void:
var type_id = type_dropdown.get_item_id(index)
if type_id == -1:
current_filters.erase("type")
else:
current_filters["type"] = type_id
_emit_filters()
func _on_cost_changed(value: float) -> void:
cost_label.text = str(int(value))
if value >= 14:
current_filters.erase("cost_max")
else:
current_filters["cost_max"] = int(value)
_emit_filters()
func _on_element_toggled(element: Enums.Element) -> void:
var selected_elements: Array[Enums.Element] = []
for elem in element_buttons:
if element_buttons[elem].button_pressed:
selected_elements.append(elem)
if selected_elements.is_empty():
current_filters.erase("elements")
else:
current_filters["elements"] = selected_elements
_emit_filters()
func _on_job_selected(index: int) -> void:
if index == 0:
current_filters.erase("job")
else:
current_filters["job"] = job_dropdown.get_item_text(index)
_emit_filters()
func _on_category_selected(index: int) -> void:
if index == 0:
current_filters.erase("category")
else:
current_filters["category"] = category_dropdown.get_item_text(index)
_emit_filters()
func _on_power_min_changed(value: float) -> void:
if value <= 0:
current_filters.erase("power_min")
else:
current_filters["power_min"] = int(value)
_emit_filters()
func _on_power_max_changed(value: float) -> void:
if value >= 20000:
current_filters.erase("power_max")
else:
current_filters["power_max"] = int(value)
_emit_filters()
func _on_ex_burst_toggled(pressed: bool) -> void:
if pressed:
current_filters["ex_burst_only"] = true
else:
current_filters.erase("ex_burst_only")
_emit_filters()
func _on_set_selected(index: int) -> void:
if index == 0:
current_filters.erase("set")
else:
# Extract set number from "Opus X" text
var text = set_dropdown.get_item_text(index)
var set_num = text.replace("Opus ", "")
current_filters["set"] = set_num + "-"
_emit_filters()
## Get current filters
func get_filters() -> Dictionary:
return current_filters.duplicate()

241
scripts/ui/CardGrid.gd Normal file
View File

@@ -0,0 +1,241 @@
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()

470
scripts/ui/DeckBuilder.gd Normal file
View File

@@ -0,0 +1,470 @@
class_name DeckBuilder
extends CanvasLayer
## DeckBuilder - Main deck builder screen with three-panel layout
signal back_pressed
signal deck_selected(deck: Deck)
const WINDOW_SIZE = Vector2i(1600, 900)
var current_deck: Deck = null
var current_deck_filename: String = ""
# UI Components
var detail_viewer: CardDetailViewer
var filter_bar: CardFilterBar
var card_grid: CardGrid
var deck_panel: DeckListPanel
# Header elements
var back_button: Button
var deck_name_field: LineEdit
var card_count_label: Label
var save_button: Button
var load_button: Button
var new_button: Button
var play_button: Button
# Dialogs
var save_dialog: Control
var load_dialog: Control
func _ready() -> void:
layer = 10
_create_ui()
_connect_signals()
_new_deck()
_load_all_cards()
func _create_ui() -> void:
# Root control
var root = Control.new()
root.set_anchors_preset(Control.PRESET_FULL_RECT)
root.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(root)
# Background
var bg = ColorRect.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.color = Color(0.05, 0.05, 0.08, 1.0)
root.add_child(bg)
# Main layout
var main_vbox = VBoxContainer.new()
main_vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
main_vbox.add_theme_constant_override("separation", 0)
root.add_child(main_vbox)
# Header bar
_create_header(main_vbox)
# Content area (3 panels)
var content_hbox = HBoxContainer.new()
content_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
content_hbox.add_theme_constant_override("separation", 0)
main_vbox.add_child(content_hbox)
# Left panel - Card Detail Viewer
detail_viewer = CardDetailViewer.new()
content_hbox.add_child(detail_viewer)
# Center panel container
var center_panel = VBoxContainer.new()
center_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
center_panel.add_theme_constant_override("separation", 0)
content_hbox.add_child(center_panel)
# Filter bar
filter_bar = CardFilterBar.new()
center_panel.add_child(filter_bar)
# Results count
var results_bar = HBoxContainer.new()
results_bar.custom_minimum_size = Vector2(0, 30)
var results_style = StyleBoxFlat.new()
results_style.bg_color = Color(0.08, 0.08, 0.1)
results_style.content_margin_left = 12
results_style.content_margin_top = 4
var results_panel = PanelContainer.new()
results_panel.add_theme_stylebox_override("panel", results_style)
results_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
center_panel.add_child(results_panel)
card_count_label = Label.new()
card_count_label.text = "Loading cards..."
card_count_label.add_theme_font_size_override("font_size", 12)
card_count_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
results_panel.add_child(card_count_label)
# Card grid
card_grid = CardGrid.new()
card_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL
center_panel.add_child(card_grid)
# Right panel - Deck List
deck_panel = DeckListPanel.new()
content_hbox.add_child(deck_panel)
# Create dialogs (hidden)
_create_save_dialog(root)
_create_load_dialog(root)
func _create_header(parent: Control) -> void:
var header = PanelContainer.new()
header.custom_minimum_size = Vector2(0, 50)
var header_style = StyleBoxFlat.new()
header_style.bg_color = Color(0.1, 0.1, 0.14)
header_style.border_color = Color(0.3, 0.25, 0.15)
header_style.border_width_bottom = 2
header_style.content_margin_left = 15
header_style.content_margin_right = 15
header.add_theme_stylebox_override("panel", header_style)
parent.add_child(header)
var header_hbox = HBoxContainer.new()
header_hbox.add_theme_constant_override("separation", 15)
header_hbox.alignment = BoxContainer.ALIGNMENT_BEGIN
header.add_child(header_hbox)
# Back button
back_button = _create_header_button("< Back")
header_hbox.add_child(back_button)
# Deck name
var name_label = Label.new()
name_label.text = "Deck:"
name_label.add_theme_font_size_override("font_size", 14)
header_hbox.add_child(name_label)
deck_name_field = LineEdit.new()
deck_name_field.text = "New Deck"
deck_name_field.custom_minimum_size = Vector2(200, 0)
deck_name_field.text_changed.connect(_on_deck_name_changed)
header_hbox.add_child(deck_name_field)
# Spacer
var spacer = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_hbox.add_child(spacer)
# Action buttons
new_button = _create_header_button("New")
save_button = _create_header_button("Save")
load_button = _create_header_button("Load")
play_button = _create_header_button("Play with Deck")
play_button.add_theme_color_override("font_color", Color(0.4, 0.8, 0.4))
header_hbox.add_child(new_button)
header_hbox.add_child(save_button)
header_hbox.add_child(load_button)
header_hbox.add_child(play_button)
func _create_header_button(text: String) -> Button:
var button = Button.new()
button.text = text
button.custom_minimum_size = Vector2(80, 32)
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color(0.2, 0.2, 0.25)
style_normal.border_color = Color(0.4, 0.4, 0.5)
style_normal.set_border_width_all(1)
style_normal.set_corner_radius_all(4)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = Color(0.3, 0.3, 0.38)
style_hover.border_color = Color(0.6, 0.5, 0.3)
style_hover.set_border_width_all(1)
style_hover.set_corner_radius_all(4)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_font_size_override("font_size", 13)
return button
func _create_save_dialog(parent: Control) -> void:
save_dialog = _create_dialog_base("Save Deck")
parent.add_child(save_dialog)
var content = save_dialog.get_node("Panel/VBox")
var name_hbox = HBoxContainer.new()
name_hbox.add_theme_constant_override("separation", 8)
content.add_child(name_hbox)
var label = Label.new()
label.text = "Filename:"
name_hbox.add_child(label)
var save_name_field = LineEdit.new()
save_name_field.name = "SaveNameField"
save_name_field.custom_minimum_size = Vector2(200, 0)
name_hbox.add_child(save_name_field)
var btn_hbox = HBoxContainer.new()
btn_hbox.add_theme_constant_override("separation", 10)
btn_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_child(btn_hbox)
var save_btn = _create_header_button("Save")
save_btn.pressed.connect(_on_save_confirmed)
btn_hbox.add_child(save_btn)
var cancel_btn = _create_header_button("Cancel")
cancel_btn.pressed.connect(func(): save_dialog.visible = false)
btn_hbox.add_child(cancel_btn)
func _create_load_dialog(parent: Control) -> void:
load_dialog = _create_dialog_base("Load Deck")
parent.add_child(load_dialog)
var content = load_dialog.get_node("Panel/VBox")
var deck_list = ItemList.new()
deck_list.name = "DeckList"
deck_list.custom_minimum_size = Vector2(300, 200)
deck_list.item_activated.connect(_on_deck_item_activated)
content.add_child(deck_list)
var btn_hbox = HBoxContainer.new()
btn_hbox.add_theme_constant_override("separation", 10)
btn_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_child(btn_hbox)
var load_btn = _create_header_button("Load")
load_btn.pressed.connect(_on_load_confirmed)
btn_hbox.add_child(load_btn)
var delete_btn = _create_header_button("Delete")
delete_btn.add_theme_color_override("font_color", Color(1.0, 0.5, 0.5))
delete_btn.pressed.connect(_on_delete_deck)
btn_hbox.add_child(delete_btn)
var cancel_btn = _create_header_button("Cancel")
cancel_btn.pressed.connect(func(): load_dialog.visible = false)
btn_hbox.add_child(cancel_btn)
func _create_dialog_base(title: String) -> Control:
var overlay = Control.new()
overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
overlay.visible = false
var bg = ColorRect.new()
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
bg.color = Color(0, 0, 0, 0.6)
bg.gui_input.connect(func(event):
if event is InputEventMouseButton and event.pressed:
overlay.visible = false
)
overlay.add_child(bg)
var panel = PanelContainer.new()
panel.name = "Panel"
panel.set_anchors_preset(Control.PRESET_CENTER)
panel.custom_minimum_size = Vector2(350, 250)
var style = StyleBoxFlat.new()
style.bg_color = Color(0.1, 0.1, 0.14, 0.98)
style.border_color = Color(0.5, 0.4, 0.2)
style.set_border_width_all(2)
style.set_corner_radius_all(8)
style.content_margin_left = 20
style.content_margin_right = 20
style.content_margin_top = 15
style.content_margin_bottom = 15
panel.add_theme_stylebox_override("panel", style)
overlay.add_child(panel)
var vbox = VBoxContainer.new()
vbox.name = "VBox"
vbox.add_theme_constant_override("separation", 15)
panel.add_child(vbox)
var title_label = Label.new()
title_label.text = title
title_label.add_theme_font_size_override("font_size", 18)
title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title_label)
return overlay
func _connect_signals() -> void:
back_button.pressed.connect(_on_back_pressed)
new_button.pressed.connect(_new_deck)
save_button.pressed.connect(_show_save_dialog)
load_button.pressed.connect(_show_load_dialog)
play_button.pressed.connect(_on_play_pressed)
filter_bar.filters_changed.connect(_on_filters_changed)
card_grid.card_selected.connect(_on_card_selected)
card_grid.card_double_clicked.connect(_on_card_double_clicked)
detail_viewer.add_to_deck_requested.connect(_on_add_to_deck)
deck_panel.card_clicked.connect(_on_deck_card_clicked)
deck_panel.card_removed.connect(_on_deck_card_removed)
deck_panel.deck_cleared.connect(_on_deck_cleared)
func _load_all_cards() -> void:
var all_cards = CardDatabase.get_all_cards()
card_grid.set_cards(all_cards)
card_count_label.text = "Showing %d of %d cards" % [all_cards.size(), all_cards.size()]
func _new_deck() -> void:
current_deck = Deck.new()
current_deck.name = DeckManager.generate_unique_name()
current_deck_filename = ""
deck_name_field.text = current_deck.name
deck_panel.set_deck(current_deck)
detail_viewer.clear()
func _on_back_pressed() -> void:
back_pressed.emit()
func _on_play_pressed() -> void:
if current_deck and current_deck.is_valid():
deck_selected.emit(current_deck)
else:
# Show validation errors
var errors = current_deck.validate() if current_deck else ["No deck loaded"]
push_warning("Cannot play - deck invalid: " + ", ".join(errors))
func _on_deck_name_changed(new_name: String) -> void:
if current_deck:
current_deck.name = new_name
func _on_filters_changed(filters: Dictionary) -> void:
var results = CardDatabase.filter_cards(filters)
card_grid.set_cards(results)
card_count_label.text = "Showing %d of %d cards" % [results.size(), CardDatabase.get_card_count()]
func _on_card_selected(card: CardDatabase.CardData) -> void:
var deck_count = current_deck.get_card_count(card.id) if current_deck else 0
detail_viewer.show_card(card, deck_count)
func _on_card_double_clicked(card: CardDatabase.CardData) -> void:
if current_deck:
var error = current_deck.add_card(card.id)
if error.is_empty():
detail_viewer.update_deck_count(current_deck.get_card_count(card.id))
func _on_add_to_deck(card: CardDatabase.CardData, quantity: int) -> void:
if not current_deck:
return
for i in range(quantity):
var error = current_deck.add_card(card.id)
if not error.is_empty():
break
detail_viewer.update_deck_count(current_deck.get_card_count(card.id))
func _on_deck_card_clicked(card_id: String) -> void:
var card_data = CardDatabase.get_card(card_id)
if card_data:
var deck_count = current_deck.get_card_count(card_id) if current_deck else 0
detail_viewer.show_card(card_data, deck_count)
func _on_deck_card_removed(card_id: String) -> void:
if current_deck:
current_deck.remove_card(card_id)
# Update detail viewer if showing this card
var card_data = CardDatabase.get_card(card_id)
if card_data:
detail_viewer.update_deck_count(current_deck.get_card_count(card_id))
func _on_deck_cleared() -> void:
if current_deck:
current_deck.clear()
detail_viewer.clear()
func _show_save_dialog() -> void:
var save_name_field = save_dialog.get_node("Panel/VBox/HBoxContainer/SaveNameField") as LineEdit
save_name_field.text = current_deck.name if current_deck else "New Deck"
save_dialog.visible = true
func _on_save_confirmed() -> void:
var save_name_field = save_dialog.get_node("Panel/VBox/HBoxContainer/SaveNameField") as LineEdit
var filename = save_name_field.text.strip_edges()
if filename.is_empty():
return
if current_deck:
current_deck.name = filename
deck_name_field.text = filename
if DeckManager.save_deck(current_deck, filename):
current_deck_filename = filename
print("Deck saved: ", filename)
else:
push_error("Failed to save deck")
save_dialog.visible = false
func _show_load_dialog() -> void:
var deck_list = load_dialog.get_node("Panel/VBox/DeckList") as ItemList
deck_list.clear()
for deck_name in DeckManager.list_decks():
deck_list.add_item(deck_name)
load_dialog.visible = true
func _on_deck_item_activated(index: int) -> void:
_on_load_confirmed()
func _on_load_confirmed() -> void:
var deck_list = load_dialog.get_node("Panel/VBox/DeckList") as ItemList
var selected = deck_list.get_selected_items()
if selected.is_empty():
return
var filename = deck_list.get_item_text(selected[0])
var loaded_deck = DeckManager.load_deck(filename)
if loaded_deck:
current_deck = loaded_deck
current_deck_filename = filename
deck_name_field.text = current_deck.name
deck_panel.set_deck(current_deck)
detail_viewer.clear()
print("Deck loaded: ", filename)
else:
push_error("Failed to load deck")
load_dialog.visible = false
func _on_delete_deck() -> void:
var deck_list = load_dialog.get_node("Panel/VBox/DeckList") as ItemList
var selected = deck_list.get_selected_items()
if selected.is_empty():
return
var filename = deck_list.get_item_text(selected[0])
if DeckManager.delete_deck(filename):
deck_list.remove_item(selected[0])
print("Deck deleted: ", filename)

352
scripts/ui/DeckListPanel.gd Normal file
View File

@@ -0,0 +1,352 @@
class_name DeckListPanel
extends Control
## DeckListPanel - Right panel showing deck contents with thumbnails and stats
signal card_clicked(card_id: String)
signal card_removed(card_id: String)
signal deck_cleared
const PANEL_WIDTH: float = 400.0
const CARD_WIDTH: float = 90.0
const CARD_HEIGHT: float = 126.0
const CARD_GAP: float = 4.0
const COLUMNS: int = 4
var current_deck: Deck = null
var card_cells: Dictionary = {} # card_id -> Control
# UI elements
var stats_panel: Control
var element_labels: Dictionary = {}
var type_labels: Dictionary = {}
var total_label: Label
var deck_scroll: ScrollContainer
var deck_grid: Control
var validation_label: Label
var clear_button: Button
func _ready() -> void:
custom_minimum_size = Vector2(PANEL_WIDTH, 0)
_create_ui()
func _create_ui() -> void:
var panel = PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
panel.add_theme_stylebox_override("panel", _create_panel_style())
add_child(panel)
var main_vbox = VBoxContainer.new()
main_vbox.add_theme_constant_override("separation", 10)
panel.add_child(main_vbox)
# Header
var header = Label.new()
header.text = "Deck"
header.add_theme_font_size_override("font_size", 18)
header.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
main_vbox.add_child(header)
# Stats panel
_create_stats_panel(main_vbox)
# Deck grid
deck_scroll = ScrollContainer.new()
deck_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
deck_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
main_vbox.add_child(deck_scroll)
deck_grid = Control.new()
deck_grid.custom_minimum_size = Vector2(COLUMNS * (CARD_WIDTH + CARD_GAP) - CARD_GAP, 0)
deck_scroll.add_child(deck_grid)
# Validation label
validation_label = Label.new()
validation_label.text = ""
validation_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
validation_label.add_theme_font_size_override("font_size", 12)
validation_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
main_vbox.add_child(validation_label)
# Clear deck button
clear_button = _create_styled_button("Clear Deck")
clear_button.pressed.connect(_on_clear_pressed)
main_vbox.add_child(clear_button)
func _create_stats_panel(parent: Control) -> void:
stats_panel = PanelContainer.new()
var stats_style = StyleBoxFlat.new()
stats_style.bg_color = Color(0.12, 0.12, 0.16, 0.8)
stats_style.set_corner_radius_all(4)
stats_style.content_margin_left = 8
stats_style.content_margin_right = 8
stats_style.content_margin_top = 6
stats_style.content_margin_bottom = 6
stats_panel.add_theme_stylebox_override("panel", stats_style)
parent.add_child(stats_panel)
var stats_vbox = VBoxContainer.new()
stats_vbox.add_theme_constant_override("separation", 4)
stats_panel.add_child(stats_vbox)
# Total cards
total_label = Label.new()
total_label.text = "Cards: 0/50"
total_label.add_theme_font_size_override("font_size", 14)
total_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
stats_vbox.add_child(total_label)
# Element breakdown
var elem_hbox = HBoxContainer.new()
elem_hbox.add_theme_constant_override("separation", 8)
stats_vbox.add_child(elem_hbox)
for element in Enums.Element.values():
var elem_container = HBoxContainer.new()
elem_container.add_theme_constant_override("separation", 2)
var elem_icon = ColorRect.new()
elem_icon.custom_minimum_size = Vector2(12, 12)
elem_icon.color = Enums.element_to_color(element)
elem_container.add_child(elem_icon)
var elem_label = Label.new()
elem_label.text = "0"
elem_label.add_theme_font_size_override("font_size", 10)
elem_container.add_child(elem_label)
element_labels[element] = elem_label
elem_hbox.add_child(elem_container)
# Type breakdown
var type_hbox = HBoxContainer.new()
type_hbox.add_theme_constant_override("separation", 12)
stats_vbox.add_child(type_hbox)
for card_type in [Enums.CardType.FORWARD, Enums.CardType.BACKUP, Enums.CardType.SUMMON]:
var type_label = Label.new()
type_label.text = "%s: 0" % Enums.card_type_to_string(card_type).substr(0, 3)
type_label.add_theme_font_size_override("font_size", 10)
type_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
type_hbox.add_child(type_label)
type_labels[card_type] = type_label
func _create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.08, 0.12, 0.95)
style.border_color = Color(0.5, 0.4, 0.2)
style.set_border_width_all(2)
style.set_corner_radius_all(6)
style.content_margin_left = 12
style.content_margin_right = 12
style.content_margin_top = 12
style.content_margin_bottom = 12
return style
func _create_styled_button(text: String) -> Button:
var button = Button.new()
button.text = text
button.custom_minimum_size = Vector2(0, 36)
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color(0.4, 0.2, 0.2)
style_normal.border_color = Color(0.6, 0.3, 0.3)
style_normal.set_border_width_all(1)
style_normal.set_corner_radius_all(4)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = Color(0.5, 0.25, 0.25)
style_hover.border_color = Color(0.8, 0.4, 0.4)
style_hover.set_border_width_all(1)
style_hover.set_corner_radius_all(4)
button.add_theme_stylebox_override("hover", style_hover)
button.add_theme_font_size_override("font_size", 14)
return button
## Set the deck to display
func set_deck(deck: Deck) -> void:
if current_deck:
current_deck.deck_changed.disconnect(_on_deck_changed)
current_deck = deck
if current_deck:
current_deck.deck_changed.connect(_on_deck_changed)
_refresh_display()
func _on_deck_changed() -> void:
_refresh_display()
func _refresh_display() -> void:
# Clear existing cells
for cell in card_cells.values():
cell.queue_free()
card_cells.clear()
if not current_deck:
total_label.text = "Cards: 0/50"
validation_label.text = ""
_update_stats({})
return
# Get stats
var stats = current_deck.get_stats()
_update_stats(stats)
# Update total
total_label.text = "Cards: %d/50" % stats.total
# Validate
var errors = current_deck.validate()
validation_label.text = "\n".join(errors)
# Create card cells
var card_ids = current_deck.get_card_ids()
card_ids.sort() # Sort alphabetically
var row = 0
var col = 0
for card_id in card_ids:
var count = current_deck.get_card_count(card_id)
var cell = _create_deck_cell(card_id, count)
cell.position = Vector2(col * (CARD_WIDTH + CARD_GAP), row * (CARD_HEIGHT + CARD_GAP))
deck_grid.add_child(cell)
card_cells[card_id] = cell
col += 1
if col >= COLUMNS:
col = 0
row += 1
# Update grid size
var total_rows = ceili(float(card_ids.size()) / COLUMNS)
deck_grid.custom_minimum_size.y = total_rows * (CARD_HEIGHT + CARD_GAP)
func _update_stats(stats: Dictionary) -> void:
# Update element counts
for element in element_labels:
var elem_name = Enums.element_to_string(element)
var count = stats.get("elements", {}).get(elem_name, 0)
element_labels[element].text = str(count)
# Update type counts
for card_type in type_labels:
var type_name = Enums.card_type_to_string(card_type)
var count = stats.get("types", {}).get(type_name, 0)
type_labels[card_type].text = "%s: %d" % [type_name.substr(0, 3), count]
func _create_deck_cell(card_id: String, count: int) -> 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
cell.set_meta("card_id", card_id)
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(2)
cell.add_theme_stylebox_override("panel", style)
# Card image
var card_data = CardDatabase.get_card(card_id)
if card_data:
var texture = CardDatabase.get_card_texture(card_data)
if texture:
var tex_rect = TextureRect.new()
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
tex_rect.texture = texture
cell.add_child(tex_rect)
else:
var fallback = ColorRect.new()
fallback.set_anchors_preset(Control.PRESET_FULL_RECT)
fallback.color = Enums.element_to_color(card_data.get_primary_element()).darkened(0.3)
cell.add_child(fallback)
var name_label = Label.new()
name_label.text = card_data.name
name_label.set_anchors_preset(Control.PRESET_CENTER)
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
name_label.add_theme_font_size_override("font_size", 8)
cell.add_child(name_label)
# Quantity badge
if count > 1:
var badge = Panel.new()
badge.size = Vector2(22, 22)
badge.position = Vector2(CARD_WIDTH - 26, 4)
var badge_style = StyleBoxFlat.new()
badge_style.bg_color = Color(0.8, 0.6, 0.2, 0.95)
badge_style.set_corner_radius_all(11)
badge.add_theme_stylebox_override("panel", badge_style)
cell.add_child(badge)
var badge_label = Label.new()
badge_label.text = "x%d" % count
badge_label.set_anchors_preset(Control.PRESET_CENTER)
badge_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
badge_label.add_theme_font_size_override("font_size", 10)
badge.add_child(badge_label)
# Remove button (X in top-left)
var remove_btn = Button.new()
remove_btn.text = "X"
remove_btn.size = Vector2(20, 20)
remove_btn.position = Vector2(4, 4)
remove_btn.flat = true
remove_btn.add_theme_font_size_override("font_size", 10)
remove_btn.add_theme_color_override("font_color", Color(1.0, 0.5, 0.5))
remove_btn.add_theme_color_override("font_hover_color", Color(1.0, 0.3, 0.3))
remove_btn.pressed.connect(_on_remove_card.bind(card_id))
remove_btn.visible = false
remove_btn.name = "RemoveBtn"
cell.add_child(remove_btn)
# Hover effects
cell.mouse_entered.connect(func():
remove_btn.visible = true
style.border_color = Color(0.6, 0.5, 0.3)
cell.add_theme_stylebox_override("panel", style)
)
cell.mouse_exited.connect(func():
remove_btn.visible = false
style.border_color = Color(0.3, 0.3, 0.35)
cell.add_theme_stylebox_override("panel", style)
)
# Click to select
cell.gui_input.connect(func(event):
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
card_clicked.emit(card_id)
)
return cell
func _on_remove_card(card_id: String) -> void:
card_removed.emit(card_id)
func _on_clear_pressed() -> void:
deck_cleared.emit()
## Refresh display for a specific card (when count changes)
func refresh_card(card_id: String) -> void:
_refresh_display()

588
scripts/ui/GameSetupMenu.gd Normal file
View File

@@ -0,0 +1,588 @@
class_name GameSetupMenu
extends CanvasLayer
## GameSetupMenu - Setup screen for configuring game before starting
## Allows selection of game type and decks for each player
signal back_pressed
signal start_game_requested(p1_deck: Array, p2_deck: Array)
const WINDOW_SIZE := Vector2(800, 600)
# UI Components
var background: PanelContainer
var main_vbox: VBoxContainer
var title_label: Label
var game_type_container: HBoxContainer
var game_type_dropdown: OptionButton
var players_container: HBoxContainer
var player1_panel: Control
var player2_panel: Control
var p1_deck_dropdown: OptionButton
var p2_deck_dropdown: OptionButton
var p1_preview: Control
var p2_preview: Control
var buttons_container: HBoxContainer
var start_button: Button
var back_button: Button
# Deck data
var saved_decks: Array[String] = []
var starter_decks: Array = [] # Array of StarterDeckData
var p1_selected_deck: Array = [] # Card IDs
var p2_selected_deck: Array = [] # Card IDs
func _ready() -> void:
# Set high layer to be on top of everything
layer = 100
_load_deck_options()
_create_ui()
_select_random_decks()
func _load_deck_options() -> void:
# Load saved decks
saved_decks = DeckManager.list_decks()
# Load starter decks
starter_decks = CardDatabase.get_starter_decks()
func _create_ui() -> void:
# Background panel - use the window size constant
background = PanelContainer.new()
add_child(background)
background.position = Vector2.ZERO
background.size = WINDOW_SIZE
background.add_theme_stylebox_override("panel", _create_panel_style())
# Main vertical layout with padding
var margin = MarginContainer.new()
background.add_child(margin)
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 25)
margin.add_theme_constant_override("margin_right", 25)
margin.add_theme_constant_override("margin_top", 15)
margin.add_theme_constant_override("margin_bottom", 15)
main_vbox = VBoxContainer.new()
margin.add_child(main_vbox)
main_vbox.add_theme_constant_override("separation", 8)
# Title
_create_title()
# Game type selector
_create_game_type_selector()
# Player panels
_create_player_panels()
# Spacer
var spacer = Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
main_vbox.add_child(spacer)
# Buttons
_create_buttons()
func _create_title() -> void:
title_label = Label.new()
title_label.text = "GAME SETUP"
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 32)
title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
main_vbox.add_child(title_label)
# Separator
var separator = HSeparator.new()
separator.add_theme_stylebox_override("separator", _create_separator_style())
main_vbox.add_child(separator)
func _create_game_type_selector() -> void:
game_type_container = HBoxContainer.new()
game_type_container.add_theme_constant_override("separation", 15)
main_vbox.add_child(game_type_container)
var label = Label.new()
label.text = "Game Type:"
label.add_theme_font_size_override("font_size", 18)
label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
game_type_container.add_child(label)
game_type_dropdown = OptionButton.new()
game_type_dropdown.custom_minimum_size = Vector2(250, 36)
game_type_dropdown.add_item("2-Player Local (Share Screen)")
game_type_dropdown.add_item("vs AI (Coming Soon)")
game_type_dropdown.set_item_disabled(1, true)
game_type_dropdown.add_theme_font_size_override("font_size", 14)
_style_dropdown(game_type_dropdown)
game_type_container.add_child(game_type_dropdown)
func _create_player_panels() -> void:
players_container = HBoxContainer.new()
players_container.add_theme_constant_override("separation", 20)
players_container.alignment = BoxContainer.ALIGNMENT_CENTER
main_vbox.add_child(players_container)
player1_panel = _create_player_panel("PLAYER 1", 1)
players_container.add_child(player1_panel)
player2_panel = _create_player_panel("PLAYER 2", 2)
players_container.add_child(player2_panel)
func _create_player_panel(title: String, player_num: int) -> Control:
var panel = PanelContainer.new()
panel.custom_minimum_size = Vector2(320, 280)
panel.add_theme_stylebox_override("panel", _create_player_panel_style())
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 8)
panel.add_child(vbox)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 12)
margin.add_theme_constant_override("margin_right", 12)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
vbox.add_child(margin)
var inner_vbox = VBoxContainer.new()
inner_vbox.add_theme_constant_override("separation", 6)
margin.add_child(inner_vbox)
# Player title
var title_label = Label.new()
title_label.text = title
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 18)
title_label.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
inner_vbox.add_child(title_label)
# Deck dropdown
var dropdown = OptionButton.new()
dropdown.custom_minimum_size = Vector2(300, 32)
dropdown.add_theme_font_size_override("font_size", 12)
_style_dropdown(dropdown)
_populate_deck_dropdown(dropdown)
dropdown.item_selected.connect(_on_deck_selected.bind(player_num))
inner_vbox.add_child(dropdown)
if player_num == 1:
p1_deck_dropdown = dropdown
else:
p2_deck_dropdown = dropdown
# Deck preview panel (with box art)
var preview = _create_deck_preview(player_num)
inner_vbox.add_child(preview)
if player_num == 1:
p1_preview = preview
else:
p2_preview = preview
return panel
func _create_deck_preview(player_num: int) -> Control:
var panel = PanelContainer.new()
panel.custom_minimum_size = Vector2(280, 170)
var style = StyleBoxFlat.new()
style.bg_color = Color(0.06, 0.06, 0.1, 0.8)
style.border_color = Color(0.3, 0.3, 0.35)
style.set_border_width_all(1)
style.set_corner_radius_all(4)
style.content_margin_left = 8
style.content_margin_right = 8
style.content_margin_top = 6
style.content_margin_bottom = 6
panel.add_theme_stylebox_override("panel", style)
var vbox = VBoxContainer.new()
vbox.name = "VBoxContainer"
vbox.add_theme_constant_override("separation", 4)
panel.add_child(vbox)
# Box art container (centered)
var art_container = CenterContainer.new()
art_container.name = "ArtContainer"
art_container.custom_minimum_size = Vector2(260, 90)
vbox.add_child(art_container)
# Box art image
var box_art = TextureRect.new()
box_art.name = "BoxArt"
box_art.custom_minimum_size = Vector2(90, 85)
box_art.expand_mode = TextureRect.EXPAND_FIT_HEIGHT_PROPORTIONAL
box_art.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
art_container.add_child(box_art)
# Placeholder when no art available
var placeholder = ColorRect.new()
placeholder.name = "Placeholder"
placeholder.custom_minimum_size = Vector2(60, 85)
placeholder.color = Color(0.15, 0.15, 0.2, 0.5)
placeholder.visible = true
art_container.add_child(placeholder)
# Placeholder icon/text
var placeholder_label = Label.new()
placeholder_label.text = "?"
placeholder_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
placeholder_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
placeholder_label.add_theme_font_size_override("font_size", 48)
placeholder_label.add_theme_color_override("font_color", Color(0.3, 0.3, 0.35))
placeholder.add_child(placeholder_label)
placeholder_label.set_anchors_preset(Control.PRESET_FULL_RECT)
# Deck name
var name_label = Label.new()
name_label.name = "DeckName"
name_label.text = "No deck selected"
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.add_theme_font_size_override("font_size", 14)
name_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
vbox.add_child(name_label)
# Elements row (centered)
var elements_center = CenterContainer.new()
elements_center.name = "ElementsCenter"
vbox.add_child(elements_center)
var elements_hbox = HBoxContainer.new()
elements_hbox.name = "ElementsRow"
elements_hbox.add_theme_constant_override("separation", 6)
elements_center.add_child(elements_hbox)
# Info row (card count + description)
var info_hbox = HBoxContainer.new()
info_hbox.name = "InfoRow"
info_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
info_hbox.add_theme_constant_override("separation", 10)
vbox.add_child(info_hbox)
# Card count
var count_label = Label.new()
count_label.name = "CardCount"
count_label.text = "0 cards"
count_label.add_theme_font_size_override("font_size", 11)
count_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6))
info_hbox.add_child(count_label)
# Separator
var sep = Label.new()
sep.text = ""
sep.add_theme_font_size_override("font_size", 11)
sep.add_theme_color_override("font_color", Color(0.4, 0.4, 0.4))
info_hbox.add_child(sep)
# Description
var desc_label = Label.new()
desc_label.name = "Description"
desc_label.text = ""
desc_label.add_theme_font_size_override("font_size", 11)
desc_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.55))
info_hbox.add_child(desc_label)
return panel
func _populate_deck_dropdown(dropdown: OptionButton) -> void:
dropdown.clear()
# Add "My Decks" section if there are saved decks
if not saved_decks.is_empty():
dropdown.add_separator("-- My Decks --")
for deck_name in saved_decks:
dropdown.add_item(deck_name)
dropdown.set_item_metadata(dropdown.get_item_count() - 1, {"type": "saved", "name": deck_name})
# Add "Starter Decks" section
dropdown.add_separator("-- Starter Decks --")
for starter_deck in starter_decks:
var display_name = "%s (%s)" % [starter_deck.name, starter_deck.opus]
dropdown.add_item(display_name)
dropdown.set_item_metadata(dropdown.get_item_count() - 1, {"type": "starter", "id": starter_deck.id})
func _select_random_decks() -> void:
# Select random starter decks for both players
if starter_decks.size() >= 2:
var indices = range(starter_decks.size())
indices.shuffle()
var p1_index = _find_dropdown_index_for_starter(p1_deck_dropdown, starter_decks[indices[0]].id)
var p2_index = _find_dropdown_index_for_starter(p2_deck_dropdown, starter_decks[indices[1]].id)
if p1_index >= 0:
p1_deck_dropdown.select(p1_index)
_on_deck_selected(p1_index, 1)
if p2_index >= 0:
p2_deck_dropdown.select(p2_index)
_on_deck_selected(p2_index, 2)
elif starter_decks.size() == 1:
var index = _find_dropdown_index_for_starter(p1_deck_dropdown, starter_decks[0].id)
if index >= 0:
p1_deck_dropdown.select(index)
_on_deck_selected(index, 1)
p2_deck_dropdown.select(index)
_on_deck_selected(index, 2)
func _find_dropdown_index_for_starter(dropdown: OptionButton, starter_id: String) -> int:
for i in range(dropdown.get_item_count()):
var meta = dropdown.get_item_metadata(i)
if meta is Dictionary and meta.get("type") == "starter" and meta.get("id") == starter_id:
return i
return -1
func _on_deck_selected(index: int, player_num: int) -> void:
var dropdown = p1_deck_dropdown if player_num == 1 else p2_deck_dropdown
var preview = p1_preview if player_num == 1 else p2_preview
var meta = dropdown.get_item_metadata(index)
if not meta is Dictionary:
return
var deck_cards: Array = []
var deck_name: String = ""
var deck_elements: Array = []
var deck_description: String = ""
var deck_texture: Texture2D = null
if meta.get("type") == "saved":
var deck = DeckManager.load_deck(meta.get("name"))
if deck:
deck_cards = deck.to_card_array()
deck_name = deck.name
deck_elements = _get_elements_from_deck(deck_cards)
deck_description = "Custom deck"
# No box art for custom decks
elif meta.get("type") == "starter":
var starter = CardDatabase.get_starter_deck(meta.get("id"))
if starter:
deck_cards = starter.cards.duplicate()
deck_name = starter.name
deck_elements = starter.elements
deck_description = starter.description
deck_texture = starter.get_texture()
# Store selected deck
if player_num == 1:
p1_selected_deck = deck_cards
else:
p2_selected_deck = deck_cards
# Update preview
_update_preview(preview, deck_name, deck_elements, deck_cards.size(), deck_description, deck_texture)
# Update start button state
_update_start_button()
func _get_elements_from_deck(card_ids: Array) -> Array:
var elements: Dictionary = {}
for card_id in card_ids:
var card = CardDatabase.get_card(card_id)
if card:
for element in card.elements:
var elem_name = Enums.element_to_string(element)
elements[elem_name] = elements.get(elem_name, 0) + 1
# Sort by count and return top elements
var sorted_elements: Array = []
for elem_name in elements.keys():
sorted_elements.append({"name": elem_name, "count": elements[elem_name]})
sorted_elements.sort_custom(func(a, b): return a.count > b.count)
var result: Array = []
for i in range(mini(2, sorted_elements.size())):
result.append(sorted_elements[i].name)
return result
func _update_preview(preview: Control, deck_name: String, elements: Array, card_count: int, description: String, texture: Texture2D = null) -> void:
var box_art = preview.get_node_or_null("VBoxContainer/ArtContainer/BoxArt") as TextureRect
var placeholder = preview.get_node_or_null("VBoxContainer/ArtContainer/Placeholder") as ColorRect
var name_label = preview.get_node_or_null("VBoxContainer/DeckName") as Label
var elements_row = preview.get_node_or_null("VBoxContainer/ElementsCenter/ElementsRow") as HBoxContainer
var count_label = preview.get_node_or_null("VBoxContainer/InfoRow/CardCount") as Label
var desc_label = preview.get_node_or_null("VBoxContainer/InfoRow/Description") as Label
# Update box art
if box_art and placeholder:
if texture:
box_art.texture = texture
box_art.visible = true
placeholder.visible = false
else:
box_art.texture = null
box_art.visible = false
placeholder.visible = true
if name_label:
name_label.text = deck_name if not deck_name.is_empty() else "No deck selected"
if elements_row:
# Clear existing elements
for child in elements_row.get_children():
child.queue_free()
# Add element indicators
for elem_name in elements:
var elem_container = HBoxContainer.new()
elem_container.add_theme_constant_override("separation", 4)
var color_rect = ColorRect.new()
color_rect.custom_minimum_size = Vector2(12, 12)
var element = Enums.element_from_string(elem_name)
color_rect.color = Enums.element_to_color(element)
elem_container.add_child(color_rect)
var elem_label = Label.new()
elem_label.text = elem_name
elem_label.add_theme_font_size_override("font_size", 11)
elem_label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8))
elem_container.add_child(elem_label)
elements_row.add_child(elem_container)
if count_label:
count_label.text = "%d cards" % card_count
if desc_label:
desc_label.text = description
func _update_start_button() -> void:
# Require at least 1 card in each deck to start (relaxed from 50 for testing with incomplete card databases)
var can_start = p1_selected_deck.size() >= 1 and p2_selected_deck.size() >= 1
start_button.disabled = not can_start
if can_start:
start_button.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
else:
start_button.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
func _create_buttons() -> void:
buttons_container = HBoxContainer.new()
buttons_container.add_theme_constant_override("separation", 20)
buttons_container.alignment = BoxContainer.ALIGNMENT_CENTER
main_vbox.add_child(buttons_container)
back_button = _create_styled_button("Back", Color(0.3, 0.25, 0.25))
back_button.pressed.connect(_on_back_pressed)
buttons_container.add_child(back_button)
start_button = _create_styled_button("Start Game", Color(0.2, 0.35, 0.25))
start_button.custom_minimum_size.x = 180
start_button.pressed.connect(_on_start_pressed)
start_button.disabled = true
buttons_container.add_child(start_button)
func _create_styled_button(text: String, base_color: Color) -> Button:
var button = Button.new()
button.text = text
button.custom_minimum_size = Vector2(140, 44)
button.add_theme_font_size_override("font_size", 16)
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = base_color
style_normal.border_color = Color(0.5, 0.4, 0.2)
style_normal.set_border_width_all(2)
style_normal.set_corner_radius_all(6)
button.add_theme_stylebox_override("normal", style_normal)
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = base_color.lightened(0.15)
style_hover.border_color = Color(0.7, 0.55, 0.3)
style_hover.set_border_width_all(2)
style_hover.set_corner_radius_all(6)
button.add_theme_stylebox_override("hover", style_hover)
var style_pressed = StyleBoxFlat.new()
style_pressed.bg_color = base_color.darkened(0.1)
style_pressed.border_color = Color(0.5, 0.4, 0.2)
style_pressed.set_border_width_all(2)
style_pressed.set_corner_radius_all(6)
button.add_theme_stylebox_override("pressed", style_pressed)
var style_disabled = StyleBoxFlat.new()
style_disabled.bg_color = Color(0.15, 0.15, 0.18)
style_disabled.border_color = Color(0.3, 0.3, 0.3)
style_disabled.set_border_width_all(2)
style_disabled.set_corner_radius_all(6)
button.add_theme_stylebox_override("disabled", style_disabled)
button.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
button.add_theme_color_override("font_hover_color", Color(1.0, 0.95, 0.8))
button.add_theme_color_override("font_pressed_color", Color(0.7, 0.65, 0.55))
button.add_theme_color_override("font_disabled_color", Color(0.4, 0.4, 0.4))
return button
func _style_dropdown(dropdown: OptionButton) -> void:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.12, 0.12, 0.16)
style.border_color = Color(0.4, 0.35, 0.25)
style.set_border_width_all(1)
style.set_corner_radius_all(4)
style.content_margin_left = 10
style.content_margin_right = 10
style.content_margin_top = 6
style.content_margin_bottom = 6
dropdown.add_theme_stylebox_override("normal", style)
var hover_style = style.duplicate()
hover_style.border_color = Color(0.6, 0.5, 0.3)
dropdown.add_theme_stylebox_override("hover", hover_style)
dropdown.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
dropdown.add_theme_color_override("font_hover_color", Color(1.0, 0.95, 0.8))
func _create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.08, 0.12, 1.0)
# No border on the outer panel to avoid gaps at window edges
style.set_border_width_all(0)
style.set_corner_radius_all(0)
return style
func _create_player_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.1, 0.1, 0.14, 0.9)
style.border_color = Color(0.4, 0.35, 0.25)
style.set_border_width_all(2)
style.set_corner_radius_all(6)
return style
func _create_separator_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.5, 0.4, 0.2, 0.5)
style.content_margin_top = 1
return style
func _on_back_pressed() -> void:
back_pressed.emit()
func _on_start_pressed() -> void:
if p1_selected_deck.size() >= 1 and p2_selected_deck.size() >= 1:
start_game_requested.emit(p1_selected_deck, p2_selected_deck)

View File

@@ -5,8 +5,8 @@ extends CanvasLayer
## The window is sized to match the image (67% of 1024x1536).
## The image fills the entire window; buttons overlay the pre-drawn slots.
signal quick_play
signal play_game
signal deck_builder
signal online_game
signal open_settings
signal quit_game
@@ -14,8 +14,8 @@ signal quit_game
# UI Components
var bg_texture: TextureRect
var buttons_container: Control
var quick_play_button: Button
var play_button: Button
var deck_builder_button: Button
var online_button: Button
var settings_button: Button
var quit_button: Button
@@ -64,15 +64,17 @@ func _create_menu() -> void:
buttons_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
# Create buttons overlaying the pre-drawn slots
quick_play_button = _create_overlay_button("Quick Play", 0)
quick_play_button.add_theme_color_override("font_color", Color(0.15, 0.13, 0.1))
quick_play_button.add_theme_color_override("font_hover_color", Color(0.3, 0.25, 0.2))
quick_play_button.add_theme_color_override("font_pressed_color", Color(0.05, 0.05, 0.05))
quick_play_button.pressed.connect(_on_quick_play_pressed)
play_button = _create_overlay_button("Play", 1)
# "Play" is now in the top golden slot (formerly Quick Play)
play_button = _create_overlay_button("Play", 0)
play_button.add_theme_color_override("font_color", Color(0.15, 0.13, 0.1))
play_button.add_theme_color_override("font_hover_color", Color(0.3, 0.25, 0.2))
play_button.add_theme_color_override("font_pressed_color", Color(0.05, 0.05, 0.05))
play_button.pressed.connect(_on_play_pressed)
# "Deck Builder" is in slot 1 (formerly Play)
deck_builder_button = _create_overlay_button("Deck Builder", 1)
deck_builder_button.pressed.connect(_on_deck_builder_pressed)
online_button = _create_overlay_button("Online", 2)
online_button.disabled = true
@@ -158,12 +160,14 @@ func _reposition_elements() -> void:
version_label.position = Vector2(win_size.x - 80, win_size.y - 24)
version_label.size = Vector2(72, 18)
func _on_quick_play_pressed() -> void:
quick_play.emit()
func _on_play_pressed() -> void:
play_game.emit()
func _on_deck_builder_pressed() -> void:
deck_builder.emit()
func _on_quit_pressed() -> void:
quit_game.emit()
get_tree().quit()