Files
FFCardGame/scripts/ui/GameSetupMenu.gd
2026-02-02 16:28:53 -05:00

640 lines
21 KiB
GDScript

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, is_vs_ai: bool, ai_difficulty: int)
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 ai_difficulty_container: HBoxContainer
var ai_difficulty_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
var p2_title_label: Label # Reference to update "PLAYER 2" / "AI OPPONENT"
# 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
# AI settings
var is_vs_ai: bool = false
var ai_difficulty: int = AIStrategy.Difficulty.NORMAL # Default to Normal
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(200, 36)
game_type_dropdown.add_item("2-Player Local")
game_type_dropdown.add_item("vs AI")
game_type_dropdown.add_theme_font_size_override("font_size", 14)
game_type_dropdown.item_selected.connect(_on_game_type_changed)
_style_dropdown(game_type_dropdown)
game_type_container.add_child(game_type_dropdown)
# AI Difficulty dropdown (initially hidden)
ai_difficulty_container = HBoxContainer.new()
ai_difficulty_container.add_theme_constant_override("separation", 10)
ai_difficulty_container.visible = false
game_type_container.add_child(ai_difficulty_container)
var diff_label = Label.new()
diff_label.text = "Difficulty:"
diff_label.add_theme_font_size_override("font_size", 18)
diff_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
ai_difficulty_container.add_child(diff_label)
ai_difficulty_dropdown = OptionButton.new()
ai_difficulty_dropdown.custom_minimum_size = Vector2(120, 36)
ai_difficulty_dropdown.add_item("Easy")
ai_difficulty_dropdown.add_item("Normal")
ai_difficulty_dropdown.add_item("Hard")
ai_difficulty_dropdown.select(1) # Default to Normal
ai_difficulty_dropdown.add_theme_font_size_override("font_size", 14)
ai_difficulty_dropdown.item_selected.connect(_on_ai_difficulty_changed)
_style_dropdown(ai_difficulty_dropdown)
ai_difficulty_container.add_child(ai_difficulty_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 player_title = Label.new()
player_title.name = "TitleLabel"
player_title.text = title
player_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
player_title.add_theme_font_size_override("font_size", 18)
player_title.add_theme_color_override("font_color", Color(1.0, 0.95, 0.8))
inner_vbox.add_child(player_title)
# Store reference for Player 2 to update when AI mode changes
if player_num == 2:
p2_title_label = player_title
# 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_game_type_changed(index: int) -> void:
is_vs_ai = (index == 1)
ai_difficulty_container.visible = is_vs_ai
# Update Player 2 panel title
if p2_title_label:
if is_vs_ai:
p2_title_label.text = "AI OPPONENT"
else:
p2_title_label.text = "PLAYER 2"
func _on_ai_difficulty_changed(index: int) -> void:
ai_difficulty = index # 0=Easy, 1=Normal, 2=Hard
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, is_vs_ai, ai_difficulty)