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 eliminated: Array = [] var locked_wrong_choice_ids: Array = [] 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 flag_button: Button var report_button: Button var nav_palette: HBoxContainer var question_content: VBoxContainer var timer_bar: ProgressBar var feedback_fog: ColorRect 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 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) question_content = VBoxContainer.new() question_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL question_content.add_theme_constant_override("separation", 12) scroll.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) 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.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) percent_label.offset_left = 0 percent_label.offset_right = -18 Style.set_label(percent_label, 18, Style.TEXT) button.add_child(percent_label) choice_percent_labels.append(percent_label) 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) 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 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() eliminated.clear() locked_wrong_choice_ids.clear() 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() lead_label.text = question.get("lead_in", "") 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: 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: 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 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": 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) selected_choice = "" current_answer = {} next_button.text = "Submit" _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: 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 _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", "")) _set_choice_button_text(index, choices[index]) choice_buttons[index].disabled = locked_wrong_choice_ids.has(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() func _set_choice_button_text(index: int, choice: Dictionary) -> void: var choice_id := str(choice.get("id", "")) var choice_text := str(choice.get("text", "")) _set_choice_percent_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) choice_buttons[index].text = "%s. %s" % [choice_id, _strike_text(choice_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 _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 _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 _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_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)