diff --git a/scenes/lobby.gd b/scenes/lobby.gd index fde2678..2f9ee61 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -2,6 +2,8 @@ extends Control # UI References - Main Menu @onready var main_menu_panel = $MainMenuPanel +@onready var main_title = $MainMenuPanel/VBoxContainer/TitleContainer/Title +@onready var main_subtitle = $MainMenuPanel/VBoxContainer/TitleContainer/Subtitle @onready var create_room_btn = $MainMenuPanel/VBoxContainer/ButtonSection/CreateRoomBtn @onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn @onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn @@ -116,11 +118,8 @@ func _ready(): # Setup Game Mode specific UI dynamically _create_custom_settings_ui() - # Set player name from profile - if AuthManager.is_guest: - LobbyManager.local_player_name = NameGenerator.generate_guest_name() - else: - LobbyManager.local_player_name = UserProfileManager.get_display_name() + # Initial UI update + _on_profile_updated() # Connect button signals - Main Menu create_room_btn.pressed.connect(_on_create_room_pressed) @@ -208,8 +207,12 @@ func _ready(): NakamaManager.connection_failed.connect(_on_connection_failed) # Connect UserProfileManager signals + UserProfileManager.profile_loaded.connect(func(_p): _on_profile_updated()) UserProfileManager.profile_updated.connect(_on_profile_updated) + # Set initial title if already loaded + _on_profile_updated() + # Show main menu initially _show_panel("main_menu") @@ -239,6 +242,9 @@ func _load_character_textures() -> void: print("[Lobby] Character texture not found: ", tex_path) func _on_server_option_selected(index: int) -> void: + if main_subtitle and server_option: + main_subtitle.text = server_option.get_item_text(index).to_upper() + if index == 0: # Nakama Localhost if server_ip_input: server_ip_input.visible = false @@ -805,10 +811,24 @@ func _on_connection_failed(error_message: String) -> void: func _on_profile_updated() -> void: """Handle profile updates (name/avatar change).""" - var new_name = UserProfileManager.get_display_name() + var display_name: String = "" - # Sync to LobbyManager if we are in a room or just locally - LobbyManager.set_player_name(new_name) + if UserProfileManager.is_profile_loaded: + display_name = UserProfileManager.get_display_name() + elif not AuthManager.is_guest and AuthManager.is_authenticated: + display_name = "LOADING..." + else: + # Is Guest or not logged in yet + if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Guest": + display_name = NameGenerator.generate_guest_name() + else: + display_name = LobbyManager.local_player_name + + if main_title: + main_title.text = display_name + + # Sync to LobbyManager + LobbyManager.set_player_name(display_name) # ============================================================================= # Player Slot Updates diff --git a/scenes/ui/leaderboard_panel.tscn b/scenes/ui/leaderboard_panel.tscn index 4f03895..1715d7f 100644 --- a/scenes/ui/leaderboard_panel.tscn +++ b/scenes/ui/leaderboard_panel.tscn @@ -58,11 +58,18 @@ layout_mode = 2 [node name="BackBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=1330539196] unique_name_in_owner = true -custom_minimum_size = Vector2(44, 44) +custom_minimum_size = Vector2(100, 44) layout_mode = 2 theme_override_fonts/font = ExtResource("3_font") text = "← BACK" +[node name="RefreshBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header"] +unique_name_in_owner = true +custom_minimum_size = Vector2(44, 44) +layout_mode = 2 +tooltip_text = "Refresh Data" +text = "⟳" + [node name="Title" type="Label" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=1037998429] layout_mode = 2 size_flags_horizontal = 3 @@ -175,22 +182,35 @@ root_node = NodePath("../Oldpop") libraries/animation-pack = ExtResource("5_animlib") autoplay = &"animation-pack/idle" -[node name="SelectedPlayerInfo" type="VBoxContainer" parent="MainLayout/RightPanel" unique_id=1592883771] +[node name="SelectedPlayerInfo" type="PanelContainer" parent="MainLayout/RightPanel" unique_id=1592883771] +layout_mode = 2 +size_flags_vertical = 8 + +[node name="InfoMargin" type="MarginContainer" parent="MainLayout/RightPanel/SelectedPlayerInfo"] +layout_mode = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_top = 15 +theme_override_constants/margin_bottom = 15 + +[node name="InfoVBox" type="VBoxContainer" parent="MainLayout/RightPanel/SelectedPlayerInfo/InfoMargin"] layout_mode = 2 theme_override_constants/separation = 4 -[node name="SelectedNameLabel" type="Label" parent="MainLayout/RightPanel/SelectedPlayerInfo" unique_id=1940372038] +[node name="SelectedNameLabel" type="Label" parent="MainLayout/RightPanel/SelectedPlayerInfo/InfoMargin/InfoVBox" unique_id=1940372038] unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(1, 1, 1, 1) theme_override_fonts/font = ExtResource("3_font") -theme_override_font_sizes/font_size = 20 +theme_override_font_sizes/font_size = 24 +text = "PLAYER NAME" horizontal_alignment = 1 -[node name="SelectedRankLabel" type="Label" parent="MainLayout/RightPanel/SelectedPlayerInfo" unique_id=994755781] +[node name="SelectedRankLabel" type="Label" parent="MainLayout/RightPanel/SelectedPlayerInfo/InfoMargin/InfoVBox" unique_id=994755781] unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1) theme_override_fonts/font = ExtResource("3_font") -theme_override_font_sizes/font_size = 14 +theme_override_font_sizes/font_size = 16 +text = "RANK #1" horizontal_alignment = 1 diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index 0049d42..14881bf 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -9,6 +9,7 @@ signal avatar_changed(url: String) # Profile data var profile: Dictionary = {} var stats: Dictionary = {} +var is_profile_loaded: bool = false # Nakama storage collection names const PROFILE_COLLECTION := "profiles" @@ -36,6 +37,7 @@ func _on_auth_completed(success: bool, _user_data: Dictionary) -> void: func _on_logged_out() -> void: profile = {} stats = {} + is_profile_loaded = false # ============================================================================= # Profile Loading @@ -81,6 +83,7 @@ func load_profile() -> Dictionary: # Load stats await load_stats() + is_profile_loaded = true emit_signal("profile_loaded", profile) print("[UserProfileManager] Profile loaded: ", profile.display_name) return profile @@ -257,8 +260,10 @@ func record_game_result(won: bool, score: int) -> void: # Getters # ============================================================================= -func get_display_name() -> String: - return profile.get("display_name", "Guest") +func get_display_name(fallback: String = "Guest") -> String: + if not is_profile_loaded: + return fallback + return profile.get("display_name", fallback) func get_avatar_url() -> String: var index: int = profile.get("avatar_index", 0) diff --git a/scripts/ui/leaderboard_panel.gd b/scripts/ui/leaderboard_panel.gd index a91541e..8cdb8f5 100644 --- a/scripts/ui/leaderboard_panel.gd +++ b/scripts/ui/leaderboard_panel.gd @@ -9,6 +9,7 @@ signal closed # UI References # ------------------------------------------------------------------------- @onready var back_btn := %BackBtn as Button +@onready var refresh_btn := %RefreshBtn 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 @@ -28,7 +29,6 @@ var current_sort_key: String = "high_score" var _anim_player: AnimationPlayer # Maps game character name -> GLB node name in the SubViewport -# Must match the mapping in player.gd's set_character() const CHAR_NODE_MAP: Dictionary = { "Copper": "Oldpop", "Dabro": "Masbro", @@ -40,6 +40,7 @@ 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) 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")) @@ -65,12 +66,12 @@ func _fetch_leaderboard_data() -> void: status_label.text = "Not connected to Nakama" return - status_label.text = "Loading data..." + status_label.text = "Fetching Global Records..." 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) + # Calls the updated RPC that returns ONLY native global_high_score records + var result = await NakamaManager.client.rpc_async(NakamaManager.session, "get_leaderboard_stats", "{}") if result.is_exception(): status_label.text = "Failed to load leaderboard" @@ -90,9 +91,9 @@ func _fetch_leaderboard_data() -> void: if leaderboard_data.size() > 0: _show_entry_preview(0) else: - status_label.text = "No data found" + status_label.text = "No records found" else: - status_label.text = "Error parsing data" + status_label.text = "Error parsing server data" func _calculate_win_rates() -> void: for entry in leaderboard_data: diff --git a/server/nakama/tekton_admin.js b/server/nakama/tekton_admin.js index 9d33d01..ee119c9 100644 --- a/server/nakama/tekton_admin.js +++ b/server/nakama/tekton_admin.js @@ -564,14 +564,14 @@ function rpcAdminUpdateStats(ctx, logger, nk, payload) { } try { - // 1. Update Storage (for legacy support/redundancy) + // 1. Update Storage (priority: game_stats) nk.storageWrite([{ collection: "stats", - key: "stats", + key: "game_stats", userId: targetUserId, value: JSON.stringify(stats), permissionRead: 1, // Public read - permissionWrite: 0 // No one can write (except server) + permissionWrite: 0 }]); // 2. Update Native Leaderboard @@ -586,7 +586,7 @@ function rpcAdminUpdateStats(ctx, logger, nk, payload) { nk.leaderboardRecordWrite("global_high_score", targetUserId, account.user.username, score, subscore, JSON.stringify(metadata)); - logger.info("Stats updated for user " + targetUserId + " by admin " + ctx.userId + " (Storage + Native)"); + logger.info("Stats updated for user " + targetUserId + " by admin " + ctx.userId + " (game_stats + Native)"); return JSON.stringify({ success: true }); } catch (e) { logger.error("Failed to update stats: " + e); @@ -605,12 +605,11 @@ function rpcAdminDeleteStats(ctx, logger, nk, payload) { } try { - // 1. Delete Storage - nk.storageDelete([{ - collection: "stats", - key: "stats", - userId: targetUserId - }]); + // 1. Delete Storage (Both keys) + nk.storageDelete([ + { collection: "stats", key: "stats", userId: targetUserId }, + { collection: "stats", key: "game_stats", userId: targetUserId } + ]); // 2. Delete Native Leaderboard Record nk.leaderboardRecordDelete("global_high_score", targetUserId); @@ -629,25 +628,62 @@ function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) { try { var result = nk.storageList(null, "stats", 100, ""); var statsObjects = result.objects || []; - var count = 0; + var userGroup = {}; // [userId] = { highScore, gamesPlayed, gamesWon, avatar } + // Phase 1: Group and merge for (var i = 0; i < statsObjects.length; i++) { var obj = statsObjects[i]; var userId = obj.userId; - var stats = JSON.parse(obj.value); + var key = obj.key; // "stats" or "game_stats" + var value; try { + value = JSON.parse(obj.value || "{}"); + } catch (jsonErr) { + logger.error("Skipping key " + key + " for user " + userId + " due to corrupt JSON"); + continue; + } + + if (!userGroup[userId]) { + userGroup[userId] = { + high_score: value.high_score || 0, + games_played: value.games_played || 0, + games_won: value.games_won || 0, + avatar_url: "" + }; + } else { + // Merge logic: sum counts, max high score + userGroup[userId].high_score = Math.max(userGroup[userId].high_score, value.high_score || 0); + userGroup[userId].games_played += (value.games_played || 0); + userGroup[userId].games_won += (value.games_won || 0); + } + + // Prioritize avatar from game_stats + if (key === "game_stats" || !userGroup[userId].avatar_url) { + try { + // Try to get avatar from storage value if present, else fallback to account later + if (value.avatar_url) userGroup[userId].avatar_url = value.avatar_url; + } catch (e) {} + } + } + + // Phase 2: Write to native leaderboard + var count = 0; + for (var userId in userGroup) { + try { + var stats = userGroup[userId]; var account = nk.accountGetId(userId); + var metadata = { - games_played: stats.games_played || 0, - games_won: stats.games_won || 0, - avatar_url: account.user.avatarUrl || "" + games_played: stats.games_played, + games_won: stats.games_won, + avatar_url: stats.avatar_url || account.user.avatarUrl || "" }; - nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score || 0, 0, JSON.stringify(metadata)); + nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score, 0, JSON.stringify(metadata)); count++; } catch (inner) { - logger.error("Failed to sync record for " + userId + ": " + inner); + logger.error("Failed to sync merged record for " + userId + ": " + inner); } }