Relie le score/statut des quêtes aux indicateurs SCORM
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:
co-authored by
Claude Sonnet 5
parent
55c6bb330b
commit
413685e871
@@ -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; }
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user