Ajoute la fonctionnalite quiz autonome/plein ecran a la boite a quiz
Introduit la double categorie de modeles (boite de dialogue / page de quiz plein ecran) avec plein ecran, minuteur, score integre et ecran de resultat pour les modeles page ; ajoute les modeles "Manga" (boite et page) et "Classique" (page), pilotables aussi par l'assistant IA Ruby. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
559331f9cf
commit
7db4803b93
@@ -110,16 +110,93 @@ function forgeResetDialogueBoxFooter(widget, objectId) {
|
||||
// ---------- Boîte à quiz ----------
|
||||
|
||||
function forgeQuizBoxWidgetOnCurrentScreen() {
|
||||
return document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"] .quizBoxWidget');
|
||||
var scoped = document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"] .quizBoxWidget');
|
||||
if (scoped) return scoped;
|
||||
// Un quiz PLEIN ÉCRAN a été déplacé hors de ".playScreen" (voir
|
||||
// forgeEscapeCameraForFullscreen ci-dessous) : ne s'y trouve donc plus
|
||||
// une fois la première question affichée — retrouvé via l'attribut
|
||||
// posé au déplacement plutôt que par sa position dans le DOM.
|
||||
return document.querySelector('.quizBoxWidget[data-owner-screen-id="' + window.currentScreenId + '"]');
|
||||
}
|
||||
|
||||
function forgeQuizBoxWidgetById(objectId) {
|
||||
return document.querySelector('.quizBoxWidget[data-object-id="' + objectId + '"]');
|
||||
}
|
||||
|
||||
// Bug signalé : "aucune différence, la taille de la fenêtre de jeu est
|
||||
// la même alors qu'il n'y a plus de caméra" — ".playScreen.playScene"
|
||||
// (templates/play.html) est centré via `transform:translate(-50%,-50%)`,
|
||||
// ce qui en fait la référence de positionnement ("containing block") de
|
||||
// tout descendant `position:fixed` : le plein écran de la boîte à quiz
|
||||
// ne couvrait donc que CE conteneur (toujours à la taille scene_width/
|
||||
// scene_height, la "caméra"), jamais le VRAI viewport du navigateur.
|
||||
// Seul un déplacement hors de ce conteneur (comme static/js/play/
|
||||
// video-overlay.js le fait déjà pour son overlay) permet à
|
||||
// `position:fixed` de couvrir tout l'écran pour de vrai. `data-owner-
|
||||
// screen-id` (posé ici) permet à forgeQuizBoxWidgetOnCurrentScreen ci-
|
||||
// dessus de le retrouver malgré le déplacement.
|
||||
function forgeEscapeCameraForFullscreen(widget) {
|
||||
if (widget.parentNode === document.body) return;
|
||||
var screenEl = widget.closest('.playScreen');
|
||||
if (screenEl) widget.setAttribute('data-owner-screen-id', screenEl.getAttribute('data-screen-id'));
|
||||
widget.classList.add('quizBoxWidget--detachedForFullscreen');
|
||||
document.body.appendChild(widget);
|
||||
}
|
||||
|
||||
// Point de sortie UNIQUE de la boîte à quiz (fin d'une question réussie
|
||||
// vers la ligne suivante, OU fin de tout le dialogue) — y arrêter aussi
|
||||
// le minuteur/retirer le plein écran ici garantit qu'aucun des deux
|
||||
// n'est jamais laissé actif par erreur, quel que soit le chemin de
|
||||
// sortie (voir forgeShowCurrentConversationLine/forgeAdvanceConversation).
|
||||
function forgeHideQuizBoxOnCurrentScreen() {
|
||||
var widget = forgeQuizBoxWidgetOnCurrentScreen();
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
if (!widget) return;
|
||||
widget.classList.remove('is-active', 'is-fullscreen');
|
||||
var objectId = widget.getAttribute('data-object-id');
|
||||
forgeStopQuizTimer(objectId);
|
||||
}
|
||||
|
||||
// ---------- Minuteur (plein écran, demande explicite : "un timer pour
|
||||
// répondre aux questions") ----------
|
||||
// Optionnel — "aucun" (screens/rendering/quiz_box_config.py) est le
|
||||
// réglage par défaut, jamais imposé. Porté par data-timer-mode/
|
||||
// data-timer-seconds sur le widget lui-même (voir screens/rendering/
|
||||
// quiz_box_templates.py), lus ici plutôt que par un nouveau canal de
|
||||
// payload séparé — cohérent avec le reste de ce moteur (data-* posés au
|
||||
// rendu, jamais un aller-retour serveur pour du pur affichage).
|
||||
var forgeQuizTimerState = {};
|
||||
|
||||
function forgeStopQuizTimer(objectId) {
|
||||
var t = forgeQuizTimerState[objectId];
|
||||
if (t && t.intervalId) clearInterval(t.intervalId);
|
||||
delete forgeQuizTimerState[objectId];
|
||||
}
|
||||
|
||||
// mode "question" : redémarre à CHAQUE question (reset=true, appelé
|
||||
// depuis forgeShowQuizBox). mode "quiz" : démarré UNE SEULE FOIS pour
|
||||
// tout le quiz (reset=false une fois déjà en cours), continue de tourner
|
||||
// d'une question à l'autre sans se réinitialiser.
|
||||
function forgeStartQuizTimer(objectId, widget, seconds, onExpire) {
|
||||
forgeStopQuizTimer(objectId);
|
||||
var state = { remaining: seconds };
|
||||
forgeQuizTimerState[objectId] = state;
|
||||
var timerEl = widget.querySelector('[data-quiz-role="timer"]');
|
||||
function render() {
|
||||
if (!timerEl) return;
|
||||
var remaining = Math.max(0, state.remaining);
|
||||
var m = Math.floor(remaining / 60), s = remaining % 60;
|
||||
timerEl.textContent = '⏱ ' + m + ':' + (s < 10 ? '0' : '') + s;
|
||||
timerEl.classList.toggle('is-urgent', remaining <= 5);
|
||||
}
|
||||
render();
|
||||
state.intervalId = setInterval(function () {
|
||||
state.remaining -= 1;
|
||||
render();
|
||||
if (state.remaining <= 0) {
|
||||
forgeStopQuizTimer(objectId);
|
||||
onExpire();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ---------- Score ----------
|
||||
@@ -127,6 +204,17 @@ function forgeHideQuizBoxOnCurrentScreen() {
|
||||
function forgeUpdateAllScoreWidgets() {
|
||||
var values = document.querySelectorAll('.scoreWidget [data-score-role="value"]');
|
||||
for (var i = 0; i < values.length; i++) values[i].textContent = window.forgeQuizScore;
|
||||
forgeUpdateQuizBoxScoreDisplays();
|
||||
}
|
||||
|
||||
// Score affiché DANS la boîte à quiz elle-même (demande explicite :
|
||||
// "lorsque la boîte à quiz prend tout l'écran, le score s'affiche dans
|
||||
// la boîte à quiz") — masqué par CSS hors plein écran (voir
|
||||
// .quizBoxTopBar, static/style.css), donc sans effet visuel pour un quiz
|
||||
// non-plein-écran, mais tenu à jour dans tous les cas (coûte rien).
|
||||
function forgeUpdateQuizBoxScoreDisplays() {
|
||||
var values = document.querySelectorAll('.quizBoxWidget [data-quiz-role="score"]');
|
||||
for (var i = 0; i < values.length; i++) values[i].textContent = 'Score : ' + window.forgeQuizScore;
|
||||
}
|
||||
|
||||
// Alimente aussi gameData.scoring (voir static/js/play/offline/
|
||||
@@ -237,9 +325,19 @@ function forgeShowCurrentConversationLine(dialogueObjectId) {
|
||||
function forgeShowDialogueBox(dialogueId, objectName, lines, markCompleted, thenAction) {
|
||||
if (!dialogueId) return;
|
||||
lines = lines || [];
|
||||
// Bug corrigé : "j'ai créé un quiz à l'affichage de l'écran, rien ne
|
||||
// s'affiche" — un dialogue fait UNIQUEMENT de questions (quiz RH
|
||||
// autonome, aucune réplique) n'a besoin d'AUCUNE "💬 Boîte de dialogue"
|
||||
// posée sur l'écran, seulement d'une "❓ Boîte à quiz" — cette fonction
|
||||
// sortait pourtant en silence dès le départ s'il n'y avait aucune boîte
|
||||
// de dialogue, empêchant même la première question de s'afficher. Sans
|
||||
// widget réel pour porter la clé d'état, `dialogueId` sert de repli
|
||||
// (unique par action "Déclencher ce dialogue") — une ligne "réplique"
|
||||
// sans boîte de dialogue posée reste ignorée plus loin (voir
|
||||
// forgeShowCurrentConversationLine), même logique symétrique qu'une
|
||||
// question sans boîte à quiz posée (voir forgeShowQuizBox).
|
||||
var widget = forgeDialogueBoxWidgetOnCurrentScreen();
|
||||
if (!widget) return;
|
||||
var objectId = widget.getAttribute('data-object-id');
|
||||
var stateKey = widget ? widget.getAttribute('data-object-id') : ('_sansBoiteDialogue_' + dialogueId);
|
||||
forgeSyncDialogueStartedToScorm();
|
||||
if (!lines.length) {
|
||||
// Rien à jouer : applique quand même markCompleted PUIS thenAction
|
||||
@@ -248,11 +346,11 @@ function forgeShowDialogueBox(dialogueId, objectName, lines, markCompleted, then
|
||||
if (thenAction) forgeRunCollisionRuleAction(thenAction, null, { name: objectName });
|
||||
return;
|
||||
}
|
||||
forgeDialogueBoxState[objectId] = {
|
||||
forgeDialogueBoxState[stateKey] = {
|
||||
dialogueId: dialogueId, objectName: objectName, lines: lines, index: 0,
|
||||
markCompleted: !!markCompleted, thenAction: thenAction,
|
||||
};
|
||||
forgeShowCurrentConversationLine(objectId);
|
||||
forgeShowCurrentConversationLine(stateKey);
|
||||
}
|
||||
|
||||
// Marque un dialogue `completed` (RUNTIME uniquement, jamais persisté —
|
||||
@@ -308,9 +406,22 @@ function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questi
|
||||
var widget = forgeQuizBoxWidgetOnCurrentScreen();
|
||||
if (!widget) { forgeAdvanceConversation(dialogueObjectId); return; }
|
||||
var quizObjectId = widget.getAttribute('data-object-id');
|
||||
var previousState = forgeQuizBoxState[quizObjectId];
|
||||
forgeQuizBoxState[quizObjectId] = {
|
||||
dialogueObjectId: dialogueObjectId, correctIndex: line.correct_index, rewardAmount: line.reward_amount, resolved: false,
|
||||
dialogueId: dialogueId, objectName: objectName, questionIndex: questionIndex, questionText: line.text,
|
||||
// Minuteur "pour tout le quiz" (voir forgeStartQuizTimer plus bas) :
|
||||
// ne démarre qu'à la PREMIÈRE question, continue de tourner ensuite
|
||||
// sans se réinitialiser — ce drapeau survit d'une question à l'autre
|
||||
// via l'état précédent de CE MÊME quiz.
|
||||
quizTimerStarted: previousState ? previousState.quizTimerStarted : false,
|
||||
// Score DE CE QUIZ (jamais window.forgeQuizScore/forgeQuizCorrectAnswered,
|
||||
// cumulés sur TOUTE la partie, voir plus haut) — survit d'une
|
||||
// question à l'autre comme quizTimerStarted : sert à afficher "X
|
||||
// bonnes réponses sur Y" à la fin (voir forgeQuizBoxContinue/
|
||||
// forgeQuizPrepareResultScreen plus bas).
|
||||
correctCount: previousState ? previousState.correctCount : 0,
|
||||
answeredCount: previousState ? previousState.answeredCount : 0,
|
||||
};
|
||||
var header = widget.querySelector('[data-quiz-role="header"]');
|
||||
var questionEl = widget.querySelector('[data-quiz-role="question"]');
|
||||
@@ -321,6 +432,33 @@ function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questi
|
||||
choicesEl.innerHTML = line.choices.map(function (choice, i) {
|
||||
return '<button type="button" class="quizBoxChoiceBtn" onclick="forgeQuizBoxAnswer(' + quizObjectId + ', ' + i + ')">' + forgeEscapeHtml(choice) + '</button>';
|
||||
}).join('');
|
||||
// "Continuer →" TOUJOURS présent dès la question (demande explicite :
|
||||
// "le bouton continuer est toujours visible mais activé seulement
|
||||
// quand un choix est fait", comme la maquette manga validée) —
|
||||
// désactivé jusqu'à ce que forgeQuizBoxAnswer le réactive (jamais
|
||||
// recréé à ce moment-là, juste .disabled=false, voir plus bas).
|
||||
choicesEl.innerHTML += '<button type="button" class="quizBoxContinueBtn" onclick="forgeQuizBoxContinue(' + quizObjectId + ')" disabled>Continuer →</button>';
|
||||
}
|
||||
// "Plein écran" (demande explicite) — reposé à CHAQUE question,
|
||||
// sans effet si déjà présent (voir .quizBoxWidget.is-fullscreen,
|
||||
// static/style.css). Échappe d'abord au conteneur de scène transformé
|
||||
// (voir forgeEscapeCameraForFullscreen) — sinon position:fixed reste
|
||||
// borné à sa taille "caméra" au lieu du vrai écran du joueur.
|
||||
var isFullscreen = widget.getAttribute('data-fullscreen') === '1';
|
||||
if (isFullscreen) forgeEscapeCameraForFullscreen(widget);
|
||||
widget.classList.toggle('is-fullscreen', isFullscreen);
|
||||
forgeUpdateQuizBoxScoreDisplays();
|
||||
// Minuteur (optionnel — "aucun" reste le réglage par défaut, jamais
|
||||
// imposé, voir screens/rendering/quiz_box_config.py).
|
||||
var timerMode = widget.getAttribute('data-timer-mode');
|
||||
var timerSeconds = parseInt(widget.getAttribute('data-timer-seconds'), 10) || 0;
|
||||
if (timerMode === 'question') {
|
||||
forgeStartQuizTimer(quizObjectId, widget, timerSeconds, function () { forgeQuizBoxTimeout(quizObjectId, false); });
|
||||
} else if (timerMode === 'quiz' && !forgeQuizBoxState[quizObjectId].quizTimerStarted) {
|
||||
forgeQuizBoxState[quizObjectId].quizTimerStarted = true;
|
||||
forgeStartQuizTimer(quizObjectId, widget, timerSeconds, function () { forgeQuizBoxTimeout(quizObjectId, true); });
|
||||
} else if (timerMode !== 'quiz') {
|
||||
forgeStopQuizTimer(quizObjectId);
|
||||
}
|
||||
// Rejoue l'animation d'entrée (@keyframes quizPopIn) même si la boîte
|
||||
// était déjà "is-active" juste avant (2 questions à la suite) : sans
|
||||
@@ -329,6 +467,18 @@ function forgeShowQuizBox(dialogueObjectId, dialogueId, objectName, line, questi
|
||||
widget.classList.remove('is-active');
|
||||
void widget.offsetWidth;
|
||||
widget.classList.add('is-active');
|
||||
// Effet d'entrée propre au modèle (voir static/js/play/
|
||||
// quiz-box-templates.js) — optionnel, "defaut" n'en a aucun.
|
||||
var showFx = forgeQuizTemplateEffectsFor(widget).onShow;
|
||||
if (showFx) showFx(widget);
|
||||
}
|
||||
|
||||
// Registre d'effets par modèle (voir static/js/play/quiz-box-
|
||||
// templates.js, chargé AVANT ce fichier) — absent en environnement de
|
||||
// test qui ne charge pas ce script, d'où la garde `typeof`.
|
||||
function forgeQuizTemplateEffectsFor(widget) {
|
||||
var template = (widget && widget.getAttribute('data-quiz-template')) || 'defaut';
|
||||
return (typeof forgeQuizTemplateEffects !== 'undefined' && forgeQuizTemplateEffects[template]) || {};
|
||||
}
|
||||
|
||||
// Appelée par le clic sur un choix (les boutons sont injectés EN JEU par
|
||||
@@ -346,6 +496,12 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
state.resolved = true;
|
||||
var widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
var correct = choiceIndex === state.correctIndex;
|
||||
// Minuteur "par question" : cette question est résolue (bonne réponse,
|
||||
// mauvaise réponse, OU minuteur expiré via forgeQuizBoxTimeout, qui
|
||||
// appelle CETTE fonction avec choiceIndex=-1) — jamais le minuteur
|
||||
// "pour tout le quiz", qui doit continuer à tourner sans interruption
|
||||
// d'une question à l'autre.
|
||||
if (widget && widget.getAttribute('data-timer-mode') === 'question') forgeStopQuizTimer(quizObjectId);
|
||||
if (widget) {
|
||||
var buttons = widget.querySelectorAll('.quizBoxChoiceBtn');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
@@ -355,9 +511,15 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
}
|
||||
}
|
||||
window.forgeQuizTotalAnswered += 1;
|
||||
state.answeredCount += 1;
|
||||
var fx = forgeQuizTemplateEffectsFor(widget);
|
||||
if (correct) {
|
||||
window.forgeQuizCorrectAnswered += 1;
|
||||
state.correctCount += 1;
|
||||
forgeAddQuizScore(state.rewardAmount);
|
||||
if (fx.onCorrect) fx.onCorrect(widget);
|
||||
} else if (fx.onWrong) {
|
||||
fx.onWrong(widget);
|
||||
}
|
||||
if (typeof forgeXapiNotifyQuestionAnswered === "function") {
|
||||
forgeXapiNotifyQuestionAnswered(state.dialogueId, state.objectName, state.questionIndex, state.questionText, correct, correct ? state.rewardAmount : 0);
|
||||
@@ -371,26 +533,133 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
// après un délai fixe (700ms/1400ms), jamais assez pour un lecteur
|
||||
// lent ou un utilisateur de lecteur d'écran — l'avancée dépend
|
||||
// maintenant uniquement d'un clic sur "Continuer →", jamais d'un
|
||||
// minuteur. Le bouton est ajouté APRÈS les choix (jamais en remplaçant
|
||||
// choicesEl.innerHTML, qui effacerait le surlignage vert/rouge déjà posé).
|
||||
// minuteur. Le bouton existe déjà (posé désactivé par forgeShowQuizBox
|
||||
// ci-dessus, TOUJOURS visible — demande explicite, comme la maquette
|
||||
// manga) : on se contente ici de l'ACTIVER, jamais de le recréer (ça
|
||||
// effacerait le surlignage vert/rouge qu'on vient de poser sur les
|
||||
// choix juste au-dessus).
|
||||
if (widget) {
|
||||
var choicesEl = widget.querySelector('[data-quiz-role="choices"]');
|
||||
if (choicesEl) {
|
||||
choicesEl.innerHTML += '<button type="button" class="quizBoxContinueBtn" onclick="forgeQuizBoxContinue(' + quizObjectId + ')">Continuer →</button>';
|
||||
}
|
||||
var continueBtnToEnable = widget.querySelector('.quizBoxContinueBtn');
|
||||
if (continueBtnToEnable) continueBtnToEnable.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Prépare l'écran de résultat de fin de quiz (voir static/js/play/
|
||||
// quiz-box-templates.js::onQuizComplete, ex. forgeQuizClassiqueOnQuizComplete)
|
||||
// et renvoie une fonction à appeler APRÈS forgeAdvanceConversation (qui
|
||||
// masque sinon le widget, voir forgeHideQuizBoxOnCurrentScreen) — null
|
||||
// si ce n'est pas la fin du quiz (forceEnd=false : encore des lignes
|
||||
// après) ou si le modèle actif n'a aucun écran de résultat (seul
|
||||
// "classique" en a un pour l'instant, "manga"/les modèles "boîte"
|
||||
// gardent leur comportement d'avant, inchangé : la boîte se masque
|
||||
// simplement). `forceEnd` (true) : le minuteur du quiz ENTIER vient
|
||||
// d'expirer (forgeQuizBoxTimeout) — le quiz se termine MÊME s'il
|
||||
// restait des questions, jamais une simple question de position dans
|
||||
// les lignes.
|
||||
function forgeQuizPrepareResultScreen(quizObjectId, widget, state, forceEnd) {
|
||||
if (!widget) return null;
|
||||
var dialogueState = forgeDialogueBoxState[state.dialogueObjectId];
|
||||
if (!dialogueState) return null;
|
||||
var isEnding = forceEnd || (dialogueState.index + 1 >= dialogueState.lines.length);
|
||||
if (!isEnding) return null;
|
||||
var completeFx = forgeQuizTemplateEffectsFor(widget).onQuizComplete;
|
||||
if (!completeFx) return null;
|
||||
var correctCount = state.correctCount;
|
||||
var answeredCount = state.answeredCount;
|
||||
return function () {
|
||||
widget.classList.add('is-active');
|
||||
if (widget.getAttribute('data-fullscreen') === '1') widget.classList.add('is-fullscreen');
|
||||
completeFx(widget, { correctCount: correctCount, totalCount: answeredCount });
|
||||
};
|
||||
}
|
||||
|
||||
// Bouton de l'écran de résultat (voir forgeQuizPrepareResultScreen
|
||||
// ci-dessus) — demande explicite : QUITTER carrément (fermer l'onglet/
|
||||
// la page du navigateur), jamais recommencer le quiz. Le dialogue est
|
||||
// déjà épuisé (forgeAdvanceConversation l'a fait avant d'afficher ce
|
||||
// résultat) — masque quand même le widget D'ABORD (repli si la fermeture
|
||||
// est refusée, voir ci-dessous), avant de tenter de fermer. "is-showing-
|
||||
// result" est retirée des DEUX emplacements possibles ("manga" la pose
|
||||
// sur la racine du widget, "classique" sur .classiqueContent, voir
|
||||
// static/js/play/quiz-box-templates.js) — l'un des deux est toujours un
|
||||
// no-op inoffensif.
|
||||
//
|
||||
// window.close() : un navigateur ne ferme RÉELLEMENT que les onglets
|
||||
// ouverts PAR SCRIPT (window.open) ou la page auto-générée d'un
|
||||
// téléchargement — jamais un onglet atteint par une navigation normale
|
||||
// (tapée, cliquée, ou un lien) : restriction de sécurité du navigateur
|
||||
// lui-même, aucun moyen de la contourner en JS. window.close() échoue
|
||||
// alors silencieusement (aucune erreur, aucun retour) : le repli
|
||||
// ci-dessus (widget déjà masqué) est donc le résultat visible le plus
|
||||
// courant en pratique.
|
||||
function forgeQuizResultQuit(quizObjectId) {
|
||||
var widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
if (widget) {
|
||||
widget.classList.remove('is-active', 'is-fullscreen', 'is-showing-result');
|
||||
var content = widget.querySelector('.classiqueContent');
|
||||
if (content) content.classList.remove('is-showing-result');
|
||||
}
|
||||
if (typeof window !== 'undefined' && typeof window.close === 'function') window.close();
|
||||
}
|
||||
|
||||
// Appelé par le bouton "Continuer →" ajouté par forgeQuizBoxAnswer
|
||||
// ci-dessus — masque la boîte à quiz et avance la conversation, jamais
|
||||
// ci-dessus — masque la boîte à quiz et avance la conversation (SAUF à
|
||||
// la toute dernière question d'un modèle qui sait afficher un écran de
|
||||
// résultat, voir forgeQuizPrepareResultScreen ci-dessus), jamais
|
||||
// automatique (voir le commentaire ci-dessus).
|
||||
function forgeQuizBoxContinue(quizObjectId) {
|
||||
var state = forgeQuizBoxState[quizObjectId];
|
||||
if (!state) return;
|
||||
var widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
var showResultScreen = forgeQuizPrepareResultScreen(quizObjectId, widget, state, false);
|
||||
if (widget && !showResultScreen) widget.classList.remove('is-active');
|
||||
delete forgeQuizBoxState[quizObjectId];
|
||||
forgeAdvanceConversation(state.dialogueObjectId);
|
||||
if (showResultScreen) showResultScreen();
|
||||
}
|
||||
|
||||
// Minuteur expiré (demande explicite : "pouvoir paramétrer un timer pour
|
||||
// répondre aux questions", jamais imposé — "aucun" reste le réglage par
|
||||
// défaut) : mode "question" — cette question est comptée fausse (0
|
||||
// point), la bonne réponse s'affiche, puis "Continuer →" fonctionne
|
||||
// normalement. Mode "quiz" (wholeQuizExpired) — même révélation, mais
|
||||
// plus AUCUN temps pour la suite : le quiz se termine tout seul après un
|
||||
// court délai de lecture plutôt que d'attendre un clic. Réutilise
|
||||
// EXACTEMENT le chemin d'une mauvaise réponse (forgeQuizBoxAnswer) avec
|
||||
// choiceIndex=-1 (ne correspond à aucun bouton : révèle la bonne réponse
|
||||
// sans en marquer une "fausse"), jamais de code dupliqué.
|
||||
function forgeQuizBoxTimeout(quizObjectId, wholeQuizExpired) {
|
||||
var state = forgeQuizBoxState[quizObjectId];
|
||||
if (!state || state.resolved) return;
|
||||
forgeQuizBoxAnswer(quizObjectId, -1);
|
||||
if (!wholeQuizExpired) return;
|
||||
var widget = forgeQuizBoxWidgetById(quizObjectId);
|
||||
// Pas de "Continuer →" ici : rien à continuer, le temps du quiz ENTIER
|
||||
// est écoulé — désactivé/relibellé pour éviter toute course avec la
|
||||
// fin automatique programmée juste en dessous.
|
||||
if (widget) {
|
||||
var continueBtn = widget.querySelector('.quizBoxContinueBtn');
|
||||
if (continueBtn) { continueBtn.disabled = true; continueBtn.textContent = 'Temps écoulé…'; }
|
||||
}
|
||||
setTimeout(function () {
|
||||
var s = forgeQuizBoxState[quizObjectId];
|
||||
if (!s) return; // déjà fermé entre-temps
|
||||
// forceEnd=true : le quiz ENTIER se termine ici, qu'il reste ou non
|
||||
// des questions (voir forgeQuizPrepareResultScreen) — capturé AVANT
|
||||
// que forgeAdvanceConversation ne supprime forgeDialogueBoxState.
|
||||
var showResultScreen = forgeQuizPrepareResultScreen(quizObjectId, widget, s, true);
|
||||
if (widget && !showResultScreen) widget.classList.remove('is-active', 'is-fullscreen');
|
||||
delete forgeQuizBoxState[quizObjectId];
|
||||
// Force l'épuisement des lignes restantes — même sortie que la fin
|
||||
// naturelle d'un dialogue (markCompleted/thenAction inclus, voir
|
||||
// forgeAdvanceConversation), sans dupliquer cette logique.
|
||||
var dialogueState = forgeDialogueBoxState[s.dialogueObjectId];
|
||||
if (dialogueState) {
|
||||
dialogueState.index = dialogueState.lines.length;
|
||||
forgeAdvanceConversation(s.dialogueObjectId);
|
||||
}
|
||||
if (showResultScreen) showResultScreen();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// static/js/play/__tests__/ (node:test) — même garde que les autres
|
||||
@@ -400,9 +669,14 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
forgeShowDialogueBox, forgeDialogueBoxAdvance, forgeCompleteDialogue,
|
||||
forgeDialogueBoxWidgetOnCurrentScreen, forgeDialogueBoxWidgetById, forgeRenderDialogueBoxLine,
|
||||
forgeResetDialogueBoxFooter,
|
||||
forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxContinue, forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById,
|
||||
forgeUpdateAllScoreWidgets, forgeAddQuizScore, forgeAdvanceConversation, forgeShowCurrentConversationLine,
|
||||
forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxContinue, forgeQuizBoxTimeout,
|
||||
forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById, forgeHideQuizBoxOnCurrentScreen,
|
||||
forgeUpdateAllScoreWidgets, forgeUpdateQuizBoxScoreDisplays, forgeAddQuizScore,
|
||||
forgeAdvanceConversation, forgeShowCurrentConversationLine,
|
||||
forgeSyncQuizScoreToScorm, forgeSyncDialogueStartedToScorm, forgeSyncAllDialoguesCompletionToScorm,
|
||||
forgeComputeFinalScormStatus,
|
||||
forgeStartQuizTimer, forgeStopQuizTimer, forgeQuizTimerState,
|
||||
forgeEscapeCameraForFullscreen,
|
||||
forgeQuizPrepareResultScreen, forgeQuizResultQuit,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user