From 3dc075854b0982485068c77144ecb87062a314f3 Mon Sep 17 00:00:00 2001 From: Adrien Ufferte Date: Mon, 26 Jan 2026 14:23:59 +0100 Subject: [PATCH] Add data collection to boss games --- resources/user/user_boss_data.gd | 49 +++++++++++++ resources/user/user_boss_data.gd.uid | 1 + sources/minigames/fish/fish_minigame.gd | 31 ++++++++ sources/utils/autoloads/user_data_manager.gd | 74 ++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 resources/user/user_boss_data.gd create mode 100644 resources/user/user_boss_data.gd.uid diff --git a/resources/user/user_boss_data.gd b/resources/user/user_boss_data.gd new file mode 100644 index 00000000..a838419c --- /dev/null +++ b/resources/user/user_boss_data.gd @@ -0,0 +1,49 @@ +class_name UserBossData +extends Resource +## Stores boss session analytics locally. + +signal boss_data_changed() + +# TODO +# Will be used to know which scores need to be synchronized with backend. +# Any score with "timestamp" greater than this variable need to be uploaded. +@export var last_synchronized_at: int = 0 +@export var sessions: Array[Dictionary] = [] + + +func start_session(timestamp: int) -> int: + var session: Dictionary = { + "timestamp": timestamp, + "victory": false, + "answers": [] + } + sessions.append(session) + boss_data_changed.emit() + return sessions.size() - 1 + + +func add_answer(session_index: int, is_real_word: bool, word_length: int, response_time_ms: int, is_correct: bool) -> void: + if session_index < 0 or session_index >= sessions.size(): + Log.warn("UserBossData: Invalid session index %d for add_answer" % session_index) + return + var session: Dictionary = sessions[session_index] + var answers: Array = session.get("answers", []) + answers.append({ + "is_real_word": is_real_word, + "word_length": word_length, + "response_time_ms": response_time_ms, + "is_correct": is_correct + }) + session["answers"] = answers + sessions[session_index] = session + boss_data_changed.emit() + + +func finish_session(session_index: int, victory: bool) -> void: + if session_index < 0 or session_index >= sessions.size(): + Log.warn("UserBossData: Invalid session index %d for finish_session" % session_index) + return + var session: Dictionary = sessions[session_index] + session["victory"] = victory + sessions[session_index] = session + boss_data_changed.emit() diff --git a/resources/user/user_boss_data.gd.uid b/resources/user/user_boss_data.gd.uid new file mode 100644 index 00000000..f6da7b82 --- /dev/null +++ b/resources/user/user_boss_data.gd.uid @@ -0,0 +1 @@ +uid://d3stq1ngt0d2e diff --git a/sources/minigames/fish/fish_minigame.gd b/sources/minigames/fish/fish_minigame.gd index 866d46c5..b280f4ab 100644 --- a/sources/minigames/fish/fish_minigame.gd +++ b/sources/minigames/fish/fish_minigame.gd @@ -17,6 +17,8 @@ var words_to_present_next: Array[String] = [] var progress_gauge_max_margin: float = 0.95 var total_number_of_words: int = 30 var tutorial_count: int = 0 +var _boss_session_index: int = -1 +var _boss_answer_start_ms: int = 0 @onready var fish_start_zone: Control = %FishStartZone @onready var beacon1: SpriteControl = %Beacon1 @@ -94,6 +96,8 @@ func _find_stimuli_and_distractions() -> void: func _start() -> void: super() + if _is_boss_session() and _boss_session_index == -1: + _boss_session_index = UserDataManager.start_boss_session(Time.get_unix_time_from_system()) tween = create_tween() tween.tween_property(path_follow, "progress_ratio", 1, game_duration) tween.finished.connect(_on_time_out) @@ -110,6 +114,8 @@ func _present_next_word() -> void: words_to_present_next = [] label.show() label.text = words_to_present[0] + if _is_boss_session(): + _boss_answer_start_ms = Time.get_ticks_msec() if tutorial_count == 0: var speech: AudioStreamMP3 = Database.load_external_sound(Database.get_kalulu_speech_path(Type.keys()[minigame_name] as String, "intro_test_game_first_word")) minigame_ui.play_kalulu_speech(speech) @@ -145,6 +151,15 @@ func _on_beacon_fish_dropped(is_answered_real: bool) -> void: var response_log: Dictionary = {"word": words_to_present[0], "is_real_answer": is_answered_real} var awaited_response: Dictionary = {"word": words_to_present[0], "is_real_answer": is_really_real} _log_new_response(response_log, awaited_response) + if _is_boss_session() and _boss_session_index >= 0: + var response_time_ms: int = max(0, Time.get_ticks_msec() - _boss_answer_start_ms) + UserDataManager.record_boss_answer( + _boss_session_index, + is_really_real, + words_to_present[0].length(), + response_time_ms, + is_correct + ) if is_correct: if is_answered_real: real_right_fx.play() @@ -213,3 +228,19 @@ func show_adult_block() -> void: func _on_adult_block_unlocked() -> void: await _reset() + + +func _win() -> void: + if _is_boss_session() and _boss_session_index >= 0: + UserDataManager.finish_boss_session(_boss_session_index, true) + await super() + + +func _lose() -> void: + if _is_boss_session() and _boss_session_index >= 0: + UserDataManager.finish_boss_session(_boss_session_index, false) + await super() + + +func _is_boss_session() -> bool: + return is_final_boss or gardens_data.has("boss_gate_lesson") diff --git a/sources/utils/autoloads/user_data_manager.gd b/sources/utils/autoloads/user_data_manager.gd index ac5a5b58..878e3ed1 100644 --- a/sources/utils/autoloads/user_data_manager.gd +++ b/sources/utils/autoloads/user_data_manager.gd @@ -14,6 +14,7 @@ var student: String = "": _load_student_progression() _load_student_remediation() _load_student_confusion_matrix() + _load_student_boss_data() _load_student_difficulty() _load_student_speeches() else: @@ -21,6 +22,7 @@ var student: String = "": _student_difficulty = null _student_remediation = null _student_confusion_matrix = null + _student_boss_data = null _student_speeches = null var student_session_id: int = 0 var _device_settings: DeviceSettings @@ -28,6 +30,7 @@ var teacher_settings: TeacherSettings var student_progression: StudentProgression var _student_remediation: UserRemediation var _student_confusion_matrix: UserConfusionMatrix +var _student_boss_data: UserBossData var _student_difficulty: UserDifficulty var _student_speeches: UserSpeeches var user_database_synchronizer: UserDatabaseSynchronizer @@ -874,6 +877,77 @@ func update_confusion_matrix_gp_scores(confusion_matrix_gp_scores: Dictionary) - #endregion +#region Student Boss Data + +func _get_student_boss_data_path(device: int = 0, student_code: int = 0) -> String: + if student_code == 0: + return get_student_folder().path_join("boss.tres") + elif device == 0 and student_code != 0: + return find_student_dir(student_code).path_join("boss.tres") + else: + var student_path: String ="user://".path_join(_device_settings.teacher).path_join(str(device)).path_join(_device_settings.language).path_join(str(student_code)) + var boss_path: String = student_path.path_join("boss.tres") + return boss_path + + +func _load_student_boss_data() -> void: + var boss_path: String = _get_student_boss_data_path() + Log.trace("UserDataManager: Loading student boss data from " + ProjectSettings.globalize_path(boss_path)) + if FileAccess.file_exists(boss_path): + _student_boss_data = load(boss_path) + + if not _student_boss_data: + _student_boss_data = UserBossData.new() + DirAccess.make_dir_recursive_absolute(get_student_folder()) + _save_student_boss_data() + Log.info("UserDataManager: Created new student boss data at " + ProjectSettings.globalize_path(boss_path)) + else: + Log.trace("UserDataManager: Loaded student boss data from " + ProjectSettings.globalize_path(boss_path)) + _student_boss_data.boss_data_changed.connect(_save_student_boss_data) + + +func _save_student_boss_data() -> void: + var boss_path: String = _get_student_boss_data_path() + Log.trace("UserDataManager: Saving student boss data in " + ProjectSettings.globalize_path(boss_path)) + var error: Error = ResourceSaver.save(_student_boss_data, boss_path) + if error != OK: + Log.error("UserDataManager: Failed to save student boss data to %s. Error: %s" % [boss_path, error_string(error)]) + else: + Log.trace("UserDataManager: Student boss data saved successfully at " + ProjectSettings.globalize_path(boss_path)) + + +func get_student_boss_data(student_code: int) -> UserBossData: + var boss_data_path: String = _get_student_boss_data_path(0, student_code) + if FileAccess.file_exists(boss_data_path): + var student_boss_data: UserBossData + student_boss_data = load(boss_data_path) + return student_boss_data + Log.trace("UserDataManager: Boss data of student code %d not found" % student_code) + return null + + +func start_boss_session(timestamp: int) -> int: + if not _student_boss_data: + Log.warn("UserDataManager: No student boss data for " + str(student)) + return -1 + return _student_boss_data.start_session(timestamp) + + +func record_boss_answer(session_index: int, is_real_word: bool, word_length: int, response_time_ms: int, is_correct: bool) -> void: + if not _student_boss_data: + Log.warn("UserDataManager: No student boss data for " + str(student)) + return + _student_boss_data.add_answer(session_index, is_real_word, word_length, response_time_ms, is_correct) + + +func finish_boss_session(session_index: int, victory: bool) -> void: + if not _student_boss_data: + Log.warn("UserDataManager: No student boss data for " + str(student)) + return + _student_boss_data.finish_session(session_index, victory) + +#endregion + #region Student Difficulty func _get_student_difficulty_path() -> String: