extends Control ## Hote d'exercice : charge la regle, enchaine les exercices d'une mission, ## valide chaque reponse via les validateurs purs (AnswerValidator / ## DictationAnalyzer), applique l'aide graduee (AttemptFlow) et enregistre ## chaque tentative dans ProgressManager (docs/specs/decisions.md, point 7). ## Ne charge et ne sauvegarde rien lui-meme sur disque : ProgressManager et ## SaveManager restent seuls responsables de la persistance. const MISSION_LENGTH := 3 const TYPE_SCENES := { "multiple_choice": "res://scenes/exercises/multiple_choice.tscn", "sentence_correction": "res://scenes/exercises/sentence_correction.tscn", "dictation": "res://scenes/exercises/dictation.tscn", } var _mission: Dictionary = {} var _rule: Dictionary = {} var _skill_id: String = "" var _current_exercise: Dictionary = {} var _current_exercise_scene: Control = null var _current_difficulty := 1 var _attempt_number := 1 var _completed_count := 0 var _last_exercise_id := "" @onready var _exercise_container: Control = %ExerciseContainer @onready var _overlay_container: Control = %OverlayContainer ## Accesseurs reserves aux tests d'integration (scenes/exercises/ ## exercise_host.tscn n'expose pas de script "ami" en GDScript). func get_current_exercise() -> Dictionary: return _current_exercise func get_attempt_number() -> int: return _attempt_number func get_completed_count() -> int: return _completed_count func _ready() -> void: _mission = ExerciseEngine.get_mission(GameManager.pending_mission_id) _rule = ExerciseEngine.get_rule(_mission["rule_id"]) _skill_id = _rule.get("skill", "") _current_difficulty = ProgressManager.next_difficulty_for_skill(_skill_id, 1) _show_rule_panel() func _show_rule_panel() -> void: var rule_panel: Control = preload("res://scenes/ui/rule_panel.tscn").instantiate() _overlay_container.add_child(rule_panel) rule_panel.show_rule(_rule) rule_panel.continued.connect(_on_rule_panel_continued.bind(rule_panel)) func _on_rule_panel_continued(rule_panel: Control) -> void: rule_panel.queue_free() _load_next_exercise() func _load_next_exercise() -> void: _current_exercise = ExerciseEngine.pick_exercise_for_mission( _mission, _last_exercise_id, _current_difficulty ) _attempt_number = 1 if _current_exercise_scene != null: _current_exercise_scene.queue_free() var scene_path: String = TYPE_SCENES[_mission["exercise_type"]] _current_exercise_scene = load(scene_path).instantiate() _exercise_container.add_child(_current_exercise_scene) _current_exercise_scene.load_exercise(_current_exercise) _connect_answer_signal() func _connect_answer_signal() -> void: match _mission["exercise_type"]: "multiple_choice": _current_exercise_scene.answer_submitted.connect(_on_multiple_choice_submitted) "sentence_correction": _current_exercise_scene.answer_submitted.connect(_on_sentence_correction_submitted) "dictation": _current_exercise_scene.answer_submitted.connect(_on_dictation_submitted) func _on_multiple_choice_submitted(selected_index: int) -> void: _handle_result(AnswerValidator.validate_multiple_choice(_current_exercise, selected_index)) func _on_sentence_correction_submitted(selected_indices: Array, corrections: Dictionary) -> void: var typed_indices: Array[int] = [] for index: int in selected_indices: typed_indices.append(index) _handle_result( AnswerValidator.validate_sentence_correction( _current_exercise, typed_indices, corrections ) ) func _on_dictation_submitted(submitted_text: String) -> void: _handle_result(DictationAnalyzer.analyze(_current_exercise, submitted_text)) func _handle_result(result: CorrectionResult) -> void: var support_level := AttemptFlow.SupportLevel.EXPLANATION var is_terminal := result.is_correct if not result.is_correct: support_level = AttemptFlow.support_level_for_attempt(_attempt_number) is_terminal = AttemptFlow.should_end_exercise(_attempt_number) ProgressManager.record_attempt( _skill_id, result.is_correct, _attempt_number > 1, _mission["exercise_type"] ) _show_feedback(result, support_level, is_terminal) func _show_feedback( result: CorrectionResult, support_level: AttemptFlow.SupportLevel, is_terminal: bool ) -> void: var feedback: Control = preload("res://scenes/ui/feedback_panel.tscn").instantiate() _overlay_container.add_child(feedback) feedback.show_result(result, support_level) feedback.continue_pressed.connect(_on_feedback_continued.bind(feedback, is_terminal)) func _on_feedback_continued(feedback: Control, is_terminal: bool) -> void: feedback.queue_free() if not is_terminal: _attempt_number += 1 return _current_difficulty = ProgressManager.next_difficulty_for_skill( _skill_id, _current_difficulty ) _last_exercise_id = _current_exercise.get("id", "") _completed_count += 1 if _completed_count >= MISSION_LENGTH: GameManager.goto(GameManager.SCENE_REWARD) else: _load_next_exercise()