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>
200 lines
8.9 KiB
JavaScript
200 lines
8.9 KiB
JavaScript
// Export Web/SCORM — bolt-on xAPI (voir publish/build_scorm_package.py,
|
||
// db/games/get_xapi_settings.py) : EN PLUS du SCORM classique
|
||
// (scorm-api.js, jamais touché par ce fichier), envoie des statements
|
||
// xAPI directement au LRS configuré par le créateur du jeu. Chargé
|
||
// TOUJOURS dans le paquet exporté (même sans réglage xAPI — voir
|
||
// templates/play.html), mais ne fait RIEN tant que
|
||
// window.FORGE_XAPI_CONFIG est `null` (réglage jamais renseigné).
|
||
//
|
||
// Volontairement PAS le protocole cmi5 (pas de négociation de jeton au
|
||
// lancement, pas de découverte d'endpoint via l'URL du LMS) : l'URL du
|
||
// LRS et les identifiants sont saisis une fois pour toutes par le
|
||
// créateur (voir routes/publish/xapi_settings.py) et embarqués tels
|
||
// quels dans le paquet — un LRS quelconque suffit, jamais besoin que le
|
||
// LMS du client supporte cmi5 lui-même.
|
||
//
|
||
// Best-effort partout : un LRS injoignable/mal configuré ne doit JAMAIS
|
||
// bloquer ni ralentir la partie du joueur.
|
||
|
||
// Statut Forge -> verbe xAPI ADL standard, uniquement pour les statuts
|
||
// qui marquent une VRAIE fin de partie — "non_commence"/"en_cours" ne
|
||
// génèrent rien ici (bruit inutile pour un suivi volontairement léger,
|
||
// voir le plan).
|
||
var FORGE_TO_XAPI_VERB = {
|
||
termine: { id: "http://adlnet.gov/expapi/verbs/completed" },
|
||
reussi: { id: "http://adlnet.gov/expapi/verbs/passed" },
|
||
echoue: { id: "http://adlnet.gov/expapi/verbs/failed" },
|
||
};
|
||
|
||
// Libellé humain par verbe — l'id xAPI (http://adlnet.gov/expapi/verbs/...)
|
||
// reste TOUJOURS en anglais (identifiant technique fixé par le
|
||
// vocabulaire ADL, jamais traduit), mais le libellé affiché par le LRS
|
||
// peut porter plusieurs langues : demande utilisateur "c'est possible en
|
||
// français ?" — un LRS qui préfère le français (ou n'importe quelle
|
||
// langue ici) affichera la bonne entrée au lieu de rester bloqué sur
|
||
// l'anglais.
|
||
var FORGE_XAPI_VERB_DISPLAY = {
|
||
"http://adlnet.gov/expapi/verbs/initialized": { "en-US": "initialized", "fr-FR": "initialisé" },
|
||
"http://adlnet.gov/expapi/verbs/terminated": { "en-US": "terminated", "fr-FR": "quitté" },
|
||
"http://adlnet.gov/expapi/verbs/completed": { "en-US": "completed", "fr-FR": "terminé" },
|
||
"http://adlnet.gov/expapi/verbs/passed": { "en-US": "passed", "fr-FR": "réussi" },
|
||
"http://adlnet.gov/expapi/verbs/failed": { "en-US": "failed", "fr-FR": "échoué" },
|
||
"http://adlnet.gov/expapi/verbs/scored": { "en-US": "scored", "fr-FR": "noté" },
|
||
"http://adlnet.gov/expapi/verbs/attempted": { "en-US": "attempted", "fr-FR": "commencé" },
|
||
"http://adlnet.gov/expapi/verbs/answered": { "en-US": "answered", "fr-FR": "répondu" },
|
||
};
|
||
|
||
var forgeXapiLastStatusSent = null; // évite de renvoyer le même statement si le statut ne change pas réellement
|
||
|
||
// Identité de l'apprenant : réutilise l'API SCORM déjà trouvée par
|
||
// scorm-api.js (window.forgeScormApi, posée sur "load" — voir plus bas,
|
||
// notre propre handler "load" s'enregistre APRÈS le sien, donc after lui)
|
||
// si le paquet tourne dans un LMS ; sinon, un acteur anonyme générique.
|
||
function forgeXapiActor() {
|
||
try {
|
||
if (typeof forgeScormApi !== "undefined" && forgeScormApi) {
|
||
var name = forgeScormApi.LMSGetValue("cmi.core.student_name");
|
||
var id = forgeScormApi.LMSGetValue("cmi.core.student_id");
|
||
if (id) {
|
||
return { objectType: "Agent", name: name || id, account: { homePage: "urn:forge-engine", name: id } };
|
||
}
|
||
}
|
||
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ }
|
||
return { objectType: "Agent", name: "Apprenant", mbox: "mailto:anonymous@forge-engine.local" };
|
||
}
|
||
|
||
function forgeXapiObject() {
|
||
var config = window.FORGE_XAPI_CONFIG;
|
||
return {
|
||
objectType: "Activity",
|
||
id: config.activity_id,
|
||
definition: { name: { "fr-FR": config.activity_name } },
|
||
};
|
||
}
|
||
|
||
// Sous-activités xAPI (voir static/js/play/dialogue-box-controller.js) :
|
||
// un dialogue/question n'est PAS l'activité de premier niveau du jeu
|
||
// (forgeXapiObject ci-dessus) mais une activité fille, identifiée par un
|
||
// id dérivé — assez pour que le LRS les distingue sans configuration
|
||
// supplémentaire côté créateur du jeu. `objectName` : le nom de l'objet
|
||
// de scène qui a déclenché ce dialogue (voir "ℹ️ Informations") — plus de
|
||
// "quête" pour le porter.
|
||
function forgeXapiDialogueObject(dialogueId, objectName) {
|
||
var config = window.FORGE_XAPI_CONFIG;
|
||
return {
|
||
objectType: "Activity",
|
||
id: config.activity_id + ":dialogue:" + dialogueId,
|
||
definition: { name: { "fr-FR": objectName || "Dialogue" }, type: "http://adlnet.gov/expapi/activities/simulation" },
|
||
};
|
||
}
|
||
|
||
function forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText) {
|
||
var config = window.FORGE_XAPI_CONFIG;
|
||
return {
|
||
objectType: "Activity",
|
||
id: config.activity_id + ":dialogue:" + dialogueId + ":question:" + questionIndex,
|
||
definition: {
|
||
name: { "fr-FR": "Question " + (questionIndex + 1) + " — " + (objectName || "Dialogue") },
|
||
description: { "fr-FR": questionText || "" },
|
||
type: "http://adlnet.gov/expapi/activities/cmi.interaction",
|
||
interactionType: "choice",
|
||
},
|
||
};
|
||
}
|
||
|
||
// verbId : voir FORGE_TO_XAPI_VERB/FORGE_XAPI_VERB_DISPLAY (le libellé
|
||
// affiché est dérivé automatiquement du verbe, jamais passé en argument)
|
||
// — result : objet xAPI "result" optionnel (score/completion/success),
|
||
// fusionné tel quel dans le statement. object : l'activité visée par CE
|
||
// statement — l'activité de premier niveau du jeu par défaut
|
||
// (forgeXapiObject), ou une sous-activité quête/question
|
||
// (forgeXapiQuestObject/forgeXapiQuestionObject) pour un suivi plus fin.
|
||
function forgeXapiSendStatement(verbId, result, object) {
|
||
var config = window.FORGE_XAPI_CONFIG;
|
||
if (!config || !config.endpoint) return;
|
||
var statement = {
|
||
actor: forgeXapiActor(),
|
||
verb: { id: verbId, display: FORGE_XAPI_VERB_DISPLAY[verbId] || { "en-US": verbId } },
|
||
object: object || forgeXapiObject(),
|
||
timestamp: new Date().toISOString(),
|
||
};
|
||
if (result) statement.result = result;
|
||
var endpoint = config.endpoint.replace(/\/+$/, "") + "/statements";
|
||
try {
|
||
fetch(endpoint, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"X-Experience-API-Version": "1.0.3",
|
||
"Authorization": "Basic " + btoa(config.login + ":" + config.password),
|
||
},
|
||
body: JSON.stringify(statement),
|
||
}).catch(function (err) {
|
||
console.warn("xAPI: envoi du statement échoué (LRS injoignable ?)", err);
|
||
});
|
||
} catch (err) {
|
||
console.warn("xAPI: impossible d'envoyer le statement", err);
|
||
}
|
||
}
|
||
|
||
// Rappelées directement par static/js/play/offline/apply-actions.js après
|
||
// chaque mutation RÉELLE (jamais un sondage périodique, contrairement à
|
||
// scorm-api.js::forgeScormSyncScore) — un seul statement par changement.
|
||
function forgeXapiNotifyScoreChanged(newScore) {
|
||
if (!window.FORGE_XAPI_CONFIG) return;
|
||
forgeXapiSendStatement(
|
||
"http://adlnet.gov/expapi/verbs/scored",
|
||
{ score: { raw: newScore } }
|
||
);
|
||
}
|
||
|
||
// Granularité dialogue/quiz (voir dialogue-box-controller.js) : demande
|
||
// explicite "il faut savoir [...] à telle question quel point obtenu,
|
||
// bien répondu ou non" — un statement DÉDIÉ par évènement, distinct du
|
||
// score/statut global (forgeXapiNotifyScoreChanged/StatusChanged
|
||
// ci-dessus, qui restent l'activité de premier niveau).
|
||
function forgeXapiNotifyDialogueCompleted(dialogueId, objectName) {
|
||
if (!window.FORGE_XAPI_CONFIG) return;
|
||
forgeXapiSendStatement(
|
||
"http://adlnet.gov/expapi/verbs/completed",
|
||
{ completion: true }, forgeXapiDialogueObject(dialogueId, objectName)
|
||
);
|
||
}
|
||
|
||
function forgeXapiNotifyQuestionAnswered(dialogueId, objectName, questionIndex, questionText, correct, scoreAwarded) {
|
||
if (!window.FORGE_XAPI_CONFIG) return;
|
||
forgeXapiSendStatement(
|
||
"http://adlnet.gov/expapi/verbs/answered",
|
||
{ success: correct, score: { raw: scoreAwarded || 0 } },
|
||
forgeXapiQuestionObject(dialogueId, objectName, questionIndex, questionText)
|
||
);
|
||
}
|
||
|
||
function forgeXapiNotifyStatusChanged(newStatus) {
|
||
if (!window.FORGE_XAPI_CONFIG) return;
|
||
var verb = FORGE_TO_XAPI_VERB[newStatus];
|
||
if (!verb || newStatus === forgeXapiLastStatusSent) return;
|
||
forgeXapiLastStatusSent = newStatus;
|
||
forgeXapiSendStatement(verb.id, {
|
||
completion: true,
|
||
success: newStatus !== "echoue",
|
||
});
|
||
}
|
||
|
||
if (typeof window !== "undefined") {
|
||
window.addEventListener("load", function () {
|
||
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/initialized");
|
||
});
|
||
window.addEventListener("beforeunload", function () {
|
||
if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/terminated");
|
||
});
|
||
}
|
||
|
||
if (typeof module !== "undefined" && module.exports) {
|
||
module.exports = {
|
||
forgeXapiSendStatement, forgeXapiNotifyScoreChanged, forgeXapiNotifyStatusChanged, FORGE_TO_XAPI_VERB,
|
||
forgeXapiNotifyDialogueCompleted, forgeXapiNotifyQuestionAnswered,
|
||
forgeXapiDialogueObject, forgeXapiQuestionObject, FORGE_XAPI_VERB_DISPLAY,
|
||
};
|
||
}
|