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))