update
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
extends Node
|
||||
## AuthManager - Centralized authentication handling for Nakama
|
||||
## Supports: Guest (device ID), Email/Password, Social (Google, Apple, Facebook)
|
||||
|
||||
# Signals
|
||||
signal auth_started
|
||||
signal auth_completed(success: bool, user_data: Dictionary)
|
||||
signal auth_failed(error: String)
|
||||
signal session_restored
|
||||
signal logged_out
|
||||
|
||||
# Auth modes
|
||||
enum AuthMode { GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, CUSTOM }
|
||||
|
||||
# User data
|
||||
var current_user: Dictionary = {}
|
||||
var is_authenticated: bool = false
|
||||
var is_guest: bool = false
|
||||
var auth_mode: AuthMode = AuthMode.GUEST
|
||||
|
||||
# Session persistence
|
||||
const SESSION_FILE := "user://auth_session.dat"
|
||||
const CREDENTIALS_FILE := "user://auth_credentials.dat"
|
||||
|
||||
# Encryption key for session storage (replace with your own!)
|
||||
const ENCRYPTION_KEY := "tekton_secret_key_change_me_123"
|
||||
|
||||
func _ready() -> void:
|
||||
# Try to restore session on startup
|
||||
call_deferred("_try_restore_session")
|
||||
|
||||
# =============================================================================
|
||||
# Session Persistence
|
||||
# =============================================================================
|
||||
|
||||
func _try_restore_session() -> void:
|
||||
if not FileAccess.file_exists(SESSION_FILE):
|
||||
print("[AuthManager] No saved session found")
|
||||
return
|
||||
|
||||
var file := FileAccess.open_encrypted_with_pass(SESSION_FILE, FileAccess.READ, ENCRYPTION_KEY)
|
||||
if not file:
|
||||
print("[AuthManager] Could not open session file")
|
||||
return
|
||||
|
||||
var session_data := file.get_var()
|
||||
file.close()
|
||||
|
||||
if not session_data or not session_data is Dictionary:
|
||||
return
|
||||
|
||||
var token: String = session_data.get("token", "")
|
||||
var refresh_token: String = session_data.get("refresh_token", "")
|
||||
var saved_auth_mode: int = session_data.get("auth_mode", AuthMode.GUEST)
|
||||
|
||||
if token.is_empty():
|
||||
return
|
||||
|
||||
print("[AuthManager] Attempting to restore session...")
|
||||
|
||||
# Try to restore the session
|
||||
var session := NakamaClient.restore_session(token)
|
||||
|
||||
if session.is_expired():
|
||||
# Try to refresh
|
||||
if refresh_token:
|
||||
var refreshed := await NakamaManager.client.session_refresh_async(session)
|
||||
if not refreshed.is_exception():
|
||||
session = refreshed
|
||||
_save_session(session, saved_auth_mode)
|
||||
else:
|
||||
print("[AuthManager] Session refresh failed, need to re-login")
|
||||
return
|
||||
else:
|
||||
print("[AuthManager] Session expired, need to re-login")
|
||||
return
|
||||
|
||||
# Session valid, connect
|
||||
NakamaManager.session = session
|
||||
auth_mode = saved_auth_mode as AuthMode
|
||||
is_guest = auth_mode == AuthMode.GUEST
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if socket_success:
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
emit_signal("session_restored")
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
print("[AuthManager] Session restored successfully")
|
||||
|
||||
func _save_session(session: NakamaSession, mode: AuthMode) -> void:
|
||||
var file := FileAccess.open_encrypted_with_pass(SESSION_FILE, FileAccess.WRITE, ENCRYPTION_KEY)
|
||||
if file:
|
||||
file.store_var({
|
||||
"token": session.token,
|
||||
"refresh_token": session.refresh_token,
|
||||
"auth_mode": mode,
|
||||
"user_id": session.user_id
|
||||
})
|
||||
file.close()
|
||||
|
||||
func clear_session() -> void:
|
||||
if FileAccess.file_exists(SESSION_FILE):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(SESSION_FILE))
|
||||
if FileAccess.file_exists(CREDENTIALS_FILE):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(CREDENTIALS_FILE))
|
||||
|
||||
# =============================================================================
|
||||
# Guest Authentication
|
||||
# =============================================================================
|
||||
|
||||
func login_as_guest() -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.GUEST
|
||||
is_guest = true
|
||||
|
||||
# Use device ID for guest auth
|
||||
var device_id := _get_device_id()
|
||||
print("[AuthManager] Guest login with device: ", device_id.substr(0, 8) + "...")
|
||||
|
||||
var session := await NakamaManager.client.authenticate_device_async(device_id, null, true)
|
||||
|
||||
if session.is_exception():
|
||||
var error: String = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
_save_session(session, AuthMode.GUEST)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
func _get_device_id() -> String:
|
||||
# Try to load saved device ID for consistent guest identity
|
||||
var id_file := "user://device_id.txt"
|
||||
if FileAccess.file_exists(id_file):
|
||||
var file := FileAccess.open(id_file, FileAccess.READ)
|
||||
if file:
|
||||
var saved_id := file.get_as_text().strip_edges()
|
||||
file.close()
|
||||
if not saved_id.is_empty():
|
||||
return saved_id
|
||||
|
||||
# Generate new device ID
|
||||
var device_id := OS.get_unique_id()
|
||||
if device_id.is_empty():
|
||||
device_id = str(randi()) + str(Time.get_ticks_msec())
|
||||
|
||||
# Save for future use
|
||||
var file := FileAccess.open(id_file, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(device_id)
|
||||
file.close()
|
||||
|
||||
return device_id
|
||||
|
||||
# =============================================================================
|
||||
# Email/Password Authentication
|
||||
# =============================================================================
|
||||
|
||||
func login_with_email(email: String, password: String, remember: bool = true) -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.EMAIL
|
||||
is_guest = false
|
||||
|
||||
print("[AuthManager] Email login: ", email)
|
||||
|
||||
var session := await NakamaManager.client.authenticate_email_async(email, password, null, false)
|
||||
|
||||
if session.is_exception():
|
||||
var error: String = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
|
||||
if remember:
|
||||
_save_session(session, AuthMode.EMAIL)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
func register_with_email(email: String, password: String, username: String = "") -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.EMAIL
|
||||
is_guest = false
|
||||
|
||||
print("[AuthManager] Registering: ", email)
|
||||
|
||||
# Create account (true = create if not exists)
|
||||
var session := await NakamaManager.client.authenticate_email_async(email, password, username, true)
|
||||
|
||||
if session.is_exception():
|
||||
var error: String = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
_save_session(session, AuthMode.EMAIL)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
# =============================================================================
|
||||
# Social Authentication
|
||||
# =============================================================================
|
||||
|
||||
func login_with_google(id_token: String) -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.GOOGLE
|
||||
is_guest = false
|
||||
|
||||
print("[AuthManager] Google login...")
|
||||
|
||||
var session := await NakamaManager.client.authenticate_google_async(id_token, null, true)
|
||||
|
||||
if session.is_exception():
|
||||
var error: String = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
_save_session(session, AuthMode.GOOGLE)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
func login_with_apple(id_token: String) -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.APPLE
|
||||
is_guest = false
|
||||
|
||||
print("[AuthManager] Apple login...")
|
||||
|
||||
var session := await NakamaManager.client.authenticate_apple_async(id_token, null, true)
|
||||
|
||||
if session.is_exception():
|
||||
var error: String = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
_save_session(session, AuthMode.APPLE)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
func login_with_facebook(access_token: String) -> bool:
|
||||
emit_signal("auth_started")
|
||||
auth_mode = AuthMode.FACEBOOK
|
||||
is_guest = false
|
||||
|
||||
print("[AuthManager] Facebook login...")
|
||||
|
||||
var session := await NakamaManager.client.authenticate_facebook_async(access_token, null, true)
|
||||
|
||||
if session.is_exception():
|
||||
var error: String = session.get_exception().message
|
||||
emit_signal("auth_failed", error)
|
||||
return false
|
||||
|
||||
NakamaManager.session = session
|
||||
_save_session(session, AuthMode.FACEBOOK)
|
||||
|
||||
var socket_success := await _connect_socket()
|
||||
if not socket_success:
|
||||
emit_signal("auth_failed", "Failed to connect to game server")
|
||||
return false
|
||||
|
||||
await _load_user_profile()
|
||||
is_authenticated = true
|
||||
|
||||
emit_signal("auth_completed", true, current_user)
|
||||
return true
|
||||
|
||||
# =============================================================================
|
||||
# Account Linking (Convert Guest to Full Account)
|
||||
# =============================================================================
|
||||
|
||||
func link_email(email: String, password: String) -> bool:
|
||||
if not is_authenticated or not NakamaManager.session:
|
||||
return false
|
||||
|
||||
print("[AuthManager] Linking email to guest account...")
|
||||
|
||||
var result := await NakamaManager.client.link_email_async(NakamaManager.session, email, password)
|
||||
|
||||
if result.is_exception():
|
||||
push_error("[AuthManager] Link failed: " + result.get_exception().message)
|
||||
return false
|
||||
|
||||
is_guest = false
|
||||
auth_mode = AuthMode.EMAIL
|
||||
_save_session(NakamaManager.session, AuthMode.EMAIL)
|
||||
|
||||
print("[AuthManager] Email linked successfully!")
|
||||
return true
|
||||
|
||||
func link_google(id_token: String) -> bool:
|
||||
if not is_authenticated or not NakamaManager.session:
|
||||
return false
|
||||
|
||||
var result := await NakamaManager.client.link_google_async(NakamaManager.session, id_token)
|
||||
|
||||
if result.is_exception():
|
||||
return false
|
||||
|
||||
is_guest = false
|
||||
auth_mode = AuthMode.GOOGLE
|
||||
_save_session(NakamaManager.session, AuthMode.GOOGLE)
|
||||
return true
|
||||
|
||||
# =============================================================================
|
||||
# Logout
|
||||
# =============================================================================
|
||||
|
||||
func logout() -> void:
|
||||
print("[AuthManager] Logging out...")
|
||||
|
||||
if NakamaManager.socket:
|
||||
NakamaManager.socket.close()
|
||||
|
||||
clear_session()
|
||||
|
||||
current_user = {}
|
||||
is_authenticated = false
|
||||
is_guest = false
|
||||
NakamaManager.session = null
|
||||
|
||||
emit_signal("logged_out")
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
func _connect_socket() -> bool:
|
||||
if NakamaManager.socket and NakamaManager.socket.is_connected_to_host():
|
||||
return true
|
||||
|
||||
NakamaManager.socket = Nakama.create_socket_from(NakamaManager.client)
|
||||
var result := await NakamaManager.socket.connect_async(NakamaManager.session)
|
||||
|
||||
if result.is_exception():
|
||||
push_error("[AuthManager] Socket connection failed: " + result.get_exception().message)
|
||||
return false
|
||||
|
||||
# Initialize multiplayer bridge
|
||||
NakamaManager.bridge = NakamaMultiplayerBridge.new(NakamaManager.socket)
|
||||
NakamaManager.bridge.match_joined.connect(NakamaManager._on_bridge_match_joined)
|
||||
NakamaManager.bridge.match_join_error.connect(NakamaManager._on_bridge_match_join_error)
|
||||
multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer)
|
||||
|
||||
return true
|
||||
|
||||
func _load_user_profile() -> void:
|
||||
if not NakamaManager.session:
|
||||
return
|
||||
|
||||
var account := await NakamaManager.client.get_account_async(NakamaManager.session)
|
||||
|
||||
if account.is_exception():
|
||||
push_error("[AuthManager] Failed to load account")
|
||||
return
|
||||
|
||||
current_user = {
|
||||
"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,
|
||||
"email": account.email,
|
||||
"is_guest": is_guest,
|
||||
"auth_mode": auth_mode,
|
||||
"created_at": account.user.create_time
|
||||
}
|
||||
|
||||
print("[AuthManager] User profile loaded: ", current_user.display_name)
|
||||
|
||||
func get_display_name() -> String:
|
||||
return current_user.get("display_name", "Guest")
|
||||
|
||||
func get_user_id() -> String:
|
||||
return current_user.get("user_id", "")
|
||||
|
||||
func is_logged_in() -> bool:
|
||||
return is_authenticated
|
||||
@@ -0,0 +1,270 @@
|
||||
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 := 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 not NakamaManager.session:
|
||||
emit_signal("profile_update_failed", "Not authenticated")
|
||||
return false
|
||||
|
||||
if new_name.strip_edges().is_empty():
|
||||
emit_signal("profile_update_failed", "Display name cannot be empty")
|
||||
return false
|
||||
|
||||
if new_name.length() > 50:
|
||||
emit_signal("profile_update_failed", "Display name too long (max 50 characters)")
|
||||
return false
|
||||
|
||||
var result := 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
|
||||
Reference in New Issue
Block a user