import db def ensure_schema(slug): """Crée les tables de ce module si elles n'existent pas encore — les jeux créés avant l'ajout de cette fonctionnalité n'ont pas besoin d'être migrés manuellement, la première visite d'un écran de ce jeu suffit.""" conn = db.connect(slug) conn.executescript( """ CREATE TABLE IF NOT EXISTS _screens ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, order_index INTEGER NOT NULL DEFAULT 0, is_template INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS _element_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, icon TEXT NOT NULL DEFAULT '🧩', default_widget TEXT NOT NULL DEFAULT 'conteneur', default_content TEXT NOT NULL DEFAULT '', default_attributes TEXT NOT NULL DEFAULT '{}', default_style TEXT NOT NULL DEFAULT '{}', template_screen_id INTEGER REFERENCES _screens(id), bound_definition_id INTEGER, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS _screen_elements ( id INTEGER PRIMARY KEY AUTOINCREMENT, screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE, parent_id INTEGER REFERENCES _screen_elements(id) ON DELETE CASCADE, widget TEXT NOT NULL DEFAULT 'conteneur', element_type_id INTEGER REFERENCES _element_types(id), name TEXT, content TEXT NOT NULL DEFAULT '', attributes TEXT NOT NULL DEFAULT '{}', x REAL NOT NULL DEFAULT 10, y REAL NOT NULL DEFAULT 10, width REAL NOT NULL DEFAULT 20, height REAL NOT NULL DEFAULT 10, z_index INTEGER NOT NULL DEFAULT 1, style TEXT NOT NULL DEFAULT '{}', created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS _actions ( id INTEGER PRIMARY KEY AUTOINCREMENT, element_id INTEGER NOT NULL REFERENCES _screen_elements(id) ON DELETE CASCADE, trigger_event TEXT NOT NULL, action_type TEXT NOT NULL, 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 ); """ ) cols = {r["name"] for r in conn.execute("PRAGMA table_info(_screen_elements)").fetchall()} if "widget" not in cols: conn.execute("ALTER TABLE _screen_elements ADD COLUMN widget TEXT NOT NULL DEFAULT 'conteneur'") if "attributes" not in cols: conn.execute("ALTER TABLE _screen_elements ADD COLUMN attributes TEXT NOT NULL DEFAULT '{}'") if "parent_id" not in cols: conn.execute("ALTER TABLE _screen_elements ADD COLUMN parent_id INTEGER REFERENCES _screen_elements(id) ON DELETE CASCADE") if "name" not in cols: conn.execute("ALTER TABLE _screen_elements ADD COLUMN name TEXT") type_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_element_types)").fetchall()} if "default_widget" not in type_cols: conn.execute("ALTER TABLE _element_types ADD COLUMN default_widget TEXT NOT NULL DEFAULT 'conteneur'") conn.execute("ALTER TABLE _element_types ADD COLUMN default_content TEXT NOT NULL DEFAULT ''") conn.execute("ALTER TABLE _element_types ADD COLUMN default_attributes TEXT NOT NULL DEFAULT '{}'") if "template_screen_id" not in type_cols: # Un élément de jeu n'est plus un simple widget isolé : c'est un # conteneur avec un vrai arbre d'éléments imbriqués à l'intérieur, # construit exactement comme le contenu de n'importe quel conteneur # sur un écran — ce petit arbre vit dans un écran-modèle CACHÉ # (voir "is_template" ci-dessous), pointé par cette colonne. conn.execute("ALTER TABLE _element_types ADD COLUMN template_screen_id INTEGER REFERENCES _screens(id)") if "bound_definition_id" not in type_cols: # "Objet lié" optionnel — comme pour le Répéteur de données, rend # {{nom_du_champ}} disponible dans tout l'arbre de ce modèle. conn.execute("ALTER TABLE _element_types ADD COLUMN bound_definition_id INTEGER") screen_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_screens)").fetchall()} if "is_template" not in screen_cols: # Un écran-modèle héberge le contenu d'un élément de jeu réutilisable # (voir template_screen_id ci-dessus) : il n'apparaît jamais dans la # liste des écrans du jeu, ni dans le mode jouable, ni dans les # menus "aller à cet écran" — seul son contenu (les éléments qu'il # contient) est utilisé, jamais l'écran lui-même. conn.execute("ALTER TABLE _screens ADD COLUMN is_template INTEGER NOT NULL DEFAULT 0") if "preview_aspect" not in screen_cols: # Format d'aperçu choisi pour cet écran (Portrait 9/16, Paysage 16/9, # ou Carré 1/1) — mémorisé par écran pour qu'il reste le même après # un ajout/suppression d'élément ou un changement d'élément # sélectionné (ces actions redemandent le panneau au serveur, qui # doit donc connaître le choix déjà fait, sinon il repart sur la # valeur par défaut à chaque fois). Sert aussi en mode Jouer, pour # garder la zone de jeu à la bonne proportion quelle que soit la # forme réelle de la fenêtre du joueur (voir #playFrame dans # play.html) — sans ça, les positions/tailles en % se déforment dès # que la fenêtre du joueur n'a pas la même proportion que celle # utilisée à la conception. conn.execute("ALTER TABLE _screens ADD COLUMN preview_aspect TEXT NOT NULL DEFAULT '9/16'") action_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_actions)").fetchall()} for col_name, col_def in [ ("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"), ]: if col_name not in action_cols: conn.execute(f"ALTER TABLE _actions ADD COLUMN {col_name} {col_def}") conn.commit() conn.close()