feat: update player auth, fix bugs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user