Introduit la double categorie de modeles (boite de dialogue / page de quiz plein ecran) avec plein ecran, minuteur, score integre et ecran de resultat pour les modeles page ; ajoute les modeles "Manga" (boite et page) et "Classique" (page), pilotables aussi par l'assistant IA Ruby. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
199 lines
12 KiB
Python
199 lines
12 KiB
Python
import json
|
||
|
||
from flask import render_template, request, abort, g, url_for
|
||
|
||
import auth
|
||
import db
|
||
import screens
|
||
|
||
|
||
def render_scene_edit(slug, screen_id, game):
|
||
"""Vue de l'éditeur de scène 2D — appelée par routes/screens/
|
||
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
|
||
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 screens/screens_repo/
|
||
ensure_schema.py — le type d'écran est une propriété PAR ÉCRAN) —
|
||
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)
|
||
if not screen:
|
||
abort(404)
|
||
objects = screens.list_scene_objects(slug, screen_id)
|
||
for o in objects:
|
||
o["rendered_html"] = screens.render_scene_object(o)
|
||
# "Plein écran" (demande explicite : la boîte à quiz doit occuper
|
||
# tout le canevas de CET éditeur, pas seulement au jeu, dès que ce
|
||
# réglage est coché) — calculé pour TOUS les objets (pas
|
||
# seulement le sélectionné) : plusieurs quiz plein écran sur le
|
||
# même écran resteraient rares/déconseillés, mais rien n'empêche
|
||
# techniquement d'en poser plusieurs.
|
||
o["quiz_fullscreen"] = o["kind"] == "quiz_box" and screens.resolve_quiz_box_config(o)["fullscreen"]
|
||
# La "notion de caméra disparaît" (demande explicite) : le cadre "🎥
|
||
# Champ de la caméra" n'a plus aucun sens dès qu'un quiz plein écran
|
||
# est posé sur cet écran, caméra ou pas — masqué dans ce cas (voir
|
||
# templates/scene_edit.html).
|
||
any_quiz_box_fullscreen = any(o["quiz_fullscreen"] for o in objects)
|
||
|
||
# "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 = screens.resolve_scene_world_size(screen, objects)
|
||
|
||
selected_id = request.args.get("selected", type=int)
|
||
personnage_data = None
|
||
personnage_commands = None
|
||
personnage_role = None
|
||
collision_settings = None
|
||
dialogue_box_style = None
|
||
quiz_box_config = None
|
||
selected_obj = next((o for o in objects if o["id"] == selected_id), None) if selected_id else None
|
||
_UI_WIDGET_KINDS = ("dialogue_box", "quiz_box", "score_widget")
|
||
if selected_obj:
|
||
if selected_obj["kind"] not in _UI_WIDGET_KINDS:
|
||
collision_settings = screens.resolve_collision_settings(selected_obj)
|
||
if selected_obj["kind"] == "personnage":
|
||
personnage_data = screens.resolve_personnage_data(selected_obj)
|
||
personnage_commands = screens.resolve_personnage_commands(selected_obj)
|
||
personnage_role = screens.resolve_personnage_role(selected_obj)
|
||
elif selected_obj["kind"] in _UI_WIDGET_KINDS:
|
||
dialogue_box_style = screens.resolve_dialogue_box_style(selected_obj)
|
||
if selected_obj["kind"] == "quiz_box":
|
||
# Plein écran/minuteur/modèle (demande explicite : quiz RH
|
||
# autonome) — voir screens/rendering/quiz_box_config.py,
|
||
# PANNEAU distinct de "🎨 Style" ci-dessus (couleurs/police,
|
||
# partagé avec dialogue_box/score_widget).
|
||
quiz_box_config = screens.resolve_quiz_box_config(selected_obj)
|
||
|
||
_DEFAULT_OBJECT_LABELS = {"personnage": "Personnage", "fond": "Fond"}
|
||
object_labels = {
|
||
o["id"]: (o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"))
|
||
for o in objects
|
||
}
|
||
element_animations_map = {
|
||
o["id"]: screens.resolve_personnage_animations(o) for o in objects if o["kind"] == "personnage"
|
||
}
|
||
|
||
flow_blocks = screens.list_flow_blocks(slug, screen_id)
|
||
flow_nodes = screens.list_flow_nodes(slug, screen_id)
|
||
flow_edges = screens.list_flow_edges(slug, screen_id)
|
||
animation_clips = screens.list_animation_clips(slug, screen_id)
|
||
global_variables = db.list_global_variables(slug)
|
||
custom_events = db.list_custom_events(slug)
|
||
custom_event_usages = screens.list_custom_event_usages(slug)
|
||
custom_events_map = {e["id"]: {"name": e["name"]} for e in custom_events}
|
||
all_screens = screens.list_screens(slug)
|
||
screens_map = {s["id"]: s["name"] for s in all_screens}
|
||
blocks_view = screens.blocks_view(flow_blocks, flow_nodes, object_labels)
|
||
|
||
# Sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY) : réservés au
|
||
# rôle admin (licence CraftPix, voir core/sprite_gate.py et le plan
|
||
# "Bibliothèque de sprites animaux CraftPix").
|
||
sprite_library = screens.SPRITE_LIBRARY if g.current_user["role"] == "admin" else screens.PUBLIC_SPRITE_LIBRARY
|
||
sprite_library_families = screens.sprite_gallery_families(sprite_library)
|
||
# Images de fond CraftPix (screens.BACKGROUND_LIBRARY) : même règle
|
||
# d'accès que les sprites animaux — réservées au rôle admin.
|
||
background_library = screens.BACKGROUND_LIBRARY if g.current_user["role"] == "admin" else {}
|
||
# "Mes assets" (voir auth/user_assets_dir.py) : par COMPTE, jamais
|
||
# filtré par rôle contrairement à sprite_library/background_library
|
||
# ci-dessus (rien à voir avec le catalogue Forge réservé à l'admin).
|
||
all_user_assets = auth.list_user_assets(g.current_user["id"])
|
||
# Sélecteur "asset_url" des actions "son"/"vidéo" (voir static/js/
|
||
# scenes/collision-rules-editor.js) — TOUS les assets (pas seulement
|
||
# les images) : url_for calculée ici (pas possible côté JS), le reste
|
||
# (id, nom) sert juste à l'affichage.
|
||
for a in all_user_assets:
|
||
a["url"] = url_for("serve_user_asset", user_id=a["user_id"], filename=a["filename"])
|
||
a["kind"] = auth.user_asset_kind(a["filename"])
|
||
user_assets_options = [
|
||
{"id": a["id"], "url": a["url"], "name": a["original_name"] or a["filename"], "kind": a["kind"]}
|
||
for a in all_user_assets
|
||
]
|
||
# "Mes assets" : sous-sections Fonds/Décors/Sons/Vidéos (demande
|
||
# explicite : "voir, utiliser ou supprimer des son, image et vidéo",
|
||
# PUIS "un bouton d'import séparé pour les images de fond et les
|
||
# objets" — sans ça, un clic posait TOUJOURS l'image en kind="decor",
|
||
# jamais en "fond", voir screens/scenes/add_scene_object.py). Une
|
||
# image importée AVANT ce réglage (scene_kind NULL) retombe sur
|
||
# "decor", comportement inchangé. Les sons/vidéos ne posent rien sur
|
||
# la scène (ils s'utilisent depuis le sélecteur de fichier des
|
||
# actions "son"/"vidéo", voir user_assets_options ci-dessus) — les
|
||
# quatre listent néanmoins un bouton "Supprimer" (voir
|
||
# deleteUserAsset(), static/js/scenes/scene-editor.js).
|
||
user_assets_fond = [a for a in all_user_assets if a["kind"] == "image" and a["scene_kind"] == "fond"]
|
||
user_assets_decor = [a for a in all_user_assets if a["kind"] == "image" and a["scene_kind"] != "fond"]
|
||
user_assets_audio = [a for a in all_user_assets if a["kind"] == "audio"]
|
||
user_assets_video = [a for a in all_user_assets if a["kind"] == "video"]
|
||
|
||
return render_template(
|
||
"scene_edit.html",
|
||
game=game, screen=screen, all_screens=all_screens,
|
||
world_width=world_width, world_height=world_height,
|
||
any_quiz_box_fullscreen=any_quiz_box_fullscreen,
|
||
objects=objects, selected_id=selected_id, personnage_data=personnage_data,
|
||
personnage_commands=personnage_commands or screens.DEFAULT_PERSONNAGE_COMMANDS,
|
||
personnage_role=personnage_role or screens.DEFAULT_PERSONNAGE_ROLE,
|
||
personnage_role_choices=screens.PERSONNAGE_ROLES, personnage_role_labels=screens.PERSONNAGE_ROLE_LABELS,
|
||
collision_settings=collision_settings or screens.DEFAULT_COLLISION_SETTINGS,
|
||
dialogue_box_style=dialogue_box_style or screens.DEFAULT_DIALOGUE_BOX_STYLE,
|
||
dialogue_box_font_weight_choices=screens.FONT_WEIGHT_CHOICES,
|
||
quiz_box_config=quiz_box_config or screens.DEFAULT_QUIZ_BOX_CONFIG,
|
||
quiz_box_timer_modes=screens.TIMER_MODES,
|
||
quiz_box_dialog_templates=screens.QUIZ_BOX_DIALOG_TEMPLATES,
|
||
quiz_box_dialog_template_labels=screens.QUIZ_BOX_DIALOG_TEMPLATE_LABELS,
|
||
quiz_box_page_templates=screens.QUIZ_BOX_PAGE_TEMPLATES,
|
||
quiz_box_page_template_labels=screens.QUIZ_BOX_PAGE_TEMPLATE_LABELS,
|
||
sprite_library=sprite_library, sprite_library_families=sprite_library_families,
|
||
sprite_library_json=json.dumps(sprite_library),
|
||
background_library=background_library,
|
||
user_assets_fond=user_assets_fond, user_assets_decor=user_assets_decor,
|
||
user_assets_audio=user_assets_audio, user_assets_video=user_assets_video,
|
||
element_animations_map_json=json.dumps(element_animations_map),
|
||
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
|
||
element_orientation_labels_json=json.dumps(screens.ELEMENT_ORIENTATION_LABELS),
|
||
element_visibility_values=screens.ELEMENT_VISIBILITY_VALUES,
|
||
element_visibility_labels_json=json.dumps(screens.ELEMENT_VISIBILITY_LABELS),
|
||
trigger_events=screens.TRIGGER_EVENTS_2D, action_type_labels=screens.ACTION_TYPE_LABELS_2D,
|
||
object_action_properties=screens.OBJECT_ACTION_PROPERTIES,
|
||
element_action_property_labels_json=json.dumps(dict(screens.OBJECT_ACTION_PROPERTIES)),
|
||
condition_operators=screens.CONDITION_OPERATORS,
|
||
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),
|
||
condition_operator_labels_json=json.dumps(screens.CONDITION_OPERATOR_LABELS),
|
||
action_type_labels_json=json.dumps(screens.ACTION_TYPE_LABELS_2D),
|
||
elements_labels_json=json.dumps(object_labels), screens_map_json=json.dumps(screens_map),
|
||
flow_nodes=flow_nodes, flow_edges=flow_edges,
|
||
flow_nodes_json=json.dumps(flow_nodes), flow_edges_json=json.dumps(flow_edges),
|
||
flow_blocks_json=json.dumps(flow_blocks), blocks_view=blocks_view,
|
||
animation_clips=animation_clips, animation_clips_json=json.dumps(animation_clips),
|
||
animate_css_catalog=screens.ANIMATE_CSS_CATALOG, animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS),
|
||
global_variables=global_variables, global_variable_types=db.GLOBAL_VARIABLE_TYPES,
|
||
custom_events=custom_events, custom_event_usages=custom_event_usages,
|
||
custom_events_map_json=json.dumps(custom_events_map),
|
||
definitions=db.list_definitions(slug),
|
||
definitions_json=json.dumps({str(d["id"]): screens.data_definition_options(slug, d["id"]) for d in db.list_definitions(slug)}),
|
||
definitions_names_json=json.dumps({d["id"]: d["name"] for d in db.list_definitions(slug)}),
|
||
# "qui parle" dans un dialogue de déclencheur (voir "ℹ️
|
||
# Informations", static/js/triggers/trigger-editor.js) : les noms
|
||
# donnés à N'IMPORTE QUEL objet de scène (personnage, décor,
|
||
# fond), toutes scènes du jeu confondues.
|
||
scene_object_names_json=json.dumps(screens.list_named_scene_object_names(slug)),
|
||
# Nouveaux déclencheurs/actions (voir screens/rendering/
|
||
# collision_rules.py, screens/rendering/screen_triggers.py) :
|
||
# vocabulaire des nouvelles actions feuilles + assets disponibles
|
||
# pour "son"/"vidéo", posés en JS comme le reste ci-dessus.
|
||
surbrillance_labels_json=json.dumps(screens.SURBRILLANCE_LABELS),
|
||
video_mode_labels_json=json.dumps(screens.VIDEO_MODE_LABELS),
|
||
user_assets_options_json=json.dumps(user_assets_options),
|
||
screen_triggers=screens.resolve_screen_triggers(screen),
|
||
)
|