Ajoute le positionnement absolu (pos_x/pos_y, réutilise left/top en % déjà en place) et relatif (pos_x_relatif/pos_y_relatif, ajoute un delta à la position actuelle plutôt que de l'écraser) comme nouvelles propriétés de l'action "Modifier un élément". Ajoute une nouvelle source de condition "collision" (aux côtés de "objet"/"variable") : deux éléments (cond_element_a/cond_element_b, ALTER TABLE sans contrainte FK, même patron que block_id/ trigger_custom_event_id) dont on compare les rectangles à l'écran via getBoundingClientRect() côté client (elementsOverlap(), dans conditions.js). Pas d'opérateur/valeur à choisir : le chevauchement EST directement le booléen vrai/faux du nœud — le créateur relie le port "Faux" pour "ne se touchent pas", exactement comme pour n'importe quelle autre condition (design plus simple que réinterpréter égal/différent, qui n'a pas de sens pour superieur/inferieur). delete_element.py et flow_nodes_referencing_element.py nettoient désormais aussi les nœuds de collision référençant un élément supprimé (ou l'un de ses descendants), pour rester cohérents avec le nettoyage déjà en place pour trigger_element_id/target_element_id. Combiné à la Phase 3 (minuteur récurrent) et au déplacement au clavier, ça couvre des jeux type casse-briques/Pong/ramasse-objets sans construire un vrai moteur physique (pas de vélocité/accélération/ gravité continues, cadrage volontairement limité). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
227 lines
11 KiB
JavaScript
227 lines
11 KiB
JavaScript
// ---------- Exécution des actions ----------
|
||
// Extrait de templates/play.html (voir plan de modularisation) :
|
||
// application d'une propriété d'élément ("Modifier un élément") et
|
||
// exécution de chaque type de nœud Action. Les URLs de routes serveur
|
||
// (flow_node_run_data/run_variable) sont injectées par play.html dans
|
||
// window.FORGE_PLAY_URLS (données Jinja, jamais codées en dur ici).
|
||
|
||
// Propriété CSS réelle + éventuel suffixe (unité) pour chaque propriété
|
||
// pilotable par une action "Modifier un élément".
|
||
var ELEMENT_PROPERTY_CSS = {
|
||
bg_color: { css: 'backgroundColor', suffix: '' },
|
||
border_color: { css: 'borderColor', suffix: '' },
|
||
width: { css: 'width', suffix: '%' },
|
||
height: { css: 'height', suffix: '%' },
|
||
// Phase 5 (moteur) — les éléments sont DÉJÀ positionnés en absolu avec
|
||
// left/top en % (voir filters/element_style_filter.py) : piloter ces
|
||
// deux propriétés est donc cohérent avec l'existant, pas une nouvelle
|
||
// mécanique de positionnement.
|
||
pos_x: { css: 'left', suffix: '%' },
|
||
pos_y: { css: 'top', suffix: '%' },
|
||
};
|
||
|
||
function applyElementProperty(targetEl, property, value) {
|
||
if (property === 'contenu') {
|
||
// Voir ELEMENT_ACTION_PROPERTIES ("contenu") : texte littéral, pas de
|
||
// binding vers un champ d'objet ici (contrairement à
|
||
// applyOpenRowBindings, propre à "Ouvrir la ligne cliquée"). Comme
|
||
// pour les autres réglages ci-dessous, cible n'importe quel élément —
|
||
// pointer ceci vers un conteneur remplacerait tout son contenu
|
||
// imbriqué, à éviter.
|
||
targetEl.textContent = value == null ? '' : value;
|
||
return;
|
||
}
|
||
if (property === 'surbrillance') {
|
||
// 3.3 (Confort) — surbrillance générique dynamique : un liseré qui
|
||
// clignote (voir la classe .forgeHighlight et son animation dans le
|
||
// <style> de play.html), applicable à N'IMPORTE QUEL élément sans
|
||
// poser de bordure colorée togglée à la main — utile pour un mentor
|
||
// qui attire l'attention du joueur vers l'élément à toucher ensuite.
|
||
if (value === 'toggle') {
|
||
targetEl.classList.toggle('forgeHighlight');
|
||
} else if (value === 'off') {
|
||
targetEl.classList.remove('forgeHighlight');
|
||
} else {
|
||
targetEl.classList.add('forgeHighlight');
|
||
}
|
||
return;
|
||
}
|
||
if (property === 'desactive') {
|
||
// 3.5 (Confort) — verrouillage d'un élément après décision : DISTINCT
|
||
// de "invisible" (l'élément reste visible, mais grisé et inerte —
|
||
// pointer-events:none bloque aussi le clic, donc le déclencheur "Au
|
||
// clic" éventuellement posé sur cet élément ne se redéclenche plus).
|
||
if (value === 'toggle') {
|
||
targetEl.classList.toggle('forgeDisabled');
|
||
} else if (value === 'off') {
|
||
targetEl.classList.remove('forgeDisabled');
|
||
} else {
|
||
targetEl.classList.add('forgeDisabled');
|
||
}
|
||
return;
|
||
}
|
||
if (property === 'visibilite') {
|
||
if (value === 'toggle') {
|
||
const hidden = targetEl.style.display === 'none';
|
||
targetEl.style.display = hidden ? '' : 'none';
|
||
targetEl.style.visibility = '';
|
||
} else if (value === 'masque') {
|
||
targetEl.style.display = 'none';
|
||
} else if (value === 'invisible') {
|
||
targetEl.style.display = '';
|
||
targetEl.style.visibility = 'hidden';
|
||
} else {
|
||
targetEl.style.display = '';
|
||
targetEl.style.visibility = '';
|
||
}
|
||
return;
|
||
}
|
||
if (property === 'pos_x_relatif' || property === 'pos_y_relatif') {
|
||
// Phase 5 — AJOUTE la valeur à la position actuelle plutôt que de
|
||
// l'écraser (contrairement à pos_x/pos_y, "absolus") : combiné à un
|
||
// déclencheur clavier (Phase 3), ça permet un personnage/objet qui se
|
||
// déplace pas à pas à chaque appui de touche plutôt que de sauter
|
||
// toujours au même endroit.
|
||
const relSpec = ELEMENT_PROPERTY_CSS[property === 'pos_x_relatif' ? 'pos_x' : 'pos_y'];
|
||
const delta = parseFloat(value);
|
||
if (isNaN(delta)) return;
|
||
const current = parseFloat(targetEl.style[relSpec.css]) || 0;
|
||
targetEl.style[relSpec.css] = (current + delta) + relSpec.suffix;
|
||
return;
|
||
}
|
||
|
||
const spec = ELEMENT_PROPERTY_CSS[property];
|
||
if (!spec || value === undefined || value === null) return;
|
||
|
||
// "valeur1||valeur2" : effet va-et-vient — un état par élément et par
|
||
// propriété, mémorisé directement sur le nœud DOM (data-toggle-<prop>) :
|
||
// un clic passe à la 2e valeur, le clic suivant revient à la 1re, et
|
||
// ainsi de suite. Ce n'est PAS enregistré en base : l'état repart de la
|
||
// 1re valeur si l'écran est rechargé.
|
||
if (typeof value === 'string' && value.indexOf('||') !== -1) {
|
||
const parts = value.split('||');
|
||
const key = 'toggle' + property;
|
||
const atSecond = targetEl.dataset[key] === '1';
|
||
const next = atSecond ? parts[0] : parts[1];
|
||
targetEl.style[spec.css] = next + spec.suffix;
|
||
targetEl.dataset[key] = atSecond ? '0' : '1';
|
||
return;
|
||
}
|
||
|
||
targetEl.style[spec.css] = value + spec.suffix;
|
||
}
|
||
|
||
function runActionNode(node) {
|
||
if (node.action_type === 'ecran_suivant') {
|
||
const idx = screenIndexById(window.currentScreenId);
|
||
if (idx > -1 && idx < screensData.length - 1) showScreen(screensData[idx + 1].id);
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'ecran_precedent') {
|
||
const idx = screenIndexById(window.currentScreenId);
|
||
if (idx > 0) showScreen(screensData[idx - 1].id);
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'aller_a' && node.target_screen_id) {
|
||
showScreen(node.target_screen_id);
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'ouvrir_ligne' && node.target_screen_id) {
|
||
// Mémorise QUELLE ligne a été cliquée juste avant d'exécuter cette
|
||
// action (voir bindClicks dans triggers.js) puis affiche l'écran de
|
||
// détail avec ses placeholders "champ" résolus contre cette ligne
|
||
// précise.
|
||
if (window.lastClickedRowId != null && window.lastClickedDefinitionId != null) {
|
||
window.currentOpenRow = { definitionId: window.lastClickedDefinitionId, rowId: window.lastClickedRowId };
|
||
}
|
||
showScreen(node.target_screen_id);
|
||
applyOpenRowBindings();
|
||
// Si l'élément "ouvert" (ou un ancêtre commun avec lui) contient AUSSI
|
||
// un Répéteur — ex. un panneau de détail posé à côté d'une liste dans
|
||
// le même conteneur — applyOpenRowBindings() vient de régénérer son
|
||
// sous-arbre, lignes du Répéteur comprises, avec des nœuds tout neufs
|
||
// SANS AUCUN écouteur de clic (voir bindClicks : le garde-fou anti-
|
||
// doublon repose sur elDiv.dataset.clickBound, absent sur un nœud
|
||
// fraîchement créé). Sans ce rappel, un clic sur "ouvrir la ligne
|
||
// cliquée" combiné à un Répéteur sur le même écran rendait TOUTES les
|
||
// lignes muettes dès le clic suivant, sans la moindre erreur.
|
||
bindClicks();
|
||
bindHoverTexts();
|
||
bindHoverTriggers();
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'modifier_element' && node.target_element_id) {
|
||
const targetEl = document.querySelector('[data-element-id="' + node.target_element_id + '"]');
|
||
if (targetEl) applyElementProperty(targetEl, node.element_property, node.element_value);
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'declencher_evenement' && node.target_custom_event_id) {
|
||
// Notification pure (voir screens/custom_events/) : ne fait que
|
||
// signaler l'événement, jamais choisir un élément — c'est à
|
||
// l'écouteur (déclencheur → condition → action) de décider quoi
|
||
// faire, avec ses propres réglages habituels.
|
||
window.dispatchGameEvent(node.target_custom_event_id);
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'activer_onglet' && node.target_element_id) {
|
||
// 2.3 — panneau à onglets / visibilité mutuellement exclusive : montre
|
||
// l'élément visé et masque tous ses FRÈRES DIRECTS (même parent_id,
|
||
// y compris "aucun parent" = les autres éléments de premier niveau de
|
||
// l'écran) en une seule action, plutôt qu'une action "Masquer" par
|
||
// frère. screenData.elements contient TOUS les éléments de l'écran
|
||
// (imbriqués compris, voir list_elements côté serveur), donc ça
|
||
// fonctionne aussi bien pour des onglets imbriqués dans un même
|
||
// conteneur que pour des "vues" posées directement sur l'écran.
|
||
const screenData = screensData.find(function(s){ return s.id === window.currentScreenId; });
|
||
const allElements = screenData ? screenData.elements : [];
|
||
const target = allElements.find(function(e){ return e.id === node.target_element_id; });
|
||
if (target) {
|
||
const parentKey = target.parent_id || null;
|
||
allElements
|
||
.filter(function(e){ return (e.parent_id || null) === parentKey; })
|
||
.forEach(function(e) {
|
||
const el = document.querySelector('[data-element-id="' + e.id + '"]');
|
||
if (!el) return;
|
||
if (e.id === target.id) { el.style.display = ''; el.style.visibility = ''; }
|
||
else { el.style.display = 'none'; }
|
||
});
|
||
}
|
||
return Promise.resolve();
|
||
} else if (node.action_type === 'attendre') {
|
||
// 3.2 (Confort) — séquences temporisées : suspend la suite du graphe
|
||
// pendant N secondes (node.data_value) avant de continuer sur le(s)
|
||
// fil(s) de sortie — permet par ex. un mail qui "arrive" tout seul
|
||
// quelques secondes après l'affichage de l'écran, sans action du
|
||
// joueur (à combiner avec le déclencheur "À l'affichage de l'écran").
|
||
const seconds = parseFloat(node.data_value);
|
||
const ms = isNaN(seconds) ? 0 : Math.max(0, seconds) * 1000;
|
||
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
||
} else if (node.action_type === 'modifier_donnee') {
|
||
const runDataUrl = FORGE_PLAY_URLS.flowNodeRunData.replace('999999', node.id);
|
||
// clicked_row_id/last_inserted_row_id : utilisés côté serveur
|
||
// SEULEMENT si ce nœud vise respectivement CLICKED_ROW_ID ou
|
||
// LAST_INSERTED_ROW_ID (voir flow_node_run_data.py) — sans effet
|
||
// sinon, donc toujours transmis sans condition ici.
|
||
return fetch(runDataUrl, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ clicked_row_id: window.lastClickedRowId, last_inserted_row_id: window.lastInsertedRowId })
|
||
})
|
||
.then(r => r.json())
|
||
.then(() => refreshRuntimeData());
|
||
} else if (node.action_type === 'modifier_variable') {
|
||
const runVarUrl = FORGE_PLAY_URLS.flowNodeRunVariable.replace('999999', node.id);
|
||
return fetch(runVarUrl, {method: 'POST'})
|
||
.then(r => r.json())
|
||
.then(() => refreshRuntimeData());
|
||
} else if (node.action_type === 'ajouter_ligne') {
|
||
// Phase 4 — crée une ligne VIDE côté serveur ; son id devient
|
||
// window.lastInsertedRowId pour que les nœuds "Modifier une donnée"
|
||
// enchaînés (ciblant LAST_INSERTED_ROW_ID, "➕ Dernière ligne
|
||
// ajoutée") sachent quelle ligne renseigner. refreshRuntimeData()
|
||
// affiche la nouvelle ligne dans tout Répéteur lié dès ce round-trip.
|
||
const runAddRowUrl = FORGE_PLAY_URLS.flowNodeRunAddRow.replace('999999', node.id);
|
||
return fetch(runAddRowUrl, { method: 'POST' })
|
||
.then(r => r.json())
|
||
.then(function(json) {
|
||
window.lastInsertedRowId = json.row_id;
|
||
return refreshRuntimeData();
|
||
});
|
||
}
|
||
// 'rien' : aucune action volontairement.
|
||
return Promise.resolve();
|
||
}
|