Files
Forge-Engine/screens/scenes/render_scene_object.py
T
williamandClaude Sonnet 5 7db4803b93
Build and deploy / test-python (push) Successful in 12m10s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Ajoute la fonctionnalite quiz autonome/plein ecran a la boite a quiz
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>
2026-09-13 15:07:23 +02:00

137 lines
9.0 KiB
Python
Raw 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.
import html as html_lib
from ..rendering.personnage_data import resolve_personnage_idle_frame
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
from ..rendering.quiz_box_config import resolve_quiz_box_config
from ..rendering.quiz_box_templates import render_quiz_box
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 <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 (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 <div> à 3 zones, pas un <img> — 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'<div class="dialogueBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
f'<div class="dialogueBoxHeader" data-dialogue-role="header" style="background:{html_lib.escape(style["header_bg"])}">Nom du personnage</div>'
f'<div class="dialogueBoxBody" data-dialogue-role="body" style="background:{html_lib.escape(style["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
f'<div class="dialogueBoxFooter" data-dialogue-role="footer" style="background:{html_lib.escape(style["footer_bg"])}">'
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" 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
# screens/rendering/quiz_box_config.py) — voir
# screens/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/js/play/dialogue-box-controller.js. "defaut" (Classique)
# reste pixel-identique au rendu d'avant cette extension.
style = resolve_dialogue_box_style(obj)
config = resolve_quiz_box_config(obj)
return render_quiz_box(obj, config, style)
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'<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}>')