Remplace le système de quêtes par des déclencheurs, ajoute l'action variable et le chaînage
Supprime le concept de "quête" au profit d'un onglet unique "Déclencheurs"
portant toute la logique (dialogue, condition, marquage terminé) directement
sur l'objet de scène. Ajoute une nouvelle action "Modifier une variable"
(réutilisant le vocabulaire du graphe de flow) utilisable après une
collision, une interaction ou une branche de condition, ainsi qu'un
chaînage d'actions ("then") permettant d'enchaîner plusieurs actions à la
suite et d'étendre un déclencheur déjà posé sans le recréer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c504ace167
commit
d664ed5637
@@ -1,55 +1,58 @@
|
||||
// ---------- Widgets "💬 Boîte de dialogue" / "❓ Boîte à quiz" / "🏆 Score" ----------
|
||||
// Fait le lien entre l'éditeur de collision (action "quete", voir
|
||||
// Fait le lien entre l'éditeur de déclencheurs (action "dialogue", 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).
|
||||
// "dialogue", parcourt les répliques DE CETTE ACTION PRÉCISE
|
||||
// (action.dialogue.lines, transmises telles quelles par
|
||||
// collision-rules-controller.js — chaque occurrence de "Déclencher ce
|
||||
// dialogue" porte son propre dialogue, voir screens/rendering/
|
||||
// collision_rules.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/triggers/trigger-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.
|
||||
// Plus aucune "quête" ni écran d'offre automatique (Accepter/Refuser) :
|
||||
// c'est le CRÉATEUR qui pilote l'avancement, via ses propres variables
|
||||
// globales et conditions (voir screens/rendering/collision_rules.py::
|
||||
// "condition") — le dialogue choisi s'affiche directement. Une action
|
||||
// "dialogue" peut cocher `mark_completed` : une fois ce dialogue épuisé
|
||||
// (ou immédiatement s'il est vide), son id est ajouté à
|
||||
// forgeCompletedDialogueIds — RUNTIME uniquement (jamais persisté en
|
||||
// base), qui sert à savoir quand calculer la synthèse SCORM finale (voir
|
||||
// forgeSyncAllDialoguesCompletionToScorm : "terminé" une fois TOUS les
|
||||
// dialogues `mark_completed` du jeu joués, voir
|
||||
// gameData.completable_dialogue_ids). Le seuil de réussite/échec SCORM
|
||||
// lui-même reste un réglage du créateur totalement indépendant
|
||||
// (db/games/get_success_threshold.py), inchangé ici.
|
||||
//
|
||||
// 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
|
||||
// à ce système de dialogue/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.
|
||||
// Silencieux (aucun effet) si le widget requis n'est pas posé 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
|
||||
// forgeComputeFinalScormStatus), 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; });
|
||||
}
|
||||
// Id de dialogue -> true une fois joué ET marqué `mark_completed` — voir
|
||||
// forgeSyncAllDialoguesCompletionToScorm.
|
||||
window.forgeCompletedDialogueIds = window.forgeCompletedDialogueIds || {};
|
||||
|
||||
function forgeEscapeHtml(str) {
|
||||
return String(str == null ? '' : str)
|
||||
@@ -78,9 +81,8 @@ function forgeRenderDialogueBoxLine(widget, line) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// "Suivant →", voir render_scene_object.py) — appelé avant CHAQUE ligne
|
||||
// affichée (voir forgeShowCurrentConversationLine).
|
||||
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>';
|
||||
@@ -110,11 +112,11 @@ function forgeUpdateAllScoreWidgets() {
|
||||
|
||||
// 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 qu'on utilise pour les dialogues 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
|
||||
// toucher, un jeu construit uniquement avec des dialogues/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/
|
||||
@@ -124,32 +126,36 @@ function forgeSyncQuizScoreToScorm() {
|
||||
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.
|
||||
// dialogue/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
|
||||
// Un dialogue affiché = 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() {
|
||||
// Appelé depuis forgeShowDialogueBox.
|
||||
function forgeSyncDialogueStartedToScorm() {
|
||||
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,
|
||||
// Le jeu entier est considéré terminé quand TOUS les dialogues du jeu
|
||||
// marqués `mark_completed` (voir gameData.completable_dialogue_ids,
|
||||
// screens/rendering/trigger_graph.py::list_completable_dialogue_ids) ont
|
||||
// été joués (voir forgeAdvanceConversation) — règle simple et
|
||||
// prévisible plutôt que de deviner lequel des dialogues créés "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).
|
||||
// SUR TOUTE LA PARTIE, pas dialogue par dialogue — cohérent avec "tous
|
||||
// les dialogues terminés" 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';
|
||||
@@ -158,9 +164,10 @@ function forgeComputeFinalScormStatus() {
|
||||
return percent >= threshold ? 'reussi' : 'echoue';
|
||||
}
|
||||
|
||||
function forgeSyncAllQuestsCompletionToScorm() {
|
||||
var quests = gameData.quests || [];
|
||||
if (!quests.length || !quests.every(function (q) { return q.status === 'terminee'; })) return;
|
||||
function forgeSyncAllDialoguesCompletionToScorm() {
|
||||
var completableIds = (gameData.completable_dialogue_ids || []);
|
||||
var allDone = completableIds.length > 0 && completableIds.every(function (id) { return !!window.forgeCompletedDialogueIds[id]; });
|
||||
if (!allDone) return;
|
||||
gameData.scoring = gameData.scoring || { score: 0, status: 'non_commence' };
|
||||
var finalStatus = forgeComputeFinalScormStatus();
|
||||
gameData.scoring.status = finalStatus;
|
||||
@@ -187,7 +194,7 @@ function forgeShowCurrentConversationLine(dialogueObjectId) {
|
||||
var widget = forgeDialogueBoxWidgetById(dialogueObjectId);
|
||||
if (line.type === 'question') {
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
forgeShowQuizBox(dialogueObjectId, forgeQuestById(state.questId), line, state.index);
|
||||
forgeShowQuizBox(dialogueObjectId, state.dialogueId, state.objectName, line, state.index);
|
||||
return;
|
||||
}
|
||||
forgeHideQuizBoxOnCurrentScreen();
|
||||
@@ -197,30 +204,54 @@ function forgeShowCurrentConversationLine(dialogueObjectId) {
|
||||
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]) || [];
|
||||
// Appelée par l'action "dialogue" (collision-rules-controller.js) —
|
||||
// trouve le widget posé sur l'écran ACTUELLEMENT affiché (un seul à la
|
||||
// fois, comme le reste du moteur 2D). `lines` : les répliques DE CETTE
|
||||
// ACTION PRÉCISE (action.dialogue.lines, voir le commentaire d'en-tête).
|
||||
// `objectName` : le nom de l'objet déclencheur (voir "ℹ️ Informations"),
|
||||
// affiché en en-tête du quiz et transmis à xAPI — plus de "quête" pour
|
||||
// le porter. `markCompleted` : voir forgeAdvanceConversation ci-dessous.
|
||||
// `thenAction` (chaînage, voir screens/rendering/collision_rules.py::
|
||||
// "then") : l'action suivante, exécutée seulement une fois ce dialogue
|
||||
// épuisé — jamais avant, contrairement à une action "variable" qui,
|
||||
// elle, chaîne immédiatement (voir collision-rules-controller.js).
|
||||
function forgeShowDialogueBox(dialogueId, objectName, lines, markCompleted, thenAction) {
|
||||
if (!dialogueId) return;
|
||||
lines = lines || [];
|
||||
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).
|
||||
forgeSyncDialogueStartedToScorm();
|
||||
if (!lines.length) {
|
||||
if (quest.status === 'nouvelle') forgeShowQuestOffer(widget, objectId, quest);
|
||||
// Rien à jouer : applique quand même markCompleted PUIS thenAction
|
||||
// immédiatement (rien à exhiber avant), sinon silencieux.
|
||||
if (markCompleted) forgeCompleteDialogue(dialogueId, objectName);
|
||||
if (thenAction) forgeRunCollisionRuleAction(thenAction, null, { name: objectName });
|
||||
return;
|
||||
}
|
||||
forgeDialogueBoxState[objectId] = { questId: questId, lines: lines, index: 0 };
|
||||
forgeDialogueBoxState[objectId] = {
|
||||
dialogueId: dialogueId, objectName: objectName, lines: lines, index: 0,
|
||||
markCompleted: !!markCompleted, thenAction: thenAction,
|
||||
};
|
||||
forgeShowCurrentConversationLine(objectId);
|
||||
}
|
||||
|
||||
// Marque un dialogue `completed` (RUNTIME uniquement, jamais persisté —
|
||||
// le créateur pilote lui-même quand, via la case "Marquer ce dialogue
|
||||
// comme terminé" de l'assistant) et propage vers le SCORM/xAPI.
|
||||
function forgeCompleteDialogue(dialogueId, objectName) {
|
||||
window.forgeCompletedDialogueIds[dialogueId] = true;
|
||||
if (typeof forgeXapiNotifyDialogueCompleted === "function") forgeXapiNotifyDialogueCompleted(dialogueId, objectName);
|
||||
forgeSyncAllDialoguesCompletionToScorm();
|
||||
}
|
||||
|
||||
// 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).
|
||||
// une bonne réponse au quiz (voir forgeQuizBoxAnswer). À l'épuisement des
|
||||
// lignes : markCompleted PUIS thenAction (voir screens/rendering/
|
||||
// collision_rules.py::"then", demande explicite : "pouvoir modifier un
|
||||
// déclencheur pour lui ajouter des actions") — MÊME point de sortie "ce
|
||||
// dialogue est fini", jamais avant.
|
||||
function forgeAdvanceConversation(dialogueObjectId) {
|
||||
var state = forgeDialogueBoxState[dialogueObjectId];
|
||||
if (!state) return;
|
||||
@@ -228,20 +259,8 @@ function forgeAdvanceConversation(dialogueObjectId) {
|
||||
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 (state.markCompleted) forgeCompleteDialogue(state.dialogueId, state.objectName);
|
||||
if (state.thenAction) forgeRunCollisionRuleAction(state.thenAction, null, { name: state.objectName });
|
||||
if (widget) widget.classList.remove('is-active');
|
||||
delete forgeDialogueBoxState[dialogueObjectId];
|
||||
return;
|
||||
@@ -260,23 +279,23 @@ function forgeDialogueBoxAdvance(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) {
|
||||
// l'écran — header = le nom de l'objet déclencheur (voir "ℹ️
|
||||
// Informations"), corps = la question + ses choix — 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, dialogueId, objectName, 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,
|
||||
dialogueId: dialogueId, objectName: objectName, 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 (header) header.textContent = objectName || '';
|
||||
if (questionEl) questionEl.textContent = line.text;
|
||||
if (choicesEl) {
|
||||
choicesEl.innerHTML = line.choices.map(function (choice, i) {
|
||||
@@ -320,13 +339,13 @@ function forgeQuizBoxAnswer(quizObjectId, choiceIndex) {
|
||||
window.forgeQuizCorrectAnswered += 1;
|
||||
forgeAddQuizScore(state.rewardAmount);
|
||||
}
|
||||
if (typeof forgeXapiNotifyQuestionAnswered === "function" && state.quest) {
|
||||
forgeXapiNotifyQuestionAnswered(state.quest, state.questionIndex, state.questionText, correct, correct ? state.rewardAmount : 0);
|
||||
if (typeof forgeXapiNotifyQuestionAnswered === "function") {
|
||||
forgeXapiNotifyQuestionAnswered(state.dialogueId, state.objectName, 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);
|
||||
if (typeof forgeScorm2004NotifyQuestionAnswered === "function") {
|
||||
forgeScorm2004NotifyQuestionAnswered(state.dialogueId, state.objectName, 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
|
||||
@@ -354,58 +373,16 @@ function forgeQuizBoxContinue(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,
|
||||
forgeShowDialogueBox, forgeDialogueBoxAdvance, forgeCompleteDialogue,
|
||||
forgeDialogueBoxWidgetOnCurrentScreen, forgeDialogueBoxWidgetById, forgeRenderDialogueBoxLine,
|
||||
forgeShowQuestOffer, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeResetDialogueBoxFooter,
|
||||
forgeResetDialogueBoxFooter,
|
||||
forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxContinue, forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById,
|
||||
forgeUpdateAllScoreWidgets, forgeAddQuizScore, forgeAdvanceConversation, forgeShowCurrentConversationLine,
|
||||
forgeSyncQuizScoreToScorm, forgeSyncQuestStartedToScorm, forgeSyncAllQuestsCompletionToScorm,
|
||||
forgeSyncQuizScoreToScorm, forgeSyncDialogueStartedToScorm, forgeSyncAllDialoguesCompletionToScorm,
|
||||
forgeComputeFinalScormStatus,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user