443 lines
14 KiB
GDScript
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()
|