From 413685e871f7d8c95281096b7e15bac4c6e93df0 Mon Sep 17 00:00:00 2001 From: william Date: Thu, 3 Sep 2026 17:56:37 +0200 Subject: [PATCH] =?UTF-8?q?Relie=20le=20score/statut=20des=20qu=C3=AAtes?= =?UTF-8?q?=20aux=20indicateurs=20SCORM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les quêtes/quiz mettaient à jour l'état local (statut, score) sans jamais toucher gameData.scoring, que scorm-api.js interroge déjà pour reporter Completion/Success/Score au LMS. Ajoute forgeSyncQuizScoreToScorm, forgeSyncQuestStartedToScorm et forgeSyncAllQuestsCompletionToScorm, appelées lors d'un bon score, de l'acceptation d'une quête et de la complétion de toutes les quêtes. Co-Authored-By: Claude Sonnet 5 --- .../__tests__/dialogue-box-controller.test.js | 73 +++++++++++++++++++ static/js/play/dialogue-box-controller.js | 43 ++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/static/js/play/__tests__/dialogue-box-controller.test.js b/static/js/play/__tests__/dialogue-box-controller.test.js index 15496380..5995a68c 100644 --- a/static/js/play/__tests__/dialogue-box-controller.test.js +++ b/static/js/play/__tests__/dialogue-box-controller.test.js @@ -12,6 +12,7 @@ const { forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeQuizBoxAnswer, forgeUpdateAllScoreWidgets, + forgeAddQuizScore, forgeSyncAllQuestsCompletionToScorm, } = require('../dialogue-box-controller.js'); function fakeWidget(objectId) { @@ -321,3 +322,75 @@ test('forgeUpdateAllScoreWidgets — met à jour tous les widgets "🏆 Score" a assert.equal(values[1].textContent, 42); } finally { global.document = previousDoc; } }); + +// ---------- Pont vers SCORM (gameData.scoring, voir static/js/play/offline/scorm-api.js) ---------- +// Bug signalé : "les indicateurs qu'on utilise pour les quêtes devraient +// aussi servir aux indicateurs SCORM, or ce n'est pas le cas" — un jeu +// construit uniquement avec des quêtes/quiz ne touchait jamais +// gameData.scoring, laissant Score/Completion perpétuellement "unknown" +// dans le LMS même après avoir fini le jeu. + +test('forgeAddQuizScore alimente aussi gameData.scoring (score + statut "en_cours")', () => { + global.gameData.scoring = undefined; + window.forgeQuizScore = 0; + const previousDoc = global.document; + global.document = { querySelectorAll: () => [] }; + try { + forgeAddQuizScore(5); + assert.equal(gameData.scoring.score, 5); + assert.equal(gameData.scoring.status, 'en_cours'); + + forgeAddQuizScore(3); + assert.equal(gameData.scoring.score, 8); // cumulé, jamais écrasé + assert.equal(gameData.scoring.status, 'en_cours'); // déjà "en_cours", pas rétrogradé + } finally { global.document = previousDoc; } +}); + +test('forgeAddQuizScore ne rétrograde jamais un statut déjà plus avancé que "non_commence"', () => { + global.gameData.scoring = { score: 0, status: 'termine' }; + window.forgeQuizScore = 0; + const previousDoc = global.document; + global.document = { querySelectorAll: () => [] }; + try { + forgeAddQuizScore(2); + assert.equal(gameData.scoring.status, 'termine'); + } finally { global.document = previousDoc; } +}); + +test('accepter une quête fait passer gameData.scoring.status à "en_cours" (la partie a commencé)', () => { + const widget = fakeWidget(42); + global.gameData.scoring = undefined; + const previousDoc = global.document; + global.document = { querySelector: () => widget }; + try { + forgeDialogueBoxAcceptQuest(5, 42); + assert.equal(gameData.scoring.status, 'en_cours'); + } finally { global.document = previousDoc; } +}); + +test('forgeSyncAllQuestsCompletionToScorm — passe "termine" seulement quand TOUTES les quêtes sont "terminee"', () => { + global.gameData.quests = [{ id: 1, status: 'terminee' }, { id: 2, status: 'en_cours' }]; + global.gameData.scoring = { score: 0, status: 'en_cours' }; + forgeSyncAllQuestsCompletionToScorm(); + assert.equal(gameData.scoring.status, 'en_cours'); // pas encore toutes finies + + gameData.quests[1].status = 'terminee'; + forgeSyncAllQuestsCompletionToScorm(); + assert.equal(gameData.scoring.status, 'termine'); +}); + +test('la dernière ligne d\'un dialogue "en_cours" propage la complétion à gameData.scoring si c\'est la seule quête', () => { + const widget = fakeWidget(42); + global.gameData.quests = [ + { id: 20, status: 'en_cours', dialogues: { nouvelle: [], en_cours: [{ speaker: 'PNJ', text: 'Bravo !' }], terminee: [] } }, + ]; + global.gameData.scoring = { score: 0, status: 'en_cours' }; + const previousDoc = global.document; + global.document = { querySelector: sel => (sel.indexOf('data-object-id="42"') !== -1 ? widget : (sel.indexOf('dialogueBoxWidget') !== -1 ? widget : null)) }; + try { + forgeShowDialogueBox(20); + forgeDialogueBoxAdvance(42); // dernière (et unique) ligne consommée -> quête "terminee" + assert.equal(forgeQuestById(20).status, 'terminee'); + assert.equal(gameData.scoring.status, 'termine'); + } finally { global.document = previousDoc; } +}); diff --git a/static/js/play/dialogue-box-controller.js b/static/js/play/dialogue-box-controller.js index 781ff9ec..0748c711 100644 --- a/static/js/play/dialogue-box-controller.js +++ b/static/js/play/dialogue-box-controller.js @@ -102,9 +102,45 @@ function forgeUpdateAllScoreWidgets() { 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'; +} + +// 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. +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' }; + gameData.scoring.status = 'termine'; +} + function forgeAddQuizScore(amount) { window.forgeQuizScore += (amount || 0); forgeUpdateAllScoreWidgets(); + forgeSyncQuizScoreToScorm(); } // ---------- Enchaînement dialogue/question ---------- @@ -171,7 +207,10 @@ function forgeAdvanceConversation(dialogueObjectId) { // 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 (quest && quest.status === 'en_cours') { + quest.status = 'terminee'; + forgeSyncAllQuestsCompletionToScorm(); + } if (widget) widget.classList.remove('is-active'); delete forgeDialogueBoxState[dialogueObjectId]; return; @@ -287,6 +326,7 @@ function forgeShowQuestOffer(widget, objectId, quest) { function forgeDialogueBoxAcceptQuest(questId, objectId) { var quest = forgeQuestById(questId); if (quest) quest.status = 'en_cours'; + forgeSyncQuestStartedToScorm(); var widget = forgeDialogueBoxWidgetById(objectId); if (widget) widget.classList.remove('is-active'); delete forgeDialogueBoxState[objectId]; @@ -310,5 +350,6 @@ if (typeof module !== 'undefined' && module.exports) { forgeShowQuestOffer, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeResetDialogueBoxFooter, forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById, forgeUpdateAllScoreWidgets, forgeAddQuizScore, forgeAdvanceConversation, forgeShowCurrentConversationLine, + forgeSyncQuizScoreToScorm, forgeSyncQuestStartedToScorm, forgeSyncAllQuestsCompletionToScorm, }; }