Fondations multi-éditeurs (1/3) : game_type + schéma/CRUD des objets de scène
Build and deploy / test-python (push) Successful in 1m41s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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>
This commit is contained in:
william
2026-08-31 09:42:38 +02:00
co-authored by Claude Sonnet 5
parent 70c2b8df05
commit efa7d1d9b0
22 changed files with 454 additions and 4 deletions
+10
View File
@@ -89,6 +89,16 @@ from .elements.flow_nodes_referencing_element import flow_nodes_referencing_elem
from .elements.duplicate_element import duplicate_element
from .elements.rename_element import rename_element
from .elements.set_personnage_data import set_personnage_data
from .scenes.ensure_scene_schema import ensure_scene_schema
from .scenes.add_scene_object import add_scene_object
from .scenes.list_scene_objects import list_scene_objects
from .scenes.get_scene_object import get_scene_object
from .scenes.update_scene_object_geometry import update_scene_object_geometry
from .scenes.delete_scene_object import delete_scene_object
from .scenes.set_scene_object_personnage_data import set_scene_object_personnage_data
from .scenes.render_scene_object import render_scene_object
from .scenes.flow_palette import TRIGGER_EVENTS_2D, ACTION_TYPE_LABELS_2D, OBJECT_ACTION_PROPERTIES
from .elements.element_label import element_label
from .element_types.list_element_types import list_element_types
+4
View File
@@ -123,4 +123,8 @@ FLOW_NODE_FIELDS = {
# rectangles — voir evaluateConditionClause() dans
# static/js/play/conditions.js.
"cond_element_a", "cond_element_b",
# Fondations multi-éditeurs — jeux "jeu_2d" : équivalents de
# trigger_element_id/target_element_id pour un objet de scène (voir
# ensure_flow_schema.py).
"trigger_object_id", "target_object_id",
}
+13
View File
@@ -116,5 +116,18 @@ def ensure_flow_schema(slug):
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()
+3
View File
@@ -0,0 +1,3 @@
"""Objets posés sur une scène (_scene_objects) — jeux "jeu_2d" (voir
db/games/get_game_type.py), équivalent de screens/elements/ pour l'éditeur
document mais en coordonnées pixel fixes."""
+41
View File
@@ -0,0 +1,41 @@
import json
import db
from .ensure_scene_schema import ensure_scene_schema
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
_KINDS = {"personnage", "decor"}
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None):
"""Pose un nouvel objet sur une scène (voir ensure_scene_schema.py).
kind="personnage" : attributes._personnage_data, MÊME structure/mêmes
fonctions resolve_personnage_* que le widget "personnage" de l'éditeur
document (screens/rendering/personnage_data.py) — un objet de scène
"personnage" partage donc directement la bibliothèque Forge et le
système d'animation déjà écrits. kind="decor" : une simple image fixe
(attributes.src), pas d'animation."""
ensure_scene_schema(slug)
kind = kind if kind in _KINDS else "personnage"
attributes = {}
if kind == "personnage":
data = dict(_DEFAULT_PERSONNAGE_DATA)
if forge_character:
from ..labels.sprite_library import SPRITE_LIBRARY
if forge_character in SPRITE_LIBRARY:
data["forge_character"] = forge_character
attributes["_personnage_data"] = json.dumps(data)
conn = db.connect(slug)
max_z = conn.execute(
"SELECT MAX(z_index) AS m FROM _scene_objects WHERE scene_id = ?", (scene_id,)
).fetchone()["m"]
conn.execute(
"""INSERT INTO _scene_objects (scene_id, kind, attributes, z_index)
VALUES (?, ?, ?, ?)""",
(scene_id, kind, json.dumps(attributes), (max_z or 0) + 1),
)
object_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
conn.commit()
conn.close()
return object_id
+24
View File
@@ -0,0 +1,24 @@
import db
from .ensure_scene_schema import ensure_scene_schema
from ..flow.ensure_flow_schema import ensure_flow_schema
def delete_scene_object(slug, object_id):
"""Supprime un objet de scène ET les nœuds de logique qui le
référencent (trigger_object_id/target_object_id/cond_element_a/
cond_element_b — aucun n'a de contrainte FK, voir
ensure_flow_schema.py) : un nœud qui vise un objet supprimé n'a plus
de sens, même patron que screens/elements/delete_element.py pour le
DOM."""
ensure_scene_schema(slug)
ensure_flow_schema(slug)
conn = db.connect(slug)
conn.execute(
"""DELETE FROM _flow_nodes WHERE trigger_object_id = ? OR target_object_id = ?
OR cond_element_a = ? OR cond_element_b = ?""",
(object_id, object_id, object_id, object_id),
)
conn.execute("DELETE FROM _scene_objects WHERE id = ?", (object_id,))
conn.commit()
conn.close()
+30
View File
@@ -0,0 +1,30 @@
import db
def ensure_scene_schema(slug):
"""Crée _scene_objects si nécessaire — le contenu d'une scène pour un
jeu "jeu_2d" (voir db/games/get_game_type.py), équivalent de
_screen_elements pour un jeu "document" mais en coordonnées PIXEL
fixes (pas des % fluides — indispensable pour la collision/
l'animation), sans arbre de nesting (pas de conteneurs imbriqués dans
une scène de jeu 2D)."""
conn = db.connect(slug)
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS _scene_objects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scene_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
kind TEXT NOT NULL DEFAULT 'personnage',
name TEXT,
x REAL NOT NULL DEFAULT 100,
y REAL NOT NULL DEFAULT 100,
width REAL NOT NULL DEFAULT 64,
height REAL NOT NULL DEFAULT 64,
z_index INTEGER NOT NULL DEFAULT 1,
attributes TEXT NOT NULL DEFAULT '{}',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
"""
)
conn.commit()
conn.close()
+42
View File
@@ -0,0 +1,42 @@
# ---------- Palette de logique réduite pour l'éditeur de scène 2D ----------
#
# Réutilise le MÊME moteur de flow (_flow_nodes/_flow_edges, flow-engine.js,
# flow-editor.js, tabs-and-blocks.js) que l'éditeur document — seule la
# palette de nœuds proposée diffère (voir décision d'architecture du plan
# "Fondations d'une plateforme multi-éditeurs"). Sous-ensembles directs de
# screens/flow/constants.py et screens/labels/action_type_labels.py : pas
# de notion de clic/soumission/survol DOM (pas d'élément HTML dans une
# scène), tout le reste est déjà générique et fonctionne tel quel sur un
# objet de scène.
from ..flow.constants import TRIGGER_EVENTS as _ALL_TRIGGER_EVENTS
_SCENE_TRIGGER_KEYS = {"affichage", "evenement", "clavier", "minuteur"}
TRIGGER_EVENTS_2D = [(k, label) for k, label in _ALL_TRIGGER_EVENTS if k in _SCENE_TRIGGER_KEYS]
ACTION_TYPE_LABELS_2D = {
"aller_a": "Aller à un écran précis",
"ecran_suivant": "Afficher l'écran suivant",
"ecran_precedent": "Afficher l'écran précédent",
"modifier_objet_scene": "Modifier un objet de scène",
"modifier_variable": "Modifier une variable globale",
"modifier_donnee": "Modifier une donnée d'un objet",
"jouer_animation_sprite": "Jouer une animation de sprites",
"jouer_son": "Jouer un son",
"declencher_evenement": "Déclencher un événement",
"attendre": "Attendre quelques secondes avant de continuer",
"rien": "Ne rien faire",
}
# Sous-palette de "modifier_objet_scene" — équivalent scène de
# ELEMENT_ACTION_PROPERTIES (screens/labels/element_action_properties.py),
# mais en pixels absolus/relatifs (pas des %) : une scène a une taille
# FIXE (voir scene_width/scene_height, ensure_schema.py), donc les
# déplacements s'expriment naturellement en pixels.
OBJECT_ACTION_PROPERTIES = [
("visibilite", "Visibilité"),
("pos_x", "Position horizontale en pixels (absolue)"),
("pos_y", "Position verticale en pixels (absolue)"),
("pos_x_relatif", "Déplacer horizontalement de... px (relatif)"),
("pos_y_relatif", "Déplacer verticalement de... px (relatif)"),
("orientation", "Orientation — sens du personnage"),
]
+17
View File
@@ -0,0 +1,17 @@
import json
import db
from .ensure_scene_schema import ensure_scene_schema
def get_scene_object(slug, object_id):
ensure_scene_schema(slug)
conn = db.connect(slug)
row = conn.execute("SELECT * FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
conn.close()
if not row:
return None
d = dict(row)
d["attributes"] = json.loads(d["attributes"] or "{}")
return d
+20
View File
@@ -0,0 +1,20 @@
import json
import db
from .ensure_scene_schema import ensure_scene_schema
def list_scene_objects(slug, scene_id):
ensure_scene_schema(slug)
conn = db.connect(slug)
rows = conn.execute(
"SELECT * FROM _scene_objects WHERE scene_id = ? ORDER BY z_index, id", (scene_id,)
).fetchall()
conn.close()
result = []
for r in rows:
d = dict(r)
d["attributes"] = json.loads(d["attributes"] or "{}")
result.append(d)
return result
+26
View File
@@ -0,0 +1,26 @@
import html as html_lib
from ..rendering.personnage_data import resolve_personnage_idle_frame
def render_scene_object(obj):
"""HTML d'un objet de scène — positionnement en PIXELS ABSOLUS (pas de
% fluide, voir ensure_scene_schema.py), mirror volontairement plus
simple que render_element_html.py (pas d'arbre de nesting, pas de
palette de widgets à gérer) : juste kind="personnage" (réutilise
resolve_personnage_idle_frame, même _personnage_data que le widget
"personnage" de l'éditeur document) ou kind="decor" (image fixe,
attributes.src). data-object-id/data-personnage jouent le même rôle
que data-element-id/data-personnage côté DOM (voir
static/js/screen_edit/personnage-preview.js, static/js/play/actions.js)."""
style = (
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']};"
)
if obj["kind"] == "personnage":
src = resolve_personnage_idle_frame(obj)
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
return f'<img data-object-id="{obj["id"]}" data-personnage="1" style="{style}"{src_attr}>'
src = (obj.get("attributes") or {}).get("src", "")
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
return f'<img data-object-id="{obj["id"]}" style="{style}"{src_attr}>'
@@ -0,0 +1,25 @@
import json
import db
from .ensure_scene_schema import ensure_scene_schema
def set_scene_object_personnage_data(slug, object_id, data):
"""Change le personnage Forge d'un objet de scène kind="personnage",
ou met à jour ses animations — mirror exact de
screens/elements/set_personnage_data.py, même clé _personnage_data
(même structure, réutilisée telle quelle par resolve_personnage_*,
voir screens/rendering/personnage_data.py), juste sur _scene_objects
au lieu de _screen_elements."""
ensure_scene_schema(slug)
conn = db.connect(slug)
row = conn.execute("SELECT attributes FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
if not row:
conn.close()
return
attributes = json.loads(row["attributes"] or "{}")
attributes["_personnage_data"] = json.dumps(data)
conn.execute("UPDATE _scene_objects SET attributes = ? WHERE id = ?", (json.dumps(attributes), object_id))
conn.commit()
conn.close()
@@ -0,0 +1,18 @@
import db
from .ensure_scene_schema import ensure_scene_schema
def update_scene_object_geometry(slug, object_id, x, y, width, height):
"""Position/taille en PIXELS (pas des %, voir ensure_scene_schema.py)
— utilisé par le glisser-déposer/redimensionnement de l'éditeur de
scène, mirror exact de update_element_geometry.py (éditeur document) :
ne touche qu'à la géométrie, rien d'autre."""
ensure_scene_schema(slug)
conn = db.connect(slug)
conn.execute(
"UPDATE _scene_objects SET x = ?, y = ?, width = ?, height = ? WHERE id = ?",
(x, y, width, height, object_id),
)
conn.commit()
conn.close()
+9
View File
@@ -114,6 +114,15 @@ def ensure_schema(slug):
# 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)"),