Corrige un bug moteur : plusieurs déclencheurs "Au clic" (ou survol) sur le même élément n'exécutaient que le premier
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2c59e54556
commit
a7a315cce7
+47
-31
@@ -265,28 +265,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trouve, pour un élément et un type de déclencheur ("clic"/"soumission"),
|
// 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
|
// TOUS les nœuds Déclencheur qui lui sont rattachés — ce sont les points
|
||||||
// graphe à exécuter. On cherche dans TOUS les écrans (pas seulement
|
// d'entrée des graphes à exécuter. Renvoie un TABLEAU (jamais un seul
|
||||||
// l'écran courant) : bindClicks() attache les gestionnaires une seule
|
// nœud) : rien n'empêche de poser plusieurs déclencheurs indépendants
|
||||||
// fois, sur tous les éléments de tous les écrans (déjà tous présents dans
|
// "Au clic" sur le MÊME élément (ex. l'un ouvre une ligne de Répéteur,
|
||||||
// le DOM, seuls les écrans non actifs sont masqués) — chaque identifiant
|
// l'autre éteint une surbrillance) — un seul clic doit alors exécuter
|
||||||
// d'élément est unique dans tout le jeu, donc pas d'ambiguïté possible.
|
// CHACUN de ces graphes, pas seulement le premier trouvé (régression
|
||||||
// Cette recherche globale couvre aussi les écrans-MODÈLES (un élément de
|
// vécue : un second déclencheur posé sur un élément qui en avait déjà un
|
||||||
// jeu réutilisable, ex. "mail content", posé sur une scène) : un
|
// restait silencieusement inerte, aucune erreur, juste jamais exécuté).
|
||||||
// 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
|
// On cherche dans TOUS les écrans (pas seulement l'écran courant) :
|
||||||
// utilisé ailleurs — d'où le besoin de renvoyer aussi l'écran d'origine du
|
// bindClicks() attache les gestionnaires une seule fois, sur tous les
|
||||||
// nœud (screenIdKey), pas seulement le nœud : runFlowFrom() doit exécuter
|
// éléments de tous les écrans (déjà tous présents dans le DOM, seuls les
|
||||||
// le graphe dans CET écran-là (celui du modèle), jamais dans celui
|
// écrans non actifs sont masqués) — chaque identifiant d'élément est
|
||||||
// réellement affiché, sans quoi le nœud trouvé n'existerait pas dans le
|
// unique dans tout le jeu, donc pas d'ambiguïté possible. Cette recherche
|
||||||
// graphe utilisé et l'action ne se déclencherait jamais.
|
// globale couvre aussi les écrans-MODÈLES (un élément de jeu réutilisable,
|
||||||
function findTriggerNode(elementId, event) {
|
// 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) {
|
for (const screenIdKey in gameData.flows) {
|
||||||
const flow = gameData.flows[screenIdKey];
|
gameData.flows[screenIdKey].nodes.forEach(function(n) {
|
||||||
const found = flow.nodes.find(n => n.node_type === 'trigger' && n.trigger_element_id === elementId && (n.trigger_event || 'clic') === event);
|
if (n.node_type === 'trigger' && n.trigger_element_id === elementId && (n.trigger_event || 'clic') === event) {
|
||||||
if (found) return { node: found, screenId: screenIdKey };
|
found.push({ node: n, screenId: screenIdKey });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ré-attache les gestionnaires de clic sur chaque élément — appelé au
|
// Ré-attache les gestionnaires de clic sur chaque élément — appelé au
|
||||||
@@ -313,8 +324,8 @@
|
|||||||
function bindClicks() {
|
function bindClicks() {
|
||||||
document.querySelectorAll('[data-element-id]').forEach(elDiv => {
|
document.querySelectorAll('[data-element-id]').forEach(elDiv => {
|
||||||
const elementId = parseInt(elDiv.dataset.elementId, 10);
|
const elementId = parseInt(elDiv.dataset.elementId, 10);
|
||||||
const trigger = findTriggerNode(elementId, 'clic');
|
const triggers = findTriggerNodess(elementId, 'clic');
|
||||||
if (trigger) {
|
if (triggers.length) {
|
||||||
// Garde-fou anti-doublon : refreshRuntimeData() rappelle bindClicks()
|
// Garde-fou anti-doublon : refreshRuntimeData() rappelle bindClicks()
|
||||||
// après chaque changement de donnée, mais un élément qui n'est ni un
|
// 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
|
// 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');
|
const rowEl = ev.target.closest('.repeaterItem');
|
||||||
window.lastClickedRowId = rowEl ? parseInt(rowEl.dataset.rowId, 10) : null;
|
window.lastClickedRowId = rowEl ? parseInt(rowEl.dataset.rowId, 10) : null;
|
||||||
window.lastClickedDefinitionId = elDiv.dataset.definitionId ? parseInt(elDiv.dataset.definitionId, 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 => {
|
document.querySelectorAll('[data-element-id]').forEach(elDiv => {
|
||||||
if (elDiv.dataset.hoverTriggerBound) return;
|
if (elDiv.dataset.hoverTriggerBound) return;
|
||||||
const elementId = parseInt(elDiv.dataset.elementId, 10);
|
const elementId = parseInt(elDiv.dataset.elementId, 10);
|
||||||
const enterTrigger = findTriggerNode(elementId, 'survol');
|
const enterTriggers = findTriggerNodess(elementId, 'survol');
|
||||||
const leaveTrigger = findTriggerNode(elementId, 'fin_survol');
|
const leaveTriggers = findTriggerNodess(elementId, 'fin_survol');
|
||||||
if (!enterTrigger && !leaveTrigger) return;
|
if (!enterTriggers.length && !leaveTriggers.length) return;
|
||||||
elDiv.dataset.hoverTriggerBound = '1';
|
elDiv.dataset.hoverTriggerBound = '1';
|
||||||
if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.node.id, enterTrigger.screenId));
|
// Exécute CHAQUE déclencheur trouvé, pas seulement le premier — voir
|
||||||
if (leaveTrigger) elDiv.addEventListener('mouseleave', () => runFlowFrom(leaveTrigger.node.id, leaveTrigger.screenId));
|
// 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
|
// 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.
|
// 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
|
// 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
|
// 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
|
// é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
|
// cherche dans tous les écrans) mais son graphe n'existait jamais dans
|
||||||
// l'écran réellement affiché : rien ne se déclenchait, silencieusement.
|
// l'écran réellement affiché : rien ne se déclenchait, silencieusement.
|
||||||
function runFlowFrom(nodeId, flowScreenId) {
|
function runFlowFrom(nodeId, flowScreenId) {
|
||||||
@@ -1018,7 +1034,7 @@
|
|||||||
// Déclenche un événement personnalisé (voir screens/custom_events/,
|
// Déclenche un événement personnalisé (voir screens/custom_events/,
|
||||||
// action "declencher_evenement") : une pure NOTIFICATION, sans aucun
|
// action "declencher_evenement") : une pure NOTIFICATION, sans aucun
|
||||||
// paramètre — scan GLOBAL de gameData.flows, même principe que
|
// 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
|
// é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é.
|
// 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
|
// C'est à CHAQUE écouteur de décider quoi faire ensuite (via sa propre
|
||||||
|
|||||||
Reference in New Issue
Block a user