diff --git a/screens/flow/constants.py b/screens/flow/constants.py index 7f1bafed..0a86530b 100644 --- a/screens/flow/constants.py +++ b/screens/flow/constants.py @@ -50,6 +50,19 @@ TRIGGER_EVENTS = [ # gameData.flows côté client (voir dispatchGameEvent() dans # templates/play.html), au même titre que findTriggerNode(). ("evenement", "Sur un événement personnalisé"), + # Phase 3 (moteur) — ni élément ni écran précis : trigger_key (quelle + # touche, ev.key du clavier) est le seul réglage propre à ce nœud, + # écouté UNE SEULE FOIS pour tout le jeu (voir bindKeyboardTriggers() + # dans static/js/play/triggers.js), même patron de scan global que + # "evenement" ci-dessus. + ("clavier", "À l'appui sur une touche"), + # Contrairement aux autres déclencheurs (réagissent à quelque chose), + # celui-ci fait AVANCER le jeu tout seul, à intervalle régulier — + # trigger_interval_ms (en millisecondes) est son seul réglage. Géré + # PAR ÉCRAN (voir showScreen() dans static/js/play/screens.js) : ne + # tourne que tant que l'écran qui le porte est affiché, jamais en + # arrière-plan sur un écran quitté. + ("minuteur", "Toutes les X millisecondes"), ] CONDITION_OPERATORS = [ @@ -91,4 +104,7 @@ FLOW_NODE_FIELDS = { # recrée puis supprime l'ancien) — le client doit donc renvoyer # block_id à chaque (ré)création, pas juste le poser une fois. "block_id", + # Phase 3 — déclencheur clavier ("clavier") et minuteur récurrent + # ("minuteur"), voir TRIGGER_EVENTS ci-dessus. + "trigger_key", "trigger_interval_ms", } diff --git a/screens/flow/ensure_flow_schema.py b/screens/flow/ensure_flow_schema.py index 47e5a6f8..46dff952 100644 --- a/screens/flow/ensure_flow_schema.py +++ b/screens/flow/ensure_flow_schema.py @@ -96,5 +96,14 @@ def ensure_flow_schema(slug): # rattache paresseusement à un bloc "Bloc principal" généré à la # volée, la première fois que l'onglet Blocs est ouvert. conn.execute("ALTER TABLE _flow_nodes ADD COLUMN block_id INTEGER") + if "trigger_key" not in cols: + # Phase 3 — déclencheur clavier (trigger_event="clavier") : quelle + # touche (ev.key du navigateur, ex. "ArrowUp", " ", "a") ; minuteur + # récurrent (trigger_event="minuteur") : trigger_interval_ms, en + # millisecondes entre deux déclenchements — voir TRIGGER_EVENTS + # (screens/flow/constants.py) pour le détail des deux. + conn.execute("ALTER TABLE _flow_nodes ADD COLUMN trigger_key TEXT") + if "trigger_interval_ms" not in cols: + conn.execute("ALTER TABLE _flow_nodes ADD COLUMN trigger_interval_ms INTEGER") conn.commit() conn.close() diff --git a/screens/labels/trigger_labels.py b/screens/labels/trigger_labels.py index 594fd32a..11a83882 100644 --- a/screens/labels/trigger_labels.py +++ b/screens/labels/trigger_labels.py @@ -5,4 +5,6 @@ TRIGGER_LABELS = { "survol": "Au survol", "fin_survol": "Fin du survol", "evenement": "Sur un événement personnalisé", + "clavier": "À l'appui sur une touche", + "minuteur": "Toutes les X millisecondes", } diff --git a/static/js/play/screens.js b/static/js/play/screens.js index 864c076b..cfc644ee 100644 --- a/static/js/play/screens.js +++ b/static/js/play/screens.js @@ -39,6 +39,7 @@ function showScreen(id) { applyScreenAspect(el.dataset.aspect || '9/16'); } runScreenShowTriggers(id); + runScreenTimerTriggers(id); runAnimationTimeline(id); } diff --git a/static/js/play/triggers.js b/static/js/play/triggers.js index 63a452a8..e95b2d81 100644 --- a/static/js/play/triggers.js +++ b/static/js/play/triggers.js @@ -16,6 +16,29 @@ function runScreenShowTriggers(screenId) { .forEach(function(n){ runFlowFrom(n.id); }); } +// Minuteur récurrent (Phase 3) : contrairement aux autres déclencheurs +// (réagissent à quelque chose), celui-ci fait avancer le jeu tout seul, à +// intervalle régulier — géré PAR ÉCRAN (appelé depuis showScreen(), +// static/js/play/screens.js) : n'existe QUE tant que l'écran qui le +// porte est affiché. On arrête d'abord tout minuteur de l'affichage +// précédent (même principe que runAnimationTimeline) — sans ça, revenir +// plusieurs fois sur le même écran accumulerait des setInterval en +// double, chacun exécutant le graphe en plus des autres. +var activeTimerTriggerIntervals = []; + +function runScreenTimerTriggers(screenId) { + activeTimerTriggerIntervals.forEach(function(id) { clearInterval(id); }); + activeTimerTriggerIntervals = []; + const flow = gameData.flows[String(screenId)]; + if (!flow) return; + flow.nodes + .filter(function(n){ return n.node_type === 'trigger' && n.trigger_event === 'minuteur' && n.trigger_interval_ms > 0; }) + .forEach(function(n){ + var intervalId = setInterval(function(){ runFlowFrom(n.id, screenId); }, n.trigger_interval_ms); + activeTimerTriggerIntervals.push(intervalId); + }); +} + // Trouve, pour un élément et un type de déclencheur ("clic"/"soumission"), // 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 @@ -130,3 +153,20 @@ function bindHoverTriggers() { if (leaveTriggers.length) elDiv.addEventListener('mouseleave', () => leaveTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId))); }); } + +// Déclencheur clavier (Phase 3) : ni élément ni écran précis — un SEUL +// écouteur posé une fois pour tout le jeu (voir l'amorçage en fin de +// play.html), même principe de scan global que dispatchGameEvent() +// (flow-engine.js) pour "Sur un événement personnalisé" : gameData.flows +// est parcouru à CHAQUE appui, pas une seule fois au chargement, pour +// que les nœuds ajoutés/déplacés après un rafraîchissement des données +// restent pris en compte sans devoir ré-attacher quoi que ce soit. +function bindKeyboardTriggers() { + window.addEventListener('keydown', function (ev) { + for (const screenIdKey in gameData.flows) { + gameData.flows[screenIdKey].nodes + .filter(function(n){ return n.node_type === 'trigger' && n.trigger_event === 'clavier' && n.trigger_key === ev.key; }) + .forEach(function(n){ runFlowFrom(n.id, screenIdKey); }); + } + }); +} diff --git a/static/js/screen_edit/flow-editor.js b/static/js/screen_edit/flow-editor.js index acd12e4e..b5a4d35d 100644 --- a/static/js/screen_edit/flow-editor.js +++ b/static/js/screen_edit/flow-editor.js @@ -37,6 +37,12 @@ function nodeLabel(node) { var listenedEvent = CUSTOM_EVENTS_MAP[node.trigger_custom_event_id]; return '📣 Sur « ' + (listenedEvent ? listenedEvent.name : '?') + ' »'; } + if (node.trigger_event === 'clavier') { + return '⌨️ Touche « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »'; + } + if (node.trigger_event === 'minuteur') { + return '⏱️ Toutes les ' + (node.trigger_interval_ms || '?') + ' ms'; + } return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' + (ELEMENTS_LABELS[node.trigger_element_id] || '?'); } @@ -278,10 +284,26 @@ function toggleFlowTriggerFields() { // l'écran entier : c'est un scan global (voir dispatchGameEvent() dans // play.html), le seul réglage propre à ce nœud est QUEL événement. var isCustomEvent = eventVal === 'evenement'; - document.getElementById('nf-trigger-element-row').style.display = (isAffichage || isCustomEvent) ? 'none' : 'block'; - document.getElementById('nf-trigger-repeater-hint').style.display = (isAffichage || isCustomEvent) ? 'none' : 'block'; + // Idem "clavier"/"minuteur" (Phase 3) : ni élément ni écran précis. + var isKeyboard = eventVal === 'clavier'; + var isTimer = eventVal === 'minuteur'; + var noElement = isAffichage || isCustomEvent || isKeyboard || isTimer; + document.getElementById('nf-trigger-element-row').style.display = noElement ? 'none' : 'block'; + document.getElementById('nf-trigger-repeater-hint').style.display = noElement ? 'none' : 'block'; document.getElementById('nf-trigger-affichage-hint').style.display = isAffichage ? 'block' : 'none'; document.getElementById('nf-trigger-event-row').style.display = isCustomEvent ? 'block' : 'none'; + document.getElementById('nf-trigger-key-row').style.display = isKeyboard ? 'block' : 'none'; + document.getElementById('nf-trigger-interval-row').style.display = isTimer ? 'block' : 'none'; +} + +// Capture la touche pressée dans le champ "Touche à surveiller" (Phase 3) +// — évite de faire deviner la syntaxe attendue (ev.key du navigateur, +// ex. "ArrowUp", "a", " " pour la barre d'espace) : l'input lui-même +// écoute keydown et se remplit tout seul, jamais tapé à la main. +function captureFlowTriggerKey(event) { + event.preventDefault(); + document.getElementById('nf-trigger-key').value = event.key; + document.getElementById('nf-trigger-key-display').value = event.key === ' ' ? 'Espace' : event.key; } function closeNodeForm() { @@ -527,6 +549,10 @@ function submitNodeForm() { payload.trigger_event = document.getElementById('nf-trigger-event').value; if (payload.trigger_event === 'evenement') { payload.trigger_custom_event_id = parseInt(document.getElementById('nf-trigger-custom-event').value, 10); + } else if (payload.trigger_event === 'clavier') { + payload.trigger_key = document.getElementById('nf-trigger-key').value; + } else if (payload.trigger_event === 'minuteur') { + payload.trigger_interval_ms = parseInt(document.getElementById('nf-trigger-interval').value, 10); } else if (payload.trigger_event !== 'affichage') { payload.trigger_element_id = parseInt(document.getElementById('nf-trigger-element').value, 10); } diff --git a/templates/play.html b/templates/play.html index 15c2161c..5257e0c4 100644 --- a/templates/play.html +++ b/templates/play.html @@ -116,6 +116,10 @@ bindClicks(); bindHoverTexts(); bindHoverTriggers(); + // Un seul écouteur clavier pour tout le jeu (Phase 3) — showScreen() + // ci-dessus a déjà démarré les minuteurs de l'écran affiché au premier + // affichage (runScreenTimerTriggers, appelé depuis showScreen()). + bindKeyboardTriggers(); diff --git a/templates/screen_edit.html b/templates/screen_edit.html index 28b9fbe2..0222754a 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -290,6 +290,17 @@

Aucun événement pour l'instant — crées-en un dans l'onglet Événements.

{% endif %} + +