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/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. 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/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 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. 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/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'
' 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'' )