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

443 lines
14 KiB
GDScript

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()