Relie le score/statut des quêtes aux indicateurs SCORM
Build and deploy / test-python (push) Failing after 1m15s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-03 17:56:37 +02:00
co-authored by Claude Sonnet 5
parent 55c6bb330b
commit 413685e871
2 changed files with 115 additions and 1 deletions
@@ -12,6 +12,7 @@ const {
forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance, forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance,
forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest,
forgeQuizBoxAnswer, forgeUpdateAllScoreWidgets, forgeQuizBoxAnswer, forgeUpdateAllScoreWidgets,
forgeAddQuizScore, forgeSyncAllQuestsCompletionToScorm,
} = require('../dialogue-box-controller.js'); } = require('../dialogue-box-controller.js');
function fakeWidget(objectId) { function fakeWidget(objectId) {
@@ -321,3 +322,75 @@ test('forgeUpdateAllScoreWidgets — met à jour tous les widgets "🏆 Score" a
assert.equal(values[1].textContent, 42); assert.equal(values[1].textContent, 42);
} finally { global.document = previousDoc; } } 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; }
});
+42 -1
View File
@@ -102,9 +102,45 @@ function forgeUpdateAllScoreWidgets() {
for (var i = 0; i < values.length; i++) values[i].textContent = window.forgeQuizScore; 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) { function forgeAddQuizScore(amount) {
window.forgeQuizScore += (amount || 0); window.forgeQuizScore += (amount || 0);
forgeUpdateAllScoreWidgets(); forgeUpdateAllScoreWidgets();
forgeSyncQuizScoreToScorm();
} }
// ---------- Enchaînement dialogue/question ---------- // ---------- Enchaînement dialogue/question ----------
@@ -171,7 +207,10 @@ function forgeAdvanceConversation(dialogueObjectId) {
// questions, toutes répondues correctement) -> quête terminée. Voir // questions, toutes répondues correctement) -> quête terminée. Voir
// demande utilisateur : "quand le joueur a répondu à toutes les // demande utilisateur : "quand le joueur a répondu à toutes les
// questions la quête passe au statut terminé". // 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'); if (widget) widget.classList.remove('is-active');
delete forgeDialogueBoxState[dialogueObjectId]; delete forgeDialogueBoxState[dialogueObjectId];
return; return;
@@ -287,6 +326,7 @@ function forgeShowQuestOffer(widget, objectId, quest) {
function forgeDialogueBoxAcceptQuest(questId, objectId) { function forgeDialogueBoxAcceptQuest(questId, objectId) {
var quest = forgeQuestById(questId); var quest = forgeQuestById(questId);
if (quest) quest.status = 'en_cours'; if (quest) quest.status = 'en_cours';
forgeSyncQuestStartedToScorm();
var widget = forgeDialogueBoxWidgetById(objectId); var widget = forgeDialogueBoxWidgetById(objectId);
if (widget) widget.classList.remove('is-active'); if (widget) widget.classList.remove('is-active');
delete forgeDialogueBoxState[objectId]; delete forgeDialogueBoxState[objectId];
@@ -310,5 +350,6 @@ if (typeof module !== 'undefined' && module.exports) {
forgeShowQuestOffer, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeResetDialogueBoxFooter, forgeShowQuestOffer, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeResetDialogueBoxFooter,
forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById, forgeShowQuizBox, forgeQuizBoxAnswer, forgeQuizBoxWidgetOnCurrentScreen, forgeQuizBoxWidgetById,
forgeUpdateAllScoreWidgets, forgeAddQuizScore, forgeAdvanceConversation, forgeShowCurrentConversationLine, forgeUpdateAllScoreWidgets, forgeAddQuizScore, forgeAdvanceConversation, forgeShowCurrentConversationLine,
forgeSyncQuizScoreToScorm, forgeSyncQuestStartedToScorm, forgeSyncAllQuestsCompletionToScorm,
}; };
} }