Files
Forge-Engine/screens/flow/constants.py
T
williamandClaude Sonnet 5 727c3c97ba
Build and deploy / test-python (push) Successful in 1m28s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase 3 : déclencheur clavier + minuteur récurrent
screens/flow/constants.py : TRIGGER_EVENTS += "clavier" (À l'appui sur
une touche) et "minuteur" (Toutes les X millisecondes) — ni élément ni
écran précis pour les deux, comme "evenement" déjà en place.
FLOW_NODE_FIELDS += trigger_key/trigger_interval_ms. ensure_flow_schema.py :
ALTER TABLE pour les 2 colonnes (patron trigger_custom_event_id).

templates/screen_edit.html + static/js/screen_edit/flow-editor.js :
- "clavier" : un champ "Touche à surveiller" qui capture lui-même la
  touche pressée (onkeydown sur l'input, captureFlowTriggerKey()) plutôt
  que de faire deviner la syntaxe attendue (ev.key du navigateur, ex.
  "ArrowUp", "a", " " pour Espace).
- "minuteur" : un simple champ numérique (millisecondes).
- nodeLabel() affiche "⌨️ Touche « X »"/"⏱️ Toutes les N ms" sur le nœud.

static/js/play/triggers.js (moteur de jeu) :
- bindKeyboardTriggers() : UN SEUL window.addEventListener('keydown', ...)
  posé une fois pour tout le jeu (voir l'amorçage en fin de
  templates/play.html) — même patron de scan global que
  dispatchGameEvent() pour "Sur un événement personnalisé".
- runScreenTimerTriggers(screenId) : géré PAR ÉCRAN (appelé depuis
  showScreen(), static/js/play/screens.js) — démarre les setInterval des
  nœuds "minuteur" de l'écran affiché, arrête d'abord tous ceux de
  l'affichage précédent (même principe que runAnimationTimeline) pour
  ne jamais accumuler des minuteurs sur des écrans quittés.

Vérifié : 255 tests passent (5 nouveaux, dont un qui verrouille que la
touche Espace — très probablement utilisée en jeu — n'est pas filtrée
comme une valeur "vide" par add_flow_node.py), 13 tests node:test
toujours au vert, syntaxe JS validée sur tous les fichiers de
static/js/play/ et static/js/screen_edit/. Comme le reste du graphe de
logique côté client, le comportement RÉEL d'un keydown/setInterval n'est
pas testable sans navigateur — test manuel recommandé (touche assignée
à un saut, minuteur faisant avancer un compteur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:05:55 +02:00

111 lines
6.4 KiB
Python

# ---------- Logique visuelle (éditeur de flow à nœuds) ----------
#
# Remplace l'ancien système "Actions" attaché à chaque élément : toute la
# logique d'une scène (déclencheurs, conditions, actions) vit maintenant dans
# un graphe de nœuds propre à l'écran, visible dans le panneau "Logique de
# la scène" de l'éditeur (voir screen_edit.html) — un nœud Déclencheur
# ("Au clic sur X") relié à des nœuds Condition (deux sorties Vrai/Faux) et
# Action (les mêmes effets qu'avant : changer d'écran, modifier un élément,
# modifier une donnée). L'ancien système _actions reste en base pour ne rien
# casser sur les jeux déjà créés, mais n'est plus exposé dans l'éditeur ni
# utilisé par le mode jouable.
# Valeur sentinelle pour cond_row_id/target_row_id (nœud Condition/action
# "Modifier une donnée") : "la ligne de Répéteur sur laquelle on vient de
# cliquer", résolue au moment de l'exécution (window.lastClickedRowId côté
# client — voir play.html) plutôt que figée à la création du nœud. Utile
# quand le déclencheur est "Au clic" sur un Répéteur : la ligne cliquée
# n'est jamais connue à l'avance (chaque ligne du Répéteur exécute le MÊME
# graphe), donc choisir une ligne précise dans l'éditeur n'a pas de sens
# ici — -1 ne collisionne jamais avec un vrai id de ligne (toujours >= 1,
# AUTOINCREMENT SQLite).
CLICKED_ROW_ID = -1
TRIGGER_EVENTS = [
("clic", "Au clic"),
("soumission", "À la soumission"),
# 3.1 (Confort) — interactions au survol, reconstruit comme déclencheur
# de flow (au lieu d'un réglage statique dans le panneau de propriétés,
# voir universal_controls.py) : "survol" et "fin_survol" sont deux
# déclencheurs distincts et explicites (comme "Au clic"), sans effet
# implicite — un créateur qui veut qu'un texte affiché au survol
# disparaisse ensuite doit poser l'action inverse sur "Fin du survol"
# lui-même, plutôt que de compter sur un retour automatique.
("survol", "Au survol"),
("fin_survol", "Fin du survol"),
# Pas de "trigger_element_id" pour celui-ci : il concerne l'ÉCRAN entier,
# pas un élément précis (voir 1.2 dans claude/forge-engine-lacunes-boitemail.md
# côté projet Forge — "affichage piloté par la donnée"). Se déclenche
# côté jouable à chaque fois que l'écran est montré (premier affichage,
# retour en arrière, changement d'écran...) ET après toute donnée
# modifiée pendant qu'on est déjà sur cet écran — ce qui permet de
# cacher/montrer un élément selon l'état de la partie SANS qu'un clic
# explicite soit nécessaire pour le réévaluer.
("affichage", "À l'affichage de l'écran"),
# Écoute un événement personnalisé (voir screens/custom_events/),
# déclenché depuis N'IMPORTE QUEL autre graphe (une autre scène, un
# autre modèle) via l'action "declencher_evenement" — ni élément ni
# écran précis : trigger_custom_event_id (quel événement) est le seul
# réglage propre à ce nœud, retrouvé par un scan global de
# 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 = [
("egal", "est égal à"),
("different", "est différent de"),
("superieur", "est supérieur à"),
("inferieur", "est inférieur à"),
("superieur_egal", "est supérieur ou égal à"),
("inferieur_egal", "est inférieur ou égal à"),
]
CONDITION_OPERATOR_LABELS = dict(CONDITION_OPERATORS)
FLOW_NODE_FIELDS = {
"trigger_element_id", "trigger_event",
"cond_definition_id", "cond_row_id", "cond_field", "cond_field_type", "cond_operator", "cond_value",
# 2.4 — conditions combinées (ET/OU) : cond_clauses est une liste JSON de
# clauses supplémentaires (en plus de la clause "historique" ci-dessus,
# qui reste la première clause) + cond_combinator ('et'/'ou') pour savoir
# comment les combiner. Absents => comportement legacy (une seule clause).
"cond_clauses", "cond_combinator",
# Condition sur une VARIABLE GLOBALE plutôt qu'un champ d'objet — voir
# ensure_flow_schema.py pour le détail des 3 clés (cond_source vaut
# "objet" ou "variable" ; absent => "objet", comportement historique).
"cond_source", "cond_variable", "cond_variable_chemin",
"action_type", "target_screen_id", "target_element_id", "element_property", "element_value",
"target_definition_id", "target_row_id", "target_field", "data_operation", "data_value",
"target_variable",
# Événements personnalisés (voir screens/custom_events/) : quel
# événement un nœud Déclencheur écoute (trigger_event="evenement") ou
# un nœud Action déclenche (action_type="declencher_evenement") — un
# événement est une pure NOTIFICATION, sans paramètre : "déclencher"
# ne fait que signaler, jamais choisir un élément. C'est à l'ÉCOUTEUR
# (déclencheur → condition → action) de décider quoi faire ensuite,
# avec ses propres réglages habituels (target_element_id normal, pas
# de mécanisme dynamique dédié).
"trigger_custom_event_id", "target_custom_event_id",
# Blocs de logique (voir screens/flow/blocks/) : à quel bloc ce nœud
# appartient. Aucun `update_flow_node` n'existe (éditer un nœud le
# 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",
}