From ba155fd38ad7d16c529018d25f3537b4a78b4528 Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 23 Jul 2026 05:06:18 -0500 Subject: [PATCH] Fix distractor explanations rendering on unanswered questions Guarded the answer-review render path (teaching cards, review follow-ups, distractor explanations) on current_answer/pending_correct_advance directly rather than solely on correct_review_panel.visible, and added the same guard inside _render_unselected_distractor_explanations itself. This closes a path where a fresh, never-answered question could show wrong-answer explanations immediately, before any selection or submission. Also: unselected distractors now render in the same red "wrong" card style as selected/locked-wrong choices once a question is being reviewed, instead of staying in the plain default row style. And removed the redundant percentage badge that was floating above the in-progress follow-up quiz prompt, since the choice button above it already shows the same percentage. --- scripts/ui/question_player.gd | 37 +++++++++-------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 1bbb63f..b5a889f 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -943,7 +943,10 @@ func _apply_question_theme() -> void: _style_choice_button_for_state(index, choice_id) for attempted_choice_id in followup_attempt_order: _render_followup_state(str(attempted_choice_id)) - if correct_review_panel != null and correct_review_panel.visible: + # Guarding on current_answer/pending_correct_advance too (not just the panel's + # own visible flag) so these can never render for a question that hasn't + # actually been answered yet, even if visible was left stale by some other path. + if correct_review_panel != null and correct_review_panel.visible and (not current_answer.is_empty() or pending_correct_advance): _render_teaching_cards() _render_review_followups() _render_unselected_distractor_explanations() @@ -1671,6 +1674,10 @@ func _render_unselected_distractor_explanations() -> void: # nest it directly in that choice's own card instead of a repeated # aggregate list at the bottom. Choices that DID trigger a follow-up # already show this same explanation inside the resolved quiz view. + # Guard against ever rendering on a question that hasn't actually been + # answered yet, regardless of what triggered this call. + if current_answer.is_empty() and not pending_correct_advance: + return var question := BlockSessionService.get_current_question() var correct_id := str(question.get("correct_choice_id", "")) var choices := _get_presented_choices() @@ -3034,7 +3041,7 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void: var reviewing := pending_correct_advance or not current_answer.is_empty() if reviewing and choice_id == correct_id: state = "correct" - elif locked_wrong_choice_ids.has(choice_id) or (reviewing and choice_id == selected_choice): + elif locked_wrong_choice_ids.has(choice_id) or (reviewing and choice_id != "" and choice_id != correct_id): state = "wrong" elif choice_id == selected_choice: state = "selected" @@ -3170,7 +3177,6 @@ func _append_followup_attempt(choice_id: String) -> void: func _render_active_followup(index: int, state: Dictionary) -> void: var content := choice_followup_contents[index] - _build_followup_header(content, state) var body_content := _add_followup_body(content, false) var prompt := Label.new() @@ -3269,31 +3275,6 @@ func _build_resolved_followup(index: int, state: Dictionary) -> void: content.add_child(explanation) -func _build_followup_header(content: VBoxContainer, state: Dictionary) -> void: - var dark := _followup_is_dark() - var is_main_correct := bool(state.get("is_main_correct", false)) - var accent := Brand.SUCCESS if is_main_correct else Color("fb5f6d") - var row := HBoxContainer.new() - row.add_theme_constant_override("separation", 10) - content.add_child(row) - - # The choice button above this panel already shows the letter badge and - # choice text, so the header only needs a spacer plus the percentage badge. - var spacer := Control.new() - spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - row.add_child(spacer) - - var percentage := _get_choice_selection_percentage(str(state.get("choice_id", ""))) - if percentage >= 0: - var percentage_label := Label.new() - percentage_label.text = "%d%%" % percentage - percentage_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - percentage_label.custom_minimum_size = Vector2(58, 30) - percentage_label.add_theme_stylebox_override("normal", Brand.badge_box(dark, accent)) - _set_scaled_label(percentage_label, 14, accent) - row.add_child(percentage_label) - - func _add_followup_body(content: VBoxContainer, is_correct: bool) -> VBoxContainer: var body := PanelContainer.new() body.add_theme_stylebox_override("panel", _followup_body_box(_followup_is_dark(), is_correct))