Files
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

218 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Any
from flask import abort, g, render_template, request, url_for
import auth
import db
import game_engine
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/game/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 game_engine/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 = game_engine.get_screen(slug, screen_id)
if not screen:
abort(404)
objects = game_engine.list_scene_objects(slug, screen_id)
for o in objects:
o["rendered_html"] = game_engine.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 game_engine.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/game/scene_edit.html).
any_quiz_box_fullscreen = any(o["quiz_fullscreen"] for o in objects)
# "Monde" (voir game_engine.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/game/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 = game_engine.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 = game_engine.resolve_collision_settings(selected_obj)
if selected_obj["kind"] == "personnage":
personnage_data = game_engine.resolve_personnage_data(selected_obj)
personnage_commands = game_engine.resolve_personnage_commands(selected_obj)
personnage_role = game_engine.resolve_personnage_role(selected_obj)
elif selected_obj["kind"] in _UI_WIDGET_KINDS:
dialogue_box_style = game_engine.resolve_dialogue_box_style(selected_obj)
if selected_obj["kind"] == "quiz_box":
# Plein écran/minuteur/modèle (demande explicite : quiz RH
# autonome) — voir game_engine/rendering/quiz_box_config.py,
# PANNEAU distinct de "🎨 Style" ci-dessus (couleurs/police,
# partagé avec dialogue_box/score_widget).
quiz_box_config = game_engine.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"]: game_engine.resolve_personnage_animations(o) for o in objects if o["kind"] == "personnage"
}
flow_blocks = game_engine.list_flow_blocks(slug, screen_id)
flow_nodes = game_engine.list_flow_nodes(slug, screen_id)
flow_edges = game_engine.list_flow_edges(slug, screen_id)
animation_clips = game_engine.list_animation_clips(slug, screen_id)
global_variables = db.list_global_variables(slug)
custom_events = db.list_custom_events(slug)
custom_event_usages = game_engine.list_custom_event_usages(slug)
custom_events_map = {e["id"]: {"name": e["name"]} for e in custom_events}
all_screens = game_engine.list_screens(slug)
screens_map = {s["id"]: s["name"] for s in all_screens}
blocks_view = game_engine.blocks_view(flow_blocks, flow_nodes, object_labels)
# Sprites animaux CraftPix (game_engine.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").
is_admin = g.current_user["role"] == "admin"
sprite_library = game_engine.SPRITE_LIBRARY if is_admin else game_engine.PUBLIC_SPRITE_LIBRARY
sprite_library_families = game_engine.sprite_gallery_families(sprite_library)
# Images de fond CraftPix (game_engine.BACKGROUND_LIBRARY) : même règle
# d'accès que les sprites animaux — réservées au rôle admin.
background_library = game_engine.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 game_engine/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/game/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(
"game/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 game_engine.DEFAULT_PERSONNAGE_COMMANDS,
personnage_role=personnage_role or game_engine.DEFAULT_PERSONNAGE_ROLE,
personnage_role_choices=game_engine.PERSONNAGE_ROLES,
personnage_role_labels=game_engine.PERSONNAGE_ROLE_LABELS,
collision_settings=collision_settings or game_engine.DEFAULT_COLLISION_SETTINGS,
dialogue_box_style=dialogue_box_style or game_engine.DEFAULT_DIALOGUE_BOX_STYLE,
dialogue_box_font_weight_choices=game_engine.FONT_WEIGHT_CHOICES,
quiz_box_config=quiz_box_config or game_engine.DEFAULT_QUIZ_BOX_CONFIG,
quiz_box_timer_modes=game_engine.TIMER_MODES,
quiz_box_dialog_templates=game_engine.QUIZ_BOX_DIALOG_TEMPLATES,
quiz_box_dialog_template_labels=game_engine.QUIZ_BOX_DIALOG_TEMPLATE_LABELS,
quiz_box_page_templates=game_engine.QUIZ_BOX_PAGE_TEMPLATES,
quiz_box_page_template_labels=game_engine.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=game_engine.ELEMENT_ORIENTATION_VALUES,
element_orientation_labels_json=db.json_for_script(game_engine.ELEMENT_ORIENTATION_LABELS),
element_visibility_values=game_engine.ELEMENT_VISIBILITY_VALUES,
element_visibility_labels_json=db.json_for_script(game_engine.ELEMENT_VISIBILITY_LABELS),
trigger_events=game_engine.TRIGGER_EVENTS_2D,
action_type_labels=game_engine.ACTION_TYPE_LABELS_2D,
object_action_properties=game_engine.OBJECT_ACTION_PROPERTIES,
element_action_property_labels_json=db.json_for_script(dict(game_engine.OBJECT_ACTION_PROPERTIES)),
condition_operators=game_engine.CONDITION_OPERATORS,
data_operations=game_engine.DATA_OPERATIONS,
data_operation_labels=game_engine.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(game_engine.DATA_OPERATION_LABELS),
condition_operator_labels_json=db.json_for_script(game_engine.CONDITION_OPERATOR_LABELS),
action_type_labels_json=db.json_for_script(game_engine.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=game_engine.ANIMATE_CSS_CATALOG,
animate_css_labels_json=db.json_for_script(game_engine.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"]): game_engine.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/game/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(game_engine.list_named_scene_object_names(slug)),
# Nouveaux déclencheurs/actions (voir game_engine/rendering/
# collision_rules.py, game_engine/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(game_engine.SURBRILLANCE_LABELS),
video_mode_labels_json=db.json_for_script(game_engine.VIDEO_MODE_LABELS),
user_assets_options_json=db.json_for_script(user_assets_options),
screen_triggers=game_engine.resolve_screen_triggers(screen),
)