From 222621139eb506c0a59c530102258ad6bf7570d4 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Thu, 9 Apr 2026 00:36:23 +0800 Subject: [PATCH] feat: update player auth, fix bugs --- .../enhanced_gridmap/meshlibrary/default.tres | 2 +- .../models/arena/free_mode/level_water.tres | 2 +- .../tile_slot.tres | 2 +- scenes/ui/leaderboard_panel.tscn | 6 +- scripts/managers/auth_manager.gd | 4 +- scripts/managers/user_profile_manager.gd | 26 ++++++--- scripts/ui/leaderboard_panel.gd | 48 ++++++++++++---- scripts/ui/profile_panel.gd | 9 ++- server/nakama/tekton_admin.js | 55 +++++++++++++++---- 9 files changed, 118 insertions(+), 36 deletions(-) diff --git a/addons/enhanced_gridmap/meshlibrary/default.tres b/addons/enhanced_gridmap/meshlibrary/default.tres index cdb133e..5c1d663 100644 --- a/addons/enhanced_gridmap/meshlibrary/default.tres +++ b/addons/enhanced_gridmap/meshlibrary/default.tres @@ -14,7 +14,7 @@ [ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"] [ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="10_uwjsj"] [ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"] -[ext_resource type="BoxMesh" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"] +[ext_resource type="BoxMesh" uid="uid://dcjdwbffgtutt" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"] [sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"] load_path = "res://.godot/imported/tile_heart.png-deeef50755ca225f028608dfd16900e6.s3tc.ctex" diff --git a/assets/models/arena/free_mode/level_water.tres b/assets/models/arena/free_mode/level_water.tres index faa2af4..15e136b 100644 --- a/assets/models/arena/free_mode/level_water.tres +++ b/assets/models/arena/free_mode/level_water.tres @@ -1,6 +1,6 @@ [gd_resource type="PanoramaSkyMaterial" format=3 uid="uid://bdwxlbar41mxw"] -[ext_resource type="Texture2D" uid="uid://didk5l287jms7" path="res://assets/levels/level_water/sky_sea_01.png" id="1_f0k4a"] +[ext_resource type="Texture2D" uid="uid://dep1ng3aqb2jw" path="res://assets/models/arena/free_mode/sky_sea_01.png" id="1_f0k4a"] [resource] panorama = ExtResource("1_f0k4a") diff --git a/assets/textures/player_board_and_blue_print/tile_slot.tres b/assets/textures/player_board_and_blue_print/tile_slot.tres index babba36..dff17f1 100644 --- a/assets/textures/player_board_and_blue_print/tile_slot.tres +++ b/assets/textures/player_board_and_blue_print/tile_slot.tres @@ -1,4 +1,4 @@ [gd_resource type="CompressedTexture2D" format=3 uid="uid://b41afev4wfdrf"] [resource] -load_path = "res://.godot/imported/tiles_slot.png-6771785cbdb9e12b6d9f880d4743aecf.ctex" +load_path = "res://.godot/imported/tiles_slot.png-e539385cf7c9354d657e81e23ace6c2b.s3tc.ctex" diff --git a/scenes/ui/leaderboard_panel.tscn b/scenes/ui/leaderboard_panel.tscn index 28bf6be..ad47080 100644 --- a/scenes/ui/leaderboard_panel.tscn +++ b/scenes/ui/leaderboard_panel.tscn @@ -65,6 +65,7 @@ text = "← BACK" [node name="RefreshBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=993543919] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(44, 44) layout_mode = 2 tooltip_text = "Refresh Data" @@ -72,10 +73,11 @@ text = "⟳" [node name="SyncBtn" type="Button" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=1452457095] unique_name_in_owner = true -custom_minimum_size = Vector2(120, 44) +custom_minimum_size = Vector2(160, 44) layout_mode = 2 tooltip_text = "Sync your score to the global leaderboard" -text = "↑ Sync Score" +text = "⟳ Refresh +" [node name="Title" type="Label" parent="MainLayout/LeftPanel/LeftVBox/Header" unique_id=1037998429] layout_mode = 2 diff --git a/scripts/managers/auth_manager.gd b/scripts/managers/auth_manager.gd index 7b9652d..edaa803 100644 --- a/scripts/managers/auth_manager.gd +++ b/scripts/managers/auth_manager.gd @@ -105,9 +105,9 @@ func _save_session(session: NakamaSession, mode: AuthMode) -> void: func clear_session() -> void: if FileAccess.file_exists(SESSION_FILE): - DirAccess.remove_absolute(ProjectSettings.globalize_path(SESSION_FILE)) + DirAccess.remove_absolute(SESSION_FILE) if FileAccess.file_exists(CREDENTIALS_FILE): - DirAccess.remove_absolute(ProjectSettings.globalize_path(CREDENTIALS_FILE)) + DirAccess.remove_absolute(CREDENTIALS_FILE) # ============================================================================= # Guest Authentication diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index c4c7204..77abb66 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -44,6 +44,11 @@ func _on_logged_out() -> void: # ============================================================================= func load_profile() -> Dictionary: + # Reset state first to ensure no old account data carries over + profile = {} + stats = {} + is_profile_loaded = false + if not NakamaManager.session: push_error("[UserProfileManager] No session available") return {} @@ -89,11 +94,14 @@ func load_profile() -> Dictionary: # Auto-sync existing score to native Nakama leaderboard in background if stats.get("high_score", 0) > 0 and NakamaManager.session and not AuthManager.is_guest: - _submit_to_leaderboard.call_deferred() + submit_to_leaderboard.call_deferred() return profile func load_stats() -> Dictionary: + # Reset stats first to ensure fresh data for new logins + stats = {} + if not NakamaManager.session: return {} @@ -104,12 +112,16 @@ func load_stats() -> Dictionary: [NakamaStorageObjectId.new(STATS_COLLECTION, "game_stats", user_id)] ) + print("[UserProfileManager] Loading stats for user_id: ", user_id) + if not storage_result.is_exception() and storage_result.objects.size() > 0: var stored_data = JSON.parse_string(storage_result.objects[0].value) if stored_data: stats = stored_data + print("[UserProfileManager] Stats loaded from Nakama: ", stats) else: # Initialize default stats + print("[UserProfileManager] No stats found in Nakama, creating defaults") stats = { "games_played": 0, "games_won": 0, @@ -191,7 +203,7 @@ func update_avatar(avatar_index: int) -> bool: # Immediately update leaderboard with new avatar if stats.get("high_score", 0) > 0: - _submit_to_leaderboard.call_deferred() + submit_to_leaderboard.call_deferred() return success @@ -277,20 +289,20 @@ func record_game_result(won: bool, score: int) -> void: await update_stats(stats) # Also submit to Nakama native leaderboard so global_high_score is populated - await _submit_to_leaderboard() + await submit_to_leaderboard() -func _submit_to_leaderboard() -> void: +func submit_to_leaderboard() -> void: """Submits the current high_score via server RPC (required for authoritative leaderboards).""" if not NakamaManager.session: return - if stats.get("high_score", 0) <= 0: - return + # We allow syncing even with 0 score, so the player appears on the leaderboard with their avatar/loadout var payload = JSON.stringify({ "score": int(stats.get("high_score", 0)), "games_played": int(stats.get("games_played", 0)), "games_won": int(stats.get("games_won", 0)), - "avatar_url": profile.get("avatar_url", "") + "avatar_url": profile.get("avatar_url", ""), + "loadout_character": profile.get("loadout_character", "Copper") }) var result = await NakamaManager.client.rpc_async( diff --git a/scripts/ui/leaderboard_panel.gd b/scripts/ui/leaderboard_panel.gd index 2f09ede..161fce2 100644 --- a/scripts/ui/leaderboard_panel.gd +++ b/scripts/ui/leaderboard_panel.gd @@ -27,6 +27,7 @@ signal closed # ------------------------------------------------------------------------- var leaderboard_data: Array = [] var current_sort_key: String = "high_score" +var current_selected_index: int = 0 var _anim_player: AnimationPlayer # Maps game character name -> GLB node name in the SubViewport @@ -43,6 +44,7 @@ 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")) @@ -74,7 +76,7 @@ func _on_sync_pressed() -> void: status_label.text = "Must be logged in to sync" return status_label.text = "Syncing your score..." - await UserProfileManager._submit_to_leaderboard() + await UserProfileManager.submit_to_leaderboard() status_label.text = "Synced! Refreshing..." await get_tree().create_timer(0.5).timeout _fetch_leaderboard_data() @@ -126,11 +128,14 @@ func _fetch_native_leaderboard() -> Array: var parsed = JSON.parse_string(record.metadata) if parsed is Dictionary: meta = parsed + if record.owner_id == NakamaManager.session.user_id: + print("[Leaderboard] Local player meta: ", meta) data.append({ "user_id": record.owner_id, "display_name": record.username if (record.username and not record.username.is_empty()) else "Unknown", "avatar_url": meta.get("avatar_url", ""), + "loadout_character": meta.get("loadout_character", "Copper"), "high_score": int(record.score), "games_played": int(meta.get("games_played", 0)), "games_won": int(meta.get("games_won", 0)), @@ -174,6 +179,7 @@ func _calculate_win_rates() -> void: # ------------------------------------------------------------------------- func _sort_by(key: String) -> void: current_sort_key = key + current_selected_index = 0 _update_tab_visuals() leaderboard_data.sort_custom(func(a, b): return a.get(key, 0) > b.get(key, 0)) _populate_list() @@ -184,6 +190,11 @@ func _update_tab_visuals() -> void: 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) + + # Make the background of unselected tabs visibly darker via modulate so they contrast against the active yellow background + sort_score_btn.self_modulate = Color(1, 1, 1) if current_sort_key == "high_score" else Color(0.5, 0.5, 0.5) + sort_win_rate_btn.self_modulate = Color(1, 1, 1) if current_sort_key == "win_rate" else Color(0.5, 0.5, 0.5) + sort_games_btn.self_modulate = Color(1, 1, 1) if current_sort_key == "games_played" else Color(0.5, 0.5, 0.5) func _populate_list() -> void: for child in leaderboard_list.get_children(): @@ -200,9 +211,15 @@ func _populate_list() -> void: func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void: var item = PanelContainer.new() var style = StyleBoxFlat.new() - style.bg_color = Color(0.15, 0.15, 0.15, 1.0) - if rank <= 3: + + 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) + else: + style.bg_color = Color(0.15, 0.15, 0.15, 1.0) + style.set_corner_radius_all(4) style.content_margin_left = 10 style.content_margin_right = 10 @@ -229,9 +246,13 @@ func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void: 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]) + + 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) @@ -259,6 +280,8 @@ func _create_leaderboard_item(rank: int, entry: Dictionary, index: int) -> void: # 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 _show_entry_preview(index) ) item.mouse_filter = Control.MOUSE_FILTER_STOP @@ -276,13 +299,16 @@ func _show_entry_preview(index: int) -> void: return var entry = leaderboard_data[index] - # Determine character from avatar_url index - var avatar_url: String = entry.get("avatar_url", "") - var char_name := "Copper" - for i in range(UserProfileManager.AVATARS.size()): - if UserProfileManager.AVATARS[i] == avatar_url: - char_name = AVATAR_TO_CHAR[i] if i < AVATAR_TO_CHAR.size() else "Copper" - break + # Determine character from metadata or avatar_url index + var char_name: String = entry.get("loadout_character", "") + + if char_name.is_empty(): + var avatar_url: String = entry.get("avatar_url", "") + char_name = "Copper" + for i in range(UserProfileManager.AVATARS.size()): + if UserProfileManager.AVATARS[i] == avatar_url: + char_name = AVATAR_TO_CHAR[i] if i < AVATAR_TO_CHAR.size() else "Copper" + break _update_3d_preview(char_name) diff --git a/scripts/ui/profile_panel.gd b/scripts/ui/profile_panel.gd index 4ea075d..fc1c460 100644 --- a/scripts/ui/profile_panel.gd +++ b/scripts/ui/profile_panel.gd @@ -80,6 +80,7 @@ func _connect_signals() -> void: char_right_btn.pressed.connect(func(): _cycle_loadout_char(1)) set_default_btn.pressed.connect(_on_set_default_pressed) + UserProfileManager.profile_loaded.connect(func(p): _on_profile_updated()) UserProfileManager.profile_updated.connect(_on_profile_updated) UserProfileManager.profile_update_failed.connect(_on_profile_update_failed) @@ -172,6 +173,8 @@ func _on_set_default_pressed() -> void: _refresh_loadout_ui() # Persist to storage _save_loadout_to_profile() + # Sync to leaderboard immediately + UserProfileManager.submit_to_leaderboard() func _save_loadout_to_profile() -> void: """Save loadout_character field to Nakama profile storage.""" @@ -336,7 +339,7 @@ func _setup_account_settings_ui() -> void: reset_stats_btn.pressed.connect(func(): var conf = ConfirmationDialog.new() conf.dialog_text = "Are you SURE you want to irreversibly wipe all your stats to 0?" - add_child(conf) + acc_settings_dialog.add_child(conf) conf.popup_centered() conf.confirmed.connect(func(): var r = await NakamaManager.client.rpc_async(NakamaManager.session, "reset_stats", "{}") @@ -376,6 +379,10 @@ func _on_close_pressed() -> void: emit_signal("closed") func show_panel() -> void: + if not UserProfileManager.is_profile_loaded: + status_label.text = "Loading profile from server..." + status_label.add_theme_color_override("font_color", Color.YELLOW) + _load_profile_data() _load_loadout() _check_admin_visibility() diff --git a/server/nakama/tekton_admin.js b/server/nakama/tekton_admin.js index 113174b..4c00052 100644 --- a/server/nakama/tekton_admin.js +++ b/server/nakama/tekton_admin.js @@ -468,6 +468,7 @@ function rpcGetLeaderboardStats(ctx, logger, nk, payload) { username: record.username, display_name: record.displayName || record.username, avatar_url: metadata.avatar_url || "", + loadout_character: metadata.loadout_character || "Copper", high_score: record.score || 0, games_played: metadata.games_played || 0, games_won: metadata.games_won || 0 @@ -492,7 +493,8 @@ function rpcSubmitScore(ctx, logger, nk, payload) { var metadata = { games_played: request.games_played || 0, games_won: request.games_won || 0, - avatar_url: account.user.avatarUrl || request.avatar_url || "" + avatar_url: account.user.avatarUrl || request.avatar_url || "", + loadout_character: request.loadout_character || "Copper" }; nk.leaderboardRecordWrite( "global_high_score", @@ -523,7 +525,6 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) { var userId = obj.userId; var value; try { - // obj.value may already be an object or a string depending on Nakama version value = (typeof obj.value === "string") ? JSON.parse(obj.value) : obj.value; } catch (e) { continue; } if (!value) continue; @@ -533,13 +534,43 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) { high_score: value.high_score || 0, games_played: value.games_played || 0, games_won: value.games_won || 0, - avatar_url: value.avatar_url || "" + avatar_url: value.avatar_url || "", + loadout_character: value.loadout_character || "" }; } else { userGroup[userId].high_score = Math.max(userGroup[userId].high_score, value.high_score || 0); userGroup[userId].games_played = Math.max(userGroup[userId].games_played, value.games_played || 0); userGroup[userId].games_won = Math.max(userGroup[userId].games_won, value.games_won || 0); } + + // Prioritize avatar and character from game_stats or if current is empty + if (obj.key === "game_stats" || !userGroup[userId].avatar_url) { + if (value.avatar_url) userGroup[userId].avatar_url = value.avatar_url; + if (value.loadout_character) userGroup[userId].loadout_character = value.loadout_character; + } + } + + // Phase 2: Read profiles collection to get loadout and avatars! + var profileResult = nk.storageList(null, "profiles", 100, ""); + var profileObjects = profileResult.objects || []; + for (var i = 0; i < profileObjects.length; i++) { + var obj = profileObjects[i]; + var userId = obj.userId; + if (obj.key !== "profile") continue; + var value; + try { + value = (typeof obj.value === "string") ? JSON.parse(obj.value) : obj.value; + } catch (e) { continue; } + + if (!userGroup[userId]) { + userGroup[userId] = { + high_score: 0, games_played: 0, games_won: 0, + avatar_url: "", loadout_character: "" + }; + } + // If the profile has avatar or loadout, merge them. Natively preferred over empty + if (value.avatar_url && !userGroup[userId].avatar_url) userGroup[userId].avatar_url = value.avatar_url; + if (value.loadout_character && !userGroup[userId].loadout_character) userGroup[userId].loadout_character = value.loadout_character; } var count = 0; @@ -551,7 +582,8 @@ function rpcSyncLeaderboard(ctx, logger, nk, payload) { var meta = { games_played: stats.games_played || 0, games_won: stats.games_won || 0, - avatar_url: stats.avatar_url || account.user.avatarUrl || "" + avatar_url: stats.avatar_url || account.user.avatarUrl || "res://assets/graphics/character_selection/sc_characters/sc_copper.png", + loadout_character: stats.loadout_character || "Copper" }; nk.leaderboardRecordWrite("global_high_score", uid, account.user.username, stats.high_score, 0, meta); count++; @@ -724,10 +756,11 @@ function rpcAdminUpdateStats(ctx, logger, nk, payload) { var metadata = { games_played: stats.games_played || 0, games_won: stats.games_won || 0, - avatar_url: account.user.avatarUrl || "" + avatar_url: account.user.avatarUrl || "", + loadout_character: stats.loadout_character || "Copper" }; - nk.leaderboardRecordWrite("global_high_score", targetUserId, account.user.username, score, subscore, JSON.stringify(metadata)); + nk.leaderboardRecordWrite("global_high_score", targetUserId, account.user.username, score, subscore, metadata); logger.info("Stats updated for user " + targetUserId + " by admin " + ctx.userId + " (game_stats + Native)"); return JSON.stringify({ success: true }); @@ -792,7 +825,8 @@ function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) { high_score: value.high_score || 0, games_played: value.games_played || 0, games_won: value.games_won || 0, - avatar_url: "" + avatar_url: "", + loadout_character: value.loadout_character || "Copper" }; } else { // Merge logic: sum counts, max high score @@ -801,11 +835,11 @@ function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) { userGroup[userId].games_won += (value.games_won || 0); } - // Prioritize avatar from game_stats + // Prioritize avatar and character 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; + if (value.loadout_character) userGroup[userId].loadout_character = value.loadout_character; } catch (e) {} } } @@ -820,7 +854,8 @@ function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) { var metadata = { games_played: stats.games_played, games_won: stats.games_won, - avatar_url: stats.avatar_url || account.user.avatarUrl || "" + avatar_url: stats.avatar_url || account.user.avatarUrl || "", + loadout_character: stats.loadout_character || "Copper" }; nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score, 0, metadata);