Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 1m0s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Le document de cadrage produit cible des formateurs non techniques créant
des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de
logique en nœuds, timeline d'animation, définitions d'objets/relations,
templates réutilisables) est une complexité hors cible que l'effort
d'ingénierie récent avait déjà abandonnée au profit du jeu_2d.

- Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire
  Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only)
- Suppression en bloc des modules exclusifs au document : routes/elements,
  routes/element_types, routes/objects, routes/legacy_actions,
  screens/elements, screens/element_types, screens/widgets,
  screens/legacy_actions, le rendu render_element_html.py et son cluster,
  templates/screen_edit.html, templates/game_dashboard.html,
  flow-editor.js/tabs-and-blocks.js/animation-timeline.js
- Dashboard toujours simplifié (un seul mode possible désormais)
- Tests document-only supprimés, tests de logique partagée (flow,
  événements personnalisés, animations) retargetés sur des écrans jeu_2d
- Aucune régression jeu_2d : 299 tests passent

Carte d'onboarding retravaillée : argumentaire RH non technique (liste à
coche, badge "Compatible LMS"), taille et interaction de retournement
ajustées.
This commit is contained in:
william
2026-09-04 20:56:32 +02:00
parent 00f7191a8b
commit 50835a18e2
221 changed files with 1239 additions and 12245 deletions
+2
View File
@@ -9,4 +9,6 @@ from . import (
scene_object_collision_rules,
scene_object_name,
scene_object_dialogue_box_style,
scene_collision_rule_objects,
scene_object_names_list,
)
+61
View File
@@ -0,0 +1,61 @@
import screens
_DEFAULT_OBJECT_LABELS = {"personnage": "Personnage", "fond": "Fond"}
_COLLISION_EXCLUDED_KINDS = ("fond", "dialogue_box", "quiz_box", "score_widget")
def build_collision_rule_objects(objects):
"""Une carte "🧩 Collision" (voir templates/scene_edit.html) par objet
ÉLIGIBLE (jamais un "fond", jamais le personnage "joueur" — ce sont
TOUJOURS les AUTRES objets qui réagissent à SA présence, voir
screens/rendering/collision_rules.py) — factorisé ici plutôt que
dupliqué : utilisé au premier rendu de la page (routes/scenes/
scene_edit_view.py) ET par l'endpoint AJAX ci-dessous (appelé après
ajout/suppression/changement de rôle ou de nom, voir static/js/scenes/
scene-editor.js) pour reconstruire la liste SANS recharger la page.
`objects` doit déjà porter "rendered_html" (voir
screens.render_scene_object) sur chaque élément.
resolve_personnage_role retombe sur "joueur" pour TOUT personnage sans
rôle explicitement enregistré (voir screens/rendering/
personnage_role.py — comportement voulu pour une scène à un seul
personnage, posé avant l'ajout de ce réglage). Appliqué tel quel,
ÇA CASSE dès qu'une scène a plusieurs personnages sans rôle explicite
(le cas de toute scène créée avant ce réglage — un personnage ajouté
depuis l'éditeur reçoit maintenant TOUJOURS un rôle "pnj" explicite,
voir add_scene_object.py) : ils retomberaient TOUS sur "joueur" et
disparaîtraient donc TOUS de cette liste. On ne considère donc comme
"joueur" implicite QUE LE PREMIER personnage sans rôle explicite (le
plus ancien, z_index croissant comme `objects`) — les suivants sans
rôle explicite sont traités comme "pnj" ici, sans rien réécrire en
base."""
implicit_joueur_claimed = any(
(o.get("attributes") or {}).get("_personnage_role") == "joueur"
for o in objects if o["kind"] == "personnage"
)
result = []
for o in objects:
if o["kind"] in _COLLISION_EXCLUDED_KINDS:
continue
role = None
if o["kind"] == "personnage":
explicit_role = (o.get("attributes") or {}).get("_personnage_role")
if explicit_role in screens.PERSONNAGE_ROLES:
role = explicit_role
elif not implicit_joueur_claimed:
role = "joueur"
implicit_joueur_claimed = True
else:
role = "pnj"
if role == "joueur":
continue
result.append({
"id": o["id"],
"name": o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"),
"kind": o["kind"],
"role": role,
"thumbnail_html": o["rendered_html"],
"rules": screens.resolve_collision_rules(o),
})
return result
@@ -0,0 +1,24 @@
from flask import jsonify
import screens
from core.flask_app import app
from .collision_rule_objects import build_collision_rule_objects
@app.route("/game/<slug>/screens/<int:screen_id>/collision-rule-objects", methods=["GET"])
def scene_collision_rule_objects(slug, screen_id):
"""Liste à jour des cartes "🧩 Collision" (voir COLLISION_RULE_OBJECTS,
templates/scene_edit.html) — appelée en AJAX après un ajout/suppression
d'objet, ou un changement de rôle/nom (voir static/js/scenes/
scene-editor.js) qui peut faire apparaître/disparaître un objet de
cette liste (rôle passé à/depuis "joueur", voir
build_collision_rule_objects) ou changer son libellé affiché (nom).
Recalculée entièrement plutôt que diffée : la liste reste petite (un
objet de scène par carte, jamais un "fond"/widget d'UI) et cette même
fonction est déjà appelée à chaque rendu complet de la page."""
objects = screens.list_scene_objects(slug, screen_id)
for o in objects:
o["rendered_html"] = screens.render_scene_object(o)
return jsonify(build_collision_rule_objects(objects))
+13 -33
View File
@@ -5,6 +5,8 @@ from flask import render_template, request, abort, g
import db
import screens
from .collision_rule_objects import build_collision_rule_objects
def render_scene_edit(slug, screen_id, game):
"""Vue de l'éditeur de scène 2D — appelée par routes/screens/
@@ -22,22 +24,17 @@ def render_scene_edit(slug, screen_id, game):
for o in objects:
o["rendered_html"] = screens.render_scene_object(o)
# "Monde" (voir static/js/play/personnage-controller.js::
# forgeUpdateSceneWorldSize, même principe côté serveur) : un "fond"
# posé à sa taille RÉELLE (souvent plus grande que la scène, voir
# add_scene_object.py) rend visible/joignable, dans CET éditeur, tout
# ce qui dépasse la scène nominale plutôt que de le rogner (le
# "Monde" (voir screens.resolve_scene_world_size, réutilisé aussi par
# la miniature de "Mes écrans", routes/games/game_dashboard.py) : un
# "fond" posé à sa taille RÉELLE (souvent plus grande que la scène,
# voir add_scene_object.py) rend visible/joignable, dans CET éditeur,
# tout ce qui dépasse la scène nominale plutôt que de le rogner (le
# glisser-déposer le permet déjà depuis le fix précédent — sans
# agrandir le canevas ici, cette partie restait invisible/injoignable
# à la souris). Le rectangle "🎥 Champ de la caméra" (voir
# templates/scene_edit.html) marque ensuite où s'arrête la scène
# nominale À L'INTÉRIEUR de ce monde plus grand.
world_width, world_height = screen["scene_width"], screen["scene_height"]
for o in objects:
if o["kind"] == "fond":
world_width = max(world_width, o["x"] + o["width"])
world_height = max(world_height, o["y"] + o["height"])
world_width, world_height = int(world_width), int(world_height)
world_width, world_height = screens.resolve_scene_world_size(screen, objects)
selected_id = request.args.get("selected", type=int)
personnage_data = None
@@ -66,28 +63,11 @@ def render_scene_edit(slug, screen_id, game):
o["id"]: screens.resolve_personnage_animations(o) for o in objects if o["kind"] == "personnage"
}
# "🧩 Collision" (voir templates/scene_edit.html) : une carte par objet
# ÉLIGIBLE (jamais un "fond", jamais le personnage "joueur" — ce sont
# TOUJOURS les AUTRES objets qui réagissent à SA présence, voir
# screens/rendering/collision_rules.py) — construit ici plutôt que
# réutiliser `objects` tel quel, pour ne poser resolve_personnage_role
# qu'une fois par objet personnage (déjà lu ci-dessus pour l'objet
# SÉLECTIONNÉ, mais nécessaire ici pour TOUS les personnages).
collision_rule_objects = []
for o in objects:
if o["kind"] in ("fond", "dialogue_box", "quiz_box", "score_widget"):
continue
role = screens.resolve_personnage_role(o) if o["kind"] == "personnage" else None
if role == "joueur":
continue
collision_rule_objects.append({
"id": o["id"],
"name": o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"),
"kind": o["kind"],
"role": role,
"thumbnail_html": o["rendered_html"],
"rules": screens.resolve_collision_rules(o),
})
# "🧩 Collision" — voir build_collision_rule_objects() (routes/scenes/
# collision_rule_objects.py, factorisé pour être réutilisé par
# l'endpoint AJAX scene_collision_rule_objects.py, appelé après ajout/
# suppression/changement de rôle ou de nom sans recharger la page).
collision_rule_objects = build_collision_rule_objects(objects)
flow_blocks = screens.list_flow_blocks(slug, screen_id)
flow_nodes = screens.list_flow_nodes(slug, screen_id)
+30 -2
View File
@@ -1,4 +1,4 @@
from flask import request, redirect, url_for
from flask import request, redirect, url_for, jsonify
import screens
@@ -10,7 +10,20 @@ from core.sprite_gate import forbid_admin_only_character, forbid_admin_only_back
def scene_object_add(slug, screen_id):
"""Pose un nouvel objet sur la scène — mirror de routes/elements/
element_add.py, mais pour _scene_objects (voir screens/scenes/
add_scene_object.py)."""
add_scene_object.py).
Appelée en AJAX (voir addSceneObject(), static/js/scenes/scene-editor.js)
plutôt qu'en soumission de formulaire classique — sans ça, poser un
objet rechargeait TOUTE la page : perceptible comme un "vidage" de
l'éditeur de collision/quêtes le temps du rechargement, et perdait tout
état JS en cours (modale ouverte, etc.). Répond en JSON avec de quoi
ajouter l'objet au canevas SANS recharger — jamais sélectionné/ouvert
dans le panneau de propriétés pour autant (ça, voir
onSceneObjectMouseDown, reste un rechargement pour l'instant). La liste
"🧩 Collision" n'est PAS incluse ici : addSceneObject() la redemande
ensuite à routes/scenes/scene_collision_rule_objects.py (même endpoint
que pour un changement de rôle/nom, ou une suppression) plutôt que de
dupliquer ici la logique de build_collision_rule_objects()."""
kind = request.form.get("kind", "personnage")
forge_character = request.form.get("forge_character")
background_slug = request.form.get("background_slug")
@@ -21,7 +34,22 @@ def scene_object_add(slug, screen_id):
new_id = screens.add_scene_object(
slug, screen_id, kind=kind, forge_character=forge_character, background_slug=background_slug,
)
if request.headers.get("X-Requested-With") == "fetch":
obj = screens.get_scene_object(slug, new_id)
return jsonify({
"id": new_id, "kind": obj["kind"],
"x": obj["x"], "y": obj["y"], "width": obj["width"], "height": obj["height"],
"rendered_html": screens.render_scene_object(obj),
# Relance l'aperçu animé tout de suite (voir personnage-preview.js
# ::restartPersonnageIdlePreview) — sans ça, un personnage
# fraîchement posé restait figé sur sa première frame jusqu'au
# prochain rechargement de page (qui n'arrive plus jamais, voir
# addSceneObject() côté JS).
"animations": screens.resolve_personnage_animations(obj) if obj["kind"] == "personnage" else None,
})
# Même route que l'éditeur document (/game/<slug>/screens/<id>/edit,
# endpoint "screen_edit") — c'est CE dispatcher qui bascule vers la
# vue de scène selon game_type, voir routes/screens/screen_edit.py.
# Conservé pour tout appelant non-JS (aucun connu actuellement, mais
# ne coûte rien à garder en secours).
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id))
+16 -1
View File
@@ -1,4 +1,4 @@
from flask import redirect, url_for
from flask import redirect, request, url_for, jsonify
import screens
@@ -7,9 +7,24 @@ from core.flask_app import app
@app.route("/game/<slug>/scene-objects/<int:object_id>/delete", methods=["POST"])
def scene_object_delete(slug, object_id):
"""Supprime un objet de scène — bouton 🗑️ du panneau de propriétés
(voir templates/scene_edit.html), toujours pour l'objet SÉLECTIONNÉ (le
seul dont ce bouton est affiché).
Appelée en AJAX (deleteSceneObject(), static/js/scenes/scene-editor.js)
plutôt qu'en soumission de formulaire classique — sinon même
"rechargé en douceur" par pjax.js (qui intercepte tout <form>, voir son
commentaire d'en-tête), ça remplaçait TOUT <main> (canevas + onglets
Collision/Quêtes/Événements), perceptible comme un "vidage" de
l'éditeur le temps du remplacement."""
obj = screens.get_scene_object(slug, object_id)
if not obj:
if request.headers.get("X-Requested-With") == "fetch":
return jsonify({"ok": False}), 404
return redirect(url_for("index"))
screen_id = obj["scene_id"]
screens.delete_scene_object(slug, object_id)
if request.headers.get("X-Requested-With") == "fetch":
return jsonify({"ok": True, "id": object_id})
# Secours pour tout appelant non-JS (aucun connu actuellement).
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id))
+20
View File
@@ -0,0 +1,20 @@
from flask import jsonify
import screens
from core.flask_app import app
@app.route("/game/<slug>/scene-object-names", methods=["GET"])
def scene_object_names_list(slug):
"""Noms d'objets de scène à jour, pour repeupler le sélecteur "qui
parle" de l'éditeur de dialogue (voir SCENE_OBJECT_NAMES_JSON,
templates/scene_edit.html, questSpeakerOptions() dans static/js/
quests/quest-editor.js) — cette variable n'est posée qu'une fois au
chargement complet de la page (HORS de #builder3, jamais rafraîchie
par une sélection d'objet sans rechargement, voir goToSelected()) :
sans cet endpoint, renommer un personnage puis rouvrir la modale de
dialogue dans la MÊME session ne faisait jamais apparaître son nouveau
nom, tant qu'aucun vrai rechargement de page n'avait eu lieu entre
temps — appelée en AJAX depuis saveSceneObjectName()."""
return jsonify(screens.list_named_scene_object_names(slug))