Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur Support de formation (voir docs/plan/PLAN.md), sans toucher a l'architecture en couches existante : - screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D, avant l'arrivee d'un second "moteur" cote document) : ~85 imports corriges, contrat import-linter mis a jour, meme forme de couches. - routes/, scripts/, static/, templates/, tests/ : tout ce qui est propre au jeu 2D deplace dans un sous-dossier game/ de chacun (routes/game/, static/game/, templates/game/, tests/game/, scripts/game/) ; ce qui est partage par le site (auth, onboarding, dashboard, uploads, db/) reste a la racine de chaque dossier. Un sous-dossier document/ (vide) cree dans chacun pour le futur chantier. - styles/ volontairement inchange : les 3 fichiers sources sont concatenes en un seul static/style.css charge par tout le site, scinder leur CONTENU (editeur vs partage) serait un refactor CSS distinct, pas un deplacement mecanique. - Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en profondeur : copie des assets, URLs d'icones relatives a static/style.css (qui ne bouge pas), manifeste, wrapper SCORM. - Deux regressions d'un sweep de renommage anterieur corrigees au passage (screens.js/screens/scene-objects incorrectement convertis en game_engine.js/game_engine/scene-objects dans des commentaires). - Effet de bord Windows decouvert et corrige : git mv + Path.write_text convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers normalises en LF. - .eslintrc.json/package.json : uniquement les chemins de glob mis a jour (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7 reste volontairement non committee (package-lock.json restaure a la version precedente). Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit, lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ; demarrage serveur + requetes HTTP manuelles confirmant que les assets deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien. SKIP=djlint : backlog H021 (styles inline) deja documente comme dette assumee dans CODE_QUALITY.md section 6, aucun template touche par ce commit au-dela d'un deplacement de fichier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
178 lines
9.4 KiB
JavaScript
178 lines
9.4 KiB
JavaScript
// ---------- Effets visuels par modèle de "❓ Boîte à quiz" ----------
|
|
// Registre appelé par static/game/js/play/dialogue-box-controller.js (JAMAIS
|
|
// l'inverse) aux points d'extension du moteur PARTAGÉ (forgeShowQuizBox,
|
|
// forgeQuizBoxAnswer) — voir game_engine/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(`<i class="${forgeQuizProgressBarSegmentClass(i, current)}"></i>`);
|
|
}
|
|
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 = `<div class="classiqueResultRing" style="--pct:${pct}"><span>${pct}%</span></div>`
|
|
+ `<p class="classiqueResultTitle">${correct} bonne${correct > 1 ? 's' : ''} réponse${correct > 1 ? 's' : ''} sur ${total}</p>`
|
|
+ `<p class="classiqueResultSub">« ${typeof forgeEscapeHtml === 'function' ? forgeEscapeHtml(title) : title} »</p>`
|
|
+ `<div class="classiqueResultBadge ${passed ? 'pass' : 'fail'}">${
|
|
passed ? '✓ Module validé' : `✕ Module non validé — seuil ${threshold}%`
|
|
}</div>`
|
|
+ `<button type="button" class="classiqueQuitBtn" onclick="forgeQuizResultQuit(${quizObjectId})">✕ Quitter le quiz</button>`;
|
|
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 = '<div class="mangaResultCard">'
|
|
+ '<span class="mangaResultTitle">Fin du quiz</span>'
|
|
+ `<p class="mangaResultScore">Score final : ${correct} / ${total}</p>`
|
|
+ `<div class="mangaResultBadge ${passed ? 'pass' : 'fail'}">${passed ? '合格' : '不合格'}</div>`
|
|
+ `<div><button type="button" class="mangaQuitBtn" onclick="forgeQuizResultQuit(${quizObjectId})">Quitter le quiz</button></div>`
|
|
+ '</div>';
|
|
const progressEl = widget.querySelector('[data-manga-role="progress"]');
|
|
if (progressEl && total > 0) {
|
|
const bars = [];
|
|
for (let i = 0; i < total; i++) bars.push('<i class="is-done"></i>');
|
|
progressEl.innerHTML = bars.join('');
|
|
}
|
|
widget.classList.add('is-showing-result');
|
|
}
|
|
|
|
const forgeQuizTemplateEffects = {
|
|
// Modèles "boîte de dialogue" (voir game_engine/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/game/js/play/__tests__/ (node:test) — même garde que le reste de
|
|
// static/game/js/play/.
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
forgeQuizTemplateEffects,
|
|
forgeQuizProgressOnShow,
|
|
forgeQuizMangaOnShow,
|
|
forgeQuizMangaOnQuizComplete,
|
|
forgeQuizClassiqueOnShow,
|
|
forgeQuizClassiqueOnQuizComplete,
|
|
};
|
|
}
|