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
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>
147 lines
9.3 KiB
Python
147 lines
9.3 KiB
Python
import html as html_lib
|
||
from typing import Any
|
||
|
||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||
from ..rendering.personnage_data import resolve_personnage_idle_frame
|
||
from ..rendering.quiz_box_config import resolve_quiz_box_config
|
||
from ..rendering.quiz_box_templates import render_quiz_box
|
||
|
||
|
||
def render_scene_object(obj: dict[str, Any]) -> str:
|
||
"""HTML d'un objet de scène — positionnement en PIXELS ABSOLUS (pas de
|
||
% fluide, voir ensure_scene_schema.py), mirror volontairement plus
|
||
simple que render_element_html.py (pas d'arbre de nesting, pas de
|
||
palette de widgets à gérer) : juste kind="personnage" (réutilise
|
||
resolve_personnage_idle_frame, même _personnage_data que le widget
|
||
"personnage" de l'éditeur document) ou kind="decor" (image fixe,
|
||
attributes.src). data-object-id/data-personnage jouent le même rôle
|
||
que data-element-id/data-personnage côté DOM (voir
|
||
static/game/js/screen_edit/personnage-preview.js, static/game/js/play/actions.js)."""
|
||
style = (
|
||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']};"
|
||
)
|
||
# data-element-id (en plus de data-object-id) : réutilise TELS QUELS
|
||
# tous les sélecteurs [data-element-id=...] déjà écrits pour l'éditeur
|
||
# document (collision, animation de sprites, "Modifier un élément") —
|
||
# voir static/game/js/play/actions.js/conditions.js. Sans risque de
|
||
# collision d'id : un jeu est ENTIÈREMENT "document" (_screen_elements)
|
||
# OU "jeu_2d" (_scene_objects), jamais les deux à la fois.
|
||
# data-anim-target (Timeline d'animation, Bug corrigé) : applyAnimationClip()
|
||
# (static/game/js/play/screens.js) et son équivalent d'aperçu éditeur
|
||
# (animation-timeline.js) sélectionnent TOUJOURS leur cible via
|
||
# [data-anim-target=...] (jamais data-element-id/data-object-id, posé
|
||
# sur .canvasElement/.playElement pour le DOM, voir visible_attrs.py) —
|
||
# un objet de scène n'ayant pas de wrapper équivalent, cet attribut est
|
||
# posé directement sur son <img> : SANS lui, aucun clip de Timeline (ni
|
||
# même une action de flow "Jouer une animation") ne trouvait jamais sa
|
||
# cible pour un objet de scène, quel que soit son type d'animation.
|
||
# class="sceneObjectSprite" (object-fit:contain, voir static/style.css) :
|
||
# les frames CraftPix (game_engine/labels/animal_sprite_library.py) sont de
|
||
# GRANDS canevas très paddés (~788×504 px, très différent des sprites
|
||
# Kenney déjà bien recadrés, ~96×128 px) — sans object-fit, l'image
|
||
# remplirait quand même son cadre (width/height:100%, voir la règle
|
||
# `.canvasElementInner > img` dans scene_edit.html) mais DÉFORMÉE,
|
||
# rendant le personnage minuscule/écrasé dans sa boîte de sélection.
|
||
if obj["kind"] == "dialogue_box":
|
||
# Widget d'interface (voir "🖥️ Interface" + game_engine/rendering/
|
||
# dialogue_box_style.py) : un <div> à 3 zones, pas un <img> — voir
|
||
# static/game/js/play/dialogue-box-controller.js pour comment header/
|
||
# body/footer sont remplis EN JEU (data-dialogue-role sert de
|
||
# sélecteur), masqué par défaut au jeu (voir static/style.css,
|
||
# `.playScreen .dialogueBoxWidget`) jusqu'à ce qu'une règle de
|
||
# collision "dialogue" ait un dialogue à afficher. Toujours VISIBLE
|
||
# dans l'éditeur (avec un texte d'exemple) pour pouvoir le
|
||
# positionner/styliser.
|
||
box_theme = resolve_dialogue_box_style(obj)
|
||
box_style = (
|
||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||
f"font-family:{html_lib.escape(box_theme['font_family'])}; font-size:{box_theme['font_size']}px; "
|
||
f"font-weight:{box_theme['font_weight']}; color:{html_lib.escape(box_theme['text_color'])};"
|
||
)
|
||
return (
|
||
# role="dialog"/aria-live (RGAA/WCAG 4.1.3) : le texte de
|
||
# dialogue change dynamiquement (innerHTML/textContent, voir
|
||
# static/game/js/play/dialogue-box-controller.js) sans jamais
|
||
# recharger la page — sans annonce explicite, un lecteur
|
||
# d'écran ne détecte aucun changement.
|
||
f'<div class="dialogueBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" '
|
||
f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||
f'<div class="dialogueBoxHeader" data-dialogue-role="header" '
|
||
f'style="background:{html_lib.escape(box_theme["header_bg"])}">Nom du personnage</div>'
|
||
f'<div class="dialogueBoxBody" data-dialogue-role="body" '
|
||
f'style="background:{html_lib.escape(box_theme["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
|
||
f'<div class="dialogueBoxFooter" data-dialogue-role="footer" '
|
||
f'style="background:{html_lib.escape(box_theme["footer_bg"])}">'
|
||
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" '
|
||
f'onclick="forgeDialogueBoxAdvance({obj["id"]})">Suivant →</button>'
|
||
f"</div></div>"
|
||
)
|
||
if obj["kind"] == "quiz_box":
|
||
# Widget "❓ Boîte à quiz" (voir "🖥️ Interface") — réutilise le
|
||
# MÊME style couleur/police que "💬 Boîte de dialogue"
|
||
# (resolve_dialogue_box_style/set_scene_object_dialogue_box_style,
|
||
# une seule "🎨 Style" pour les deux, appliquée à TOUS les
|
||
# modèles ci-dessous). Le rendu HTML lui-même dépend du modèle
|
||
# choisi (plein écran/minuteur/modèle, voir
|
||
# game_engine/rendering/quiz_box_config.py) — voir
|
||
# game_engine/rendering/quiz_box_templates.py pour le détail de
|
||
# chacun (thèmes complets HTML+CSS+JS, demande explicite) ; tous
|
||
# respectent le même contrat de data-quiz-role, lu EN JEU par
|
||
# static/game/js/play/dialogue-box-controller.js. "defaut" (Classique)
|
||
# reste pixel-identique au rendu d'avant cette extension.
|
||
box_theme = resolve_dialogue_box_style(obj)
|
||
config = resolve_quiz_box_config(obj)
|
||
return render_quiz_box(obj, config, box_theme)
|
||
if obj["kind"] == "score_widget":
|
||
# Widget "🏆 Score" (voir "🖥️ Interface") — toujours VISIBLE une
|
||
# fois posé (pas de collision/déclencheur, contrairement aux deux
|
||
# widgets ci-dessus) : affiche en continu le score de la partie en
|
||
# cours (voir static/game/js/play/dialogue-box-controller.js::
|
||
# forgeUpdateAllScoreWidgets). Même style que les deux autres
|
||
# widgets, header_bg sert de fond unique (pas de zone body/footer).
|
||
box_theme = resolve_dialogue_box_style(obj)
|
||
box_style = (
|
||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||
f"font-family:{html_lib.escape(box_theme['font_family'])}; font-size:{box_theme['font_size']}px; "
|
||
f"font-weight:{box_theme['font_weight']}; color:{html_lib.escape(box_theme['text_color'])}; "
|
||
f"background:{html_lib.escape(box_theme['header_bg'])};"
|
||
)
|
||
return (
|
||
f'<div class="scoreWidget" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||
f'🏆 <span data-score-role="value">0</span>'
|
||
f"</div>"
|
||
)
|
||
if obj["kind"] == "personnage":
|
||
src = resolve_personnage_idle_frame(obj)
|
||
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
|
||
# alt (RGAA/WCAG 1.1.1) : le nom donné par l'auteur ("ℹ️
|
||
# Informations", voir scene_object_names.py) s'il existe — c'est
|
||
# déjà ce qu'un lecteur d'écran devrait annoncer ("qui parle" dans
|
||
# un dialogue de quête réutilise le même nom). alt="" (décoratif)
|
||
# sinon, jamais le nom de fichier brut par défaut du navigateur.
|
||
alt_attr = f' alt="{html_lib.escape(obj.get("name") or "")}"'
|
||
return (
|
||
f'<img class="sceneObjectSprite" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||
f'data-anim-target="{obj["id"]}" data-personnage="1" style="{style}"{src_attr}{alt_attr}>'
|
||
)
|
||
src = (obj.get("attributes") or {}).get("src", "")
|
||
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
|
||
# "fond" (voir add_scene_object.py) : même image fixe que "decor",
|
||
# seule la classe change — .sceneBackground (static/style.css) la
|
||
# rend insensible au clic (pointer-events:none), pour qu'elle ne
|
||
# bloque jamais un clic destiné à un objet posé par-dessus, même
|
||
# étendue sur tout le "monde" (voir personnage-controller.js, la
|
||
# caméra qui la suit en défilant).
|
||
css_class = "sceneObjectSprite sceneBackground" if obj["kind"] == "fond" else "sceneObjectSprite"
|
||
# "fond" est TOUJOURS décoratif (un décor de scène entier, jamais une
|
||
# information à transmettre) — un "decor" nommé par l'auteur utilise
|
||
# ce nom, sinon alt="" plutôt qu'aucun attribut du tout.
|
||
alt_attr = ' alt=""' if obj["kind"] == "fond" else f' alt="{html_lib.escape(obj.get("name") or "")}"'
|
||
return (
|
||
f'<img class="{css_class}" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||
f'data-anim-target="{obj["id"]}" style="{style}"{src_attr}{alt_attr}>'
|
||
)
|