Première brique du plan "Fondations d'une plateforme multi-éditeurs" — l'utilisateur veut un éditeur dédié aux jeux 2D/serious games (scène à coordonnées pixel fixes, objets en couches, collision, personnages animés), distinct de l'éditeur générique actuel, sans dupliquer ce qui peut être partagé (comptes, objets de données, moteur de logique de flow, hébergement/publication). - db/games/get_game_type.py (nouveau) : "document" (défaut, éditeur actuel) ou "jeu_2d" (nouveau), stocké dans _meta comme is_public_played — aucune migration pour les jeux déjà créés (retombent sur "document"). Choisi obligatoirement à la création (templates/index.html), jamais modifiable ensuite. - screens/scenes/ (nouveau sous-module) : table _scene_objects (une scène = des objets en pixels fixes, pas les % fluides de _screen_elements — indispensable pour la collision/l'animation), CRUD complet, rendu HTML. kind="personnage" réutilise TELLE QUELLE la structure _personnage_data et les fonctions resolve_personnage_* déjà écrites pour le widget "personnage" de l'éditeur document (Phase 8) — même bibliothèque Forge, même moteur d'animation, juste une autre table de stockage. - screens/flow/ensure_flow_schema.py : += trigger_object_id/ target_object_id (ALTER TABLE sans contrainte FK, même patron que cond_element_a) — le moteur de logique de flow (_flow_nodes/_flow_edges, flow-engine.js) reste EXACTEMENT le même pour les deux éditeurs, seule la palette de nœuds change (screens/scenes/flow_palette.py, nouveau : sous-ensemble direct des triggers/actions existants, déjà génériques). - screens/screens_repo/ensure_schema.py : += scene_width/scene_height sur _screens (taille de scène fixe en pixels, sans effet sur un écran "document"). Reste à faire (prochains commits) : route + template de l'éditeur de scène, puis le rendu en mode jouable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
141 lines
7.8 KiB
Python
141 lines
7.8 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")
|
|
if "scene_width" not in screen_cols:
|
|
# Fondations multi-éditeurs — jeux "jeu_2d" (voir db/games/
|
|
# get_game_type.py) : une scène a une taille FIXE en pixels
|
|
# (contrairement à un écran "document", positionné en % fluides
|
|
# via preview_aspect ci-dessus) — indispensable pour la collision
|
|
# et l'animation de sprites. Sans effet sur un écran "document"
|
|
# (jamais lu par son rendu) — voir screens/scenes/.
|
|
conn.execute("ALTER TABLE _screens ADD COLUMN scene_width INTEGER NOT NULL DEFAULT 960")
|
|
conn.execute("ALTER TABLE _screens ADD COLUMN scene_height INTEGER NOT NULL DEFAULT 540")
|
|
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()
|