353 lines
14 KiB
GDScript
353 lines
14 KiB
GDScript
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 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)
|
|
|
|
_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()
|
|
|
|
|
|
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()
|
|
|
|
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 = AuthState.role.to_upper() == "SUPER"
|
|
|
|
|
|
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
|