873 lines
30 KiB
GDScript
873 lines
30 KiB
GDScript
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)
|