extends Node signal profile_changed const SAVE_PATH := "user://profile.json" var profile: Dictionary = { "xp": 0, "level": 1, "current_streak": 0, "best_streak": 0, "badges": [], "completed_question_ids": [], "correct_question_ids": [], "question_wrong_counts": {}, "missed_question_ids": [], "system_stats": {}, "block_history": [], "daily_date": "", "daily_questions_answered": 0, "daily_xp_earned": 0, "daily_goal_questions": 10 } func _ready() -> void: load_profile() func load_profile() -> void: if not FileAccess.file_exists(SAVE_PATH): _ensure_daily_current() save_profile() return var file := FileAccess.open(SAVE_PATH, FileAccess.READ) if file == null: push_error("Unable to open profile save.") return var parsed = JSON.parse_string(file.get_as_text()) if typeof(parsed) == TYPE_DICTIONARY: for key in profile.keys(): if parsed.has(key): profile[key] = parsed[key] _ensure_tracking_fields() _ensure_daily_current() profile_changed.emit() func save_profile() -> void: var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE) if file == null: push_error("Unable to write profile save.") return file.store_string(JSON.stringify(profile, "\t")) func get_profile() -> Dictionary: _ensure_daily_current() return profile.duplicate(true) func apply_block_results(results: Dictionary) -> void: _ensure_daily_current() profile["xp"] = int(profile["xp"]) + int(results.get("xp_earned", 0)) profile["level"] = ProgressionService.level_for_xp(int(profile["xp"])) profile["current_streak"] = int(results.get("ending_streak", profile["current_streak"])) profile["best_streak"] = max(int(profile["best_streak"]), int(profile["current_streak"])) profile["daily_questions_answered"] = int(profile["daily_questions_answered"]) + int(results.get("total", 0)) profile["daily_xp_earned"] = int(profile["daily_xp_earned"]) + int(results.get("xp_earned", 0)) var unlocked_badges: Array = results.get("badges_unlocked", []) for badge in unlocked_badges: if not profile["badges"].has(badge): profile["badges"].append(badge) var answers: Array = results.get("answers", []) for answer in answers: var question: Dictionary = answer.get("question", {}) var question_id := str(question.get("id", "")) if question_id != "" and not profile["completed_question_ids"].has(question_id): profile["completed_question_ids"].append(question_id) _apply_question_tracking(question_id, bool(answer.get("is_correct", false))) var system_name := str(question.get("system", "General")) _apply_system_result(system_name, bool(answer.get("is_correct", false))) var summary: Dictionary = { "timestamp": Time.get_datetime_string_from_system(), "mode": results.get("mode", "Tutor"), "system": results.get("system", "All Systems"), "correct": results.get("correct", 0), "total": results.get("total", 0), "xp_earned": results.get("xp_earned", 0) } profile["block_history"].push_front(summary) if profile["block_history"].size() > 12: profile["block_history"].resize(12) save_profile() profile_changed.emit() func reset_profile() -> void: profile = { "xp": 0, "level": 1, "current_streak": 0, "best_streak": 0, "badges": [], "completed_question_ids": [], "correct_question_ids": [], "question_wrong_counts": {}, "missed_question_ids": [], "system_stats": {}, "block_history": [], "daily_date": _today_string(), "daily_questions_answered": 0, "daily_xp_earned": 0, "daily_goal_questions": 10 } save_profile() profile_changed.emit() func reset_qbank_memory() -> void: _ensure_tracking_fields() profile["completed_question_ids"] = [] profile["correct_question_ids"] = [] profile["question_wrong_counts"] = {} profile["missed_question_ids"] = [] save_profile() profile_changed.emit() func apply_question_results(results: Dictionary) -> void: _ensure_daily_current() profile["xp"] = int(profile["xp"]) + int(results.get("xp_earned", 0)) profile["level"] = ProgressionService.level_for_xp(int(profile["xp"])) profile["current_streak"] = int(results.get("ending_streak", profile["current_streak"])) profile["best_streak"] = max(int(profile["best_streak"]), int(profile["current_streak"])) profile["daily_questions_answered"] = int(profile["daily_questions_answered"]) + int(results.get("total", 0)) profile["daily_xp_earned"] = int(profile["daily_xp_earned"]) + int(results.get("xp_earned", 0)) var unlocked_badges: Array = results.get("badges_unlocked", []) for badge in unlocked_badges: if not profile["badges"].has(badge): profile["badges"].append(badge) var answers: Array = results.get("answers", []) for answer in answers: var question: Dictionary = answer.get("question", {}) var question_id := str(question.get("id", "")) if question_id != "" and not profile["completed_question_ids"].has(question_id): profile["completed_question_ids"].append(question_id) _apply_question_tracking(question_id, bool(answer.get("is_correct", false)), bool(results.get("mastered", true))) _apply_system_result(str(question.get("system", "General")), bool(answer.get("is_correct", false))) save_profile() profile_changed.emit() func record_wrong_attempt(question: Dictionary) -> void: _ensure_daily_current() var question_id := str(question.get("id", "")) _apply_question_tracking(question_id, false, false) save_profile() profile_changed.emit() func _ensure_daily_current() -> void: _ensure_tracking_fields() var today := _today_string() var changed := false if not profile.has("daily_goal_questions"): profile["daily_goal_questions"] = 10 changed = true if str(profile.get("daily_date", "")) != today: profile["daily_date"] = today profile["daily_questions_answered"] = 0 profile["daily_xp_earned"] = 0 changed = true if changed: save_profile() func _ensure_tracking_fields() -> void: if not profile.has("correct_question_ids"): profile["correct_question_ids"] = [] if not profile.has("question_wrong_counts"): profile["question_wrong_counts"] = {} if not profile.has("missed_question_ids"): profile["missed_question_ids"] = [] func _apply_question_tracking(question_id: String, is_correct: bool, mastered: bool = true) -> void: if question_id == "": return if is_correct and mastered: if not profile["correct_question_ids"].has(question_id): profile["correct_question_ids"].append(question_id) elif not is_correct: if not profile["missed_question_ids"].has(question_id): profile["missed_question_ids"].append(question_id) profile["question_wrong_counts"][question_id] = int(profile["question_wrong_counts"].get(question_id, 0)) + 1 func _apply_system_result(system_name: String, is_correct: bool) -> void: if not profile["system_stats"].has(system_name): profile["system_stats"][system_name] = {"seen": 0, "correct": 0} profile["system_stats"][system_name]["seen"] = int(profile["system_stats"][system_name]["seen"]) + 1 if is_correct: profile["system_stats"][system_name]["correct"] = int(profile["system_stats"][system_name]["correct"]) + 1 func _today_string() -> String: var date := Time.get_datetime_dict_from_system() return "%04d-%02d-%02d" % [int(date["year"]), int(date["month"]), int(date["day"])]