Files
step3/scripts/ui/study_selector.gd
T

691 lines
27 KiB
GDScript

extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
signal stage_changed(stage_name: String)
var main: Control
var exam_view: VBoxContainer
var mode_view: VBoxContainer
var stage_scroll: ScrollContainer
var stage_content: VBoxContainer
var exam_grid: VBoxContainer
var exam_top_row: BoxContainer
var mode_grid: GridContainer
var question_bank_button: Button
var bank_status_label: Label
var bank_error_label: Label
var retry_button: Button
var ccs_button: Button
var ccs_status_label: Label
var pending_ccs_launch := false
var current_stage := "exam"
var pending_question_bank_launch := false
var themed_controls: Array[Control] = []
var responsive_cards: Array[PanelContainer] = []
var responsive_card_rows: Array[BoxContainer] = []
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 CCSService.attempt_changed.is_connected(_on_ccs_attempt_changed):
CCSService.attempt_changed.connect(_on_ccs_attempt_changed)
if not CCSService.request_failed.is_connected(_on_ccs_failed):
CCSService.request_failed.connect(_on_ccs_failed)
stage_scroll = ScrollContainer.new()
stage_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stage_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
stage_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(stage_scroll)
stage_content = VBoxContainer.new()
stage_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stage_scroll.add_child(stage_content)
_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()
func on_hide() -> void:
pending_question_bank_launch = false
pending_ccs_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
stage_content.add_child(exam_view)
var eyebrow := Label.new()
eyebrow.text = "GET STARTED"
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 14)
eyebrow.add_theme_color_override("font_color", Brand.SKY_BLUE)
exam_view.add_child(eyebrow)
var title := Label.new()
title.text = "What are you preparing for?"
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 34)
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
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 17)
subtitle.add_theme_color_override("font_color", Brand.SLATE)
subtitle.set_meta("medmind_role", "muted")
themed_controls.append(subtitle)
exam_view.add_child(subtitle)
exam_grid = VBoxContainer.new()
exam_grid.add_theme_constant_override("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)
exam_top_row = BoxContainer.new()
exam_top_row.add_theme_constant_override("separation", 16)
exam_top_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
exam_grid.add_child(exam_top_row)
_add_exam_option("STEP 1", "Foundational Sciences", "Build a strong understanding of the basic science concepts tested on Step 1.", "microscope", false)
_add_exam_option("STEP 2", "Clinical Knowledge", "Master clinical concepts and apply knowledge to patient care scenarios.", "stethoscope", false)
_add_exam_option("STEP 3", "Advanced Clinical Management", "Focus on advanced clinical management, CCS cases, and real-world decision making.", "clipboard-plus", true)
func _build_mode_view() -> void:
mode_view = VBoxContainer.new()
mode_view.add_theme_constant_override("separation", 16)
mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
stage_content.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(0, 48)
back_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_style_back_bar(back_button)
back_button.set_meta("medmind_role", "back_bar")
themed_controls.append(back_button)
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"
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 14)
eyebrow.add_theme_color_override("font_color", Brand.TEAL)
mode_view.add_child(eyebrow)
var title := Label.new()
title.text = "Choose a study mode"
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 34)
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
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 17)
subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
subtitle.set_meta("medmind_role", "muted")
themed_controls.append(subtitle)
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)
var question_card := _add_mode_card(
"Question Bank",
"Adaptive core run",
"Available",
"Recommended",
"Targeted practice with adaptive learning\nto strengthen your weaknesses.",
"clipboard-list",
true,
Brand.TEAL)
question_bank_button = question_card.action
question_bank_button.pressed.connect(_select_question_bank)
bank_status_label = question_card.status
bank_error_label = question_card.error
retry_button = question_card.retry
retry_button.pressed.connect(_retry_question_bank)
_add_mode_card(
"Dynamic Questions",
"AI-generated practice",
"Coming Soon",
"Coming Soon",
"Endless practice with AI-generated questions\ntailored to your performance.",
"medmind",
false,
Brand.PURPLE)
var ccs_card := _add_mode_card(
"CCS Cases",
"Interactive case simulations",
"Available",
"Available",
"Practice real-world clinical scenarios and\nimprove your management skills.",
"stethoscope",
true,
Brand.TEAL)
ccs_button = ccs_card.action
ccs_status_label = ccs_card.status
ccs_button.pressed.connect(_select_ccs)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
mode_view.add_child(spacer)
var simulation_note := HBoxContainer.new()
simulation_note.add_theme_constant_override("separation", 8)
mode_view.add_child(simulation_note)
var info := Label.new()
info.text = "i"
info.add_theme_font_size_override("font_size", 17)
info.add_theme_color_override("font_color", Brand.SLATE)
simulation_note.add_child(info)
var note := RichTextLabel.new()
note.bbcode_enabled = true
note.fit_content = true
note.scroll_active = false
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
note.size_flags_horizontal = Control.SIZE_EXPAND_FILL
note.custom_minimum_size.x = 0
note.text = "Full-length, 12-block simulation is available in the [url=exams][color=#7c3aed]Exams[/color][/url] section."
note.add_theme_font_override("normal_font", Brand.font)
note.add_theme_font_size_override("normal_font_size", 15)
note.add_theme_color_override("default_color", Brand.SLATE)
note.set_meta("medmind_role", "muted_rich")
themed_controls.append(note)
note.meta_clicked.connect(func(_meta: Variant): main.show_screen("simulation_history"))
simulation_note.add_child(note)
func _add_mode_card(
title_text: String,
subtitle_text: String,
availability_text: String,
badge_text: String,
description: String,
icon_name: String,
enabled: bool,
accent: Color
) -> Dictionary:
var dark := UIThemeService.is_dark_mode()
var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE
var badge_accent := Brand.SUCCESS if enabled else accent
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(430, 210)
card.set_meta("medmind_wide_minimum", Vector2(430, 210))
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent))
card.set_meta("medmind_role", "mode_card")
card.set_meta("medmind_enabled", enabled)
card.set_meta("medmind_accent", accent)
themed_controls.append(card)
responsive_cards.append(card)
mode_grid.add_child(card)
var content := VBoxContainer.new()
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_theme_constant_override("separation", 14)
card.add_child(content)
var header := BoxContainer.new()
header.vertical = false
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
header.add_theme_constant_override("separation", 20)
content.add_child(header)
responsive_card_rows.append(header)
var icon_panel := PanelContainer.new()
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon_panel.custom_minimum_size = Vector2(72, 72)
icon_panel.add_theme_stylebox_override("panel", _circle_box(visual_accent, 44))
icon_panel.set_meta("medmind_role", "icon_circle")
icon_panel.set_meta("medmind_accent", accent)
icon_panel.set_meta("medmind_enabled", enabled)
themed_controls.append(icon_panel)
header.add_child(icon_panel)
var icon := TextureRect.new()
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.texture = preload("res://assets/branding/medmind-icon.png") if icon_name == "medmind" else load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.material = Brand.logo_cutout_material() if icon_name == "medmind" else null
icon.modulate = visual_accent
icon.set_meta("medmind_role", "accent_icon")
icon.set_meta("medmind_accent", accent)
icon.set_meta("medmind_enabled", enabled)
themed_controls.append(icon)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_panel.add_child(icon)
var details := VBoxContainer.new()
details.mouse_filter = Control.MOUSE_FILTER_IGNORE
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.alignment = BoxContainer.ALIGNMENT_CENTER
details.add_theme_constant_override("separation", 5)
header.add_child(details)
var title := Label.new()
title.text = title_text
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 22)
details.add_child(title)
var subtitle := Label.new()
subtitle.text = "%s | %s" % [subtitle_text, availability_text]
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 16)
subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
subtitle.set_meta("medmind_role", "muted")
themed_controls.append(subtitle)
details.add_child(subtitle)
var badge := Label.new()
badge.text = badge_text
badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
badge.add_theme_font_override("font", Brand.font)
badge.add_theme_font_size_override("font_size", 13)
badge.add_theme_color_override("font_color", badge_accent)
badge.add_theme_stylebox_override("normal", _badge_box(badge_accent))
badge.set_meta("medmind_role", "badge")
badge.set_meta("medmind_accent", accent)
badge.set_meta("medmind_enabled", enabled)
themed_controls.append(badge)
details.add_child(badge)
content.add_child(HSeparator.new())
var bottom := BoxContainer.new()
bottom.vertical = false
bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE
bottom.add_theme_constant_override("separation", 16)
content.add_child(bottom)
responsive_card_rows.append(bottom)
var description_label := Label.new()
description_label.text = description
description_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
description_label.add_theme_font_override("font", Brand.font)
description_label.add_theme_font_size_override("font_size", 16)
description_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
description_label.set_meta("medmind_role", "muted")
themed_controls.append(description_label)
bottom.add_child(description_label)
var arrow_panel := PanelContainer.new()
arrow_panel.custom_minimum_size = Vector2(48, 48)
arrow_panel.add_theme_stylebox_override("panel", _circle_box(visual_accent if enabled else Brand.SLATE, 24, enabled))
arrow_panel.set_meta("medmind_role", "arrow_circle")
arrow_panel.set_meta("medmind_accent", accent if enabled else Brand.SLATE)
arrow_panel.set_meta("medmind_enabled", enabled)
themed_controls.append(arrow_panel)
bottom.add_child(arrow_panel)
var arrow := TextureRect.new()
arrow.texture = preload("res://assets/icons/lucide/chevron-right.svg")
arrow.modulate = Color.WHITE if UIThemeService.is_dark_mode() else Brand.ROYAL_BLUE
arrow.set_meta("medmind_role", "arrow_icon")
themed_controls.append(arrow)
arrow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
arrow.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
arrow_panel.add_child(arrow)
var status := Label.new()
status.visible = false
status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
status.add_theme_font_override("font", Brand.font)
status.add_theme_font_size_override("font_size", 14)
status.add_theme_color_override("font_color", accent)
content.add_child(status)
var error := Label.new()
error.visible = false
error.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
error.add_theme_font_override("font", Brand.font)
error.add_theme_font_size_override("font_size", 14)
error.add_theme_color_override("font_color", Style.RED)
content.add_child(error)
var retry := Button.new()
retry.text = "Retry"
retry.visible = false
retry.custom_minimum_size = Vector2(110, 38)
retry.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_chip(retry, Style.AMBER)
content.add_child(retry)
var action := Button.new()
action.text = ""
action.flat = true
action.disabled = not enabled
action.tooltip_text = title_text if enabled else "Coming Soon"
action.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
card.add_child(action)
card.move_child(action, 0)
return {"action": action, "status": status, "error": error, "retry": retry}
func _mode_card_box(enabled: bool, accent: Color) -> StyleBoxFlat:
var dark := UIThemeService.is_dark_mode()
var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE
var box := Brand.content_card_box(dark, enabled, visual_accent)
box.content_margin_left = 24
box.content_margin_right = 24
box.content_margin_top = 20
box.content_margin_bottom = 18
return box
func _circle_box(accent: Color, radius: int, filled: bool = false) -> StyleBoxFlat:
var box := Brand.icon_circle_box(UIThemeService.is_dark_mode(), accent, radius, filled)
box.content_margin_left = 22 if radius > 30 else 12
box.content_margin_right = box.content_margin_left
box.content_margin_top = box.content_margin_left
box.content_margin_bottom = box.content_margin_left
return box
func _badge_box(accent: Color) -> StyleBoxFlat:
return Brand.badge_box(UIThemeService.is_dark_mode(), accent)
func _style_back_bar(button: Button) -> void:
var dark := UIThemeService.is_dark_mode()
var normal := StyleBoxFlat.new()
normal.bg_color = Color("0b1427aa") if dark else Color("ffffffdd")
normal.border_color = Color("33415d") if dark else Color("d4def0")
normal.set_border_width_all(1)
normal.set_corner_radius_all(8)
normal.content_margin_left = 16
var hover := normal.duplicate() as StyleBoxFlat
hover.border_color = Brand.SKY_BLUE
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
func _add_exam_option(option_name: String, title_text: String, description: String, icon_name: String, available: bool) -> void:
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(470, 178)
card.set_meta("medmind_wide_minimum", Vector2(470, 178))
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.add_theme_stylebox_override("panel", _exam_card_box(available))
card.set_meta("medmind_role", "exam_card")
card.set_meta("medmind_enabled", available)
themed_controls.append(card)
responsive_cards.append(card)
if available:
exam_grid.add_child(card)
else:
exam_top_row.add_child(card)
var content := VBoxContainer.new()
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_theme_constant_override("separation", 10)
card.add_child(content)
var header := BoxContainer.new()
header.vertical = false
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
header.add_theme_constant_override("separation", 20)
content.add_child(header)
responsive_card_rows.append(header)
var icon_panel := PanelContainer.new()
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon_panel.custom_minimum_size = Vector2(78, 78)
var icon_box := Brand.icon_circle_box(UIThemeService.is_dark_mode(), Brand.ROYAL_BLUE if available else Brand.PURPLE, 39)
icon_box.content_margin_left = 20
icon_box.content_margin_right = 20
icon_box.content_margin_top = 20
icon_box.content_margin_bottom = 20
icon_panel.add_theme_stylebox_override("panel", icon_box)
icon_panel.set_meta("medmind_role", "exam_icon_circle")
icon_panel.set_meta("medmind_enabled", available)
themed_controls.append(icon_panel)
header.add_child(icon_panel)
var icon := TextureRect.new()
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.modulate = Brand.ROYAL_BLUE if available else Brand.PURPLE
icon.set_meta("medmind_role", "exam_icon")
icon.set_meta("medmind_enabled", available)
themed_controls.append(icon)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_panel.add_child(icon)
var details := VBoxContainer.new()
details.mouse_filter = Control.MOUSE_FILTER_IGNORE
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.alignment = BoxContainer.ALIGNMENT_CENTER
details.add_theme_constant_override("separation", 8)
header.add_child(details)
var eyebrow := Label.new()
eyebrow.mouse_filter = Control.MOUSE_FILTER_IGNORE
eyebrow.text = option_name
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 16)
eyebrow.add_theme_color_override("font_color", Brand.ROYAL_BLUE)
details.add_child(eyebrow)
var title := Label.new()
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
title.text = title_text
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 22)
details.add_child(title)
var status := Label.new()
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
status.text = "Available" if available else "Coming Soon"
status.add_theme_font_override("font", Brand.font)
status.add_theme_font_size_override("font_size", 14)
status.add_theme_color_override("font_color", Brand.SUCCESS if available else Brand.PURPLE)
status.add_theme_stylebox_override("normal", Brand.badge_box(UIThemeService.is_dark_mode(), Brand.SUCCESS if available else Brand.PURPLE))
status.set_meta("medmind_role", "exam_badge")
status.set_meta("medmind_enabled", available)
themed_controls.append(status)
details.add_child(status)
var arrow := TextureRect.new()
arrow.mouse_filter = Control.MOUSE_FILTER_IGNORE
arrow.texture = preload("res://assets/icons/lucide/chevron-right.svg")
arrow.modulate = Brand.TEAL if UIThemeService.is_dark_mode() and available else Brand.ROYAL_BLUE
arrow.set_meta("medmind_role", "exam_arrow")
arrow.set_meta("medmind_enabled", available)
themed_controls.append(arrow)
arrow.custom_minimum_size = Vector2(32, 32)
arrow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
arrow.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
header.add_child(arrow)
card.tooltip_text = description
var action := Button.new()
action.text = ""
action.flat = true
action.disabled = not available
action.tooltip_text = "Open Step 3 study modes" if available else "Coming Soon"
action.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
if available:
action.pressed.connect(func(): _show_stage("step3"))
card.add_child(action)
func _exam_card_box(available: bool) -> StyleBoxFlat:
var box := Brand.content_card_box(UIThemeService.is_dark_mode(), available, Brand.ROYAL_BLUE)
box.content_margin_left = 24
box.content_margin_right = 24
box.content_margin_top = 20
box.content_margin_bottom = 20
return box
func refresh_theme_controls() -> void:
var dark := UIThemeService.is_dark_mode()
for control in themed_controls:
if not is_instance_valid(control):
continue
var role := str(control.get_meta("medmind_role", ""))
var accent: Color = control.get_meta("medmind_accent", Brand.ROYAL_BLUE)
var enabled := bool(control.get_meta("medmind_enabled", false))
var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE
match role:
"mode_card": control.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent))
"exam_card": control.add_theme_stylebox_override("panel", _exam_card_box(enabled))
"icon_circle": control.add_theme_stylebox_override("panel", _circle_box(visual_accent, 36))
"arrow_circle": control.add_theme_stylebox_override("panel", _circle_box(visual_accent, 24, enabled))
"exam_icon_circle": control.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE if enabled else Brand.PURPLE, 39))
"badge":
var badge_accent := Brand.SUCCESS if enabled else accent
control.add_theme_stylebox_override("normal", _badge_box(badge_accent))
control.add_theme_color_override("font_color", badge_accent)
"exam_badge": control.add_theme_stylebox_override("normal", Brand.badge_box(dark, Brand.SUCCESS if enabled else Brand.PURPLE))
"muted": control.add_theme_color_override("font_color", Brand.muted_color(dark))
"muted_rich": control.add_theme_color_override("default_color", Brand.muted_color(dark))
"accent_icon": control.modulate = visual_accent
"exam_icon": control.modulate = Brand.ROYAL_BLUE if enabled else Brand.PURPLE
"arrow_icon": control.modulate = Color.WHITE if dark else Brand.ROYAL_BLUE
"exam_arrow": control.modulate = Brand.TEAL if dark and enabled else Brand.ROYAL_BLUE
"back_bar": _style_back_bar(control as Button)
for node in [exam_view, mode_view]:
if node != null:
for child in node.find_children("*", "Label", true, false):
var label := child as Label
if not label.has_meta("medmind_role"):
label.add_theme_color_override("font_color", Brand.text_color(dark))
func _show_stage(stage_name: String) -> void:
current_stage = stage_name
stage_changed.emit(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 _select_ccs() -> void:
pending_ccs_launch = true
ccs_button.disabled = true
ccs_status_label.text = "Selecting a timed case..."
ccs_status_label.visible = true
CCSService.create_attempt("", "timed")
func _on_ccs_attempt_changed() -> void:
if not pending_ccs_launch or CCSService.attempt.is_empty():
return
pending_ccs_launch = false
ccs_button.disabled = false
ccs_status_label.text = ""
ccs_status_label.visible = false
main.show_screen("ccs_player")
func _on_ccs_failed(message: String) -> void:
if not pending_ccs_launch:
return
pending_ccs_launch = false
ccs_button.disabled = false
ccs_status_label.text = message
ccs_status_label.visible = true
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 _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 ""
bank_error_label.visible = not bank_error_label.text.is_empty()
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 = ""
bank_status_label.visible = not bank_status_label.text.is_empty()
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
var compact_cards := size.x < 520.0
if exam_top_row != null:
exam_top_row.vertical = column_count == 1
if mode_grid != null:
mode_grid.columns = column_count
for card in responsive_cards:
if is_instance_valid(card):
card.custom_minimum_size = Vector2(0, 0) if compact_cards else card.get_meta("medmind_wide_minimum", Vector2(430, 178))
for row in responsive_card_rows:
if is_instance_valid(row):
row.vertical = compact_cards
row.alignment = BoxContainer.ALIGNMENT_BEGIN if compact_cards else BoxContainer.ALIGNMENT_CENTER