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>
110 lines
6.0 KiB
Python
110 lines
6.0 KiB
Python
import db
|
|
|
|
|
|
def ensure_flow_schema(slug):
|
|
conn = db.connect(slug)
|
|
conn.executescript(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS _flow_nodes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
|
|
node_type TEXT NOT NULL,
|
|
x REAL NOT NULL DEFAULT 40,
|
|
y REAL NOT NULL DEFAULT 40,
|
|
trigger_element_id INTEGER REFERENCES _screen_elements(id),
|
|
trigger_event TEXT,
|
|
cond_definition_id INTEGER,
|
|
cond_row_id INTEGER,
|
|
cond_field TEXT,
|
|
cond_field_type TEXT,
|
|
cond_operator TEXT,
|
|
cond_value TEXT,
|
|
action_type TEXT,
|
|
target_screen_id INTEGER REFERENCES _screens(id),
|
|
target_element_id INTEGER REFERENCES _screen_elements(id),
|
|
element_property TEXT,
|
|
element_value TEXT,
|
|
target_definition_id INTEGER,
|
|
target_row_id INTEGER,
|
|
target_field TEXT,
|
|
data_operation TEXT,
|
|
data_value TEXT
|
|
);
|
|
CREATE TABLE IF NOT EXISTS _flow_edges (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
|
|
from_node_id INTEGER NOT NULL REFERENCES _flow_nodes(id) ON DELETE CASCADE,
|
|
from_port TEXT NOT NULL DEFAULT 'out',
|
|
to_node_id INTEGER NOT NULL REFERENCES _flow_nodes(id) ON DELETE CASCADE
|
|
);
|
|
"""
|
|
)
|
|
# 2.4 — conditions combinées (ET/OU) : un nœud Condition peut tester
|
|
# PLUSIEURS champs à la fois plutôt qu'un seul. cond_clauses (JSON, une
|
|
# liste de clauses en plus de la clause "historique" cond_definition_id/
|
|
# cond_field/etc.) + cond_combinator ('et'/'ou') sont ajoutés en ALTER
|
|
# TABLE pour ne rien casser sur les jeux déjà créés — un nœud sans
|
|
# cond_clauses garde exactement son comportement d'avant (une seule
|
|
# comparaison), voir evaluateConditionNode() dans templates/play.html.
|
|
cols = {r["name"] for r in conn.execute("PRAGMA table_info(_flow_nodes)").fetchall()}
|
|
if "cond_clauses" not in cols:
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_clauses TEXT")
|
|
if "cond_combinator" not in cols:
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_combinator TEXT NOT NULL DEFAULT 'et'")
|
|
if "target_variable" not in cols:
|
|
# Action "Modifier une variable" (voir apply_variable_action.py) —
|
|
# cible une variable globale par NOM plutôt qu'un champ d'objet ;
|
|
# réutilise data_operation/data_value, déjà là pour "modifier_donnee".
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_variable TEXT")
|
|
if "cond_source" not in cols:
|
|
# Un nœud Condition peut désormais tester soit un champ d'objet
|
|
# (comportement historique, cond_source absent ou "objet"), soit
|
|
# une VARIABLE GLOBALE (cond_source="variable" — cond_variable :
|
|
# son nom ; cond_variable_chemin : chemin optionnel ".champ"/
|
|
# "[index]" si elle est de type objet/tableau, même syntaxe que
|
|
# "Chemin dans la variable" de la Condition de visibilité — voir
|
|
# visibility_condition_controls.py). Chaque clause de cond_clauses
|
|
# (JSON) porte les mêmes 3 clés en plus des siennes. Évalué côté
|
|
# CLIENT (templates/play.html, evaluateConditionClause) contre
|
|
# gameData.variables, exposé par full_game_payload.py.
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_source TEXT")
|
|
if "cond_variable" not in cols:
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_variable TEXT")
|
|
if "cond_variable_chemin" not in cols:
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_variable_chemin TEXT")
|
|
if "trigger_custom_event_id" not in cols:
|
|
# É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").
|
|
# Pas de FK vers _custom_events : une suppression d'événement
|
|
# nettoie déjà ces colonnes elle-même (voir
|
|
# screens/custom_events/delete_custom_event.py), une contrainte
|
|
# ferait double emploi sans rien apporter de plus.
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN trigger_custom_event_id INTEGER")
|
|
if "target_custom_event_id" not in cols:
|
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_custom_event_id INTEGER")
|
|
if "block_id" not in cols:
|
|
# Blocs de logique (voir screens/flow/blocks/) : regroupe les nœuds
|
|
# d'un même écran en sous-graphes nommés/décrits pour rester lisible
|
|
# à l'échelle de milliers de nœuds. Pas de FK vers _flow_blocks —
|
|
# même rationale que trigger_element_id/target_element_id
|
|
# (screens/elements/delete_element.py) : la suppression d'un bloc
|
|
# nettoie déjà elle-même ces lignes (voir
|
|
# screens/flow/blocks/delete_flow_block.py), une contrainte ferait
|
|
# double emploi. NULL = nœud pas encore rattaché à un bloc (jeux
|
|
# créés avant cette fonctionnalité) — list_flow_blocks() les
|
|
# 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()
|