Ajoute deux mécanismes distincts, à la demande de l'utilisateur qui a précisé qu'un son doit pouvoir être attaché à une SCÈNE (pas seulement joué ponctuellement par une action de flow, comme prévu initialement) : - Musique de fond par écran (nouveau champ background_music_url sur _screens, ALTER TABLE nullable) : réglée dans le panneau gauche de l'éditeur (URL ou envoi de fichier, réutilise la route d'upload générique existante), enregistrée en AJAX au même patron que le format d'aperçu (screen_set_aspect.py). Démarrée en boucle à l'affichage de l'écran et arrêtée au changement d'écran (runScreenBackgroundMusic(), appelée depuis showScreen() dans static/js/play/screens.js) — un seul Audio actif à la fois, jamais cumulé avec une musique restée d'un écran précédent. - Action de flow "jouer_son" (aux côtés des actions existantes) : effet sonore ponctuel, non bouclé, déclenchable sur n'importe quel nœud Déclencheur. Réutilise data_value (déjà un champ texte générique sur le nœud action, comme pour "attendre") plutôt qu'une nouvelle colonne dédiée — fire-and-forget côté client (runActionNode), ne bloque jamais la suite du graphe. Les deux réutilisent le même mécanisme d'upload de fichier déjà en place ailleurs dans l'éditeur (ex. source d'une vidéo), sans nouvelle route. Ceci complète les 6 phases du plan d'extension du moteur (état par joueur, hasard/maths, clavier/minuteur, ajout de ligne, position/ collision, son) : Forge Engine peut désormais couvrir des jeux bien au-delà du narratif/puzzle/quiz (action, arcade, jeux à contrainte de temps). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
7.1 KiB
Python
132 lines
7.1 KiB
Python
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'")
|
|
if "background_music_url" not in screen_cols:
|
|
# Phase 6 — musique de fond de cet écran (URL) : démarrée en boucle
|
|
# à l'affichage de l'écran, arrêtée au changement d'écran (voir
|
|
# runScreenBackgroundMusic() dans static/js/play/screens.js).
|
|
# Indépendante de l'action "Jouer un son" (effet sonore ponctuel,
|
|
# non bouclé, sur n'importe quel déclencheur).
|
|
conn.execute("ALTER TABLE _screens ADD COLUMN background_music_url TEXT")
|
|
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()
|