feature updates
This commit is contained in:
347
scripts/ui/ChoiceModal.gd
Normal file
347
scripts/ui/ChoiceModal.gd
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
442
scripts/ui/LeaderboardScreen.gd
Normal file
442
scripts/ui/LeaderboardScreen.gd
Normal 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
371
scripts/ui/LoginScreen.gd
Normal 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()
|
||||
@@ -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
661
scripts/ui/OnlineLobby.gd
Normal 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
494
scripts/ui/ProfileScreen.gd
Normal 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()
|
||||
431
scripts/ui/RegisterScreen.gd
Normal file
431
scripts/ui/RegisterScreen.gd
Normal 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()
|
||||
Reference in New Issue
Block a user