extends Node ## UserProfileManager - Manages user profile data with Nakama storage signal profile_loaded(profile: Dictionary) signal profile_updated signal profile_update_failed(error: String) signal avatar_changed(url: String) # Profile data var profile: Dictionary = {} var stats: Dictionary = {} # Nakama storage collection names const PROFILE_COLLECTION := "profiles" const STATS_COLLECTION := "stats" # Available avatars (predefined) const AVATARS := [ "res://assets/avatars/avatar_default.png", "res://assets/avatars/avatar_warrior.png", "res://assets/avatars/avatar_mage.png", "res://assets/avatars/avatar_rogue.png", "res://assets/avatars/avatar_tank.png", "res://assets/avatars/avatar_healer.png", ] func _ready() -> void: # Connect to auth signals if has_node("/root/AuthManager"): var auth := get_node("/root/AuthManager") auth.auth_completed.connect(_on_auth_completed) auth.logged_out.connect(_on_logged_out) func _on_auth_completed(success: bool, _user_data: Dictionary) -> void: if success: await load_profile() func _on_logged_out() -> void: profile = {} stats = {} # ============================================================================= # Profile Loading # ============================================================================= func load_profile() -> Dictionary: if not NakamaManager.session: push_error("[UserProfileManager] No session available") return {} # First get basic account info var account = await NakamaManager.client.get_account_async(NakamaManager.session) if account.is_exception(): push_error("[UserProfileManager] Failed to load account") return {} profile = { "user_id": account.user.id, "username": account.user.username, "display_name": account.user.display_name if account.user.display_name else account.user.username, "avatar_url": account.user.avatar_url, "avatar_index": 0, "email": account.email, "created_at": account.user.create_time, "online": account.user.online, "bio": "", "country": "", "language": "en" } # Load custom profile data from storage var storage_result = await NakamaManager.client.read_storage_objects_async( NakamaManager.session, [NakamaStorageObjectId.new(PROFILE_COLLECTION, "profile", account.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: profile.merge(stored_data, true) # Load stats await load_stats() emit_signal("profile_loaded", profile) print("[UserProfileManager] Profile loaded: ", profile.display_name) return profile func load_stats() -> Dictionary: if not NakamaManager.session: return {} var user_id: String = NakamaManager.session.user_id var storage_result = await NakamaManager.client.read_storage_objects_async( NakamaManager.session, [NakamaStorageObjectId.new(STATS_COLLECTION, "game_stats", 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 else: # Initialize default stats stats = { "games_played": 0, "games_won": 0, "games_lost": 0, "total_score": 0, "high_score": 0, "play_time_minutes": 0 } return stats # ============================================================================= # Profile Updates # ============================================================================= func update_display_name(new_name: String) -> bool: if new_name.strip_edges().is_empty(): emit_signal("profile_update_failed", "Display name cannot be empty") return false if new_name.length() > 6: emit_signal("profile_update_failed", "Display name too long (max 6 characters)") return false var regex = RegEx.new() regex.compile("^[A-Za-z]+$") if not regex.search(new_name): emit_signal("profile_update_failed", "Name must contain only letters") return false # Allow guest name updates locally if not NakamaManager.session or AuthManager.is_guest: profile["display_name"] = new_name emit_signal("profile_updated") return true var result: NakamaAsyncResult = await NakamaManager.client.update_account_async( NakamaManager.session, null, # username (don't change) new_name # display_name ) if result.is_exception(): emit_signal("profile_update_failed", result.get_exception().message) return false profile["display_name"] = new_name emit_signal("profile_updated") return true func update_avatar(avatar_index: int) -> bool: if avatar_index < 0 or avatar_index >= AVATARS.size(): emit_signal("profile_update_failed", "Invalid avatar index") return false if not NakamaManager.session: emit_signal("profile_update_failed", "Not authenticated") return false # Store avatar in custom profile data profile["avatar_index"] = avatar_index profile["avatar_url"] = AVATARS[avatar_index] var success := await _save_profile_data() if success: emit_signal("avatar_changed", AVATARS[avatar_index]) emit_signal("profile_updated") return success func update_bio(new_bio: String) -> bool: if new_bio.length() > 200: emit_signal("profile_update_failed", "Bio too long (max 200 characters)") return false profile["bio"] = new_bio return await _save_profile_data() func _save_profile_data() -> bool: if not NakamaManager.session: return false var custom_data := { "avatar_index": profile.get("avatar_index", 0), "bio": profile.get("bio", ""), "country": profile.get("country", ""), "language": profile.get("language", "en") } var write_obj := NakamaWriteStorageObject.new( PROFILE_COLLECTION, "profile", 2, # Public read 1, # Owner write JSON.stringify(custom_data), "" # Version (empty = overwrite) ) var result = await NakamaManager.client.write_storage_objects_async( NakamaManager.session, [write_obj] ) if result.is_exception(): emit_signal("profile_update_failed", result.get_exception().message) return false emit_signal("profile_updated") return true # ============================================================================= # Stats Management # ============================================================================= func update_stats(new_stats: Dictionary) -> bool: stats.merge(new_stats, true) if not NakamaManager.session: return false var write_obj := NakamaWriteStorageObject.new( STATS_COLLECTION, "game_stats", 2, # Public read 1, # Owner write JSON.stringify(stats), "" ) var result = await NakamaManager.client.write_storage_objects_async( NakamaManager.session, [write_obj] ) return not result.is_exception() func record_game_result(won: bool, score: int) -> void: stats["games_played"] = stats.get("games_played", 0) + 1 if won: stats["games_won"] = stats.get("games_won", 0) + 1 else: stats["games_lost"] = stats.get("games_lost", 0) + 1 stats["total_score"] = stats.get("total_score", 0) + score if score > stats.get("high_score", 0): stats["high_score"] = score await update_stats(stats) # ============================================================================= # Getters # ============================================================================= func get_display_name() -> String: return profile.get("display_name", "Guest") func get_avatar_url() -> String: var index: int = profile.get("avatar_index", 0) if index >= 0 and index < AVATARS.size(): return AVATARS[index] return AVATARS[0] func get_stats() -> Dictionary: return stats func get_win_rate() -> float: var played: int = stats.get("games_played", 0) if played == 0: return 0.0 return float(stats.get("games_won", 0)) / float(played) * 100.0