SCORM/xAPI : - Ajoute l'export SCORM 2004 (3rd/4th edition) au choix, en plus du 1.2 par défaut : sépare completion_status/success_status (un échec reste "completed" au lieu de retomber à tort en "incomplete" comme force la 1.2), remonte aussi cmi.interactions.n.* par question répondue. - Fournit cmi.core.score.min/max (1.2 et 2004), calculé depuis les récompenses de quiz du jeu, pour que le LMS affiche un vrai pourcentage au lieu du score brut à tort étiqueté "%". - Libellés de verbes xAPI en français en plus de l'anglais. Accessibilité (RGAA/WCAG 2.1 AA) sur le player : - Navigation clavier des objets de scène "au clic"/"au survol" (tabindex, role, Entrée/Espace, focus/blur). - alt sur les images (nom auteur ou décoratif), aria-hidden sur les icônes seules, role="dialog"/aria-live sur les boîtes de dialogue/quiz. - Landmark <main> + titre de page, respect de prefers-reduced-motion. - Le quiz n'avance plus automatiquement après un délai fixe : bouton "Continuer →" explicite (RGAA 2.2.1). - Avertissement de contraste dans l'éditeur de style de dialogue. - Déclaration d'accessibilité téléchargeable depuis la modale d'export.
412 lines
20 KiB
JavaScript
412 lines
20 KiB
JavaScript
// ---------- Widgets "💬 Boîte de dialogue" / "❓ Boîte à quiz" / "🏆 Score" ----------
|
|
// Fait le lien entre l'éditeur de collision (action "quete", voir
|
|
// static/js/play/collision-rules-controller.js) et les widgets d'interface
|
|
// posés par l'auteur (voir "🖥️ Interface", templates/scene_edit.html,
|
|
// screens/rendering/dialogue_box_style.py) : à l'exécution d'une action
|
|
// "quete", parcourt le dialogue correspondant au STATUT ACTUEL de la
|
|
// quête (gameData.quests, voir full_game_payload.py) ligne par ligne —
|
|
// une ligne "dialogue" s'affiche dans la boîte de dialogue (bouton
|
|
// "Suivant" pour avancer) ; une ligne "question" ("❓ Question", voir
|
|
// static/js/quests/quest-editor.js) s'affiche dans la boîte à QUIZ (si
|
|
// posée sur la scène — sinon ignorée silencieusement, la conversation
|
|
// avance sans elle) : cliquer la BONNE réponse crédite le score du
|
|
// montant prévu et avance ; une mauvaise réponse ne fait RIEN, la
|
|
// question reste affichée (le joueur peut réessayer).
|
|
//
|
|
// Une fois le dialogue "nouvelle" épuisé : la boîte bascule en écran
|
|
// d'OFFRE ("Quête : <titre>" / objectif / boutons Accepter-Refuser) au
|
|
// lieu de se masquer — accepter fait passer la quête "en_cours" ; refuser
|
|
// ferme simplement la boîte SANS rien changer (la quête reste "nouvelle",
|
|
// la prochaine interaction rejoue exactement le même dialogue). Une fois
|
|
// le dialogue "en_cours" épuisé (donc, s'il contenait des questions,
|
|
// toutes répondues correctement), la quête passe "terminee" — le joueur
|
|
// peut alors quitter la scène normalement (aucun blocage, ces widgets ne
|
|
// sont jamais des obstacles). Tout ceci reste LOCAL à la partie en cours
|
|
// (gameData.quests n'est qu'un instantané par joueur, voir
|
|
// full_game_payload.py) : jamais persisté en base, qui garde le statut
|
|
// de DÉPART pour toute nouvelle partie.
|
|
//
|
|
// Le score du quiz (window.forgeQuizScore) est un compteur RUNTIME dédié
|
|
// à ce système de quête/quiz — distinct du score "classique" du moteur
|
|
// document (db/scoring/, action de flow "Modifier un score"), qui
|
|
// suppose un nœud de flow préconfiguré et un aller-retour serveur, hors
|
|
// de propos pour ce sous-système entièrement client (voir
|
|
// collision-rules-controller.js : "Pur JS client, sans le moindre appel
|
|
// serveur").
|
|
//
|
|
// Silencieux (aucun effet) si la quête est introuvable ou si l'auteur n'a
|
|
// posé aucun widget requis sur l'écran — même esprit que le reste du moteur.
|
|
|
|
var forgeDialogueBoxState = {};
|
|
var forgeQuizBoxState = {};
|
|
window.forgeQuizScore = window.forgeQuizScore || 0;
|
|
// Compte TOUTE réponse (bonne ou mauvaise) au fil de la partie — sert
|
|
// uniquement à calculer un taux de réussite global (voir
|
|
// forgeSyncAllQuestsCompletionToScorm), distinct de window.forgeQuizScore
|
|
// qui ne compte que les points gagnés.
|
|
window.forgeQuizTotalAnswered = window.forgeQuizTotalAnswered || 0;
|
|
window.forgeQuizCorrectAnswered = window.forgeQuizCorrectAnswered || 0;
|
|
|
|
function forgeQuestById(questId) {
|
|
return (gameData.quests || []).find(function (q) { return q.id === questId; });
|
|
}
|
|
|
|
function forgeEscapeHtml(str) {
|
|
return String(str == null ? '' : str)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// ---------- Boîte de dialogue ----------
|
|
|
|
function forgeDialogueBoxWidgetOnCurrentScreen() {
|
|
return document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"] .dialogueBoxWidget');
|
|
}
|
|
|
|
function forgeDialogueBoxWidgetById(objectId) {
|
|
return document.querySelector('.dialogueBoxWidget[data-object-id="' + objectId + '"]');
|
|
}
|
|
|
|
function forgeDialogueBoxFooterEl(widget) {
|
|
return widget.querySelector('[data-dialogue-role="footer"]');
|
|
}
|
|
|
|
function forgeRenderDialogueBoxLine(widget, line) {
|
|
var header = widget.querySelector('[data-dialogue-role="header"]');
|
|
var body = widget.querySelector('[data-dialogue-role="body"]');
|
|
if (header) header.textContent = line.speaker;
|
|
if (body) body.textContent = line.text;
|
|
}
|
|
|
|
// Remet le pied de la boîte dans son état "dialogue" (un seul bouton
|
|
// "Suivant →", voir render_scene_object.py) — nécessaire pour effacer
|
|
// les boutons Accepter/Refuser d'une offre PRÉCÉDENTE avant de rejouer
|
|
// un nouveau dialogue dans la même boîte.
|
|
function forgeResetDialogueBoxFooter(widget, objectId) {
|
|
var footer = forgeDialogueBoxFooterEl(widget);
|
|
if (footer) footer.innerHTML = '<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" onclick="forgeDialogueBoxAdvance(' + objectId + ')">Suivant →</button>';
|
|
}
|
|
|
|
// ---------- Boîte à quiz ----------
|
|
|
|
function forgeQuizBoxWidgetOnCurrentScreen() {
|
|
return document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"] .quizBoxWidget');
|
|
}
|
|
|
|
function forgeQuizBoxWidgetById(objectId) {
|
|
return document.querySelector('.quizBoxWidget[data-object-id="' + objectId + '"]');
|
|
}
|
|
|
|
function forgeHideQuizBoxOnCurrentScreen() {
|
|
var widget = forgeQuizBoxWidgetOnCurrentScreen();
|
|
if (widget) widget.classList.remove('is-active');
|
|
}
|
|
|
|
// ---------- Score ----------
|
|
|
|
function forgeUpdateAllScoreWidgets() {
|
|
var values = document.querySelectorAll('.scoreWidget [data-score-role="value"]');
|
|
for (var i = 0; i < values.length; i++) values[i].textContent = window.forgeQuizScore;
|
|
}
|
|
|
|
// Alimente aussi gameData.scoring (voir static/js/play/offline/
|
|
// apply-actions.js, même forme {score, status}) — bug signalé : "les
|
|
// indicateurs qu'on utilise pour les quêtes devraient aussi servir aux
|
|
// indicateurs SCORM, or ce n'est pas le cas". C'est CE champ que
|
|
// static/js/play/offline/scorm-api.js lit toutes les 2s pour pousser
|
|
// cmi.core.score.raw/cmi.core.lesson_status au LMS — sans jamais y
|
|
// toucher, un jeu construit uniquement avec des quêtes/quiz (jamais
|
|
// l'action de flow "Modifier un score") laissait Score/Completion
|
|
// perpétuellement "unknown" dans SCORM Cloud, même après y avoir joué
|
|
// jusqu'au bout. Vocabulaire de statut identique à db/scoring/
|
|
// (non_commence/en_cours/termine/...), voir FORGE_TO_SCORM_STATUS.
|
|
function forgeSyncQuizScoreToScorm() {
|
|
gameData.scoring = gameData.scoring || { score: 0, status: 'non_commence' };
|
|
gameData.scoring.score = window.forgeQuizScore;
|
|
if (gameData.scoring.status === 'non_commence') gameData.scoring.status = 'en_cours';
|
|
// xAPI bolt-on (voir static/js/play/offline/xapi-client.js) : ce chemin
|
|
// quête/quiz contourne static/js/play/offline/apply-actions.js (aucune
|
|
// action de flow "Modifier un score" ici), donc jamais notifié sans cet
|
|
// appel direct — même bug que le commentaire ci-dessus pour SCORM.
|
|
if (typeof forgeXapiNotifyScoreChanged === "function") forgeXapiNotifyScoreChanged(gameData.scoring.score);
|
|
}
|
|
|
|
// Une quête acceptée = la partie a commencé, même sans le moindre point
|
|
// encore gagné — sans cet appel, Score restait "0" mais Completion
|
|
// restait "unknown" tant qu'aucune question n'avait encore été résolue.
|
|
function forgeSyncQuestStartedToScorm() {
|
|
gameData.scoring = gameData.scoring || { score: 0, status: 'non_commence' };
|
|
if (gameData.scoring.status === 'non_commence') gameData.scoring.status = 'en_cours';
|
|
}
|
|
|
|
// Le jeu entier est considéré terminé quand TOUTES les quêtes connues
|
|
// sont "terminee" — règle simple et prévisible plutôt que de deviner
|
|
// laquelle des quêtes créées "compte" pour la fin du jeu.
|
|
// Réussi/Échoué (voir db/games/get_success_threshold.py,
|
|
// window.FORGE_SUCCESS_THRESHOLD_PERCENT injecté par publish/
|
|
// build_scorm_package.py) : demande utilisateur — "il manque
|
|
// l'indicateur succès/échec sur le SCORM". Calculé sur le taux de
|
|
// bonnes réponses au quiz (forgeQuizCorrectAnswered/forgeQuizTotalAnswered)
|
|
// SUR TOUTE LA PARTIE, pas quête par quête — cohérent avec "toutes les
|
|
// quêtes terminées" qui est déjà la seule condition de fin de partie.
|
|
// Aucune question posée du tout (jeu fait uniquement de dialogues) :
|
|
// reste "termine" (ni réussi ni échoué, rien à évaluer).
|
|
function forgeComputeFinalScormStatus() {
|
|
var total = window.forgeQuizTotalAnswered || 0;
|
|
if (total === 0) return 'termine';
|
|
var percent = (window.forgeQuizCorrectAnswered / total) * 100;
|
|
var threshold = typeof window.FORGE_SUCCESS_THRESHOLD_PERCENT === 'number' ? window.FORGE_SUCCESS_THRESHOLD_PERCENT : 70;
|
|
return percent >= threshold ? 'reussi' : 'echoue';
|
|
}
|
|
|
|
function forgeSyncAllQuestsCompletionToScorm() {
|
|
var quests = gameData.quests || [];
|
|
if (!quests.length || !quests.every(function (q) { return q.status === 'terminee'; })) return;
|
|
gameData.scoring = gameData.scoring || { score: 0, status: 'non_commence' };
|
|
var finalStatus = forgeComputeFinalScormStatus();
|
|
gameData.scoring.status = finalStatus;
|
|
if (typeof forgeXapiNotifyStatusChanged === "function") forgeXapiNotifyStatusChanged(finalStatus);
|
|
}
|
|
|
|
function forgeAddQuizScore(amount) {
|
|
window.forgeQuizScore += (amount || 0);
|
|
forgeUpdateAllScoreWidgets();
|
|
forgeSyncQuizScoreToScorm();
|
|
}
|
|
|
|
// ---------- Enchaînement dialogue/question ----------
|
|
|
|
// Affiche la ligne COURANTE de l'état de conversation `dialogueObjectId`
|
|
// (voir forgeDialogueBoxState) — une ligne "dialogue" dans la boîte de
|
|
// dialogue, une ligne "question" dans la boîte à quiz (voir
|
|
// forgeShowQuizBox ci-dessous, qui avance seule si aucune boîte à quiz
|
|
// n'est posée : impossible de bloquer la conversation indéfiniment).
|
|
function forgeShowCurrentConversationLine(dialogueObjectId) {
|
|
var state = forgeDialogueBoxState[dialogueObjectId];
|
|
if (!state) return;
|
|
var line = state.lines[state.index];
|
|
var widget = forgeDialogueBoxWidgetById(dialogueObjectId);
|
|
if (line.type === 'question') {
|
|
if (widget) widget.classList.remove('is-active');
|
|
forgeShowQuizBox(dialogueObjectId, forgeQuestById(state.questId), line, state.index);
|
|
return;
|
|
}
|
|
forgeHideQuizBoxOnCurrentScreen();
|
|
if (!widget) return;
|
|
forgeResetDialogueBoxFooter(widget, dialogueObjectId);
|
|
forgeRenderDialogueBoxLine(widget, line);
|
|
widget.classList.add('is-active');
|
|
}
|
|
|
|
// Appelée par l'action "quete" (collision-rules-controller.js) — trouve
|
|
// le widget posé sur l'écran ACTUELLEMENT affiché (un seul à la fois,
|
|
// comme le reste du moteur 2D).
|
|
function forgeShowDialogueBox(questId) {
|
|
var quest = forgeQuestById(questId);
|
|
if (!quest) return;
|
|
var lines = (quest.dialogues && quest.dialogues[quest.status]) || [];
|
|
var widget = forgeDialogueBoxWidgetOnCurrentScreen();
|
|
if (!widget) return;
|
|
var objectId = widget.getAttribute('data-object-id');
|
|
// Aucune ligne posée pour ce statut : une quête "nouvelle" propose
|
|
// quand même directement l'offre (accepter/refuser), les autres
|
|
// statuts restent silencieux (rien à montrer).
|
|
if (!lines.length) {
|
|
if (quest.status === 'nouvelle') forgeShowQuestOffer(widget, objectId, quest);
|
|
return;
|
|
}
|
|
forgeDialogueBoxState[objectId] = { questId: questId, lines: lines, index: 0 };
|
|
forgeShowCurrentConversationLine(objectId);
|
|
}
|
|
|
|
// Avance d'une ligne (fin de dialogue -> question, fin de question
|
|
// correcte -> ligne suivante) — appelée par le bouton "Suivant →" ET par
|
|
// une bonne réponse au quiz (voir forgeQuizBoxAnswer).
|
|
function forgeAdvanceConversation(dialogueObjectId) {
|
|
var state = forgeDialogueBoxState[dialogueObjectId];
|
|
if (!state) return;
|
|
state.index += 1;
|
|
if (state.index >= state.lines.length) {
|
|
forgeHideQuizBoxOnCurrentScreen();
|
|
var widget = forgeDialogueBoxWidgetById(dialogueObjectId);
|
|
var quest = forgeQuestById(state.questId);
|
|
if (quest && quest.status === 'nouvelle') {
|
|
if (widget) forgeShowQuestOffer(widget, dialogueObjectId, quest);
|
|
return;
|
|
}
|
|
// Tout le dialogue "en_cours" épuisé (donc, s'il contenait des
|
|
// questions, toutes répondues correctement) -> quête terminée. Voir
|
|
// demande utilisateur : "quand le joueur a répondu à toutes les
|
|
// questions la quête passe au statut terminé".
|
|
if (quest && quest.status === 'en_cours') {
|
|
quest.status = 'terminee';
|
|
if (typeof forgeXapiNotifyQuestCompleted === "function") forgeXapiNotifyQuestCompleted(quest);
|
|
forgeSyncAllQuestsCompletionToScorm();
|
|
}
|
|
if (widget) widget.classList.remove('is-active');
|
|
delete forgeDialogueBoxState[dialogueObjectId];
|
|
return;
|
|
}
|
|
forgeShowCurrentConversationLine(dialogueObjectId);
|
|
}
|
|
|
|
// Appelée par le bouton "Suivant →" rendu dans le widget lui-même (voir
|
|
// render_scene_object.py, onclick="forgeDialogueBoxAdvance(<id>)") — sans
|
|
// effet pendant une question en cours (le "Suivant" n'existe pas alors,
|
|
// voir forgeShowCurrentConversationLine, mais gardé strict par sécurité).
|
|
function forgeDialogueBoxAdvance(objectId) {
|
|
var state = forgeDialogueBoxState[objectId];
|
|
if (!state || (state.lines[state.index] && state.lines[state.index].type === 'question')) return;
|
|
forgeAdvanceConversation(objectId);
|
|
}
|
|
|
|
// Affiche une question ("❓ Question") dans la boîte à quiz posée sur
|
|
// l'écran — header = "Quête : <titre>", corps = la question + ses choix
|
|
// (voir demande utilisateur) — si l'auteur n'a posé aucune boîte à quiz,
|
|
// la question est ignorée (avance directement) plutôt que de bloquer la
|
|
// conversation pour toujours en attendant une réponse qu'aucun widget ne
|
|
// peut recueillir.
|
|
function forgeShowQuizBox(dialogueObjectId, quest, line, questionIndex) {
|
|
var widget = forgeQuizBoxWidgetOnCurrentScreen();
|
|
if (!widget) { forgeAdvanceConversation(dialogueObjectId); return; }
|
|
var quizObjectId = widget.getAttribute('data-object-id');
|
|
forgeQuizBoxState[quizObjectId] = {
|
|
dialogueObjectId: dialogueObjectId, correctIndex: line.correct_index, rewardAmount: line.reward_amount, resolved: false,
|
|
quest: quest, questionIndex: questionIndex, questionText: line.text,
|
|
};
|
|
var header = widget.querySelector('[data-quiz-role="header"]');
|
|
var questionEl = widget.querySelector('[data-quiz-role="question"]');
|
|
var choicesEl = widget.querySelector('[data-quiz-role="choices"]');
|
|
if (header) header.textContent = 'Quête : ' + (quest ? quest.title : '');
|
|
if (questionEl) questionEl.textContent = line.text;
|
|
if (choicesEl) {
|
|
choicesEl.innerHTML = line.choices.map(function (choice, i) {
|
|
return '<button type="button" class="quizBoxChoiceBtn" onclick="forgeQuizBoxAnswer(' + quizObjectId + ', ' + i + ')">' + forgeEscapeHtml(choice) + '</button>';
|
|
}).join('');
|
|
}
|
|
// 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
|
|
// ce retrait/reflow/réajout, une classe déjà posée ne redéclenche pas
|
|
// son animation CSS.
|
|
widget.classList.remove('is-active');
|
|
void widget.offsetWidth;
|
|
widget.classList.add('is-active');
|
|
}
|
|
|
|
// Appelée par le clic sur un choix (les boutons sont injectés EN JEU par
|
|
// forgeShowQuizBox ci-dessus, jamais rendus côté serveur — voir
|
|
// render_scene_object.py pour le seul aperçu statique de l'éditeur).
|
|
// TOUJOURS crédite le score si la réponse est bonne PUIS avance à la
|
|
// ligne suivante, QUE la réponse soit bonne ou mauvaise (demande
|
|
// explicite : une mauvaise réponse ne doit jamais bloquer la
|
|
// progression) — surligne la bonne réponse en vert (et le choix cliqué
|
|
// en rouge s'il était faux) le temps de la voir avant d'avancer. Un
|
|
// second clic pendant cet affichage est ignoré (`resolved`).
|
|
function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
|
var state = forgeQuizBoxState[quizObjectId];
|
|
if (!state || state.resolved) return;
|
|
state.resolved = true;
|
|
var widget = forgeQuizBoxWidgetById(quizObjectId);
|
|
var correct = choiceIndex === state.correctIndex;
|
|
if (widget) {
|
|
var buttons = widget.querySelectorAll('.quizBoxChoiceBtn');
|
|
for (var i = 0; i < buttons.length; i++) {
|
|
buttons[i].disabled = true;
|
|
if (i === state.correctIndex) buttons[i].classList.add('is-correct');
|
|
else if (i === choiceIndex) buttons[i].classList.add('is-wrong');
|
|
}
|
|
}
|
|
window.forgeQuizTotalAnswered += 1;
|
|
if (correct) {
|
|
window.forgeQuizCorrectAnswered += 1;
|
|
forgeAddQuizScore(state.rewardAmount);
|
|
}
|
|
if (typeof forgeXapiNotifyQuestionAnswered === "function" && state.quest) {
|
|
forgeXapiNotifyQuestionAnswered(state.quest, state.questionIndex, state.questionText, correct, correct ? state.rewardAmount : 0);
|
|
}
|
|
// cmi.interactions.n.* (SCORM 2004 uniquement, voir scorm2004-api.js) :
|
|
// no-op en SCORM 1.2/hors LMS (forgeScorm2004Api y reste null).
|
|
if (typeof forgeScorm2004NotifyQuestionAnswered === "function" && state.quest) {
|
|
forgeScorm2004NotifyQuestionAnswered(state.quest, state.questionIndex, state.questionText, correct, choiceIndex, state.correctIndex);
|
|
}
|
|
// RGAA/WCAG 2.2.1 (délai réglable) : avancait auparavant tout seul
|
|
// 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é).
|
|
if (widget) {
|
|
var choicesEl = widget.querySelector('[data-quiz-role="choices"]');
|
|
if (choicesEl) {
|
|
choicesEl.innerHTML += '<button type="button" class="quizBoxContinueBtn" onclick="forgeQuizBoxContinue(' + quizObjectId + ')">Continuer →</button>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Appelé par le bouton "Continuer →" ajouté par forgeQuizBoxAnswer
|
|
// ci-dessus — masque la boîte à quiz et avance la conversation, 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');
|
|
delete forgeQuizBoxState[quizObjectId];
|
|
forgeAdvanceConversation(state.dialogueObjectId);
|
|
}
|
|
|
|
// ---------- Offre de quête (Accepter/Refuser) ----------
|
|
|
|
// Écran d'offre : header = "Quête : <titre>", body = l'objectif, pied =
|
|
// Accepter/Refuser au lieu du "Suivant" habituel.
|
|
function forgeShowQuestOffer(widget, objectId, quest) {
|
|
var header = widget.querySelector('[data-dialogue-role="header"]');
|
|
var body = widget.querySelector('[data-dialogue-role="body"]');
|
|
var footer = forgeDialogueBoxFooterEl(widget);
|
|
if (header) header.textContent = 'Quête : ' + quest.title;
|
|
if (body) body.textContent = quest.objectif || '';
|
|
if (footer) {
|
|
footer.innerHTML =
|
|
'<button type="button" class="dialogueBoxNextBtn dialogueBoxAcceptBtn" onclick="forgeDialogueBoxAcceptQuest(' + quest.id + ', ' + objectId + ')">Accepter</button>' +
|
|
'<button type="button" class="dialogueBoxNextBtn dialogueBoxDeclineBtn" onclick="forgeDialogueBoxDeclineQuest(' + objectId + ')">Refuser</button>';
|
|
}
|
|
forgeDialogueBoxState[objectId] = { questId: quest.id, lines: [], index: 0 };
|
|
widget.classList.add('is-active');
|
|
}
|
|
|
|
// Fait passer la quête "en_cours" — UNIQUEMENT dans l'instantané en
|
|
// mémoire (gameData.quests), jamais persisté en base : voir la note en
|
|
// tête de fichier (le statut en base est celui de DÉPART pour toute
|
|
// nouvelle partie, pas un état de partie en cours).
|
|
function forgeDialogueBoxAcceptQuest(questId, objectId) {
|
|
var quest = forgeQuestById(questId);
|
|
if (quest) quest.status = 'en_cours';
|
|
forgeSyncQuestStartedToScorm();
|
|
if (quest && typeof forgeXapiNotifyQuestAccepted === "function") forgeXapiNotifyQuestAccepted(quest);
|
|
var widget = forgeDialogueBoxWidgetById(objectId);
|
|
if (widget) widget.classList.remove('is-active');
|
|
delete forgeDialogueBoxState[objectId];
|
|
}
|
|
|
|
// Referme la boîte SANS toucher au statut de la quête (reste "nouvelle")
|
|
// — la prochaine interaction rejoue donc le même dialogue depuis le
|
|
// début : impossible d'avancer la quête sans un jour l'accepter.
|
|
function forgeDialogueBoxDeclineQuest(objectId) {
|
|
var widget = forgeDialogueBoxWidgetById(objectId);
|
|
if (widget) widget.classList.remove('is-active');
|
|
delete forgeDialogueBoxState[objectId];
|
|
}
|
|
|
|
// static/js/play/__tests__/ (node:test) — même garde que les autres
|
|
// contrôleurs de ce dossier.
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance,
|
|
forgeDialogueBoxWidgetOnCurrentScreen, forgeDialogueBoxWidgetById, forgeRenderDialogueBoxLine,
|
|
forgeShowQuestOffer, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeResetDialogueBoxFooter,
|
|
forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxContinue, forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById,
|
|
forgeUpdateAllScoreWidgets, forgeAddQuizScore, forgeAdvanceConversation, forgeShowCurrentConversationLine,
|
|
forgeSyncQuizScoreToScorm, forgeSyncQuestStartedToScorm, forgeSyncAllQuestsCompletionToScorm,
|
|
forgeComputeFinalScormStatus,
|
|
};
|
|
}
|