- Bug corrigé : les panneaux flottants "🧩 Objets"/"⚙️ Propriétés" sont en position:fixed (hors du flux flex de .builder3) — ils flottaient PAR-DESSUS le canevas sans jamais réduire sa largeur, rendant sa partie droite/gauche inaccessible au clic/glisser tant qu'un panneau restait ouvert. .builderCanvasArea réserve maintenant leur largeur (marge) dès qu'un panneau est ouvert (:has()). - Boîte à quiz : header = "Quête : <titre>" (au lieu du texte de la question), corps = la question ET ses choix ensemble. - Mauvaise réponse : ne bloque plus JAMAIS la progression (bug signalé : "je suis obligé de bien répondre sinon j'avance pas") — surligne la bonne réponse en vert (le choix cliqué en rouge s'il était faux) puis avance automatiquement après un court délai, sans jamais octroyer de points. Un second clic pendant la révélation est ignoré. - Animations : la boîte à quiz rejoue une entrée (pop-in) à CHAQUE nouvelle question, la bonne réponse pulse en vert, une mauvaise réponse "secoue" en rouge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
131 lines
8.7 KiB
Python
131 lines
8.7 KiB
Python
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 <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 "quete" 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 (
|
||
f'<div class="dialogueBoxWidget" 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 que "💬 Boîte de dialogue" (resolve_dialogue_box_style/
|
||
# set_scene_object_dialogue_box_style, une seule "🎨 Style" pour
|
||
# les deux) : header = "Quête : <titre>" (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'<div class="quizBoxWidget" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||
f'<div class="quizBoxHeader" data-quiz-role="header" style="background:{html_lib.escape(style["header_bg"])}">Quête : Titre de la quête</div>'
|
||
f'<div class="quizBoxBody" data-quiz-role="body" style="background:{html_lib.escape(style["body_bg"])}">'
|
||
f'<p class="quizBoxQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||
f'<div class="quizBoxChoices" data-quiz-role="choices">'
|
||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||
f'</div></div></div>'
|
||
)
|
||
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 ""
|
||
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}>')
|
||
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"
|
||
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}>')
|