Add study selection and account registration flows
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
|
||||
var main: Control
|
||||
var status_label: Label
|
||||
var daily_label: Label
|
||||
var elo_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 QuestionBankService.ratings_changed.is_connected(_refresh):
|
||||
QuestionBankService.ratings_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")
|
||||
elo_label = _add_metric(summary, "ELO")
|
||||
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]
|
||||
elo_label.text = "ELO\n%d overall\n%d rated" % [
|
||||
QuestionBankService.overall_elo,
|
||||
QuestionBankService.rated_question_count
|
||||
]
|
||||
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()
|
||||
@@ -1 +0,0 @@
|
||||
uid://7r2eyomsnpoe
|
||||
+239
-54
@@ -2,13 +2,30 @@ extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const AUTH_LOGIN_URL := "https://pauth.ranaze.com/api/Auth/login"
|
||||
const AUTH_REGISTER_URL := "https://pauth.ranaze.com/api/Auth/register"
|
||||
|
||||
var main: Control
|
||||
var auth_panel: PanelContainer
|
||||
var login_tab: Button
|
||||
var register_tab: Button
|
||||
var login_fields: VBoxContainer
|
||||
var register_fields: VBoxContainer
|
||||
var username_input: LineEdit
|
||||
var password_input: LineEdit
|
||||
var registration_username_input: LineEdit
|
||||
var registration_email_input: LineEdit
|
||||
var registration_password_input: LineEdit
|
||||
var registration_confirm_input: LineEdit
|
||||
var login_button: Button
|
||||
var register_button: Button
|
||||
var error_label: Label
|
||||
var success_label: Label
|
||||
var login_request: HTTPRequest
|
||||
var register_request: HTTPRequest
|
||||
var auth_mode := "login"
|
||||
var automatic_login_pending := false
|
||||
var login_attempt_username := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -22,57 +39,82 @@ func _ready() -> void:
|
||||
center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_child(center)
|
||||
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(520, 430)
|
||||
panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("2bdbff")))
|
||||
center.add_child(panel)
|
||||
auth_panel = PanelContainer.new()
|
||||
auth_panel.custom_minimum_size = Vector2(540, 470)
|
||||
auth_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Style.CYAN))
|
||||
center.add_child(auth_panel)
|
||||
|
||||
var content := VBoxContainer.new()
|
||||
content.add_theme_constant_override("separation", 14)
|
||||
panel.add_child(content)
|
||||
auth_panel.add_child(content)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "STEP 3 Arcade"
|
||||
title.text = "RanaZe's Doctor Maker"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(title, 36, Style.TEXT)
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
content.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "Sign in with your Promiscuity account to start studying."
|
||||
subtitle.text = "Sign in or create an account to start studying."
|
||||
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(subtitle, 16, Style.MUTED)
|
||||
content.add_child(subtitle)
|
||||
|
||||
var username_label := Label.new()
|
||||
username_label.text = "Username"
|
||||
Style.set_label(username_label, 15, Style.CYAN)
|
||||
content.add_child(username_label)
|
||||
var tabs := HBoxContainer.new()
|
||||
tabs.add_theme_constant_override("separation", 8)
|
||||
content.add_child(tabs)
|
||||
|
||||
username_input = LineEdit.new()
|
||||
username_input.placeholder_text = "enter username"
|
||||
username_input.custom_minimum_size = Vector2(0, 44)
|
||||
username_input.text_submitted.connect(_on_input_submitted)
|
||||
content.add_child(username_input)
|
||||
login_tab = Button.new()
|
||||
login_tab.text = "Log In"
|
||||
login_tab.custom_minimum_size = Vector2(0, 44)
|
||||
login_tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
login_tab.pressed.connect(func(): _set_auth_mode("login"))
|
||||
tabs.add_child(login_tab)
|
||||
|
||||
var password_label := Label.new()
|
||||
password_label.text = "Password"
|
||||
Style.set_label(password_label, 15, Style.CYAN)
|
||||
content.add_child(password_label)
|
||||
register_tab = Button.new()
|
||||
register_tab.text = "Create Account"
|
||||
register_tab.custom_minimum_size = Vector2(0, 44)
|
||||
register_tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
register_tab.pressed.connect(func(): _set_auth_mode("register"))
|
||||
tabs.add_child(register_tab)
|
||||
|
||||
password_input = LineEdit.new()
|
||||
password_input.placeholder_text = "enter password"
|
||||
password_input.secret = true
|
||||
password_input.custom_minimum_size = Vector2(0, 44)
|
||||
password_input.text_submitted.connect(_on_input_submitted)
|
||||
content.add_child(password_input)
|
||||
login_fields = VBoxContainer.new()
|
||||
login_fields.add_theme_constant_override("separation", 10)
|
||||
content.add_child(login_fields)
|
||||
username_input = _add_text_field(login_fields, "Username", "enter username")
|
||||
username_input.text_submitted.connect(_on_login_input_submitted)
|
||||
password_input = _add_text_field(login_fields, "Password", "enter password", true)
|
||||
password_input.text_submitted.connect(_on_login_input_submitted)
|
||||
|
||||
login_button = Button.new()
|
||||
login_button.text = "Log In"
|
||||
login_button.custom_minimum_size = Vector2(0, 50)
|
||||
Style.style_primary_button(login_button)
|
||||
login_button.pressed.connect(_on_login_pressed)
|
||||
content.add_child(login_button)
|
||||
login_fields.add_child(login_button)
|
||||
|
||||
register_fields = VBoxContainer.new()
|
||||
register_fields.add_theme_constant_override("separation", 10)
|
||||
content.add_child(register_fields)
|
||||
registration_username_input = _add_text_field(register_fields, "Username", "3-24 letters, numbers, or underscores")
|
||||
registration_email_input = _add_text_field(register_fields, "Email", "[email protected]")
|
||||
registration_password_input = _add_text_field(register_fields, "Password", "at least 8 characters", true)
|
||||
registration_confirm_input = _add_text_field(register_fields, "Confirm password", "re-enter password", true)
|
||||
registration_confirm_input.text_submitted.connect(_on_register_input_submitted)
|
||||
|
||||
register_button = Button.new()
|
||||
register_button.text = "Create Account"
|
||||
register_button.custom_minimum_size = Vector2(0, 50)
|
||||
Style.style_primary_button(register_button)
|
||||
register_button.pressed.connect(_on_register_pressed)
|
||||
register_fields.add_child(register_button)
|
||||
|
||||
success_label = Label.new()
|
||||
success_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
success_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(success_label, 15, Style.LIME)
|
||||
content.add_child(success_label)
|
||||
|
||||
error_label = Label.new()
|
||||
error_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
@@ -89,47 +131,90 @@ func _ready() -> void:
|
||||
login_request.request_completed.connect(_on_login_request_completed)
|
||||
add_child(login_request)
|
||||
|
||||
register_request = HTTPRequest.new()
|
||||
register_request.name = "RegisterRequest"
|
||||
register_request.request_completed.connect(_on_register_request_completed)
|
||||
add_child(register_request)
|
||||
_set_auth_mode("login")
|
||||
|
||||
|
||||
func on_show(_payload: Variant = null) -> void:
|
||||
error_label.text = ""
|
||||
login_button.disabled = false
|
||||
login_button.text = "Log In"
|
||||
_set_busy(false)
|
||||
_set_auth_mode("login")
|
||||
_clear_messages()
|
||||
username_input.grab_focus()
|
||||
|
||||
|
||||
func _add_text_field(parent: VBoxContainer, field_name: String, placeholder: String, secret: bool = false) -> LineEdit:
|
||||
var label := Label.new()
|
||||
label.text = field_name
|
||||
Style.set_label(label, 15, Style.CYAN)
|
||||
parent.add_child(label)
|
||||
|
||||
var input := LineEdit.new()
|
||||
input.placeholder_text = placeholder
|
||||
input.secret = secret
|
||||
input.custom_minimum_size = Vector2(0, 42)
|
||||
parent.add_child(input)
|
||||
return input
|
||||
|
||||
|
||||
func _set_auth_mode(next_mode: String) -> void:
|
||||
auth_mode = next_mode
|
||||
if login_fields == null:
|
||||
return
|
||||
login_fields.visible = auth_mode == "login"
|
||||
register_fields.visible = auth_mode == "register"
|
||||
auth_panel.custom_minimum_size.y = 470 if auth_mode == "login" else 650
|
||||
if auth_mode == "login":
|
||||
Style.style_primary_button(login_tab)
|
||||
Style.style_chip(register_tab, Style.CYAN)
|
||||
username_input.grab_focus()
|
||||
else:
|
||||
Style.style_chip(login_tab, Style.CYAN)
|
||||
Style.style_primary_button(register_tab)
|
||||
registration_username_input.grab_focus()
|
||||
_clear_messages()
|
||||
|
||||
|
||||
func _on_login_pressed() -> void:
|
||||
var username := username_input.text.strip_edges()
|
||||
var password := password_input.text
|
||||
_send_login(username_input.text.strip_edges(), password_input.text, false)
|
||||
|
||||
|
||||
func _send_login(username: String, password: String, is_automatic: bool) -> void:
|
||||
if username.is_empty() or password.is_empty():
|
||||
_show_error("Username and password required.")
|
||||
return
|
||||
|
||||
login_button.disabled = true
|
||||
automatic_login_pending = is_automatic
|
||||
login_attempt_username = username
|
||||
_set_busy(true)
|
||||
login_button.text = "Signing in..."
|
||||
error_label.text = ""
|
||||
|
||||
var payload := {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
_clear_messages()
|
||||
if is_automatic:
|
||||
success_label.text = "Account created. Signing you in..."
|
||||
var payload := {"username": username, "password": password}
|
||||
var headers := PackedStringArray(["Content-Type: application/json"])
|
||||
var err := login_request.request(AUTH_LOGIN_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if err != OK:
|
||||
login_button.disabled = false
|
||||
var request_error := login_request.request(AUTH_LOGIN_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if request_error != OK:
|
||||
_set_busy(false)
|
||||
login_button.text = "Log In"
|
||||
_show_error("Failed to send login request.")
|
||||
|
||||
|
||||
func _on_login_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
login_button.disabled = false
|
||||
_set_busy(false)
|
||||
login_button.text = "Log In"
|
||||
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
_show_error("Network error. Please try again.")
|
||||
return
|
||||
|
||||
if response_code < 200 or response_code >= 300:
|
||||
_show_error("Login failed. Check your credentials.")
|
||||
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||
if automatic_login_pending:
|
||||
_set_auth_mode("login")
|
||||
username_input.text = login_attempt_username
|
||||
password_input.text = ""
|
||||
_show_error("Account created, but automatic login failed. Please log in with your new account.")
|
||||
else:
|
||||
_show_error("Login failed. Check your credentials and try again.")
|
||||
automatic_login_pending = false
|
||||
return
|
||||
|
||||
var response: Variant = JSON.parse_string(body.get_string_from_utf8())
|
||||
@@ -138,13 +223,13 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
|
||||
return
|
||||
|
||||
var token := String(response.get("accessToken", ""))
|
||||
var username := String(response.get("username", username_input.text.strip_edges()))
|
||||
var response_username := String(response.get("username", login_attempt_username))
|
||||
if token.is_empty():
|
||||
_show_error("Login response did not include an access token.")
|
||||
return
|
||||
|
||||
AuthState.set_session(
|
||||
username,
|
||||
response_username,
|
||||
token,
|
||||
String(response.get("refreshToken", "")),
|
||||
String(response.get("role", "")),
|
||||
@@ -152,12 +237,112 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack
|
||||
)
|
||||
QuestionBankService.load_questions_from_api()
|
||||
password_input.text = ""
|
||||
main.show_screen("dashboard")
|
||||
registration_password_input.text = ""
|
||||
registration_confirm_input.text = ""
|
||||
automatic_login_pending = false
|
||||
main.show_screen("selection", "exam")
|
||||
|
||||
|
||||
func _on_input_submitted(_new_text: String) -> void:
|
||||
func _on_register_pressed() -> void:
|
||||
var username := registration_username_input.text.strip_edges()
|
||||
var email := registration_email_input.text.strip_edges()
|
||||
var password := registration_password_input.text
|
||||
var confirmation := registration_confirm_input.text
|
||||
var validation_error := _validate_registration(username, email, password, confirmation)
|
||||
if validation_error != "":
|
||||
_show_error(validation_error)
|
||||
return
|
||||
|
||||
_set_busy(true)
|
||||
register_button.text = "Creating account..."
|
||||
_clear_messages()
|
||||
var payload := {"username": username, "email": email, "password": password}
|
||||
var headers := PackedStringArray(["Content-Type: application/json"])
|
||||
var request_error := register_request.request(AUTH_REGISTER_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||
if request_error != OK:
|
||||
_set_busy(false)
|
||||
register_button.text = "Create Account"
|
||||
_show_error("Failed to send account creation request.")
|
||||
|
||||
|
||||
func _on_register_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
_set_busy(false)
|
||||
register_button.text = "Create Account"
|
||||
if result != HTTPRequest.RESULT_SUCCESS:
|
||||
_clear_registration_passwords()
|
||||
_show_error("Network error. Please try again.")
|
||||
return
|
||||
if response_code < 200 or response_code >= 300:
|
||||
_clear_registration_passwords()
|
||||
var server_message := _response_message(body)
|
||||
_show_error(server_message if server_message != "" else "Account creation failed.")
|
||||
return
|
||||
|
||||
var username := registration_username_input.text.strip_edges()
|
||||
var password := registration_password_input.text
|
||||
username_input.text = username
|
||||
_send_login(username, password, true)
|
||||
|
||||
|
||||
func _validate_registration(username: String, email: String, password: String, confirmation: String) -> String:
|
||||
var username_pattern := RegEx.new()
|
||||
username_pattern.compile("^[A-Za-z0-9_]{3,24}$")
|
||||
if username_pattern.search(username) == null:
|
||||
registration_username_input.grab_focus()
|
||||
return "Username must be 3-24 characters using only letters, numbers, or underscores."
|
||||
var email_pattern := RegEx.new()
|
||||
email_pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
|
||||
if email_pattern.search(email) == null:
|
||||
registration_email_input.grab_focus()
|
||||
return "Enter a valid email address."
|
||||
if password.length() < 8:
|
||||
registration_password_input.grab_focus()
|
||||
return "Password must be at least 8 characters."
|
||||
if password != confirmation:
|
||||
registration_confirm_input.grab_focus()
|
||||
return "Passwords do not match."
|
||||
return ""
|
||||
|
||||
|
||||
func _set_busy(is_busy: bool) -> void:
|
||||
if login_button == null:
|
||||
return
|
||||
login_button.disabled = is_busy
|
||||
register_button.disabled = is_busy
|
||||
login_tab.disabled = is_busy
|
||||
register_tab.disabled = is_busy
|
||||
username_input.editable = not is_busy
|
||||
password_input.editable = not is_busy
|
||||
registration_username_input.editable = not is_busy
|
||||
registration_email_input.editable = not is_busy
|
||||
registration_password_input.editable = not is_busy
|
||||
registration_confirm_input.editable = not is_busy
|
||||
|
||||
|
||||
func _clear_registration_passwords() -> void:
|
||||
registration_password_input.text = ""
|
||||
registration_confirm_input.text = ""
|
||||
|
||||
|
||||
func _response_message(body: PackedByteArray) -> String:
|
||||
var raw := body.get_string_from_utf8().strip_edges()
|
||||
var parsed: Variant = JSON.parse_string(raw)
|
||||
return String(parsed) if typeof(parsed) == TYPE_STRING else raw
|
||||
|
||||
|
||||
func _on_login_input_submitted(_new_text: String) -> void:
|
||||
_on_login_pressed()
|
||||
|
||||
|
||||
func _on_register_input_submitted(_new_text: String) -> void:
|
||||
_on_register_pressed()
|
||||
|
||||
|
||||
func _clear_messages() -> void:
|
||||
error_label.text = ""
|
||||
success_label.text = ""
|
||||
|
||||
|
||||
func _show_error(message: String) -> void:
|
||||
success_label.text = ""
|
||||
error_label.text = message
|
||||
|
||||
+12
-10
@@ -13,12 +13,14 @@ var current_screen_name := ""
|
||||
func _ready() -> void:
|
||||
Style.apply_app_theme(self)
|
||||
_build_ui()
|
||||
show_screen("dashboard" if AuthState.is_logged_in else "login")
|
||||
show_screen("selection" if AuthState.is_logged_in else "login", "exam")
|
||||
|
||||
|
||||
func show_screen(screen_name: String, payload: Variant = null) -> void:
|
||||
_close_app_menu()
|
||||
for child in $Root/Screens.get_children():
|
||||
if child.visible and child.has_method("on_hide"):
|
||||
child.on_hide()
|
||||
child.visible = false
|
||||
|
||||
var screen = screens.get(screen_name)
|
||||
@@ -28,7 +30,7 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
|
||||
current_screen_name = screen_name
|
||||
screen.visible = true
|
||||
if header_panel != null:
|
||||
header_panel.visible = screen_name != "login"
|
||||
header_panel.visible = screen_name != "login" and screen_name != "player"
|
||||
if screen.has_method("on_show"):
|
||||
screen.on_show(payload)
|
||||
|
||||
@@ -56,7 +58,7 @@ func _build_ui() -> void:
|
||||
header_panel.add_child(header)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "STEP 3 Arcade"
|
||||
title.text = "RanaZe's Doctor Maker"
|
||||
Style.set_label(title, 31, Style.TEXT)
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header.add_child(title)
|
||||
@@ -98,11 +100,11 @@ 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 selector := preload("res://scripts/ui/study_selector.gd").new()
|
||||
selector.name = "StudySelector"
|
||||
selector.main = self
|
||||
screens_container.add_child(selector)
|
||||
screens["selection"] = selector
|
||||
|
||||
var map := preload("res://scripts/ui/study_map.gd").new()
|
||||
map.name = "StudyMap"
|
||||
@@ -153,7 +155,7 @@ func _refresh_header_stats() -> void:
|
||||
if header_stats == null:
|
||||
return
|
||||
var profile := ProfileService.get_profile()
|
||||
header_stats.text = "ELO %d | Streak %d" % [
|
||||
header_stats.text = "Rating: %d | Streak %d" % [
|
||||
QuestionBankService.overall_elo,
|
||||
profile.get("current_streak", 0)
|
||||
]
|
||||
@@ -242,7 +244,7 @@ func _exit_run() -> void:
|
||||
_close_app_menu()
|
||||
if BlockSessionService.block_started:
|
||||
BlockSessionService.finish_block()
|
||||
show_screen("dashboard")
|
||||
show_screen("selection", "step3")
|
||||
|
||||
|
||||
func _open_profile_from_menu() -> void:
|
||||
|
||||
@@ -28,8 +28,8 @@ func _ready() -> void:
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
var back := Button.new()
|
||||
back.text = "Dashboard"
|
||||
back.pressed.connect(func(): main.show_screen("dashboard"))
|
||||
back.text = "Back"
|
||||
back.pressed.connect(func(): main.show_screen("selection", "step3"))
|
||||
row.add_child(back)
|
||||
|
||||
var reset := Button.new()
|
||||
|
||||
+245
-55
@@ -3,6 +3,9 @@ extends VBoxContainer
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
const FEEDBACK_FADE_SECONDS := 0.45
|
||||
const CORRECT_ADVANCE_DELAY_SECONDS := 0.6
|
||||
const TEXT_SCALE_MIN := 0.85
|
||||
const TEXT_SCALE_MAX := 1.35
|
||||
const TEXT_SCALE_STEP := 0.1
|
||||
|
||||
var main: Control
|
||||
var choice_buttons: Array[Button] = []
|
||||
@@ -25,18 +28,24 @@ var stem_plain_text := ""
|
||||
var highlight_drag_started_in_stem := false
|
||||
var timer_color_state := ""
|
||||
var pending_correct_advance := false
|
||||
var question_text_scale := 1.0
|
||||
var presented_choices: Array = []
|
||||
|
||||
var progress_label: Label
|
||||
var timer_label: Label
|
||||
var player_stats_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 text_smaller_button: Button
|
||||
var text_larger_button: Button
|
||||
var labs_button: Button
|
||||
var flag_button: Button
|
||||
var report_button: Button
|
||||
var question_toolbar: HBoxContainer
|
||||
var nav_palette: HBoxContainer
|
||||
var question_content: VBoxContainer
|
||||
var media_container: VBoxContainer
|
||||
@@ -61,15 +70,21 @@ func _ready() -> void:
|
||||
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)
|
||||
if not ProfileService.profile_changed.is_connected(_refresh_player_stats):
|
||||
ProfileService.profile_changed.connect(_refresh_player_stats)
|
||||
if not QuestionBankService.ratings_changed.is_connected(_refresh_player_stats):
|
||||
QuestionBankService.ratings_changed.connect(_refresh_player_stats)
|
||||
if not AuthState.auth_changed.is_connected(_refresh_player_stats):
|
||||
AuthState.auth_changed.connect(_refresh_player_stats)
|
||||
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 12)
|
||||
progress_label = Label.new()
|
||||
Style.set_label(progress_label, 18, Style.CYAN)
|
||||
_set_scaled_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)
|
||||
_set_scaled_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
|
||||
@@ -79,9 +94,30 @@ func _ready() -> void:
|
||||
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
|
||||
player_stats_label = Label.new()
|
||||
player_stats_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_set_scaled_label(player_stats_label, 15, Style.MUTED)
|
||||
top.add_child(player_stats_label)
|
||||
|
||||
question_toolbar = HBoxContainer.new()
|
||||
question_toolbar.add_theme_constant_override("separation", 8)
|
||||
question_toolbar.alignment = BoxContainer.ALIGNMENT_END
|
||||
|
||||
text_smaller_button = Button.new()
|
||||
text_smaller_button.text = "A-"
|
||||
text_smaller_button.tooltip_text = "Decrease text size"
|
||||
text_smaller_button.custom_minimum_size = Vector2(44, 44)
|
||||
Style.style_chip(text_smaller_button, Style.CYAN)
|
||||
text_smaller_button.pressed.connect(func(): _adjust_question_text_scale(-TEXT_SCALE_STEP))
|
||||
question_toolbar.add_child(text_smaller_button)
|
||||
|
||||
text_larger_button = Button.new()
|
||||
text_larger_button.text = "A+"
|
||||
text_larger_button.tooltip_text = "Increase text size"
|
||||
text_larger_button.custom_minimum_size = Vector2(44, 44)
|
||||
Style.style_chip(text_larger_button, Style.CYAN)
|
||||
text_larger_button.pressed.connect(func(): _adjust_question_text_scale(TEXT_SCALE_STEP))
|
||||
question_toolbar.add_child(text_larger_button)
|
||||
|
||||
labs_button = Button.new()
|
||||
labs_button.tooltip_text = "Review lab results"
|
||||
@@ -89,7 +125,7 @@ func _ready() -> void:
|
||||
labs_button.custom_minimum_size = Vector2(44, 44)
|
||||
Style.style_chip(labs_button, Style.LIME)
|
||||
labs_button.pressed.connect(_show_labs_modal)
|
||||
toolbar.add_child(labs_button)
|
||||
question_toolbar.add_child(labs_button)
|
||||
|
||||
var clear_highlight_button := Button.new()
|
||||
clear_highlight_button.tooltip_text = "Clear highlights"
|
||||
@@ -97,7 +133,7 @@ func _ready() -> void:
|
||||
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)
|
||||
question_toolbar.add_child(clear_highlight_button)
|
||||
|
||||
flag_button = Button.new()
|
||||
flag_button.toggle_mode = true
|
||||
@@ -109,16 +145,16 @@ func _ready() -> void:
|
||||
flagged = value
|
||||
_refresh_toolbar_state()
|
||||
)
|
||||
toolbar.add_child(flag_button)
|
||||
question_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)
|
||||
question_toolbar.add_child(report_button)
|
||||
|
||||
top.add_child(toolbar)
|
||||
top.add_child(question_toolbar)
|
||||
add_child(top)
|
||||
|
||||
nav_palette = HBoxContainer.new()
|
||||
@@ -151,6 +187,7 @@ func _ready() -> void:
|
||||
stem_label.selection_enabled = true
|
||||
stem_label.scroll_active = false
|
||||
stem_label.custom_minimum_size = Vector2(0, 96)
|
||||
_set_scaled_rich_text(stem_label, 18)
|
||||
question_content.add_child(stem_label)
|
||||
|
||||
media_container = VBoxContainer.new()
|
||||
@@ -159,15 +196,20 @@ func _ready() -> void:
|
||||
media_container.add_theme_constant_override("separation", 10)
|
||||
question_content.add_child(media_container)
|
||||
|
||||
var lead_margin := MarginContainer.new()
|
||||
lead_margin.add_theme_constant_override("margin_top", 12)
|
||||
lead_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
question_content.add_child(lead_margin)
|
||||
|
||||
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)
|
||||
_set_scaled_label(lead_label, 21, Style.TEXT)
|
||||
lead_margin.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)
|
||||
_set_scaled_label(question_elo_label, 16, Style.AMBER)
|
||||
question_content.add_child(question_elo_label)
|
||||
|
||||
for index in range(5):
|
||||
@@ -175,6 +217,7 @@ func _ready() -> void:
|
||||
button.custom_minimum_size = Vector2(0, 64)
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
Style.style_answer_button(button)
|
||||
_set_scaled_button_font(button, 17)
|
||||
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)
|
||||
@@ -192,7 +235,7 @@ func _ready() -> void:
|
||||
percent_label.offset_top = 8
|
||||
percent_label.offset_right = -18
|
||||
percent_label.offset_bottom = 40
|
||||
Style.set_label(percent_label, 18, Style.TEXT)
|
||||
_set_scaled_label(percent_label, 18, Style.TEXT)
|
||||
button.add_child(percent_label)
|
||||
choice_percent_labels.append(percent_label)
|
||||
|
||||
@@ -207,7 +250,7 @@ func _ready() -> void:
|
||||
top_label.offset_top = 10
|
||||
top_label.offset_right = -96
|
||||
top_label.offset_bottom = -252
|
||||
Style.set_label(top_label, 16, Style.RED)
|
||||
_set_scaled_label(top_label, 16, Style.RED)
|
||||
button.add_child(top_label)
|
||||
choice_top_labels.append(top_label)
|
||||
|
||||
@@ -236,6 +279,7 @@ func _ready() -> void:
|
||||
explanation_label.scroll_active = false
|
||||
explanation_label.custom_minimum_size = Vector2(0, 150)
|
||||
explanation_label.visible = false
|
||||
_set_scaled_rich_text(explanation_label, 17)
|
||||
question_content.add_child(explanation_label)
|
||||
|
||||
var actions := HBoxContainer.new()
|
||||
@@ -311,6 +355,8 @@ func _input(event: InputEvent) -> void:
|
||||
elif highlight_drag_started_in_stem:
|
||||
highlight_drag_started_in_stem = false
|
||||
call_deferred("_commit_current_text_selection")
|
||||
elif _should_clear_selection_from_click(mouse_event):
|
||||
_clear_selected_choice()
|
||||
|
||||
|
||||
func _load_question() -> void:
|
||||
@@ -328,13 +374,13 @@ func _load_question() -> void:
|
||||
question_elo_label.visible = false
|
||||
question_elo_label.text = ""
|
||||
next_button.text = "Submit"
|
||||
next_button.disabled = false
|
||||
next_button.disabled = true
|
||||
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")
|
||||
main.show_screen("selection", "step3")
|
||||
return
|
||||
progress_label.visible = BlockSessionService.session_kind != "core"
|
||||
progress_label.text = "" if BlockSessionService.session_kind == "core" else "Question %s" % BlockSessionService.get_progress_text()
|
||||
@@ -348,7 +394,9 @@ func _load_question() -> void:
|
||||
lead_label.text = question.get("lead_in", "")
|
||||
_refresh_labs_button()
|
||||
|
||||
var choices: Array = question.get("choices", [])
|
||||
presented_choices = question.get("choices", []).duplicate(true)
|
||||
presented_choices.shuffle()
|
||||
var choices := presented_choices
|
||||
for index in range(choice_buttons.size()):
|
||||
var button := choice_buttons[index]
|
||||
button.visible = index < choices.size()
|
||||
@@ -357,6 +405,9 @@ func _load_question() -> void:
|
||||
var choice: Dictionary = choices[index]
|
||||
_set_choice_button_text(index, choice)
|
||||
button.modulate = Color.WHITE
|
||||
_style_choice_button_for_state(index, str(choice.get("id", "")))
|
||||
|
||||
_refresh_submit_state()
|
||||
|
||||
var saved_answer = BlockSessionService.get_answer_for_index(BlockSessionService.current_index)
|
||||
if saved_answer != null:
|
||||
@@ -372,28 +423,78 @@ func _load_question() -> void:
|
||||
else:
|
||||
_apply_saved_choice_state()
|
||||
next_button.text = "Next"
|
||||
next_button.disabled = false
|
||||
_update_question_elo_label(true)
|
||||
|
||||
_refresh_nav_palette()
|
||||
_refresh_toolbar_state()
|
||||
_refresh_player_stats()
|
||||
|
||||
|
||||
func _select_choice(index: int) -> void:
|
||||
if pending_correct_advance or followup_required:
|
||||
return
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
var choices := _get_presented_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", ""))
|
||||
var choice_id := str(choices[index].get("id", ""))
|
||||
selected_choice = "" if selected_choice == choice_id else choice_id
|
||||
if BlockSessionService.mode == "Timed":
|
||||
current_answer = {}
|
||||
next_button.text = "Submit"
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _clear_selected_choice() -> void:
|
||||
if selected_choice == "":
|
||||
return
|
||||
selected_choice = ""
|
||||
if BlockSessionService.mode == "Timed":
|
||||
current_answer = {}
|
||||
next_button.text = "Submit"
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _should_clear_selection_from_click(mouse_event: InputEventMouseButton) -> bool:
|
||||
if selected_choice == "" or followup_required or not current_answer.is_empty():
|
||||
return false
|
||||
if mouse_event.pressed or mouse_event.button_index != MOUSE_BUTTON_LEFT:
|
||||
return false
|
||||
var pointer_position := mouse_event.global_position
|
||||
if stem_label.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if lead_label.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if explanation_label.visible and explanation_label.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if labs_overlay != null and labs_overlay.visible:
|
||||
return false
|
||||
if skip_button != null and skip_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if next_button != null and next_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if question_toolbar != null and question_toolbar.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if labs_button != null and labs_button.visible and labs_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if flag_button != null and flag_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if report_button != null and report_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
for button in choice_buttons:
|
||||
if button.visible and button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
for panel in choice_followup_panels:
|
||||
if panel.visible and panel.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
for child in media_container.get_children():
|
||||
if child is Control and child.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _skip_current_question() -> void:
|
||||
if pending_correct_advance or not current_answer.is_empty() or BlockSessionService.session_kind != "core":
|
||||
return
|
||||
@@ -422,8 +523,7 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
|
||||
if not current_answer.is_empty():
|
||||
return
|
||||
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
var choices := _get_presented_choices()
|
||||
if index >= choices.size():
|
||||
return
|
||||
|
||||
@@ -442,8 +542,7 @@ func _choice_gui_input(event: InputEvent, index: int) -> void:
|
||||
func _toggle_eliminate_selected() -> void:
|
||||
if pending_correct_advance or followup_required or selected_choice == "" or not current_answer.is_empty():
|
||||
return
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
var choices := _get_presented_choices()
|
||||
for index in range(choices.size()):
|
||||
if str(choices[index].get("id", "")) == selected_choice:
|
||||
if eliminated.has(selected_choice):
|
||||
@@ -499,8 +598,9 @@ func _show_explanation() -> void:
|
||||
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 choices := _get_presented_choices()
|
||||
for index in range(choices.size()):
|
||||
var choice: Dictionary = choices[index]
|
||||
var id := str(choice.get("id", ""))
|
||||
_set_choice_button_text(index, choice)
|
||||
if id == correct_id:
|
||||
@@ -525,7 +625,7 @@ func _advance_or_finish() -> void:
|
||||
func _finish() -> void:
|
||||
if BlockSessionService.session_kind == "core":
|
||||
BlockSessionService.finish_block()
|
||||
main.show_screen("dashboard")
|
||||
main.show_screen("selection", "step3")
|
||||
return
|
||||
var results := BlockSessionService.finish_block()
|
||||
main.show_screen("review", results)
|
||||
@@ -534,7 +634,7 @@ func _finish() -> void:
|
||||
func _exit_run() -> void:
|
||||
BlockSessionService.resume_current_question_timer()
|
||||
BlockSessionService.finish_block()
|
||||
main.show_screen("dashboard")
|
||||
main.show_screen("selection", "step3")
|
||||
|
||||
|
||||
func _update_timer_bar() -> void:
|
||||
@@ -690,6 +790,61 @@ func _refresh_toolbar_state() -> void:
|
||||
Style.style_chip(report_button, Style.RED if reported_question_ids.has(question_id) else Style.CYAN)
|
||||
|
||||
|
||||
func _refresh_player_stats() -> void:
|
||||
if player_stats_label == null:
|
||||
return
|
||||
var display_name := AuthState.username if AuthState.username != "" else AuthState.role
|
||||
player_stats_label.text = "Rating: %d | %s" % [
|
||||
QuestionBankService.overall_elo,
|
||||
display_name
|
||||
]
|
||||
|
||||
|
||||
func _adjust_question_text_scale(delta: float) -> void:
|
||||
var next_scale: float = clamp(question_text_scale + delta, TEXT_SCALE_MIN, TEXT_SCALE_MAX)
|
||||
if is_equal_approx(next_scale, question_text_scale):
|
||||
return
|
||||
question_text_scale = next_scale
|
||||
_refresh_scaled_fonts(self)
|
||||
_render_stem()
|
||||
_apply_saved_choice_state()
|
||||
|
||||
|
||||
func _set_scaled_label(label: Label, base_size: int, color: Color = Style.TEXT) -> void:
|
||||
label.set_meta("question_base_font_size", base_size)
|
||||
label.add_theme_color_override("font_color", color)
|
||||
label.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
|
||||
|
||||
|
||||
func _set_scaled_button_font(button: Button, base_size: int) -> void:
|
||||
button.set_meta("question_base_font_size", base_size)
|
||||
button.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
|
||||
|
||||
|
||||
func _set_scaled_rich_text(label: RichTextLabel, base_size: int) -> void:
|
||||
label.set_meta("question_base_font_size", base_size)
|
||||
label.add_theme_font_size_override("normal_font_size", _scaled_font_size(base_size))
|
||||
label.add_theme_font_size_override("bold_font_size", _scaled_font_size(base_size))
|
||||
|
||||
|
||||
func _refresh_scaled_fonts(node: Node) -> void:
|
||||
if node.has_meta("question_base_font_size"):
|
||||
var base_size := int(node.get_meta("question_base_font_size"))
|
||||
if node is Label:
|
||||
node.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
|
||||
elif node is Button:
|
||||
node.add_theme_font_size_override("font_size", _scaled_font_size(base_size))
|
||||
elif node is RichTextLabel:
|
||||
node.add_theme_font_size_override("normal_font_size", _scaled_font_size(base_size))
|
||||
node.add_theme_font_size_override("bold_font_size", _scaled_font_size(base_size))
|
||||
for child in node.get_children():
|
||||
_refresh_scaled_fonts(child)
|
||||
|
||||
|
||||
func _scaled_font_size(base_size: int) -> int:
|
||||
return int(round(float(base_size) * question_text_scale))
|
||||
|
||||
|
||||
func _refresh_labs_button() -> void:
|
||||
if labs_button == null:
|
||||
return
|
||||
@@ -728,7 +883,7 @@ func _load_question_media(question: Dictionary) -> void:
|
||||
var status := Label.new()
|
||||
status.text = "Loading image..."
|
||||
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
Style.set_label(status, 15, Style.MUTED)
|
||||
_set_scaled_label(status, 15, Style.MUTED)
|
||||
image_box.add_child(status)
|
||||
|
||||
var texture_rect := TextureRect.new()
|
||||
@@ -847,7 +1002,7 @@ func _build_labs_modal() -> void:
|
||||
var title := Label.new()
|
||||
title.text = "Lab Results"
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
Style.set_label(title, 26, Style.TEXT)
|
||||
_set_scaled_label(title, 26, Style.TEXT)
|
||||
header.add_child(title)
|
||||
|
||||
var close_button := Button.new()
|
||||
@@ -926,7 +1081,7 @@ func _add_lab_table_label(parent: GridContainer, text: String, color: Color, is_
|
||||
label.text = text if text.strip_edges() != "" else "-"
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
Style.set_label(label, 16 if not is_header else 17, color)
|
||||
_set_scaled_label(label, 16 if not is_header else 17, color)
|
||||
parent.add_child(label)
|
||||
|
||||
|
||||
@@ -983,8 +1138,7 @@ func _subtract_highlight_range(highlights: Array, start: int, end: int) -> bool:
|
||||
|
||||
|
||||
func _apply_saved_choice_state() -> void:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
var choices := _get_presented_choices()
|
||||
for index in range(choices.size()):
|
||||
var id := str(choices[index].get("id", ""))
|
||||
var has_followup_panel := index < choice_followup_panels.size() and choice_followup_panels[index].visible
|
||||
@@ -1008,10 +1162,25 @@ func _apply_saved_choice_state() -> void:
|
||||
var id := str(choices[index].get("id", ""))
|
||||
choice_buttons[index].disabled = id != active_followup_choice_id
|
||||
|
||||
_refresh_submit_state()
|
||||
|
||||
|
||||
func _refresh_submit_state() -> void:
|
||||
if next_button == null:
|
||||
return
|
||||
if pending_correct_advance or followup_required:
|
||||
next_button.disabled = true
|
||||
return
|
||||
if not current_answer.is_empty():
|
||||
next_button.disabled = false
|
||||
return
|
||||
next_button.disabled = selected_choice == ""
|
||||
|
||||
|
||||
func _set_choice_button_text(index: int, choice: Dictionary) -> void:
|
||||
var choice_id := str(choice.get("id", ""))
|
||||
var choice_text := str(choice.get("text", ""))
|
||||
var display_label := _display_label_for_index(index)
|
||||
var has_visible_followup := index < choice_followup_panels.size() and choice_followup_panels[index].visible
|
||||
choice_buttons[index].custom_minimum_size = _choice_row_size(index, choice_id, has_visible_followup)
|
||||
_set_choice_percent_label(index, "")
|
||||
@@ -1021,21 +1190,21 @@ func _set_choice_button_text(index: int, choice: Dictionary) -> void:
|
||||
if percentage >= 0:
|
||||
_set_choice_percent_label(index, "%d%%" % percentage)
|
||||
if has_visible_followup:
|
||||
var wrong_text := "%s. %s" % [choice_id, choice_text]
|
||||
var wrong_text := "%s. %s" % [display_label, choice_text]
|
||||
choice_buttons[index].text = ""
|
||||
_set_choice_top_label(index, wrong_text)
|
||||
else:
|
||||
var wrong_text := "%s. %s" % [choice_id, _strike_text(choice_text)]
|
||||
var wrong_text := "%s. %s" % [display_label, _strike_text(choice_text)]
|
||||
choice_buttons[index].text = wrong_text
|
||||
elif _should_show_correct_choice_percentage(choice_id):
|
||||
var correct_percentage := _get_choice_selection_percentage(choice_id)
|
||||
if correct_percentage >= 0:
|
||||
_set_choice_percent_label(index, "%d%%" % correct_percentage)
|
||||
choice_buttons[index].text = "%s. %s" % [choice_id, choice_text]
|
||||
choice_buttons[index].text = "%s. %s" % [display_label, choice_text]
|
||||
elif eliminated.has(choice_id):
|
||||
choice_buttons[index].text = "%s. [eliminated] %s" % [choice_id, _strike_text(choice_text)]
|
||||
choice_buttons[index].text = "%s. [eliminated] %s" % [display_label, _strike_text(choice_text)]
|
||||
else:
|
||||
choice_buttons[index].text = "%s. %s" % [choice_id, choice_text]
|
||||
choice_buttons[index].text = "%s. %s" % [display_label, choice_text]
|
||||
|
||||
|
||||
func _style_choice_button_for_state(index: int, choice_id: String) -> void:
|
||||
@@ -1095,7 +1264,7 @@ func _should_show_correct_choice_percentage(choice_id: String) -> bool:
|
||||
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", [])
|
||||
var choices := _get_presented_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)
|
||||
@@ -1127,11 +1296,14 @@ func _build_followup_panel(index: int, choice_id: String) -> void:
|
||||
var panel := choice_followup_panels[index]
|
||||
var content := choice_followup_contents[index]
|
||||
var followup := _get_followup_for_choice(choice_id)
|
||||
var followup_choices: Array = followup.get("choices", []).duplicate(true)
|
||||
followup_choices.shuffle()
|
||||
followup["choices"] = followup_choices
|
||||
followup_states[choice_id] = {
|
||||
"choice_id": choice_id,
|
||||
"choice_text": _choice_text_for_id(choice_id),
|
||||
"prompt": str(followup.get("prompt", "")),
|
||||
"choices": followup.get("choices", []),
|
||||
"choices": followup_choices,
|
||||
"correct_choice_id": str(followup.get("correct_choice_id", "")),
|
||||
"teaching_point": str(followup.get("teaching_point", "")),
|
||||
"selected_option_id": "",
|
||||
@@ -1145,19 +1317,21 @@ func _build_followup_panel(index: int, choice_id: String) -> void:
|
||||
var prompt := Label.new()
|
||||
prompt.text = str(followup.get("prompt", "What finding in the stem makes this answer less appropriate?"))
|
||||
prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(prompt, 14, Style.TEXT)
|
||||
_set_scaled_label(prompt, 14, Style.TEXT)
|
||||
content.add_child(prompt)
|
||||
|
||||
var choices: Array = followup.get("choices", [])
|
||||
var choices: Array = followup_choices
|
||||
if bool(followup.get("has_authored_choices", false)):
|
||||
for option in choices:
|
||||
for option_index in range(choices.size()):
|
||||
var option = choices[option_index]
|
||||
if typeof(option) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var option_button := Button.new()
|
||||
option_button.text = "%s. %s" % [str(option.get("id", "")), str(option.get("text", ""))]
|
||||
option_button.text = "%s. %s" % [_display_label_for_index(option_index), str(option.get("text", ""))]
|
||||
option_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
option_button.custom_minimum_size = Vector2(0, 32)
|
||||
Style.style_chip(option_button, Style.CYAN)
|
||||
_set_scaled_button_font(option_button, 15)
|
||||
option_button.set_meta("option_id", str(option.get("id", "")))
|
||||
option_button.pressed.connect(func(option_id := str(option.get("id", "")), selected_button := option_button, followup_data := followup):
|
||||
_answer_followup(option_id, selected_button, followup_data)
|
||||
@@ -1173,6 +1347,7 @@ func _build_followup_panel(index: int, choice_id: String) -> void:
|
||||
continue_button.text = "Continue"
|
||||
continue_button.custom_minimum_size = Vector2(130, 32)
|
||||
Style.style_chip(continue_button, Style.CYAN)
|
||||
_set_scaled_button_font(continue_button, 15)
|
||||
continue_button.pressed.connect(func(followup_data := followup): _resolve_followup(followup_data, null, false, false))
|
||||
action_row.add_child(continue_button)
|
||||
|
||||
@@ -1209,7 +1384,7 @@ func _resolve_followup(followup: Dictionary, selected_button: Button, style_sele
|
||||
var teaching := Label.new()
|
||||
teaching.text = str(followup.get("teaching_point", "Review the relevant stem finding before choosing again."))
|
||||
teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(teaching, 14, Style.AMBER)
|
||||
_set_scaled_label(teaching, 14, Style.AMBER)
|
||||
choice_followup_contents[index].add_child(teaching)
|
||||
|
||||
if followup_states.has(resolved_choice_id):
|
||||
@@ -1280,10 +1455,11 @@ func _render_followup_state(choice_id: String) -> void:
|
||||
func _build_collapsed_followup(index: int, state: Dictionary) -> void:
|
||||
var content := choice_followup_contents[index]
|
||||
var summary := Button.new()
|
||||
summary.text = "Review: %s. %s" % [str(state.get("choice_id", "")), str(state.get("choice_text", ""))]
|
||||
summary.text = "Review: %s. %s" % [_display_label_for_choice_id(str(state.get("choice_id", ""))), str(state.get("choice_text", ""))]
|
||||
summary.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
summary.custom_minimum_size = Vector2(0, 40)
|
||||
Style.style_chip(summary, Style.AMBER)
|
||||
_set_scaled_button_font(summary, 15)
|
||||
summary.gui_input.connect(func(_event: InputEvent): _mark_review_input_handled())
|
||||
summary.pressed.connect(func(choice_id := str(state.get("choice_id", ""))): _toggle_followup_state(choice_id))
|
||||
content.add_child(summary)
|
||||
@@ -1292,10 +1468,11 @@ func _build_collapsed_followup(index: int, state: Dictionary) -> void:
|
||||
func _build_readonly_followup(index: int, state: Dictionary) -> void:
|
||||
var content := choice_followup_contents[index]
|
||||
var summary := Button.new()
|
||||
summary.text = "Hide review: %s. %s" % [str(state.get("choice_id", "")), str(state.get("choice_text", ""))]
|
||||
summary.text = "Hide review: %s. %s" % [_display_label_for_choice_id(str(state.get("choice_id", ""))), str(state.get("choice_text", ""))]
|
||||
summary.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
summary.custom_minimum_size = Vector2(0, 36)
|
||||
Style.style_chip(summary, Style.AMBER)
|
||||
_set_scaled_button_font(summary, 15)
|
||||
summary.gui_input.connect(func(_event: InputEvent): _mark_review_input_handled())
|
||||
summary.pressed.connect(func(choice_id := str(state.get("choice_id", ""))): _toggle_followup_state(choice_id))
|
||||
content.add_child(summary)
|
||||
@@ -1303,20 +1480,20 @@ func _build_readonly_followup(index: int, state: Dictionary) -> void:
|
||||
var prompt := Label.new()
|
||||
prompt.text = str(state.get("prompt", ""))
|
||||
prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(prompt, 14, Style.TEXT)
|
||||
_set_scaled_label(prompt, 14, Style.TEXT)
|
||||
content.add_child(prompt)
|
||||
|
||||
var selected := Label.new()
|
||||
var selected_text := str(state.get("selected_option_text", ""))
|
||||
selected.text = "Your follow-up answer: %s" % selected_text if selected_text != "" else "Follow-up reviewed."
|
||||
selected.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(selected, 14, Style.LIME if bool(state.get("was_correct", false)) else Style.RED)
|
||||
_set_scaled_label(selected, 14, Style.LIME if bool(state.get("was_correct", false)) else Style.RED)
|
||||
content.add_child(selected)
|
||||
|
||||
var teaching := Label.new()
|
||||
teaching.text = str(state.get("teaching_point", ""))
|
||||
teaching.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(teaching, 14, Style.AMBER)
|
||||
_set_scaled_label(teaching, 14, Style.AMBER)
|
||||
content.add_child(teaching)
|
||||
|
||||
|
||||
@@ -1361,9 +1538,23 @@ func _get_followup_for_choice(choice_id: String) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
func _get_presented_choices() -> Array:
|
||||
if not presented_choices.is_empty():
|
||||
return presented_choices
|
||||
return BlockSessionService.get_current_question().get("choices", [])
|
||||
|
||||
|
||||
func _display_label_for_index(index: int) -> String:
|
||||
return String.chr(65 + index)
|
||||
|
||||
|
||||
func _display_label_for_choice_id(choice_id: String) -> String:
|
||||
var index := _choice_index_for_id(choice_id)
|
||||
return _display_label_for_index(index) if index >= 0 else choice_id
|
||||
|
||||
|
||||
func _choice_index_for_id(choice_id: String) -> int:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
var choices := _get_presented_choices()
|
||||
for index in range(choices.size()):
|
||||
if str(choices[index].get("id", "")) == choice_id:
|
||||
return index
|
||||
@@ -1371,8 +1562,7 @@ func _choice_index_for_id(choice_id: String) -> int:
|
||||
|
||||
|
||||
func _choice_text_for_id(choice_id: String) -> String:
|
||||
var question := BlockSessionService.get_current_question()
|
||||
var choices: Array = question.get("choices", [])
|
||||
var choices := _get_presented_choices()
|
||||
for choice in choices:
|
||||
if typeof(choice) == TYPE_DICTIONARY and str(choice.get("id", "")) == choice_id:
|
||||
return str(choice.get("text", ""))
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
extends VBoxContainer
|
||||
|
||||
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||
|
||||
var main: Control
|
||||
var exam_view: VBoxContainer
|
||||
var mode_view: VBoxContainer
|
||||
var exam_grid: GridContainer
|
||||
var mode_grid: GridContainer
|
||||
var question_bank_button: Button
|
||||
var bank_status_label: Label
|
||||
var bank_error_label: Label
|
||||
var retry_button: Button
|
||||
var current_stage := "exam"
|
||||
var pending_question_bank_launch := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_theme_constant_override("separation", 18)
|
||||
resized.connect(_refresh_grid_columns)
|
||||
if not QuestionBankService.questions_changed.is_connected(_on_questions_changed):
|
||||
QuestionBankService.questions_changed.connect(_on_questions_changed)
|
||||
if not QuestionBankService.questions_load_failed.is_connected(_on_questions_load_failed):
|
||||
QuestionBankService.questions_load_failed.connect(_on_questions_load_failed)
|
||||
|
||||
_build_exam_view()
|
||||
_build_mode_view()
|
||||
_show_stage("exam")
|
||||
|
||||
|
||||
func on_show(payload: Variant = null) -> void:
|
||||
if str(payload) == "step3":
|
||||
_show_stage("step3")
|
||||
elif str(payload) == "exam":
|
||||
_show_stage("exam")
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func on_hide() -> void:
|
||||
pending_question_bank_launch = false
|
||||
|
||||
|
||||
func _build_exam_view() -> void:
|
||||
exam_view = VBoxContainer.new()
|
||||
exam_view.add_theme_constant_override("separation", 18)
|
||||
exam_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(exam_view)
|
||||
|
||||
var eyebrow := Label.new()
|
||||
eyebrow.text = "CHOOSE YOUR EXAM"
|
||||
Style.set_label(eyebrow, 14, Style.CYAN)
|
||||
exam_view.add_child(eyebrow)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "What are you preparing for?"
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
exam_view.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "Select a path to enter its study modes. More exams will be added as they are ready."
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(subtitle, 17, Style.MUTED)
|
||||
exam_view.add_child(subtitle)
|
||||
|
||||
exam_grid = GridContainer.new()
|
||||
exam_grid.columns = 2
|
||||
exam_grid.add_theme_constant_override("h_separation", 16)
|
||||
exam_grid.add_theme_constant_override("v_separation", 16)
|
||||
exam_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
exam_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
exam_view.add_child(exam_grid)
|
||||
|
||||
_add_exam_option("STEP 1", "Foundational sciences", false)
|
||||
_add_exam_option("STEP 2", "Clinical knowledge", false)
|
||||
_add_exam_option("STEP 3", "Advanced clinical management", true)
|
||||
_add_exam_option("CCS Cases", "Interactive case simulations", false)
|
||||
|
||||
|
||||
func _build_mode_view() -> void:
|
||||
mode_view = VBoxContainer.new()
|
||||
mode_view.add_theme_constant_override("separation", 18)
|
||||
mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(mode_view)
|
||||
|
||||
var back_button := Button.new()
|
||||
back_button.text = "< All exams"
|
||||
back_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
back_button.custom_minimum_size = Vector2(150, 42)
|
||||
Style.style_chip(back_button, Style.CYAN)
|
||||
back_button.pressed.connect(func(): _show_stage("exam"))
|
||||
mode_view.add_child(back_button)
|
||||
|
||||
var eyebrow := Label.new()
|
||||
eyebrow.text = "STEP 3"
|
||||
Style.set_label(eyebrow, 14, Style.LIME)
|
||||
mode_view.add_child(eyebrow)
|
||||
|
||||
var title := Label.new()
|
||||
title.text = "Choose a study mode"
|
||||
Style.set_label(title, 34, Style.TEXT)
|
||||
mode_view.add_child(title)
|
||||
|
||||
var subtitle := Label.new()
|
||||
subtitle.text = "Build clinical judgment through focused questions. Additional modes are in development."
|
||||
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(subtitle, 17, Style.MUTED)
|
||||
mode_view.add_child(subtitle)
|
||||
|
||||
mode_grid = GridContainer.new()
|
||||
mode_grid.columns = 2
|
||||
mode_grid.add_theme_constant_override("h_separation", 16)
|
||||
mode_grid.add_theme_constant_override("v_separation", 16)
|
||||
mode_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
mode_view.add_child(mode_grid)
|
||||
|
||||
question_bank_button = Button.new()
|
||||
question_bank_button.text = "Question Bank\nAdaptive core run | Available"
|
||||
question_bank_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
question_bank_button.custom_minimum_size = Vector2(360, 150)
|
||||
question_bank_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
question_bank_button.add_theme_font_size_override("font_size", 20)
|
||||
Style.style_primary_button(question_bank_button)
|
||||
question_bank_button.pressed.connect(_select_question_bank)
|
||||
mode_grid.add_child(question_bank_button)
|
||||
|
||||
var dynamic_button := Button.new()
|
||||
dynamic_button.text = "Dynamic Questions\nAI-generated practice | Coming Soon"
|
||||
dynamic_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
dynamic_button.custom_minimum_size = Vector2(360, 150)
|
||||
dynamic_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
dynamic_button.add_theme_font_size_override("font_size", 20)
|
||||
dynamic_button.disabled = true
|
||||
mode_grid.add_child(dynamic_button)
|
||||
|
||||
bank_status_label = Label.new()
|
||||
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_status_label, 16, Style.CYAN)
|
||||
mode_view.add_child(bank_status_label)
|
||||
|
||||
bank_error_label = Label.new()
|
||||
bank_error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_error_label, 15, Style.RED)
|
||||
mode_view.add_child(bank_error_label)
|
||||
|
||||
retry_button = Button.new()
|
||||
retry_button.text = "Retry Question Bank"
|
||||
retry_button.custom_minimum_size = Vector2(220, 46)
|
||||
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_chip(retry_button, Style.AMBER)
|
||||
retry_button.pressed.connect(_retry_question_bank)
|
||||
mode_view.add_child(retry_button)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
mode_view.add_child(spacer)
|
||||
|
||||
|
||||
func _add_exam_option(option_name: String, description: String, available: bool) -> void:
|
||||
var button := Button.new()
|
||||
button.text = "%s\n%s | %s" % [option_name, description, "Available" if available else "Coming Soon"]
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.custom_minimum_size = Vector2(360, 150)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
button.add_theme_font_size_override("font_size", 20)
|
||||
button.disabled = not available
|
||||
if available:
|
||||
Style.style_primary_button(button)
|
||||
button.pressed.connect(func(): _show_stage("step3"))
|
||||
exam_grid.add_child(button)
|
||||
|
||||
|
||||
func _show_stage(stage_name: String) -> void:
|
||||
current_stage = stage_name
|
||||
pending_question_bank_launch = false
|
||||
if exam_view != null:
|
||||
exam_view.visible = stage_name == "exam"
|
||||
if mode_view != null:
|
||||
mode_view.visible = stage_name == "step3"
|
||||
_refresh_grid_columns()
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _select_question_bank() -> void:
|
||||
if QuestionBankService.has_questions() and QuestionBankService.load_state == "ready":
|
||||
_start_question_bank()
|
||||
return
|
||||
|
||||
pending_question_bank_launch = true
|
||||
if QuestionBankService.load_state == "idle":
|
||||
QuestionBankService.load_questions_from_api()
|
||||
elif QuestionBankService.load_state == "failed":
|
||||
QuestionBankService.retry_now()
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _retry_question_bank() -> void:
|
||||
pending_question_bank_launch = true
|
||||
QuestionBankService.retry_now()
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _on_questions_changed() -> void:
|
||||
_refresh_bank_state()
|
||||
if pending_question_bank_launch and visible and current_stage == "step3" and QuestionBankService.load_state == "ready":
|
||||
pending_question_bank_launch = false
|
||||
call_deferred("_start_question_bank")
|
||||
|
||||
|
||||
func _on_questions_load_failed(_message: String) -> void:
|
||||
pending_question_bank_launch = false
|
||||
_refresh_bank_state()
|
||||
|
||||
|
||||
func _refresh_bank_state() -> void:
|
||||
if question_bank_button == null:
|
||||
return
|
||||
var state := QuestionBankService.load_state
|
||||
question_bank_button.disabled = pending_question_bank_launch
|
||||
retry_button.visible = state == "failed" or state == "retrying"
|
||||
retry_button.disabled = state == "retrying"
|
||||
bank_error_label.text = QuestionBankService.last_error if state == "failed" or state == "retrying" else ""
|
||||
if pending_question_bank_launch or state == "loading":
|
||||
bank_status_label.text = "Preparing your question bank..."
|
||||
elif state == "retrying":
|
||||
bank_status_label.text = "Question bank unavailable. Retrying in %d seconds." % QuestionBankService.next_retry_seconds
|
||||
elif state == "ready":
|
||||
bank_status_label.text = "%d questions ready" % QuestionBankService.get_all_questions().size()
|
||||
elif state == "failed":
|
||||
bank_status_label.text = "The question bank could not be loaded."
|
||||
else:
|
||||
bank_status_label.text = "Question bank will load when selected."
|
||||
|
||||
|
||||
func _start_question_bank() -> void:
|
||||
if not visible or current_stage != "step3" or not QuestionBankService.has_questions():
|
||||
return
|
||||
if BlockSessionService.start_core_run():
|
||||
main.show_screen("player")
|
||||
else:
|
||||
bank_error_label.text = BlockSessionService.core_status_message
|
||||
|
||||
|
||||
func _refresh_grid_columns() -> void:
|
||||
var column_count := 1 if size.x < 820.0 else 2
|
||||
if exam_grid != null:
|
||||
exam_grid.columns = column_count
|
||||
if mode_grid != null:
|
||||
mode_grid.columns = column_count
|
||||
@@ -0,0 +1 @@
|
||||
uid://db6gcbemiotwl
|
||||
Reference in New Issue
Block a user