from typing import Any from flask import abort, g, render_template, request, url_for import auth import db import screens def render_scene_edit(slug: str, screen_id: int, game: dict[str, Any]) -> str: """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=db.json_for_script(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=db.json_for_script(element_animations_map), element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES, element_orientation_labels_json=db.json_for_script(screens.ELEMENT_ORIENTATION_LABELS), element_visibility_values=screens.ELEMENT_VISIBILITY_VALUES, element_visibility_labels_json=db.json_for_script(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=db.json_for_script(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=db.json_for_script(db.SCORE_STATUS_LABELS), data_operation_labels_json=db.json_for_script(screens.DATA_OPERATION_LABELS), condition_operator_labels_json=db.json_for_script(screens.CONDITION_OPERATOR_LABELS), action_type_labels_json=db.json_for_script(screens.ACTION_TYPE_LABELS_2D), elements_labels_json=db.json_for_script(object_labels), screens_map_json=db.json_for_script(screens_map), flow_nodes=flow_nodes, flow_edges=flow_edges, flow_nodes_json=db.json_for_script(flow_nodes), flow_edges_json=db.json_for_script(flow_edges), flow_blocks_json=db.json_for_script(flow_blocks), blocks_view=blocks_view, animation_clips=animation_clips, animation_clips_json=db.json_for_script(animation_clips), animate_css_catalog=screens.ANIMATE_CSS_CATALOG, animate_css_labels_json=db.json_for_script(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=db.json_for_script(custom_events_map), definitions=db.list_definitions(slug), definitions_json=db.json_for_script( {str(d["id"]): screens.data_definition_options(slug, d["id"]) for d in db.list_definitions(slug)} ), definitions_names_json=db.json_for_script({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=db.json_for_script(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=db.json_for_script(screens.SURBRILLANCE_LABELS), video_mode_labels_json=db.json_for_script(screens.VIDEO_MODE_LABELS), user_assets_options_json=db.json_for_script(user_assets_options), screen_triggers=screens.resolve_screen_triggers(screen), )