Bug réel identifié grâce à la vidéo fournie + inspection directe de la
base de jeu de test : quand un nœud de flow "Jouer une animation"
(ou un clip de Timeline "sprite") était configuré pour un personnage,
ses frames étaient résolues et FIGÉES dans data_value/custom_keyframes
au moment de la configuration — changer ensuite le personnage Forge de
l'élément (galerie des propriétés) n'avait donc aucun effet sur les
animations déjà posées, qui continuaient à jouer indéfiniment les
frames de l'ANCIEN personnage.
Le nœud/clip ne stocke désormais que le NOM de l'animation
({"animation": "walk", "fps": 8, "loop": true}) — ses frames sont
résolues à l'EXÉCUTION, à partir du personnage ACTUELLEMENT assigné à
l'élément cible :
- screens/payload/full_game_payload.py expose un nouveau
gameData.personnage_animations (élément → animations), reconstruit à
chaque chargement de la page de jeu depuis _personnage_data — donc
toujours à jour, y compris après un changement de personnage.
- static/js/play/actions.js (resolveSpriteFrames) et
static/js/play/screens.js (applyAnimationClip) résolvent le nom
d'animation en frames à ce moment précis, plutôt que d'utiliser des
frames figées — repli sur l'ancien format {frames,...} pour les
nœuds/clips déjà créés avant ce correctif.
- Éditeur (flow-editor.js/animation-timeline.js) : simplifié en
conséquence — plus besoin de deviner rétroactivement quelle animation
correspond à une liste de frames stockées (l'ancien hack de
comparaison), le nom est maintenant stocké directement.
Nouveau test de régression (test_swapping_forge_character_updates_
already_configured_flow_action) qui reproduit exactement le scénario
filmé : configure l'action pour "male-adventurer", change le personnage
en "zombie", vérifie que gameData.personnage_animations reflète bien
zombie sans avoir à retoucher le nœud de flow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
775 lines
42 KiB
JavaScript
775 lines
42 KiB
JavaScript
// ---------- 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;
|
||
// 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';
|
||
}
|
||
return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' +
|
||
(ELEMENTS_LABELS[node.trigger_element_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_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 === '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] || '?');
|
||
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.
|
||
var isKeyboard = eventVal === 'clavier';
|
||
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, '"') + '" 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, '"') + '" 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 dataFields = document.getElementById('nf-action-data-fields');
|
||
var variableFields = document.getElementById('nf-action-variable-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';
|
||
soundFields.style.display = typeSel.value === 'jouer_son' ? 'block' : 'none';
|
||
spriteFields.style.display = typeSel.value === 'jouer_animation_sprite' ? 'block' : 'none';
|
||
if (typeSel.value === 'jouer_animation_sprite') onSpriteTargetElementChange();
|
||
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
||
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
||
dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
||
variableFields.style.display = typeSel.value === 'modifier_variable' ? 'block' : 'none';
|
||
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';
|
||
document.getElementById('nf-action-onglet-hint').style.display = isOnglet ? 'block' : 'none';
|
||
document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block';
|
||
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
|
||
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');
|
||
}
|
||
|
||
// "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;
|
||
};
|
||
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_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.trigger_element_id = 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_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 === '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.target_element_id = 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();
|
||
});
|
||
}
|
||
|