Count up question bank answer timer
This commit is contained in:
@@ -20,6 +20,10 @@ var core_current_target_seconds := 90.0
|
|||||||
var core_current_had_wrong_attempt := false
|
var core_current_had_wrong_attempt := false
|
||||||
var current_question_started_at := 0.0
|
var current_question_started_at := 0.0
|
||||||
var current_question_pause_started_at := 0.0
|
var current_question_pause_started_at := 0.0
|
||||||
|
var core_question_total_elapsed_seconds := 0.0
|
||||||
|
var core_question_total_active_since := 0.0
|
||||||
|
var core_question_display_target_seconds := 90.0
|
||||||
|
var core_question_total_frozen := false
|
||||||
var core_status_message := ""
|
var core_status_message := ""
|
||||||
var simulation_block_index := 0
|
var simulation_block_index := 0
|
||||||
var simulation_total_blocks := 12
|
var simulation_total_blocks := 12
|
||||||
@@ -62,6 +66,10 @@ func start_core_run() -> bool:
|
|||||||
core_target_seconds = 90.0
|
core_target_seconds = 90.0
|
||||||
core_current_target_seconds = core_target_seconds
|
core_current_target_seconds = core_target_seconds
|
||||||
core_current_had_wrong_attempt = false
|
core_current_had_wrong_attempt = false
|
||||||
|
core_question_total_elapsed_seconds = 0.0
|
||||||
|
core_question_total_active_since = 0.0
|
||||||
|
core_question_display_target_seconds = core_target_seconds
|
||||||
|
core_question_total_frozen = false
|
||||||
core_status_message = ""
|
core_status_message = ""
|
||||||
if not advance_core_question():
|
if not advance_core_question():
|
||||||
block_started = false
|
block_started = false
|
||||||
@@ -161,6 +169,7 @@ func submit_core_attempt(choice_id: String, eliminated: Array, flagged: bool) ->
|
|||||||
if bool(answer.get("is_correct", false)):
|
if bool(answer.get("is_correct", false)):
|
||||||
var mastered := not core_current_had_wrong_attempt
|
var mastered := not core_current_had_wrong_attempt
|
||||||
answers[current_index] = answer
|
answers[current_index] = answer
|
||||||
|
_freeze_core_question_total_timer()
|
||||||
_apply_core_answer_rewards(answer, mastered)
|
_apply_core_answer_rewards(answer, mastered)
|
||||||
session_changed.emit()
|
session_changed.emit()
|
||||||
return {
|
return {
|
||||||
@@ -311,6 +320,10 @@ func append_core_question(question: Dictionary) -> bool:
|
|||||||
current_question_started_at = Time.get_ticks_msec() / 1000.0
|
current_question_started_at = Time.get_ticks_msec() / 1000.0
|
||||||
current_question_pause_started_at = 0.0
|
current_question_pause_started_at = 0.0
|
||||||
core_current_target_seconds = core_target_seconds
|
core_current_target_seconds = core_target_seconds
|
||||||
|
core_question_display_target_seconds = core_current_target_seconds
|
||||||
|
core_question_total_elapsed_seconds = 0.0
|
||||||
|
core_question_total_active_since = current_question_started_at
|
||||||
|
core_question_total_frozen = false
|
||||||
core_current_had_wrong_attempt = false
|
core_current_had_wrong_attempt = false
|
||||||
session_changed.emit()
|
session_changed.emit()
|
||||||
return true
|
return true
|
||||||
@@ -339,6 +352,21 @@ func get_core_timer_ratio() -> float:
|
|||||||
return clamp(ratio, 0.0, 1.0)
|
return clamp(ratio, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func get_core_question_total_elapsed_seconds() -> float:
|
||||||
|
if session_kind != "core":
|
||||||
|
return 0.0
|
||||||
|
var elapsed := core_question_total_elapsed_seconds
|
||||||
|
if core_question_total_active_since > 0.0:
|
||||||
|
elapsed += Time.get_ticks_msec() / 1000.0 - core_question_total_active_since
|
||||||
|
return maxf(0.0, elapsed)
|
||||||
|
|
||||||
|
|
||||||
|
func get_core_question_elapsed_ratio() -> float:
|
||||||
|
if session_kind != "core" or core_question_display_target_seconds <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return clampf(get_core_question_total_elapsed_seconds() / core_question_display_target_seconds, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
func get_session_timer_ratio() -> float:
|
func get_session_timer_ratio() -> float:
|
||||||
if session_kind == "core":
|
if session_kind == "core":
|
||||||
return get_core_timer_ratio()
|
return get_core_timer_ratio()
|
||||||
@@ -349,14 +377,31 @@ func get_session_timer_ratio() -> float:
|
|||||||
|
|
||||||
func pause_current_question_timer() -> void:
|
func pause_current_question_timer() -> void:
|
||||||
if session_kind == "core" and current_question_pause_started_at <= 0.0:
|
if session_kind == "core" and current_question_pause_started_at <= 0.0:
|
||||||
current_question_pause_started_at = Time.get_ticks_msec() / 1000.0
|
var now := Time.get_ticks_msec() / 1000.0
|
||||||
|
current_question_pause_started_at = now
|
||||||
|
_pause_core_question_total_timer(now)
|
||||||
|
|
||||||
|
|
||||||
func resume_current_question_timer() -> void:
|
func resume_current_question_timer() -> void:
|
||||||
if session_kind == "core" and current_question_pause_started_at > 0.0:
|
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
|
var now := Time.get_ticks_msec() / 1000.0
|
||||||
|
var paused_duration := now - current_question_pause_started_at
|
||||||
current_question_started_at += paused_duration
|
current_question_started_at += paused_duration
|
||||||
current_question_pause_started_at = 0.0
|
current_question_pause_started_at = 0.0
|
||||||
|
if not core_question_total_frozen:
|
||||||
|
core_question_total_active_since = now
|
||||||
|
|
||||||
|
|
||||||
|
func _pause_core_question_total_timer(now: float) -> void:
|
||||||
|
if core_question_total_active_since <= 0.0:
|
||||||
|
return
|
||||||
|
core_question_total_elapsed_seconds += maxf(0.0, now - core_question_total_active_since)
|
||||||
|
core_question_total_active_since = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _freeze_core_question_total_timer() -> void:
|
||||||
|
_pause_core_question_total_timer(Time.get_ticks_msec() / 1000.0)
|
||||||
|
core_question_total_frozen = true
|
||||||
|
|
||||||
|
|
||||||
func _draw_core_question() -> Dictionary:
|
func _draw_core_question() -> Dictionary:
|
||||||
|
|||||||
@@ -986,8 +986,7 @@ func _load_question() -> void:
|
|||||||
timer_footer.visible = show_timer
|
timer_footer.visible = show_timer
|
||||||
if show_timer:
|
if show_timer:
|
||||||
if BlockSessionService.session_kind == "core":
|
if BlockSessionService.session_kind == "core":
|
||||||
var core_seconds := int(ceil(BlockSessionService.core_current_target_seconds - BlockSessionService.get_current_elapsed_seconds()))
|
_set_timer_display(int(floor(BlockSessionService.get_core_question_total_elapsed_seconds())))
|
||||||
_set_timer_display(maxi(0, core_seconds))
|
|
||||||
else:
|
else:
|
||||||
_set_timer_display(int(ceil(BlockSessionService.seconds_remaining)))
|
_set_timer_display(int(ceil(BlockSessionService.seconds_remaining)))
|
||||||
_update_timer_bar()
|
_update_timer_bar()
|
||||||
@@ -1297,9 +1296,7 @@ func _show_answer_review() -> void:
|
|||||||
_show_references(BlockSessionService.get_current_question().get("references", []))
|
_show_references(BlockSessionService.get_current_question().get("references", []))
|
||||||
_ensure_answer_review_followups()
|
_ensure_answer_review_followups()
|
||||||
_render_answer_review_sections()
|
_render_answer_review_sections()
|
||||||
timer_bar.visible = false
|
_set_answer_review_timer_visibility()
|
||||||
timer_label.visible = false
|
|
||||||
timer_footer.visible = false
|
|
||||||
next_button.text = "Next"
|
next_button.text = "Next"
|
||||||
next_button.disabled = false
|
next_button.disabled = false
|
||||||
if skip_button != null:
|
if skip_button != null:
|
||||||
@@ -1309,6 +1306,15 @@ func _show_answer_review() -> void:
|
|||||||
question_scroll.call_deferred("ensure_control_visible", explanation_label)
|
question_scroll.call_deferred("ensure_control_visible", explanation_label)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_answer_review_timer_visibility() -> void:
|
||||||
|
var keep_core_timer := BlockSessionService.session_kind == "core"
|
||||||
|
timer_bar.visible = keep_core_timer
|
||||||
|
timer_label.visible = keep_core_timer
|
||||||
|
timer_footer.visible = keep_core_timer
|
||||||
|
if keep_core_timer:
|
||||||
|
_update_timer_bar()
|
||||||
|
|
||||||
|
|
||||||
func _show_explanation() -> void:
|
func _show_explanation() -> void:
|
||||||
var question := BlockSessionService.get_current_question()
|
var question := BlockSessionService.get_current_question()
|
||||||
for button in choice_buttons:
|
for button in choice_buttons:
|
||||||
@@ -1619,13 +1625,21 @@ func _exit_run() -> void:
|
|||||||
func _update_timer_bar() -> void:
|
func _update_timer_bar() -> void:
|
||||||
if timer_bar == null:
|
if timer_bar == null:
|
||||||
return
|
return
|
||||||
var ratio := BlockSessionService.get_session_timer_ratio()
|
var is_core_timer := BlockSessionService.session_kind == "core"
|
||||||
|
var ratio := BlockSessionService.get_core_question_elapsed_ratio() if is_core_timer else BlockSessionService.get_session_timer_ratio()
|
||||||
timer_bar.value = ratio * 100.0
|
timer_bar.value = ratio * 100.0
|
||||||
if BlockSessionService.session_kind == "core" and timer_label.visible:
|
if is_core_timer and timer_label.visible:
|
||||||
var remaining := int(ceil(BlockSessionService.core_current_target_seconds - BlockSessionService.get_current_elapsed_seconds()))
|
_set_timer_display(int(floor(BlockSessionService.get_core_question_total_elapsed_seconds())))
|
||||||
_set_timer_display(maxi(0, remaining))
|
|
||||||
var color_state := "blue"
|
var color_state := "blue"
|
||||||
var fill_color := Brand.ROYAL_BLUE
|
var fill_color := Brand.ROYAL_BLUE
|
||||||
|
if is_core_timer:
|
||||||
|
if ratio >= 1.0:
|
||||||
|
color_state = "red"
|
||||||
|
fill_color = Style.RED
|
||||||
|
elif ratio >= 0.75:
|
||||||
|
color_state = "amber"
|
||||||
|
fill_color = Style.AMBER
|
||||||
|
else:
|
||||||
if ratio < 0.25:
|
if ratio < 0.25:
|
||||||
color_state = "red"
|
color_state = "red"
|
||||||
fill_color = Style.RED
|
fill_color = Style.RED
|
||||||
|
|||||||
@@ -28,6 +28,29 @@ func _initialize() -> void:
|
|||||||
service.session_initial_seconds = 900.0
|
service.session_initial_seconds = 900.0
|
||||||
service.seconds_remaining = 450.0
|
service.seconds_remaining = 450.0
|
||||||
_assert(is_equal_approx(service.get_session_timer_ratio(), 0.5), "timed block progress should use the full session duration")
|
_assert(is_equal_approx(service.get_session_timer_ratio(), 0.5), "timed block progress should use the full session duration")
|
||||||
|
service.session_kind = "core"
|
||||||
|
service.core_question_display_target_seconds = 100.0
|
||||||
|
service.core_question_total_elapsed_seconds = 25.0
|
||||||
|
service.core_question_total_active_since = 0.0
|
||||||
|
_assert(is_equal_approx(service.get_core_question_total_elapsed_seconds(), 25.0), "core question timer should expose cumulative elapsed time")
|
||||||
|
_assert(is_equal_approx(service.get_core_question_elapsed_ratio(), 0.25), "core question progress should fill toward its original target")
|
||||||
|
service._start_core_retry_window()
|
||||||
|
_assert(is_equal_approx(service.get_core_question_total_elapsed_seconds(), 25.0), "wrong-answer retry windows should not reset cumulative question time")
|
||||||
|
service.core_question_total_active_since = 100.0
|
||||||
|
service.core_question_total_elapsed_seconds = 4.0
|
||||||
|
service._pause_core_question_total_timer(106.0)
|
||||||
|
_assert(is_equal_approx(service.get_core_question_total_elapsed_seconds(), 10.0), "pausing should preserve accumulated question time")
|
||||||
|
service.core_question_total_elapsed_seconds = 0.0
|
||||||
|
service.core_question_total_active_since = 0.001
|
||||||
|
service._freeze_core_question_total_timer()
|
||||||
|
var frozen_elapsed: float = service.get_core_question_total_elapsed_seconds()
|
||||||
|
OS.delay_msec(10)
|
||||||
|
_assert(is_equal_approx(service.get_core_question_total_elapsed_seconds(), frozen_elapsed), "freezing should stop cumulative question time")
|
||||||
|
var resume_now := Time.get_ticks_msec() / 1000.0
|
||||||
|
service.current_question_started_at = maxf(0.001, resume_now - 5.0)
|
||||||
|
service.current_question_pause_started_at = maxf(0.001, resume_now - 0.001)
|
||||||
|
service.resume_current_question_timer()
|
||||||
|
_assert(service.core_question_total_active_since == 0.0, "a completed core question timer should not restart")
|
||||||
service.free()
|
service.free()
|
||||||
quit(1 if failures > 0 else 0)
|
quit(1 if failures > 0 else 0)
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,39 @@ func _initialize() -> void:
|
|||||||
_test_feedback_form_validation()
|
_test_feedback_form_validation()
|
||||||
_test_revisitable_navigation_prefers_unanswered_questions()
|
_test_revisitable_navigation_prefers_unanswered_questions()
|
||||||
_test_submit_styles_keep_stable_geometry()
|
_test_submit_styles_keep_stable_geometry()
|
||||||
|
_test_core_timer_counts_up_and_remains_visible()
|
||||||
await _test_player_runtime_construction()
|
await _test_player_runtime_construction()
|
||||||
quit(1 if failures > 0 else 0)
|
quit(1 if failures > 0 else 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_core_timer_counts_up_and_remains_visible() -> void:
|
||||||
|
var player = load("res://scripts/ui/question_player.gd").new()
|
||||||
|
var block_service = root.get_node("BlockSessionService")
|
||||||
|
player.timer_bar = ProgressBar.new()
|
||||||
|
player.timer_label = Label.new()
|
||||||
|
player.timer_footer = HBoxContainer.new()
|
||||||
|
player.timer_color_state = ""
|
||||||
|
block_service.session_kind = "core"
|
||||||
|
block_service.core_question_display_target_seconds = 100.0
|
||||||
|
block_service.core_question_total_elapsed_seconds = 75.0
|
||||||
|
block_service.core_question_total_active_since = 0.0
|
||||||
|
player._set_answer_review_timer_visibility()
|
||||||
|
_assert(player.timer_footer.visible and player.timer_bar.visible and player.timer_label.visible, "core timer should remain visible during correct-answer review")
|
||||||
|
_assert(is_equal_approx(player.timer_bar.value, 75.0), "core timer bar should fill with elapsed progress")
|
||||||
|
_assert(player.timer_label.text == "01:15", "core timer should display cumulative elapsed time")
|
||||||
|
_assert(player.timer_color_state == "amber", "core timer should warn when approaching its target")
|
||||||
|
block_service.core_question_total_elapsed_seconds = 110.0
|
||||||
|
player._update_timer_bar()
|
||||||
|
_assert(is_equal_approx(player.timer_bar.value, 100.0) and player.timer_color_state == "red", "core timer should remain full and red after exceeding its target")
|
||||||
|
block_service.session_kind = "block"
|
||||||
|
player._set_answer_review_timer_visibility()
|
||||||
|
_assert(not player.timer_footer.visible, "non-core answer review should retain its existing hidden timer")
|
||||||
|
player.timer_bar.free()
|
||||||
|
player.timer_label.free()
|
||||||
|
player.timer_footer.free()
|
||||||
|
player.free()
|
||||||
|
|
||||||
|
|
||||||
func _test_reference_normalization() -> void:
|
func _test_reference_normalization() -> void:
|
||||||
var service = load("res://scripts/autoload/question_bank_service.gd").new()
|
var service = load("res://scripts/autoload/question_bank_service.gd").new()
|
||||||
var references: Array = service._normalize_references([{
|
var references: Array = service._normalize_references([{
|
||||||
|
|||||||
Reference in New Issue
Block a user