extends PanelContainer signal closed @onready var close_btn := %CloseBtn as Button @onready var sort_score_btn := %SortScoreBtn as Button @onready var sort_win_rate_btn := %SortWinRateBtn as Button @onready var sort_games_btn := %SortGamesBtn as Button @onready var leaderboard_list := %LeaderboardList as VBoxContainer @onready var status_label := %StatusLabel as Label var leaderboard_data: Array = [] var current_sort_key: String = "high_score" func _ready() -> void: close_btn.pressed.connect(_on_close_pressed) sort_score_btn.pressed.connect(func(): _sort_by("high_score")) sort_win_rate_btn.pressed.connect(func(): _sort_by("win_rate")) sort_games_btn.pressed.connect(func(): _sort_by("games_played")) _update_tab_visuals() func show_panel() -> void: show() _fetch_leaderboard_data() func _on_close_pressed() -> void: hide() emit_signal("closed") func _fetch_leaderboard_data() -> void: if not NakamaManager.session: status_label.text = "Not connected to Nakama" return status_label.text = "Loading data..." # Clear existing items for child in leaderboard_list.get_children(): child.queue_free() var payload = JSON.stringify({}) var result = await NakamaManager.client.rpc_async(NakamaManager.session, "get_leaderboard_stats", payload) if result.is_exception(): status_label.text = "Failed to load leaderboard" push_error("[Leaderboard] RPC failed: ", result.get_exception().message) return var response_text = result.payload var json = JSON.new() var error = json.parse(response_text) if error == OK: var data = json.get_data() if data.has("leaderboard"): leaderboard_data = data.leaderboard _calculate_win_rates() status_label.text = "" _sort_by(current_sort_key) else: status_label.text = "No data found" else: status_label.text = "Error parsing data" func _calculate_win_rates() -> void: for entry in leaderboard_data: var played = entry.get("games_played", 0) var won = entry.get("games_won", 0) if played > 0: entry["win_rate"] = float(won) / float(played) * 100.0 else: entry["win_rate"] = 0.0 func _sort_by(key: String) -> void: current_sort_key = key _update_tab_visuals() leaderboard_data.sort_custom(func(a, b): return a.get(key, 0) > b.get(key, 0)) _populate_list() func _update_tab_visuals() -> void: var color_active = Color(0.647, 0.996, 0.224, 1) var color_inactive = Color(0.69, 0.529, 0.357, 1) sort_score_btn.add_theme_color_override("font_color", color_active if current_sort_key == "high_score" else color_inactive) sort_win_rate_btn.add_theme_color_override("font_color", color_active if current_sort_key == "win_rate" else color_inactive) sort_games_btn.add_theme_color_override("font_color", color_active if current_sort_key == "games_played" else color_inactive) func _populate_list() -> void: for child in leaderboard_list.get_children(): child.queue_free() if leaderboard_data.size() == 0: status_label.text = "No players found" return for i in range(leaderboard_data.size()): var entry = leaderboard_data[i] _create_leaderboard_item(i + 1, entry) func _create_leaderboard_item(rank: int, entry: Dictionary) -> void: var item = PanelContainer.new() var style = StyleBoxFlat.new() style.bg_color = Color(0.15, 0.15, 0.15, 1.0) if rank <= 3: style.bg_color = Color(0.2, 0.2, 0.15, 1.0) # Slightly highlight top 3 style.set_corner_radius_all(4) style.content_margin_left = 10 style.content_margin_right = 10 style.content_margin_top = 8 style.content_margin_bottom = 8 item.add_theme_stylebox_override("panel", style) var hbox = HBoxContainer.new() hbox.theme_override_constants.separation = 16 item.add_child(hbox) # Rank var rank_label = Label.new() rank_label.text = "#" + str(rank) rank_label.custom_minimum_size = Vector2(40, 0) if rank == 1: rank_label.add_theme_color_override("font_color", Color.GOLD) elif rank == 2: rank_label.add_theme_color_override("font_color", Color.SILVER) elif rank == 3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE) else: rank_label.add_theme_color_override("font_color", Color.LIGHT_GRAY) hbox.add_child(rank_label) # Avatar var avatar_rect = TextureRect.new() avatar_rect.custom_minimum_size = Vector2(32, 32) avatar_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE var avatar_url = entry.get("avatar_url", UserProfileManager.AVATARS[0]) if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url): avatar_url = UserProfileManager.AVATARS[0] avatar_rect.texture = load(avatar_url) hbox.add_child(avatar_rect) # Name var name_label = Label.new() name_label.text = entry.get("display_name", "Unknown") name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL name_label.add_theme_color_override("font_color", Color.WHITE) hbox.add_child(name_label) # Value based on current sort var value_label = Label.new() var color = Color(0.647, 0.996, 0.224, 1) # TEKTON green match current_sort_key: "high_score": value_label.text = str(entry.get("high_score", 0)) "win_rate": value_label.text = "%.1f%%" % entry.get("win_rate", 0.0) "games_played": value_label.text = str(entry.get("games_played", 0)) value_label.add_theme_color_override("font_color", color) value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT value_label.custom_minimum_size = Vector2(80, 0) hbox.add_child(value_label) leaderboard_list.add_child(item)