class_name AccountManager extends Node signal profile_changed const ACCOUNT_PATH := "user://void_bastion_account.json" const MIN_DISPLAY_NAME_LENGTH := 3 const MAX_DISPLAY_NAME_LENGTH := 20 const RESERVED_DISPLAY_NAMES := ["ADMIN", "ADMINISTRATOR", "GERGAMES", "MODERATOR", "SUPPORT", "SYSTEM"] var player_id := "" var display_name := "" var primary_provider := "guest" var linked_identities: Array = [] var highscores: Dictionary = {} var pending_submissions: Array = [] var created_at := 0 var storage_enabled := true func _ready() -> void: _load_or_create_profile() func _load_or_create_profile() -> void: if FileAccess.file_exists(ACCOUNT_PATH): var file := FileAccess.open(ACCOUNT_PATH, FileAccess.READ) if file: var data = JSON.parse_string(file.get_as_text()) if data is Dictionary: player_id = str(data.get("player_id", "")) display_name = str(data.get("display_name", "")) primary_provider = str(data.get("primary_provider", "guest")) linked_identities = data.get("linked_identities", []) highscores = data.get("highscores", {}) pending_submissions = data.get("pending_submissions", []) created_at = int(data.get("created_at", 0)) if player_id.is_empty(): _create_guest_profile() if display_name.is_empty(): display_name = "PILOT-%s" % player_id.right(4).to_upper() _save_profile() func _create_guest_profile() -> void: var random_bytes := Crypto.new().generate_random_bytes(16) player_id = "guest_%s" % random_bytes.hex_encode() display_name = "PILOT-%s" % player_id.right(4).to_upper() primary_provider = "guest" linked_identities = [] highscores = {} pending_submissions = [] created_at = int(Time.get_unix_time_from_system()) func _save_profile() -> void: if not storage_enabled: return var file := FileAccess.open(ACCOUNT_PATH, FileAccess.WRITE) if not file: return file.store_string(JSON.stringify({ "player_id": player_id, "display_name": display_name, "primary_provider": primary_provider, "linked_identities": linked_identities, "highscores": highscores, "pending_submissions": pending_submissions, "created_at": created_at })) func record_run(map_index: int, difficulty_index: int, endless: bool, reached_wave: int, score: int) -> bool: var mode := "endless" if endless else "mission" var key := "%d:%d:%s" % [map_index, difficulty_index, mode] var previous: Dictionary = highscores.get(key, {}) if int(previous.get("score", -1)) >= score: return false var record := { "map": map_index, "difficulty": difficulty_index, "mode": mode, "wave": reached_wave, "score": score, "recorded_at": int(Time.get_unix_time_from_system()) } highscores[key] = record pending_submissions.append(record.duplicate(true)) _save_profile() profile_changed.emit() return true func attach_online_identity(online_user_id: String, online_username: String) -> void: if online_user_id.is_empty(): return var identity := {"provider": "nakama", "user_id": online_user_id} if not linked_identities.has(identity): linked_identities.append(identity) if not online_username.is_empty() and display_name.begins_with("PILOT-"): display_name = online_username _save_profile() profile_changed.emit() func normalize_display_name(value: String) -> String: var normalized := " ".join(value.strip_edges().split(" ", false)) return normalized.left(MAX_DISPLAY_NAME_LENGTH) func validate_display_name(value: String) -> String: var normalized := normalize_display_name(value) if normalized.length() < MIN_DISPLAY_NAME_LENGTH: return "Der Spielername muss mindestens %d Zeichen lang sein." % MIN_DISPLAY_NAME_LENGTH if normalized.length() > MAX_DISPLAY_NAME_LENGTH: return "Der Spielername darf höchstens %d Zeichen lang sein." % MAX_DISPLAY_NAME_LENGTH if normalized.to_upper() in RESERVED_DISPLAY_NAMES: return "Dieser Spielername ist reserviert." var allowed_characters := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-ÄÖÜäöüß" for character in normalized: if not allowed_characters.contains(character): return "Erlaubt sind Buchstaben, Zahlen, Leerzeichen, _ und -." return "" func set_display_name(value: String) -> bool: var normalized := normalize_display_name(value) if not validate_display_name(normalized).is_empty(): return false display_name = normalized _save_profile() profile_changed.emit() return true func reset_after_account_deletion() -> void: _create_guest_profile() _save_profile() profile_changed.emit() func mark_submission_complete(record: Dictionary) -> void: for index in range(pending_submissions.size() - 1, -1, -1): var queued = pending_submissions[index] if queued is Dictionary and _same_run(queued, record): pending_submissions.remove_at(index) _save_profile() profile_changed.emit() return func _same_run(left: Dictionary, right: Dictionary) -> bool: return int(left.get("map", -1)) == int(right.get("map", -2)) \ and int(left.get("difficulty", -1)) == int(right.get("difficulty", -2)) \ and str(left.get("mode", "")) == str(right.get("mode", "_")) \ and int(left.get("wave", -1)) == int(right.get("wave", -2)) \ and int(left.get("score", -1)) == int(right.get("score", -2)) func get_map_best_wave(map_index: int, endless := false) -> int: var mode := "endless" if endless else "mission" var result := 0 for difficulty_index in 3: var key := "%d:%d:%s" % [map_index, difficulty_index, mode] var record: Dictionary = highscores.get(key, {}) result = maxi(result, int(record.get("wave", 0))) return result func get_map_best_score(map_index: int, endless := false) -> int: var mode := "endless" if endless else "mission" var result := 0 for difficulty_index in 3: var key := "%d:%d:%s" % [map_index, difficulty_index, mode] var record: Dictionary = highscores.get(key, {}) result = maxi(result, int(record.get("score", 0))) return result func provider_label() -> String: match primary_provider: "apple": return "APPLE / GAME CENTER" "google": return "GOOGLE PLAY GAMES" "email": return "E-MAIL" _: return "GASTPROFIL • NUR DIESES GERÄT" func short_id() -> String: return player_id.right(12).to_upper()