From a7a315cce7a11e23670aaa2b6c874b7bb240cee9 Mon Sep 17 00:00:00 2001 From: william Date: Sun, 30 Aug 2026 10:26:37 +0200 Subject: [PATCH] =?UTF-8?q?Corrige=20un=20bug=20moteur=20:=20plusieurs=20d?= =?UTF-8?q?=C3=A9clencheurs=20"Au=20clic"=20(ou=20survol)=20sur=20le=20m?= =?UTF-8?q?=C3=AAme=20=C3=A9l=C3=A9ment=20n'ex=C3=A9cutaient=20que=20le=20?= =?UTF-8?q?premier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnostic effectué directement sur projects/test/game.db de l'utilisateur (suite à son signalement "je ne parviens pas à mettre fin à la surbrillance") : deux nœuds Déclencheur distincts ("Au clic", id=4 et id=42) référençaient le même trigger_element_id=65. La fonction findTriggerNode() utilisait Array.find(), qui ne retourne que la PREMIÈRE correspondance — le second nœud (celui qui devait couper la surbrillance) n'était donc jamais exécuté, silencieusement, quel que soit le graphe construit dans l'éditeur. Ce n'est pas un bug lié aux événements personnalisés ni aux conditions par variable (deux pistes explorées avant ce diagnostic) : c'est une limitation générale du moteur, qui n'a jamais géré plus d'un déclencheur "Au clic"/"Au survol"/"À la fin du survol" par élément. Renomme findTriggerNode() en findTriggerNodes() (pluriel) : retourne désormais TOUS les nœuds correspondants (élément + type d'événement), et bindClicks()/bindHoverTriggers() exécutent chaque flow trouvé au lieu de s'arrêter au premier. Vérifié : 208 tests passent, syntaxe JS validée (script de play.html rendu via le client de test puis node --check). Comme pour le reste du graphe de logique côté client, ce changement n'est pas couvert par les tests automatisés (pas de harnais navigateur/DOM) — vérification manuelle recommandée sur le scénario réel de l'utilisateur. Co-Authored-By: Claude Sonnet 5 --- templates/play.html | 78 +++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/templates/play.html b/templates/play.html index a0509424..4a97faf8 100644 --- a/templates/play.html +++ b/templates/play.html @@ -265,28 +265,39 @@ } // Trouve, pour un élément et un type de déclencheur ("clic"/"soumission"), - // le nœud Déclencheur qui lui est rattaché — c'est le point d'entrée du - // graphe à exécuter. On cherche dans TOUS les écrans (pas seulement - // l'écran courant) : bindClicks() attache les gestionnaires une seule - // fois, sur tous les éléments de tous les écrans (déjà tous présents dans - // le DOM, seuls les écrans non actifs sont masqués) — chaque identifiant - // d'élément est unique dans tout le jeu, donc pas d'ambiguïté possible. - // Cette recherche globale couvre aussi les écrans-MODÈLES (un élément de - // jeu réutilisable, ex. "mail content", posé sur une scène) : un - // déclencheur "Au clic"/"Au survol" posé dans l'éditeur du modèle, sur un - // de SES PROPRES enfants, est donc retrouvé même quand ce modèle est - // utilisé ailleurs — d'où le besoin de renvoyer aussi l'écran d'origine du - // nœud (screenIdKey), pas seulement le nœud : runFlowFrom() doit exécuter - // le graphe dans CET écran-là (celui du modèle), jamais dans celui - // réellement affiché, sans quoi le nœud trouvé n'existerait pas dans le - // graphe utilisé et l'action ne se déclencherait jamais. - function findTriggerNode(elementId, event) { + // TOUS les nœuds Déclencheur qui lui sont rattachés — ce sont les points + // d'entrée des graphes à exécuter. Renvoie un TABLEAU (jamais un seul + // nœud) : rien n'empêche de poser plusieurs déclencheurs indépendants + // "Au clic" sur le MÊME élément (ex. l'un ouvre une ligne de Répéteur, + // l'autre éteint une surbrillance) — un seul clic doit alors exécuter + // CHACUN de ces graphes, pas seulement le premier trouvé (régression + // vécue : un second déclencheur posé sur un élément qui en avait déjà un + // restait silencieusement inerte, aucune erreur, juste jamais exécuté). + // + // On cherche dans TOUS les écrans (pas seulement l'écran courant) : + // bindClicks() attache les gestionnaires une seule fois, sur tous les + // éléments de tous les écrans (déjà tous présents dans le DOM, seuls les + // écrans non actifs sont masqués) — chaque identifiant d'élément est + // unique dans tout le jeu, donc pas d'ambiguïté possible. Cette recherche + // globale couvre aussi les écrans-MODÈLES (un élément de jeu réutilisable, + // ex. "mail content", posé sur une scène) : un déclencheur "Au clic"/"Au + // survol" posé dans l'éditeur du modèle, sur un de SES PROPRES enfants, + // est donc retrouvé même quand ce modèle est utilisé ailleurs — d'où le + // besoin de renvoyer aussi l'écran d'origine de CHAQUE nœud (screenIdKey), + // pas seulement le nœud : runFlowFrom() doit exécuter le graphe dans CET + // écran-là (celui du modèle), jamais dans celui réellement affiché, sans + // quoi le nœud trouvé n'existerait pas dans le graphe utilisé et l'action + // ne se déclencherait jamais. + function findTriggerNodess(elementId, event) { + const found = []; for (const screenIdKey in gameData.flows) { - const flow = gameData.flows[screenIdKey]; - const found = flow.nodes.find(n => n.node_type === 'trigger' && n.trigger_element_id === elementId && (n.trigger_event || 'clic') === event); - if (found) return { node: found, screenId: screenIdKey }; + gameData.flows[screenIdKey].nodes.forEach(function(n) { + if (n.node_type === 'trigger' && n.trigger_element_id === elementId && (n.trigger_event || 'clic') === event) { + found.push({ node: n, screenId: screenIdKey }); + } + }); } - return null; + return found; } // Ré-attache les gestionnaires de clic sur chaque élément — appelé au @@ -313,8 +324,8 @@ function bindClicks() { document.querySelectorAll('[data-element-id]').forEach(elDiv => { const elementId = parseInt(elDiv.dataset.elementId, 10); - const trigger = findTriggerNode(elementId, 'clic'); - if (trigger) { + const triggers = findTriggerNodess(elementId, 'clic'); + if (triggers.length) { // Garde-fou anti-doublon : refreshRuntimeData() rappelle bindClicks() // après chaque changement de donnée, mais un élément qui n'est ni un // Répéteur ni une Jauge garde le MÊME nœud DOM d'un rafraîchissement @@ -337,7 +348,9 @@ const rowEl = ev.target.closest('.repeaterItem'); window.lastClickedRowId = rowEl ? parseInt(rowEl.dataset.rowId, 10) : null; window.lastClickedDefinitionId = elDiv.dataset.definitionId ? parseInt(elDiv.dataset.definitionId, 10) : null; - runFlowFrom(trigger.node.id, trigger.screenId); + // Exécute CHAQUE déclencheur "Au clic" posé sur cet élément, pas + // seulement le premier (voir le commentaire de findTriggerNodess). + triggers.forEach(trigger => runFlowFrom(trigger.node.id, trigger.screenId)); }); } }); @@ -354,12 +367,15 @@ document.querySelectorAll('[data-element-id]').forEach(elDiv => { if (elDiv.dataset.hoverTriggerBound) return; const elementId = parseInt(elDiv.dataset.elementId, 10); - const enterTrigger = findTriggerNode(elementId, 'survol'); - const leaveTrigger = findTriggerNode(elementId, 'fin_survol'); - if (!enterTrigger && !leaveTrigger) return; + const enterTriggers = findTriggerNodess(elementId, 'survol'); + const leaveTriggers = findTriggerNodess(elementId, 'fin_survol'); + if (!enterTriggers.length && !leaveTriggers.length) return; elDiv.dataset.hoverTriggerBound = '1'; - if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.node.id, enterTrigger.screenId)); - if (leaveTrigger) elDiv.addEventListener('mouseleave', () => runFlowFrom(leaveTrigger.node.id, leaveTrigger.screenId)); + // Exécute CHAQUE déclencheur trouvé, pas seulement le premier — voir + // le commentaire de findTriggerNodess (plusieurs déclencheurs + // indépendants sur le même élément/événement doivent tous s'exécuter). + if (enterTriggers.length) elDiv.addEventListener('mouseenter', () => enterTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId))); + if (leaveTriggers.length) elDiv.addEventListener('mouseleave', () => leaveTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId))); }); } @@ -982,10 +998,10 @@ // continue sur son (ou ses) fil de sortie ; chaque nœud Condition évalue // une donnée du jeu et suit le(s) fil(s) "Vrai" ou "Faux" en conséquence. // flowScreenId : l'écran PROPRIÉTAIRE du graphe à exécuter — par défaut - // l'écran affiché, mais peut être un écran-MODÈLE (voir findTriggerNode) + // l'écran affiché, mais peut être un écran-MODÈLE (voir findTriggerNodes) // quand le nœud de départ vient d'un déclencheur posé dans l'éditeur d'un // élément de jeu réutilisable, sur un de ses propres enfants. Sans ce - // paramètre, un tel déclencheur était bien retrouvé (findTriggerNode + // paramètre, un tel déclencheur était bien retrouvé (findTriggerNodes // cherche dans tous les écrans) mais son graphe n'existait jamais dans // l'écran réellement affiché : rien ne se déclenchait, silencieusement. function runFlowFrom(nodeId, flowScreenId) { @@ -1018,7 +1034,7 @@ // Déclenche un événement personnalisé (voir screens/custom_events/, // action "declencher_evenement") : une pure NOTIFICATION, sans aucun // paramètre — scan GLOBAL de gameData.flows, même principe que - // findTriggerNode()/runScreenShowTriggers() ci-dessus, puisqu'un + // findTriggerNodes()/runScreenShowTriggers() ci-dessus, puisqu'un // écouteur (nœud Déclencheur trigger_event="evenement") peut vivre sur // N'IMPORTE QUELLE autre scène ou modèle, pas seulement l'écran affiché. // C'est à CHAQUE écouteur de décider quoi faire ensuite (via sa propre