Add full-length Step 3 Day 1 simulation
This commit is contained in:
+22
-3
@@ -155,6 +155,18 @@ func _build_ui() -> void:
|
||||
screens_container.add_child(review)
|
||||
screens["review"] = review
|
||||
|
||||
var simulation_break := preload("res://scripts/ui/simulation_break.gd").new()
|
||||
simulation_break.name = "SimulationBreak"
|
||||
simulation_break.main = self
|
||||
screens_container.add_child(simulation_break)
|
||||
screens["simulation_break"] = simulation_break
|
||||
|
||||
var simulation_results := preload("res://scripts/ui/simulation_results.gd").new()
|
||||
simulation_results.name = "SimulationResults"
|
||||
simulation_results.main = self
|
||||
screens_container.add_child(simulation_results)
|
||||
screens["simulation_results"] = simulation_results
|
||||
|
||||
var profile := preload("res://scripts/ui/progress_profile.gd").new()
|
||||
profile.name = "ProgressProfile"
|
||||
profile.main = self
|
||||
@@ -206,8 +218,10 @@ func _refresh_theme_button() -> void:
|
||||
|
||||
func _logout() -> void:
|
||||
_close_app_menu()
|
||||
if BlockSessionService.block_started:
|
||||
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
|
||||
BlockSessionService.finish_block()
|
||||
elif BlockSessionService.session_kind == "simulation":
|
||||
BlockSessionService.block_started = false
|
||||
QuestionBankService.clear_questions()
|
||||
AuthState.clear_session()
|
||||
show_screen("login")
|
||||
@@ -284,6 +298,7 @@ func _toggle_app_menu() -> void:
|
||||
app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5
|
||||
menu_exit_button.visible = current_screen_name == "player" and BlockSessionService.block_started
|
||||
if menu_exit_button.visible:
|
||||
menu_exit_button.text = "Leave (timer continues)" if BlockSessionService.session_kind == "simulation" else "Exit Run"
|
||||
Style.style_chip(menu_exit_button, Style.AMBER)
|
||||
BlockSessionService.pause_current_question_timer()
|
||||
|
||||
@@ -296,15 +311,19 @@ func _close_app_menu() -> void:
|
||||
|
||||
func _exit_run() -> void:
|
||||
_close_app_menu()
|
||||
if BlockSessionService.block_started:
|
||||
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
|
||||
BlockSessionService.finish_block()
|
||||
elif BlockSessionService.session_kind == "simulation":
|
||||
BlockSessionService.block_started = false
|
||||
show_screen("selection", "step3")
|
||||
|
||||
|
||||
func _open_profile_from_menu() -> void:
|
||||
_close_app_menu()
|
||||
if BlockSessionService.block_started:
|
||||
if BlockSessionService.block_started and BlockSessionService.session_kind != "simulation":
|
||||
BlockSessionService.finish_block()
|
||||
elif BlockSessionService.session_kind == "simulation":
|
||||
BlockSessionService.block_started = false
|
||||
show_screen("profile")
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ var pending_correct_advance := false
|
||||
var question_text_scale := 1.0
|
||||
var presented_choices: Array = []
|
||||
var awaiting_next_question := false
|
||||
var simulation_submit_pending := false
|
||||
|
||||
var progress_label: Label
|
||||
var timer_label: Label
|
||||
@@ -131,6 +132,10 @@ func _ready() -> void:
|
||||
QuestionBankService.questions_changed.connect(_on_random_question_changed)
|
||||
if not QuestionBankService.questions_load_failed.is_connected(_on_random_question_failed):
|
||||
QuestionBankService.questions_load_failed.connect(_on_random_question_failed)
|
||||
if not ExamSimulationService.state_changed.is_connected(_on_simulation_state_changed):
|
||||
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_simulation_request_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_simulation_request_failed)
|
||||
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 12)
|
||||
@@ -212,6 +217,8 @@ func _ready() -> void:
|
||||
flag_button.toggled.connect(func(value: bool):
|
||||
flagged = value
|
||||
_refresh_toolbar_state()
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
_save_simulation_current_response()
|
||||
)
|
||||
question_toolbar.add_child(flag_button)
|
||||
|
||||
@@ -369,7 +376,7 @@ func _ready() -> void:
|
||||
actions.add_child(skip_button)
|
||||
|
||||
next_button = Button.new()
|
||||
next_button.text = "Submit"
|
||||
next_button.text = "Submit Block" if BlockSessionService.session_kind == "simulation" and BlockSessionService.current_index + 1 == BlockSessionService.active_questions.size() else "Next" if BlockSessionService.session_kind == "simulation" else "Submit"
|
||||
next_button.tooltip_text = ""
|
||||
next_button.custom_minimum_size = Vector2(140, 48)
|
||||
next_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -420,14 +427,62 @@ func on_show(_payload: Variant = null) -> void:
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
if BlockSessionService.session_kind == "simulation" and BlockSessionService.block_started:
|
||||
_save_simulation_current_response()
|
||||
awaiting_next_question = false
|
||||
_hide_media_viewer()
|
||||
_hide_report_modal(true)
|
||||
_hide_labs_modal()
|
||||
|
||||
|
||||
func _save_simulation_current_response() -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
var question := BlockSessionService.get_current_question()
|
||||
if question.is_empty():
|
||||
return
|
||||
current_answer = BlockSessionService.answer_current(selected_choice, eliminated, flagged)
|
||||
ExamSimulationService.save_response(
|
||||
str(question.get("id", "")),
|
||||
selected_choice,
|
||||
eliminated,
|
||||
flagged,
|
||||
int(BlockSessionService.get_current_elapsed_seconds()))
|
||||
|
||||
|
||||
func _on_simulation_state_changed() -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
var state := str(ExamSimulationService.simulation.get("state", ""))
|
||||
if state == "between_blocks":
|
||||
simulation_submit_pending = false
|
||||
BlockSessionService.block_started = false
|
||||
main.show_screen("simulation_break")
|
||||
elif state == "completed":
|
||||
simulation_submit_pending = false
|
||||
BlockSessionService.block_started = false
|
||||
main.show_screen("simulation_results", ExamSimulationService.simulation)
|
||||
|
||||
|
||||
func _on_simulation_request_failed(message: String) -> void:
|
||||
if BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
simulation_submit_pending = false
|
||||
next_button.disabled = false
|
||||
next_button.text = "Retry"
|
||||
next_button.tooltip_text = message
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_update_answer_feedback(delta)
|
||||
if visible and BlockSessionService.session_kind == "simulation":
|
||||
BlockSessionService.seconds_remaining = maxf(0.0, ExamSimulationService.get_block_deadline_unix() - Time.get_unix_time_from_system())
|
||||
var simulation_seconds := int(ceil(BlockSessionService.seconds_remaining))
|
||||
timer_label.text = "Block time: %02d:%02d" % [int(simulation_seconds / 60), simulation_seconds % 60]
|
||||
if simulation_seconds <= 0 and not simulation_submit_pending:
|
||||
simulation_submit_pending = true
|
||||
ExamSimulationService.submit_block()
|
||||
return
|
||||
if visible and BlockSessionService.block_started and BlockSessionService.session_kind == "core":
|
||||
_update_timer_bar()
|
||||
if visible and BlockSessionService.block_started and BlockSessionService.mode == "Timed" and (report_overlay == null or not report_overlay.visible):
|
||||
@@ -499,13 +554,14 @@ func _load_question() -> void:
|
||||
next_button.disabled = true
|
||||
skip_button.disabled = false
|
||||
skip_button.visible = BlockSessionService.session_kind == "core"
|
||||
report_button.visible = BlockSessionService.session_kind != "simulation"
|
||||
|
||||
var question := BlockSessionService.get_current_question()
|
||||
if question.is_empty():
|
||||
main.show_screen("selection", "step3")
|
||||
return
|
||||
progress_label.visible = BlockSessionService.session_kind != "core"
|
||||
progress_label.text = "" if BlockSessionService.session_kind == "core" else "Question %s" % BlockSessionService.get_progress_text()
|
||||
progress_label.text = "" if BlockSessionService.session_kind == "core" else BlockSessionService.get_progress_text()
|
||||
timer_label.visible = BlockSessionService.session_kind != "core"
|
||||
timer_label.text = "" if BlockSessionService.session_kind == "core" else "Tutor Mode" if BlockSessionService.mode == "Tutor" else "Timed"
|
||||
timer_bar.visible = BlockSessionService.session_kind == "core"
|
||||
@@ -538,8 +594,11 @@ func _load_question() -> void:
|
||||
flagged = bool(saved_answer.get("flagged", false))
|
||||
flag_button.button_pressed = flagged
|
||||
_refresh_toolbar_state()
|
||||
_show_answer_review()
|
||||
_update_question_elo_label(true)
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
_apply_saved_choice_state()
|
||||
else:
|
||||
_show_answer_review()
|
||||
_update_question_elo_label(true)
|
||||
|
||||
_refresh_nav_palette()
|
||||
_refresh_toolbar_state()
|
||||
@@ -556,9 +615,12 @@ func _select_choice(index: int) -> void:
|
||||
return
|
||||
var choice_id := str(choices[index].get("id", ""))
|
||||
selected_choice = "" if selected_choice == choice_id else choice_id
|
||||
if BlockSessionService.mode == "Timed":
|
||||
if BlockSessionService.mode == "Timed" or BlockSessionService.mode == "Simulation":
|
||||
current_answer = {}
|
||||
next_button.text = "Submit"
|
||||
if BlockSessionService.mode == "Simulation":
|
||||
_save_simulation_current_response()
|
||||
else:
|
||||
next_button.text = "Submit"
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
@@ -566,9 +628,12 @@ func _clear_selected_choice() -> void:
|
||||
if selected_choice == "" or _is_correct_answer_locked():
|
||||
return
|
||||
selected_choice = ""
|
||||
if BlockSessionService.mode == "Timed":
|
||||
if BlockSessionService.mode == "Timed" or BlockSessionService.mode == "Simulation":
|
||||
current_answer = {}
|
||||
next_button.text = "Submit"
|
||||
if BlockSessionService.mode == "Simulation":
|
||||
_save_simulation_current_response()
|
||||
else:
|
||||
next_button.text = "Submit"
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
@@ -635,7 +700,7 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if mouse_event == null or mouse_event.button_index != MOUSE_BUTTON_RIGHT or not mouse_event.pressed:
|
||||
return
|
||||
if not current_answer.is_empty():
|
||||
if not current_answer.is_empty() and BlockSessionService.session_kind != "simulation":
|
||||
return
|
||||
|
||||
var choices := _get_presented_choices()
|
||||
@@ -653,10 +718,12 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
|
||||
UISoundService.play_sound("eliminate")
|
||||
_set_choice_button_text(index, choice)
|
||||
_apply_saved_choice_state()
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
_save_simulation_current_response()
|
||||
|
||||
|
||||
func _toggle_eliminate_selected() -> void:
|
||||
if pending_correct_advance or followup_required or selected_choice == "" or not current_answer.is_empty() or _is_correct_answer_locked():
|
||||
if pending_correct_advance or followup_required or selected_choice == "" or (not current_answer.is_empty() and BlockSessionService.session_kind != "simulation") or _is_correct_answer_locked():
|
||||
return
|
||||
var choices := _get_presented_choices()
|
||||
for index in range(choices.size()):
|
||||
@@ -668,9 +735,23 @@ func _toggle_eliminate_selected() -> void:
|
||||
UISoundService.play_sound("eliminate")
|
||||
_set_choice_button_text(index, choices[index])
|
||||
_apply_saved_choice_state()
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
_save_simulation_current_response()
|
||||
|
||||
|
||||
func _submit_or_next() -> void:
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
_save_simulation_current_response()
|
||||
if BlockSessionService.current_index + 1 < BlockSessionService.active_questions.size():
|
||||
BlockSessionService.move_next()
|
||||
_load_question()
|
||||
return
|
||||
elif not simulation_submit_pending:
|
||||
simulation_submit_pending = true
|
||||
next_button.disabled = true
|
||||
next_button.text = "Submitting..."
|
||||
ExamSimulationService.submit_block()
|
||||
return
|
||||
if awaiting_next_question:
|
||||
return
|
||||
if pending_correct_advance:
|
||||
@@ -1917,6 +1998,9 @@ func _is_correct_answer_locked() -> bool:
|
||||
func _refresh_submit_state() -> void:
|
||||
if next_button == null:
|
||||
return
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
next_button.disabled = false
|
||||
return
|
||||
if pending_correct_advance or followup_required:
|
||||
next_button.disabled = true
|
||||
return
|
||||
@@ -2489,12 +2573,14 @@ func _refresh_nav_palette() -> void:
|
||||
Style.style_chip(button, Style.CYAN)
|
||||
elif answer != null and bool(answer.get("flagged", false)):
|
||||
Style.style_chip(button, Style.AMBER)
|
||||
elif answer != null:
|
||||
elif answer != null and str(answer.get("selected_choice_id", "")) != "":
|
||||
Style.style_chip(button, Style.LIME)
|
||||
else:
|
||||
Style.style_chip(button, Color("596982"))
|
||||
button.pressed.connect(func(target_index: int = index):
|
||||
if BlockSessionService.mode == "Timed":
|
||||
if BlockSessionService.mode == "Timed" or BlockSessionService.mode == "Simulation":
|
||||
if BlockSessionService.mode == "Simulation":
|
||||
_save_simulation_current_response()
|
||||
BlockSessionService.move_to(target_index)
|
||||
_load_question()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
|
||||
var main: Control
|
||||
var title: Label
|
||||
var detail: Label
|
||||
var start_button: Button
|
||||
var abandon_button: Button
|
||||
var error_label: Label
|
||||
var abandon_armed := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 18)
|
||||
if not ExamSimulationService.state_changed.is_connected(_on_state_changed):
|
||||
ExamSimulationService.state_changed.connect(_on_state_changed)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_failed)
|
||||
|
||||
var eyebrow := Label.new()
|
||||
eyebrow.text = "STEP 3 DAY 1 SIMULATION"
|
||||
Style.set_label(eyebrow, 14, Style.CYAN)
|
||||
add_child(eyebrow)
|
||||
title = Label.new()
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
add_child(title)
|
||||
detail = Label.new()
|
||||
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(detail, 18, Style.MUTED)
|
||||
add_child(detail)
|
||||
|
||||
start_button = Button.new()
|
||||
start_button.custom_minimum_size = Vector2(240, 54)
|
||||
start_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_primary_button(start_button)
|
||||
start_button.pressed.connect(_start_block)
|
||||
add_child(start_button)
|
||||
|
||||
abandon_button = Button.new()
|
||||
abandon_button.text = "Abandon simulation"
|
||||
abandon_button.custom_minimum_size = Vector2(240, 44)
|
||||
abandon_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_chip(abandon_button, Style.RED)
|
||||
abandon_button.pressed.connect(_abandon)
|
||||
add_child(abandon_button)
|
||||
|
||||
error_label = Label.new()
|
||||
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(error_label, 16, Style.RED)
|
||||
add_child(error_label)
|
||||
|
||||
var note := Label.new()
|
||||
note.text = "Each block runs for 30 minutes. You may review and change answers within the active block, but closed blocks cannot be reopened. Leaving during a block does not stop its server timer."
|
||||
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(note, 16, Style.MUTED)
|
||||
add_child(note)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(spacer)
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
abandon_armed = false
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
var block := int(ExamSimulationService.simulation.get("currentBlock", -1))
|
||||
var break_seconds := int(ExamSimulationService.simulation.get("breakSecondsRemaining", 3300))
|
||||
if block < 0:
|
||||
title.text = "Foundations of Independent Practice"
|
||||
detail.text = "232 questions across 12 timed blocks. Results and explanations remain hidden until the simulation is complete. Estimated break pool: %d minutes." % int(break_seconds / 60)
|
||||
else:
|
||||
title.text = "Block %d complete" % (block + 1)
|
||||
detail.text = "%d of 12 blocks complete. Your progress is saved. Break pool: approximately %d minutes." % [block + 1, int(break_seconds / 60)]
|
||||
start_button.text = "Start Block %d" % (block + 2)
|
||||
start_button.disabled = ExamSimulationService.is_loading
|
||||
abandon_button.text = "Confirm abandon" if abandon_armed else "Abandon simulation"
|
||||
abandon_button.disabled = ExamSimulationService.is_loading
|
||||
error_label.text = ""
|
||||
|
||||
|
||||
func _start_block() -> void:
|
||||
start_button.disabled = true
|
||||
start_button.text = "Preparing..."
|
||||
ExamSimulationService.start_next_block()
|
||||
|
||||
|
||||
func _abandon() -> void:
|
||||
if not abandon_armed:
|
||||
abandon_armed = true
|
||||
abandon_button.text = "Confirm abandon"
|
||||
error_label.text = "This permanently closes the current simulation. Select Confirm abandon to continue."
|
||||
return
|
||||
abandon_button.disabled = true
|
||||
ExamSimulationService.abandon()
|
||||
|
||||
|
||||
func _on_state_changed() -> void:
|
||||
if not visible:
|
||||
return
|
||||
if ExamSimulationService.simulation.is_empty():
|
||||
main.show_screen("selection", "step3")
|
||||
return
|
||||
if str(ExamSimulationService.simulation.get("state", "")) != "block_active":
|
||||
_refresh()
|
||||
return
|
||||
BlockSessionService.start_simulation_block(
|
||||
ExamSimulationService.get_current_questions(),
|
||||
int(ExamSimulationService.simulation.get("currentBlock", 0)),
|
||||
int(ExamSimulationService.simulation.get("totalBlocks", 12)),
|
||||
ExamSimulationService.get_block_deadline_unix())
|
||||
BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", []))
|
||||
main.show_screen("player")
|
||||
|
||||
|
||||
func _on_failed(message: String) -> void:
|
||||
if not visible:
|
||||
return
|
||||
error_label.text = message
|
||||
start_button.disabled = false
|
||||
_refresh()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnyyo3s2jkmkr
|
||||
@@ -0,0 +1,169 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
|
||||
var main: Control
|
||||
var score_label: Label
|
||||
var summary_label: Label
|
||||
var category_list: VBoxContainer
|
||||
var review_list: VBoxContainer
|
||||
var outcome_score: SpinBox
|
||||
var outcome_date: LineEdit
|
||||
var outcome_consent: CheckBox
|
||||
var outcome_status: Label
|
||||
var outcome_delete_button: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 12)
|
||||
var eyebrow := Label.new()
|
||||
eyebrow.text = "STEP 3 DAY 1 SIMULATION COMPLETE"
|
||||
Style.set_label(eyebrow, 14, Style.CYAN)
|
||||
add_child(eyebrow)
|
||||
score_label = Label.new()
|
||||
Style.set_label(score_label, 36, Style.LIME)
|
||||
add_child(score_label)
|
||||
summary_label = Label.new()
|
||||
summary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(summary_label, 17, Style.TEXT)
|
||||
add_child(summary_label)
|
||||
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(scroll)
|
||||
var content := VBoxContainer.new()
|
||||
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
content.add_theme_constant_override("separation", 10)
|
||||
scroll.add_child(content)
|
||||
var category_heading := Label.new()
|
||||
category_heading.text = "Performance by content area"
|
||||
Style.set_label(category_heading, 22, Style.TEXT)
|
||||
content.add_child(category_heading)
|
||||
category_list = VBoxContainer.new()
|
||||
content.add_child(category_list)
|
||||
var review_heading := Label.new()
|
||||
review_heading.text = "Answer review"
|
||||
Style.set_label(review_heading, 22, Style.TEXT)
|
||||
content.add_child(review_heading)
|
||||
review_list = VBoxContainer.new()
|
||||
review_list.add_theme_constant_override("separation", 8)
|
||||
content.add_child(review_list)
|
||||
|
||||
var outcome_heading := Label.new()
|
||||
outcome_heading.text = "Help calibrate future predictions"
|
||||
Style.set_label(outcome_heading, 22, Style.TEXT)
|
||||
content.add_child(outcome_heading)
|
||||
var outcome_note := Label.new()
|
||||
outcome_note.text = "After receiving your official result, you may voluntarily submit it. You can edit or delete it at any time; calibration uses de-identified aggregates."
|
||||
outcome_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(outcome_note, 15, Style.MUTED)
|
||||
content.add_child(outcome_note)
|
||||
var outcome_row := HBoxContainer.new()
|
||||
outcome_row.add_theme_constant_override("separation", 10)
|
||||
content.add_child(outcome_row)
|
||||
outcome_score = SpinBox.new()
|
||||
outcome_score.min_value = 1
|
||||
outcome_score.max_value = 300
|
||||
outcome_score.value = 200
|
||||
outcome_score.custom_minimum_size = Vector2(120, 44)
|
||||
outcome_row.add_child(outcome_score)
|
||||
outcome_date = LineEdit.new()
|
||||
outcome_date.placeholder_text = "Exam date: YYYY-MM-DD"
|
||||
outcome_date.custom_minimum_size = Vector2(220, 44)
|
||||
outcome_row.add_child(outcome_date)
|
||||
outcome_consent = CheckBox.new()
|
||||
outcome_consent.text = "I consent to calibration use"
|
||||
outcome_row.add_child(outcome_consent)
|
||||
var save_outcome := Button.new()
|
||||
save_outcome.text = "Save outcome"
|
||||
Style.style_chip(save_outcome, Style.CYAN)
|
||||
save_outcome.pressed.connect(_save_outcome)
|
||||
outcome_row.add_child(save_outcome)
|
||||
outcome_delete_button = Button.new()
|
||||
outcome_delete_button.text = "Delete outcome"
|
||||
Style.style_chip(outcome_delete_button, Style.RED)
|
||||
outcome_delete_button.pressed.connect(ExamSimulationService.delete_outcome)
|
||||
outcome_row.add_child(outcome_delete_button)
|
||||
outcome_status = Label.new()
|
||||
Style.set_label(outcome_status, 15, Style.MUTED)
|
||||
content.add_child(outcome_status)
|
||||
if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome):
|
||||
ExamSimulationService.outcome_changed.connect(_refresh_outcome)
|
||||
|
||||
var done := Button.new()
|
||||
done.text = "Return to study modes"
|
||||
done.custom_minimum_size = Vector2(240, 50)
|
||||
done.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_primary_button(done)
|
||||
done.pressed.connect(func(): main.show_screen("selection", "step3"))
|
||||
add_child(done)
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
var simulation: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else ExamSimulationService.simulation
|
||||
var score: Dictionary = simulation.get("score", {})
|
||||
var predicted := int(score.get("predictedScore", 0))
|
||||
var low := int(score.get("rangeLow", 0))
|
||||
var high := int(score.get("rangeHigh", 0))
|
||||
var probability := int(round(float(score.get("passProbability", 0.0)) * 100.0))
|
||||
score_label.text = "Predicted Step 3 score: %d (%d-%d)" % [predicted, low, high]
|
||||
summary_label.text = "%d/%d correct | Estimated probability of reaching 200: %d%% | Previously seen questions: %d\nThis provisional educational estimate is not an official USMLE or NBME score and excludes CCS performance." % [
|
||||
int(score.get("correct", 0)), int(score.get("total", 0)), probability, int(score.get("seenCount", 0))]
|
||||
_clear(category_list)
|
||||
for category in score.get("categories", {}):
|
||||
var value: Dictionary = score["categories"][category]
|
||||
var label := Label.new()
|
||||
label.text = "%s: %d/%d" % [category, int(value.get("correct", 0)), int(value.get("total", 0))]
|
||||
Style.set_label(label, 16, Style.TEXT)
|
||||
category_list.add_child(label)
|
||||
_build_review(simulation)
|
||||
ExamSimulationService.load_outcome()
|
||||
|
||||
|
||||
func _build_review(simulation: Dictionary) -> void:
|
||||
_clear(review_list)
|
||||
var responses := {}
|
||||
for response in simulation.get("responses", []):
|
||||
responses[str(response.get("questionExternalId", ""))] = response
|
||||
for raw_question in simulation.get("reviewQuestions", []):
|
||||
if typeof(raw_question) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var question: Dictionary = raw_question
|
||||
var id := str(question.get("externalId", ""))
|
||||
var selected := str(responses.get(id, {}).get("choiceId", ""))
|
||||
var correct := str(question.get("correctChoiceId", ""))
|
||||
var item := RichTextLabel.new()
|
||||
item.bbcode_enabled = true
|
||||
item.fit_content = true
|
||||
item.custom_minimum_size = Vector2(0, 84)
|
||||
item.text = "[b]%s[/b] %s\nSelected: %s | Correct: %s\n%s" % ["CORRECT" if selected == correct else "MISS", str(question.get("leadIn", "")), selected if selected != "" else "Unanswered", correct, str(question.get("explanation", ""))]
|
||||
review_list.add_child(item)
|
||||
|
||||
|
||||
func _clear(container: Control) -> void:
|
||||
for child in container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
|
||||
func _save_outcome() -> void:
|
||||
if not outcome_consent.button_pressed:
|
||||
outcome_status.text = "Consent is required before saving an outcome."
|
||||
return
|
||||
if outcome_date.text.length() != 10:
|
||||
outcome_status.text = "Enter the exam date as YYYY-MM-DD."
|
||||
return
|
||||
ExamSimulationService.save_outcome(int(outcome_score.value), outcome_date.text)
|
||||
outcome_status.text = "Saving..."
|
||||
|
||||
|
||||
func _refresh_outcome() -> void:
|
||||
if ExamSimulationService.outcome.is_empty():
|
||||
outcome_status.text = "No official outcome is stored."
|
||||
outcome_delete_button.visible = false
|
||||
return
|
||||
outcome_score.value = int(ExamSimulationService.outcome.get("officialScore", 200))
|
||||
outcome_date.text = str(ExamSimulationService.outcome.get("examDate", "")).left(10)
|
||||
outcome_consent.button_pressed = true
|
||||
outcome_delete_button.visible = true
|
||||
outcome_status.text = "Official outcome saved."
|
||||
@@ -0,0 +1 @@
|
||||
uid://h7bg77wtfl3t
|
||||
@@ -11,7 +11,9 @@ var question_bank_button: Button
|
||||
var bank_status_label: Label
|
||||
var bank_error_label: Label
|
||||
var retry_button: Button
|
||||
var image_question_test_link: LinkButton
|
||||
var simulation_button: Button
|
||||
var simulation_status_label: Label
|
||||
var pending_simulation_launch := false
|
||||
var current_stage := "exam"
|
||||
var pending_question_bank_launch := false
|
||||
|
||||
@@ -24,6 +26,12 @@ func _ready() -> void:
|
||||
QuestionBankService.questions_changed.connect(_on_questions_changed)
|
||||
if not QuestionBankService.questions_load_failed.is_connected(_on_questions_load_failed):
|
||||
QuestionBankService.questions_load_failed.connect(_on_questions_load_failed)
|
||||
if not ExamSimulationService.readiness_changed.is_connected(_refresh_simulation_state):
|
||||
ExamSimulationService.readiness_changed.connect(_refresh_simulation_state)
|
||||
if not ExamSimulationService.state_changed.is_connected(_on_simulation_state_changed):
|
||||
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_simulation_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_simulation_failed)
|
||||
|
||||
_build_exam_view()
|
||||
_build_mode_view()
|
||||
@@ -36,6 +44,7 @@ func on_show(payload: Variant = null) -> void:
|
||||
elif str(payload) == "exam":
|
||||
_show_stage("exam")
|
||||
_refresh_bank_state()
|
||||
ExamSimulationService.load_readiness()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
@@ -135,6 +144,22 @@ func _build_mode_view() -> void:
|
||||
dynamic_button.disabled = true
|
||||
mode_grid.add_child(dynamic_button)
|
||||
|
||||
simulation_button = Button.new()
|
||||
simulation_button.text = "Step 3 Day 1 Simulation\n232 questions | Checking readiness"
|
||||
simulation_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
simulation_button.custom_minimum_size = Vector2(360, 150)
|
||||
simulation_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
simulation_button.add_theme_font_size_override("font_size", 20)
|
||||
Style.style_primary_button(simulation_button)
|
||||
simulation_button.disabled = true
|
||||
simulation_button.pressed.connect(_select_simulation)
|
||||
mode_grid.add_child(simulation_button)
|
||||
|
||||
simulation_status_label = Label.new()
|
||||
simulation_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(simulation_status_label, 15, Style.MUTED)
|
||||
mode_view.add_child(simulation_status_label)
|
||||
|
||||
bank_status_label = Label.new()
|
||||
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_status_label, 16, Style.CYAN)
|
||||
@@ -153,15 +178,6 @@ func _build_mode_view() -> void:
|
||||
retry_button.pressed.connect(_retry_question_bank)
|
||||
mode_view.add_child(retry_button)
|
||||
|
||||
image_question_test_link = LinkButton.new()
|
||||
image_question_test_link.text = "Temporary: test a question with an image"
|
||||
image_question_test_link.tooltip_text = "Load a random published question that includes media"
|
||||
image_question_test_link.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
image_question_test_link.add_theme_color_override("font_color", Style.CYAN)
|
||||
image_question_test_link.add_theme_color_override("font_hover_color", Style.MAGENTA)
|
||||
image_question_test_link.pressed.connect(_select_image_question_for_testing)
|
||||
mode_view.add_child(image_question_test_link)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
mode_view.add_child(spacer)
|
||||
@@ -206,12 +222,6 @@ func _retry_question_bank() -> void:
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _select_image_question_for_testing() -> void:
|
||||
pending_question_bank_launch = true
|
||||
QuestionBankService.load_image_question_for_testing()
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _on_questions_changed() -> void:
|
||||
_refresh_bank_state()
|
||||
if pending_question_bank_launch and visible and current_stage == "step3" and QuestionBankService.load_state == "ready":
|
||||
@@ -224,12 +234,63 @@ func _on_questions_load_failed(_message: String) -> void:
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _select_simulation() -> void:
|
||||
pending_simulation_launch = true
|
||||
simulation_button.disabled = true
|
||||
if bool(ExamSimulationService.readiness.get("hasActiveSimulation", false)):
|
||||
ExamSimulationService.load_active()
|
||||
else:
|
||||
ExamSimulationService.create_simulation()
|
||||
|
||||
|
||||
func _on_simulation_state_changed() -> void:
|
||||
_refresh_simulation_state()
|
||||
if not pending_simulation_launch or ExamSimulationService.simulation.is_empty():
|
||||
return
|
||||
pending_simulation_launch = false
|
||||
var state := str(ExamSimulationService.simulation.get("state", ""))
|
||||
if state == "block_active":
|
||||
_launch_simulation_block()
|
||||
elif state == "completed":
|
||||
main.show_screen("simulation_results", ExamSimulationService.simulation)
|
||||
else:
|
||||
main.show_screen("simulation_break")
|
||||
|
||||
|
||||
func _launch_simulation_block() -> void:
|
||||
BlockSessionService.start_simulation_block(
|
||||
ExamSimulationService.get_current_questions(),
|
||||
int(ExamSimulationService.simulation.get("currentBlock", 0)),
|
||||
int(ExamSimulationService.simulation.get("totalBlocks", 12)),
|
||||
ExamSimulationService.get_block_deadline_unix())
|
||||
BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", []))
|
||||
main.show_screen("player")
|
||||
|
||||
|
||||
func _on_simulation_failed(message: String) -> void:
|
||||
pending_simulation_launch = false
|
||||
if simulation_status_label != null:
|
||||
simulation_status_label.text = message
|
||||
_refresh_simulation_state()
|
||||
|
||||
|
||||
func _refresh_simulation_state() -> void:
|
||||
if simulation_button == null:
|
||||
return
|
||||
var ready := bool(ExamSimulationService.readiness.get("ready", false))
|
||||
var active := bool(ExamSimulationService.readiness.get("hasActiveSimulation", false)) or not ExamSimulationService.simulation.is_empty() and str(ExamSimulationService.simulation.get("state", "")) in ["between_blocks", "block_active"]
|
||||
var available := int(ExamSimulationService.readiness.get("availableQuestions", 0))
|
||||
simulation_button.disabled = pending_simulation_launch or (not ready and not active)
|
||||
simulation_button.text = "Resume Step 3 Day 1 Simulation\nProgress is saved between blocks" if active else "Step 3 Day 1 Simulation\n232 questions | %s" % ("Available" if ready else "%d/232 ready" % available)
|
||||
if simulation_status_label != null and not pending_simulation_launch:
|
||||
simulation_status_label.text = "Full-length, 12-block simulation. Predicted scores are provisional and exclude CCS." if ready or active else "The simulation unlocks when at least 232 published questions are available."
|
||||
|
||||
|
||||
func _refresh_bank_state() -> void:
|
||||
if question_bank_button == null:
|
||||
return
|
||||
var state := QuestionBankService.load_state
|
||||
question_bank_button.disabled = pending_question_bank_launch
|
||||
image_question_test_link.disabled = pending_question_bank_launch
|
||||
retry_button.visible = state == "failed" or state == "retrying"
|
||||
retry_button.disabled = state == "retrying"
|
||||
bank_error_label.text = QuestionBankService.last_error if state == "failed" or state == "retrying" else ""
|
||||
|
||||
Reference in New Issue
Block a user