feature updates

This commit is contained in:
2026-02-02 16:28:53 -05:00
parent bf9aa3fa23
commit 44c06530ac
83 changed files with 282641 additions and 11251 deletions

347
scripts/ui/ChoiceModal.gd Normal file
View File

@@ -0,0 +1,347 @@
class_name ChoiceModal
extends CanvasLayer
## ChoiceModal - UI component for multi-modal ability choices
## Displays options like "Select 1 of 3 following actions" and returns selection
signal choice_made(selected_indices: Array)
signal choice_cancelled
# UI Elements
var backdrop: ColorRect
var modal_panel: Panel
var title_label: Label
var options_container: VBoxContainer
var confirm_button: Button
var cancel_button: Button
# State
var _modes: Array = []
var _select_count: int = 1
var _select_up_to: bool = false
var _selected_indices: Array = []
var _cancellable: bool = false
var _option_buttons: Array = []
# Cached styles for option buttons (created once, reused)
var _option_normal_style: StyleBoxFlat
var _option_selected_style: StyleBoxFlat
func _ready() -> void:
layer = 200 # High z-index for modal overlay
_create_cached_styles()
_create_ui()
visible = false
func _create_cached_styles() -> void:
# Normal button style
_option_normal_style = StyleBoxFlat.new()
_option_normal_style.bg_color = Color(0.15, 0.15, 0.2, 0.9)
_option_normal_style.set_border_width_all(1)
_option_normal_style.border_color = Color(0.3, 0.3, 0.4)
_option_normal_style.set_corner_radius_all(4)
_option_normal_style.set_content_margin_all(10)
# Selected button style (gold highlight)
_option_selected_style = StyleBoxFlat.new()
_option_selected_style.bg_color = Color(0.25, 0.22, 0.15, 0.95)
_option_selected_style.border_color = Color(0.7, 0.55, 0.2)
_option_selected_style.set_border_width_all(2)
_option_selected_style.set_corner_radius_all(4)
_option_selected_style.set_content_margin_all(10)
func _create_ui() -> void:
# Backdrop - semi-transparent dark overlay
backdrop = ColorRect.new()
add_child(backdrop)
backdrop.color = Color(0, 0, 0, 0.7)
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
backdrop.gui_input.connect(_on_backdrop_input)
# Center container for modal
var center = CenterContainer.new()
add_child(center)
center.set_anchors_preset(Control.PRESET_FULL_RECT)
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
# Modal panel
modal_panel = Panel.new()
center.add_child(modal_panel)
modal_panel.custom_minimum_size = Vector2(500, 200)
var style = StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.08, 0.12, 0.98)
style.border_color = Color(0.5, 0.4, 0.2) # Gold border
style.set_border_width_all(2)
style.set_corner_radius_all(8)
style.set_content_margin_all(20)
modal_panel.add_theme_stylebox_override("panel", style)
# Main vertical layout
var vbox = VBoxContainer.new()
modal_panel.add_child(vbox)
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
vbox.offset_left = 20
vbox.offset_right = -20
vbox.offset_top = 20
vbox.offset_bottom = -20
vbox.add_theme_constant_override("separation", 15)
# Title
title_label = Label.new()
vbox.add_child(title_label)
title_label.text = "Select an action:"
title_label.add_theme_font_size_override("font_size", 20)
title_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
# Options container
options_container = VBoxContainer.new()
vbox.add_child(options_container)
options_container.add_theme_constant_override("separation", 8)
options_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
# Button row
var button_row = HBoxContainer.new()
vbox.add_child(button_row)
button_row.add_theme_constant_override("separation", 15)
button_row.alignment = BoxContainer.ALIGNMENT_CENTER
# Confirm button (for multi-select)
confirm_button = _create_button("Confirm", Color(0.2, 0.5, 0.3))
button_row.add_child(confirm_button)
confirm_button.pressed.connect(_on_confirm_pressed)
confirm_button.visible = false # Only shown for multi-select
# Cancel button
cancel_button = _create_button("Cancel", Color(0.5, 0.3, 0.3))
button_row.add_child(cancel_button)
cancel_button.pressed.connect(_on_cancel_pressed)
cancel_button.visible = false # Only shown if cancellable
func _create_button(text: String, base_color: Color) -> Button:
var button = Button.new()
button.text = text
button.custom_minimum_size = Vector2(100, 40)
var normal_style = StyleBoxFlat.new()
normal_style.bg_color = base_color
normal_style.set_border_width_all(1)
normal_style.border_color = base_color.lightened(0.3)
normal_style.set_corner_radius_all(4)
normal_style.set_content_margin_all(8)
button.add_theme_stylebox_override("normal", normal_style)
var hover_style = normal_style.duplicate()
hover_style.bg_color = base_color.lightened(0.15)
button.add_theme_stylebox_override("hover", hover_style)
var pressed_style = normal_style.duplicate()
pressed_style.bg_color = base_color.darkened(0.1)
button.add_theme_stylebox_override("pressed", pressed_style)
return button
func _create_option_button(index: int, description: String) -> Button:
var button = Button.new()
button.text = str(index + 1) + ". " + description
button.custom_minimum_size = Vector2(460, 50)
button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
button.text_overrun_behavior = TextServer.OVERRUN_NO_TRIMMING
# Normal state
var normal_style = StyleBoxFlat.new()
normal_style.bg_color = Color(0.15, 0.15, 0.2, 0.9)
normal_style.set_border_width_all(1)
normal_style.border_color = Color(0.3, 0.3, 0.4)
normal_style.set_corner_radius_all(4)
normal_style.set_content_margin_all(10)
button.add_theme_stylebox_override("normal", normal_style)
# Hover state
var hover_style = normal_style.duplicate()
hover_style.bg_color = Color(0.2, 0.2, 0.3, 0.95)
hover_style.border_color = Color(0.5, 0.4, 0.2)
button.add_theme_stylebox_override("hover", hover_style)
# Pressed/selected state (gold highlight)
var pressed_style = normal_style.duplicate()
pressed_style.bg_color = Color(0.25, 0.22, 0.15, 0.95)
pressed_style.border_color = Color(0.7, 0.55, 0.2)
pressed_style.set_border_width_all(2)
button.add_theme_stylebox_override("pressed", pressed_style)
# Font settings
button.add_theme_font_size_override("font_size", 14)
button.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85))
button.add_theme_color_override("font_hover_color", Color(1, 0.95, 0.8))
# Connect press signal
button.pressed.connect(_on_option_pressed.bind(index))
return button
## Show modal and await selection
## Returns array of selected mode indices
func show_choices(
title: String,
modes: Array,
select_count: int = 1,
select_up_to: bool = false,
cancellable: bool = false
) -> Array:
_modes = modes
_select_count = select_count
_select_up_to = select_up_to
_cancellable = cancellable
_selected_indices = []
_option_buttons = []
# Update title
if select_up_to:
title_label.text = "Select up to %d action%s:" % [select_count, "s" if select_count > 1 else ""]
else:
title_label.text = "Select %d action%s:" % [select_count, "s" if select_count > 1 else ""]
# Clear and populate options
for child in options_container.get_children():
child.queue_free()
await get_tree().process_frame # Wait for queue_free
for i in range(modes.size()):
var mode = modes[i]
var description = mode.get("description", "Option " + str(i + 1))
var button = _create_option_button(i, description)
options_container.add_child(button)
_option_buttons.append(button)
# Show confirm button only for multi-select
confirm_button.visible = (select_count > 1 or select_up_to)
_update_confirm_button()
# Show cancel if cancellable
cancel_button.visible = cancellable
# Resize panel to fit content
await get_tree().process_frame
var content_height = 20 + 30 + 15 + (modes.size() * 58) + 15 + 50 + 20
modal_panel.custom_minimum_size = Vector2(500, min(content_height, 600))
visible = true
# Wait for selection
var result = await _wait_for_selection()
visible = false
return result
## Internal: Wait for user selection using a callback pattern
func _wait_for_selection() -> Array:
var result: Array = []
# Create a one-shot signal connection
var completed = false
var on_choice = func(indices: Array):
result = indices
completed = true
var on_cancel = func():
result = []
completed = true
choice_made.connect(on_choice, CONNECT_ONE_SHOT)
choice_cancelled.connect(on_cancel, CONNECT_ONE_SHOT)
# Wait until completed
while not completed:
await get_tree().process_frame
return result
func _on_option_pressed(index: int) -> void:
if _select_count == 1 and not _select_up_to:
# Single select - immediately return
choice_made.emit([index])
return
# Multi-select - toggle selection
if index in _selected_indices:
_selected_indices.erase(index)
else:
if _selected_indices.size() < _select_count:
_selected_indices.append(index)
_update_option_visuals()
_update_confirm_button()
func _update_option_visuals() -> void:
for i in range(_option_buttons.size()):
var button = _option_buttons[i] as Button
var is_selected = i in _selected_indices
# Use cached styles instead of creating new ones each time
if is_selected:
button.add_theme_stylebox_override("normal", _option_selected_style)
else:
button.add_theme_stylebox_override("normal", _option_normal_style)
func _update_confirm_button() -> void:
if _select_up_to:
confirm_button.disabled = false
confirm_button.text = "Confirm (%d)" % _selected_indices.size()
else:
confirm_button.disabled = _selected_indices.size() != _select_count
confirm_button.text = "Confirm (%d/%d)" % [_selected_indices.size(), _select_count]
func _on_confirm_pressed() -> void:
if _select_up_to or _selected_indices.size() == _select_count:
choice_made.emit(_selected_indices.duplicate())
func _on_cancel_pressed() -> void:
choice_cancelled.emit()
func _on_backdrop_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
if _cancellable:
choice_cancelled.emit()
func _input(event: InputEvent) -> void:
if not visible:
return
# Keyboard shortcuts
if event is InputEventKey and event.pressed:
# Number keys 1-9 for quick selection
if event.keycode >= KEY_1 and event.keycode <= KEY_9:
var index = event.keycode - KEY_1
if index < _modes.size():
_on_option_pressed(index)
get_viewport().set_input_as_handled()
# Enter to confirm (multi-select only)
elif event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER:
if confirm_button.visible and not confirm_button.disabled:
_on_confirm_pressed()
get_viewport().set_input_as_handled()
# Escape to cancel
elif event.keycode == KEY_ESCAPE:
if _cancellable:
choice_cancelled.emit()
get_viewport().set_input_as_handled()

View File

@@ -5,7 +5,7 @@ extends CanvasLayer
## Allows selection of game type and decks for each player
signal back_pressed
signal start_game_requested(p1_deck: Array, p2_deck: Array)
signal start_game_requested(p1_deck: Array, p2_deck: Array, is_vs_ai: bool, ai_difficulty: int)
const WINDOW_SIZE := Vector2(800, 600)
@@ -15,6 +15,8 @@ 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
@@ -25,6 +27,7 @@ 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] = []
@@ -32,6 +35,10 @@ 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
@@ -114,14 +121,37 @@ func _create_game_type_selector() -> void:
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.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()
@@ -157,12 +187,17 @@ func _create_player_panel(title: String, player_num: int) -> Control:
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)
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()
@@ -579,10 +614,26 @@ func _create_separator_style() -> StyleBoxFlat:
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)
start_game_requested.emit(p1_selected_deck, p2_selected_deck, is_vs_ai, ai_difficulty)

View File

@@ -309,6 +309,14 @@ func _show_next_message() -> void:
func _on_message_timer_timeout() -> void:
_show_next_message()
## Hide message immediately (e.g., when AI finishes thinking)
func hide_message() -> void:
message_queue.clear()
message_panel.visible = false
message_timer.stop()
## Show card detail panel
func show_card_detail(card: CardInstance) -> void:
if not card or not card.card_data:

View File

@@ -0,0 +1,442 @@
class_name LeaderboardScreen
extends CanvasLayer
## LeaderboardScreen - Displays top players ranked by ELO
signal back_pressed
# Window dimensions
const WINDOW_SIZE := Vector2i(600, 700)
# Pagination
const PLAYERS_PER_PAGE = 20
# UI Components
var main_container: VBoxContainer
var back_button: Button
var title_label: Label
var info_label: Label
var leaderboard_section: PanelContainer
var leaderboard_list: VBoxContainer
var header_row: HBoxContainer
var pagination_container: HBoxContainer
var prev_button: Button
var page_label: Label
var next_button: Button
var loading_label: Label
var error_label: Label
# State
var current_page: int = 0
var is_loading: bool = false
var players_cache: Array = []
# Styling
var custom_font: Font = preload("res://JimNightshade-Regular.ttf")
const BG_COLOR := Color(0.12, 0.11, 0.15, 1.0)
const PANEL_COLOR := Color(0.18, 0.16, 0.22, 1.0)
const ACCENT_COLOR := Color(0.4, 0.35, 0.55, 1.0)
const GOLD_COLOR := Color(1.0, 0.84, 0.0, 1.0)
const SILVER_COLOR := Color(0.75, 0.75, 0.75, 1.0)
const BRONZE_COLOR := Color(0.8, 0.5, 0.2, 1.0)
const TEXT_COLOR := Color(0.9, 0.88, 0.82, 1.0)
const MUTED_COLOR := Color(0.6, 0.58, 0.52, 1.0)
const HIGHLIGHT_COLOR := Color(0.3, 0.35, 0.5, 1.0)
func _ready() -> void:
_create_ui()
_load_leaderboard()
func _create_ui() -> void:
# Background
var bg = ColorRect.new()
bg.color = BG_COLOR
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(bg)
# Main container
main_container = VBoxContainer.new()
main_container.set_anchors_preset(Control.PRESET_FULL_RECT)
main_container.add_theme_constant_override("separation", 16)
add_child(main_container)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_top", 16)
margin.add_theme_constant_override("margin_bottom", 16)
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
main_container.add_child(margin)
var content = VBoxContainer.new()
content.add_theme_constant_override("separation", 16)
margin.add_child(content)
# Back button
back_button = _create_button("< Back", false)
back_button.custom_minimum_size = Vector2(80, 32)
back_button.pressed.connect(_on_back_pressed)
content.add_child(back_button)
# Title
title_label = Label.new()
title_label.add_theme_font_override("font", custom_font)
title_label.add_theme_font_size_override("font_size", 28)
title_label.add_theme_color_override("font_color", TEXT_COLOR)
title_label.text = "LEADERBOARD"
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(title_label)
# Info label
info_label = Label.new()
info_label.add_theme_font_override("font", custom_font)
info_label.add_theme_font_size_override("font_size", 12)
info_label.add_theme_color_override("font_color", MUTED_COLOR)
info_label.text = "Minimum 10 games required to appear on leaderboard"
info_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(info_label)
# Leaderboard section
_create_leaderboard_section(content)
# Error label
error_label = Label.new()
error_label.add_theme_font_override("font", custom_font)
error_label.add_theme_font_size_override("font_size", 14)
error_label.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3))
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD
error_label.visible = false
content.add_child(error_label)
func _create_leaderboard_section(parent: VBoxContainer) -> void:
leaderboard_section = _create_panel_section("TOP PLAYERS")
leaderboard_section.size_flags_vertical = Control.SIZE_EXPAND_FILL
parent.add_child(leaderboard_section)
var content = leaderboard_section.get_child(0) as VBoxContainer
# Header row
header_row = _create_header_row()
content.add_child(header_row)
# Separator
var separator = HSeparator.new()
var sep_style = StyleBoxFlat.new()
sep_style.bg_color = MUTED_COLOR
sep_style.content_margin_top = 1
separator.add_theme_stylebox_override("separator", sep_style)
content.add_child(separator)
# Loading indicator
loading_label = Label.new()
loading_label.add_theme_font_override("font", custom_font)
loading_label.add_theme_font_size_override("font_size", 14)
loading_label.add_theme_color_override("font_color", MUTED_COLOR)
loading_label.text = "Loading..."
loading_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(loading_label)
# Player list
leaderboard_list = VBoxContainer.new()
leaderboard_list.add_theme_constant_override("separation", 4)
leaderboard_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
content.add_child(leaderboard_list)
# Pagination
pagination_container = HBoxContainer.new()
pagination_container.add_theme_constant_override("separation", 16)
pagination_container.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_child(pagination_container)
prev_button = _create_button("< Prev", false)
prev_button.custom_minimum_size = Vector2(80, 32)
prev_button.pressed.connect(_on_prev_page)
pagination_container.add_child(prev_button)
page_label = Label.new()
page_label.add_theme_font_override("font", custom_font)
page_label.add_theme_font_size_override("font_size", 14)
page_label.add_theme_color_override("font_color", TEXT_COLOR)
page_label.text = "Page 1"
pagination_container.add_child(page_label)
next_button = _create_button("Next >", false)
next_button.custom_minimum_size = Vector2(80, 32)
next_button.pressed.connect(_on_next_page)
pagination_container.add_child(next_button)
func _create_header_row() -> HBoxContainer:
var row = HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
# Rank
var rank_header = _create_header_label("RANK", 50)
row.add_child(rank_header)
# Player
var player_header = _create_header_label("PLAYER", 0)
player_header.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(player_header)
# ELO
var elo_header = _create_header_label("ELO", 60)
elo_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(elo_header)
# Win Rate
var wr_header = _create_header_label("WIN%", 60)
wr_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(wr_header)
# Games
var games_header = _create_header_label("GAMES", 60)
games_header.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(games_header)
return row
func _create_header_label(text: String, min_width: int) -> Label:
var label = Label.new()
label.add_theme_font_override("font", custom_font)
label.add_theme_font_size_override("font_size", 12)
label.add_theme_color_override("font_color", MUTED_COLOR)
label.text = text
if min_width > 0:
label.custom_minimum_size.x = min_width
return label
func _create_panel_section(title: String) -> PanelContainer:
var panel = PanelContainer.new()
var style = StyleBoxFlat.new()
style.bg_color = PANEL_COLOR
style.set_corner_radius_all(8)
style.set_content_margin_all(16)
panel.add_theme_stylebox_override("panel", style)
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 12)
panel.add_child(vbox)
var title_label = Label.new()
title_label.add_theme_font_override("font", custom_font)
title_label.add_theme_font_size_override("font_size", 18)
title_label.add_theme_color_override("font_color", TEXT_COLOR)
title_label.text = title
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title_label)
return panel
func _create_button(text: String, primary: bool) -> Button:
var button = Button.new()
button.text = text
button.add_theme_font_override("font", custom_font)
button.add_theme_font_size_override("font_size", 16)
button.custom_minimum_size = Vector2(0, 40)
var normal = StyleBoxFlat.new()
var hover = StyleBoxFlat.new()
var pressed = StyleBoxFlat.new()
var disabled = StyleBoxFlat.new()
if primary:
normal.bg_color = ACCENT_COLOR
hover.bg_color = ACCENT_COLOR.lightened(0.15)
pressed.bg_color = ACCENT_COLOR.darkened(0.15)
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("font_hover_color", Color.WHITE)
else:
normal.bg_color = Color(0.25, 0.23, 0.3)
hover.bg_color = Color(0.3, 0.28, 0.35)
pressed.bg_color = Color(0.2, 0.18, 0.25)
button.add_theme_color_override("font_color", TEXT_COLOR)
button.add_theme_color_override("font_hover_color", TEXT_COLOR)
disabled.bg_color = Color(0.2, 0.18, 0.22)
button.add_theme_color_override("font_disabled_color", MUTED_COLOR)
for style in [normal, hover, pressed, disabled]:
style.set_corner_radius_all(6)
style.set_content_margin_all(8)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", pressed)
button.add_theme_stylebox_override("disabled", disabled)
return button
func _load_leaderboard() -> void:
is_loading = true
loading_label.visible = true
_clear_leaderboard_list()
var offset = current_page * PLAYERS_PER_PAGE
var result = await NetworkManager.get_leaderboard(PLAYERS_PER_PAGE, offset)
loading_label.visible = false
is_loading = false
if result.success:
var players = result.get("players", [])
players_cache = players
_display_players(players)
_update_pagination()
else:
_show_error(result.get("message", "Failed to load leaderboard"))
func _display_players(players: Array) -> void:
_clear_leaderboard_list()
if players.is_empty():
var empty_label = Label.new()
empty_label.add_theme_font_override("font", custom_font)
empty_label.add_theme_font_size_override("font_size", 14)
empty_label.add_theme_color_override("font_color", MUTED_COLOR)
empty_label.text = "No players found"
empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
leaderboard_list.add_child(empty_label)
return
for player_data in players:
var player_row = _create_player_row(player_data)
leaderboard_list.add_child(player_row)
func _create_player_row(player_data: Dictionary) -> PanelContainer:
var panel = PanelContainer.new()
var style = StyleBoxFlat.new()
style.set_corner_radius_all(4)
style.set_content_margin_all(8)
# Check if this is the current user
var current_username = ""
if NetworkManager and NetworkManager.is_authenticated:
current_username = NetworkManager.current_user.get("username", "")
var is_current_user = player_data.get("username", "") == current_username
if is_current_user:
style.bg_color = HIGHLIGHT_COLOR
else:
style.bg_color = Color(0.15, 0.14, 0.18, 0.5)
panel.add_theme_stylebox_override("panel", style)
var row = HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
panel.add_child(row)
var rank = player_data.get("rank", 0)
# Rank with medal colors for top 3
var rank_label = Label.new()
rank_label.add_theme_font_override("font", custom_font)
rank_label.add_theme_font_size_override("font_size", 16)
rank_label.custom_minimum_size.x = 50
rank_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
match rank:
1:
rank_label.text = "#1"
rank_label.add_theme_color_override("font_color", GOLD_COLOR)
2:
rank_label.text = "#2"
rank_label.add_theme_color_override("font_color", SILVER_COLOR)
3:
rank_label.text = "#3"
rank_label.add_theme_color_override("font_color", BRONZE_COLOR)
_:
rank_label.text = "#%d" % rank
rank_label.add_theme_color_override("font_color", TEXT_COLOR)
row.add_child(rank_label)
# Player name
var name_label = Label.new()
name_label.add_theme_font_override("font", custom_font)
name_label.add_theme_font_size_override("font_size", 16)
name_label.add_theme_color_override("font_color", ACCENT_COLOR if is_current_user else TEXT_COLOR)
name_label.text = player_data.get("username", "Unknown")
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(name_label)
# ELO
var elo_label = Label.new()
elo_label.add_theme_font_override("font", custom_font)
elo_label.add_theme_font_size_override("font_size", 16)
elo_label.add_theme_color_override("font_color", TEXT_COLOR)
elo_label.text = str(player_data.get("eloRating", 1000))
elo_label.custom_minimum_size.x = 60
elo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(elo_label)
# Win rate
var wr_label = Label.new()
wr_label.add_theme_font_override("font", custom_font)
wr_label.add_theme_font_size_override("font_size", 14)
wr_label.add_theme_color_override("font_color", MUTED_COLOR)
wr_label.text = "%d%%" % player_data.get("winRate", 0)
wr_label.custom_minimum_size.x = 60
wr_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(wr_label)
# Games played
var games_label = Label.new()
games_label.add_theme_font_override("font", custom_font)
games_label.add_theme_font_size_override("font_size", 14)
games_label.add_theme_color_override("font_color", MUTED_COLOR)
games_label.text = str(player_data.get("gamesPlayed", 0))
games_label.custom_minimum_size.x = 60
games_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
row.add_child(games_label)
return panel
func _clear_leaderboard_list() -> void:
for child in leaderboard_list.get_children():
child.queue_free()
func _update_pagination() -> void:
page_label.text = "Page %d" % (current_page + 1)
prev_button.disabled = current_page == 0
# Disable next if we got fewer results than requested
next_button.disabled = players_cache.size() < PLAYERS_PER_PAGE
func _show_error(message: String) -> void:
error_label.text = message
error_label.visible = true
await get_tree().create_timer(5.0).timeout
if is_instance_valid(error_label):
error_label.visible = false
func _on_back_pressed() -> void:
back_pressed.emit()
func _on_prev_page() -> void:
if current_page > 0:
current_page -= 1
_load_leaderboard()
func _on_next_page() -> void:
current_page += 1
_load_leaderboard()
func refresh() -> void:
current_page = 0
_load_leaderboard()

371
scripts/ui/LoginScreen.gd Normal file
View File

@@ -0,0 +1,371 @@
class_name LoginScreen
extends CanvasLayer
## LoginScreen - Email/password login form for online play
signal login_successful(user_data: Dictionary)
signal register_requested
signal forgot_password_requested
signal back_pressed
const WINDOW_SIZE := Vector2(400, 500)
# UI Components
var background: PanelContainer
var main_vbox: VBoxContainer
var title_label: Label
var email_input: LineEdit
var password_input: LineEdit
var login_button: Button
var register_link: Button
var forgot_password_link: Button
var back_button: Button
var error_label: Label
var loading_spinner: Control
var status_label: Label
# State
var _is_loading: bool = false
func _ready() -> void:
layer = 100
_create_ui()
# Connect to NetworkManager signals
if NetworkManager:
NetworkManager.authenticated.connect(_on_authenticated)
NetworkManager.authentication_failed.connect(_on_auth_failed)
func _create_ui() -> void:
# Background panel
background = PanelContainer.new()
add_child(background)
background.position = Vector2.ZERO
background.size = WINDOW_SIZE
background.add_theme_stylebox_override("panel", _create_panel_style())
# Main layout with margin
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", 40)
margin.add_theme_constant_override("margin_right", 40)
margin.add_theme_constant_override("margin_top", 30)
margin.add_theme_constant_override("margin_bottom", 30)
main_vbox = VBoxContainer.new()
margin.add_child(main_vbox)
main_vbox.add_theme_constant_override("separation", 15)
# Title
_create_title()
# Login form
_create_form()
# Error label
_create_error_label()
# Spacer
var spacer = Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
main_vbox.add_child(spacer)
# Links
_create_links()
# Back button
_create_back_button()
func _create_title() -> void:
title_label = Label.new()
title_label.text = "LOGIN"
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)
# Spacer
var spacer = Control.new()
spacer.custom_minimum_size.y = 20
main_vbox.add_child(spacer)
func _create_form() -> void:
# Email field
var email_label = Label.new()
email_label.text = "Email"
email_label.add_theme_font_size_override("font_size", 16)
email_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
main_vbox.add_child(email_label)
email_input = LineEdit.new()
email_input.placeholder_text = "Enter your email"
email_input.custom_minimum_size = Vector2(0, 40)
_style_input(email_input)
email_input.text_submitted.connect(_on_input_submitted)
main_vbox.add_child(email_input)
# Password field
var password_label = Label.new()
password_label.text = "Password"
password_label.add_theme_font_size_override("font_size", 16)
password_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
main_vbox.add_child(password_label)
password_input = LineEdit.new()
password_input.placeholder_text = "Enter your password"
password_input.secret = true
password_input.custom_minimum_size = Vector2(0, 40)
_style_input(password_input)
password_input.text_submitted.connect(_on_input_submitted)
main_vbox.add_child(password_input)
# Login button
var button_spacer = Control.new()
button_spacer.custom_minimum_size.y = 10
main_vbox.add_child(button_spacer)
login_button = Button.new()
login_button.text = "Login"
login_button.custom_minimum_size = Vector2(0, 45)
_style_button(login_button, true)
login_button.pressed.connect(_on_login_pressed)
main_vbox.add_child(login_button)
func _create_error_label() -> void:
error_label = Label.new()
error_label.text = ""
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.add_theme_font_size_override("font_size", 14)
error_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD
error_label.visible = false
main_vbox.add_child(error_label)
func _create_links() -> void:
var links_container = VBoxContainer.new()
links_container.add_theme_constant_override("separation", 8)
main_vbox.add_child(links_container)
# Register link
register_link = Button.new()
register_link.text = "Don't have an account? Register"
register_link.flat = true
register_link.add_theme_font_size_override("font_size", 14)
register_link.add_theme_color_override("font_color", Color(0.6, 0.7, 1.0))
register_link.add_theme_color_override("font_hover_color", Color(0.8, 0.85, 1.0))
register_link.pressed.connect(_on_register_pressed)
links_container.add_child(register_link)
# Forgot password link
forgot_password_link = Button.new()
forgot_password_link.text = "Forgot Password?"
forgot_password_link.flat = true
forgot_password_link.add_theme_font_size_override("font_size", 14)
forgot_password_link.add_theme_color_override("font_color", Color(0.7, 0.7, 0.8))
forgot_password_link.add_theme_color_override("font_hover_color", Color(0.9, 0.9, 1.0))
forgot_password_link.pressed.connect(_on_forgot_password_pressed)
links_container.add_child(forgot_password_link)
func _create_back_button() -> void:
var button_container = HBoxContainer.new()
button_container.alignment = BoxContainer.ALIGNMENT_CENTER
main_vbox.add_child(button_container)
back_button = Button.new()
back_button.text = "Back"
back_button.custom_minimum_size = Vector2(100, 40)
_style_button(back_button, false)
back_button.pressed.connect(_on_back_pressed)
button_container.add_child(back_button)
# ======= STYLING =======
func _create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.08, 0.12, 1.0)
style.set_border_width_all(0)
style.set_corner_radius_all(0)
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 _style_input(input: LineEdit) -> 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 = 12
style.content_margin_right = 12
style.content_margin_top = 8
style.content_margin_bottom = 8
input.add_theme_stylebox_override("normal", style)
var focus_style = style.duplicate()
focus_style.border_color = Color(0.7, 0.6, 0.3)
input.add_theme_stylebox_override("focus", focus_style)
input.add_theme_color_override("font_color", Color(0.95, 0.9, 0.8))
input.add_theme_color_override("font_placeholder_color", Color(0.5, 0.5, 0.55))
input.add_theme_font_size_override("font_size", 16)
func _style_button(button: Button, is_primary: bool) -> void:
var style = StyleBoxFlat.new()
if is_primary:
style.bg_color = Color(0.3, 0.25, 0.15)
style.border_color = Color(0.6, 0.5, 0.3)
else:
style.bg_color = Color(0.15, 0.15, 0.2)
style.border_color = Color(0.4, 0.35, 0.25)
style.set_border_width_all(2)
style.set_corner_radius_all(6)
style.content_margin_left = 20
style.content_margin_right = 20
style.content_margin_top = 10
style.content_margin_bottom = 10
button.add_theme_stylebox_override("normal", style)
var hover_style = style.duplicate()
if is_primary:
hover_style.bg_color = Color(0.4, 0.35, 0.2)
hover_style.border_color = Color(0.8, 0.7, 0.4)
else:
hover_style.bg_color = Color(0.2, 0.2, 0.25)
hover_style.border_color = Color(0.5, 0.45, 0.35)
button.add_theme_stylebox_override("hover", hover_style)
var pressed_style = style.duplicate()
pressed_style.bg_color = Color(0.1, 0.1, 0.12)
button.add_theme_stylebox_override("pressed", pressed_style)
var disabled_style = style.duplicate()
disabled_style.bg_color = Color(0.1, 0.1, 0.12)
disabled_style.border_color = Color(0.25, 0.25, 0.3)
button.add_theme_stylebox_override("disabled", disabled_style)
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.45, 0.42, 0.38))
button.add_theme_font_size_override("font_size", 18)
# ======= EVENT HANDLERS =======
func _on_input_submitted(_text: String) -> void:
_on_login_pressed()
func _on_login_pressed() -> void:
if _is_loading:
return
var email = email_input.text.strip_edges()
var password = password_input.text
# Validate inputs
if email.is_empty():
_show_error("Please enter your email")
return
if password.is_empty():
_show_error("Please enter your password")
return
if not _is_valid_email(email):
_show_error("Please enter a valid email address")
return
# Start login
_set_loading(true)
_hide_error()
var result = await NetworkManager.login(email, password)
_set_loading(false)
if result.success:
login_successful.emit(result.user)
else:
_show_error(result.message)
func _on_register_pressed() -> void:
register_requested.emit()
func _on_forgot_password_pressed() -> void:
forgot_password_requested.emit()
func _on_back_pressed() -> void:
back_pressed.emit()
func _on_authenticated(user_data: Dictionary) -> void:
login_successful.emit(user_data)
func _on_auth_failed(error: String) -> void:
_set_loading(false)
_show_error(error)
# ======= HELPERS =======
func _is_valid_email(email: String) -> bool:
var regex = RegEx.new()
regex.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
return regex.search(email) != null
func _show_error(message: String) -> void:
error_label.text = message
error_label.visible = true
func _hide_error() -> void:
error_label.text = ""
error_label.visible = false
func _set_loading(loading: bool) -> void:
_is_loading = loading
login_button.disabled = loading
login_button.text = "Logging in..." if loading else "Login"
email_input.editable = not loading
password_input.editable = not loading
func clear_form() -> void:
email_input.text = ""
password_input.text = ""
_hide_error()
_set_loading(false)
func focus_email() -> void:
email_input.grab_focus()

View File

@@ -76,7 +76,7 @@ func _create_menu() -> void:
deck_builder_button.pressed.connect(_on_deck_builder_pressed)
online_button = _create_overlay_button("Online", 2)
online_button.disabled = true
online_button.pressed.connect(_on_online_pressed)
settings_button = _create_overlay_button("Settings", 3)
settings_button.disabled = true
@@ -168,6 +168,10 @@ func _on_deck_builder_pressed() -> void:
deck_builder.emit()
func _on_online_pressed() -> void:
online_game.emit()
func _on_quit_pressed() -> void:
quit_game.emit()
get_tree().quit()

661
scripts/ui/OnlineLobby.gd Normal file
View File

@@ -0,0 +1,661 @@
class_name OnlineLobby
extends CanvasLayer
## OnlineLobby - Matchmaking UI for ranked queue and private rooms
signal game_starting(game_data: Dictionary)
signal back_pressed
signal profile_requested
signal leaderboard_requested
# Window dimensions
const WINDOW_SIZE := Vector2i(600, 700)
# UI Components
var main_container: VBoxContainer
var back_button: Button
var header_container: HBoxContainer
var username_label: Label
var elo_label: Label
var deck_section: VBoxContainer
var deck_dropdown: OptionButton
var ranked_section: PanelContainer
var ranked_content: VBoxContainer
var find_match_button: Button
var cancel_search_button: Button
var queue_status_label: Label
var private_section: PanelContainer
var private_content: VBoxContainer
var create_room_button: Button
var join_container: HBoxContainer
var room_code_input: LineEdit
var join_room_button: Button
var room_section: PanelContainer
var room_content: VBoxContainer
var room_code_label: Label
var copy_code_button: Button
var host_label: Label
var guest_label: Label
var ready_button: Button
var leave_room_button: Button
var error_label: Label
var nav_buttons_container: HBoxContainer
var profile_button: Button
var leaderboard_button: Button
# State
var is_in_queue: bool = false
var is_in_room: bool = false
var is_ready: bool = false
var queue_start_time: float = 0.0
var current_room_code: String = ""
var selected_deck_id: String = ""
# Styling
var custom_font: Font = preload("res://JimNightshade-Regular.ttf")
const BG_COLOR := Color(0.12, 0.11, 0.15, 1.0)
const PANEL_COLOR := Color(0.18, 0.16, 0.22, 1.0)
const ACCENT_COLOR := Color(0.4, 0.35, 0.55, 1.0)
const TEXT_COLOR := Color(0.9, 0.88, 0.82, 1.0)
const MUTED_COLOR := Color(0.6, 0.58, 0.52, 1.0)
func _ready() -> void:
_create_ui()
_connect_network_signals()
_update_user_info()
_fetch_decks()
func _process(_delta: float) -> void:
# Update queue timer if searching
if is_in_queue:
var elapsed = Time.get_ticks_msec() / 1000.0 - queue_start_time
var minutes = int(elapsed) / 60
var seconds = int(elapsed) % 60
queue_status_label.text = "Searching... %d:%02d" % [minutes, seconds]
func _create_ui() -> void:
# Background
var bg = ColorRect.new()
bg.color = BG_COLOR
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(bg)
# Main container
main_container = VBoxContainer.new()
main_container.set_anchors_preset(Control.PRESET_FULL_RECT)
main_container.add_theme_constant_override("separation", 16)
add_child(main_container)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_top", 16)
margin.add_theme_constant_override("margin_bottom", 16)
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
main_container.add_child(margin)
var content = VBoxContainer.new()
content.add_theme_constant_override("separation", 16)
margin.add_child(content)
# Back button
back_button = _create_button("< Back", false)
back_button.custom_minimum_size = Vector2(80, 32)
back_button.pressed.connect(_on_back_pressed)
content.add_child(back_button)
# Header with username and ELO
header_container = HBoxContainer.new()
header_container.add_theme_constant_override("separation", 16)
content.add_child(header_container)
username_label = Label.new()
username_label.add_theme_font_override("font", custom_font)
username_label.add_theme_font_size_override("font_size", 24)
username_label.add_theme_color_override("font_color", TEXT_COLOR)
username_label.text = "Welcome!"
username_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_container.add_child(username_label)
elo_label = Label.new()
elo_label.add_theme_font_override("font", custom_font)
elo_label.add_theme_font_size_override("font_size", 20)
elo_label.add_theme_color_override("font_color", ACCENT_COLOR)
elo_label.text = "ELO: 1000"
header_container.add_child(elo_label)
# Deck selection section
deck_section = VBoxContainer.new()
deck_section.add_theme_constant_override("separation", 8)
content.add_child(deck_section)
var deck_label = Label.new()
deck_label.add_theme_font_override("font", custom_font)
deck_label.add_theme_font_size_override("font_size", 16)
deck_label.add_theme_color_override("font_color", MUTED_COLOR)
deck_label.text = "SELECT DECK"
deck_section.add_child(deck_label)
deck_dropdown = OptionButton.new()
deck_dropdown.add_theme_font_override("font", custom_font)
deck_dropdown.add_theme_font_size_override("font_size", 16)
deck_dropdown.custom_minimum_size = Vector2(0, 40)
deck_dropdown.item_selected.connect(_on_deck_selected)
deck_section.add_child(deck_dropdown)
# Ranked match section
ranked_section = _create_panel_section("RANKED MATCH")
content.add_child(ranked_section)
ranked_content = ranked_section.get_child(0) as VBoxContainer
var ranked_desc = Label.new()
ranked_desc.add_theme_font_override("font", custom_font)
ranked_desc.add_theme_font_size_override("font_size", 14)
ranked_desc.add_theme_color_override("font_color", MUTED_COLOR)
ranked_desc.text = "Find opponents near your skill level"
ranked_content.add_child(ranked_desc)
find_match_button = _create_button("Find Match", true)
find_match_button.pressed.connect(_on_find_match_pressed)
ranked_content.add_child(find_match_button)
cancel_search_button = _create_button("Cancel Search", false)
cancel_search_button.pressed.connect(_on_cancel_search_pressed)
cancel_search_button.visible = false
ranked_content.add_child(cancel_search_button)
queue_status_label = Label.new()
queue_status_label.add_theme_font_override("font", custom_font)
queue_status_label.add_theme_font_size_override("font_size", 14)
queue_status_label.add_theme_color_override("font_color", ACCENT_COLOR)
queue_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
queue_status_label.visible = false
ranked_content.add_child(queue_status_label)
# Private match section
private_section = _create_panel_section("PRIVATE MATCH")
content.add_child(private_section)
private_content = private_section.get_child(0) as VBoxContainer
create_room_button = _create_button("Create Room", true)
create_room_button.pressed.connect(_on_create_room_pressed)
private_content.add_child(create_room_button)
var separator_label = Label.new()
separator_label.add_theme_font_override("font", custom_font)
separator_label.add_theme_font_size_override("font_size", 12)
separator_label.add_theme_color_override("font_color", MUTED_COLOR)
separator_label.text = "─────────── OR ───────────"
separator_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
private_content.add_child(separator_label)
join_container = HBoxContainer.new()
join_container.add_theme_constant_override("separation", 8)
private_content.add_child(join_container)
var code_label = Label.new()
code_label.add_theme_font_override("font", custom_font)
code_label.add_theme_font_size_override("font_size", 14)
code_label.add_theme_color_override("font_color", TEXT_COLOR)
code_label.text = "Code:"
join_container.add_child(code_label)
room_code_input = LineEdit.new()
room_code_input.add_theme_font_override("font", custom_font)
room_code_input.add_theme_font_size_override("font_size", 16)
room_code_input.placeholder_text = "ABC123"
room_code_input.max_length = 6
room_code_input.custom_minimum_size = Vector2(100, 36)
room_code_input.size_flags_horizontal = Control.SIZE_EXPAND_FILL
room_code_input.text_changed.connect(_on_room_code_changed)
join_container.add_child(room_code_input)
join_room_button = _create_button("Join", false)
join_room_button.custom_minimum_size = Vector2(80, 36)
join_room_button.pressed.connect(_on_join_room_pressed)
join_container.add_child(join_room_button)
# Room section (shown when in a room)
room_section = _create_panel_section("ROOM")
room_section.visible = false
content.add_child(room_section)
room_content = room_section.get_child(0) as VBoxContainer
var room_header = HBoxContainer.new()
room_header.add_theme_constant_override("separation", 8)
room_content.add_child(room_header)
room_code_label = Label.new()
room_code_label.add_theme_font_override("font", custom_font)
room_code_label.add_theme_font_size_override("font_size", 20)
room_code_label.add_theme_color_override("font_color", ACCENT_COLOR)
room_code_label.text = "Room: ------"
room_code_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
room_header.add_child(room_code_label)
copy_code_button = _create_button("Copy", false)
copy_code_button.custom_minimum_size = Vector2(60, 28)
copy_code_button.pressed.connect(_on_copy_code_pressed)
room_header.add_child(copy_code_button)
var players_container = VBoxContainer.new()
players_container.add_theme_constant_override("separation", 4)
room_content.add_child(players_container)
host_label = Label.new()
host_label.add_theme_font_override("font", custom_font)
host_label.add_theme_font_size_override("font_size", 16)
host_label.add_theme_color_override("font_color", TEXT_COLOR)
host_label.text = "Host: ---"
players_container.add_child(host_label)
guest_label = Label.new()
guest_label.add_theme_font_override("font", custom_font)
guest_label.add_theme_font_size_override("font_size", 16)
guest_label.add_theme_color_override("font_color", TEXT_COLOR)
guest_label.text = "Guest: Waiting..."
players_container.add_child(guest_label)
var room_buttons = HBoxContainer.new()
room_buttons.add_theme_constant_override("separation", 8)
room_content.add_child(room_buttons)
ready_button = _create_button("Ready", true)
ready_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
ready_button.pressed.connect(_on_ready_pressed)
room_buttons.add_child(ready_button)
leave_room_button = _create_button("Leave Room", false)
leave_room_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
leave_room_button.pressed.connect(_on_leave_room_pressed)
room_buttons.add_child(leave_room_button)
# Error label
error_label = Label.new()
error_label.add_theme_font_override("font", custom_font)
error_label.add_theme_font_size_override("font_size", 14)
error_label.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3))
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD
error_label.visible = false
content.add_child(error_label)
# Navigation buttons (Profile and Leaderboard)
nav_buttons_container = HBoxContainer.new()
nav_buttons_container.add_theme_constant_override("separation", 16)
nav_buttons_container.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_child(nav_buttons_container)
profile_button = _create_button("Profile", false)
profile_button.custom_minimum_size = Vector2(120, 36)
profile_button.pressed.connect(_on_profile_pressed)
nav_buttons_container.add_child(profile_button)
leaderboard_button = _create_button("Leaderboard", false)
leaderboard_button.custom_minimum_size = Vector2(120, 36)
leaderboard_button.pressed.connect(_on_leaderboard_pressed)
nav_buttons_container.add_child(leaderboard_button)
func _create_panel_section(title: String) -> PanelContainer:
var panel = PanelContainer.new()
var style = StyleBoxFlat.new()
style.bg_color = PANEL_COLOR
style.set_corner_radius_all(8)
style.set_content_margin_all(16)
panel.add_theme_stylebox_override("panel", style)
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 12)
panel.add_child(vbox)
var title_label = Label.new()
title_label.add_theme_font_override("font", custom_font)
title_label.add_theme_font_size_override("font_size", 18)
title_label.add_theme_color_override("font_color", TEXT_COLOR)
title_label.text = title
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title_label)
return panel
func _create_button(text: String, primary: bool) -> Button:
var button = Button.new()
button.text = text
button.add_theme_font_override("font", custom_font)
button.add_theme_font_size_override("font_size", 16)
button.custom_minimum_size = Vector2(0, 40)
var normal = StyleBoxFlat.new()
var hover = StyleBoxFlat.new()
var pressed = StyleBoxFlat.new()
var disabled = StyleBoxFlat.new()
if primary:
normal.bg_color = ACCENT_COLOR
hover.bg_color = ACCENT_COLOR.lightened(0.15)
pressed.bg_color = ACCENT_COLOR.darkened(0.15)
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("font_hover_color", Color.WHITE)
else:
normal.bg_color = Color(0.25, 0.23, 0.3)
hover.bg_color = Color(0.3, 0.28, 0.35)
pressed.bg_color = Color(0.2, 0.18, 0.25)
button.add_theme_color_override("font_color", TEXT_COLOR)
button.add_theme_color_override("font_hover_color", TEXT_COLOR)
disabled.bg_color = Color(0.2, 0.18, 0.22)
button.add_theme_color_override("font_disabled_color", MUTED_COLOR)
for style in [normal, hover, pressed, disabled]:
style.set_corner_radius_all(6)
style.set_content_margin_all(8)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", pressed)
button.add_theme_stylebox_override("disabled", disabled)
return button
func _connect_network_signals() -> void:
NetworkManager.queue_joined.connect(_on_queue_joined)
NetworkManager.queue_left.connect(_on_queue_left)
NetworkManager.room_created.connect(_on_room_created)
NetworkManager.room_joined.connect(_on_room_joined)
NetworkManager.room_updated.connect(_on_room_updated)
NetworkManager.matchmaking_update.connect(_on_matchmaking_update)
NetworkManager.match_found.connect(_on_match_found)
NetworkManager.network_error.connect(_on_network_error)
func _disconnect_network_signals() -> void:
if NetworkManager.queue_joined.is_connected(_on_queue_joined):
NetworkManager.queue_joined.disconnect(_on_queue_joined)
if NetworkManager.queue_left.is_connected(_on_queue_left):
NetworkManager.queue_left.disconnect(_on_queue_left)
if NetworkManager.room_created.is_connected(_on_room_created):
NetworkManager.room_created.disconnect(_on_room_created)
if NetworkManager.room_joined.is_connected(_on_room_joined):
NetworkManager.room_joined.disconnect(_on_room_joined)
if NetworkManager.room_updated.is_connected(_on_room_updated):
NetworkManager.room_updated.disconnect(_on_room_updated)
if NetworkManager.matchmaking_update.is_connected(_on_matchmaking_update):
NetworkManager.matchmaking_update.disconnect(_on_matchmaking_update)
if NetworkManager.match_found.is_connected(_on_match_found):
NetworkManager.match_found.disconnect(_on_match_found)
if NetworkManager.network_error.is_connected(_on_network_error):
NetworkManager.network_error.disconnect(_on_network_error)
func _update_user_info() -> void:
var user = NetworkManager.current_user
if user.has("username"):
username_label.text = "Welcome, %s!" % user.username
if user.has("stats") and user.stats.has("elo_rating"):
elo_label.text = "ELO: %d" % user.stats.elo_rating
else:
elo_label.text = "ELO: 1000"
func _fetch_decks() -> void:
deck_dropdown.clear()
deck_dropdown.add_item("-- Select a Deck --", 0)
# Add decks from user profile
var user = NetworkManager.current_user
if user.has("decks") and user.decks is Array:
for i in range(user.decks.size()):
var deck = user.decks[i]
deck_dropdown.add_item(deck.name, i + 1)
deck_dropdown.set_item_metadata(i + 1, deck.id)
# Also add local starter decks as fallback
if deck_dropdown.item_count <= 1:
var starter_decks = _get_local_decks()
for i in range(starter_decks.size()):
deck_dropdown.add_item(starter_decks[i].name, i + 1)
deck_dropdown.set_item_metadata(i + 1, "local_%d" % i)
func _get_local_decks() -> Array:
# Load starter decks from local file
var decks = []
var file_path = "res://data/starter_decks.json"
if FileAccess.file_exists(file_path):
var file = FileAccess.open(file_path, FileAccess.READ)
if file:
var json = JSON.new()
var result = json.parse(file.get_as_text())
if result == OK and json.data is Dictionary:
if json.data.has("decks"):
decks = json.data.decks
file.close()
return decks
func _show_error(message: String) -> void:
error_label.text = message
error_label.visible = true
# Auto-hide after 5 seconds
await get_tree().create_timer(5.0).timeout
if is_instance_valid(error_label):
error_label.visible = false
func _update_ui_state() -> void:
# Update button states based on current state
var has_deck = selected_deck_id != ""
# Queue UI
find_match_button.visible = not is_in_queue and not is_in_room
find_match_button.disabled = not has_deck
cancel_search_button.visible = is_in_queue
queue_status_label.visible = is_in_queue
# Private match UI
private_section.visible = not is_in_queue and not is_in_room
create_room_button.disabled = not has_deck
join_room_button.disabled = not has_deck or room_code_input.text.length() != 6
# Room UI
room_section.visible = is_in_room
# Disable ranked section when in room
ranked_section.visible = not is_in_room
# ========== BUTTON HANDLERS ==========
func _on_profile_pressed() -> void:
profile_requested.emit()
func _on_leaderboard_pressed() -> void:
leaderboard_requested.emit()
func _on_back_pressed() -> void:
# Leave queue or room before going back
if is_in_queue:
NetworkManager.leave_queue()
if is_in_room:
NetworkManager.leave_room()
_disconnect_network_signals()
back_pressed.emit()
func _on_deck_selected(index: int) -> void:
if index == 0:
selected_deck_id = ""
else:
selected_deck_id = str(deck_dropdown.get_item_metadata(index))
_update_ui_state()
func _on_room_code_changed(_new_text: String) -> void:
room_code_input.text = room_code_input.text.to_upper()
_update_ui_state()
func _on_find_match_pressed() -> void:
if selected_deck_id == "":
_show_error("Please select a deck first")
return
# Connect to WebSocket if not connected
if NetworkManager.connection_state < NetworkManager.ConnectionState.CONNECTED:
NetworkManager.connect_websocket()
await NetworkManager.connection_state_changed
# Wait a bit for auth
await get_tree().create_timer(0.5).timeout
NetworkManager.join_queue(selected_deck_id)
func _on_cancel_search_pressed() -> void:
NetworkManager.leave_queue()
func _on_create_room_pressed() -> void:
if selected_deck_id == "":
_show_error("Please select a deck first")
return
# Connect to WebSocket if not connected
if NetworkManager.connection_state < NetworkManager.ConnectionState.CONNECTED:
NetworkManager.connect_websocket()
await NetworkManager.connection_state_changed
await get_tree().create_timer(0.5).timeout
NetworkManager.create_room(selected_deck_id)
func _on_join_room_pressed() -> void:
var code = room_code_input.text.strip_edges().to_upper()
if code.length() != 6:
_show_error("Room code must be 6 characters")
return
if selected_deck_id == "":
_show_error("Please select a deck first")
return
# Connect to WebSocket if not connected
if NetworkManager.connection_state < NetworkManager.ConnectionState.CONNECTED:
NetworkManager.connect_websocket()
await NetworkManager.connection_state_changed
await get_tree().create_timer(0.5).timeout
NetworkManager.join_room(code, selected_deck_id)
func _on_copy_code_pressed() -> void:
DisplayServer.clipboard_set(current_room_code)
copy_code_button.text = "Copied!"
await get_tree().create_timer(1.5).timeout
if is_instance_valid(copy_code_button):
copy_code_button.text = "Copy"
func _on_ready_pressed() -> void:
is_ready = not is_ready
ready_button.text = "Not Ready" if is_ready else "Ready"
NetworkManager.set_room_ready(is_ready)
func _on_leave_room_pressed() -> void:
NetworkManager.leave_room()
# ========== NETWORK SIGNAL HANDLERS ==========
func _on_queue_joined() -> void:
is_in_queue = true
queue_start_time = Time.get_ticks_msec() / 1000.0
queue_status_label.text = "Searching... 0:00"
_update_ui_state()
func _on_queue_left() -> void:
is_in_queue = false
_update_ui_state()
func _on_room_created(room_data: Dictionary) -> void:
is_in_room = true
current_room_code = room_data.get("code", "")
_update_room_display(room_data)
_update_ui_state()
func _on_room_joined(room_data: Dictionary) -> void:
is_in_room = true
current_room_code = room_data.get("code", "")
_update_room_display(room_data)
_update_ui_state()
func _on_room_updated(room_data: Dictionary) -> void:
_update_room_display(room_data)
func _update_room_display(room_data: Dictionary) -> void:
room_code_label.text = "Room: %s" % room_data.get("code", "------")
var host = room_data.get("host", {})
var host_ready = "" if host.get("ready", false) else ""
host_label.text = "Host: %s%s" % [host.get("username", "---"), host_ready]
var guest = room_data.get("guest", null)
if guest:
var guest_ready = "" if guest.get("ready", false) else ""
guest_label.text = "Guest: %s%s" % [guest.get("username", "---"), guest_ready]
ready_button.disabled = false
else:
guest_label.text = "Guest: Waiting for opponent..."
ready_button.disabled = true
func _on_matchmaking_update(data: Dictionary) -> void:
var update_type = data.get("type", "")
match update_type:
"queue_left":
is_in_queue = false
_update_ui_state()
"room_left":
is_in_room = false
is_ready = false
ready_button.text = "Ready"
current_room_code = ""
room_code_input.text = ""
_update_ui_state()
var reason = data.get("reason", "")
if reason != "":
_show_error(reason)
func _on_match_found(game_data: Dictionary) -> void:
print("Match found! Game ID: ", game_data.get("game_id", ""))
is_in_queue = false
is_in_room = false
_disconnect_network_signals()
game_starting.emit(game_data)
func _on_network_error(error: String) -> void:
_show_error(error)
# ========== CLEANUP ==========
func _exit_tree() -> void:
_disconnect_network_signals()

494
scripts/ui/ProfileScreen.gd Normal file
View File

@@ -0,0 +1,494 @@
class_name ProfileScreen
extends CanvasLayer
## ProfileScreen - Displays player stats and match history
signal back_pressed
# Window dimensions
const WINDOW_SIZE := Vector2i(600, 700)
# Pagination
const MATCHES_PER_PAGE = 10
# UI Components
var main_container: VBoxContainer
var back_button: Button
var username_label: Label
var stats_container: HBoxContainer
var elo_value: Label
var wins_value: Label
var losses_value: Label
var winrate_value: Label
var games_value: Label
var history_section: PanelContainer
var history_list: VBoxContainer
var pagination_container: HBoxContainer
var prev_button: Button
var page_label: Label
var next_button: Button
var loading_label: Label
var error_label: Label
# State
var current_page: int = 0
var total_matches: int = 0
var is_loading: bool = false
var matches_cache: Array = []
# Styling
var custom_font: Font = preload("res://JimNightshade-Regular.ttf")
const BG_COLOR := Color(0.12, 0.11, 0.15, 1.0)
const PANEL_COLOR := Color(0.18, 0.16, 0.22, 1.0)
const ACCENT_COLOR := Color(0.4, 0.35, 0.55, 1.0)
const TEXT_COLOR := Color(0.9, 0.88, 0.82, 1.0)
const MUTED_COLOR := Color(0.6, 0.58, 0.52, 1.0)
const WIN_COLOR := Color(0.4, 0.8, 0.4, 1.0)
const LOSS_COLOR := Color(0.8, 0.4, 0.4, 1.0)
func _ready() -> void:
_create_ui()
_load_profile()
func _create_ui() -> void:
# Background
var bg = ColorRect.new()
bg.color = BG_COLOR
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(bg)
# Main container
main_container = VBoxContainer.new()
main_container.set_anchors_preset(Control.PRESET_FULL_RECT)
main_container.add_theme_constant_override("separation", 16)
add_child(main_container)
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_top", 16)
margin.add_theme_constant_override("margin_bottom", 16)
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
main_container.add_child(margin)
var content = VBoxContainer.new()
content.add_theme_constant_override("separation", 16)
margin.add_child(content)
# Back button
back_button = _create_button("< Back", false)
back_button.custom_minimum_size = Vector2(80, 32)
back_button.pressed.connect(_on_back_pressed)
content.add_child(back_button)
# Title
var title = Label.new()
title.add_theme_font_override("font", custom_font)
title.add_theme_font_size_override("font_size", 28)
title.add_theme_color_override("font_color", TEXT_COLOR)
title.text = "PLAYER PROFILE"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(title)
# Username
username_label = Label.new()
username_label.add_theme_font_override("font", custom_font)
username_label.add_theme_font_size_override("font_size", 22)
username_label.add_theme_color_override("font_color", ACCENT_COLOR)
username_label.text = "Loading..."
username_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(username_label)
# Stats section
_create_stats_section(content)
# Match history section
_create_history_section(content)
# Error label
error_label = Label.new()
error_label.add_theme_font_override("font", custom_font)
error_label.add_theme_font_size_override("font_size", 14)
error_label.add_theme_color_override("font_color", Color(0.9, 0.3, 0.3))
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD
error_label.visible = false
content.add_child(error_label)
func _create_stats_section(parent: VBoxContainer) -> void:
var stats_panel = _create_panel_section("STATISTICS")
parent.add_child(stats_panel)
stats_container = HBoxContainer.new()
stats_container.add_theme_constant_override("separation", 24)
stats_panel.get_child(0).add_child(stats_container)
# Create stat boxes
var elo_box = _create_stat_box("ELO RATING")
elo_value = elo_box.get_child(1)
stats_container.add_child(elo_box)
var wins_box = _create_stat_box("WINS")
wins_value = wins_box.get_child(1)
stats_container.add_child(wins_box)
var losses_box = _create_stat_box("LOSSES")
losses_value = losses_box.get_child(1)
stats_container.add_child(losses_box)
var winrate_box = _create_stat_box("WIN RATE")
winrate_value = winrate_box.get_child(1)
stats_container.add_child(winrate_box)
var games_box = _create_stat_box("GAMES")
games_value = games_box.get_child(1)
stats_container.add_child(games_box)
func _create_stat_box(title: String) -> VBoxContainer:
var box = VBoxContainer.new()
box.add_theme_constant_override("separation", 4)
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var title_label = Label.new()
title_label.add_theme_font_override("font", custom_font)
title_label.add_theme_font_size_override("font_size", 12)
title_label.add_theme_color_override("font_color", MUTED_COLOR)
title_label.text = title
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(title_label)
var value_label = Label.new()
value_label.add_theme_font_override("font", custom_font)
value_label.add_theme_font_size_override("font_size", 24)
value_label.add_theme_color_override("font_color", TEXT_COLOR)
value_label.text = "-"
value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(value_label)
return box
func _create_history_section(parent: VBoxContainer) -> void:
history_section = _create_panel_section("MATCH HISTORY")
history_section.size_flags_vertical = Control.SIZE_EXPAND_FILL
parent.add_child(history_section)
var content = history_section.get_child(0) as VBoxContainer
# Loading indicator
loading_label = Label.new()
loading_label.add_theme_font_override("font", custom_font)
loading_label.add_theme_font_size_override("font_size", 14)
loading_label.add_theme_color_override("font_color", MUTED_COLOR)
loading_label.text = "Loading..."
loading_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
content.add_child(loading_label)
# Match list
history_list = VBoxContainer.new()
history_list.add_theme_constant_override("separation", 8)
history_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
content.add_child(history_list)
# Pagination
pagination_container = HBoxContainer.new()
pagination_container.add_theme_constant_override("separation", 16)
pagination_container.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_child(pagination_container)
prev_button = _create_button("< Prev", false)
prev_button.custom_minimum_size = Vector2(80, 32)
prev_button.pressed.connect(_on_prev_page)
pagination_container.add_child(prev_button)
page_label = Label.new()
page_label.add_theme_font_override("font", custom_font)
page_label.add_theme_font_size_override("font_size", 14)
page_label.add_theme_color_override("font_color", TEXT_COLOR)
page_label.text = "Page 1"
pagination_container.add_child(page_label)
next_button = _create_button("Next >", false)
next_button.custom_minimum_size = Vector2(80, 32)
next_button.pressed.connect(_on_next_page)
pagination_container.add_child(next_button)
func _create_panel_section(title: String) -> PanelContainer:
var panel = PanelContainer.new()
var style = StyleBoxFlat.new()
style.bg_color = PANEL_COLOR
style.set_corner_radius_all(8)
style.set_content_margin_all(16)
panel.add_theme_stylebox_override("panel", style)
var vbox = VBoxContainer.new()
vbox.add_theme_constant_override("separation", 12)
panel.add_child(vbox)
var title_label = Label.new()
title_label.add_theme_font_override("font", custom_font)
title_label.add_theme_font_size_override("font_size", 18)
title_label.add_theme_color_override("font_color", TEXT_COLOR)
title_label.text = title
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(title_label)
return panel
func _create_button(text: String, primary: bool) -> Button:
var button = Button.new()
button.text = text
button.add_theme_font_override("font", custom_font)
button.add_theme_font_size_override("font_size", 16)
button.custom_minimum_size = Vector2(0, 40)
var normal = StyleBoxFlat.new()
var hover = StyleBoxFlat.new()
var pressed = StyleBoxFlat.new()
var disabled = StyleBoxFlat.new()
if primary:
normal.bg_color = ACCENT_COLOR
hover.bg_color = ACCENT_COLOR.lightened(0.15)
pressed.bg_color = ACCENT_COLOR.darkened(0.15)
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("font_hover_color", Color.WHITE)
else:
normal.bg_color = Color(0.25, 0.23, 0.3)
hover.bg_color = Color(0.3, 0.28, 0.35)
pressed.bg_color = Color(0.2, 0.18, 0.25)
button.add_theme_color_override("font_color", TEXT_COLOR)
button.add_theme_color_override("font_hover_color", TEXT_COLOR)
disabled.bg_color = Color(0.2, 0.18, 0.22)
button.add_theme_color_override("font_disabled_color", MUTED_COLOR)
for style in [normal, hover, pressed, disabled]:
style.set_corner_radius_all(6)
style.set_content_margin_all(8)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", pressed)
button.add_theme_stylebox_override("disabled", disabled)
return button
func _load_profile() -> void:
if not NetworkManager or not NetworkManager.is_authenticated:
_show_error("Not logged in")
return
is_loading = true
loading_label.visible = true
var result = await NetworkManager.get_profile()
if result.success:
_update_profile_display(result.user)
_load_match_history()
else:
_show_error(result.message)
is_loading = false
func _update_profile_display(user: Dictionary) -> void:
username_label.text = user.get("username", "Unknown")
var stats = user.get("stats", {})
elo_value.text = str(stats.get("eloRating", 1000))
wins_value.text = str(stats.get("wins", 0))
losses_value.text = str(stats.get("losses", 0))
games_value.text = str(stats.get("gamesPlayed", 0))
# Calculate win rate
var games_played = stats.get("gamesPlayed", 0)
if games_played > 0:
var win_rate = float(stats.get("wins", 0)) / float(games_played) * 100.0
winrate_value.text = "%.1f%%" % win_rate
else:
winrate_value.text = "N/A"
func _load_match_history() -> void:
loading_label.visible = true
_clear_history_list()
var offset = current_page * MATCHES_PER_PAGE
var result = await NetworkManager.get_match_history(MATCHES_PER_PAGE, offset)
loading_label.visible = false
if result.success:
var matches = result.get("matches", [])
matches_cache = matches
_display_matches(matches)
_update_pagination()
else:
_show_error(result.message)
func _display_matches(matches: Array) -> void:
_clear_history_list()
if matches.is_empty():
var empty_label = Label.new()
empty_label.add_theme_font_override("font", custom_font)
empty_label.add_theme_font_size_override("font_size", 14)
empty_label.add_theme_color_override("font_color", MUTED_COLOR)
empty_label.text = "No matches found"
empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
history_list.add_child(empty_label)
return
for match_data in matches:
var match_row = _create_match_row(match_data)
history_list.add_child(match_row)
func _create_match_row(match_data: Dictionary) -> HBoxContainer:
var row = HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
# Win/Loss indicator
var result_label = Label.new()
result_label.add_theme_font_override("font", custom_font)
result_label.add_theme_font_size_override("font_size", 14)
result_label.custom_minimum_size.x = 50
var is_win = match_data.get("isWin", false)
if is_win:
result_label.text = "WIN"
result_label.add_theme_color_override("font_color", WIN_COLOR)
else:
result_label.text = "LOSS"
result_label.add_theme_color_override("font_color", LOSS_COLOR)
row.add_child(result_label)
# Opponent
var vs_label = Label.new()
vs_label.add_theme_font_override("font", custom_font)
vs_label.add_theme_font_size_override("font_size", 14)
vs_label.add_theme_color_override("font_color", TEXT_COLOR)
vs_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var opponent = match_data.get("player1", "")
var current_username = NetworkManager.current_user.get("username", "")
if opponent == current_username:
opponent = match_data.get("player2", "Unknown")
vs_label.text = "vs %s" % opponent
row.add_child(vs_label)
# Result type
var reason_label = Label.new()
reason_label.add_theme_font_override("font", custom_font)
reason_label.add_theme_font_size_override("font_size", 12)
reason_label.add_theme_color_override("font_color", MUTED_COLOR)
reason_label.custom_minimum_size.x = 80
var result_reason = match_data.get("result", "")
match result_reason:
"damage":
reason_label.text = "Damage"
"deck_out":
reason_label.text = "Deck Out"
"concede":
reason_label.text = "Concede"
"timeout":
reason_label.text = "Timeout"
"disconnect":
reason_label.text = "Disconnect"
_:
reason_label.text = result_reason.capitalize()
row.add_child(reason_label)
# ELO change
var elo_label = Label.new()
elo_label.add_theme_font_override("font", custom_font)
elo_label.add_theme_font_size_override("font_size", 14)
elo_label.custom_minimum_size.x = 60
elo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
var elo_change = match_data.get("eloChange", 0)
if is_win:
elo_label.text = "+%d" % elo_change
elo_label.add_theme_color_override("font_color", WIN_COLOR)
else:
elo_label.text = "-%d" % elo_change
elo_label.add_theme_color_override("font_color", LOSS_COLOR)
row.add_child(elo_label)
# Date
var date_label = Label.new()
date_label.add_theme_font_override("font", custom_font)
date_label.add_theme_font_size_override("font_size", 12)
date_label.add_theme_color_override("font_color", MUTED_COLOR)
date_label.custom_minimum_size.x = 80
date_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
var played_at = match_data.get("playedAt", "")
if played_at != "":
# Parse ISO date and format nicely
date_label.text = _format_date(played_at)
row.add_child(date_label)
return row
func _format_date(iso_date: String) -> String:
# Simple date formatting - extracts date portion
if iso_date.contains("T"):
var parts = iso_date.split("T")
var date_part = parts[0]
var date_components = date_part.split("-")
if date_components.size() >= 3:
return "%s/%s" % [date_components[1], date_components[2]]
return iso_date.left(10)
func _clear_history_list() -> void:
for child in history_list.get_children():
child.queue_free()
func _update_pagination() -> void:
page_label.text = "Page %d" % (current_page + 1)
prev_button.disabled = current_page == 0
# Disable next if we got fewer results than requested (end of data)
next_button.disabled = matches_cache.size() < MATCHES_PER_PAGE
func _show_error(message: String) -> void:
error_label.text = message
error_label.visible = true
await get_tree().create_timer(5.0).timeout
if is_instance_valid(error_label):
error_label.visible = false
func _on_back_pressed() -> void:
back_pressed.emit()
func _on_prev_page() -> void:
if current_page > 0:
current_page -= 1
_load_match_history()
func _on_next_page() -> void:
current_page += 1
_load_match_history()
func refresh() -> void:
current_page = 0
_load_profile()

View File

@@ -0,0 +1,431 @@
class_name RegisterScreen
extends CanvasLayer
## RegisterScreen - Account creation form for online play
signal registration_successful(message: String)
signal login_requested
signal back_pressed
const WINDOW_SIZE := Vector2(400, 600)
# Validation constants
const USERNAME_MIN_LENGTH = 3
const USERNAME_MAX_LENGTH = 32
const PASSWORD_MIN_LENGTH = 8
# UI Components
var background: PanelContainer
var main_vbox: VBoxContainer
var title_label: Label
var email_input: LineEdit
var username_input: LineEdit
var password_input: LineEdit
var confirm_password_input: LineEdit
var register_button: Button
var login_link: Button
var back_button: Button
var error_label: Label
var success_label: Label
# State
var _is_loading: bool = false
func _ready() -> void:
layer = 100
_create_ui()
func _create_ui() -> void:
# Background panel
background = PanelContainer.new()
add_child(background)
background.position = Vector2.ZERO
background.size = WINDOW_SIZE
background.add_theme_stylebox_override("panel", _create_panel_style())
# Main layout with margin
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", 40)
margin.add_theme_constant_override("margin_right", 40)
margin.add_theme_constant_override("margin_top", 25)
margin.add_theme_constant_override("margin_bottom", 25)
main_vbox = VBoxContainer.new()
margin.add_child(main_vbox)
main_vbox.add_theme_constant_override("separation", 10)
# Title
_create_title()
# Registration form
_create_form()
# Messages
_create_message_labels()
# Spacer
var spacer = Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
main_vbox.add_child(spacer)
# Links
_create_links()
# Back button
_create_back_button()
func _create_title() -> void:
title_label = Label.new()
title_label.text = "CREATE ACCOUNT"
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 28)
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)
# Small spacer
var spacer = Control.new()
spacer.custom_minimum_size.y = 10
main_vbox.add_child(spacer)
func _create_form() -> void:
# Email field
var email_label = Label.new()
email_label.text = "Email"
email_label.add_theme_font_size_override("font_size", 15)
email_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
main_vbox.add_child(email_label)
email_input = LineEdit.new()
email_input.placeholder_text = "Enter your email"
email_input.custom_minimum_size = Vector2(0, 38)
_style_input(email_input)
main_vbox.add_child(email_input)
# Username field
var username_label = Label.new()
username_label.text = "Username"
username_label.add_theme_font_size_override("font_size", 15)
username_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
main_vbox.add_child(username_label)
username_input = LineEdit.new()
username_input.placeholder_text = "3-32 characters"
username_input.custom_minimum_size = Vector2(0, 38)
username_input.max_length = USERNAME_MAX_LENGTH
_style_input(username_input)
main_vbox.add_child(username_input)
# Password field
var password_label = Label.new()
password_label.text = "Password"
password_label.add_theme_font_size_override("font_size", 15)
password_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
main_vbox.add_child(password_label)
password_input = LineEdit.new()
password_input.placeholder_text = "At least 8 characters"
password_input.secret = true
password_input.custom_minimum_size = Vector2(0, 38)
_style_input(password_input)
main_vbox.add_child(password_input)
# Confirm password field
var confirm_label = Label.new()
confirm_label.text = "Confirm Password"
confirm_label.add_theme_font_size_override("font_size", 15)
confirm_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.7))
main_vbox.add_child(confirm_label)
confirm_password_input = LineEdit.new()
confirm_password_input.placeholder_text = "Re-enter your password"
confirm_password_input.secret = true
confirm_password_input.custom_minimum_size = Vector2(0, 38)
_style_input(confirm_password_input)
confirm_password_input.text_submitted.connect(_on_input_submitted)
main_vbox.add_child(confirm_password_input)
# Register button
var button_spacer = Control.new()
button_spacer.custom_minimum_size.y = 10
main_vbox.add_child(button_spacer)
register_button = Button.new()
register_button.text = "Create Account"
register_button.custom_minimum_size = Vector2(0, 45)
_style_button(register_button, true)
register_button.pressed.connect(_on_register_pressed)
main_vbox.add_child(register_button)
func _create_message_labels() -> void:
# Error label
error_label = Label.new()
error_label.text = ""
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
error_label.add_theme_font_size_override("font_size", 13)
error_label.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD
error_label.visible = false
main_vbox.add_child(error_label)
# Success label
success_label = Label.new()
success_label.text = ""
success_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
success_label.add_theme_font_size_override("font_size", 13)
success_label.add_theme_color_override("font_color", Color(0.4, 1.0, 0.5))
success_label.autowrap_mode = TextServer.AUTOWRAP_WORD
success_label.visible = false
main_vbox.add_child(success_label)
func _create_links() -> void:
# Login link
login_link = Button.new()
login_link.text = "Already have an account? Login"
login_link.flat = true
login_link.add_theme_font_size_override("font_size", 14)
login_link.add_theme_color_override("font_color", Color(0.6, 0.7, 1.0))
login_link.add_theme_color_override("font_hover_color", Color(0.8, 0.85, 1.0))
login_link.pressed.connect(_on_login_pressed)
main_vbox.add_child(login_link)
func _create_back_button() -> void:
var button_container = HBoxContainer.new()
button_container.alignment = BoxContainer.ALIGNMENT_CENTER
main_vbox.add_child(button_container)
back_button = Button.new()
back_button.text = "Back"
back_button.custom_minimum_size = Vector2(100, 38)
_style_button(back_button, false)
back_button.pressed.connect(_on_back_pressed)
button_container.add_child(back_button)
# ======= STYLING =======
func _create_panel_style() -> StyleBoxFlat:
var style = StyleBoxFlat.new()
style.bg_color = Color(0.08, 0.08, 0.12, 1.0)
style.set_border_width_all(0)
style.set_corner_radius_all(0)
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 _style_input(input: LineEdit) -> 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 = 12
style.content_margin_right = 12
style.content_margin_top = 6
style.content_margin_bottom = 6
input.add_theme_stylebox_override("normal", style)
var focus_style = style.duplicate()
focus_style.border_color = Color(0.7, 0.6, 0.3)
input.add_theme_stylebox_override("focus", focus_style)
input.add_theme_color_override("font_color", Color(0.95, 0.9, 0.8))
input.add_theme_color_override("font_placeholder_color", Color(0.5, 0.5, 0.55))
input.add_theme_font_size_override("font_size", 15)
func _style_button(button: Button, is_primary: bool) -> void:
var style = StyleBoxFlat.new()
if is_primary:
style.bg_color = Color(0.3, 0.25, 0.15)
style.border_color = Color(0.6, 0.5, 0.3)
else:
style.bg_color = Color(0.15, 0.15, 0.2)
style.border_color = Color(0.4, 0.35, 0.25)
style.set_border_width_all(2)
style.set_corner_radius_all(6)
style.content_margin_left = 20
style.content_margin_right = 20
style.content_margin_top = 8
style.content_margin_bottom = 8
button.add_theme_stylebox_override("normal", style)
var hover_style = style.duplicate()
if is_primary:
hover_style.bg_color = Color(0.4, 0.35, 0.2)
hover_style.border_color = Color(0.8, 0.7, 0.4)
else:
hover_style.bg_color = Color(0.2, 0.2, 0.25)
hover_style.border_color = Color(0.5, 0.45, 0.35)
button.add_theme_stylebox_override("hover", hover_style)
var pressed_style = style.duplicate()
pressed_style.bg_color = Color(0.1, 0.1, 0.12)
button.add_theme_stylebox_override("pressed", pressed_style)
var disabled_style = style.duplicate()
disabled_style.bg_color = Color(0.1, 0.1, 0.12)
disabled_style.border_color = Color(0.25, 0.25, 0.3)
button.add_theme_stylebox_override("disabled", disabled_style)
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.45, 0.42, 0.38))
button.add_theme_font_size_override("font_size", 16)
# ======= EVENT HANDLERS =======
func _on_input_submitted(_text: String) -> void:
_on_register_pressed()
func _on_register_pressed() -> void:
if _is_loading:
return
var email = email_input.text.strip_edges()
var username = username_input.text.strip_edges()
var password = password_input.text
var confirm_password = confirm_password_input.text
# Validate inputs
var validation_error = _validate_inputs(email, username, password, confirm_password)
if validation_error != "":
_show_error(validation_error)
return
# Start registration
_set_loading(true)
_hide_messages()
var result = await NetworkManager.register(email, password, username)
_set_loading(false)
if result.success:
_show_success(result.message)
registration_successful.emit(result.message)
else:
_show_error(result.message)
func _on_login_pressed() -> void:
login_requested.emit()
func _on_back_pressed() -> void:
back_pressed.emit()
# ======= VALIDATION =======
func _validate_inputs(email: String, username: String, password: String, confirm_password: String) -> String:
# Email validation
if email.is_empty():
return "Please enter your email"
if not _is_valid_email(email):
return "Please enter a valid email address"
# Username validation
if username.is_empty():
return "Please enter a username"
if username.length() < USERNAME_MIN_LENGTH:
return "Username must be at least %d characters" % USERNAME_MIN_LENGTH
if username.length() > USERNAME_MAX_LENGTH:
return "Username must be at most %d characters" % USERNAME_MAX_LENGTH
if not _is_valid_username(username):
return "Username can only contain letters, numbers, underscores, and hyphens"
# Password validation
if password.is_empty():
return "Please enter a password"
if password.length() < PASSWORD_MIN_LENGTH:
return "Password must be at least %d characters" % PASSWORD_MIN_LENGTH
# Confirm password
if confirm_password != password:
return "Passwords do not match"
return ""
func _is_valid_email(email: String) -> bool:
var regex = RegEx.new()
regex.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
return regex.search(email) != null
func _is_valid_username(username: String) -> bool:
var regex = RegEx.new()
regex.compile("^[a-zA-Z0-9_-]+$")
return regex.search(username) != null
# ======= HELPERS =======
func _show_error(message: String) -> void:
error_label.text = message
error_label.visible = true
success_label.visible = false
func _show_success(message: String) -> void:
success_label.text = message
success_label.visible = true
error_label.visible = false
func _hide_messages() -> void:
error_label.visible = false
success_label.visible = false
func _set_loading(loading: bool) -> void:
_is_loading = loading
register_button.disabled = loading
register_button.text = "Creating Account..." if loading else "Create Account"
email_input.editable = not loading
username_input.editable = not loading
password_input.editable = not loading
confirm_password_input.editable = not loading
func clear_form() -> void:
email_input.text = ""
username_input.text = ""
password_input.text = ""
confirm_password_input.text = ""
_hide_messages()
_set_loading(false)
func focus_email() -> void:
email_input.grab_focus()