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
@@ -5,6 +5,7 @@ from ..elements.list_elements import list_elements
|
|||||||
from ..flow.list_flow_nodes import list_flow_nodes
|
from ..flow.list_flow_nodes import list_flow_nodes
|
||||||
from ..flow.list_flow_edges import list_flow_edges
|
from ..flow.list_flow_edges import list_flow_edges
|
||||||
from ..animations.list_animation_clips import list_animation_clips
|
from ..animations.list_animation_clips import list_animation_clips
|
||||||
|
from ..element_types.list_element_types import list_element_types
|
||||||
|
|
||||||
|
|
||||||
def full_game_payload(slug):
|
def full_game_payload(slug):
|
||||||
@@ -42,4 +43,16 @@ def full_game_payload(slug):
|
|||||||
]
|
]
|
||||||
fields_meta[str(d["id"])] = [{"name": f["name"], "type": f["type"]} for f in full["fields"]]
|
fields_meta[str(d["id"])] = [{"name": f["name"], "type": f["type"]} for f in full["fields"]]
|
||||||
|
|
||||||
return {"screens": payload_screens, "flows": flows, "animations": animations, "data": data, "fields_meta": fields_meta}
|
# element_type_id -> id de son écran-modèle : nécessaire côté client (voir
|
||||||
|
# collectAnimationClips() dans play.html) pour savoir, quand un élément de
|
||||||
|
# jeu réutilisable (ex. "mail content") est posé sur un écran, quels
|
||||||
|
# autres clips d'animation (ceux de SON PROPRE écran-modèle) doivent
|
||||||
|
# aussi être joués à l'affichage de cet écran — sans quoi une animation
|
||||||
|
# posée directement dans l'éditeur du modèle ne se jouait jamais quand le
|
||||||
|
# modèle est utilisé ailleurs.
|
||||||
|
element_types = {str(t["id"]): t["template_screen_id"] for t in list_element_types(slug)}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"screens": payload_screens, "flows": flows, "animations": animations,
|
||||||
|
"element_types": element_types, "data": data, "fields_meta": fields_meta,
|
||||||
|
}
|
||||||
|
|||||||
+81
-29
@@ -163,33 +163,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyAnimationClip(clip) {
|
function applyAnimationClip(clip) {
|
||||||
var target = document.querySelector('[data-anim-target="' + clip.element_id + '"]');
|
// querySelectorAll (pas querySelector) : un clip posé dans l'éditeur d'un
|
||||||
if (!target) return;
|
// é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 duration = clip.duration || 1, delay = clip.delay || 0;
|
||||||
var iterationCount = clip.iteration_count || '1';
|
var iterationCount = clip.iteration_count || '1';
|
||||||
var infinite = iterationCount === 'infinite';
|
var infinite = iterationCount === 'infinite';
|
||||||
if (clip.kind === 'custom') {
|
targets.forEach(function(target) {
|
||||||
target.style.animation = 'customAnim_' + clip.id + ' ' + duration + 's ' + (clip.easing || 'ease') +
|
if (clip.kind === 'custom') {
|
||||||
' ' + delay + 's ' + iterationCount;
|
target.style.animation = 'customAnim_' + clip.id + ' ' + duration + 's ' + (clip.easing || 'ease') +
|
||||||
if (!infinite) {
|
' ' + delay + 's ' + iterationCount;
|
||||||
target.addEventListener('animationend', function handler() {
|
if (!infinite) {
|
||||||
target.style.animation = '';
|
target.addEventListener('animationend', function handler() {
|
||||||
target.removeEventListener('animationend', handler);
|
target.style.animation = '';
|
||||||
});
|
target.removeEventListener('animationend', handler);
|
||||||
}
|
});
|
||||||
} else {
|
}
|
||||||
var animClass = 'animate__' + clip.animate_name;
|
} else {
|
||||||
target.style.setProperty('--animate-duration', duration + 's');
|
var animClass = 'animate__' + clip.animate_name;
|
||||||
target.style.setProperty('--animate-delay', delay + 's');
|
target.style.setProperty('--animate-duration', duration + 's');
|
||||||
target.style.animationIterationCount = iterationCount;
|
target.style.setProperty('--animate-delay', delay + 's');
|
||||||
target.classList.add('animate__animated', animClass);
|
target.style.animationIterationCount = iterationCount;
|
||||||
if (!infinite) {
|
target.classList.add('animate__animated', animClass);
|
||||||
target.addEventListener('animationend', function handler() {
|
if (!infinite) {
|
||||||
target.classList.remove('animate__animated', animClass);
|
target.addEventListener('animationend', function handler() {
|
||||||
target.removeEventListener('animationend', 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) {
|
function runAnimationTimeline(screenId) {
|
||||||
@@ -200,7 +236,7 @@
|
|||||||
// suite sur le même écran avant la fin de sa timeline.
|
// suite sur le même écran avant la fin de sa timeline.
|
||||||
pendingAnimationTimers.forEach(function(t) { clearTimeout(t); });
|
pendingAnimationTimers.forEach(function(t) { clearTimeout(t); });
|
||||||
pendingAnimationTimers = [];
|
pendingAnimationTimers = [];
|
||||||
var clips = (gameData.animations || {})[String(screenId)] || [];
|
var clips = collectAnimationClips(screenId);
|
||||||
clips.forEach(function(clip) {
|
clips.forEach(function(clip) {
|
||||||
var t = setTimeout(function() { applyAnimationClip(clip); }, (clip.start_time || 0) * 1000);
|
var t = setTimeout(function() { applyAnimationClip(clip); }, (clip.start_time || 0) * 1000);
|
||||||
pendingAnimationTimers.push(t);
|
pendingAnimationTimers.push(t);
|
||||||
@@ -233,11 +269,20 @@
|
|||||||
// fois, sur tous les éléments de tous les écrans (déjà tous présents dans
|
// 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
|
// 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.
|
// 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) {
|
function findTriggerNode(elementId, event) {
|
||||||
for (const screenIdKey in gameData.flows) {
|
for (const screenIdKey in gameData.flows) {
|
||||||
const flow = gameData.flows[screenIdKey];
|
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);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -290,7 +335,7 @@
|
|||||||
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.id);
|
runFlowFrom(trigger.node.id, trigger.screenId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -311,8 +356,8 @@
|
|||||||
const leaveTrigger = findTriggerNode(elementId, 'fin_survol');
|
const leaveTrigger = findTriggerNode(elementId, 'fin_survol');
|
||||||
if (!enterTrigger && !leaveTrigger) return;
|
if (!enterTrigger && !leaveTrigger) return;
|
||||||
elDiv.dataset.hoverTriggerBound = '1';
|
elDiv.dataset.hoverTriggerBound = '1';
|
||||||
if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.id));
|
if (enterTrigger) elDiv.addEventListener('mouseenter', () => runFlowFrom(enterTrigger.node.id, enterTrigger.screenId));
|
||||||
if (leaveTrigger) elDiv.addEventListener('mouseleave', () => runFlowFrom(leaveTrigger.id));
|
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
|
// seulement le premier. Chaque nœud Action produit son effet puis
|
||||||
// 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.
|
||||||
function runFlowFrom(nodeId) {
|
// flowScreenId : l'écran PROPRIÉTAIRE du graphe à exécuter — par défaut
|
||||||
const flow = gameData.flows[String(window.currentScreenId)];
|
// 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;
|
if (!flow) return;
|
||||||
const nodesById = {};
|
const nodesById = {};
|
||||||
flow.nodes.forEach(n => { nodesById[n.id] = n; });
|
flow.nodes.forEach(n => { nodesById[n.id] = n; });
|
||||||
|
|||||||
Reference in New Issue
Block a user