Files
Forge-Engine/static/js/screen_edit/flow-editor.js
T
williamandClaude Sonnet 5 9776fbc057
Build and deploy / test-python (push) Successful in 7m27s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Score/Progression (Phase 1.1 de la feuille de route produit)
Concept de premier ordre, distinct du système de variables globales —
préalable identifié aux futurs exports SCORM/xAPI (note de cadrage
.claude/Forge_Engine_Cadrage.pdf) : ces standards ont besoin d'un signal
"score"/"terminé" propre, pas d'une convention sur une variable choisie
par le créateur.

- db/scoring/ (même patron que db/global_vars/) : table _scoring, un
  score numérique + un statut (non_commence/en_cours/termine/reussi/
  echoue) par joueur (voir db.PLAYER_SHARED pour l'aperçu créateur).
- Deux nouvelles actions de flux, dans les deux éditeurs (document et
  scène) : "Modifier le score" (réutilise le vocabulaire d'opérations
  déjà là pour "Modifier une variable" — incrémenter/définir/etc., aucune
  nouvelle colonne de nœud) et "Définir le statut de la partie".
- Routes créateur (routes/flow/flow_node_run_score.py,
  flow_node_run_status.py) + miroirs publics par joueur
  (routes/public_play/) — même principe que flow_node_run_variable.py.
- GET /game/<slug>/scoring/<player_id> : lecture interne, pas exposée au
  joueur, préparée pour être consommée par le futur export Web/SCORM.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 06:23:37 +02:00

876 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---------- Logique de la scène : éditeur de flow à nœuds ----------
//
// FLOW_NODES_INITIAL/FLOW_EDGES_INITIAL/FLOW_BLOCKS/ELEMENTS_LABELS/
// SCREENS_MAP/*_LABELS_MAP/CUSTOM_EVENTS_MAP/GAME_SLUG/SCREEN_ID sont
// posés par le petit <script> inline en tête de screen_edit.html (données
// Jinja) — voir static/js/play/ pour le même patron déjà en place côté
// moteur de jeu.
var FLOW_SCREEN_ID = SCREEN_ID;
// Fondations multi-éditeurs — ce même fichier (moteur de logique de flow
// partagé, voir le plan "Fondations d'une plateforme multi-éditeurs")
// sert aussi bien à l'éditeur document (cible trigger_element_id/
// target_element_id, vers _screen_elements) qu'à l'éditeur de scène 2D
// (cible trigger_object_id/target_object_id, vers _scene_objects) : posé
// à `true` par templates/scene_edit.html, sinon absent (donc falsy) sur
// templates/screen_edit.html — décide quelle paire de colonnes remplir à
// l'enregistrement d'un nœud (voir submitNodeForm()).
var FLOW_TARGETS_OBJECTS = typeof FLOW_TARGETS_OBJECTS !== 'undefined' ? FLOW_TARGETS_OBJECTS : false;
var FLOW_TRIGGER_ID_FIELD = FLOW_TARGETS_OBJECTS ? 'trigger_object_id' : 'trigger_element_id';
var FLOW_TARGET_ID_FIELD = FLOW_TARGETS_OBJECTS ? 'target_object_id' : 'target_element_id';
// Blocs de logique (voir screens/flow/blocks/) : ALL_FLOW porte TOUS les
// nœuds/arêtes de cet écran, tous blocs confondus (chargement inchangé) ;
// un seul bloc est ouvert à la fois (modale unique, à la Unity) — pas
// besoin d'une seconde copie filtrée à tenir synchronisée, currentBlockNodes()/
// currentBlockEdges() recalculent la vue du bloc ouvert à chaque rendu.
var ALL_FLOW = {
nodes: FLOW_NODES_INITIAL,
edges: FLOW_EDGES_INITIAL,
};
var CURRENT_BLOCK_ID = null;
function currentBlockNodes() {
return ALL_FLOW.nodes.filter(function(n) { return n.block_id === CURRENT_BLOCK_ID; });
}
function currentBlockEdges() {
var ids = {};
currentBlockNodes().forEach(function(n) { ids[n.id] = true; });
return ALL_FLOW.edges.filter(function(e) { return ids[e.from_node_id] && ids[e.to_node_id]; });
}
var flowPendingPort = null; // {nodeId, port} en attente de connexion
function flowUrl(path) { return "/game/" + GAME_SLUG + path; }
function nodeLabel(node) {
if (node.node_type === 'trigger') {
if (node.trigger_event === 'affichage') return '🖥️ ' + TRIGGER_LABELS_MAP['affichage'];
if (node.trigger_event === 'evenement') {
var listenedEvent = CUSTOM_EVENTS_MAP[node.trigger_custom_event_id];
return '📣 Sur « ' + (listenedEvent ? listenedEvent.name : '?') + ' »';
}
if (node.trigger_event === 'clavier') {
return '⌨️ Touche « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »';
}
if (node.trigger_event === 'minuteur') {
return '⏱️ Toutes les ' + (node.trigger_interval_ms || '?') + ' ms';
}
if (node.trigger_event === 'touche_maintenue') {
return '⌨️⏳ Touche maintenue « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »';
}
if (node.trigger_event === 'touche_relachee') {
return '⌨️⬆️ Relâchement de « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »';
}
return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' +
(ELEMENTS_LABELS[node.trigger_element_id || node.trigger_object_id] || '?');
}
if (node.node_type === 'condition') {
var clauseSubject = function(c, field, variable) {
return (c.source ?? c.cond_source) === 'variable' ? ('🔧 ' + (variable || c.cond_variable || '?')) : (field || c.cond_field || '?');
};
// Phase 5 — condition de collision : pas de champ/opérateur/valeur,
// juste les deux éléments comparés (voir evaluateConditionClause()
// dans static/js/play/conditions.js).
var clauseFullLabel = function(c, field, variable) {
if ((c.source ?? c.cond_source) === 'collision') {
var elA = ELEMENTS_LABELS[c.element_a ?? c.cond_element_a] || '?';
var elB = ELEMENTS_LABELS[c.element_b ?? c.cond_element_b] || '?';
return '💥 ' + elA + ' touche ' + elB;
}
return clauseSubject(c, field, variable) + ' ' + (CONDITION_OPERATOR_LABELS_MAP[c.operator ?? c.cond_operator] || (c.operator ?? c.cond_operator)) + ' ' + ((c.value ?? c.cond_value) || '');
};
var mainClause = '❓ SI ' + clauseFullLabel(node, node.cond_field, node.cond_variable);
if (node.cond_clauses) {
// 2.4 — plusieurs clauses combinées : on affiche chaque champ/variable
// testé, relié par le connecteur ET/OU choisi, plutôt qu'une seule clause.
try {
var clauses = JSON.parse(node.cond_clauses);
var combLabel = node.cond_combinator === 'ou' ? ' OU ' : ' ET ';
return '❓ SI ' + clauses.map(function(c) {
return clauseFullLabel(c, c.field, c.variable);
}).join(combLabel);
} catch (e) { /* JSON invalide : on retombe sur la clause historique */ }
}
return mainClause;
}
// action
var base = '⚡ ' + (ACTION_TYPE_LABELS_MAP[node.action_type] || node.action_type);
if (node.action_type === 'aller_a') {
base += ' → ' + (SCREENS_MAP[node.target_screen_id] || '?');
} else if (node.action_type === 'ouvrir_ligne') {
base += ' → ' + (SCREENS_MAP[node.target_screen_id] || '?') + ' (ligne cliquée)';
} else if (node.action_type === 'activer_onglet') {
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' (masque ses frères)';
} else if (node.action_type === 'modifier_objet_scene') {
// Fondations multi-éditeurs — équivalent scène de "modifier_element"
// ci-dessous, palette réduite (visibilité/position en pixels/
// orientation seulement).
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id || node.target_object_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property);
if (node.element_property === 'visibilite') {
base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value);
} else if (node.element_property === 'orientation') {
base += ' = ' + (ELEMENT_ORIENTATION_LABELS_MAP[node.element_value] || node.element_value);
} else {
base += ' = ' + node.element_value;
}
} else if (node.action_type === 'modifier_element') {
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property);
if (node.element_property === 'visibilite') {
base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value);
} else if (node.element_property === 'surbrillance') {
base += ' = ' + (ELEMENT_SURBRILLANCE_LABELS_MAP[node.element_value] || node.element_value);
} else if (node.element_property === 'desactive') {
base += ' = ' + (ELEMENT_DESACTIVE_LABELS_MAP[node.element_value] || node.element_value);
} else if (node.element_property === 'orientation') {
base += ' = ' + (ELEMENT_ORIENTATION_LABELS_MAP[node.element_value] || node.element_value);
} else if (node.element_value && node.element_value.indexOf('||') !== -1) {
var vparts = node.element_value.split('||');
base += ' = ' + vparts[0] + ' ↔ ' + vparts[1] + ' (va-et-vient)';
} else {
base += ' = ' + node.element_value;
}
} else if (node.action_type === 'modifier_donnee') {
base += ' → ' + (node.target_field || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
if (node.data_value) base += ' ' + node.data_value;
} else if (node.action_type === 'modifier_variable') {
base += ' → ' + (node.target_variable || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
if (node.data_value) base += ' ' + node.data_value;
} else if (node.action_type === 'modifier_score') {
base += ' → ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
if (node.data_value) base += ' ' + node.data_value;
} else if (node.action_type === 'definir_statut_partie') {
base += ' → ' + (SCORE_STATUS_LABELS_MAP[node.data_value] || node.data_value || '?');
} else if (node.action_type === 'attendre') {
base += ' (' + (node.data_value || '0') + ' s)';
} else if (node.action_type === 'declencher_evenement') {
// Notification pure : rien d'autre à afficher que le nom de
// l'événement (voir screens/custom_events/) — aucun élément/ligne
// n'est jamais transmis au déclenchement.
var dispatchedEvent = CUSTOM_EVENTS_MAP[node.target_custom_event_id];
base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »';
} else if (node.action_type === 'ajouter_ligne') {
base += ' → ' + (DEFINITIONS_NAMES[node.target_definition_id] || '?');
} else if (node.action_type === 'jouer_son') {
base += ' « ' + (node.data_value || '?') + ' »';
} else if (node.action_type === 'jouer_animation_sprite') {
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id || node.target_object_id] || '?');
try {
var spriteData = JSON.parse(node.data_value || '{}');
base += ' « ' + (spriteData.animation || '?') + ' » (' + (spriteData.fps || '?') + ' i/s' + (spriteData.loop ? ', boucle' : '') + ')';
} catch (e) { /* data_value invalide : pas de détail affiché */ }
}
return base;
}
function renderFlow() {
var canvas = document.getElementById('flowCanvas');
var svg = document.getElementById('flowEdges');
canvas.innerHTML = '';
svg.innerHTML = '';
currentBlockNodes().forEach(function(node) {
var div = document.createElement('div');
div.className = 'flowNode flowNode-' + node.node_type;
div.style.left = node.x + 'px';
div.style.top = node.y + 'px';
div.dataset.nodeId = node.id;
var del = document.createElement('button');
del.className = 'flowNodeDelete';
del.type = 'button';
del.textContent = '✕';
del.onclick = function(ev) { ev.stopPropagation(); deleteFlowNode(node.id); };
div.appendChild(del);
var body = document.createElement('div');
body.className = 'flowNodeBody';
body.textContent = nodeLabel(node);
div.appendChild(body);
if (node.node_type !== 'trigger') {
var inPort = document.createElement('div');
inPort.className = 'flowPort flowPortIn';
inPort.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'in', null); };
div.appendChild(inPort);
}
if (node.node_type === 'condition') {
var outTrue = document.createElement('div');
outTrue.className = 'flowPort flowPortOut flowPortTrue';
outTrue.title = 'Vrai';
outTrue.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'vrai'); };
div.appendChild(outTrue);
var outFalse = document.createElement('div');
outFalse.className = 'flowPort flowPortOut flowPortFalse';
outFalse.title = 'Faux';
outFalse.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'faux'); };
div.appendChild(outFalse);
} else {
var outPort = document.createElement('div');
outPort.className = 'flowPort flowPortOut';
outPort.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'out'); };
div.appendChild(outPort);
}
div.addEventListener('mousedown', function(e) { onFlowNodeMouseDown(e, node.id); });
canvas.appendChild(div);
});
// Redessine les fils après que les nœuds soient dans le DOM (pour lire leur position réelle).
requestAnimationFrame(drawFlowEdges);
}
function portPosition(nodeId, side, sub) {
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
if (!div) return null;
var wrap = document.querySelector('.flowCanvasWrap');
var wrapRect = wrap.getBoundingClientRect();
var selector = side === 'in' ? '.flowPortIn' : (sub === 'vrai' ? '.flowPortTrue' : (sub === 'faux' ? '.flowPortFalse' : '.flowPortOut'));
var port = div.querySelector(selector);
if (!port) return null;
var r = port.getBoundingClientRect();
return { x: r.left + r.width / 2 - wrapRect.left + wrap.scrollLeft, y: r.top + r.height / 2 - wrapRect.top + wrap.scrollTop };
}
function drawFlowEdges() {
var svg = document.getElementById('flowEdges');
svg.innerHTML = '';
currentBlockEdges().forEach(function(edge) {
var from = portPosition(edge.from_node_id, 'out', edge.from_port);
var to = portPosition(edge.to_node_id, 'in', null);
if (!from || !to) return;
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', from.x); line.setAttribute('y1', from.y);
line.setAttribute('x2', to.x); line.setAttribute('y2', to.y);
line.setAttribute('class', 'flowEdgeLine' + (edge.from_port === 'faux' ? ' flowEdgeFalse' : (edge.from_port === 'vrai' ? ' flowEdgeTrue' : '')));
line.style.pointerEvents = 'stroke';
line.addEventListener('click', function() { deleteFlowEdge(edge.id); });
svg.appendChild(line);
});
}
function onPortClick(nodeId, side, port) {
if (side === 'out') {
flowPendingPort = { nodeId: nodeId, port: port };
return;
}
// side === 'in'
if (!flowPendingPort) return;
if (flowPendingPort.nodeId === nodeId) { flowPendingPort = null; return; }
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/edges/add'), {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId })
}).then(function(r){ return r.json(); }).then(function(edge){
ALL_FLOW.edges.push({ id: edge.id, screen_id: FLOW_SCREEN_ID, from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId });
flowPendingPort = null;
drawFlowEdges();
});
}
function deleteFlowEdge(edgeId) {
fetch(flowUrl('/flow/edges/' + edgeId + '/delete'), { method: 'POST' }).then(function(){
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.id !== edgeId; });
drawFlowEdges();
});
}
function deleteFlowNode(nodeId) {
fetch(flowUrl('/flow/nodes/' + nodeId + '/delete'), { method: 'POST' }).then(function(){
ALL_FLOW.nodes = ALL_FLOW.nodes.filter(function(n){ return n.id !== nodeId; });
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.from_node_id !== nodeId && e.to_node_id !== nodeId; });
renderFlow();
});
}
function onFlowNodeMouseDown(e, nodeId) {
if (e.target.classList.contains('flowPort') || e.target.classList.contains('flowNodeDelete')) return;
e.preventDefault();
var node = ALL_FLOW.nodes.find(function(n){ return n.id === nodeId; });
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
var startX = e.clientX, startY = e.clientY;
var startLeft = node.x, startTop = node.y;
function onMove(ev) {
var dx = ev.clientX - startX, dy = ev.clientY - startY;
node.x = Math.max(0, startLeft + dx);
node.y = Math.max(0, startTop + dy);
div.style.left = node.x + 'px';
div.style.top = node.y + 'px';
drawFlowEdges();
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
fetch(flowUrl('/flow/nodes/' + nodeId + '/move'), {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ x: node.x, y: node.y })
});
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
// ---------- Formulaire d'ajout de nœud ----------
var currentNodeFormType = null;
function openNodeForm(type) {
currentNodeFormType = type;
document.getElementById('nodeForm').style.display = 'block';
document.getElementById('nodeFormTitle').textContent =
type === 'trigger' ? 'Nouveau déclencheur' : (type === 'condition' ? 'Nouvelle condition' : 'Nouvelle action');
document.getElementById('nodeFormTrigger').style.display = type === 'trigger' ? 'block' : 'none';
document.getElementById('nodeFormCondition').style.display = type === 'condition' ? 'block' : 'none';
document.getElementById('nodeFormAction').style.display = type === 'action' ? 'block' : 'none';
if (type === 'trigger') toggleFlowTriggerFields();
if (type === 'condition') {
document.getElementById('nf-cond-source').value = 'objet';
onFlowConditionSourceChange();
onFlowDefinitionChange('cond');
flowConditionExtraClauses = [];
renderFlowConditionClauses();
}
if (type === 'action') {
document.getElementById('nf-action-element-toggle').checked = false;
toggleFlowActionFields();
}
}
function toggleFlowTriggerFields() {
var eventVal = document.getElementById('nf-trigger-event').value;
var isAffichage = eventVal === 'affichage';
// "Sur un événement personnalisé" ne concerne ni un élément précis ni
// l'écran entier : c'est un scan global (voir dispatchGameEvent() dans
// play.html), le seul réglage propre à ce nœud est QUEL événement.
var isCustomEvent = eventVal === 'evenement';
// Idem "clavier"/"minuteur" (Phase 3) : ni élément ni écran précis.
// "touche_maintenue"/"touche_relachee" (mouvement continu) réutilisent
// le même champ trigger_key que "clavier".
var isKeyboard = eventVal === 'clavier' || eventVal === 'touche_maintenue' || eventVal === 'touche_relachee';
var isTimer = eventVal === 'minuteur';
var noElement = isAffichage || isCustomEvent || isKeyboard || isTimer;
document.getElementById('nf-trigger-element-row').style.display = noElement ? 'none' : 'block';
document.getElementById('nf-trigger-repeater-hint').style.display = noElement ? 'none' : 'block';
document.getElementById('nf-trigger-affichage-hint').style.display = isAffichage ? 'block' : 'none';
document.getElementById('nf-trigger-event-row').style.display = isCustomEvent ? 'block' : 'none';
document.getElementById('nf-trigger-key-row').style.display = isKeyboard ? 'block' : 'none';
document.getElementById('nf-trigger-interval-row').style.display = isTimer ? 'block' : 'none';
}
// Capture la touche pressée dans le champ "Touche à surveiller" (Phase 3)
// — évite de faire deviner la syntaxe attendue (ev.key du navigateur,
// ex. "ArrowUp", "a", " " pour la barre d'espace) : l'input lui-même
// écoute keydown et se remplit tout seul, jamais tapé à la main.
function captureFlowTriggerKey(event) {
event.preventDefault();
document.getElementById('nf-trigger-key').value = event.key;
document.getElementById('nf-trigger-key-display').value = event.key === ' ' ? 'Espace' : event.key;
}
function closeNodeForm() {
document.getElementById('nodeForm').style.display = 'none';
currentNodeFormType = null;
}
// CLICKED_ROW_ID (-1, voir screens/flow/constants.py) : "la ligne de
// Répéteur sur laquelle on vient de cliquer" — pour un déclencheur "Au
// clic" posé sur un Répéteur, chaque ligne exécute le MÊME graphe et la
// ligne cliquée n'est jamais connue à l'avance ; choisir une ligne fixe
// dans l'éditeur n'a donc pas de sens pour ce cas. Résolue au moment de
// l'exécution (window.lastClickedRowId, voir play.html), pas à la
// création du nœud — proposée en tête de TOUTE liste "Ligne concernée",
// avant les lignes réelles de l'objet.
var FLOW_CLICKED_ROW_ID = -1;
// LAST_INSERTED_ROW_ID (-3, voir screens/flow/constants.py) : "la ligne
// qu'un nœud Action 'Ajouter une ligne' vient de créer" (Phase 4) — même
// principe que CLICKED_ROW_ID ci-dessus, résolue au moment de l'exécution
// (window.lastInsertedRowId, voir static/js/play/actions.js) puisqu'un
// id de ligne n'existe qu'APRÈS l'insertion, jamais connu à la création
// du nœud. Permet d'enchaîner "Ajouter une ligne" -> "Modifier une
// donnée"/condition ciblant cette même ligne fraîchement créée.
var FLOW_LAST_INSERTED_ROW_ID = -3;
function _flowRowOptionsHtml(info) {
var options = '<option value="' + FLOW_CLICKED_ROW_ID + '">🖱️ Ligne cliquée (Répéteur)</option>';
options += '<option value="' + FLOW_LAST_INSERTED_ROW_ID + '">➕ Dernière ligne ajoutée</option>';
options += info.rows.length
? info.rows.map(function(r){ return '<option value="' + r.id + '">' + r.label + '</option>'; }).join('')
: '<option value="" disabled>Aucune donnée dans cet objet</option>';
return options;
}
function onFlowDefinitionChange(prefix) {
var defSel = document.getElementById(prefix === 'cond' ? 'nf-cond-definition' : 'nf-action-data-definition');
if (!defSel) return;
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
var rowSel = document.getElementById(prefix === 'cond' ? 'nf-cond-row' : 'nf-action-data-row');
var fieldSel = document.getElementById(prefix === 'cond' ? 'nf-cond-field' : 'nf-action-data-field');
rowSel.innerHTML = _flowRowOptionsHtml(info);
fieldSel.innerHTML = info.fields.length
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
: '<option value="">Aucun champ simple</option>';
}
function onFlowFieldChange() {
// Pas d'action nécessaire ici pour l'instant : le type du champ est relu
// directement sur l'option sélectionnée au moment de la création du nœud.
}
// Bascule la clause PRINCIPALE d'un nœud Condition entre "Champ d'un
// objet" (historique) et "Variable globale" — voir ensure_flow_schema.py
// (cond_source) et evaluateConditionClause() dans play.html. Même bascule
// pour une clause SUPPLÉMENTAIRE : voir onFlowClauseSourceChange() plus
// bas, sur le même principe mais scopée par index.
function onFlowConditionSourceChange() {
var source = document.getElementById('nf-cond-source').value;
var isVariable = source === 'variable';
var isCollision = source === 'collision';
document.getElementById('nf-cond-objet-fields').style.display = (isVariable || isCollision) ? 'none' : 'block';
document.getElementById('nf-cond-variable-fields').style.display = isVariable ? 'block' : 'none';
var hint = document.getElementById('nf-cond-no-objet-hint');
if (hint) hint.style.display = (isVariable || isCollision) ? 'none' : 'block';
// Collision (Phase 5) : ni opérateur ni valeur de comparaison — le
// chevauchement EST directement le booléen vrai/faux du nœud (voir
// evaluateConditionClause() dans static/js/play/conditions.js).
document.getElementById('nf-cond-collision-fields').style.display = isCollision ? 'block' : 'none';
document.getElementById('nf-cond-operator-value-row').style.display = isCollision ? 'none' : 'block';
}
// ---------- 2.4 — clauses supplémentaires (ET/OU) d'un nœud Condition ----------
// Chaque clause supplémentaire réutilise EXACTEMENT le même modèle que la
// clause principale (nf-cond-definition/row/field/operator/value) mais dans
// une ligne indépendante, numérotée par index dans flowConditionExtraClauses.
var flowConditionExtraClauses = [];
function addFlowConditionClause() {
var firstDefSel = document.getElementById('nf-cond-definition');
var defaultDefId = firstDefSel && firstDefSel.value ? firstDefSel.value : '';
flowConditionExtraClauses.push({
source: 'objet', definition_id: defaultDefId, row_id: '', field: '', field_type: '',
variable: '', variable_chemin: '', operator: 'egal', value: '',
});
renderFlowConditionClauses();
}
function removeFlowConditionClause(idx) {
flowConditionExtraClauses.splice(idx, 1);
renderFlowConditionClauses();
}
// Bascule une clause SUPPLÉMENTAIRE (indexée) entre "Champ d'un objet" et
// "Variable globale" — même principe que onFlowConditionSourceChange() pour
// la clause principale, scopé par index plutôt que par id fixe.
function onFlowClauseSourceChange(idx) {
var sel = document.getElementById('nf-cond-extra-source-' + idx);
if (!sel) return;
flowConditionExtraClauses[idx].source = sel.value;
var isVariable = sel.value === 'variable';
var objetFields = document.getElementById('nf-cond-extra-objet-fields-' + idx);
var varFields = document.getElementById('nf-cond-extra-variable-fields-' + idx);
if (objetFields) objetFields.style.display = isVariable ? 'none' : 'block';
if (varFields) varFields.style.display = isVariable ? 'block' : 'none';
}
function renderFlowConditionClauses() {
var wrap = document.getElementById('nf-cond-extra-clauses');
var combinatorRow = document.getElementById('nf-cond-combinator-row');
if (!wrap || !combinatorRow) return;
combinatorRow.style.display = flowConditionExtraClauses.length ? 'block' : 'none';
var defOptionsHtml = document.getElementById('nf-cond-definition') ? document.getElementById('nf-cond-definition').innerHTML : '';
wrap.innerHTML = flowConditionExtraClauses.map(function(clause, idx) {
var isVariable = clause.source === 'variable';
return '' +
'<div class="flowConditionClauseRow" style="border-top:1px dashed var(--border); margin-top:8px; padding-top:8px;">' +
'<label>Source<select id="nf-cond-extra-source-' + idx + '" onchange="onFlowClauseSourceChange(' + idx + ')">' +
'<option value="objet"' + (isVariable ? '' : ' selected') + '>Champ d\'un objet</option>' +
'<option value="variable"' + (isVariable ? ' selected' : '') + '>Variable globale</option>' +
'</select></label>' +
'<div id="nf-cond-extra-objet-fields-' + idx + '" style="display:' + (isVariable ? 'none' : 'block') + ';">' +
'<label>Objet<select id="nf-cond-extra-definition-' + idx + '" onchange="onFlowClauseDefinitionChange(' + idx + ')">' + defOptionsHtml + '</select></label>' +
'<label>Ligne concernée<select id="nf-cond-extra-row-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].row_id = this.value"></select></label>' +
'<label>Champ<select id="nf-cond-extra-field-' + idx + '" onchange="onFlowClauseFieldChange(' + idx + ')"></select></label>' +
'</div>' +
'<div id="nf-cond-extra-variable-fields-' + idx + '" style="display:' + (isVariable ? 'block' : 'none') + ';">' +
'<label>Variable<select id="nf-cond-extra-variable-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].variable = this.value">' +
'<option value="">(sélectionner une variable)</option>' + GLOBAL_VARIABLES_OPTIONS_HTML +
'</select></label>' +
'<label>Chemin (optionnel)<input type="text" id="nf-cond-extra-variable-chemin-' + idx + '" placeholder="ex. .nom ou [0].valeur" value="' + (clause.variable_chemin || '').replace(/"/g, '&quot;') + '" onchange="flowConditionExtraClauses[' + idx + '].variable_chemin = this.value"></label>' +
'</div>' +
'<label>Condition<select id="nf-cond-extra-operator-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].operator = this.value">' +
CONDITION_OPERATORS_HTML +
'</select></label>' +
'<label>Valeur de comparaison<input type="text" id="nf-cond-extra-value-' + idx + '" placeholder="ex. 1, vrai, 10..." value="' + (clause.value || '').replace(/"/g, '&quot;') + '" onchange="flowConditionExtraClauses[' + idx + '].value = this.value"></label>' +
'<button type="button" class="smallBtn button is-small" onclick="removeFlowConditionClause(' + idx + ')">Retirer cette condition</button>' +
'</div>';
}).join('');
flowConditionExtraClauses.forEach(function(clause, idx) {
var defSel = document.getElementById('nf-cond-extra-definition-' + idx);
if (defSel && clause.definition_id) defSel.value = clause.definition_id;
onFlowClauseDefinitionChange(idx);
var opSel = document.getElementById('nf-cond-extra-operator-' + idx);
if (opSel) opSel.value = clause.operator || 'egal';
var varSel = document.getElementById('nf-cond-extra-variable-' + idx);
if (varSel && clause.variable) varSel.value = clause.variable;
});
}
function onFlowClauseDefinitionChange(idx) {
var defSel = document.getElementById('nf-cond-extra-definition-' + idx);
var rowSel = document.getElementById('nf-cond-extra-row-' + idx);
var fieldSel = document.getElementById('nf-cond-extra-field-' + idx);
if (!defSel || !rowSel || !fieldSel) return;
flowConditionExtraClauses[idx].definition_id = defSel.value;
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
rowSel.innerHTML = _flowRowOptionsHtml(info);
fieldSel.innerHTML = info.fields.length
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
: '<option value="">Aucun champ simple</option>';
flowConditionExtraClauses[idx].row_id = rowSel.value;
onFlowClauseFieldChange(idx);
}
function onFlowClauseFieldChange(idx) {
var fieldSel = document.getElementById('nf-cond-extra-field-' + idx);
if (!fieldSel) return;
var opt = fieldSel.options[fieldSel.selectedIndex];
flowConditionExtraClauses[idx].field = fieldSel.value;
flowConditionExtraClauses[idx].field_type = opt ? opt.dataset.type : '';
}
function toggleFlowActionFields() {
var typeSel = document.getElementById('nf-action-type');
var screenSel = document.getElementById('nf-action-target-screen');
var elementFields = document.getElementById('nf-action-element-fields');
var objectFields = document.getElementById('nf-action-object-fields');
var dataFields = document.getElementById('nf-action-data-fields');
var variableFields = document.getElementById('nf-action-variable-fields');
var scoreFields = document.getElementById('nf-action-score-fields');
var statusFields = document.getElementById('nf-action-status-fields');
var waitFields = document.getElementById('nf-action-wait-fields');
var eventFields = document.getElementById('nf-action-event-fields');
var addRowFields = document.getElementById('nf-action-add-row-fields');
var soundFields = document.getElementById('nf-action-sound-fields');
var spriteFields = document.getElementById('nf-action-sprite-fields');
var isOnglet = typeSel.value === 'activer_onglet';
// Champs propres à l'éditeur document ci-dessous (nf-action-element-*/
// -onglet-*) : absents du DOM de l'éditeur de scène (templates/
// scene_edit.html), d'où les gardes "if (x)" partout — même
// #nodeForm/flow-editor.js réutilisé pour les deux éditeurs (voir
// FLOW_TARGETS_OBJECTS en tête de ce fichier).
if (soundFields) soundFields.style.display = typeSel.value === 'jouer_son' ? 'block' : 'none';
if (spriteFields) spriteFields.style.display = typeSel.value === 'jouer_animation_sprite' ? 'block' : 'none';
if (typeSel.value === 'jouer_animation_sprite') onSpriteTargetElementChange();
if (screenSel) screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
if (elementFields) elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
if (objectFields) objectFields.style.display = typeSel.value === 'modifier_objet_scene' ? 'block' : 'none';
if (dataFields) dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
if (variableFields) variableFields.style.display = typeSel.value === 'modifier_variable' ? 'block' : 'none';
if (scoreFields) scoreFields.style.display = typeSel.value === 'modifier_score' ? 'block' : 'none';
if (statusFields) statusFields.style.display = typeSel.value === 'definir_statut_partie' ? 'block' : 'none';
if (waitFields) waitFields.style.display = typeSel.value === 'attendre' ? 'block' : 'none';
if (eventFields) eventFields.style.display = typeSel.value === 'declencher_evenement' ? 'block' : 'none';
if (addRowFields) addRowFields.style.display = typeSel.value === 'ajouter_ligne' ? 'block' : 'none';
var ongletHint = document.getElementById('nf-action-onglet-hint');
if (ongletHint) ongletHint.style.display = isOnglet ? 'block' : 'none';
var elementPropertyWrap = document.getElementById('nf-action-element-property-wrap');
if (elementPropertyWrap) elementPropertyWrap.style.display = isOnglet ? 'none' : 'block';
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
if (typeSel.value === 'modifier_objet_scene') toggleFlowObjectPropertyValue();
if (typeSel.value === 'modifier_donnee') onFlowDefinitionChange('action');
updateDataValuePlaceholder('nf-action-data-operation', 'nf-action-data-value');
updateDataValuePlaceholder('nf-action-variable-operation', 'nf-action-variable-value');
updateDataValuePlaceholder('nf-action-score-operation', 'nf-action-score-value');
}
// Fondations multi-éditeurs — équivalent de toggleFlowElementPropertyValue()
// pour l'action "Modifier un objet de scène" (palette réduite : pas de
// contenu/couleur/largeur/hauteur, juste visibilité/position en pixels/
// orientation — voir screens/scenes/flow_palette.py OBJECT_ACTION_PROPERTIES).
function toggleFlowObjectPropertyValue() {
var propSel = document.getElementById('nf-action-object-property');
if (!propSel) return;
var visSel = document.getElementById('nf-action-object-value-visibility');
var orientSel = document.getElementById('nf-action-object-value-orientation');
var numInp = document.getElementById('nf-action-object-value-number');
[visSel, orientSel, numInp].forEach(function(el) { if (el) el.style.display = 'none'; });
if (propSel.value === 'visibilite' && visSel) {
visSel.style.display = 'block';
} else if (propSel.value === 'orientation' && orientSel) {
orientSel.style.display = 'block';
} else if (numInp) {
numInp.style.display = 'block';
}
}
// "alea" (Phase 2 — hasard) attend "min,max" dans data_value (voir
// screens/data_actions/compute_operation.py) plutôt qu'un seul nombre
// comme les autres opérations — indice visuel dans le champ plutôt que
// d'ajouter un champ dédié (aucun changement de schéma nécessaire).
function updateDataValuePlaceholder(operationSelectId, valueInputId) {
var opSel = document.getElementById(operationSelectId);
var valueInp = document.getElementById(valueInputId);
if (!opSel || !valueInp) return;
valueInp.placeholder = opSel.value === 'alea' ? 'min,max (ex. 1,6)' : 'Valeur / montant';
}
// Phase 8 — sélecteur d'animation à miniatures RÉELLES, scopé au
// personnage réellement ciblé (ELEMENT_ANIMATIONS_MAP, injecté par
// screen_edit.html à partir des propriétés de CET élément précis — voir
// resolve_personnage_animations() côté serveur) — jamais un menu déroulant
// ni un catalogue global. Mirror de onFlowDefinitionChange() (précédent le
// plus proche pour "choisir A repeuple B").
function onSpriteTargetElementChange() {
var elSel = document.getElementById('nf-action-sprite-target-element');
var grid = document.getElementById('nf-action-sprite-animation-grid');
var nameField = document.getElementById('nf-action-sprite-animation-name');
grid.innerHTML = '';
nameField.value = '';
var elementId = elSel.value;
var animations = elementId ? (ELEMENT_ANIMATIONS_MAP[elementId] || {}) : {};
Object.keys(animations).forEach(function(animName) {
var frames = animations[animName];
if (!frames.length) return;
var tile = document.createElement('button');
tile.type = 'button';
tile.className = 'iconGalleryTile';
tile.title = animName;
tile.innerHTML = '<img src="' + frames[0] + '" alt="" class="personnageThumb">';
tile.onclick = function() {
grid.querySelectorAll('.iconGalleryTile').forEach(function(t) { t.classList.remove('selected'); });
tile.classList.add('selected');
nameField.value = animName;
// Vitesse par défaut proportionnelle au nombre d'images (8 pour un
// cycle Kenney à 8 images = ~1 cycle/seconde, mais un cycle CraftPix
// à 30 images (personnages animaux) au même 8 i/s prenait ~3,75 s —
// visiblement "très lent", signalé par l'utilisateur. Reste modifiable
// à la main ensuite (ce n'est qu'une valeur de départ raisonnable).
var fpsField = document.getElementById('nf-action-sprite-fps');
if (fpsField) fpsField.value = Math.max(8, Math.min(24, frames.length));
};
grid.appendChild(tile);
});
}
function toggleFlowElementPropertyValue() {
var propSel = document.getElementById('nf-action-element-property');
var visSel = document.getElementById('nf-action-element-value-visibility');
var surbrillanceSel = document.getElementById('nf-action-element-value-surbrillance');
var desactiveSel = document.getElementById('nf-action-element-value-desactive');
var orientationSel = document.getElementById('nf-action-element-value-orientation');
var colorInp = document.getElementById('nf-action-element-value-color');
var numInp = document.getElementById('nf-action-element-value-number');
var textInp = document.getElementById('nf-action-element-value-text');
var toggleRow = document.getElementById('nf-action-element-toggle-row');
[visSel, surbrillanceSel, desactiveSel, orientationSel, colorInp, numInp, textInp].forEach(function(el){ el.style.display = 'none'; });
if (propSel.value === 'visibilite') {
visSel.style.display = 'block';
toggleRow.style.display = 'none';
document.getElementById('nf-action-element-toggle').checked = false;
} else if (propSel.value === 'surbrillance') {
surbrillanceSel.style.display = 'block';
toggleRow.style.display = 'none';
document.getElementById('nf-action-element-toggle').checked = false;
} else if (propSel.value === 'desactive') {
desactiveSel.style.display = 'block';
toggleRow.style.display = 'none';
document.getElementById('nf-action-element-toggle').checked = false;
} else if (propSel.value === 'orientation') {
orientationSel.style.display = 'block';
toggleRow.style.display = 'none';
document.getElementById('nf-action-element-toggle').checked = false;
} else if (propSel.value === 'contenu') {
textInp.style.display = 'block';
toggleRow.style.display = 'none';
document.getElementById('nf-action-element-toggle').checked = false;
} else if (propSel.value === 'bg_color' || propSel.value === 'border_color') {
colorInp.style.display = 'block';
toggleRow.style.display = 'flex';
} else {
numInp.style.display = 'block';
toggleRow.style.display = 'flex';
}
toggleFlowElementToggleMode();
}
function toggleFlowElementToggleMode() {
var propSel = document.getElementById('nf-action-element-property');
var checked = document.getElementById('nf-action-element-toggle').checked;
var wrap = document.getElementById('nf-action-element-value2-wrap');
var colorInp2 = document.getElementById('nf-action-element-value2-color');
var numInp2 = document.getElementById('nf-action-element-value2-number');
var isColor = propSel.value === 'bg_color' || propSel.value === 'border_color';
wrap.style.display = checked ? 'block' : 'none';
colorInp2.style.display = (checked && isColor) ? 'block' : 'none';
numInp2.style.display = (checked && !isColor) ? 'block' : 'none';
}
function submitNodeForm() {
var payload = { node_type: currentNodeFormType, x: 20, y: 20 + currentBlockNodes().length * 10, block_id: CURRENT_BLOCK_ID };
if (currentNodeFormType === 'trigger') {
payload.trigger_event = document.getElementById('nf-trigger-event').value;
if (payload.trigger_event === 'evenement') {
payload.trigger_custom_event_id = parseInt(document.getElementById('nf-trigger-custom-event').value, 10);
} else if (payload.trigger_event === 'clavier' || payload.trigger_event === 'touche_maintenue' || payload.trigger_event === 'touche_relachee') {
payload.trigger_key = document.getElementById('nf-trigger-key').value;
} else if (payload.trigger_event === 'minuteur') {
payload.trigger_interval_ms = parseInt(document.getElementById('nf-trigger-interval').value, 10);
} else if (payload.trigger_event !== 'affichage') {
payload[FLOW_TRIGGER_ID_FIELD] = parseInt(document.getElementById('nf-trigger-element').value, 10);
}
} else if (currentNodeFormType === 'condition') {
var condSource = document.getElementById('nf-cond-source').value;
payload.cond_source = condSource;
if (condSource === 'collision') {
// Phase 5 — ni opérateur ni valeur de comparaison pour une
// collision (voir onFlowConditionSourceChange()) : juste les deux
// éléments dont on compare les rectangles. Pas de clauses combinées
// (ET/OU) pour une collision — même limitation assumée que pour les
// autres sources tant qu'aucune clause supplémentaire n'a été ajoutée.
payload.cond_element_a = parseInt(document.getElementById('nf-cond-element-a').value, 10);
payload.cond_element_b = parseInt(document.getElementById('nf-cond-element-b').value, 10);
} else {
payload.cond_operator = document.getElementById('nf-cond-operator').value;
payload.cond_value = document.getElementById('nf-cond-value').value;
if (condSource === 'variable') {
payload.cond_variable = document.getElementById('nf-cond-variable').value;
payload.cond_variable_chemin = document.getElementById('nf-cond-variable-chemin').value;
} else {
var fieldSel = document.getElementById('nf-cond-field');
var fieldOpt = fieldSel.options[fieldSel.selectedIndex];
payload.cond_definition_id = parseInt(document.getElementById('nf-cond-definition').value, 10);
payload.cond_row_id = parseInt(document.getElementById('nf-cond-row').value, 10) || null;
payload.cond_field = fieldSel.value;
payload.cond_field_type = fieldOpt ? fieldOpt.dataset.type : '';
}
}
if (condSource !== 'collision' && flowConditionExtraClauses.length) {
// 2.4 — clauses combinées : la clause "historique" ci-dessus devient
// la première clause de la liste, suivie des clauses supplémentaires.
var mainClause = condSource === 'variable'
? { source: 'variable', variable: payload.cond_variable, variable_chemin: payload.cond_variable_chemin,
operator: payload.cond_operator, value: payload.cond_value }
: { source: 'objet', definition_id: payload.cond_definition_id, row_id: payload.cond_row_id,
field: payload.cond_field, field_type: payload.cond_field_type,
operator: payload.cond_operator, value: payload.cond_value };
var allClauses = [mainClause].concat(flowConditionExtraClauses.map(function(c) {
return c.source === 'variable'
? { source: 'variable', variable: c.variable, variable_chemin: c.variable_chemin, operator: c.operator, value: c.value }
: { source: 'objet', definition_id: parseInt(c.definition_id, 10), row_id: parseInt(c.row_id, 10) || null,
field: c.field, field_type: c.field_type, operator: c.operator, value: c.value };
}));
payload.cond_clauses = JSON.stringify(allClauses);
payload.cond_combinator = document.getElementById('nf-cond-combinator').value;
}
} else if (currentNodeFormType === 'action') {
var actionType = document.getElementById('nf-action-type').value;
payload.action_type = actionType;
if (actionType === 'aller_a' || actionType === 'ouvrir_ligne') {
payload.target_screen_id = parseInt(document.getElementById('nf-action-target-screen').value, 10);
} else if (actionType === 'activer_onglet') {
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
} else if (actionType === 'modifier_element') {
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
var prop = document.getElementById('nf-action-element-property').value;
payload.element_property = prop;
var isColorProp = prop === 'bg_color' || prop === 'border_color';
var toggleOn = prop !== 'visibilite' && prop !== 'surbrillance' && prop !== 'desactive' && prop !== 'orientation' && document.getElementById('nf-action-element-toggle').checked;
if (prop === 'visibilite') {
payload.element_value = document.getElementById('nf-action-element-value-visibility').value;
} else if (prop === 'surbrillance') {
payload.element_value = document.getElementById('nf-action-element-value-surbrillance').value;
} else if (prop === 'desactive') {
payload.element_value = document.getElementById('nf-action-element-value-desactive').value;
} else if (prop === 'orientation') {
payload.element_value = document.getElementById('nf-action-element-value-orientation').value;
} else if (prop === 'contenu') {
payload.element_value = document.getElementById('nf-action-element-value-text').value;
} else {
var v1 = isColorProp
? document.getElementById('nf-action-element-value-color').value
: document.getElementById('nf-action-element-value-number').value;
if (toggleOn) {
var v2 = isColorProp
? document.getElementById('nf-action-element-value2-color').value
: document.getElementById('nf-action-element-value2-number').value;
payload.element_value = v1 + '||' + v2;
} else {
payload.element_value = v1;
}
}
} else if (actionType === 'modifier_objet_scene') {
// Fondations multi-éditeurs — équivalent scène de "modifier_element"
// ci-dessus, palette réduite (voir OBJECT_ACTION_PROPERTIES) : pas
// de va-et-vient/couleur, juste visibilité/position en pixels/
// orientation. FLOW_TARGET_ID_FIELD vaut "target_object_id" ici
// (voir tête de fichier).
payload[FLOW_TARGET_ID_FIELD] = parseInt(document.getElementById('nf-action-target-object').value, 10);
var objProp = document.getElementById('nf-action-object-property').value;
payload.element_property = objProp;
if (objProp === 'visibilite') {
payload.element_value = document.getElementById('nf-action-object-value-visibility').value;
} else if (objProp === 'orientation') {
payload.element_value = document.getElementById('nf-action-object-value-orientation').value;
} else {
payload.element_value = document.getElementById('nf-action-object-value-number').value;
}
} else if (actionType === 'modifier_donnee') {
payload.target_definition_id = parseInt(document.getElementById('nf-action-data-definition').value, 10);
payload.target_row_id = parseInt(document.getElementById('nf-action-data-row').value, 10) || null;
payload.target_field = document.getElementById('nf-action-data-field').value;
payload.data_operation = document.getElementById('nf-action-data-operation').value;
payload.data_value = document.getElementById('nf-action-data-value').value;
} else if (actionType === 'modifier_variable') {
payload.target_variable = document.getElementById('nf-action-variable-name').value;
payload.data_operation = document.getElementById('nf-action-variable-operation').value;
payload.data_value = document.getElementById('nf-action-variable-value').value;
} else if (actionType === 'modifier_score') {
// Score/progression (voir db/scoring/) : un seul score par joueur,
// pas de nom à transmettre (contrairement à "modifier_variable").
payload.data_operation = document.getElementById('nf-action-score-operation').value;
payload.data_value = document.getElementById('nf-action-score-value').value;
} else if (actionType === 'definir_statut_partie') {
payload.data_value = document.getElementById('nf-action-status-value').value;
} else if (actionType === 'attendre') {
payload.data_value = document.getElementById('nf-action-wait-seconds').value;
} else if (actionType === 'declencher_evenement') {
// Notification pure : aucun élément/ligne à transmettre (voir
// screens/custom_events/) — c'est à l'écouteur de décider quoi
// faire, avec ses propres réglages habituels.
payload.target_custom_event_id = parseInt(document.getElementById('nf-action-custom-event').value, 10);
} else if (actionType === 'ajouter_ligne') {
// Phase 4 — crée une ligne VIDE ; ses champs se renseignent via un ou
// plusieurs nœuds "Modifier une donnée" enchaînés, ciblant
// FLOW_LAST_INSERTED_ROW_ID ("➕ Dernière ligne ajoutée").
payload.target_definition_id = parseInt(document.getElementById('nf-action-add-row-definition').value, 10);
} else if (actionType === 'jouer_son') {
// Phase 6 — réutilise data_value (déjà un simple champ texte générique
// sur le nœud action, comme pour "attendre") plutôt qu'une nouvelle
// colonne dédiée.
payload.data_value = document.getElementById('nf-action-sound-url').value;
} else if (actionType === 'jouer_animation_sprite') {
// Phase 8 — stocke le NOM de l'animation choisie (miniature cliquée,
// voir onSpriteTargetElementChange()), PAS ses frames : résolues à
// l'exécution depuis le personnage ACTUELLEMENT assigné à la cible
// (voir resolveSpriteFrames() dans static/js/play/actions.js) —
// sinon changer le personnage Forge de l'élément dans l'éditeur
// n'avait aucun effet sur les animations déjà posées dans la
// logique, qui continuaient à jouer les frames de l'ancien
// personnage indéfiniment (bug signalé par l'utilisateur).
var elementId = document.getElementById('nf-action-sprite-target-element').value;
payload[FLOW_TARGET_ID_FIELD] = parseInt(elementId, 10);
payload.data_value = JSON.stringify({
animation: document.getElementById('nf-action-sprite-animation-name').value,
fps: parseInt(document.getElementById('nf-action-sprite-fps').value, 10) || 8,
loop: document.getElementById('nf-action-sprite-loop').checked,
});
}
}
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/nodes/add'), {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
}).then(function(r){ return r.json(); }).then(function(node){
ALL_FLOW.nodes.push(node);
closeNodeForm();
renderFlow();
});
}