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)
|
||||
|
||||
Reference in New Issue
Block a user