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
+21
-29
@@ -1,11 +1,12 @@
|
||||
// Bout en bout : une règle de collision "quete" doit réellement faire
|
||||
// Bout en bout : une règle de collision "dialogue" doit réellement faire
|
||||
// apparaître/avancer/disparaître la boîte de dialogue posée sur la scène
|
||||
// — pas seulement chaque brique testée isolément (voir
|
||||
// collision-rules-controller.test.js et dialogue-box-controller.test.js).
|
||||
// Reproduit le scénario signalé : un objet "Mr. Pinguin" avec une règle
|
||||
// "à la collision -> déclencher la quête « test »", une quête au statut
|
||||
// "nouvelle" avec 2 répliques, un widget "💬 Boîte de dialogue" posé sur
|
||||
// la même scène.
|
||||
// "à la collision -> déclencher ce dialogue (avec mark_completed)", un
|
||||
// widget "💬 Boîte de dialogue" posé sur la même scène — plus aucun écran
|
||||
// d'offre automatique, le dialogue s'affiche directement et se marque
|
||||
// terminé à la fin.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
@@ -22,7 +23,6 @@ const { forgeDialogueBoxAdvance } = require('../dialogue-box-controller.js'); //
|
||||
const {
|
||||
forgeCollisionRuleTick,
|
||||
} = require('../collision-rules-controller.js');
|
||||
const { forgeDialogueBoxAcceptQuest, forgeQuestById } = require('../dialogue-box-controller.js');
|
||||
|
||||
function fakeEl(style) {
|
||||
const classes = new Set();
|
||||
@@ -43,11 +43,17 @@ function fakeEl(style) {
|
||||
};
|
||||
}
|
||||
|
||||
test('collision "quete" -> dialogue puis offre de quête -> Accepter fait passer la quête en_cours', () => {
|
||||
test('collision "dialogue" -> boîte de dialogue -> fin de dialogue (mark_completed) marque le dialogue terminé', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur' };
|
||||
const mrPinguin = {
|
||||
id: 2, kind: 'personnage',
|
||||
collision_rules: [{ trigger: 'collision', action: { type: 'quete', quete_id: 99 } }],
|
||||
id: 2, kind: 'personnage', name: 'Mr. Pinguin',
|
||||
collision_rules: [{
|
||||
trigger: 'collision',
|
||||
action: {
|
||||
type: 'dialogue', mark_completed: true,
|
||||
dialogue: { id: 'd_intro', lines: [{ speaker: 'Mr. Pinguin', text: 'Bonjour !' }, { speaker: 'Charlie', text: 'Salut !' }] },
|
||||
},
|
||||
}],
|
||||
};
|
||||
const box = { id: 3, kind: 'dialogue_box' };
|
||||
|
||||
@@ -61,18 +67,10 @@ test('collision "quete" -> dialogue puis offre de quête -> Accepter fait passer
|
||||
boxWidget._footer = { innerHTML: '' };
|
||||
|
||||
global.window.currentScreenId = 1;
|
||||
global.window.forgeCompletedDialogueIds = {};
|
||||
global.gameData = {
|
||||
personnage_animations: {},
|
||||
quests: [{
|
||||
id: 99, title: 'Retrouver Charlie', status: 'nouvelle', objectif: 'Parler à Mr. Pinguin',
|
||||
dialogues: {
|
||||
nouvelle: [
|
||||
{ speaker: 'Mr. Pinguin', text: 'Bonjour !' },
|
||||
{ speaker: 'Charlie', text: 'Salut !' },
|
||||
],
|
||||
en_cours: [], terminee: [],
|
||||
},
|
||||
}],
|
||||
completable_dialogue_ids: ['d_intro'],
|
||||
};
|
||||
|
||||
const previousDoc = global.document;
|
||||
@@ -94,12 +92,12 @@ test('collision "quete" -> dialogue puis offre de quête -> Accepter fait passer
|
||||
try {
|
||||
const screenData = { elements: [player, mrPinguin, box] };
|
||||
|
||||
forgeCollisionRuleTick(screenData); // entrée en collision -> déclenche la quête
|
||||
forgeCollisionRuleTick(screenData); // entrée en collision -> déclenche le dialogue
|
||||
assert.equal(boxWidget._header.textContent, 'Mr. Pinguin');
|
||||
assert.equal(boxWidget._body.textContent, 'Bonjour !');
|
||||
assert.equal(boxWidget.classList.contains('is-active'), true);
|
||||
|
||||
forgeCollisionRuleTick(screenData); // toujours en collision -> ne redéclenche pas la quête
|
||||
forgeCollisionRuleTick(screenData); // toujours en collision -> ne redéclenche pas
|
||||
assert.equal(boxWidget._header.textContent, 'Mr. Pinguin'); // pas repartie de la 1ère réplique
|
||||
|
||||
forgeDialogueBoxAdvance('3'); // clic "Suivant" (bouton du widget)
|
||||
@@ -107,14 +105,8 @@ test('collision "quete" -> dialogue puis offre de quête -> Accepter fait passer
|
||||
assert.equal(boxWidget._body.textContent, 'Salut !');
|
||||
assert.equal(boxWidget.classList.contains('is-active'), true);
|
||||
|
||||
forgeDialogueBoxAdvance('3'); // dernière réplique consommée -> quête "nouvelle" -> écran d'offre, PAS masqué
|
||||
assert.equal(boxWidget.classList.contains('is-active'), true);
|
||||
assert.equal(boxWidget._header.textContent, 'Quête : Retrouver Charlie');
|
||||
assert.equal(boxWidget._body.textContent, 'Parler à Mr. Pinguin');
|
||||
assert.match(boxWidget._footer.innerHTML, /Accepter/);
|
||||
|
||||
forgeDialogueBoxAcceptQuest(99, '3'); // clic "Accepter"
|
||||
assert.equal(forgeQuestById(99).status, 'en_cours');
|
||||
assert.equal(boxWidget.classList.contains('is-active'), false); // la modale disparaît
|
||||
forgeDialogueBoxAdvance('3'); // dernière réplique consommée -> mark_completed -> dialogue terminé, boîte masquée
|
||||
assert.equal(boxWidget.classList.contains('is-active'), false);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_intro'], true);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// Moteur d'exécution des règles "🧩 Collision" (voir
|
||||
// Moteur d'exécution des déclencheurs (voir
|
||||
// static/js/play/collision-rules-controller.js) : déclenche une action une
|
||||
// seule fois par ENTRÉE en collision/périmètre, jamais en continu tant que
|
||||
// la condition reste vraie — et gère "interagir" à part (bulle + attente
|
||||
@@ -14,6 +14,7 @@ global.heldKeys = new Set();
|
||||
const actionsMod = require('../actions.js');
|
||||
Object.assign(global, actionsMod);
|
||||
require('../personnage-controller.js');
|
||||
global.forgeApplyVariableActionOffline = global.forgeApplyVariableActionOffline || require('../offline/apply-actions.js').forgeApplyVariableActionOffline;
|
||||
|
||||
const {
|
||||
forgeCollisionRuleTriggerActive, forgeRunCollisionRuleAction, forgeCollisionRuleTick,
|
||||
@@ -86,54 +87,117 @@ test('forgeCollisionRuleTriggerActive — trigger "collision" : vrai aussi pour
|
||||
assert.equal(forgeCollisionRuleTriggerActive({ trigger: 'collision' }, objRect, touchingPlayer), true);
|
||||
});
|
||||
|
||||
test('forgeCollisionRuleTriggerActive — trigger "perimetre" : distance entre centres <= perimetre_px', () => {
|
||||
const objRect = rect(0, 0, 10, 10); // centre (5,5)
|
||||
const near = rect(10, 0, 10, 10); // centre (15,5) -> distance 10
|
||||
assert.equal(forgeCollisionRuleTriggerActive({ trigger: 'perimetre', perimetre_px: 12 }, objRect, near), true);
|
||||
assert.equal(forgeCollisionRuleTriggerActive({ trigger: 'perimetre', perimetre_px: 5 }, objRect, near), false);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "evenement" appelle window.dispatchGameEvent avec evenement_id', () => {
|
||||
const calls = [];
|
||||
global.window.dispatchGameEvent = id => calls.push(id);
|
||||
forgeRunCollisionRuleAction({ type: 'evenement', evenement_id: 42 }, fakeEl(), { id: 1 });
|
||||
assert.deepEqual(calls, [42]);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "quete" délègue à forgeShowDialogueBox (voir dialogue-box-controller.js)', () => {
|
||||
test('forgeRunCollisionRuleAction — "dialogue" délègue à forgeShowDialogueBox (voir dialogue-box-controller.js) avec le nom de l\'objet, les répliques DE CETTE ACTION et mark_completed', () => {
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = id => calls.push(id);
|
||||
forgeRunCollisionRuleAction({ type: 'quete', quete_id: 42 }, fakeEl(), { id: 1 });
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const lines = [{ speaker: 'PNJ', text: 'Bonjour !' }];
|
||||
forgeRunCollisionRuleAction({ type: 'dialogue', dialogue: { id: 'd_1', lines: lines }, mark_completed: true }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [42]);
|
||||
assert.deepEqual(calls, [['d_1', 'Crokine', lines, true, undefined]]);
|
||||
});
|
||||
|
||||
test('tick — la collision entre le joueur et un ennemi déclenche "attaque" une seule fois tant que le contact persiste', () => {
|
||||
global.window.dispatchGameEvent = () => {};
|
||||
test('forgeRunCollisionRuleAction — "dialogue" sans dialogue (référence orpheline) : transmet une liste vide plutôt que de planter', () => {
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
forgeRunCollisionRuleAction({ type: 'dialogue' }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [[undefined, 'Crokine', [], undefined, undefined]]);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "condition" vraie exécute la branche si_vrai', () => {
|
||||
global.gameData = { variables: { inventaire_cle: { value: '1' } } };
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const vraiLines = [{ speaker: 'PNJ', text: 'Vrai !' }];
|
||||
const action = {
|
||||
type: 'condition', variable: 'inventaire_cle', operateur: 'superieur', valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: vraiLines } }, si_faux: null,
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [['d_vrai', 'Crokine', vraiLines, undefined, undefined]]);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "condition" fausse exécute la branche si_faux, jamais si_vrai', () => {
|
||||
global.gameData = { variables: { inventaire_cle: { value: '0' } } };
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const fauxLines = [{ speaker: 'PNJ', text: 'Faux !' }];
|
||||
const action = {
|
||||
type: 'condition', variable: 'inventaire_cle', operateur: 'superieur', valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: [] } },
|
||||
si_faux: { type: 'dialogue', dialogue: { id: 'd_faux', lines: fauxLines } },
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [['d_faux', 'Crokine', fauxLines, undefined, undefined]]);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "condition" avec une branche "Ne rien faire" (null) ne fait rien', () => {
|
||||
global.gameData = { variables: { inventaire_cle: { value: '0' } } };
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const action = {
|
||||
type: 'condition', variable: 'inventaire_cle', operateur: 'superieur', valeur: '0',
|
||||
si_vrai: { type: 'dialogue', dialogue: { id: 'd_vrai', lines: [] } }, si_faux: null,
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "variable" modifie gameData.variables via forgeApplyVariableActionOffline', () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '2' } } };
|
||||
forgeRunCollisionRuleAction({ type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '3' }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
assert.equal(gameData.variables.score.value, '5');
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "variable" chaînée (then) exécute IMMÉDIATEMENT la suivante (instantané, pas besoin d\'attendre)', () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' }, vies: { type: 'nombre_entier', value: '3' } } };
|
||||
const action = {
|
||||
type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
|
||||
then: { type: 'variable', target_variable: 'vies', data_operation: 'decrementer', data_value: '1' },
|
||||
};
|
||||
forgeRunCollisionRuleAction(action, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
assert.equal(gameData.variables.vies.value, '2');
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "dialogue" transmet son "then" à forgeShowDialogueBox (exécuté APRÈS le dialogue, pas ici)', () => {
|
||||
global.gameData = { variables: {} };
|
||||
const calls = [];
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
global.forgeShowDialogueBox = (...args) => calls.push(args);
|
||||
const thenAction = { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' };
|
||||
forgeRunCollisionRuleAction({ type: 'dialogue', dialogue: { id: 'd_1', lines: [] }, then: thenAction }, fakeEl(), { id: 1, name: 'Crokine' });
|
||||
global.forgeShowDialogueBox = previousShow;
|
||||
assert.deepEqual(calls, [['d_1', 'Crokine', [], undefined, thenAction]]);
|
||||
});
|
||||
|
||||
test('tick — la collision entre le joueur et un objet ne redéclenche pas "dialogue" tant que le contact persiste', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' } };
|
||||
const enemy = { id: 2, kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'attaque' } }] };
|
||||
const npc = { id: 2, kind: 'personnage', name: 'Crokine', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue', dialogue: { id: 'd_7', lines: [] } } }] };
|
||||
const playerEl = fakeEl();
|
||||
const enemyEl = fakeEl();
|
||||
enemyEl.style = { left: '5px', top: '5px', width: '20px', height: '20px' }; // chevauche playerEl (0,0,20,20)
|
||||
const npcEl = fakeEl();
|
||||
npcEl.style = { left: '5px', top: '5px', width: '20px', height: '20px' }; // chevauche playerEl (0,0,20,20)
|
||||
|
||||
const previousDoc = global.document;
|
||||
global.document = fakeDocument({ 1: playerEl, 2: enemyEl });
|
||||
global.document = fakeDocument({ 1: playerEl, 2: npcEl });
|
||||
global.gameData = { personnage_animations: {} };
|
||||
const previousShow = global.forgeShowDialogueBox;
|
||||
const calls = [];
|
||||
global.forgeShowDialogueBox = id => calls.push(id);
|
||||
try {
|
||||
const screenData = { elements: [player, enemy] };
|
||||
let attackCalls = 0;
|
||||
const originalRun = global.runSpriteAnimation;
|
||||
global.runSpriteAnimation = () => { attackCalls++; };
|
||||
const screenData = { elements: [player, npc] };
|
||||
forgeCollisionRuleTick(screenData); // entrée en collision -> déclenche
|
||||
forgeCollisionRuleTick(screenData); // toujours en collision -> ne redéclenche pas
|
||||
global.runSpriteAnimation = originalRun;
|
||||
// "attaque" ne joue une animation que si le personnage a une pose
|
||||
// "attaque" (voir forgePersonnageAnimExists) — absente ici, donc 0
|
||||
// appel réel à runSpriteAnimation, mais le test vérifie surtout
|
||||
// l'absence de double déclenchement plutôt que l'appel lui-même.
|
||||
assert.equal(attackCalls, 0);
|
||||
} finally { global.document = previousDoc; }
|
||||
assert.deepEqual(calls, ['d_7']);
|
||||
} finally { global.document = previousDoc; global.forgeShowDialogueBox = previousShow; }
|
||||
});
|
||||
|
||||
test('tick — la bulle "Appuie sur X" s\'ancre sur la BOÎTE DE COLLISION, pas le cadre brut du sprite', () => {
|
||||
@@ -148,7 +212,7 @@ test('tick — la bulle "Appuie sur X" s\'ancre sur la BOÎTE DE COLLISION, pas
|
||||
// Sprite de 100×200 px, mais silhouette réelle 20×20 centrée en bas
|
||||
// (offset_y positif = décalée vers le bas, voir collision_settings.py).
|
||||
collision: { enabled: true, shape: 'rectangle', width: 20, height: 20, offset_x: 0, offset_y: 80 },
|
||||
collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'quete', quete_id: 1 } } }],
|
||||
collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'dialogue' } } }],
|
||||
};
|
||||
const parent = fakeParentEl();
|
||||
const playerEl = fakeEl(parent);
|
||||
@@ -170,7 +234,7 @@ test('tick — la bulle "Appuie sur X" s\'ancre sur la BOÎTE DE COLLISION, pas
|
||||
|
||||
test('tick — "interagir" affiche la bulle "Appuie sur [touche]" pendant la collision, puis la cache en sortant', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' } };
|
||||
const pnj = { id: 3, kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'quete', quete_id: 'q-2' } } }] };
|
||||
const pnj = { id: 3, kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'dialogue' } } }] };
|
||||
const parentNear = fakeParentEl();
|
||||
const parentFar = fakeParentEl();
|
||||
const playerElNear = fakeEl(parentNear);
|
||||
@@ -198,5 +262,5 @@ test('tick — "interagir" affiche la bulle "Appuie sur [touche]" pendant la col
|
||||
|
||||
test('forgeStartCollisionRuleControllers — inactif sans joueur ou sans règle (aucun intervalle démarré)', () => {
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', personnage_role: 'joueur' }] }));
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'attaque' } }] }] }));
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue' } }] }] }));
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Widget "💬 Boîte de dialogue" — moteur d'exécution (voir
|
||||
// static/js/play/dialogue-box-controller.js) : affiche la réplique en
|
||||
// cours du dialogue d'une quête (gameData.quests) correspondant à son
|
||||
// STATUT ACTUEL, avance au clic sur "Suivant", se masque à la fin.
|
||||
// static/js/play/dialogue-box-controller.js) : affiche les répliques
|
||||
// D'UNE ACTION PRÉCISE (transmises en paramètre par collision-rules-
|
||||
// controller.js, chaque occurrence de "Déclencher ce dialogue" porte les
|
||||
// siennes, voir screens/rendering/collision_rules.py), avance au clic sur
|
||||
// "Suivant", se masque à la fin — sans écran d'offre automatique ni
|
||||
// notion de quête : le nom affiché (en-tête du quiz, xAPI) est celui de
|
||||
// l'objet déclencheur.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
@@ -9,10 +13,9 @@ global.window = global.window || {};
|
||||
global.gameData = {};
|
||||
|
||||
const {
|
||||
forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance,
|
||||
forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest,
|
||||
forgeShowDialogueBox, forgeDialogueBoxAdvance, forgeCompleteDialogue,
|
||||
forgeQuizBoxAnswer, forgeQuizBoxContinue, forgeUpdateAllScoreWidgets,
|
||||
forgeAddQuizScore, forgeSyncAllQuestsCompletionToScorm,
|
||||
forgeAddQuizScore, forgeSyncAllDialoguesCompletionToScorm,
|
||||
forgeComputeFinalScormStatus,
|
||||
} = require('../dialogue-box-controller.js');
|
||||
|
||||
@@ -43,50 +46,37 @@ test.beforeEach(() => {
|
||||
// fuiraient sur le calcul de statut final du test suivant.
|
||||
global.window.forgeQuizTotalAnswered = 0;
|
||||
global.window.forgeQuizCorrectAnswered = 0;
|
||||
global.gameData = {
|
||||
quests: [
|
||||
{ id: 5, title: 'La quête du sorcier', status: 'nouvelle', objectif: 'Parler au sorcier', dialogues: { nouvelle: [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }], en_cours: [], terminee: [] } },
|
||||
{ id: 6, status: 'en_cours', dialogues: { nouvelle: [{ speaker: 'PNJ', text: 'x' }], en_cours: [], terminee: [] } },
|
||||
{ id: 8, status: 'terminee', dialogues: { nouvelle: [], en_cours: [], terminee: [{ speaker: 'PNJ', text: 'Merci !' }] } },
|
||||
],
|
||||
};
|
||||
global.window.forgeCompletedDialogueIds = {};
|
||||
global.gameData = { completable_dialogue_ids: [] };
|
||||
});
|
||||
|
||||
test('forgeQuestById retrouve une quête par id', () => {
|
||||
assert.equal(forgeQuestById(5).id, 5);
|
||||
assert.equal(forgeQuestById(999), undefined);
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox — quête introuvable : aucun effet (pas de plantage)', () => {
|
||||
test('forgeShowDialogueBox — sans dialogueId : aucun effet (pas de plantage)', () => {
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => { throw new Error('ne devrait jamais chercher de widget'); } };
|
||||
try {
|
||||
assert.doesNotThrow(() => forgeShowDialogueBox(999));
|
||||
assert.doesNotThrow(() => forgeShowDialogueBox(null, 'PNJ', []));
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox — quête sans réplique pour son statut "en_cours" (pas "nouvelle") : aucun effet', () => {
|
||||
test('forgeShowDialogueBox — dialogue vide, sans mark_completed : aucun effet', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
forgeShowDialogueBox(6); // "en_cours" -> dialogues.en_cours est vide, et statut != "nouvelle"
|
||||
forgeShowDialogueBox('d_6', 'PNJ', []);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_6'], undefined);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox — quête "nouvelle" SANS réplique : propose directement l\'offre (accepter/refuser)', () => {
|
||||
global.gameData.quests.push({ id: 7, title: 'Le trésor perdu', status: 'nouvelle', objectif: 'Trouver le trésor', dialogues: {} });
|
||||
test('forgeShowDialogueBox — dialogue vide AVEC mark_completed : marque le dialogue terminé immédiatement, sans rien afficher', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
forgeShowDialogueBox(7);
|
||||
assert.equal(widget._roles.header.textContent, 'Quête : Le trésor perdu');
|
||||
assert.equal(widget._roles.body.textContent, 'Trouver le trésor');
|
||||
assert.match(widget._roles.footer.innerHTML, /Accepter/);
|
||||
assert.match(widget._roles.footer.innerHTML, /Refuser/);
|
||||
assert.equal(widget._classes.has('is-active'), true);
|
||||
forgeShowDialogueBox('d_6', 'PNJ', [], true);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_6'], true);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
@@ -94,16 +84,16 @@ test('forgeShowDialogueBox — aucun widget posé sur l\'écran : aucun effet',
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => null };
|
||||
try {
|
||||
assert.doesNotThrow(() => forgeShowDialogueBox(5));
|
||||
assert.doesNotThrow(() => forgeShowDialogueBox('d_5', 'Le Sorcier', [{ speaker: 'Le Sorcier', text: 'Bonjour !' }]));
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — cycle toutes les répliques puis se masque (quête pas "nouvelle")', () => {
|
||||
test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — cycle toutes les répliques puis se masque', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: sel => (sel.indexOf('data-object-id="42"') !== -1 ? widget : (sel.indexOf('dialogueBoxWidget') !== -1 ? widget : null)) };
|
||||
try {
|
||||
forgeShowDialogueBox(8); // statut "terminee" -> pas d'offre à la fin, se masque normalement
|
||||
forgeShowDialogueBox('d_8', 'PNJ', [{ speaker: 'PNJ', text: 'Merci !' }]);
|
||||
assert.equal(widget._roles.header.textContent, 'PNJ');
|
||||
assert.equal(widget._roles.body.textContent, 'Merci !');
|
||||
assert.equal(widget._classes.has('is-active'), true);
|
||||
@@ -113,48 +103,68 @@ test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — cycle toutes les ré
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('quête "nouvelle" : le dialogue épuisé bascule sur l\'offre (Accepter/Refuser) au lieu de se masquer', () => {
|
||||
test('fin de dialogue SANS mark_completed : le dialogue n\'est jamais marqué terminé', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: sel => (sel.indexOf('data-object-id="42"') !== -1 ? widget : (sel.indexOf('dialogueBoxWidget') !== -1 ? widget : null)) };
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }]);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée -> écran d'offre, PAS masqué
|
||||
assert.equal(widget._classes.has('is-active'), true);
|
||||
assert.equal(widget._roles.header.textContent, 'Quête : La quête du sorcier');
|
||||
assert.equal(widget._roles.body.textContent, 'Parler au sorcier');
|
||||
assert.match(widget._roles.footer.innerHTML, /Accepter/);
|
||||
assert.match(widget._roles.footer.innerHTML, /Refuser/);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeDialogueBoxAcceptQuest — fait passer la quête "en_cours" (en mémoire seulement) et masque la boîte', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
widget._classes.add('is-active');
|
||||
forgeDialogueBoxAcceptQuest(5, 42);
|
||||
assert.equal(forgeQuestById(5).status, 'en_cours');
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_5'], undefined);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeDialogueBoxDeclineQuest — masque la boîte SANS changer le statut (reste "nouvelle")', () => {
|
||||
test('fin de dialogue AVEC mark_completed : marque le dialogue terminé et propage vers le SCORM', () => {
|
||||
const widget = fakeWidget(42);
|
||||
global.gameData.completable_dialogue_ids = ['d_5']; // seul dialogue connu
|
||||
global.gameData.scoring = { score: 0, status: 'en_cours' };
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
widget._classes.add('is-active');
|
||||
forgeDialogueBoxDeclineQuest(42);
|
||||
assert.equal(forgeQuestById(5).status, 'nouvelle'); // inchangé -> le dialogue rejouera depuis le début
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }], true);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée -> dialogue terminé
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_5'], true);
|
||||
assert.equal(widget._classes.has('is-active'), false);
|
||||
assert.equal(gameData.scoring.status, 'termine'); // seul dialogue connu, désormais complété
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox — dialogue vide AVEC "then" : exécute immédiatement l\'action chaînée (rien à exhiber avant)', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
const previousRun = global.forgeRunCollisionRuleAction;
|
||||
const calls = [];
|
||||
global.forgeRunCollisionRuleAction = (...args) => calls.push(args);
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
const thenAction = { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' };
|
||||
forgeShowDialogueBox('d_6', 'Crokine', [], false, thenAction);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], thenAction);
|
||||
assert.equal(calls[0][2].name, 'Crokine');
|
||||
} finally { global.document = previousDoc; global.forgeRunCollisionRuleAction = previousRun; }
|
||||
});
|
||||
|
||||
test('fin de dialogue AVEC "then" : exécute l\'action chaînée APRÈS la dernière réplique, jamais avant', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
const previousRun = global.forgeRunCollisionRuleAction;
|
||||
const calls = [];
|
||||
global.forgeRunCollisionRuleAction = (...args) => calls.push(args);
|
||||
global.document = { querySelector: () => widget };
|
||||
try {
|
||||
const thenAction = { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' };
|
||||
forgeShowDialogueBox('d_9', 'Crokine', [{ speaker: 'PNJ', text: 'Salut !' }], false, thenAction);
|
||||
assert.equal(calls.length, 0); // pas encore, le dialogue n'est pas terminé
|
||||
forgeDialogueBoxAdvance(42); // dernière réplique consommée -> "then" exécuté maintenant
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], thenAction);
|
||||
} finally { global.document = previousDoc; global.forgeRunCollisionRuleAction = previousRun; }
|
||||
});
|
||||
|
||||
test('forgeDialogueBoxAdvance — objet inconnu (jamais affiché) : aucun effet', () => {
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => null };
|
||||
@@ -206,36 +216,30 @@ function docWithDialogueAndQuiz(dialogueWidget, quizWidget) {
|
||||
};
|
||||
}
|
||||
|
||||
const QUIZ_LINES = [
|
||||
{ type: 'dialogue', speaker: 'PNJ', text: 'Une question pour toi.' },
|
||||
{ type: 'question', text: '2+2 ?', choices: ['3', '4'], correct_index: 1, reward_type: 'score', reward_amount: 5 },
|
||||
{ type: 'dialogue', speaker: 'PNJ', text: 'Bien joué !' },
|
||||
];
|
||||
|
||||
test.beforeEach(() => {
|
||||
global.gameData.quests.push({
|
||||
id: 10, title: 'Le quiz du sorcier', status: 'en_cours',
|
||||
dialogues: {
|
||||
nouvelle: [],
|
||||
en_cours: [
|
||||
{ type: 'dialogue', speaker: 'PNJ', text: 'Une question pour toi.' },
|
||||
{ type: 'question', text: '2+2 ?', choices: ['3', '4'], correct_index: 1, reward_type: 'score', reward_amount: 5 },
|
||||
{ type: 'dialogue', speaker: 'PNJ', text: 'Bien joué !' },
|
||||
],
|
||||
terminee: [],
|
||||
},
|
||||
});
|
||||
window.forgeQuizScore = 0;
|
||||
});
|
||||
|
||||
test('une ligne "question" est affichée dans la boîte à QUIZ (header = quête, corps = question+choix), pas la boîte de dialogue', () => {
|
||||
test('une ligne "question" est affichée dans la boîte à QUIZ (header = nom de l\'objet déclencheur, corps = question+choix), pas la boîte de dialogue', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
dialogueWidget.getAttribute = attr => (attr === 'data-object-id' ? '42' : null);
|
||||
const quizWidget = fakeQuizWidget(99, 2);
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
forgeShowDialogueBox('d_10', 'Le sorcier', QUIZ_LINES);
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), true); // 1ère ligne : dialogue normal
|
||||
|
||||
forgeDialogueBoxAdvance(42); // -> ligne "question"
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), false); // masquée pendant la question
|
||||
assert.equal(quizWidget._classes.has('is-active'), true);
|
||||
assert.equal(quizWidget._roles.header.textContent, 'Quête : Le quiz du sorcier');
|
||||
assert.equal(quizWidget._roles.header.textContent, 'Le sorcier');
|
||||
assert.equal(quizWidget._roles.question.textContent, '2+2 ?');
|
||||
assert.match(quizWidget._roles.choices.innerHTML, /forgeQuizBoxAnswer\(99, 0\)/);
|
||||
assert.match(quizWidget._roles.choices.innerHTML, /forgeQuizBoxAnswer\(99, 1\)/);
|
||||
@@ -248,7 +252,7 @@ test('mauvaise réponse au quiz : aucun point, MAIS avance quand même sur "Cont
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
forgeShowDialogueBox('d_10', 'Le sorcier', QUIZ_LINES);
|
||||
forgeDialogueBoxAdvance(42); // -> question
|
||||
forgeQuizBoxAnswer(99, 0); // mauvaise réponse (correct_index=1)
|
||||
assert.equal(window.forgeQuizScore, 0); // aucun point
|
||||
@@ -271,7 +275,7 @@ test('un second clic pendant la révélation (avant "Continuer →") est ignoré
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
forgeShowDialogueBox('d_10', 'Le sorcier', QUIZ_LINES);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
forgeQuizBoxAnswer(99, 0); // 1er clic (faux)
|
||||
forgeQuizBoxAnswer(99, 1); // 2nd clic pendant la révélation -> ignoré
|
||||
@@ -287,7 +291,7 @@ test('bonne réponse au quiz : crédite le score, la surligne en vert, "Continue
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, quizWidget);
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
forgeShowDialogueBox('d_10', 'Le sorcier', QUIZ_LINES, true);
|
||||
forgeDialogueBoxAdvance(42); // -> question
|
||||
forgeQuizBoxAnswer(99, 1); // bonne réponse
|
||||
assert.equal(window.forgeQuizScore, 5);
|
||||
@@ -298,8 +302,8 @@ test('bonne réponse au quiz : crédite le score, la surligne en vert, "Continue
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), true);
|
||||
assert.equal(dialogueWidget._roles.body.textContent, 'Bien joué !');
|
||||
|
||||
forgeDialogueBoxAdvance(42); // dernière ligne -> dialogue "en_cours" épuisé -> quête "terminee"
|
||||
assert.equal(forgeQuestById(10).status, 'terminee');
|
||||
forgeDialogueBoxAdvance(42); // dernière ligne -> dialogue épuisé, mark_completed -> terminé
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_10'], true);
|
||||
assert.equal(dialogueWidget._classes.has('is-active'), false);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
@@ -313,7 +317,7 @@ test('aucune boîte à quiz posée sur la scène : la question est ignorée, la
|
||||
const previousDoc = global.document;
|
||||
global.document = docWithDialogueAndQuiz(dialogueWidget, null); // pas de widget à quiz
|
||||
try {
|
||||
forgeShowDialogueBox(10);
|
||||
forgeShowDialogueBox('d_10', 'Le sorcier', QUIZ_LINES);
|
||||
forgeDialogueBoxAdvance(42); // -> question, mais pas de boîte à quiz -> avance directement
|
||||
assert.equal(dialogueWidget._roles.body.textContent, 'Bien joué !'); // déjà à la ligne suivante
|
||||
assert.equal(window.forgeQuizScore, 0); // pas de récompense sans réponse
|
||||
@@ -333,9 +337,9 @@ test('forgeUpdateAllScoreWidgets — met à jour tous les widgets "🏆 Score" a
|
||||
});
|
||||
|
||||
// ---------- 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
|
||||
// Bug signalé : "les indicateurs qu'on utilise pour les dialogues
|
||||
// devraient aussi servir aux indicateurs SCORM, or ce n'est pas le cas" —
|
||||
// un jeu construit uniquement avec des dialogues/quiz ne touchait jamais
|
||||
// gameData.scoring, laissant Score/Completion perpétuellement "unknown"
|
||||
// dans le LMS même après avoir fini le jeu.
|
||||
|
||||
@@ -366,42 +370,40 @@ test('forgeAddQuizScore ne rétrograde jamais un statut déjà plus avancé que
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('accepter une quête fait passer gameData.scoring.status à "en_cours" (la partie a commencé)', () => {
|
||||
test('afficher un dialogue 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);
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ speaker: 'Le Sorcier', text: 'Bonjour !' }]);
|
||||
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' }];
|
||||
test('forgeSyncAllDialoguesCompletionToScorm — passe "termine" seulement quand TOUS les dialogues completable sont completed', () => {
|
||||
global.gameData.completable_dialogue_ids = ['d_1', 'd_2'];
|
||||
window.forgeCompletedDialogueIds = { d_1: true };
|
||||
global.gameData.scoring = { score: 0, status: 'en_cours' };
|
||||
forgeSyncAllQuestsCompletionToScorm();
|
||||
forgeSyncAllDialoguesCompletionToScorm();
|
||||
assert.equal(gameData.scoring.status, 'en_cours'); // pas encore toutes finies
|
||||
|
||||
gameData.quests[1].status = 'terminee';
|
||||
forgeSyncAllQuestsCompletionToScorm();
|
||||
window.forgeCompletedDialogueIds.d_2 = true;
|
||||
forgeSyncAllDialoguesCompletionToScorm();
|
||||
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: [] } },
|
||||
];
|
||||
test('forgeCompleteDialogue marque le dialogue completed et notifie xAPI', () => {
|
||||
const calls = [];
|
||||
global.forgeXapiNotifyDialogueCompleted = (id, name) => calls.push({ id, name });
|
||||
global.gameData.completable_dialogue_ids = ['d_20'];
|
||||
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');
|
||||
forgeCompleteDialogue('d_20', 'Crokine');
|
||||
assert.equal(window.forgeCompletedDialogueIds['d_20'], true);
|
||||
assert.deepEqual(calls, [{ id: 'd_20', name: 'Crokine' }]);
|
||||
assert.equal(gameData.scoring.status, 'termine');
|
||||
} finally { global.document = previousDoc; }
|
||||
} finally { delete global.forgeXapiNotifyDialogueCompleted; }
|
||||
});
|
||||
|
||||
// ---------- Seuil de réussite (voir db/games/get_success_threshold.py) ----------
|
||||
@@ -433,31 +435,18 @@ test('forgeComputeFinalScormStatus — seuil par défaut (70%) si FORGE_SUCCESS_
|
||||
assert.equal(forgeComputeFinalScormStatus(), 'reussi');
|
||||
});
|
||||
|
||||
test('forgeSyncAllQuestsCompletionToScorm propage reussi/echoue (pas juste "termine") une fois toutes les quêtes finies', () => {
|
||||
global.gameData.quests = [{ id: 1, status: 'terminee' }];
|
||||
test('forgeSyncAllDialoguesCompletionToScorm propage reussi/echoue (pas juste "termine") une fois tous les dialogues finis', () => {
|
||||
global.gameData.completable_dialogue_ids = ['d_1'];
|
||||
window.forgeCompletedDialogueIds = { d_1: true };
|
||||
global.gameData.scoring = { score: 0, status: 'en_cours' };
|
||||
global.window.forgeQuizTotalAnswered = 4;
|
||||
global.window.forgeQuizCorrectAnswered = 1; // 25% < seuil par défaut (70%)
|
||||
delete global.window.FORGE_SUCCESS_THRESHOLD_PERCENT;
|
||||
forgeSyncAllQuestsCompletionToScorm();
|
||||
forgeSyncAllDialoguesCompletionToScorm();
|
||||
assert.equal(gameData.scoring.status, 'echoue');
|
||||
});
|
||||
|
||||
// ---------- Granularité xAPI (quête acceptée/terminée, question répondue) ----------
|
||||
|
||||
test('accepter une quête notifie xAPI (verbe "attempted" ciblant CETTE quête)', () => {
|
||||
const widget = fakeWidget(42);
|
||||
global.gameData.scoring = undefined;
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => widget };
|
||||
const calls = [];
|
||||
global.forgeXapiNotifyQuestAccepted = quest => calls.push(quest);
|
||||
try {
|
||||
forgeDialogueBoxAcceptQuest(5, 42);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].id, 5);
|
||||
} finally { global.document = previousDoc; delete global.forgeXapiNotifyQuestAccepted; }
|
||||
});
|
||||
// ---------- Granularité xAPI (dialogue terminé, question répondue) ----------
|
||||
|
||||
test('répondre à une question notifie xAPI avec la bonne/mauvaise réponse et le score gagné', () => {
|
||||
const dialogueWidget = fakeWidget(42);
|
||||
@@ -467,21 +456,19 @@ test('répondre à une question notifie xAPI avec la bonne/mauvaise réponse et
|
||||
querySelector: sel => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
global.gameData.quests = [
|
||||
{ id: 5, title: 'La quête du sorcier', status: 'en_cours', dialogues: { nouvelle: [], en_cours: [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }], terminee: [] } },
|
||||
];
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const calls = [];
|
||||
global.forgeXapiNotifyQuestionAnswered = (quest, index, text, correct, score) => calls.push({ quest, index, text, correct, score });
|
||||
global.forgeXapiNotifyQuestionAnswered = (dialogueId, objectName, index, text, correct, score) => calls.push({ dialogueId, objectName, index, text, correct, score });
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }]);
|
||||
forgeQuizBoxAnswer(99, 0); // bonne réponse (correct_index=0)
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].quest.id, 5);
|
||||
assert.equal(calls[0].dialogueId, 'd_5');
|
||||
assert.equal(calls[0].objectName, 'Le Sorcier');
|
||||
assert.equal(calls[0].correct, true);
|
||||
assert.equal(calls[0].score, 5);
|
||||
forgeQuizBoxContinue(99); // clic explicite (RGAA 2.2.1, plus d'avance automatique)
|
||||
@@ -496,22 +483,19 @@ test('répondre à une question notifie AUSSI le SCORM 2004 (cmi.interactions.n.
|
||||
querySelector: sel => (sel.indexOf('choices') !== -1 ? { innerHTML: '' } : { textContent: '' }),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
global.gameData.quests = [
|
||||
{ id: 5, title: 'La quête du sorcier', status: 'en_cours', dialogues: { nouvelle: [], en_cours: [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }], terminee: [] } },
|
||||
];
|
||||
const previousDoc = global.document;
|
||||
global.document = {
|
||||
querySelector: sel => (sel.indexOf('quizBoxWidget') !== -1 ? quizWidget : dialogueWidget),
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
const calls = [];
|
||||
global.forgeScorm2004NotifyQuestionAnswered = (quest, index, text, correct, choiceIndex, correctIndex) =>
|
||||
calls.push({ quest, index, text, correct, choiceIndex, correctIndex });
|
||||
global.forgeScorm2004NotifyQuestionAnswered = (dialogueId, objectName, index, text, correct, choiceIndex, correctIndex) =>
|
||||
calls.push({ dialogueId, objectName, index, text, correct, choiceIndex, correctIndex });
|
||||
try {
|
||||
forgeShowDialogueBox(5);
|
||||
forgeShowDialogueBox('d_5', 'Le Sorcier', [{ type: 'question', text: 'Qui es-tu ?', choices: ['A', 'B'], correct_index: 0, reward_amount: 5 }]);
|
||||
forgeQuizBoxAnswer(99, 1); // mauvaise réponse (correct_index=0, répond 1)
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].quest.id, 5);
|
||||
assert.equal(calls[0].dialogueId, 'd_5');
|
||||
assert.equal(calls[0].correct, false);
|
||||
assert.equal(calls[0].choiceIndex, 1);
|
||||
assert.equal(calls[0].correctIndex, 0);
|
||||
|
||||
Reference in New Issue
Block a user