extends VBoxContainer const Style := preload("res://scripts/ui/ui_style.gd") const FEEDBACK_FADE_SECONDS := 0.45 const CORRECT_ADVANCE_DELAY_SECONDS := 0.6 var main: Control var choice_buttons: Array[Button] = [] var choice_percent_labels: Array[Label] = [] var choice_top_labels: Array[Label] = [] var choice_followup_panels: Array[PanelContainer] = [] var choice_followup_contents: Array[VBoxContainer] = [] var eliminated: Array = [] var locked_wrong_choice_ids: Array = [] var followup_states := {} var active_followup_choice_id := "" var followup_required := false var followup_answered := false var flagged := false var selected_choice := "" var current_answer := {} var question_highlights := {} var reported_question_ids := [] var stem_plain_text := "" var highlight_drag_started_in_stem := false var timer_color_state := "" var pending_correct_advance := false var progress_label: Label var timer_label: Label var stem_label: RichTextLabel var lead_label: Label var explanation_label: RichTextLabel var question_elo_label: Label var next_button: Button var skip_button: Button var labs_button: Button var flag_button: Button var report_button: Button var nav_palette: HBoxContainer var question_content: VBoxContainer var media_container: VBoxContainer var timer_bar: ProgressBar var feedback_fog: ColorRect var labs_overlay: Control var labs_panel: PanelContainer var labs_content: VBoxContainer var media_requests: Array[HTTPRequest] = [] var red_fog_time := 0.0 var green_fog_time := 0.0 var correct_advance_delay := 0.0 var shake_time := 0.0 var shake_origin := Vector2.ZERO func _ready() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) add_theme_constant_override("separation", 10) if not QuestionBankService.choice_percentages_updated.is_connected(_on_choice_percentages_updated): QuestionBankService.choice_percentages_updated.connect(_on_choice_percentages_updated) if not QuestionBankService.question_rating_updated.is_connected(_on_question_rating_updated): QuestionBankService.question_rating_updated.connect(_on_question_rating_updated) if not QuestionBankService.question_skipped.is_connected(_on_question_skipped): QuestionBankService.question_skipped.connect(_on_question_skipped) var top := HBoxContainer.new() top.add_theme_constant_override("separation", 12) progress_label = Label.new() Style.set_label(progress_label, 18, Style.CYAN) progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(progress_label) timer_label = Label.new() Style.set_label(timer_label, 18, Style.AMBER) timer_label.visible = false timer_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT top.add_child(timer_label) var toolbar_spacer := Control.new() toolbar_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL top.add_child(toolbar_spacer) var toolbar := HBoxContainer.new() toolbar.add_theme_constant_override("separation", 8) toolbar.alignment = BoxContainer.ALIGNMENT_END labs_button = Button.new() labs_button.tooltip_text = "Review lab results" labs_button.icon = _make_labs_icon() labs_button.custom_minimum_size = Vector2(44, 44) Style.style_chip(labs_button, Style.LIME) labs_button.pressed.connect(_show_labs_modal) toolbar.add_child(labs_button) var clear_highlight_button := Button.new() clear_highlight_button.tooltip_text = "Clear highlights" clear_highlight_button.icon = _make_clear_highlights_icon() clear_highlight_button.custom_minimum_size = Vector2(44, 44) Style.style_chip(clear_highlight_button, Style.CYAN) clear_highlight_button.pressed.connect(_clear_current_highlights) toolbar.add_child(clear_highlight_button) flag_button = Button.new() flag_button.toggle_mode = true flag_button.tooltip_text = "Flag question" flag_button.icon = _make_flag_icon() flag_button.custom_minimum_size = Vector2(44, 44) Style.style_chip(flag_button, Style.AMBER) flag_button.toggled.connect(func(value: bool): flagged = value _refresh_toolbar_state() ) toolbar.add_child(flag_button) report_button = Button.new() report_button.tooltip_text = "Report question" report_button.icon = _make_report_icon() report_button.custom_minimum_size = Vector2(44, 44) report_button.pressed.connect(_toggle_report_current_question) toolbar.add_child(report_button) top.add_child(toolbar) add_child(top) nav_palette = HBoxContainer.new() nav_palette.add_theme_constant_override("separation", 6) add_child(nav_palette) var question_panel := PanelContainer.new() question_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("182235"), Color("344463"))) question_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL add_child(question_panel) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED question_panel.add_child(scroll) var content_margin := MarginContainer.new() content_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_margin.add_theme_constant_override("margin_right", 16) scroll.add_child(content_margin) question_content = VBoxContainer.new() question_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL question_content.add_theme_constant_override("separation", 12) content_margin.add_child(question_content) stem_label = RichTextLabel.new() stem_label.fit_content = true stem_label.bbcode_enabled = true stem_label.selection_enabled = true stem_label.scroll_active = false stem_label.custom_minimum_size = Vector2(0, 96) question_content.add_child(stem_label) media_container = VBoxContainer.new() media_container.visible = false media_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL media_container.add_theme_constant_override("separation", 10) question_content.add_child(media_container) lead_label = Label.new() lead_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(lead_label, 21, Style.TEXT) question_content.add_child(lead_label) question_elo_label = Label.new() question_elo_label.visible = false question_elo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT Style.set_label(question_elo_label, 16, Style.AMBER) question_content.add_child(question_elo_label) for index in range(5): var button := Button.new() button.custom_minimum_size = Vector2(0, 64) button.alignment = HORIZONTAL_ALIGNMENT_LEFT Style.style_answer_button(button) button.pressed.connect(func(i := index): _select_choice(i)) button.gui_input.connect(func(event: InputEvent, i := index): _choice_gui_input(event, i)) choice_buttons.append(button) var percent_label := Label.new() percent_label.visible = false percent_label.mouse_filter = Control.MOUSE_FILTER_IGNORE percent_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT percent_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER percent_label.anchor_left = 1.0 percent_label.anchor_top = 0.0 percent_label.anchor_right = 1.0 percent_label.anchor_bottom = 0.0 percent_label.offset_left = -92 percent_label.offset_top = 8 percent_label.offset_right = -18 percent_label.offset_bottom = 40 Style.set_label(percent_label, 18, Style.TEXT) button.add_child(percent_label) choice_percent_labels.append(percent_label) var top_label := Label.new() top_label.visible = false top_label.mouse_filter = Control.MOUSE_FILTER_IGNORE top_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT top_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP top_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART top_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) top_label.offset_left = 14 top_label.offset_top = 10 top_label.offset_right = -96 top_label.offset_bottom = -252 Style.set_label(top_label, 16, Style.RED) button.add_child(top_label) choice_top_labels.append(top_label) var followup_panel := PanelContainer.new() followup_panel.visible = false followup_panel.mouse_filter = Control.MOUSE_FILTER_STOP followup_panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) followup_panel.offset_left = 16 followup_panel.offset_top = 48 followup_panel.offset_right = -16 followup_panel.offset_bottom = -16 followup_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("26151a"), Style.RED)) button.add_child(followup_panel) choice_followup_panels.append(followup_panel) var followup_content := VBoxContainer.new() followup_content.add_theme_constant_override("separation", 6) followup_panel.add_child(followup_content) choice_followup_contents.append(followup_content) question_content.add_child(button) explanation_label = RichTextLabel.new() explanation_label.bbcode_enabled = true explanation_label.fit_content = true explanation_label.scroll_active = false explanation_label.custom_minimum_size = Vector2(0, 150) explanation_label.visible = false question_content.add_child(explanation_label) var actions := HBoxContainer.new() actions.add_theme_constant_override("separation", 10) skip_button = Button.new() skip_button.text = "Skip" skip_button.custom_minimum_size = Vector2(120, 48) Style.style_chip(skip_button, Style.AMBER) skip_button.pressed.connect(_skip_current_question) actions.add_child(skip_button) next_button = Button.new() next_button.text = "Submit" next_button.custom_minimum_size = Vector2(140, 48) next_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL Style.style_primary_button(next_button) next_button.pressed.connect(_submit_or_next) actions.add_child(next_button) add_child(actions) timer_bar = ProgressBar.new() timer_bar.show_percentage = false timer_bar.min_value = 0.0 timer_bar.max_value = 100.0 timer_bar.value = 100.0 timer_bar.custom_minimum_size = Vector2(0, 14) timer_bar.size_flags_horizontal = Control.SIZE_EXPAND_FILL timer_bar.add_theme_stylebox_override("background", Style.button_box(Color("232b3d"), Color("303b53"))) timer_bar.add_theme_stylebox_override("fill", Style.button_box(Style.LIME, Style.LIME)) add_child(timer_bar) feedback_fog = ColorRect.new() feedback_fog.color = Color(1.0, 0.0, 0.0, 0.0) feedback_fog.mouse_filter = Control.MOUSE_FILTER_IGNORE feedback_fog.top_level = true feedback_fog.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(feedback_fog) _build_labs_modal() func on_show(_payload: Variant = null) -> void: _load_question() func _process(delta: float) -> void: _update_answer_feedback(delta) if visible and BlockSessionService.block_started and BlockSessionService.session_kind == "core": _update_timer_bar() if visible and BlockSessionService.block_started and BlockSessionService.mode == "Timed": BlockSessionService.seconds_remaining = max(0.0, BlockSessionService.seconds_remaining - delta) var shown_seconds := int(ceil(BlockSessionService.seconds_remaining)) var minutes := int(floor(float(shown_seconds) / 60.0)) timer_label.text = "Timed: %02d:%02d" % [minutes, shown_seconds % 60] if BlockSessionService.seconds_remaining <= 0: _finish() func _input(event: InputEvent) -> void: if labs_overlay != null and labs_overlay.visible and event.is_action_pressed("ui_cancel"): _hide_labs_modal() get_viewport().set_input_as_handled() return if not visible or pending_correct_advance or stem_label == null: return var mouse_event := event as InputEventMouseButton if mouse_event == null or mouse_event.button_index != MOUSE_BUTTON_LEFT: return if mouse_event.pressed: highlight_drag_started_in_stem = stem_label.get_global_rect().has_point(mouse_event.global_position) elif highlight_drag_started_in_stem: highlight_drag_started_in_stem = false call_deferred("_commit_current_text_selection") func _load_question() -> void: _reset_wrong_feedback() _hide_labs_modal() eliminated.clear() locked_wrong_choice_ids.clear() _clear_followups() flagged = false selected_choice = "" current_answer = {} flag_button.button_pressed = false _refresh_toolbar_state() explanation_label.visible = false question_elo_label.visible = false question_elo_label.text = "" next_button.text = "Submit" next_button.disabled = false skip_button.disabled = false skip_button.visible = BlockSessionService.session_kind == "core" var question := BlockSessionService.get_current_question() if question.is_empty(): main.show_screen("dashboard") return progress_label.visible = BlockSessionService.session_kind != "core" progress_label.text = "" if BlockSessionService.session_kind == "core" else "Question %s" % 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" _update_timer_bar() stem_plain_text = str(question.get("stem", "")) _render_stem() _load_question_media(question) lead_label.text = question.get("lead_in", "") _refresh_labs_button() var choices: Array = question.get("choices", []) for index in range(choice_buttons.size()): var button := choice_buttons[index] button.visible = index < choices.size() button.disabled = false if index < choices.size(): var choice: Dictionary = choices[index] _set_choice_button_text(index, choice) button.modulate = Color.WHITE var saved_answer = BlockSessionService.get_answer_for_index(BlockSessionService.current_index) if saved_answer != null: current_answer = saved_answer selected_choice = str(saved_answer.get("selected_choice_id", "")) eliminated = saved_answer.get("eliminated_choice_ids", []).duplicate() flagged = bool(saved_answer.get("flagged", false)) flag_button.button_pressed = flagged _refresh_toolbar_state() if BlockSessionService.mode == "Tutor": _show_explanation() next_button.text = "Next" else: _apply_saved_choice_state() next_button.text = "Next" _update_question_elo_label(true) _refresh_nav_palette() _refresh_toolbar_state() func _select_choice(index: int) -> void: if pending_correct_advance or followup_required: return var question := BlockSessionService.get_current_question() var choices: Array = question.get("choices", []) if index >= choices.size() or (BlockSessionService.mode == "Tutor" and not current_answer.is_empty()): return if locked_wrong_choice_ids.has(str(choices[index].get("id", ""))): return selected_choice = str(choices[index].get("id", "")) if BlockSessionService.mode == "Timed": current_answer = {} next_button.text = "Submit" _apply_saved_choice_state() func _skip_current_question() -> void: if pending_correct_advance or not current_answer.is_empty() or BlockSessionService.session_kind != "core": return var question_id := str(BlockSessionService.get_current_question().get("id", "")) if question_id == "": return current_answer = BlockSessionService.skip_core_question(eliminated, flagged) if current_answer.is_empty(): return QuestionBankService.skip_question(question_id) selected_choice = "" next_button.disabled = true skip_button.disabled = true pending_correct_advance = true correct_advance_delay = CORRECT_ADVANCE_DELAY_SECONDS _show_explanation() _update_question_elo_label(true) func _choice_gui_input(event: InputEvent, index: int) -> void: if pending_correct_advance or followup_required: return 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(): return var question := BlockSessionService.get_current_question() var choices: Array = question.get("choices", []) if index >= choices.size(): return var choice: Dictionary = choices[index] var choice_id := str(choice.get("id", "")) if locked_wrong_choice_ids.has(choice_id): return if eliminated.has(choice_id): eliminated.erase(choice_id) else: eliminated.append(choice_id) _set_choice_button_text(index, choice) _apply_saved_choice_state() func _toggle_eliminate_selected() -> void: if pending_correct_advance or followup_required or selected_choice == "" or not current_answer.is_empty(): return var question := BlockSessionService.get_current_question() var choices: Array = question.get("choices", []) for index in range(choices.size()): if str(choices[index].get("id", "")) == selected_choice: if eliminated.has(selected_choice): eliminated.erase(selected_choice) else: eliminated.append(selected_choice) _set_choice_button_text(index, choices[index]) _apply_saved_choice_state() func _submit_or_next() -> void: if pending_correct_advance: return if current_answer.is_empty(): if selected_choice == "": return if BlockSessionService.session_kind == "core": _collapse_resolved_followups() var attempted_choice := selected_choice var attempted_question_id := str(BlockSessionService.get_current_question().get("id", "")) QuestionBankService.record_first_attempt(attempted_question_id, attempted_choice) var result := BlockSessionService.submit_core_attempt(selected_choice, eliminated, flagged) if result.is_empty(): return var answer: Dictionary = result.get("answer", {}) if bool(result.get("is_correct", false)): current_answer = answer _update_question_elo_label(true) _trigger_correct_feedback() return _lock_wrong_choice(attempted_choice) _show_wrong_answer_followup(attempted_choice) selected_choice = "" current_answer = {} next_button.text = "Submit" next_button.disabled = true _trigger_wrong_feedback() _apply_saved_choice_state() return current_answer = BlockSessionService.answer_current(selected_choice, eliminated, flagged) if BlockSessionService.mode == "Tutor": _show_explanation() next_button.text = "Next" else: next_button.text = "Next" _refresh_nav_palette() else: _advance_or_finish() func _show_explanation() -> void: var question := BlockSessionService.get_current_question() var correct_id := str(question.get("correct_choice_id", "")) for button in choice_buttons: button.disabled = true for index in range(question.get("choices", []).size()): var choice: Dictionary = question["choices"][index] var id := str(choice.get("id", "")) _set_choice_button_text(index, choice) if id == correct_id: choice_buttons[index].modulate = Color(0.54, 1.0, 0.58) elif id == selected_choice: choice_buttons[index].modulate = Color(1.0, 0.48, 0.48) var result_text := "Correct" if current_answer.get("is_correct", false) else "Incorrect" explanation_label.text = "[b]%s[/b]\n%s" % [result_text, question.get("explanation", "")] explanation_label.visible = true _update_question_elo_label(true) _refresh_nav_palette() func _advance_or_finish() -> void: if BlockSessionService.move_next(): _load_question() else: _finish() func _finish() -> void: if BlockSessionService.session_kind == "core": BlockSessionService.finish_block() main.show_screen("dashboard") return var results := BlockSessionService.finish_block() main.show_screen("review", results) func _exit_run() -> void: BlockSessionService.resume_current_question_timer() BlockSessionService.finish_block() main.show_screen("dashboard") func _update_timer_bar() -> void: if timer_bar == null: return var ratio := BlockSessionService.get_core_timer_ratio() timer_bar.value = ratio * 100.0 var color_state := "green" var fill_color := Style.LIME if ratio < 0.25: color_state = "red" fill_color = Style.RED elif ratio <= 0.5: color_state = "amber" fill_color = Style.AMBER if color_state != timer_color_state: timer_color_state = color_state timer_bar.add_theme_stylebox_override("fill", Style.button_box(fill_color, fill_color)) func _lock_wrong_choice(choice_id: String) -> void: if choice_id == "": return if not locked_wrong_choice_ids.has(choice_id): locked_wrong_choice_ids.append(choice_id) func _trigger_wrong_feedback() -> void: shake_time = 0.28 shake_origin = position green_fog_time = 0.0 red_fog_time = FEEDBACK_FADE_SECONDS if feedback_fog != null: feedback_fog.color = Color(1.0, 0.0, 0.0, 0.26) func _trigger_correct_feedback() -> void: pending_correct_advance = true correct_advance_delay = CORRECT_ADVANCE_DELAY_SECONDS red_fog_time = 0.0 green_fog_time = FEEDBACK_FADE_SECONDS next_button.disabled = true if skip_button != null: skip_button.disabled = true for button in choice_buttons: button.disabled = true if feedback_fog != null: feedback_fog.color = Color(0.25, 1.0, 0.25, 0.23) func _update_answer_feedback(delta: float) -> void: if shake_time > 0.0: shake_time = max(0.0, shake_time - delta) if shake_time > 0.0: position = shake_origin + Vector2(randf_range(-9.0, 9.0), randf_range(-4.0, 4.0)) else: position = shake_origin if red_fog_time > 0.0 and feedback_fog != null: red_fog_time = max(0.0, red_fog_time - delta) var red_alpha := 0.26 * (red_fog_time / FEEDBACK_FADE_SECONDS) feedback_fog.color = Color(1.0, 0.0, 0.0, red_alpha) elif green_fog_time > 0.0 and feedback_fog != null: green_fog_time = max(0.0, green_fog_time - delta) var green_alpha := 0.23 * (green_fog_time / FEEDBACK_FADE_SECONDS) feedback_fog.color = Color(0.25, 1.0, 0.25, green_alpha) if pending_correct_advance: correct_advance_delay = max(0.0, correct_advance_delay - delta) if correct_advance_delay <= 0.0: pending_correct_advance = false next_button.disabled = false if skip_button != null: skip_button.disabled = false _advance_or_finish() func _reset_wrong_feedback() -> void: if shake_time > 0.0: position = shake_origin shake_time = 0.0 red_fog_time = 0.0 green_fog_time = 0.0 correct_advance_delay = 0.0 pending_correct_advance = false next_button.disabled = false if skip_button != null: skip_button.disabled = false if feedback_fog != null: feedback_fog.color = Color(1.0, 0.0, 0.0, 0.0) func _commit_current_text_selection() -> void: if stem_label == null: return var raw_selected_text := stem_label.get_selected_text() var selected_text := raw_selected_text.strip_edges() if selected_text == "": return var left_trimmed_text := raw_selected_text.strip_edges(true, false) var leading_trim_count := raw_selected_text.length() - left_trimmed_text.length() var selection_start := stem_label.get_selection_from() + leading_trim_count _highlight_range(selection_start, selection_start + selected_text.length()) func _highlight_range(start: int, end: int) -> void: var question_id := str(BlockSessionService.get_current_question().get("id", "")) if not question_highlights.has(question_id): question_highlights[question_id] = [] var highlights: Array = question_highlights[question_id] start = clamp(start, 0, stem_plain_text.length()) end = clamp(end, start, stem_plain_text.length()) if start == end: return if _subtract_highlight_range(highlights, start, end): _render_stem() if stem_label.has_method("deselect"): stem_label.deselect() return highlights.append({"start": start, "end": end}) _render_stem() if stem_label.has_method("deselect"): stem_label.deselect() func _clear_current_highlights() -> void: var question_id := str(BlockSessionService.get_current_question().get("id", "")) question_highlights[question_id] = [] _render_stem() func _toggle_report_current_question() -> void: var question_id := str(BlockSessionService.get_current_question().get("id", "")) if question_id == "": return if reported_question_ids.has(question_id): reported_question_ids.erase(question_id) else: reported_question_ids.append(question_id) _refresh_toolbar_state() func _refresh_toolbar_state() -> void: _refresh_labs_button() if flag_button != null: Style.style_chip(flag_button, Style.AMBER if flagged else Style.CYAN) if report_button != null: var question_id := str(BlockSessionService.get_current_question().get("id", "")) Style.style_chip(report_button, Style.RED if reported_question_ids.has(question_id) else Style.CYAN) func _refresh_labs_button() -> void: if labs_button == null: return labs_button.visible = _current_question_has_labs() func _current_question_has_labs() -> bool: var question := BlockSessionService.get_current_question() if question.is_empty(): return false var labs: Array = question.get("labs", []) return not labs.is_empty() func _load_question_media(question: Dictionary) -> void: _clear_question_media() var media: Array = question.get("media", []) if media.is_empty(): return media_container.visible = true var question_id := str(question.get("id", "")) for item in media: var media_url := _resolve_media_url(str(item)) if media_url == "": continue var frame := PanelContainer.new() frame.size_flags_horizontal = Control.SIZE_EXPAND_FILL frame.add_theme_stylebox_override("panel", Style.panel_box(Color("111a2a"), Color("344463"))) media_container.add_child(frame) var image_box := VBoxContainer.new() image_box.add_theme_constant_override("separation", 8) frame.add_child(image_box) var status := Label.new() status.text = "Loading image..." status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER Style.set_label(status, 15, Style.MUTED) image_box.add_child(status) var texture_rect := TextureRect.new() texture_rect.visible = false texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED texture_rect.custom_minimum_size = Vector2(0, 260) texture_rect.size_flags_horizontal = Control.SIZE_EXPAND_FILL image_box.add_child(texture_rect) var request := HTTPRequest.new() request.request_completed.connect( func(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray): _on_media_loaded(result, response_code, body, question_id, texture_rect, status) ) media_requests.append(request) add_child(request) var err := request.request(media_url) if err != OK: status.text = "Image unavailable." func _clear_question_media() -> void: for request in media_requests: if is_instance_valid(request): request.queue_free() media_requests.clear() if media_container == null: return for child in media_container.get_children(): media_container.remove_child(child) child.queue_free() media_container.visible = false func _resolve_media_url(value: String) -> String: var media_url := value.strip_edges() if media_url == "": return "" if media_url.begins_with("http://") or media_url.begins_with("https://"): return media_url if not media_url.begins_with("/"): media_url = "/%s" % media_url var base_url := QuestionBankService.question_api_base_url var scheme_end := base_url.find("://") if scheme_end < 0: return media_url var path_start := base_url.find("/", scheme_end + 3) var origin := base_url if path_start < 0 else base_url.substr(0, path_start) return "%s%s" % [origin, media_url] func _on_media_loaded( result: int, response_code: int, body: PackedByteArray, question_id: String, texture_rect: TextureRect, status: Label) -> void: if str(BlockSessionService.get_current_question().get("id", "")) != question_id: return if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: status.text = "Image unavailable." return var image := Image.new() var err := image.load_png_from_buffer(body) if err != OK: err = image.load_jpg_from_buffer(body) if err != OK: err = image.load_webp_from_buffer(body) if err != OK: status.text = "Image unavailable." return texture_rect.texture = ImageTexture.create_from_image(image) texture_rect.visible = true status.visible = false func _build_labs_modal() -> void: labs_overlay = Control.new() labs_overlay.visible = false labs_overlay.top_level = true labs_overlay.mouse_filter = Control.MOUSE_FILTER_STOP labs_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) labs_overlay.gui_input.connect(_labs_overlay_gui_input) add_child(labs_overlay) var dim := ColorRect.new() dim.color = Color(0.02, 0.03, 0.05, 0.72) dim.mouse_filter = Control.MOUSE_FILTER_IGNORE dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) labs_overlay.add_child(dim) var center := CenterContainer.new() center.mouse_filter = Control.MOUSE_FILTER_IGNORE center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) labs_overlay.add_child(center) labs_panel = PanelContainer.new() labs_panel.custom_minimum_size = Vector2(760, 430) labs_panel.mouse_filter = Control.MOUSE_FILTER_STOP labs_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.LIME)) center.add_child(labs_panel) var modal_body := VBoxContainer.new() modal_body.add_theme_constant_override("separation", 14) labs_panel.add_child(modal_body) var header := HBoxContainer.new() header.add_theme_constant_override("separation", 10) modal_body.add_child(header) var title := Label.new() title.text = "Lab Results" title.size_flags_horizontal = Control.SIZE_EXPAND_FILL Style.set_label(title, 26, Style.TEXT) header.add_child(title) var close_button := Button.new() close_button.text = "X" close_button.tooltip_text = "Close lab results" close_button.custom_minimum_size = Vector2(42, 38) Style.style_chip(close_button, Style.RED) close_button.pressed.connect(_hide_labs_modal) header.add_child(close_button) var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(0, 320) scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED modal_body.add_child(scroll) labs_content = VBoxContainer.new() labs_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL labs_content.add_theme_constant_override("separation", 8) scroll.add_child(labs_content) func _labs_overlay_gui_input(event: InputEvent) -> void: var mouse_event := event as InputEventMouseButton if mouse_event == null or not mouse_event.pressed or mouse_event.button_index != MOUSE_BUTTON_LEFT: return if labs_panel == null or not labs_panel.get_global_rect().has_point(mouse_event.global_position): _hide_labs_modal() get_viewport().set_input_as_handled() func _show_labs_modal() -> void: if labs_overlay == null or labs_content == null: return var question := BlockSessionService.get_current_question() var labs: Array = question.get("labs", []) if labs.is_empty(): return _populate_labs_modal(labs) labs_overlay.visible = true labs_overlay.move_to_front() func _hide_labs_modal() -> void: if labs_overlay != null: labs_overlay.visible = false func _populate_labs_modal(labs: Array) -> void: for child in labs_content.get_children(): labs_content.remove_child(child) child.queue_free() var grid := GridContainer.new() grid.columns = 3 grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL grid.add_theme_constant_override("h_separation", 16) grid.add_theme_constant_override("v_separation", 8) labs_content.add_child(grid) _add_lab_table_label(grid, "Test", Style.CYAN, true) _add_lab_table_label(grid, "Value", Style.CYAN, true) _add_lab_table_label(grid, "Reference", Style.CYAN, true) for lab in labs: if typeof(lab) != TYPE_DICTIONARY: continue var lab_dict: Dictionary = lab _add_lab_table_label(grid, str(lab_dict.get("name", "-")), Style.TEXT) _add_lab_table_label(grid, _format_lab_value(lab_dict), Style.TEXT) _add_lab_table_label(grid, str(lab_dict.get("reference", "-")), Style.MUTED) func _add_lab_table_label(parent: GridContainer, text: String, color: Color, is_header: bool = false) -> void: var label := Label.new() label.text = text if text.strip_edges() != "" else "-" label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.size_flags_horizontal = Control.SIZE_EXPAND_FILL Style.set_label(label, 16 if not is_header else 17, color) parent.add_child(label) func _format_lab_value(lab: Dictionary) -> String: var value := str(lab.get("value", "")).strip_edges() var unit := str(lab.get("unit", "")).strip_edges() if value == "" and unit == "": return "-" if unit == "": return value if value == "": return unit return "%s %s" % [value, unit] func _render_stem() -> void: var question_id := str(BlockSessionService.get_current_question().get("id", "")) var ranges: Array = question_highlights.get(question_id, []).duplicate() ranges.sort_custom(func(a: Dictionary, b: Dictionary): return int(a["start"]) < int(b["start"])) var rendered := "" var cursor := 0 for highlight_range in ranges: var start: int = clamp(int(highlight_range.get("start", 0)), 0, stem_plain_text.length()) var end: int = clamp(int(highlight_range.get("end", 0)), start, stem_plain_text.length()) if start < cursor: continue rendered += _escape_bbcode(stem_plain_text.substr(cursor, start - cursor)) rendered += "[bgcolor=yellow][color=black]%s[/color][/bgcolor]" % _escape_bbcode(stem_plain_text.substr(start, end - start)) cursor = end rendered += _escape_bbcode(stem_plain_text.substr(cursor)) stem_label.text = rendered func _escape_bbcode(value: String) -> String: return value.replace("[", "[lb]").replace("]", "[rb]") func _subtract_highlight_range(highlights: Array, start: int, end: int) -> bool: var changed := false for index in range(highlights.size() - 1, -1, -1): var highlight_range: Dictionary = highlights[index] var highlight_start := int(highlight_range.get("start", 0)) var highlight_end := int(highlight_range.get("end", 0)) if start < highlight_end and end > highlight_start: highlights.remove_at(index) if highlight_start < start: highlights.insert(index, {"start": highlight_start, "end": start}) index += 1 if end < highlight_end: highlights.insert(index, {"start": end, "end": highlight_end}) changed = true return changed func _apply_saved_choice_state() -> void: var question := BlockSessionService.get_current_question() var choices: Array = question.get("choices", []) for index in range(choices.size()): var id := str(choices[index].get("id", "")) var has_followup_panel := index < choice_followup_panels.size() and choice_followup_panels[index].visible _set_choice_button_text(index, choices[index]) choice_buttons[index].disabled = locked_wrong_choice_ids.has(id) and not has_followup_panel _style_choice_button_for_state(index, id) if locked_wrong_choice_ids.has(id): choice_buttons[index].modulate = Color(1.0, 0.25, 0.25) elif eliminated.has(id): choice_buttons[index].modulate = Color(0.45, 0.45, 0.45) elif id == selected_choice: choice_buttons[index].modulate = Color(0.68, 0.92, 1.0) else: choice_buttons[index].modulate = Color.WHITE if pending_correct_advance: _apply_correct_answer_preview() if followup_required: for index in range(choices.size()): var id := str(choices[index].get("id", "")) choice_buttons[index].disabled = id != active_followup_choice_id func _set_choice_button_text(index: int, choice: Dictionary) -> void: var choice_id := str(choice.get("id", "")) var choice_text := str(choice.get("text", "")) var has_visible_followup := index < choice_followup_panels.size() and choice_followup_panels[index].visible choice_buttons[index].custom_minimum_size = _choice_row_size(index, choice_id, has_visible_followup) _set_choice_percent_label(index, "") _set_choice_top_label(index, "") if locked_wrong_choice_ids.has(choice_id): var percentage := _get_choice_selection_percentage(choice_id) if percentage >= 0: _set_choice_percent_label(index, "%d%%" % percentage) if has_visible_followup: var wrong_text := "%s. %s" % [choice_id, choice_text] choice_buttons[index].text = "" _set_choice_top_label(index, wrong_text) else: var wrong_text := "%s. %s" % [choice_id, _strike_text(choice_text)] choice_buttons[index].text = wrong_text elif _should_show_correct_choice_percentage(choice_id): var correct_percentage := _get_choice_selection_percentage(choice_id) if correct_percentage >= 0: _set_choice_percent_label(index, "%d%%" % correct_percentage) choice_buttons[index].text = "%s. %s" % [choice_id, choice_text] elif eliminated.has(choice_id): choice_buttons[index].text = "%s. [eliminated] %s" % [choice_id, _strike_text(choice_text)] else: choice_buttons[index].text = "%s. %s" % [choice_id, choice_text] func _style_choice_button_for_state(index: int, choice_id: String) -> void: if choice_id == selected_choice: choice_buttons[index].add_theme_stylebox_override("normal", Style.button_box(Color("18465a"), Style.CYAN)) choice_buttons[index].add_theme_stylebox_override("hover", Style.button_box(Color("20647e"), Style.CYAN)) choice_buttons[index].add_theme_stylebox_override("pressed", Style.button_box(Color("18465a"), Style.CYAN)) else: Style.style_answer_button(choice_buttons[index]) func _choice_row_size(_index: int, choice_id: String, has_visible_followup: bool) -> Vector2: if not has_visible_followup and choice_id != active_followup_choice_id: return Vector2(0, 64) if followup_states.has(choice_id): var state: Dictionary = followup_states[choice_id] if bool(state.get("collapsed", false)): return Vector2(0, 136) return Vector2(0, 300) func _mark_review_input_handled() -> void: get_viewport().set_input_as_handled() func _set_choice_percent_label(index: int, text: String) -> void: if index < 0 or index >= choice_percent_labels.size(): return var label := choice_percent_labels[index] label.text = text label.visible = text != "" func _set_choice_top_label(index: int, text: String) -> void: if index < 0 or index >= choice_top_labels.size(): return var label := choice_top_labels[index] label.text = text label.visible = text != "" func _get_choice_selection_percentage(choice_id: String) -> int: var question := BlockSessionService.get_current_question() var percentages: Dictionary = question.get("choice_selection_percentages", {}) if percentages.is_empty(): return -1 if not percentages.has(choice_id): return 0 return clamp(int(percentages.get(choice_id, -1)), 0, 100) func _should_show_correct_choice_percentage(choice_id: String) -> bool: var question := BlockSessionService.get_current_question() return choice_id == str(question.get("correct_choice_id", "")) and (pending_correct_advance or not current_answer.is_empty()) func _apply_correct_answer_preview() -> void: var question := BlockSessionService.get_current_question() var correct_id := str(question.get("correct_choice_id", "")) var choices: Array = question.get("choices", []) for index in range(choices.size()): if str(choices[index].get("id", "")) == correct_id: choice_buttons[index].modulate = Color(0.54, 1.0, 0.58) return func _on_choice_percentages_updated(question_id: String, percentages: Dictionary) -> void: if not visible: return if str(BlockSessionService.get_current_question().get("id", "")) != question_id: return BlockSessionService.update_question_choice_percentages(question_id, percentages) _apply_saved_choice_state() func _show_wrong_answer_followup(choice_id: String) -> void: active_followup_choice_id = choice_id followup_required = true followup_answered = false var index := _choice_index_for_id(choice_id) if index < 0: _unlock_after_followup() return _build_followup_panel(index, choice_id) func _build_followup_panel(index: int, choice_id: String) -> void: _clear_followup_content(index) var panel := choice_followup_panels[index] var content := choice_followup_contents[index] var followup := _get_followup_for_choice(choice_id) followup_states[choice_id] = { "choice_id": choice_id, "choice_text": _choice_text_for_id(choice_id), "prompt": str(followup.get("prompt", "")), "choices": followup.get("choices", []), "correct_choice_id": str(followup.get("correct_choice_id", "")), "teaching_point": str(followup.get("teaching_point", "")), "selected_option_id": "", "selected_option_text": "", "was_correct": false, "resolved": false, "collapsed": false, "has_authored_choices": bool(followup.get("has_authored_choices", false)) } var prompt := Label.new() prompt.text = str(followup.get("prompt", "What finding in the stem makes this answer less appropriate?")) prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(prompt, 14, Style.TEXT) content.add_child(prompt) var choices: Array = followup.get("choices", []) if bool(followup.get("has_authored_choices", false)): for option in choices: if typeof(option) != TYPE_DICTIONARY: continue var option_button := Button.new() option_button.text = "%s. %s" % [str(option.get("id", "")), str(option.get("text", ""))] option_button.alignment = HORIZONTAL_ALIGNMENT_LEFT option_button.custom_minimum_size = Vector2(0, 32) Style.style_chip(option_button, Style.CYAN) option_button.set_meta("option_id", str(option.get("id", ""))) option_button.pressed.connect(func(option_id := str(option.get("id", "")), selected_button := option_button, followup_data := followup): _answer_followup(option_id, selected_button, followup_data) ) content.add_child(option_button) if not bool(followup.get("has_authored_choices", false)): var action_row := HBoxContainer.new() action_row.add_theme_constant_override("separation", 8) content.add_child(action_row) var continue_button := Button.new() continue_button.text = "Continue" continue_button.custom_minimum_size = Vector2(130, 32) Style.style_chip(continue_button, Style.CYAN) continue_button.pressed.connect(func(followup_data := followup): _resolve_followup(followup_data, null, false, false)) action_row.add_child(continue_button) panel.visible = true func _answer_followup(option_id: String, selected_button: Button, followup: Dictionary) -> void: if not followup_required or followup_answered: return var correct_id := str(followup.get("correct_choice_id", "")) var is_correct := option_id == correct_id _resolve_followup(followup, selected_button, true, is_correct) func _resolve_followup(followup: Dictionary, selected_button: Button, style_selected: bool, is_correct: bool) -> void: if not followup_required or followup_answered: return followup_answered = true var resolved_choice_id := active_followup_choice_id var selected_option_text := selected_button.text if selected_button != null else "Continued" var index := _choice_index_for_id(active_followup_choice_id) if index >= 0: for child in choice_followup_contents[index].get_children(): if child is Button: child.disabled = true elif child is HBoxContainer: for row_child in child.get_children(): if row_child is Button: row_child.disabled = true if selected_button != null and style_selected: _style_followup_result_button(selected_button, is_correct) var teaching := Label.new() teaching.text = str(followup.get("teaching_point", "Review the relevant stem finding before choosing again.")) teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(teaching, 14, Style.AMBER) choice_followup_contents[index].add_child(teaching) if followup_states.has(resolved_choice_id): var state: Dictionary = followup_states[resolved_choice_id] state["selected_option_id"] = str(selected_button.get_meta("option_id", "")) if selected_button != null and selected_button.has_meta("option_id") else "" state["selected_option_text"] = selected_option_text state["was_correct"] = is_correct state["resolved"] = true state["collapsed"] = false followup_states[resolved_choice_id] = state _unlock_after_followup() func _style_followup_result_button(button: Button, is_correct: bool) -> void: var fill := Color("1f7a3a") if is_correct else Color("8f1d2c") var border := Style.LIME if is_correct else Style.RED button.add_theme_stylebox_override("normal", Style.button_box(fill, border)) button.add_theme_stylebox_override("hover", Style.button_box(fill, border)) button.add_theme_stylebox_override("disabled", Style.button_box(fill, border)) button.add_theme_color_override("font_color", Style.TEXT) func _unlock_after_followup() -> void: active_followup_choice_id = "" followup_required = false next_button.disabled = false if skip_button != null: skip_button.disabled = false _apply_saved_choice_state() func _collapse_resolved_followups() -> void: for choice_id in followup_states.keys(): var state: Dictionary = followup_states[choice_id] if bool(state.get("resolved", false)): state["collapsed"] = true followup_states[choice_id] = state _render_followup_state(choice_id) _apply_saved_choice_state() func _toggle_followup_state(choice_id: String) -> void: if not followup_states.has(choice_id): return var state: Dictionary = followup_states[choice_id] state["collapsed"] = not bool(state.get("collapsed", false)) followup_states[choice_id] = state _render_followup_state(choice_id) _apply_saved_choice_state() func _render_followup_state(choice_id: String) -> void: if not followup_states.has(choice_id): return var index := _choice_index_for_id(choice_id) if index < 0 or index >= choice_followup_panels.size(): return var state: Dictionary = followup_states[choice_id] _clear_followup_content(index) if bool(state.get("collapsed", false)): _build_collapsed_followup(index, state) else: _build_readonly_followup(index, state) choice_followup_panels[index].visible = true func _build_collapsed_followup(index: int, state: Dictionary) -> void: var content := choice_followup_contents[index] var summary := Button.new() summary.text = "Review: %s. %s" % [str(state.get("choice_id", "")), str(state.get("choice_text", ""))] summary.alignment = HORIZONTAL_ALIGNMENT_LEFT summary.custom_minimum_size = Vector2(0, 40) Style.style_chip(summary, Style.AMBER) summary.gui_input.connect(func(_event: InputEvent): _mark_review_input_handled()) summary.pressed.connect(func(choice_id := str(state.get("choice_id", ""))): _toggle_followup_state(choice_id)) content.add_child(summary) func _build_readonly_followup(index: int, state: Dictionary) -> void: var content := choice_followup_contents[index] var summary := Button.new() summary.text = "Hide review: %s. %s" % [str(state.get("choice_id", "")), str(state.get("choice_text", ""))] summary.alignment = HORIZONTAL_ALIGNMENT_LEFT summary.custom_minimum_size = Vector2(0, 36) Style.style_chip(summary, Style.AMBER) summary.gui_input.connect(func(_event: InputEvent): _mark_review_input_handled()) summary.pressed.connect(func(choice_id := str(state.get("choice_id", ""))): _toggle_followup_state(choice_id)) content.add_child(summary) var prompt := Label.new() prompt.text = str(state.get("prompt", "")) prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(prompt, 14, Style.TEXT) content.add_child(prompt) var selected := Label.new() var selected_text := str(state.get("selected_option_text", "")) selected.text = "Your follow-up answer: %s" % selected_text if selected_text != "" else "Follow-up reviewed." selected.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(selected, 14, Style.LIME if bool(state.get("was_correct", false)) else Style.RED) content.add_child(selected) var teaching := Label.new() teaching.text = str(state.get("teaching_point", "")) teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART Style.set_label(teaching, 14, Style.AMBER) content.add_child(teaching) func _clear_followups() -> void: active_followup_choice_id = "" followup_required = false followup_answered = false followup_states.clear() for index in range(choice_followup_panels.size()): _clear_followup_content(index) choice_followup_panels[index].visible = false func _clear_followup_content(index: int) -> void: if index < 0 or index >= choice_followup_contents.size(): return for child in choice_followup_contents[index].get_children(): choice_followup_contents[index].remove_child(child) child.queue_free() func _get_followup_for_choice(choice_id: String) -> Dictionary: var question := BlockSessionService.get_current_question() var followups: Dictionary = question.get("wrong_answer_followups", {}) if followups.has(choice_id) and typeof(followups[choice_id]) == TYPE_DICTIONARY: var authored: Dictionary = followups[choice_id] var choices: Array = authored.get("choices", []) return { "prompt": str(authored.get("prompt", "What finding in the stem makes this answer less appropriate?")), "choices": choices, "has_authored_choices": not choices.is_empty(), "correct_choice_id": str(authored.get("correctChoiceId", authored.get("correct_choice_id", ""))), "teaching_point": str(authored.get("teachingPoint", authored.get("teaching_point", "Review the relevant stem finding before choosing again."))) } return { "prompt": "No follow-up has been authored for this answer yet. Re-read the stem before retrying.", "choices": [], "has_authored_choices": false, "correct_choice_id": "", "teaching_point": "Review the stem for the finding that makes this answer less appropriate before trying again." } func _choice_index_for_id(choice_id: String) -> int: var question := BlockSessionService.get_current_question() var choices: Array = question.get("choices", []) for index in range(choices.size()): if str(choices[index].get("id", "")) == choice_id: return index return -1 func _choice_text_for_id(choice_id: String) -> String: var question := BlockSessionService.get_current_question() var choices: Array = question.get("choices", []) for choice in choices: if typeof(choice) == TYPE_DICTIONARY and str(choice.get("id", "")) == choice_id: return str(choice.get("text", "")) return "" func _on_question_rating_updated(question_id: String, question_elo: int, _rating: Dictionary) -> void: if str(BlockSessionService.get_current_question().get("id", "")) != question_id: return BlockSessionService.update_question_elo(question_id, question_elo) _update_question_elo_label(not current_answer.is_empty() or pending_correct_advance) _apply_saved_choice_state() func _on_question_skipped(question_id: String, correct_choice_id: String, question_elo: int, _rating: Dictionary) -> void: if str(BlockSessionService.get_current_question().get("id", "")) != question_id: return var question := BlockSessionService.get_current_question() if correct_choice_id != "": question["correct_choice_id"] = correct_choice_id BlockSessionService.update_question_elo(question_id, question_elo) _update_question_elo_label(true) _apply_saved_choice_state() func _update_question_elo_label(show_label: bool) -> void: if question_elo_label == null: return if not show_label: question_elo_label.visible = false return var question := BlockSessionService.get_current_question() var question_elo := int(question.get("question_elo", 1000)) question_elo_label.text = "Question ELO %d" % question_elo question_elo_label.visible = true func _strike_text(value: String) -> String: var result := "" for character in value: result += character + char(0x0336) return result func _make_labs_icon() -> Texture2D: var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) var glass_color := Style.TEXT var fluid_color := Style.LIME for x in range(8, 16): image.set_pixel(x, 4, glass_color) for y in range(5, 10): image.set_pixel(10, y, glass_color) image.set_pixel(13, y, glass_color) for y in range(10, 20): var left := 10 - int(floor(float(y - 10) / 3.0)) var right := 13 + int(floor(float(y - 10) / 3.0)) image.set_pixel(left, y, glass_color) image.set_pixel(right, y, glass_color) for x in range(7, 17): image.set_pixel(x, 20, glass_color) for y in range(15, 20): for x in range(8, 16): if x >= 10 - int(floor(float(y - 10) / 3.0)) and x <= 13 + int(floor(float(y - 10) / 3.0)): image.set_pixel(x, y, fluid_color) image.set_pixel(7, 7, Style.CYAN) image.set_pixel(17, 9, Style.CYAN) image.set_pixel(18, 6, Style.CYAN) return ImageTexture.create_from_image(image) func _make_flag_icon() -> Texture2D: var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) for y in range(4, 20): image.set_pixel(6, y, Style.TEXT) image.set_pixel(7, y, Style.TEXT) for x in range(8, 18): for y in range(4, 11): if x < 16 or y < 9: image.set_pixel(x, y, Style.AMBER) for y in range(11, 14): for x in range(8, 14 - (y - 11)): image.set_pixel(x, y, Style.AMBER) return ImageTexture.create_from_image(image) func _make_clear_highlights_icon() -> Texture2D: var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) var handle_color := Style.TEXT var bristle_color := Style.AMBER for offset in range(3): _draw_icon_line(image, Vector2i(6 + offset, 16), Vector2i(16 + offset, 6), handle_color) for x in range(13, 20): for y in range(15, 19): if x + y < 36: image.set_pixel(x, y, bristle_color) for x in range(12, 20): image.set_pixel(x, 19, bristle_color) return ImageTexture.create_from_image(image) func _make_report_icon() -> Texture2D: var image := Image.create(24, 24, false, Image.FORMAT_RGBA8) image.fill(Color(0, 0, 0, 0)) var body_color := Style.TEXT var accent_color := Style.RED for x in range(5, 14): for y in range(9, 15): image.set_pixel(x, y, body_color) for x in range(14, 20): image.set_pixel(x, 7, accent_color) image.set_pixel(x, 16, accent_color) for y in range(8, 16): image.set_pixel(20, y, accent_color) for y in range(10, 14): image.set_pixel(4, y, body_color) for y in range(15, 21): image.set_pixel(8, y, body_color) if y < 20: image.set_pixel(9, y, body_color) _draw_icon_line(image, Vector2i(2, 5), Vector2i(5, 8), accent_color) _draw_icon_line(image, Vector2i(2, 19), Vector2i(5, 16), accent_color) return ImageTexture.create_from_image(image) func _draw_icon_line(image: Image, from: Vector2i, to: Vector2i, color: Color) -> void: var dx := int(abs(to.x - from.x)) var sx := 1 if from.x < to.x else -1 var dy := -int(abs(to.y - from.y)) var sy := 1 if from.y < to.y else -1 var err: int = dx + dy var point := from while true: if point.x >= 0 and point.x < image.get_width() and point.y >= 0 and point.y < image.get_height(): image.set_pixel(point.x, point.y, color) if point == to: break var e2: int = 2 * err if e2 >= dy: err += dy point.x += sx if e2 <= dx: err += dx point.y += sy func _refresh_nav_palette() -> void: for child in nav_palette.get_children(): child.queue_free() if BlockSessionService.session_kind == "core": return for index in range(BlockSessionService.active_questions.size()): var button := Button.new() button.text = str(index + 1) button.custom_minimum_size = Vector2(38, 34) var answer = BlockSessionService.get_answer_for_index(index) if index == BlockSessionService.current_index: Style.style_chip(button, Style.CYAN) elif answer != null and bool(answer.get("flagged", false)): Style.style_chip(button, Style.AMBER) elif answer != null: 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": BlockSessionService.move_to(target_index) _load_question() ) nav_palette.add_child(button)