// ---------- Évaluation des conditions ---------- // Extrait de templates/play.html (voir plan de modularisation) : partie // PURE du moteur (aucun accès DOM) — lecture de champs/variables et // comparaison, c'est la logique la plus amenée à grossir (nouvelles // opérations, condition de collision...), donc la plus utile à tester // (voir static/js/play/__tests__/conditions.test.js). // Lit la valeur actuelle d'un champ d'objet dans l'instantané de données // du jeu (gameData.data), pour l'évaluation d'une condition. function readFieldValue(definitionId, rowId, fieldName) { // CLICKED_ROW_ID (-1, voir flow_node_run_data.py) : "la ligne de // Répéteur sur laquelle on vient de cliqué" — jamais connue à l'avance // dans l'éditeur (choisie ici via "🖱️ Ligne cliquée (Répéteur)"), // résolue seulement au moment de l'évaluation via le dernier clic // capturé (voir bindClicks() dans triggers.js). if (rowId === -1) rowId = window.lastClickedRowId; const rows = gameData.data[String(definitionId)] || []; const row = rows.find(r => r.id === rowId); return row ? row[fieldName] : undefined; } function compareValues(actual, operator, expected, fieldType) { if (fieldType === 'booleen') { const a = actual ? 1 : 0; // "oui"/"non" (voir data_list.html) est le vocabulaire affiché // partout ailleurs pour un champ booléen — une valeur de comparaison // fixe tapée "Oui" doit donc être reconnue vraie ici aussi, pas // seulement "1"/"true" (et insensible à la casse, aligné avec // _compare() côté Python, voir filter_repeater_rows.py). const expectedStr = String(expected).trim().toLowerCase(); const e = (expected === true || ['1', 'true', 'vrai', 'oui'].includes(expectedStr)) ? 1 : 0; return operator === 'different' ? a !== e : a === e; } const an = parseFloat(actual), en = parseFloat(expected); const numeric = !isNaN(an) && !isNaN(en); if (numeric) { switch (operator) { case 'egal': return an === en; case 'different': return an !== en; case 'superieur': return an > en; case 'inferieur': return an < en; case 'superieur_egal': return an >= en; case 'inferieur_egal': return an <= en; } } const as = (actual === undefined || actual === null) ? '' : String(actual); const es = (expected === undefined || expected === null) ? '' : String(expected); switch (operator) { case 'egal': return as === es; case 'different': return as !== es; case 'superieur': return as > es; case 'inferieur': return as < es; case 'superieur_egal': return as >= es; case 'inferieur_egal': return as <= es; default: return false; } } // Lit la valeur ACTUELLE d'une variable globale (gameData.variables, // exposé par full_game_payload.py, tenu à jour par refreshRuntimeData() // après toute action qui en modifie une), pour l'évaluation d'une // condition — équivalent, côté client, de _resolve_filter_value côté // serveur (screens/rendering/filter_repeater_rows.py), mais SANS la // syntaxe utilisée par le Répéteur/la Condition de visibilité (jamais // nécessaire ici : le nom de la variable est choisi dans un menu // déroulant, voir screen_edit.html). function readVariableValue(name) { const v = (gameData.variables || {})[name]; return v ? v.value : undefined; } // Navigue dans une valeur JSON (variable de type "objet"/"tableau") // selon un chemin ".champ"/"[index]" chaînable — équivalent JS de // _resolve_variable_path (même fichier Python que ci-dessus). Chemin // vide -> valeur brute inchangée (le cas normal pour une variable // scalaire). Ne lève jamais : JSON invalide ou chemin qui ne correspond // à rien -> null, comme côté serveur. function resolveVariablePath(rawValue, path) { if (!path) return rawValue; let current; try { current = rawValue ? JSON.parse(rawValue) : null; } catch (e) { return null; } const segmentRe = /\.([^.\[\]]+)|\[(\d+)\]/g; let m; while ((m = segmentRe.exec(path)) !== null) { if (current === null || current === undefined) return null; current = m[1] !== undefined ? current[m[1]] : current[parseInt(m[2], 10)]; } return current === undefined ? null : current; } // 2.4 — conditions combinées (ET/OU) : un nœud Condition peut porter une // liste cond_clauses (JSON) en plus de sa clause historique. Un nœud sans // cond_clauses (tous les nœuds créés avant 2.4, ou un nœud à une seule // clause) garde EXACTEMENT son ancien comportement — une seule comparaison. // // Chaque clause peut tester soit un champ d'objet (source absente/"objet", // comportement historique), soit une VARIABLE GLOBALE (source // "variable" — voir ensure_flow_schema.py pour cond_source/cond_variable/ // cond_variable_chemin). function evaluateConditionClause(clause) { const source = clause.source ?? clause.cond_source ?? 'objet'; const operator = clause.operator ?? clause.cond_operator; const expected = clause.value ?? clause.cond_value; if (source === 'variable') { const varName = clause.variable ?? clause.cond_variable; const path = clause.variable_chemin ?? clause.cond_variable_chemin; const varInfo = (gameData.variables || {})[varName]; const actual = resolveVariablePath(readVariableValue(varName), path); const fieldType = varInfo && varInfo.type === 'booleen' ? 'booleen' : ''; return compareValues(actual, operator, expected, fieldType); } const actual = readFieldValue(clause.definition_id ?? clause.cond_definition_id, clause.row_id ?? clause.cond_row_id, clause.field ?? clause.cond_field); return compareValues(actual, operator, expected, clause.field_type ?? clause.cond_field_type); } function evaluateConditionNode(node) { if (node.cond_clauses) { let clauses; try { clauses = JSON.parse(node.cond_clauses); } catch (e) { clauses = null; } if (Array.isArray(clauses) && clauses.length) { const results = clauses.map(evaluateConditionClause); return node.cond_combinator === 'ou' ? results.some(Boolean) : results.every(Boolean); } } return evaluateConditionClause(node); } // static/js/play/__tests__/ (node:test) importe ces fonctions via // require() — pas de risque en navigateur : `module` n'existe pas là-bas, // cette branche ne s'exécute jamais côté client. if (typeof module !== 'undefined' && module.exports) { module.exports = { readFieldValue, compareValues, readVariableValue, resolveVariablePath, evaluateConditionClause, evaluateConditionNode }; }