Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
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:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user