Fait fonctionner logique et animations d'un modèle réutilisable partout où il est posé
Jusqu'ici, la logique (déclencheurs Au clic/Au survol/Fin du survol) et
les animations posées dans l'éditeur de l'écran-MODÈLE d'un élément de
jeu réutilisable (ex. "mail content") sur SES PROPRES enfants ne
s'exécutaient jamais quand cet élément était simplement posé sur une
autre scène : findTriggerNode() cherchait bien le déclencheur dans tous
les écrans (modèles compris) mais runFlowFrom() n'exécutait ensuite le
graphe que dans l'écran RÉELLEMENT affiché — le nœud trouvé n'existait
pas dans ce graphe-là, donc rien ne se déclenchait, silencieusement.
Même limitation pour les animations, dont la timeline ne lisait que les
clips propres à l'écran affiché.
Logique (templates/play.html) :
- findTriggerNode() renvoie désormais { node, screenId } plutôt que
juste le nœud, pour transmettre l'écran D'ORIGINE du déclencheur (qui
peut être un écran-modèle).
- runFlowFrom(nodeId, flowScreenId) accepte un 2e paramètre optionnel
(par défaut l'écran affiché, comportement inchangé pour tout le
reste) pour exécuter le graphe dans le BON écran.
- bindClicks()/bindHoverTriggers() passent maintenant cet écran
d'origine à runFlowFrom(). runScreenShowTriggers() (déclencheur "À
l'affichage de l'écran") reste volontairement inchangé — hors scope,
ambiguïté sur plusieurs exemplaires d'un même modèle sur un écran.
Animations (screens/payload/full_game_payload.py, templates/play.html) :
- Le payload expose désormais element_types (element_type_id -> id de
son écran-modèle), via screens.list_element_types() déjà existant.
- collectAnimationClips(screenId) rassemble récursivement les clips de
l'écran affiché ET de tout écran-modèle utilisé par un de ses
éléments (garde anti-boucle, dédoublonnage par écran).
- applyAnimationClip() cible désormais TOUS les exemplaires d'un id
d'élément (querySelectorAll, plus querySelector) : un enfant de
modèle garde le même id à chaque exemplaire, y compris pour chaque
ligne d'un Répéteur utilisant ce modèle comme gabarit de ligne.
Limite connue, non corrigée ici (pas la demande) : une action "Modifier
un élément" ciblant un enfant de modèle reste, elle, scopée au premier
exemplaire trouvé dans le DOM (document.querySelector singulier dans
runActionNode/applyElementProperty) — sans impact pour un modèle posé
une seule fois par écran, comme dans le cas d'usage actuel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
19d3810164
commit
17c5e93d8d
+81
-29
@@ -163,33 +163,69 @@
|
||||
}
|
||||
|
||||
function applyAnimationClip(clip) {
|
||||
var target = document.querySelector('[data-anim-target="' + clip.element_id + '"]');
|
||||
if (!target) return;
|
||||
// querySelectorAll (pas querySelector) : un clip posé dans l'éditeur d'un
|
||||
// écran-MODÈLE (élément de jeu réutilisable) vise l'un de ses propres
|
||||
// enfants — un id qui se répète à l'IDENTIQUE à chaque exemplaire de ce
|
||||
// modèle (rechargé en direct depuis le même écran-modèle, voir
|
||||
// collectAnimationClips ci-dessous), y compris pour chaque ligne d'un
|
||||
// Répéteur utilisant ce modèle comme gabarit. Un seul querySelector
|
||||
// n'aurait animé que le tout premier exemplaire trouvé dans le DOM.
|
||||
var targets = document.querySelectorAll('[data-anim-target="' + clip.element_id + '"]');
|
||||
if (!targets.length) return;
|
||||
var duration = clip.duration || 1, delay = clip.delay || 0;
|
||||
var iterationCount = clip.iteration_count || '1';
|
||||
var infinite = iterationCount === 'infinite';
|
||||
if (clip.kind === 'custom') {
|
||||
target.style.animation = 'customAnim_' + clip.id + ' ' + duration + 's ' + (clip.easing || 'ease') +
|
||||
' ' + delay + 's ' + iterationCount;
|
||||
if (!infinite) {
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.style.animation = '';
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var animClass = 'animate__' + clip.animate_name;
|
||||
target.style.setProperty('--animate-duration', duration + 's');
|
||||
target.style.setProperty('--animate-delay', delay + 's');
|
||||
target.style.animationIterationCount = iterationCount;
|
||||
target.classList.add('animate__animated', animClass);
|
||||
if (!infinite) {
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.classList.remove('animate__animated', animClass);
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
targets.forEach(function(target) {
|
||||
if (clip.kind === 'custom') {
|
||||
target.style.animation = 'customAnim_' + clip.id + ' ' + duration + 's ' + (clip.easing || 'ease') +
|
||||
' ' + delay + 's ' + iterationCount;
|
||||
if (!infinite) {
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.style.animation = '';
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var animClass = 'animate__' + clip.animate_name;
|
||||
target.style.setProperty('--animate-duration', duration + 's');
|
||||
target.style.setProperty('--animate-delay', delay + 's');
|
||||
target.style.animationIterationCount = iterationCount;
|
||||
target.classList.add('animate__animated', animClass);
|
||||
if (!infinite) {
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.classList.remove('animate__animated', animClass);
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rassemble les clips propres à cet écran ET ceux de tout écran-MODÈLE
|
||||
// utilisé par un de ses éléments (élément de jeu réutilisable posé sur cet
|
||||
// écran, ex. "mail content") — récursif (un modèle peut lui-même utiliser
|
||||
// un autre modèle), avec une garde anti-boucle et un dédoublonnage par
|
||||
// écran (un même modèle posé plusieurs fois ne doit pas répéter ses
|
||||
// clips). Sans ça, une animation posée directement dans l'éditeur d'un
|
||||
// modèle ne se jouait jamais quand le modèle est utilisé ailleurs — seul
|
||||
// l'écran-modèle affiché EN TANT QU'ÉCRAN pouvait la jouer.
|
||||
function collectAnimationClips(screenId) {
|
||||
var visited = {};
|
||||
var clips = [];
|
||||
function visit(sid) {
|
||||
if (visited[sid]) return;
|
||||
visited[sid] = true;
|
||||
clips = clips.concat((gameData.animations || {})[String(sid)] || []);
|
||||
var screenData = screensData.find(function(s){ return String(s.id) === String(sid); });
|
||||
if (!screenData) return;
|
||||
screenData.elements.forEach(function(el) {
|
||||
if (!el.element_type_id) return;
|
||||
var templateScreenId = (gameData.element_types || {})[String(el.element_type_id)];
|
||||
if (templateScreenId != null) visit(templateScreenId);
|
||||
});
|
||||
}
|
||||
visit(screenId);
|
||||
return clips;
|
||||
}
|
||||
|
||||
function runAnimationTimeline(screenId) {
|
||||
@@ -200,7 +236,7 @@
|
||||
// suite sur le même écran avant la fin de sa timeline.
|
||||
pendingAnimationTimers.forEach(function(t) { clearTimeout(t); });
|
||||
pendingAnimationTimers = [];
|
||||
var clips = (gameData.animations || {})[String(screenId)] || [];
|
||||
var clips = collectAnimationClips(screenId);
|
||||
clips.forEach(function(clip) {
|
||||
var t = setTimeout(function() { applyAnimationClip(clip); }, (clip.start_time || 0) * 1000);
|
||||
pendingAnimationTimers.push(t);
|
||||
@@ -233,11 +269,20 @@
|
||||
// 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) {
|
||||
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 found;
|
||||
if (found) return { node: found, screenId: screenIdKey };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -290,7 +335,7 @@
|
||||
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.id);
|
||||
runFlowFrom(trigger.node.id, trigger.screenId);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -311,8 +356,8 @@
|
||||
const leaveTrigger = findTriggerNode(elementId, 'fin_survol');
|
||||
if (!enterTrigger && !leaveTrigger) return;
|
||||
elDiv.dataset.hoverTriggerBound = '1';
|
||||
if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.id));
|
||||
if (leaveTrigger) elDiv.addEventListener('mouseleave', () => runFlowFrom(leaveTrigger.id));
|
||||
if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.node.id, enterTrigger.screenId));
|
||||
if (leaveTrigger) elDiv.addEventListener('mouseleave', () => runFlowFrom(leaveTrigger.node.id, leaveTrigger.screenId));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -819,8 +864,15 @@
|
||||
// seulement le premier. Chaque nœud Action produit son effet puis
|
||||
// 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.
|
||||
function runFlowFrom(nodeId) {
|
||||
const flow = gameData.flows[String(window.currentScreenId)];
|
||||
// flowScreenId : l'écran PROPRIÉTAIRE du graphe à exécuter — par défaut
|
||||
// l'écran affiché, mais peut être un écran-MODÈLE (voir findTriggerNode)
|
||||
// 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
|
||||
// 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) {
|
||||
const flow = gameData.flows[String(flowScreenId != null ? flowScreenId : window.currentScreenId)];
|
||||
if (!flow) return;
|
||||
const nodesById = {};
|
||||
flow.nodes.forEach(n => { nodesById[n.id] = n; });
|
||||
|
||||
Reference in New Issue
Block a user