Deuxième et dernier commit de la Phase A du plan "Fondations d'une
plateforme multi-éditeurs" (le premier, efa7d1d, posait le schéma et le
CRUD des objets de scène). Ce commit branche l'éditeur et le mode
jouable sur ces fondations, en réutilisant TEL QUEL tout ce qui est déjà
générique côté moteur de logique.
Éditeur (routes/scenes/, templates/scene_edit.html,
static/js/scenes/scene-editor.js) :
- routes/screens/screen_edit.py délègue à render_scene_edit() dès que
game_type == "jeu_2d" — même endpoint Flask "screen_edit" pour les
deux éditeurs, aucune route dupliquée.
- Nouveau template scene_edit.html : canevas à taille FIXE en pixels
(scene_width/scene_height) avec glisser-déposer/redimensionnement en
pixels (scene-editor.js, mirror pixel de tree-panels.js), galerie
personnages/décors. Les onglets "Blocs de logique"/"Timeline
d'animation"/"Événements" réutilisent tels quels flow-editor.js,
tabs-and-blocks.js et animation-timeline.js.
- flow-editor.js : nouveau flag FLOW_TARGETS_OBJECTS (false par défaut,
true dans scene_edit.html) qui fait écrire submitNodeForm() vers
trigger_object_id/target_object_id au lieu de trigger_element_id/
target_element_id, plus une branche "Modifier un objet de scène"
(position px, visibilité, orientation) et des gardes null partout
(le DOM de scene_edit.html n'a pas tous les champs de l'éditeur
document).
- blocks_view.py étend l'ensemble d'ids d'éléments concernés par un
bloc pour inclure aussi trigger_object_id/target_object_id.
Runtime jouable (templates/play.html, static/js/play/actions.js,
screens/payload/full_game_payload.py) :
- full_game_payload() construit les écrans d'un jeu jeu_2d à partir de
_scene_objects (render_scene_object) au lieu de _screen_elements, et
récupère les animations de personnage sur les objets de scène.
- play.html : #playFrame occupe tout le viewport pour une scène jeu_2d
(pas de ratio fluide) ; la scène se centre via .playScreen.playScene,
qui réutilise tel quel showScreen() (déjà indexé par id d'écran, pas
par forme DOM) — aucun fichier "scenes.js" séparé n'a été nécessaire.
- actions.js : jouer_animation_sprite retombe sur target_object_id si
target_element_id est absent ; nouvelle action modifier_objet_scene
avec applyObjectProperty() (positions en px, contrairement aux % de
applyElementProperty()).
Un bug de gabarit a été découvert et corrigé pendant la vérification :
le commentaire CSS de play.html contenait littéralement "{% for %}"
comme texte français, que Jinja interprétait comme une vraie balise et
faisait planter le rendu — reformulé. render_scene_object() n'était en
outre jamais appelé par la vue de l'éditeur (les objets de scène
s'affichaient sans image) — scene_edit_view.py attache maintenant
rendered_html à chaque objet avant de les passer au template.
Nouveaux tests (tests/test_scene_edit_view.py, 10 cas) : dispatch
document vs jeu_2d, rendu d'un personnage sélectionné, CRUD géométrie/
suppression d'objet, nœuds de flow ciblant un objet de scène (action et
condition de collision), payload et page /play pour un jeu jeu_2d,
présence des nouveaux helpers JS. Suite complète : 309 tests passent
(299 existants + 10 nouveaux, aucune régression). node --check et
node --test (13 tests JS) passent également.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
844 lines
47 KiB
JavaScript
844 lines
47 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;
|
||
// 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';
|
||
}
|
||
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 === '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.
|
||
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 objectFields = document.getElementById('nf-action-object-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';
|
||
// 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 (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');
|
||
}
|
||
|
||
// 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;
|
||
};
|
||
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[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 === '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();
|
||
});
|
||
}
|
||
|