Add core game retry flow and question stats
This commit is contained in:
@@ -9,10 +9,26 @@ var mode := "Tutor"
|
||||
var system_name := "All Systems"
|
||||
var seconds_remaining := 0.0
|
||||
var block_started := false
|
||||
var session_kind := "block"
|
||||
var core_question_pool: Array = []
|
||||
var core_answer_count := 0
|
||||
var core_score := 0
|
||||
var core_energy := 70
|
||||
var core_target_seconds := 90.0
|
||||
var core_current_target_seconds := 90.0
|
||||
var core_current_had_wrong_attempt := false
|
||||
var current_question_started_at := 0.0
|
||||
var current_question_pause_started_at := 0.0
|
||||
var core_status_message := ""
|
||||
|
||||
const CORE_MIN_TARGET_SECONDS := 45.0
|
||||
const CORE_MAX_TARGET_SECONDS := 150.0
|
||||
const CORE_RETRY_TARGET_SECONDS := 30.0
|
||||
|
||||
func start_block(selected_system: String, block_size: int, selected_mode: String) -> void:
|
||||
system_name = selected_system
|
||||
mode = selected_mode
|
||||
session_kind = "block"
|
||||
active_questions = QuestionBankService.build_block(selected_system, block_size)
|
||||
answers.clear()
|
||||
answers.resize(active_questions.size())
|
||||
@@ -22,6 +38,31 @@ func start_block(selected_system: String, block_size: int, selected_mode: String
|
||||
session_changed.emit()
|
||||
|
||||
|
||||
func start_core_run() -> bool:
|
||||
system_name = "All Systems"
|
||||
mode = "Core"
|
||||
session_kind = "core"
|
||||
active_questions.clear()
|
||||
answers.clear()
|
||||
core_question_pool.clear()
|
||||
current_index = -1
|
||||
seconds_remaining = 0.0
|
||||
block_started = true
|
||||
core_answer_count = 0
|
||||
core_score = 0
|
||||
core_energy = 70
|
||||
core_target_seconds = 90.0
|
||||
core_current_target_seconds = core_target_seconds
|
||||
core_current_had_wrong_attempt = false
|
||||
core_status_message = ""
|
||||
if not advance_core_question():
|
||||
block_started = false
|
||||
core_status_message = "All available questions cleared."
|
||||
session_changed.emit()
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func get_current_question() -> Dictionary:
|
||||
if active_questions.is_empty() or current_index < 0 or current_index >= active_questions.size():
|
||||
return {}
|
||||
@@ -38,13 +79,57 @@ func answer_current(choice_id: String, eliminated: Array, flagged: bool) -> Dict
|
||||
"selected_choice_id": choice_id,
|
||||
"is_correct": choice_id == str(question.get("correct_choice_id", "")),
|
||||
"eliminated_choice_ids": eliminated.duplicate(),
|
||||
"flagged": flagged
|
||||
"flagged": flagged,
|
||||
"elapsed_seconds": get_current_elapsed_seconds(),
|
||||
"target_seconds": core_target_seconds if session_kind == "core" else 0.0
|
||||
}
|
||||
answers[current_index] = answer
|
||||
if session_kind == "core":
|
||||
_apply_core_answer_rewards(answer)
|
||||
session_changed.emit()
|
||||
return answer
|
||||
|
||||
|
||||
func submit_core_attempt(choice_id: String, eliminated: Array, flagged: bool) -> Dictionary:
|
||||
var question := get_current_question()
|
||||
if question.is_empty():
|
||||
return {}
|
||||
|
||||
var answer := {
|
||||
"question": question.duplicate(true),
|
||||
"selected_choice_id": choice_id,
|
||||
"is_correct": choice_id == str(question.get("correct_choice_id", "")),
|
||||
"eliminated_choice_ids": eliminated.duplicate(),
|
||||
"flagged": flagged,
|
||||
"elapsed_seconds": get_current_elapsed_seconds(),
|
||||
"target_seconds": core_current_target_seconds
|
||||
}
|
||||
|
||||
if bool(answer.get("is_correct", false)):
|
||||
var mastered := not core_current_had_wrong_attempt
|
||||
answers[current_index] = answer
|
||||
_apply_core_answer_rewards(answer, mastered)
|
||||
session_changed.emit()
|
||||
return {
|
||||
"answer": answer,
|
||||
"is_correct": true,
|
||||
"final": true,
|
||||
"mastered": mastered
|
||||
}
|
||||
|
||||
ProfileService.record_wrong_attempt(question)
|
||||
core_current_had_wrong_attempt = true
|
||||
_apply_core_wrong_penalty(answer)
|
||||
_start_core_retry_window()
|
||||
session_changed.emit()
|
||||
return {
|
||||
"answer": answer,
|
||||
"is_correct": false,
|
||||
"final": false,
|
||||
"mastered": false
|
||||
}
|
||||
|
||||
|
||||
func move_previous() -> bool:
|
||||
if current_index > 0:
|
||||
current_index -= 1
|
||||
@@ -54,6 +139,8 @@ func move_previous() -> bool:
|
||||
|
||||
|
||||
func move_next() -> bool:
|
||||
if session_kind == "core":
|
||||
return advance_core_question()
|
||||
if current_index + 1 < active_questions.size():
|
||||
current_index += 1
|
||||
session_changed.emit()
|
||||
@@ -70,10 +157,17 @@ func move_to(index: int) -> bool:
|
||||
|
||||
|
||||
func get_progress_text() -> String:
|
||||
if session_kind == "core":
|
||||
return str(core_answer_count + 1)
|
||||
return "%d / %d" % [min(current_index + 1, active_questions.size()), active_questions.size()]
|
||||
|
||||
|
||||
func finish_block() -> Dictionary:
|
||||
if session_kind == "core":
|
||||
block_started = false
|
||||
session_changed.emit()
|
||||
return {}
|
||||
|
||||
var final_answers: Array = []
|
||||
for index in range(active_questions.size()):
|
||||
if answers[index] == null:
|
||||
@@ -102,3 +196,119 @@ func get_answer_for_index(index: int) -> Variant:
|
||||
if index < 0 or index >= answers.size():
|
||||
return null
|
||||
return answers[index]
|
||||
|
||||
|
||||
func advance_core_question() -> bool:
|
||||
var question := _draw_core_question()
|
||||
if question.is_empty():
|
||||
core_status_message = "All available questions cleared."
|
||||
return false
|
||||
active_questions.append(question)
|
||||
answers.append(null)
|
||||
current_index = active_questions.size() - 1
|
||||
current_question_started_at = Time.get_ticks_msec() / 1000.0
|
||||
current_question_pause_started_at = 0.0
|
||||
core_current_target_seconds = core_target_seconds
|
||||
core_current_had_wrong_attempt = false
|
||||
session_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func get_current_elapsed_seconds() -> float:
|
||||
if current_question_started_at <= 0.0:
|
||||
return 0.0
|
||||
var now := current_question_pause_started_at if current_question_pause_started_at > 0.0 else Time.get_ticks_msec() / 1000.0
|
||||
return max(0.0, now - current_question_started_at)
|
||||
|
||||
|
||||
func get_core_timer_ratio() -> float:
|
||||
if session_kind != "core" or core_current_target_seconds <= 0.0:
|
||||
return 1.0
|
||||
var ratio := 1.0 - (get_current_elapsed_seconds() / core_current_target_seconds)
|
||||
return clamp(ratio, 0.0, 1.0)
|
||||
|
||||
|
||||
func pause_current_question_timer() -> void:
|
||||
if session_kind == "core" and current_question_pause_started_at <= 0.0:
|
||||
current_question_pause_started_at = Time.get_ticks_msec() / 1000.0
|
||||
|
||||
|
||||
func resume_current_question_timer() -> void:
|
||||
if session_kind == "core" and current_question_pause_started_at > 0.0:
|
||||
var paused_duration := Time.get_ticks_msec() / 1000.0 - current_question_pause_started_at
|
||||
current_question_started_at += paused_duration
|
||||
current_question_pause_started_at = 0.0
|
||||
|
||||
|
||||
func _draw_core_question() -> Dictionary:
|
||||
if core_question_pool.is_empty():
|
||||
core_question_pool = _build_core_question_pool()
|
||||
core_question_pool.shuffle()
|
||||
if core_question_pool.is_empty():
|
||||
return {}
|
||||
|
||||
var last_id := str(get_current_question().get("id", ""))
|
||||
for index in range(core_question_pool.size()):
|
||||
var candidate: Dictionary = core_question_pool[index]
|
||||
if str(candidate.get("id", "")) != last_id or core_question_pool.size() == 1:
|
||||
core_question_pool.remove_at(index)
|
||||
return candidate.duplicate(true)
|
||||
return {}
|
||||
|
||||
|
||||
func _build_core_question_pool() -> Array:
|
||||
var pool: Array = []
|
||||
var profile := ProfileService.get_profile()
|
||||
var correct_ids: Array = profile.get("correct_question_ids", [])
|
||||
for question in QuestionBankService.get_all_questions():
|
||||
var question_id := str(question.get("id", ""))
|
||||
if question_id == "" or not correct_ids.has(question_id):
|
||||
pool.append(question)
|
||||
return pool
|
||||
|
||||
|
||||
func _start_core_retry_window() -> void:
|
||||
core_current_target_seconds = CORE_RETRY_TARGET_SECONDS
|
||||
current_question_started_at = Time.get_ticks_msec() / 1000.0
|
||||
current_question_pause_started_at = 0.0
|
||||
|
||||
|
||||
func _apply_core_wrong_penalty(_answer: Dictionary) -> void:
|
||||
core_score -= 25
|
||||
core_energy = max(0, core_energy - 14)
|
||||
|
||||
|
||||
func _apply_core_answer_rewards(answer: Dictionary, mastered: bool = true) -> void:
|
||||
core_answer_count += 1
|
||||
var elapsed := float(answer.get("elapsed_seconds", 0.0))
|
||||
var target := core_current_target_seconds
|
||||
if target < CORE_MIN_TARGET_SECONDS:
|
||||
target = CORE_MIN_TARGET_SECONDS
|
||||
var is_correct := bool(answer.get("is_correct", false))
|
||||
var score_delta := 100 if is_correct else 0
|
||||
if elapsed > target:
|
||||
var overage_ratio := (elapsed - target) / target
|
||||
if overage_ratio > 1.0:
|
||||
overage_ratio = 1.0
|
||||
score_delta -= int(round(40.0 * overage_ratio))
|
||||
if not is_correct:
|
||||
score_delta -= 25
|
||||
core_score += score_delta
|
||||
|
||||
if is_correct:
|
||||
core_energy = min(100, core_energy + 8)
|
||||
else:
|
||||
core_energy = max(0, core_energy - 14)
|
||||
if elapsed > target:
|
||||
core_energy = max(0, core_energy - 4)
|
||||
|
||||
if mastered and is_correct and elapsed <= target:
|
||||
core_target_seconds = max(CORE_MIN_TARGET_SECONDS, core_target_seconds * 0.97)
|
||||
elif not mastered or not is_correct or elapsed > target * 1.25:
|
||||
core_target_seconds = min(CORE_MAX_TARGET_SECONDS, core_target_seconds * 1.05)
|
||||
else:
|
||||
core_target_seconds = max(CORE_MIN_TARGET_SECONDS, core_target_seconds * 0.99)
|
||||
|
||||
var results := ProgressionService.calculate_question_rewards(answer, elapsed, target, score_delta)
|
||||
results["mastered"] = mastered
|
||||
ProfileService.apply_question_results(results)
|
||||
|
||||
@@ -11,8 +11,15 @@ var profile: Dictionary = {
|
||||
"best_streak": 0,
|
||||
"badges": [],
|
||||
"completed_question_ids": [],
|
||||
"correct_question_ids": [],
|
||||
"question_wrong_counts": {},
|
||||
"missed_question_ids": [],
|
||||
"system_stats": {},
|
||||
"block_history": []
|
||||
"block_history": [],
|
||||
"daily_date": "",
|
||||
"daily_questions_answered": 0,
|
||||
"daily_xp_earned": 0,
|
||||
"daily_goal_questions": 10
|
||||
}
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -21,6 +28,7 @@ func _ready() -> void:
|
||||
|
||||
func load_profile() -> void:
|
||||
if not FileAccess.file_exists(SAVE_PATH):
|
||||
_ensure_daily_current()
|
||||
save_profile()
|
||||
return
|
||||
|
||||
@@ -34,6 +42,8 @@ func load_profile() -> void:
|
||||
for key in profile.keys():
|
||||
if parsed.has(key):
|
||||
profile[key] = parsed[key]
|
||||
_ensure_tracking_fields()
|
||||
_ensure_daily_current()
|
||||
profile_changed.emit()
|
||||
|
||||
|
||||
@@ -46,14 +56,18 @@ func save_profile() -> void:
|
||||
|
||||
|
||||
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:
|
||||
@@ -66,13 +80,10 @@ func apply_block_results(results: Dictionary) -> void:
|
||||
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"))
|
||||
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 bool(answer.get("is_correct", false)):
|
||||
profile["system_stats"][system_name]["correct"] = int(profile["system_stats"][system_name]["correct"]) + 1
|
||||
_apply_system_result(system_name, bool(answer.get("is_correct", false)))
|
||||
|
||||
var summary: Dictionary = {
|
||||
"timestamp": Time.get_datetime_string_from_system(),
|
||||
@@ -98,8 +109,100 @@ func reset_profile() -> void:
|
||||
"best_streak": 0,
|
||||
"badges": [],
|
||||
"completed_question_ids": [],
|
||||
"correct_question_ids": [],
|
||||
"question_wrong_counts": {},
|
||||
"missed_question_ids": [],
|
||||
"system_stats": {},
|
||||
"block_history": []
|
||||
"block_history": [],
|
||||
"daily_date": _today_string(),
|
||||
"daily_questions_answered": 0,
|
||||
"daily_xp_earned": 0,
|
||||
"daily_goal_questions": 10
|
||||
}
|
||||
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"])]
|
||||
|
||||
@@ -4,6 +4,7 @@ const XP_PER_LEVEL := 500
|
||||
|
||||
var badge_catalog := {
|
||||
"First Block": "Complete your first question block.",
|
||||
"First Run": "Answer your first core mode question.",
|
||||
"Hot Streak": "Answer 5 questions correctly in a row.",
|
||||
"Tenacious": "Complete 10 total questions.",
|
||||
"Sharp Shooter": "Score at least 80% on a block.",
|
||||
@@ -62,6 +63,38 @@ func calculate_block_rewards(answers: Array, mode: String, system_name: String)
|
||||
}
|
||||
|
||||
|
||||
func calculate_question_rewards(answer: Dictionary, elapsed_seconds: float, target_seconds: float, score_delta: int) -> Dictionary:
|
||||
var is_correct := bool(answer.get("is_correct", false))
|
||||
var profile: Dictionary = ProfileService.get_profile()
|
||||
var ending_streak := int(profile.get("current_streak", 0)) + 1 if is_correct else 0
|
||||
var xp := 20
|
||||
if is_correct:
|
||||
xp += 35
|
||||
if elapsed_seconds <= target_seconds:
|
||||
xp += 10
|
||||
elif elapsed_seconds > target_seconds * 1.5:
|
||||
xp = max(10, xp - 10)
|
||||
|
||||
var badges: Array[String] = []
|
||||
_add_badge_if_needed(badges, profile, "First Run", true)
|
||||
_add_badge_if_needed(badges, profile, "Hot Streak", ending_streak >= 5)
|
||||
_add_badge_if_needed(badges, profile, "Tenacious", profile.get("completed_question_ids", []).size() + 1 >= 10)
|
||||
|
||||
return {
|
||||
"mode": "Core",
|
||||
"system": str(answer.get("question", {}).get("system", "All Systems")),
|
||||
"correct": 1 if is_correct else 0,
|
||||
"total": 1,
|
||||
"accuracy": 1.0 if is_correct else 0.0,
|
||||
"xp_earned": xp,
|
||||
"score_delta": score_delta,
|
||||
"ending_streak": ending_streak,
|
||||
"longest_streak": ending_streak,
|
||||
"badges_unlocked": badges,
|
||||
"answers": [answer]
|
||||
}
|
||||
|
||||
|
||||
func _add_badge_if_needed(badges: Array[String], profile: Dictionary, badge_name: String, condition: bool) -> void:
|
||||
if condition and not profile.get("badges", []).has(badge_name):
|
||||
badges.append(badge_name)
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
extends Node
|
||||
|
||||
const DEFAULT_QUESTIONS_API_URL := "https://s3q.ranaze.com/api/questions/search"
|
||||
const RETRY_DELAYS := [2, 5, 10, 20, 30]
|
||||
|
||||
signal questions_changed
|
||||
signal questions_load_failed(message: String)
|
||||
|
||||
var questions: Array = []
|
||||
var request: HTTPRequest
|
||||
var retry_timer: Timer
|
||||
var questions_api_url := DEFAULT_QUESTIONS_API_URL
|
||||
var is_loading := false
|
||||
var last_error := ""
|
||||
var load_state := "idle"
|
||||
var retry_attempt := 0
|
||||
var next_retry_seconds := 0
|
||||
|
||||
func _ready() -> void:
|
||||
questions_api_url = str(ProjectSettings.get_setting("step3/questions_api_url", DEFAULT_QUESTIONS_API_URL))
|
||||
@@ -17,13 +22,24 @@ func _ready() -> void:
|
||||
request.request_completed.connect(_on_api_questions_loaded)
|
||||
add_child(request)
|
||||
|
||||
retry_timer = Timer.new()
|
||||
retry_timer.one_shot = true
|
||||
retry_timer.timeout.connect(_on_retry_timer_timeout)
|
||||
add_child(retry_timer)
|
||||
|
||||
func load_questions_from_api() -> void:
|
||||
|
||||
func load_questions_from_api(reset_retry: bool = true) -> void:
|
||||
if AuthState.access_token.is_empty():
|
||||
_set_load_error("Login required before loading questions.")
|
||||
_set_load_error("Login required before loading questions.", false)
|
||||
return
|
||||
|
||||
if reset_retry:
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
retry_timer.stop()
|
||||
|
||||
is_loading = true
|
||||
load_state = "retrying" if retry_attempt > 0 else "loading"
|
||||
last_error = ""
|
||||
questions.clear()
|
||||
questions_changed.emit()
|
||||
@@ -63,6 +79,31 @@ func _on_api_questions_loaded(result: int, response_code: int, _headers: PackedS
|
||||
|
||||
questions = api_questions
|
||||
last_error = ""
|
||||
load_state = "ready"
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
retry_timer.stop()
|
||||
questions_changed.emit()
|
||||
|
||||
|
||||
func retry_now() -> void:
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
retry_timer.stop()
|
||||
load_questions_from_api(false)
|
||||
|
||||
|
||||
func clear_questions() -> void:
|
||||
if request != null:
|
||||
request.cancel_request()
|
||||
if retry_timer != null:
|
||||
retry_timer.stop()
|
||||
questions.clear()
|
||||
is_loading = false
|
||||
last_error = ""
|
||||
load_state = "idle"
|
||||
retry_attempt = 0
|
||||
next_retry_seconds = 0
|
||||
questions_changed.emit()
|
||||
|
||||
|
||||
@@ -101,14 +142,34 @@ func has_questions() -> bool:
|
||||
return not questions.is_empty()
|
||||
|
||||
|
||||
func _set_load_error(message: String) -> void:
|
||||
func _set_load_error(message: String, schedule_retry: bool = true) -> void:
|
||||
is_loading = false
|
||||
last_error = message
|
||||
push_warning(message)
|
||||
questions_load_failed.emit(message)
|
||||
if schedule_retry and not AuthState.access_token.is_empty():
|
||||
_schedule_retry()
|
||||
else:
|
||||
load_state = "failed"
|
||||
next_retry_seconds = 0
|
||||
questions_changed.emit()
|
||||
|
||||
|
||||
func _schedule_retry() -> void:
|
||||
var delay_index := retry_attempt
|
||||
if delay_index >= RETRY_DELAYS.size():
|
||||
delay_index = RETRY_DELAYS.size() - 1
|
||||
next_retry_seconds = int(RETRY_DELAYS[delay_index])
|
||||
retry_attempt += 1
|
||||
load_state = "retrying"
|
||||
retry_timer.start(float(next_retry_seconds))
|
||||
|
||||
|
||||
func _on_retry_timer_timeout() -> void:
|
||||
next_retry_seconds = 0
|
||||
load_questions_from_api(false)
|
||||
|
||||
|
||||
func _is_valid_question(item: Variant) -> bool:
|
||||
if typeof(item) != TYPE_DICTIONARY:
|
||||
return false
|
||||
@@ -132,6 +193,7 @@ func _normalize_api_question(item: Variant) -> Dictionary:
|
||||
"correct_choice_id": str(item.get("correctChoiceId", item.get("correct_choice_id", ""))),
|
||||
"explanation": str(item.get("explanation", "")),
|
||||
"choice_explanations": item.get("choiceExplanations", item.get("choice_explanations", {})),
|
||||
"choice_selection_percentages": item.get("choiceSelectionPercentages", item.get("choice_selection_percentages", {})),
|
||||
"discipline": str(item.get("discipline", "")),
|
||||
"system": str(item.get("system", "")),
|
||||
"difficulty": int(item.get("difficulty", 1)),
|
||||
|
||||
Reference in New Issue
Block a user