feat: 2.3.2

This commit is contained in:
2026-05-19 17:30:29 +08:00
parent 7ca11c6534
commit 8430d1054e
39 changed files with 6581 additions and 738 deletions
+65 -74
View File
@@ -10,17 +10,19 @@ signal closed
# -------------------------------------------------------------------------
@onready var back_btn := %BackBtn as Button
@onready var refresh_btn := %RefreshBtn as Button
@onready var sync_btn := %SyncBtn 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
@onready var item_template := %ItemTemplate as PanelContainer
# 3D Preview
@onready var character_root := %CharacterRoot as Node3D
@onready var selected_name_label := %SelectedNameLabel as Label
@onready var selected_rank_label := %SelectedRankLabel as Label
@onready var selected_score_label := %SelectedScoreLabel as Label
@onready var selected_avatar_rect := %SelectedAvatarRect as TextureRect
# -------------------------------------------------------------------------
# State
@@ -43,13 +45,15 @@ const AVATAR_TO_CHAR: Array[String] = ["Pip", "Gatot", "Dabro", "Copper"]
func _ready() -> void:
back_btn.pressed.connect(_on_close_pressed)
refresh_btn.pressed.connect(_fetch_leaderboard_data)
sync_btn.pressed.connect(_on_sync_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()
_setup_3d_preview()
if item_template:
item_template.hide()
# Listen to profile and stats changes to keep the panel updated
UserProfileManager.profile_updated.connect(_on_profile_or_stats_changed)
@@ -79,17 +83,6 @@ func _on_close_pressed() -> void:
hide()
emit_signal("closed")
func _on_sync_pressed() -> void:
"""Push the current player's stored stats up to the native Nakama leaderboard."""
if not NakamaManager.session or AuthManager.is_guest:
status_label.text = "Must be logged in to sync"
return
status_label.text = "Syncing your score..."
await UserProfileManager.submit_to_leaderboard()
status_label.text = "Synced! Refreshing..."
await get_tree().create_timer(0.5).timeout
_fetch_leaderboard_data()
# -------------------------------------------------------------------------
# Data
# -------------------------------------------------------------------------
@@ -99,8 +92,10 @@ func _fetch_leaderboard_data() -> void:
return
status_label.text = "Fetching Leaderboard..."
status_label.show()
for child in leaderboard_list.get_children():
child.queue_free()
if child != item_template:
child.queue_free()
# Try native Nakama leaderboard first (fastest, ranked already)
var native_data = await _fetch_native_leaderboard()
@@ -231,90 +226,76 @@ func _update_tab_visuals() -> void:
func _populate_list() -> void:
for child in leaderboard_list.get_children():
child.queue_free()
if child != item_template:
child.queue_free()
if leaderboard_data.size() == 0:
status_label.text = "No players found.\nPlay a match to appear here!"
status_label.show()
return
status_label.hide()
for i in range(leaderboard_data.size()):
var entry = leaderboard_data[i]
_create_leaderboard_item(i + 1, entry, i)
func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void:
var item = PanelContainer.new()
var style = StyleBoxFlat.new()
var item = item_template.duplicate()
item.show()
var style: StyleBoxFlat
if item.has_theme_stylebox_override("panel"):
style = item.get_theme_stylebox("panel").duplicate()
else:
style = StyleBoxFlat.new()
style.bg_color = Color(0.25, 0.3, 0.35, 1.0)
style.set_corner_radius_all(8)
if index == current_selected_index:
# Highlight color for the currently selected player row
style.bg_color = Color(0.25, 0.35, 0.20, 1.0)
elif rank <= 3:
style.bg_color = Color(0.2, 0.2, 0.15, 1.0)
style.bg_color = Color(0.35, 0.45, 0.30, 1.0)
else:
style.bg_color = Color(0.15, 0.15, 0.15, 1.0)
style.bg_color = Color(0.25, 0.3, 0.35, 1.0)
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.add_theme_constant_override("separation", 16)
item.add_child(hbox)
var rank_label = item.get_node("HBoxContainer/RankLabel") as Label
if rank_label:
var rank_suffix = "th"
if rank % 10 == 1 and rank % 100 != 11: rank_suffix = "st"
elif rank % 10 == 2 and rank % 100 != 12: rank_suffix = "nd"
elif rank % 10 == 3 and rank % 100 != 13: rank_suffix = "rd"
rank_label.text = str(rank) + rank_suffix
# Rank
var rank_label = Label.new()
rank_label.text = "#" + str(rank)
rank_label.custom_minimum_size = Vector2(40, 0)
match rank:
1: rank_label.add_theme_color_override("font_color", Color.GOLD)
2: rank_label.add_theme_color_override("font_color", Color.SILVER)
3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE)
_: rank_label.add_theme_color_override("font_color", Color.LIGHT_GRAY)
hbox.add_child(rank_label)
match rank:
1: rank_label.add_theme_color_override("font_color", Color.GOLD)
2: rank_label.add_theme_color_override("font_color", Color.SILVER)
3: rank_label.add_theme_color_override("font_color", Color.DARK_ORANGE)
_: rank_label.add_theme_color_override("font_color", Color.WHITE)
# 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", "")
if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url):
if not avatar_url.is_empty():
print("[Leaderboard] Avatar URL not found or invalid: ", avatar_url)
avatar_url = UserProfileManager.AVATARS[0]
avatar_rect.texture = load(avatar_url)
hbox.add_child(avatar_rect)
var avatar_rect = item.get_node("HBoxContainer/Margin/InnerHBox/AvatarRect") as TextureRect
if avatar_rect:
var avatar_url = entry.get("avatar_url", "")
if avatar_url.is_empty() or not ResourceLoader.exists(avatar_url):
avatar_url = UserProfileManager.AVATARS[0]
avatar_rect.texture = load(avatar_url)
# 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)
var name_label = item.get_node("HBoxContainer/Margin/InnerHBox/NameLabel") as Label
if name_label:
name_label.text = entry.get("display_name", "Unknown")
# Value
var value_label = Label.new()
var color = Color(0.647, 0.996, 0.224, 1)
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)
var value_label = item.get_node("HBoxContainer/ValueLabel") as Label
if value_label:
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))
leaderboard_list.add_child(item)
# Make row clickable to update 3D preview
item.gui_input.connect(func(event: InputEvent):
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
current_selected_index = index
_populate_list() # Re-draw list to apply the new active highlight colors visually
_populate_list()
_show_entry_preview(index)
)
item.mouse_filter = Control.MOUSE_FILTER_STOP
@@ -347,8 +328,18 @@ func _show_entry_preview(index: int) -> void:
var display_name: String = entry.get("display_name", "Unknown")
var rank := index + 1
selected_name_label.text = display_name
selected_rank_label.text = "#%d" % rank
if selected_name_label: selected_name_label.text = display_name
if selected_rank_label: selected_rank_label.text = "Rank #%d" % rank
if selected_score_label:
match current_sort_key:
"high_score": selected_score_label.text = str(entry.get("high_score", 0))
"win_rate": selected_score_label.text = "%.1f%%" % entry.get("win_rate", 0.0)
"games_played": selected_score_label.text = str(entry.get("games_played", 0))
if selected_avatar_rect:
var avatar_url2 = entry.get("avatar_url", "")
if avatar_url2.is_empty() or not ResourceLoader.exists(avatar_url2):
avatar_url2 = UserProfileManager.AVATARS[0]
selected_avatar_rect.texture = load(avatar_url2)
func _update_3d_preview(character_name: String) -> void:
if not character_root: