Greffe l'envoi de statements xAPI vers un LRS configurable par le créateur du jeu, en parallèle du reporting SCORM existant : réglages stockés en base (_meta), formulaire dans la modale d'export, injection dans le paquet exporté. Relié aussi bien à l'action de flow "Modifier un score/statut" qu'au parcours quête/quiz (qui alimentait déjà le SCORM classique via un chemin séparé). L'export ne se lance plus automatiquement à l'ouverture de la modale, pour laisser le temps d'enregistrer les réglages xAPI avant de générer le paquet.
169 lines
7.5 KiB
JavaScript
169 lines
7.5 KiB
JavaScript
// Export Web/SCORM (voir le plan "port complet du runtime jouable côté
|
|
// navigateur") — port fidèle des 5 handlers d'action qui, en ligne,
|
|
// passent par le serveur (voir static/js/play/actions.js) : ici, ils
|
|
// mutent gameData EN PLACE, sans aucun réseau. N'existe que pour
|
|
// window.FORGE_OFFLINE === true (paquet exporté) ; le runtime en ligne
|
|
// garde son comportement actuel, inchangé.
|
|
//
|
|
// Un score/statut de partie n'a PAS de notion per-player hors ligne (un
|
|
// paquet exporté est joué par UNE seule personne à la fois, sur SON
|
|
// poste) : gameData.scoring = {score, status}, pas une carte par joueur
|
|
// comme db/scoring/ côté serveur (voir db.PLAYER_SHARED).
|
|
|
|
// ---------- Modifier une variable globale ----------
|
|
// Port de screens/data_actions/apply_variable_action.py — même
|
|
// coercion de type, même vocabulaire d'opérations (compute-operation.js),
|
|
// la valeur reste stockée en CHAÎNE (gameData.variables[name].value est
|
|
// toujours une chaîne côté serveur, colonne SQLite TEXT — voir
|
|
// full_game_payload.py) pour que conditions.js continue de la lire
|
|
// exactement comme aujourd'hui.
|
|
function forgeApplyVariableActionOffline(gameData, action) {
|
|
var varName = action.target_variable;
|
|
var operation = action.data_operation;
|
|
var rawValue = action.data_value;
|
|
if (!varName || !operation) return false;
|
|
var variable = gameData.variables && gameData.variables[varName];
|
|
if (!variable) return false;
|
|
var varType = variable.type;
|
|
var current = variable.value;
|
|
if (varType === "nombre_entier" || varType === "nombre_decimal") {
|
|
current = (current === null || current === undefined || current === "") ? 0 : parseFloat(current);
|
|
if (isNaN(current)) current = 0;
|
|
} else if (varType === "booleen") {
|
|
var normalized = String(current == null ? "" : current).trim().toLowerCase();
|
|
current = (normalized === "1" || normalized === "true" || normalized === "vrai") ? 1 : 0;
|
|
}
|
|
var newValue;
|
|
try {
|
|
newValue = forgeComputeNewValue(operation, current, rawValue, varType === "nombre_decimal");
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
variable.value = String(newValue);
|
|
return true;
|
|
}
|
|
|
|
// ---------- Modifier le score / Définir le statut de la partie ----------
|
|
// Port de apply_score_action.py/apply_status_action.py — voir db/scoring/
|
|
// pour le vocabulaire des statuts (db.SCORE_STATUS_LABELS côté serveur,
|
|
// dupliqué ici en dur : ces 5 clés sont figées, voir db/constants.py).
|
|
var FORGE_SCORE_STATUS_KEYS = ["non_commence", "en_cours", "termine", "reussi", "echoue"];
|
|
|
|
function forgeApplyScoreActionOffline(gameData, action) {
|
|
var operation = action.data_operation;
|
|
var rawValue = action.data_value;
|
|
if (!operation) return false;
|
|
if (!gameData.scoring) gameData.scoring = { score: 0, status: "non_commence" };
|
|
var current = gameData.scoring.score || 0;
|
|
var newValue;
|
|
try {
|
|
newValue = forgeComputeNewValue(operation, current, rawValue, true);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
gameData.scoring.score = newValue;
|
|
// xAPI bolt-on (voir static/js/play/offline/xapi-client.js) : no-op si
|
|
// le fichier n'est pas chargé (tests Node via module.exports ci-dessous,
|
|
// ou réglage xAPI jamais configuré pour ce jeu).
|
|
if (typeof forgeXapiNotifyScoreChanged === "function") forgeXapiNotifyScoreChanged(newValue);
|
|
return true;
|
|
}
|
|
|
|
function forgeApplyStatusActionOffline(gameData, action) {
|
|
var status = action.data_value;
|
|
if (!status || FORGE_SCORE_STATUS_KEYS.indexOf(status) === -1) return false;
|
|
if (!gameData.scoring) gameData.scoring = { score: 0, status: "non_commence" };
|
|
gameData.scoring.status = status;
|
|
if (typeof forgeXapiNotifyStatusChanged === "function") forgeXapiNotifyStatusChanged(status);
|
|
return true;
|
|
}
|
|
|
|
// ---------- Modifier une donnée d'un objet ----------
|
|
// Port de apply_data_action.py — même bornage min_value/max_value (voir
|
|
// full_game_payload.py, étendu pour transmettre ces deux bornes au
|
|
// client). gameData.data[definitionId] est un tableau de lignes
|
|
// {champ: valeur, id}, gameData.fields_meta[definitionId] la liste
|
|
// {name, type, min_value, max_value} — même forme que le payload en
|
|
// ligne (screens/payload/full_game_payload.py).
|
|
function forgeFindFieldMeta(gameData, definitionId, fieldName) {
|
|
var fields = gameData.fields_meta && gameData.fields_meta[definitionId];
|
|
if (!fields) return null;
|
|
for (var i = 0; i < fields.length; i++) {
|
|
if (fields[i].name === fieldName) return fields[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function forgeClampToFieldBounds(value, fieldMeta) {
|
|
if (fieldMeta.type !== "nombre_entier" && fieldMeta.type !== "nombre_decimal") return value;
|
|
if (fieldMeta.min_value !== null && fieldMeta.min_value !== undefined && value < fieldMeta.min_value) {
|
|
value = fieldMeta.min_value;
|
|
}
|
|
if (fieldMeta.max_value !== null && fieldMeta.max_value !== undefined && value > fieldMeta.max_value) {
|
|
value = fieldMeta.max_value;
|
|
}
|
|
return fieldMeta.type === "nombre_decimal" ? value : Math.trunc(value);
|
|
}
|
|
|
|
function forgeApplyDataActionOffline(gameData, action) {
|
|
var definitionId = String(action.target_definition_id || "");
|
|
var rowId = action.target_row_id;
|
|
var fieldName = action.target_field;
|
|
var operation = action.data_operation;
|
|
var rawValue = action.data_value;
|
|
if (!(definitionId && rowId && fieldName && operation)) return false;
|
|
var fieldMeta = forgeFindFieldMeta(gameData, definitionId, fieldName);
|
|
if (!fieldMeta) return false;
|
|
var rows = gameData.data && gameData.data[definitionId];
|
|
if (!rows) return false;
|
|
var row = null;
|
|
for (var i = 0; i < rows.length; i++) {
|
|
if (rows[i].id === rowId) { row = rows[i]; break; }
|
|
}
|
|
if (!row) return false;
|
|
var current = row[fieldName];
|
|
var newValue;
|
|
try {
|
|
newValue = forgeComputeNewValue(operation, current, rawValue, fieldMeta.type === "nombre_decimal");
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
row[fieldName] = forgeClampToFieldBounds(newValue, fieldMeta);
|
|
return true;
|
|
}
|
|
|
|
// ---------- Ajouter une ligne à un objet ----------
|
|
// Port de apply_add_row_action.py/db/rows/insert_row.py : une ligne VIDE
|
|
// (aucun champ renseigné — c'est à un ou plusieurs nœuds "Modifier une
|
|
// donnée" enchaînés, ciblant LAST_INSERTED_ROW_ID, de la remplir ensuite),
|
|
// même défauts par type que row_columns_and_values.py (booléen -> 0,
|
|
// tout le reste -> null). L'id est un simple auto-incrément local (pas
|
|
// de vraie base : aucune ligne d'un autre joueur à ignorer hors ligne).
|
|
function forgeApplyAddRowActionOffline(gameData, action) {
|
|
var definitionId = String(action.target_definition_id || "");
|
|
if (!definitionId) return null;
|
|
var fields = gameData.fields_meta && gameData.fields_meta[definitionId];
|
|
if (!fields) return null;
|
|
if (!gameData.data[definitionId]) gameData.data[definitionId] = [];
|
|
var rows = gameData.data[definitionId];
|
|
var maxId = 0;
|
|
rows.forEach(function (r) { if (r.id > maxId) maxId = r.id; });
|
|
var newRow = { id: maxId + 1 };
|
|
fields.forEach(function (f) {
|
|
newRow[f.name] = f.type === "booleen" ? 0 : null;
|
|
});
|
|
rows.push(newRow);
|
|
return newRow.id;
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
forgeApplyVariableActionOffline, forgeApplyScoreActionOffline, forgeApplyStatusActionOffline,
|
|
forgeApplyDataActionOffline, forgeApplyAddRowActionOffline,
|
|
forgeFindFieldMeta, forgeClampToFieldBounds, FORGE_SCORE_STATUS_KEYS,
|
|
};
|
|
// node:test charge ce fichier isolément (voir __tests__/) — forgeComputeNewValue
|
|
// vit dans compute-operation.js, jamais un <script> séparé en test.
|
|
global.forgeComputeNewValue = global.forgeComputeNewValue || require('./compute-operation.js').forgeComputeNewValue;
|
|
}
|