Add core game retry flow and question stats
This commit is contained in:
@@ -13,6 +13,8 @@ var start_button: Button
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 16)
|
||||
if not QuestionBankService.questions_changed.is_connected(_refresh_setup):
|
||||
QuestionBankService.questions_changed.connect(_refresh_setup)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Block Setup"
|
||||
@@ -66,12 +68,23 @@ func _ready() -> void:
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
selected_system = str(payload) if payload != null else "All Systems"
|
||||
_refresh_setup()
|
||||
|
||||
|
||||
func _refresh_setup() -> void:
|
||||
if system_label == null:
|
||||
return
|
||||
system_label.text = "Zone: %s" % selected_system
|
||||
var available := QuestionBankService.get_questions_for_system(selected_system).size()
|
||||
size_spin.max_value = max(1, available)
|
||||
size_spin.value = min(size_spin.value, available)
|
||||
start_button.disabled = available == 0
|
||||
status_label.text = QuestionBankService.last_error if available == 0 else ""
|
||||
start_button.disabled = available == 0 or QuestionBankService.is_loading
|
||||
if QuestionBankService.is_loading:
|
||||
status_label.text = "Question API unavailable. Retrying in %d seconds..." % QuestionBankService.next_retry_seconds if QuestionBankService.load_state == "retrying" else "Loading question bank..."
|
||||
elif available == 0:
|
||||
status_label.text = QuestionBankService.last_error if QuestionBankService.last_error != "" else "No questions are available for this zone yet."
|
||||
else:
|
||||
status_label.text = "%d questions available." % available
|
||||
|
||||
|
||||
func _start_block() -> void:
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
|
||||
var main: Control
|
||||
var status_label: Label
|
||||
var daily_label: Label
|
||||
var xp_label: Label
|
||||
var streak_label: Label
|
||||
var bank_label: Label
|
||||
var error_label: Label
|
||||
var begin_button: Button
|
||||
var retry_button: Button
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 18)
|
||||
if not QuestionBankService.questions_changed.is_connected(_refresh):
|
||||
QuestionBankService.questions_changed.connect(_refresh)
|
||||
if not ProfileService.profile_changed.is_connected(_refresh):
|
||||
ProfileService.profile_changed.connect(_refresh)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Core Mode"
|
||||
Style.set_label(title, 36, Style.TEXT)
|
||||
add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "Begin a continuous STEP 3 question run. Keep your energy up, build score, and stay on pace."
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(subtitle, 17, Style.MUTED)
|
||||
add_child(subtitle)
|
||||
|
||||
var summary := HBoxContainer.new()
|
||||
summary.add_theme_constant_override("separation", 14)
|
||||
add_child(summary)
|
||||
|
||||
daily_label = _add_metric(summary, "Today")
|
||||
xp_label = _add_metric(summary, "Daily XP")
|
||||
streak_label = _add_metric(summary, "Streak")
|
||||
bank_label = _add_metric(summary, "Question Bank")
|
||||
|
||||
var action_panel := PanelContainer.new()
|
||||
action_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475")))
|
||||
action_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_child(action_panel)
|
||||
|
||||
var actions := VBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 12)
|
||||
action_panel.add_child(actions)
|
||||
|
||||
status_label = Label.new()
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(status_label, 22, Style.CYAN)
|
||||
actions.add_child(status_label)
|
||||
|
||||
error_label = Label.new()
|
||||
error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(error_label, 15, Style.RED)
|
||||
actions.add_child(error_label)
|
||||
|
||||
var button_row := HBoxContainer.new()
|
||||
button_row.add_theme_constant_override("separation", 12)
|
||||
actions.add_child(button_row)
|
||||
|
||||
begin_button = Button.new()
|
||||
begin_button.text = "Begin"
|
||||
begin_button.custom_minimum_size = Vector2(260, 60)
|
||||
Style.style_primary_button(begin_button)
|
||||
begin_button.pressed.connect(_begin_core_mode)
|
||||
button_row.add_child(begin_button)
|
||||
|
||||
retry_button = Button.new()
|
||||
retry_button.text = "Retry Questions"
|
||||
retry_button.custom_minimum_size = Vector2(170, 54)
|
||||
Style.style_chip(retry_button, Style.AMBER)
|
||||
retry_button.pressed.connect(QuestionBankService.retry_now)
|
||||
button_row.add_child(retry_button)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(spacer)
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
if QuestionBankService.load_state == "idle" and AuthState.is_logged_in:
|
||||
QuestionBankService.load_questions_from_api()
|
||||
_refresh()
|
||||
|
||||
|
||||
func _add_metric(parent: HBoxContainer, metric_name: String) -> Label:
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", Style.panel_box(Style.SURFACE, Color("33415c")))
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
parent.add_child(panel)
|
||||
|
||||
var label := Label.new()
|
||||
label.text = metric_name
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(label, 18, Style.TEXT)
|
||||
panel.add_child(label)
|
||||
return label
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
if daily_label == null:
|
||||
return
|
||||
|
||||
var profile := ProfileService.get_profile()
|
||||
var answered := int(profile.get("daily_questions_answered", 0))
|
||||
var goal := int(profile.get("daily_goal_questions", 10))
|
||||
if goal < 1:
|
||||
goal = 1
|
||||
var progress := int(round(float(answered) / float(goal) * 100.0))
|
||||
if progress > 100:
|
||||
progress = 100
|
||||
daily_label.text = "Today\n%d/%d questions\n%d%% complete" % [answered, goal, progress]
|
||||
xp_label.text = "Daily XP\n%d XP earned" % int(profile.get("daily_xp_earned", 0))
|
||||
streak_label.text = "Streak\n%d current\n%d best" % [
|
||||
int(profile.get("current_streak", 0)),
|
||||
int(profile.get("best_streak", 0))
|
||||
]
|
||||
bank_label.text = "Question Bank\n%d available\n%s" % [
|
||||
QuestionBankService.get_all_questions().size(),
|
||||
QuestionBankService.load_state.capitalize()
|
||||
]
|
||||
|
||||
var state := QuestionBankService.load_state
|
||||
var has_questions := QuestionBankService.has_questions()
|
||||
begin_button.disabled = not has_questions
|
||||
retry_button.disabled = state == "loading"
|
||||
retry_button.visible = state != "ready"
|
||||
error_label.text = ""
|
||||
|
||||
if state == "ready":
|
||||
status_label.text = BlockSessionService.core_status_message if BlockSessionService.core_status_message != "" else "Question bank ready. Begin core mode when you are ready."
|
||||
elif state == "loading":
|
||||
status_label.text = "Loading question bank..."
|
||||
elif state == "retrying":
|
||||
status_label.text = "Question API unavailable. Retrying in %d seconds." % QuestionBankService.next_retry_seconds
|
||||
error_label.text = QuestionBankService.last_error
|
||||
elif state == "failed":
|
||||
status_label.text = "Question bank is not available."
|
||||
error_label.text = QuestionBankService.last_error
|
||||
else:
|
||||
status_label.text = "Question bank has not loaded yet."
|
||||
|
||||
|
||||
func _begin_core_mode() -> void:
|
||||
if not QuestionBankService.has_questions():
|
||||
return
|
||||
if BlockSessionService.start_core_run():
|
||||
main.show_screen("player")
|
||||
else:
|
||||
_refresh()
|
||||
@@ -0,0 +1 @@
|
||||
uid://7r2eyomsnpoe
|
||||
@@ -152,7 +152,7 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
|
||||
)
|
||||
QuestionBankService.load_questions_from_api()
|
||||
password_input.text = ""
|
||||
main.show_screen("menu")
|
||||
main.show_screen("dashboard")
|
||||
|
||||
|
||||
func _on_input_submitted(_new_text: String) -> void:
|
||||
|
||||
+93
-16
@@ -6,14 +6,18 @@ var screens := {}
|
||||
var header_stats: Label
|
||||
var header_panel: PanelContainer
|
||||
var user_label: Label
|
||||
var app_menu: PanelContainer
|
||||
var menu_exit_button: Button
|
||||
var current_screen_name := ""
|
||||
|
||||
func _ready() -> void:
|
||||
Style.apply_app_theme(self)
|
||||
_build_ui()
|
||||
show_screen("menu" if AuthState.is_logged_in else "login")
|
||||
show_screen("dashboard" if AuthState.is_logged_in else "login")
|
||||
|
||||
|
||||
func show_screen(screen_name: String, payload: Variant = null) -> void:
|
||||
_close_app_menu()
|
||||
for child in $Root/Screens.get_children():
|
||||
child.visible = false
|
||||
|
||||
@@ -21,6 +25,7 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
|
||||
if screen == null:
|
||||
return
|
||||
|
||||
current_screen_name = screen_name
|
||||
screen.visible = true
|
||||
if header_panel != null:
|
||||
header_panel.visible = screen_name != "login"
|
||||
@@ -68,21 +73,6 @@ func _build_ui() -> void:
|
||||
user_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
header.add_child(user_label)
|
||||
|
||||
var profile_button := Button.new()
|
||||
profile_button.text = "Profile"
|
||||
profile_button.pressed.connect(func(): show_screen("profile"))
|
||||
header.add_child(profile_button)
|
||||
|
||||
var map_button := Button.new()
|
||||
map_button.text = "Study Map"
|
||||
map_button.pressed.connect(func(): show_screen("map"))
|
||||
header.add_child(map_button)
|
||||
|
||||
var logout_button := Button.new()
|
||||
logout_button.text = "Log Out"
|
||||
logout_button.pressed.connect(_logout)
|
||||
header.add_child(logout_button)
|
||||
|
||||
var screens_container := MarginContainer.new()
|
||||
screens_container.name = "Screens"
|
||||
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
@@ -104,6 +94,12 @@ func _build_ui() -> void:
|
||||
screens_container.add_child(login)
|
||||
screens["login"] = login
|
||||
|
||||
var dashboard := preload("res://scripts/ui/dashboard.gd").new()
|
||||
dashboard.name = "Dashboard"
|
||||
dashboard.main = self
|
||||
screens_container.add_child(dashboard)
|
||||
screens["dashboard"] = dashboard
|
||||
|
||||
var map := preload("res://scripts/ui/study_map.gd").new()
|
||||
map.name = "StudyMap"
|
||||
map.main = self
|
||||
@@ -137,6 +133,15 @@ func _build_ui() -> void:
|
||||
ProfileService.profile_changed.connect(_refresh_header_stats)
|
||||
AuthState.auth_changed.connect(_refresh_header_stats)
|
||||
_refresh_header_stats()
|
||||
_build_app_menu()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not AuthState.is_logged_in or current_screen_name == "login":
|
||||
return
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_toggle_app_menu()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _refresh_header_stats() -> void:
|
||||
@@ -153,5 +158,77 @@ func _refresh_header_stats() -> void:
|
||||
|
||||
|
||||
func _logout() -> void:
|
||||
_close_app_menu()
|
||||
if BlockSessionService.block_started:
|
||||
BlockSessionService.finish_block()
|
||||
QuestionBankService.clear_questions()
|
||||
AuthState.clear_session()
|
||||
show_screen("login")
|
||||
|
||||
|
||||
func _build_app_menu() -> void:
|
||||
app_menu = PanelContainer.new()
|
||||
app_menu.visible = false
|
||||
app_menu.top_level = true
|
||||
app_menu.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
app_menu.custom_minimum_size = Vector2(340, 230)
|
||||
app_menu.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
|
||||
add_child(app_menu)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 12)
|
||||
app_menu.add_child(content)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Paused"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(title, 28, Style.TEXT)
|
||||
content.add_child(title)
|
||||
|
||||
var resume := Button.new()
|
||||
resume.text = "Resume"
|
||||
resume.custom_minimum_size = Vector2(0, 48)
|
||||
Style.style_primary_button(resume)
|
||||
resume.pressed.connect(_close_app_menu)
|
||||
content.add_child(resume)
|
||||
|
||||
menu_exit_button = Button.new()
|
||||
menu_exit_button.text = "Exit Run"
|
||||
menu_exit_button.custom_minimum_size = Vector2(0, 48)
|
||||
menu_exit_button.pressed.connect(_exit_run)
|
||||
content.add_child(menu_exit_button)
|
||||
|
||||
var logout := Button.new()
|
||||
logout.text = "Log Out"
|
||||
logout.custom_minimum_size = Vector2(0, 48)
|
||||
Style.style_chip(logout, Style.RED)
|
||||
logout.pressed.connect(_logout)
|
||||
content.add_child(logout)
|
||||
|
||||
|
||||
func _toggle_app_menu() -> void:
|
||||
if app_menu == null:
|
||||
return
|
||||
if app_menu.visible:
|
||||
_close_app_menu()
|
||||
else:
|
||||
app_menu.visible = true
|
||||
app_menu.size = app_menu.custom_minimum_size
|
||||
app_menu.position = (get_viewport_rect().size - app_menu.size) * 0.5
|
||||
menu_exit_button.visible = current_screen_name == "player" and BlockSessionService.block_started
|
||||
if menu_exit_button.visible:
|
||||
Style.style_chip(menu_exit_button, Style.AMBER)
|
||||
BlockSessionService.pause_current_question_timer()
|
||||
|
||||
|
||||
func _close_app_menu() -> void:
|
||||
if app_menu != null:
|
||||
app_menu.visible = false
|
||||
BlockSessionService.resume_current_question_timer()
|
||||
|
||||
|
||||
func _exit_run() -> void:
|
||||
_close_app_menu()
|
||||
if BlockSessionService.block_started:
|
||||
BlockSessionService.finish_block()
|
||||
show_screen("dashboard")
|
||||
|
||||
@@ -26,8 +26,8 @@ func _ready() -> void:
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
var back := Button.new()
|
||||
back.text = "Back to Menu"
|
||||
back.pressed.connect(func(): main.show_screen("menu"))
|
||||
back.text = "Dashboard"
|
||||
back.pressed.connect(func(): main.show_screen("dashboard"))
|
||||
row.add_child(back)
|
||||
|
||||
var reset := Button.new()
|
||||
@@ -44,13 +44,16 @@ func _ready() -> void:
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
var profile := ProfileService.get_profile()
|
||||
var xp_into := ProgressionService.xp_into_level(int(profile["xp"]))
|
||||
var text := "[b]Level %d[/b]\n%d total XP (%d/%d to next level)\nCurrent streak: %d\nBest streak: %d\n\n[b]Badges[/b]\n" % [
|
||||
var text := "[b]Level %d[/b]\n%d total XP (%d/%d to next level)\nCurrent streak: %d\nBest streak: %d\nToday: %d/%d questions, +%d XP\n\n[b]Badges[/b]\n" % [
|
||||
profile["level"],
|
||||
profile["xp"],
|
||||
xp_into,
|
||||
ProgressionService.XP_PER_LEVEL,
|
||||
profile["current_streak"],
|
||||
profile["best_streak"]
|
||||
profile["best_streak"],
|
||||
profile.get("daily_questions_answered", 0),
|
||||
profile.get("daily_goal_questions", 10),
|
||||
profile.get("daily_xp_earned", 0)
|
||||
]
|
||||
if profile["badges"].is_empty():
|
||||
text += "No badges yet.\n"
|
||||
|
||||
+330
-45
@@ -1,17 +1,22 @@
|
||||
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 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_mode_enabled := false
|
||||
var highlight_drag_started_in_stem := false
|
||||
var timer_color_state := ""
|
||||
var pending_correct_advance := false
|
||||
|
||||
var progress_label: Label
|
||||
var timer_label: Label
|
||||
@@ -19,10 +24,17 @@ var stem_label: RichTextLabel
|
||||
var lead_label: Label
|
||||
var explanation_label: RichTextLabel
|
||||
var next_button: Button
|
||||
var prev_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)
|
||||
@@ -36,31 +48,48 @@ func _ready() -> void:
|
||||
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)
|
||||
flag_button = Button.new()
|
||||
flag_button.text = "Flag"
|
||||
flag_button.toggle_mode = true
|
||||
Style.style_chip(flag_button, Style.AMBER)
|
||||
flag_button.toggled.connect(func(value: bool): flagged = value)
|
||||
top.add_child(flag_button)
|
||||
add_child(top)
|
||||
|
||||
var highlight_row := HBoxContainer.new()
|
||||
highlight_row.add_theme_constant_override("separation", 8)
|
||||
var highlight_button := Button.new()
|
||||
highlight_button.text = "Highlight Mode"
|
||||
highlight_button.toggle_mode = true
|
||||
highlight_button.toggled.connect(func(enabled: bool):
|
||||
highlight_mode_enabled = enabled
|
||||
highlight_drag_started_in_stem = false
|
||||
)
|
||||
Style.style_chip(highlight_button, Style.AMBER)
|
||||
highlight_row.add_child(highlight_button)
|
||||
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.text = "Clear Highlights"
|
||||
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)
|
||||
highlight_row.add_child(clear_highlight_button)
|
||||
add_child(highlight_row)
|
||||
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)
|
||||
@@ -96,7 +125,7 @@ func _ready() -> void:
|
||||
|
||||
for index in range(5):
|
||||
var button := Button.new()
|
||||
button.custom_minimum_size = Vector2(0, 52)
|
||||
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))
|
||||
@@ -114,33 +143,42 @@ func _ready() -> void:
|
||||
|
||||
var actions := HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 10)
|
||||
prev_button = Button.new()
|
||||
prev_button.text = "Previous"
|
||||
prev_button.pressed.connect(func():
|
||||
if BlockSessionService.move_previous():
|
||||
_load_question()
|
||||
)
|
||||
actions.add_child(prev_button)
|
||||
|
||||
var eliminate := Button.new()
|
||||
eliminate.text = "Eliminate Selected"
|
||||
eliminate.pressed.connect(_toggle_eliminate_selected)
|
||||
actions.add_child(eliminate)
|
||||
|
||||
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))
|
||||
@@ -151,7 +189,7 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible or not highlight_mode_enabled or stem_label == null:
|
||||
if not visible or pending_correct_advance or stem_label == null:
|
||||
return
|
||||
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
@@ -166,17 +204,27 @@ func _input(event: InputEvent) -> void:
|
||||
|
||||
|
||||
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
|
||||
next_button.text = "Submit"
|
||||
|
||||
var question := BlockSessionService.get_current_question()
|
||||
progress_label.text = "Question %s" % BlockSessionService.get_progress_text()
|
||||
timer_label.text = "Tutor Mode" if BlockSessionService.mode == "Tutor" else "Timed"
|
||||
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", "")
|
||||
@@ -198,6 +246,7 @@ func _load_question() -> void:
|
||||
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"
|
||||
@@ -205,15 +254,19 @@ func _load_question() -> void:
|
||||
_apply_saved_choice_state()
|
||||
next_button.text = "Next"
|
||||
|
||||
prev_button.disabled = BlockSessionService.current_index == 0
|
||||
_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 = {}
|
||||
@@ -222,6 +275,8 @@ func _select_choice(index: int) -> void:
|
||||
|
||||
|
||||
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
|
||||
@@ -235,6 +290,8 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
|
||||
|
||||
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:
|
||||
@@ -244,7 +301,7 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
|
||||
|
||||
|
||||
func _toggle_eliminate_selected() -> void:
|
||||
if selected_choice == "" or not current_answer.is_empty():
|
||||
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", [])
|
||||
@@ -259,9 +316,27 @@ func _toggle_eliminate_selected() -> void:
|
||||
|
||||
|
||||
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 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
|
||||
_trigger_correct_feedback()
|
||||
return
|
||||
_lock_wrong_choice(selected_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()
|
||||
@@ -300,12 +375,106 @@ func _advance_or_finish() -> void:
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
_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 feedback_fog != null:
|
||||
feedback_fog.color = Color(1.0, 0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
func _commit_current_text_selection() -> void:
|
||||
if not highlight_mode_enabled or stem_label == null:
|
||||
if stem_label == null:
|
||||
return
|
||||
var raw_selected_text := stem_label.get_selected_text()
|
||||
var selected_text := raw_selected_text.strip_edges()
|
||||
@@ -347,6 +516,25 @@ func _clear_current_highlights() -> void:
|
||||
_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()
|
||||
@@ -393,7 +581,10 @@ func _apply_saved_choice_state() -> void:
|
||||
for index in range(choices.size()):
|
||||
var id := str(choices[index].get("id", ""))
|
||||
_set_choice_button_text(index, choices[index])
|
||||
if eliminated.has(id):
|
||||
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)
|
||||
@@ -404,12 +595,26 @@ func _apply_saved_choice_state() -> void:
|
||||
func _set_choice_button_text(index: int, choice: Dictionary) -> void:
|
||||
var choice_id := str(choice.get("id", ""))
|
||||
var choice_text := str(choice.get("text", ""))
|
||||
if eliminated.has(choice_id):
|
||||
if locked_wrong_choice_ids.has(choice_id):
|
||||
var text := "%s. [wrong] %s" % [choice_id, _strike_text(choice_text)]
|
||||
var percentage := _get_choice_selection_percentage(choice_id)
|
||||
if percentage >= 0:
|
||||
text += "\n %d%% chose this on first attempt" % percentage
|
||||
choice_buttons[index].text = 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 _get_choice_selection_percentage(choice_id: String) -> int:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var percentages: Dictionary = question.get("choice_selection_percentages", {})
|
||||
if not percentages.has(choice_id):
|
||||
return -1
|
||||
return clamp(int(percentages.get(choice_id, -1)), 0, 100)
|
||||
|
||||
|
||||
func _strike_text(value: String) -> String:
|
||||
var result := ""
|
||||
for character in value:
|
||||
@@ -417,10 +622,90 @@ func _strike_text(value: String) -> String:
|
||||
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)
|
||||
|
||||
@@ -41,7 +41,7 @@ func _refresh_zones() -> void:
|
||||
|
||||
if QuestionBankService.is_loading:
|
||||
var loading := Label.new()
|
||||
loading.text = "Loading question bank..."
|
||||
loading.text = "Question API unavailable. Retrying in %d seconds..." % QuestionBankService.next_retry_seconds if QuestionBankService.load_state == "retrying" else "Loading question bank..."
|
||||
Style.set_label(loading, 20, Style.AMBER)
|
||||
zone_list.add_child(loading)
|
||||
return
|
||||
@@ -52,6 +52,13 @@ func _refresh_zones() -> void:
|
||||
empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(empty, 20, Style.RED)
|
||||
zone_list.add_child(empty)
|
||||
|
||||
var retry := Button.new()
|
||||
retry.text = "Retry Questions"
|
||||
retry.custom_minimum_size = Vector2(220, 54)
|
||||
Style.style_chip(retry, Style.AMBER)
|
||||
retry.pressed.connect(QuestionBankService.retry_now)
|
||||
zone_list.add_child(retry)
|
||||
return
|
||||
|
||||
var systems: Array[String] = ["All Systems"]
|
||||
|
||||
Reference in New Issue
Block a user