Fusion des deux moteurs : type d'écran par écran, plus par projet
Build and deploy / test-python (push) Failing after 6m10s
Build and deploy / test-js (push) Successful in 58s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

"document" (écrans %) et "jeu_2d" (scène pixels) devient une propriété
PAR ÉCRAN (_screens.kind, migration automatique idempotente dans
ensure_schema.py, source = l'ancien game_type au niveau projet) plutôt
qu'un choix figé pour tout le jeu — un même projet peut désormais
mélanger écrans classiques et scènes 2D librement.

- routes/screens/screen_edit.py : dispatch vers l'éditeur de scène selon
  screen["kind"] (l'écran demandé), plus game["game_type"].
- screens/payload/full_game_payload.py, templates/play.html,
  static/js/play/screens.js : le rendu jouable (payload, markup, bascule
  du mode plein-écran #playFrame) décide écran par écran, y compris en
  cours de partie (changer d'écran ne recharge pas la page).
- screens/screens_repo/create_screen.py : nouveau paramètre kind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 06:21:13 +02:00
co-authored by Claude Sonnet 5
parent f767ed4fab
commit d5a59bdbd8
12 changed files with 132 additions and 80 deletions
+8 -5
View File
@@ -8,12 +8,13 @@ import screens
def render_scene_edit(slug, screen_id, game): def render_scene_edit(slug, screen_id, game):
"""Vue de l'éditeur de scène 2D — appelée par routes/screens/ """Vue de l'éditeur de scène 2D — appelée par routes/screens/
screen_edit.py (le dispatcher, selon game["game_type"]), PAS une route screen_edit.py (le dispatcher, selon screen["kind"]), PAS une route
Flask elle-même : même URL/endpoint "screen_edit" pour les deux types Flask elle-même : même URL/endpoint "screen_edit" pour les deux types
de jeu. Réutilise TEL QUEL tout ce qui est déjà générique côté logique d'écran. Réutilise TEL QUEL tout ce qui est déjà générique côté logique
de flow/animation/variables/événements (voir le plan "Fondations de flow/animation/variables/événements (voir screens/screens_repo/
d'une plateforme multi-éditeurs") — seuls les objets de scène et la ensure_schema.py — le type d'écran est une propriété PAR ÉCRAN) —
palette de nœuds proposée diffèrent de l'éditeur document.""" seuls les objets de scène et la palette de nœuds proposée diffèrent de
l'éditeur document."""
screen = screens.get_screen(slug, screen_id) screen = screens.get_screen(slug, screen_id)
if not screen: if not screen:
abort(404) abort(404)
@@ -68,6 +69,8 @@ def render_scene_edit(slug, screen_id, game):
element_action_property_labels_json=json.dumps(dict(screens.OBJECT_ACTION_PROPERTIES)), element_action_property_labels_json=json.dumps(dict(screens.OBJECT_ACTION_PROPERTIES)),
condition_operators=screens.CONDITION_OPERATORS, condition_operators=screens.CONDITION_OPERATORS,
data_operations=screens.DATA_OPERATIONS, data_operation_labels=screens.DATA_OPERATION_LABELS, data_operations=screens.DATA_OPERATIONS, data_operation_labels=screens.DATA_OPERATION_LABELS,
score_status_choices=db.SCORE_STATUS_CHOICES,
score_status_labels_json=json.dumps(db.SCORE_STATUS_LABELS),
data_operation_labels_json=json.dumps(screens.DATA_OPERATION_LABELS), data_operation_labels_json=json.dumps(screens.DATA_OPERATION_LABELS),
condition_operator_labels_json=json.dumps(screens.CONDITION_OPERATOR_LABELS), condition_operator_labels_json=json.dumps(screens.CONDITION_OPERATOR_LABELS),
action_type_labels_json=json.dumps(screens.ACTION_TYPE_LABELS_2D), action_type_labels_json=json.dumps(screens.ACTION_TYPE_LABELS_2D),
+10 -6
View File
@@ -12,15 +12,17 @@ from ..scenes.scene_edit_view import render_scene_edit
@app.route("/game/<slug>/screens/<int:screen_id>/edit") @app.route("/game/<slug>/screens/<int:screen_id>/edit")
def screen_edit(slug, screen_id): def screen_edit(slug, screen_id):
game = db.game_meta(slug) game = db.game_meta(slug)
if game["game_type"] == "jeu_2d":
# Fondations multi-éditeurs (voir db/games/get_game_type.py) :
# même route/endpoint pour les deux types de jeu, dispatch ici —
# tout le reste de cette fonction ne concerne QUE l'éditeur
# document ("document", le défaut, comportement inchangé).
return render_scene_edit(slug, screen_id, game)
screen = screens.get_screen(slug, screen_id) screen = screens.get_screen(slug, screen_id)
if not screen: if not screen:
abort(404) abort(404)
if screen["kind"] == "jeu_2d":
# Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) :
# le type d'écran ("document" ou "jeu_2d") est une propriété de
# L'ÉCRAN, pas du projet — même route/endpoint pour les deux,
# dispatch ici selon l'écran demandé — tout le reste de cette
# fonction ne concerne QUE l'éditeur document (comportement
# inchangé pour un écran "document").
return render_scene_edit(slug, screen_id, game)
elements = screens.list_elements(slug, screen_id) elements = screens.list_elements(slug, screen_id)
all_screens = screens.list_screens(slug) all_screens = screens.list_screens(slug)
element_types = screens.list_element_types(slug) element_types = screens.list_element_types(slug)
@@ -106,6 +108,8 @@ def screen_edit(slug, screen_id):
element_animations_map_json=json.dumps(element_animations_map), element_animations_map_json=json.dumps(element_animations_map),
data_operations=screens.DATA_OPERATIONS, data_operations=screens.DATA_OPERATIONS,
data_operation_labels=screens.DATA_OPERATION_LABELS, data_operation_labels=screens.DATA_OPERATION_LABELS,
score_status_choices=db.SCORE_STATUS_CHOICES,
score_status_labels_json=json.dumps(db.SCORE_STATUS_LABELS),
definitions=definitions, definitions_json=json.dumps(definitions_data), definitions=definitions, definitions_json=json.dumps(definitions_data),
selected_id=selected_id, selected_controls=selected_controls, selected_id=selected_id, selected_controls=selected_controls,
flow_nodes=flow_nodes, flow_edges=flow_edges, flow_nodes=flow_nodes, flow_edges=flow_edges,
+7 -7
View File
@@ -27,18 +27,18 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
conception), l'identifiant réel du visiteur pour une partie publique conception), l'identifiant réel du visiteur pour une partie publique
(voir core/player_identity.py) — chaque objet/variable per_player (voir core/player_identity.py) — chaque objet/variable per_player
renvoie alors la valeur propre à CE joueur.""" renvoie alors la valeur propre à CE joueur."""
# Fondations multi-éditeurs — un jeu "jeu_2d" (voir db/games/ # Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : un
# get_game_type.py) construit "elements" depuis _scene_objects # écran "jeu_2d" construit "elements" depuis _scene_objects (objets de
# (objets de scène en pixels) plutôt que _screen_elements (DOM en %) ; # scène en pixels) plutôt que _screen_elements (DOM en %) — décidé
# ÉCRAN PAR ÉCRAN (s["kind"]), un même jeu peut mélanger les deux ;
# tout le reste de cette fonction (flows/animations/variables/data) # tout le reste de cette fonction (flows/animations/variables/data)
# est déjà générique et inchangé pour les deux types de jeu. # est déjà générique et inchangé pour les deux types d'écran.
is_scene_game = db.get_game_type(slug) == "jeu_2d"
screens_ = list_screens(slug) screens_ = list_screens(slug)
payload_screens = [] payload_screens = []
flows = {} flows = {}
animations = {} animations = {}
for s in screens_: for s in screens_:
if is_scene_game: if s["kind"] == "jeu_2d":
objects = list_scene_objects(slug, s["id"]) objects = list_scene_objects(slug, s["id"])
for o in objects: for o in objects:
o["rendered_html"] = render_scene_object(o) o["rendered_html"] = render_scene_object(o)
@@ -80,7 +80,7 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
"edges": list_flow_edges(slug, s["id"]), "edges": list_flow_edges(slug, s["id"]),
} }
animations[str(s["id"])] = list_animation_clips(slug, s["id"]) animations[str(s["id"])] = list_animation_clips(slug, s["id"])
if is_scene_game: if s["kind"] == "jeu_2d":
for o in list_scene_objects(slug, s["id"]): for o in list_scene_objects(slug, s["id"]):
if o["kind"] == "personnage": if o["kind"] == "personnage":
personnage_animations[str(o["id"])] = resolve_personnage_animations(o) personnage_animations[str(o["id"])] = resolve_personnage_animations(o)
+3 -3
View File
@@ -3,7 +3,7 @@ import db
from .ensure_schema import ensure_schema from .ensure_schema import ensure_schema
def create_screen(slug, name, is_template=False): def create_screen(slug, name, is_template=False, kind="document"):
ensure_schema(slug) ensure_schema(slug)
conn = db.connect(slug) conn = db.connect(slug)
if is_template: if is_template:
@@ -12,8 +12,8 @@ def create_screen(slug, name, is_template=False):
max_order = conn.execute("SELECT MAX(order_index) AS m FROM _screens WHERE is_template = 0").fetchone()["m"] max_order = conn.execute("SELECT MAX(order_index) AS m FROM _screens WHERE is_template = 0").fetchone()["m"]
order_index = (max_order or 0) + 1 order_index = (max_order or 0) + 1
conn.execute( conn.execute(
"INSERT INTO _screens (name, order_index, is_template) VALUES (?, ?, ?)", "INSERT INTO _screens (name, order_index, is_template, kind) VALUES (?, ?, ?, ?)",
(name, order_index, 1 if is_template else 0), (name, order_index, 1 if is_template else 0, kind),
) )
screen_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"] screen_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
conn.commit() conn.commit()
+9
View File
@@ -123,6 +123,15 @@ def ensure_schema(slug):
# (jamais lu par son rendu) — voir screens/scenes/. # (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_width INTEGER NOT NULL DEFAULT 960")
conn.execute("ALTER TABLE _screens ADD COLUMN scene_height INTEGER NOT NULL DEFAULT 540") conn.execute("ALTER TABLE _screens ADD COLUMN scene_height INTEGER NOT NULL DEFAULT 540")
if "kind" not in screen_cols:
# Fusion des moteurs — "document" (écran %) ou "jeu_2d" (scène
# pixels) devient une propriété PAR ÉCRAN plutôt que par projet
# (voir db/games/get_game_type.py, désormais lu UNIQUEMENT ici,
# pour la rétro-compatibilité) : un même jeu peut mélanger les
# deux types d'écran librement (voir routes/screens/screen_edit.py,
# qui dispatche maintenant sur screen["kind"]).
conn.execute("ALTER TABLE _screens ADD COLUMN kind TEXT NOT NULL DEFAULT 'document'")
conn.execute("UPDATE _screens SET kind = ?", (db.get_game_type(slug),))
action_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_actions)").fetchall()} action_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_actions)").fetchall()}
for col_name, col_def in [ for col_name, col_def in [
("target_element_id", "INTEGER REFERENCES _screen_elements(id)"), ("target_element_id", "INTEGER REFERENCES _screen_elements(id)"),
+14
View File
@@ -37,6 +37,7 @@ function showScreen(id) {
el.style.display = 'block'; el.style.display = 'block';
window.currentScreenId = id; window.currentScreenId = id;
applyScreenAspect(el.dataset.aspect || '9/16'); applyScreenAspect(el.dataset.aspect || '9/16');
applyScreenKind(el.dataset.kind || 'document');
} }
// Phase 7 — nettoie les animations de sprites de l'écran quitté (sinon // Phase 7 — nettoie les animations de sprites de l'écran quitté (sinon
// une boucle setInterval reste active sur un élément désormais caché, // une boucle setInterval reste active sur un élément désormais caché,
@@ -83,6 +84,19 @@ function applyScreenAspect(aspect) {
} }
} }
// Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : le
// type d'écran est désormais choisi PAR ÉCRAN — changer d'écran ne
// recharge pas la page, donc #playFrame doit pouvoir basculer entre sa
// taille fixe en pixels (scène "jeu_2d", voir .sceneMode dans play.html)
// et sa taille en ratio fluide (écran "document") À CHAQUE showScreen(),
// pas une seule fois au chargement.
function applyScreenKind(kind) {
var frame = document.getElementById('playFrame');
if (frame) {
frame.classList.toggle('sceneMode', kind === 'jeu_2d');
}
}
function screenIndexById(id) { function screenIndexById(id) {
return screensData.findIndex(s => s.id === id); return screensData.findIndex(s => s.id === id);
} }
+8
View File
@@ -34,6 +34,13 @@
</div> </div>
<form method="post" action="{{ url_for('screens_new', slug=game.slug) }}" class="dashCreateBar" id="dashCreate-screens" style="display:none;"> <form method="post" action="{{ url_for('screens_new', slug=game.slug) }}" class="dashCreateBar" id="dashCreate-screens" style="display:none;">
<input type="text" name="name" placeholder="Nom du nouvel écran (ex. Accueil, Scène 1...)" required autocomplete="off"> <input type="text" name="name" placeholder="Nom du nouvel écran (ex. Accueil, Scène 1...)" required autocomplete="off">
<!-- Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) :
chaque écran choisit son propre type à la création, librement,
plus le jeu entier. -->
<select name="kind">
<option value="document">Écran classique (%, quiz/formulaire/contenu)</option>
<option value="jeu_2d">Scène 2D (pixels, RPG/serious game)</option>
</select>
<button type="submit" class="primary">Créer</button> <button type="submit" class="primary">Créer</button>
</form> </form>
{% if screen_list %} {% if screen_list %}
@@ -45,6 +52,7 @@
<tr> <tr>
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td> <td>
{{ '🧩' if s.kind == 'jeu_2d' else '📄' }}
<a href="{{ url_for('screen_edit', slug=game.slug, screen_id=s.id) }}">{{ s.name }}</a> <a href="{{ url_for('screen_edit', slug=game.slug, screen_id=s.id) }}">{{ s.name }}</a>
{% if loop.first %} <span class="tag is-info is-light">★ Départ</span>{% endif %} {% if loop.first %} <span class="tag is-info is-light">★ Départ</span>{% endif %}
</td> </td>
+19 -18
View File
@@ -27,18 +27,19 @@
} }
#playFrame{ #playFrame{
position:relative; overflow:hidden; background:#0b0d12; position:relative; overflow:hidden; background:#0b0d12;
{% if game.game_type == 'jeu_2d' %}
/* Fondations multi-éditeurs — une scène jeu_2d a une taille FIXE en
pixels (voir scene_width/scene_height, ensure_schema.py), pas un
ratio fluide : #playFrame remplit simplement tout le viewport, la
scène elle-même se centre dedans (voir .playScreen ci-dessous et
la boucle d'écrans jeu_2d plus bas). */
width:100vw; height:100vh;
{% else %}
width: min(100vw, calc(100vh * var(--ratio-w, 9) / var(--ratio-h, 16))); width: min(100vw, calc(100vh * var(--ratio-w, 9) / var(--ratio-h, 16)));
height: min(100vh, calc(100vw * var(--ratio-h, 16) / var(--ratio-w, 9))); height: min(100vh, calc(100vw * var(--ratio-h, 16) / var(--ratio-w, 9)));
{% endif %}
} }
/* Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : le
type d'écran ("document"/"jeu_2d") est désormais choisi PAR ÉCRAN, pas
figé pour toute la page au chargement — changer d'écran ne recharge
pas la page (voir showScreen() dans static/js/play/screens.js), donc
la taille de #playFrame doit pouvoir basculer EN COURS DE PARTIE.
.sceneMode (posée/enlevée par showScreen() selon screen.kind) donne à
une scène jeu_2d sa taille FIXE en pixels (scene_width/scene_height) —
elle-même se centre dedans (voir .playScreen.playScene ci-dessous) —
au lieu du ratio fluide par défaut ci-dessus. */
#playFrame.sceneMode{ width:100vw; height:100vh; }
.playScreen{ position:absolute; inset:0; display:none; } .playScreen{ position:absolute; inset:0; display:none; }
.playScreen.playScene{ inset:auto; top:50%; left:50%; transform:translate(-50%, -50%); background:#1a1d24; } .playScreen.playScene{ inset:auto; top:50%; left:50%; transform:translate(-50%, -50%); background:#1a1d24; }
.playElement{ .playElement{
@@ -70,27 +71,25 @@
<body> <body>
<div id="playRoot"> <div id="playRoot">
<div id="playFrame"> <div id="playFrame">
{% if game.game_type == 'jeu_2d' %} {% for s in screens_data %}
<!-- Fondations multi-éditeurs — scène jeu_2d : objets déjà entièrement {% if s.kind == 'jeu_2d' %}
<!-- Fusion des moteurs — écran "jeu_2d" : objets déjà entièrement
positionnés en pixels absolus par render_scene_object.py (voir positionnés en pixels absolus par render_scene_object.py (voir
screens/scenes/), pas de wrapper .playElement/elstyle (spécifiques screens/scenes/), pas de wrapper .playElement/elstyle (spécifiques
au %/DOM de l'éditeur document) : juste le HTML tel quel. --> au %/DOM d'un écran "document") : juste le HTML tel quel. -->
{% for s in screens_data %} <div class="playScreen playScene" data-screen-id="{{ s.id }}" data-kind="jeu_2d" style="width:{{ s.scene_width }}px; height:{{ s.scene_height }}px;">
<div class="playScreen playScene" data-screen-id="{{ s.id }}" style="width:{{ s.scene_width }}px; height:{{ s.scene_height }}px;">
{% for obj in s.elements %}{{ obj.rendered_html|safe }}{% endfor %} {% for obj in s.elements %}{{ obj.rendered_html|safe }}{% endfor %}
</div> </div>
{% endfor %}
{% else %} {% else %}
{% for s in screens_data %} <div class="playScreen" data-screen-id="{{ s.id }}" data-kind="document" data-aspect="{{ s.preview_aspect or '9/16' }}">
<div class="playScreen" data-screen-id="{{ s.id }}" data-aspect="{{ s.preview_aspect or '9/16' }}">
{% for el in s.elements if not el.parent_id %} {% for el in s.elements if not el.parent_id %}
<div class="playElement" data-el-id="{{ el.id }}" data-anim-target="{{ el.id }}" style="{{ el|elstyle }}"> <div class="playElement" data-el-id="{{ el.id }}" data-anim-target="{{ el.id }}" style="{{ el|elstyle }}">
<div class="playElementInner" style="{{ el|eltransform }}">{{ el.rendered_html|safe }}</div> <div class="playElementInner" style="{{ el|eltransform }}">{{ el.rendered_html|safe }}</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %}
{% endif %} {% endif %}
{% endfor %}
{% if not screens_data %} {% if not screens_data %}
<div id="emptyState">Aucun écran défini pour l'instant — retourne dans l'éditeur pour en créer un.</div> <div id="emptyState">Aucun écran défini pour l'instant — retourne dans l'éditeur pour en créer un.</div>
{% endif %} {% endif %}
@@ -118,6 +117,8 @@
runtimePayload: {{ runtime_payload_url|tojson }}, runtimePayload: {{ runtime_payload_url|tojson }},
flowNodeRunData: {{ flow_node_run_data_url|tojson }}, flowNodeRunData: {{ flow_node_run_data_url|tojson }},
flowNodeRunVariable: {{ flow_node_run_variable_url|tojson }}, flowNodeRunVariable: {{ flow_node_run_variable_url|tojson }},
flowNodeRunScore: {{ flow_node_run_score_url|tojson }},
flowNodeRunStatus: {{ flow_node_run_status_url|tojson }},
flowNodeRunAddRow: {{ flow_node_run_add_row_url|tojson }}, flowNodeRunAddRow: {{ flow_node_run_add_row_url|tojson }},
}; };
</script> </script>
+7 -4
View File
@@ -46,7 +46,10 @@ def test_document_editor_shows_animal_characters_to_admin(game, client):
def _create_jeu2d_game_as(post_client, name, slug_owner_client=None): def _create_jeu2d_game_as(post_client, name, slug_owner_client=None):
resp = post_client.post("/games/new", data={"name": name, "game_type": "jeu_2d"}, follow_redirects=False) # Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : un
# jeu n'a plus de type propre — seul l'écran créé ensuite par chaque
# test (screens.create_screen(..., kind="jeu_2d")) est une scène.
resp = post_client.post("/games/new", data={"name": name}, follow_redirects=False)
assert resp.status_code == 302 assert resp.status_code == 302
return resp.headers["Location"].rstrip("/").split("/")[-1] return resp.headers["Location"].rstrip("/").split("/")[-1]
@@ -56,7 +59,7 @@ def test_scene_editor_hides_animal_characters_from_a_user_account(client, user_c
with user_client.session_transaction() as sess: with user_client.session_transaction() as sess:
import auth import auth
auth.set_project_slug(sess["user_id"], slug) auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1") screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
html = user_client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True) html = user_client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" not in html assert "animal-bunny-01" not in html
@@ -66,7 +69,7 @@ def test_scene_object_add_rejects_animal_character_for_a_user_account(client, us
with user_client.session_transaction() as sess: with user_client.session_transaction() as sess:
import auth import auth
auth.set_project_slug(sess["user_id"], slug) auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1") screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
resp = user_client.post( resp = user_client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add", f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "animal-bunny-01"}, data={"kind": "personnage", "forge_character": "animal-bunny-01"},
@@ -76,7 +79,7 @@ def test_scene_object_add_rejects_animal_character_for_a_user_account(client, us
def test_scene_object_add_allows_animal_character_for_admin(client, tmp_game_slug_cleanup): def test_scene_object_add_allows_animal_character_for_admin(client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin")) slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin"))
screen_id = screens.create_screen(slug, "Scène 1") screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
resp = client.post( resp = client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add", f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "animal-bunny-01"}, data={"kind": "personnage", "forge_character": "animal-bunny-01"},
+37 -27
View File
@@ -1,38 +1,48 @@
"""Type de jeu (fondations multi-éditeurs) : "document" (éditeur générique """Type d'écran (fusion des moteurs, voir screens/screens_repo/
actuel) ou "jeu_2d" (futur éditeur de scène) — choisi à la création, ensure_schema.py) : "document" (éditeur générique, éléments en %) ou
jamais modifié ensuite. Réutilise _meta comme is_public_played, aucune "jeu_2d" (éditeur de scène, objets en pixels) — choisi PAR ÉCRAN, à sa
migration pour les jeux déjà créés (retombent sur "document").""" création, plus par jeu. Un jeu peut librement mélanger les deux."""
import db import db
import screens
def test_default_game_type_is_document(client, game): def test_default_screen_kind_is_document(client, game):
assert db.get_game_type(game) == "document" screen_id = screens.create_screen(game, "Accueil")
assert db.game_meta(game)["game_type"] == "document" assert screens.get_screen(game, screen_id)["kind"] == "document"
def test_create_game_with_jeu_2d_type(tmp_game_slug_cleanup): def test_create_screen_with_jeu_2d_kind(tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(db.create_game("pytest_jeu2d_test", game_type="jeu_2d")) slug = tmp_game_slug_cleanup(db.create_game("pytest_jeu2d_test"))
assert db.get_game_type(slug) == "jeu_2d" screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
assert db.game_meta(slug)["game_type"] == "jeu_2d" screen = screens.get_screen(slug, screen_id)
assert screen["kind"] == "jeu_2d"
def test_games_new_route_creates_jeu_2d_game(client): def test_a_game_can_mix_both_screen_kinds(tmp_game_slug_cleanup):
resp = client.post("/games/new", data={"name": "pytest_route_jeu2d", "game_type": "jeu_2d"}, follow_redirects=False) slug = tmp_game_slug_cleanup(db.create_game("pytest_mixed_test"))
doc_id = screens.create_screen(slug, "Accueil", kind="document")
scene_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
assert screens.get_screen(slug, doc_id)["kind"] == "document"
assert screens.get_screen(slug, scene_id)["kind"] == "jeu_2d"
def test_screens_new_route_creates_jeu_2d_screen(client, game):
resp = client.post(
f"/game/{game}/screens/new",
data={"name": "pytest_route_jeu2d", "kind": "jeu_2d"},
follow_redirects=False,
)
assert resp.status_code == 302 assert resp.status_code == 302
slug = resp.headers["Location"].rstrip("/").split("/")[-1] screen_id = int(resp.headers["Location"].rstrip("/").split("/")[-2])
try: assert screens.get_screen(game, screen_id)["kind"] == "jeu_2d"
assert db.get_game_type(slug) == "jeu_2d"
finally:
import shutil
shutil.rmtree(db.game_dir(slug), ignore_errors=True)
def test_games_new_route_rejects_unknown_game_type(client): def test_screens_new_route_rejects_unknown_kind(client, game):
resp = client.post("/games/new", data={"name": "pytest_route_bad_type", "game_type": "n_importe_quoi"}, follow_redirects=False) resp = client.post(
f"/game/{game}/screens/new",
data={"name": "pytest_route_bad_kind", "kind": "n_importe_quoi"},
follow_redirects=False,
)
assert resp.status_code == 302 assert resp.status_code == 302
slug = resp.headers["Location"].rstrip("/").split("/")[-1] screen_id = int(resp.headers["Location"].rstrip("/").split("/")[-2])
try: assert screens.get_screen(game, screen_id)["kind"] == "document"
assert db.get_game_type(slug) == "document"
finally:
import shutil
shutil.rmtree(db.game_dir(slug), ignore_errors=True)
+6 -6
View File
@@ -1,8 +1,8 @@
"""Vue de l'éditeur de scène 2D (routes/scenes/scene_edit_view.py) — """Vue de l'éditeur de scène 2D (routes/scenes/scene_edit_view.py) —
appelée par routes/screens/screen_edit.py selon game_type, RÉUTILISE tel appelée par routes/screens/screen_edit.py selon screen["kind"] (fusion des
quel le moteur de logique de flow partagé (flow-editor.js/tabs-and-blocks.js/ moteurs, voir screens/screens_repo/ensure_schema.py), RÉUTILISE tel quel
animation-timeline.js) — voir le plan "Fondations d'une plateforme le moteur de logique de flow partagé (flow-editor.js/tabs-and-blocks.js/
multi-éditeurs".""" animation-timeline.js)."""
import json import json
import re import re
@@ -12,9 +12,9 @@ from conftest import screen_edit_js_bundle
def _create_jeu2d_game(client, tmp_game_slug_cleanup, name="pytest_scene_edit"): def _create_jeu2d_game(client, tmp_game_slug_cleanup, name="pytest_scene_edit"):
resp = client.post("/games/new", data={"name": name, "game_type": "jeu_2d"}, follow_redirects=False) resp = client.post("/games/new", data={"name": name}, follow_redirects=False)
slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1]) slug = tmp_game_slug_cleanup(resp.headers["Location"].rstrip("/").split("/")[-1])
screen_id = screens.create_screen(slug, "Scène 1") screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
return slug, screen_id return slug, screen_id
+4 -4
View File
@@ -1,6 +1,6 @@
"""Objets de scène (_scene_objects) — fondations de l'éditeur de scène 2D """Objets de scène (_scene_objects) — fondations de l'éditeur de scène 2D
(jeux "jeu_2d", voir db/games/get_game_type.py). Positions en PIXELS (écrans "jeu_2d", voir screens/screens_repo/ensure_schema.py). Positions
fixes (pas des %, voir ensure_scene_schema.py). kind="personnage" en PIXELS fixes (pas des %, voir ensure_scene_schema.py). kind="personnage"
réutilise TELLE QUELLE la structure _personnage_data et les fonctions réutilise TELLE QUELLE la structure _personnage_data et les fonctions
resolve_personnage_* déjà écrites pour le widget "personnage" de resolve_personnage_* déjà écrites pour le widget "personnage" de
l'éditeur document (Phase 8) — même bibliothèque Forge, même moteur l'éditeur document (Phase 8) — même bibliothèque Forge, même moteur
@@ -10,8 +10,8 @@ import screens
def _create_jeu2d_game(tmp_game_slug_cleanup, name="pytest_scene_test"): def _create_jeu2d_game(tmp_game_slug_cleanup, name="pytest_scene_test"):
slug = tmp_game_slug_cleanup(db.create_game(name, game_type="jeu_2d")) slug = tmp_game_slug_cleanup(db.create_game(name))
screen_id = screens.create_screen(slug, "Scène 1") screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
return slug, screen_id return slug, screen_id