extends VBoxContainer const Style := preload("res://scripts/ui/ui_style.gd") var main: Control var exam_view: VBoxContainer var mode_view: VBoxContainer var exam_grid: GridContainer var mode_grid: GridContainer var question_bank_button: Button var bank_status_label: Label var bank_error_label: Label var retry_button: Button var simulation_button: Button var simulation_status_label: Label var simulation_preview_link: LinkButton var completed_history_list: VBoxContainer var completed_history_more: Button var pending_simulation_launch := false var current_stage := "exam" var pending_question_bank_launch := false func _ready() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) add_theme_constant_override("separation", 18) resized.connect(_refresh_grid_columns) if not QuestionBankService.questions_changed.is_connected(_on_questions_changed): 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) if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility): AuthState.auth_changed.connect(_refresh_simulation_preview_visibility) if not ExamSimulationService.completed_history_changed.is_connected(_refresh_completed_history): ExamSimulationService.completed_history_changed.connect(_refresh_completed_history) _build_exam_view() _build_mode_view() _show_stage("exam") func on_show(payload: Variant = null) -> void: if str(payload) == "step3": _show_stage("step3") elif str(payload) == "exam": _show_stage("exam") _refresh_bank_state() _refresh_simulation_preview_visibility() ExamSimulationService.load_readiness() ExamSimulationService.load_completed_history() func on_hide() -> void: pending_question_bank_launch = false func _build_exam_view() -> void: exam_view = VBoxContainer.new() exam_view.add_theme_constant_override("separation", 18) exam_view.size_flags_vertical = Control.SIZE_EXPAND_FILL add_child(exam_view) var eyebrow := Label.new() eyebrow.text = "CHOOSE YOUR EXAM" Style.set_label(eyebrow, 14, Style.CYAN) exam_view.add_child(eyebrow) var title := Label.new() title.text = "What are you preparing for?" Style.set_label(title, 34, Style.TEXT) exam_view.add_child(title) var subtitle := Label.new() subtitle.text = "Select a path to enter its study modes. More exams will be added as they are ready." subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(subtitle, 17, Style.MUTED) exam_view.add_child(subtitle) exam_grid = GridContainer.new() exam_grid.columns = 2 exam_grid.add_theme_constant_override("h_separation", 16) exam_grid.add_theme_constant_override("v_separation", 16) exam_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL exam_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL exam_view.add_child(exam_grid) _add_exam_option("STEP 1", "Foundational sciences", false) _add_exam_option("STEP 2", "Clinical knowledge", false) _add_exam_option("STEP 3", "Advanced clinical management", true) _add_exam_option("CCS Cases", "Interactive case simulations", false) func _build_mode_view() -> void: mode_view = VBoxContainer.new() mode_view.add_theme_constant_override("separation", 18) mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL add_child(mode_view) var back_button := Button.new() back_button.text = "< All exams" back_button.alignment = HORIZONTAL_ALIGNMENT_LEFT back_button.custom_minimum_size = Vector2(150, 42) Style.style_chip(back_button, Style.CYAN) UISoundService.set_button_sound(back_button, "back") back_button.pressed.connect(func(): _show_stage("exam")) mode_view.add_child(back_button) var eyebrow := Label.new() eyebrow.text = "STEP 3" Style.set_label(eyebrow, 14, Style.LIME) mode_view.add_child(eyebrow) var title := Label.new() title.text = "Choose a study mode" Style.set_label(title, 34, Style.TEXT) mode_view.add_child(title) var subtitle := Label.new() subtitle.text = "Build clinical judgment through focused questions. Additional modes are in development." subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(subtitle, 17, Style.MUTED) mode_view.add_child(subtitle) mode_grid = GridContainer.new() mode_grid.columns = 2 mode_grid.add_theme_constant_override("h_separation", 16) mode_grid.add_theme_constant_override("v_separation", 16) mode_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL mode_view.add_child(mode_grid) question_bank_button = Button.new() question_bank_button.text = "Question Bank\nAdaptive core run | Available" question_bank_button.alignment = HORIZONTAL_ALIGNMENT_LEFT question_bank_button.custom_minimum_size = Vector2(360, 150) question_bank_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL question_bank_button.add_theme_font_size_override("font_size", 20) Style.style_primary_button(question_bank_button) question_bank_button.pressed.connect(_select_question_bank) mode_grid.add_child(question_bank_button) var dynamic_button := Button.new() dynamic_button.text = "Dynamic Questions\nAI-generated practice | Coming Soon" dynamic_button.alignment = HORIZONTAL_ALIGNMENT_LEFT dynamic_button.custom_minimum_size = Vector2(360, 150) dynamic_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL dynamic_button.add_theme_font_size_override("font_size", 20) 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) simulation_preview_link = LinkButton.new() simulation_preview_link.text = "Temporary: preview completed exam results" simulation_preview_link.tooltip_text = "SUPER only; generates a nonpersistent simulated result" simulation_preview_link.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN simulation_preview_link.add_theme_color_override("font_color", Style.CYAN) simulation_preview_link.add_theme_color_override("font_hover_color", Style.MAGENTA) simulation_preview_link.pressed.connect(_preview_simulation_results) mode_view.add_child(simulation_preview_link) _refresh_simulation_preview_visibility() var history_heading := Label.new() history_heading.text = "Completed simulations" Style.set_label(history_heading, 20, Style.TEXT) mode_view.add_child(history_heading) completed_history_list = VBoxContainer.new() completed_history_list.add_theme_constant_override("separation", 6) completed_history_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL var history_scroll := ScrollContainer.new() history_scroll.custom_minimum_size = Vector2(0, 150) history_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL history_scroll.add_child(completed_history_list) mode_view.add_child(history_scroll) completed_history_more = Button.new() completed_history_more.text = "Load more" completed_history_more.visible = false completed_history_more.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN Style.style_chip(completed_history_more, Style.CYAN) completed_history_more.pressed.connect(func(): ExamSimulationService.load_completed_history(ExamSimulationService.completed_history_next_before)) mode_view.add_child(completed_history_more) bank_status_label = Label.new() bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(bank_status_label, 16, Style.CYAN) mode_view.add_child(bank_status_label) bank_error_label = Label.new() bank_error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(bank_error_label, 15, Style.RED) mode_view.add_child(bank_error_label) retry_button = Button.new() retry_button.text = "Retry Question Bank" retry_button.custom_minimum_size = Vector2(220, 46) retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN Style.style_chip(retry_button, Style.AMBER) retry_button.pressed.connect(_retry_question_bank) mode_view.add_child(retry_button) var spacer := Control.new() spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL mode_view.add_child(spacer) func _add_exam_option(option_name: String, description: String, available: bool) -> void: var button := Button.new() button.text = "%s\n%s | %s" % [option_name, description, "Available" if available else "Coming Soon"] button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.custom_minimum_size = Vector2(360, 150) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.size_flags_vertical = Control.SIZE_EXPAND_FILL button.add_theme_font_size_override("font_size", 20) button.disabled = not available if available: Style.style_primary_button(button) button.pressed.connect(func(): _show_stage("step3")) exam_grid.add_child(button) func _show_stage(stage_name: String) -> void: current_stage = stage_name pending_question_bank_launch = false if exam_view != null: exam_view.visible = stage_name == "exam" if mode_view != null: mode_view.visible = stage_name == "step3" _refresh_grid_columns() _refresh_bank_state() func _select_question_bank() -> void: pending_question_bank_launch = true var excluded_ids: Array = ProfileService.get_profile().get("correct_question_ids", []).duplicate() QuestionBankService.load_next_random_question(excluded_ids) _refresh_bank_state() func _retry_question_bank() -> void: pending_question_bank_launch = true QuestionBankService.retry_now() _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": pending_question_bank_launch = false call_deferred("_start_question_bank") func _on_questions_load_failed(_message: String) -> void: pending_question_bank_launch = false _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 _preview_simulation_results() -> void: pending_simulation_launch = true simulation_preview_link.disabled = true simulation_status_label.text = "Generating simulated exam results..." ExamSimulationService.load_results_preview() func _refresh_simulation_preview_visibility() -> void: if simulation_preview_link != null: simulation_preview_link.visible = false func _refresh_completed_history() -> void: if completed_history_list == null: return for child in completed_history_list.get_children(): child.queue_free() for raw_summary in ExamSimulationService.completed_history: var summary: Dictionary = raw_summary var button := Button.new() button.text = "%s | Predicted %d (%d-%d) | %d/%d correct" % [ str(summary.get("completedUtc", "")).left(10), int(summary.get("predictedScore", 0)), int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)), int(summary.get("correct", 0)), int(summary.get("total", 0))] button.alignment = HORIZONTAL_ALIGNMENT_LEFT Style.style_chip(button, Style.CYAN) button.pressed.connect(_open_completed_simulation.bind(str(summary.get("id", "")))) completed_history_list.add_child(button) completed_history_more.visible = not ExamSimulationService.completed_history_next_before.is_empty() func _open_completed_simulation(simulation_id: String) -> void: pending_simulation_launch = true ExamSimulationService.load_simulation(simulation_id) 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 _refresh_simulation_state() if simulation_preview_link != null: simulation_preview_link.disabled = false if simulation_status_label != null: simulation_status_label.text = message func _refresh_simulation_state() -> void: if simulation_button == null: return var simulation_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 simulation_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 simulation_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 simulation_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 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 "" if pending_question_bank_launch or state == "loading": bank_status_label.text = "Preparing your question bank..." elif state == "retrying": bank_status_label.text = "Question bank unavailable. Retrying in %d seconds." % QuestionBankService.next_retry_seconds elif state == "ready": bank_status_label.text = "Question bank ready" elif state == "failed": bank_status_label.text = "The question bank could not be loaded." else: bank_status_label.text = "" func _start_question_bank() -> void: if not visible or current_stage != "step3" or not QuestionBankService.has_questions(): return if BlockSessionService.start_core_run(): UISoundService.play_sound("question") main.show_screen("player") else: bank_error_label.text = BlockSessionService.core_status_message func _refresh_grid_columns() -> void: var column_count := 1 if size.x < 820.0 else 2 if exam_grid != null: exam_grid.columns = column_count if mode_grid != null: mode_grid.columns = column_count