// ---------- Effets visuels par modèle de "❓ Boîte à quiz" ---------- // Registre appelé par static/js/play/dialogue-box-controller.js (JAMAIS // l'inverse) aux points d'extension du moteur PARTAGÉ (forgeShowQuizBox, // forgeQuizBoxAnswer) — voir screens/rendering/quiz_box_templates.py // pour le HTML/CSS de chaque modèle. Ajouter un futur modèle n'oblige // donc jamais à toucher dialogue-box-controller.js : juste une nouvelle // entrée ici (optionnelle) + son CSS/HTML. // Effet PARTAGÉ entre plusieurs modèles "page de quiz" (Manga, Classique, // et tout futur modèle qui voudrait la même mécanique) — demande // explicite : au-dessus de la question, "Question N/M" (nombre de // QUESTIONS de ce dialogue, pas la position brute dans lines qui mélange // aussi les répliques) plutôt que le nom de l'objet déclencheur — et une // barre d'étapes juste en dessous du bandeau. `forgeQuizBoxState`/ // `forgeDialogueBoxState` (déclarés dans dialogue-box-controller.js, // chargé à côté de ce fichier) sont des variables GLOBALES de script // classique (pas de module), lisibles ici telles quelles. // Extrait de forgeQuizProgressOnShow (lot 4 "modernisation JS", S7721) : // fonction pure (aucune fermeture), pas besoin d'être redéfinie à chaque appel. function isQuestionLine(line) { return line?.type === 'question'; } // Extrait de forgeQuizProgressOnShow (S3358) : segment "fait"/"courant"/ // "à venir" de la barre d'étapes pour la question d'indice `i` (1-based). function forgeQuizProgressBarSegmentClass(i, current) { if (i < current) return 'is-done'; if (i === current) return 'is-current'; return ''; } // `progressSelector` : chaque modèle a son propre conteneur de barre // d'étapes (data-manga-role/data-classique-role, jamais data-quiz-role — // décoration propre à CHAQUE modèle, pas du contrat partagé). function forgeQuizProgressOnShow(widget, progressSelector) { if (!widget) return; const quizObjectId = widget.dataset.objectId; const quizState = (typeof forgeQuizBoxState !== 'undefined') ? forgeQuizBoxState[quizObjectId] : null; if (!quizState) return; const dialogueState = (typeof forgeDialogueBoxState !== 'undefined') ? forgeDialogueBoxState[quizState.dialogueObjectId] : null; if (!dialogueState) return; const total = dialogueState.lines.filter(isQuestionLine).length; if (total === 0) return; const current = dialogueState.lines.slice(0, dialogueState.index + 1).filter(isQuestionLine).length; const header = widget.querySelector('[data-quiz-role="header"]'); if (header) header.textContent = `Question ${current}/${total}`; const progressEl = widget.querySelector(progressSelector); if (progressEl) { const bars = []; for (let i = 1; i <= total; i++) { bars.push(``); } progressEl.innerHTML = bars.join(''); } } function forgeQuizMangaOnShow(widget) { // Défensif : si CE widget est réutilisé pour un quiz déclenché à // nouveau plus tard dans la partie (voir forgeQuizResultQuit, // dialogue-box-controller.js — le widget lui-même n'est jamais // reconstruit, juste masqué), une question fraîchement affichée ne // doit jamais rester sur l'écran de résultat de la fois précédente. if (widget) widget.classList.remove('is-showing-result'); forgeQuizProgressOnShow(widget, '[data-manga-role="progress"]'); } function forgeQuizClassiqueOnShow(widget) { // Même garde-fou que forgeQuizMangaOnShow ci-dessus, ici posée sur // .classiqueContent (pas la racine du widget, voir // render_quiz_box_page_classique) plutôt que sur le widget lui-même. if (widget) { const content = widget.querySelector('.classiqueContent'); if (content) content.classList.remove('is-showing-result'); } forgeQuizProgressOnShow(widget, '[data-classique-role="progress"]'); } // Écrans de résultat de fin de quiz ("classique" ci-dessous, "manga" // plus bas) — appelés par forgeQuizPrepareResultScreen // (dialogue-box-controller.js) UNIQUEMENT à la toute dernière question // (ou si le minuteur du quiz ENTIER expire). `result.correctCount`/ // `result.totalCount` : comptés PAR CE QUIZ (voir forgeQuizBoxState dans // dialogue-box-controller.js), jamais window.forgeQuizScore/ // forgeQuizCorrectAnswered qui cumulent TOUTE la partie. Le seuil de // réussite réutilise EXACTEMENT window.FORGE_SUCCESS_THRESHOLD_PERCENT // (déjà utilisé pour le statut SCORM final, voir // forgeComputeFinalScormStatus) plutôt qu'un réglage à part. Le bouton // QUITTE le quiz (jamais ne le recommence, demande explicite) — voir // forgeQuizResultQuit, dialogue-box-controller.js. function forgeQuizClassiqueOnQuizComplete(widget, result) { if (!widget) return; const content = widget.querySelector('.classiqueContent'); const zone = widget.querySelector('[data-classique-role="result"]'); if (!content || !zone) return; const total = result?.totalCount || 0; const correct = result?.correctCount || 0; const pct = total > 0 ? Math.round((correct / total) * 100) : 0; const threshold = (typeof window !== 'undefined' && typeof window.FORGE_SUCCESS_THRESHOLD_PERCENT === 'number') ? window.FORGE_SUCCESS_THRESHOLD_PERCENT : 70; const passed = pct >= threshold; const titleEl = widget.querySelector('.classiqueHeaderTitle'); const title = titleEl ? titleEl.textContent : 'Quiz'; const quizObjectId = widget.dataset.objectId; zone.innerHTML = `
${pct}%
` + `

${correct} bonne${correct > 1 ? 's' : ''} réponse${correct > 1 ? 's' : ''} sur ${total}

` + `

« ${typeof forgeEscapeHtml === 'function' ? forgeEscapeHtml(title) : title} »

` + `
${ passed ? '✓ Module validé' : `✕ Module non validé — seuil ${threshold}%` }
` + ``; content.classList.add('is-showing-result'); } // "Manga" — même mécanique que forgeQuizClassiqueOnQuizComplete // ci-dessus, thème manga propre à ce modèle (voir static/style.css) : // pas de conteneur "content" à part ici (contrairement à // .classiqueContent), la classe "is-showing-result" bascule sur la // RACINE du widget elle-même (voir render_quiz_box_page_manga). La // barre d'étapes est reposée à 100% "terminé" (tous les segments // is-done) : forgeQuizProgressOnShow ne le fait pas toute seule, sa // dernière question reste "is-current" (jamais "is-done", il n'y a // plus de question suivante vers laquelle avancer) — un quiz FINI doit // visuellement se lire comme entièrement complété. function forgeQuizMangaOnQuizComplete(widget, result) { if (!widget) return; const zone = widget.querySelector('[data-manga-role="result"]'); if (!zone) return; const total = result?.totalCount || 0; const correct = result?.correctCount || 0; const pct = total > 0 ? Math.round((correct / total) * 100) : 0; const threshold = (typeof window !== 'undefined' && typeof window.FORGE_SUCCESS_THRESHOLD_PERCENT === 'number') ? window.FORGE_SUCCESS_THRESHOLD_PERCENT : 70; const passed = pct >= threshold; const quizObjectId = widget.dataset.objectId; zone.innerHTML = '
' + 'Fin du quiz' + `

Score final : ${correct} / ${total}

` + `
${passed ? '合格' : '不合格'}
` + `
` + '
'; const progressEl = widget.querySelector('[data-manga-role="progress"]'); if (progressEl && total > 0) { const bars = []; for (let i = 0; i < total; i++) bars.push(''); progressEl.innerHTML = bars.join(''); } widget.classList.add('is-showing-result'); } const forgeQuizTemplateEffects = { // Modèles "boîte de dialogue" (voir screens/rendering/quiz_box_config.py // ::QUIZ_BOX_DIALOG_TEMPLATES) — "Verre dépoli"/"Néon"/"Minimal"/ // "Ludique" ont été retirés (demande explicite : "supprime tous les // modele de boite sauf le base"). "manga_dialogue" (clé DIFFÉRENTE du // modèle "page" "manga" ci-dessous, même registre partagé) reste // proche de "defaut" — aucun effet JS particulier, juste sa propre // décoration CSS (voir render_quiz_box_manga_dialogue). defaut: {}, manga_dialogue: {}, // Modèles "page de quiz" (QUIZ_BOX_PAGE_TEMPLATES) — le tampon "正解"/ // "不正解" du modèle "manga" est posé en CSS pur (::after sur // .is-correct/.is-wrong, voir static/style.css), seul "Question N/M" + // la barre d'étapes ont besoin d'un vrai calcul JS (onShow ci-dessus). manga: { onShow: forgeQuizMangaOnShow, onQuizComplete: forgeQuizMangaOnQuizComplete }, classique: { onShow: forgeQuizClassiqueOnShow, onQuizComplete: forgeQuizClassiqueOnQuizComplete }, }; // static/js/play/__tests__/ (node:test) — même garde que le reste de // static/js/play/. if (typeof module !== 'undefined' && module.exports) { module.exports = { forgeQuizTemplateEffects, forgeQuizProgressOnShow, forgeQuizMangaOnShow, forgeQuizMangaOnQuizComplete, forgeQuizClassiqueOnShow, forgeQuizClassiqueOnQuizComplete, }; }