import db def ensure_flow_schema(slug: str) -> None: 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, 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, 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") if "cond_element_a" not in cols: # Phase 5 — condition de collision (cond_source="collision") : les # deux éléments dont on compare les rectangles à l'écran. Pas de # contrainte FK — même patron que block_id/trigger_custom_event_id # ci-dessus (colonne ajoutée par ALTER TABLE, jamais contrainte # dans ce fichier) : screens/elements/delete_element.py nettoie # lui-même les nœuds qui référencent un élément supprimé (voir ce # fichier), une contrainte ferait double emploi. conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_element_a INTEGER") if "cond_element_b" not in cols: conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_element_b INTEGER") if "trigger_object_id" not in cols: # Fondations multi-éditeurs — jeux "jeu_2d" (voir db/games/ # get_game_type.py) : équivalents de trigger_element_id/ # target_element_id, mais pour un objet de _scene_objects plutôt # qu'un élément DOM (_screen_elements). Pas de contrainte FK, même # patron que cond_element_a ci-dessus (nettoyage manuel à la # suppression d'un objet de scène, voir screens/scenes/) — ces deux # colonnes ne pouvaient pas réutiliser trigger_element_id/ # target_element_id, qui portent une VRAIE contrainte FK vers # _screen_elements depuis la création de cette table. conn.execute("ALTER TABLE _flow_nodes ADD COLUMN trigger_object_id INTEGER") if "target_object_id" not in cols: conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_object_id INTEGER") conn.commit() conn.close()