import html as html_lib
from ..rendering.personnage_data import resolve_personnage_idle_frame
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
def render_scene_object(obj):
"""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/js/screen_edit/personnage-preview.js, static/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/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/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
: 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 (screens/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" + screens/rendering/
# dialogue_box_style.py) : un
à 3 zones, pas un
![]()
— voir
# static/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.
style = 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(style['font_family'])}; font-size:{style['font_size']}px; "
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])};"
)
return (
# role="dialog"/aria-live (RGAA/WCAG 4.1.3) : le texte de
# dialogue change dynamiquement (innerHTML/textContent, voir
# static/js/play/dialogue-box-controller.js) sans jamais
# recharger la page — sans annonce explicite, un lecteur
# d'écran ne détecte aucun changement.
f'
'
)
if obj["kind"] == "quiz_box":
# Widget "❓ Boîte à quiz" (voir "🖥️ Interface") — réutilise le
# MÊME style que "💬 Boîte de dialogue" (resolve_dialogue_box_style/
# set_scene_object_dialogue_box_style, une seule "🎨 Style" pour
# les deux) : header = "Quête :
" (comme demandé), corps =
# la question ET ses choix de réponse ensemble (remplis EN JEU par
# static/js/play/dialogue-box-controller.js, data-quiz-role sert de
# sélecteur) — pas de pied (une question se résout au clic sur un
# choix, pas de "Suivant"). Masqué par défaut au jeu, comme la
# boîte de dialogue.
style = 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(style['font_family'])}; font-size:{style['font_size']}px; "
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])};"
)
return (
f''
)
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/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).
style = 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(style['font_family'])}; font-size:{style['font_size']}px; "
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])}; "
f"background:{html_lib.escape(style['header_bg'])};"
)
return (
f''
f'🏆 0'
f'
'
)
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'
')
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'
')