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