// 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", display: "completed" }, reussi: { id: "http://adlnet.gov/expapi/verbs/passed", display: "passed" }, echoue: { id: "http://adlnet.gov/expapi/verbs/failed", display: "failed" }, }; 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 } }, }; } // verbId/verbDisplay : voir FORGE_TO_XAPI_VERB, ou directement les // verbes "initialized"/"terminated" (posés une seule fois chacun, voir // plus bas) — result : objet xAPI "result" optionnel (score/completion/ // success), fusionné tel quel dans le statement. function forgeXapiSendStatement(verbId, verbDisplay, result) { var config = window.FORGE_XAPI_CONFIG; if (!config || !config.endpoint) return; var statement = { actor: forgeXapiActor(), verb: { id: verbId, display: { "en-US": verbDisplay } }, 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", "scored", { score: { raw: newScore } } ); } 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, verb.display, { 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", "initialized"); }); window.addEventListener("beforeunload", function () { if (window.FORGE_XAPI_CONFIG) forgeXapiSendStatement("http://adlnet.gov/expapi/verbs/terminated", "terminated"); }); } if (typeof module !== "undefined" && module.exports) { module.exports = { forgeXapiSendStatement, forgeXapiNotifyScoreChanged, forgeXapiNotifyStatusChanged, FORGE_TO_XAPI_VERB }; }