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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
559331f9cf
commit
7db4803b93
@@ -63,6 +63,11 @@ from .labels.video_modes import VIDEO_MODES, VIDEO_MODE_LABELS
|
||||
from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS, sanitize_extra_commands
|
||||
from .rendering.scene_object_names import list_named_scene_object_names
|
||||
from .rendering.dialogue_box_style import resolve_dialogue_box_style, sanitize_dialogue_box_style, DEFAULT_DIALOGUE_BOX_STYLE, FONT_WEIGHT_CHOICES
|
||||
from .rendering.quiz_box_config import (
|
||||
resolve_quiz_box_config, sanitize_quiz_box_config, DEFAULT_QUIZ_BOX_CONFIG,
|
||||
TIMER_MODES, QUIZ_BOX_DIALOG_TEMPLATES, QUIZ_BOX_DIALOG_TEMPLATE_LABELS,
|
||||
QUIZ_BOX_PAGE_TEMPLATES, QUIZ_BOX_PAGE_TEMPLATE_LABELS,
|
||||
)
|
||||
|
||||
from .screens_repo.ensure_schema import ensure_schema
|
||||
from .screens_repo.list_screens import list_screens
|
||||
@@ -93,6 +98,7 @@ from .scenes.set_scene_object_collision import set_scene_object_collision
|
||||
from .scenes.set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from .scenes.set_scene_object_name import set_scene_object_name
|
||||
from .scenes.set_scene_object_dialogue_box_style import set_scene_object_dialogue_box_style
|
||||
from .scenes.set_scene_object_quiz_config import set_scene_object_quiz_config
|
||||
from .scenes.render_scene_object import render_scene_object
|
||||
from .scenes.build_collision_rule_objects import build_collision_rule_objects
|
||||
from .scenes.append_action_to_trigger import append_action_to_trigger
|
||||
|
||||
@@ -11,6 +11,7 @@ from ..rendering.collision_settings import resolve_collision_settings
|
||||
from ..rendering.collision_rules import resolve_collision_rules
|
||||
from ..rendering.screen_triggers import resolve_screen_triggers
|
||||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||||
from ..rendering.quiz_box_config import resolve_quiz_box_config
|
||||
from ..rendering.trigger_graph import list_completable_dialogue_ids
|
||||
from ..scenes.list_scene_objects import list_scene_objects
|
||||
from ..scenes.render_scene_object import render_scene_object
|
||||
@@ -62,6 +63,13 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
# collision, ni cible/source de règle — juste leur
|
||||
# style, lu par static/js/play/dialogue-box-controller.js.
|
||||
o["dialogue_box_style"] = resolve_dialogue_box_style(o)
|
||||
if o["kind"] == "quiz_box":
|
||||
# Plein écran/minuteur/modèle (voir screens/rendering/
|
||||
# quiz_box_config.py) — même convention que
|
||||
# dialogue_box_style ci-dessus (cohérence de contrat),
|
||||
# même si le JS runtime lit en pratique les data-*
|
||||
# déjà posés sur rendered_html.
|
||||
o["quiz_config"] = resolve_quiz_box_config(o)
|
||||
continue
|
||||
# Boîte de collision (voir static/js/play/conditions.js::
|
||||
# elementsOverlap) : posée sur TOUT objet de scène, pas
|
||||
@@ -83,6 +91,19 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
# colonne brute _screen_triggers (laissée telle quelle par ailleurs
|
||||
# dans `s`, comme le reste des colonnes _screens).
|
||||
s["screen_triggers"] = resolve_screen_triggers(s)
|
||||
# "La notion de caméra disparaît" quand un quiz plein écran existe
|
||||
# sur cet écran (demande explicite) — pas seulement le widget lui-
|
||||
# même : ".playScreen.playScene" (templates/play.html), dont la
|
||||
# taille EST la "caméra" (scene_width/scene_height) et qui reste
|
||||
# centré via un `transform`, casse `position:fixed` pour tout
|
||||
# descendant (voir static/js/play/dialogue-box-controller.js::
|
||||
# forgeEscapeCameraForFullscreen). Exposé ici pour que play.html
|
||||
# rende directement CET écran en plein viewport, sans transform ni
|
||||
# taille caméra — le réglage "Caméra (px)" ne s'applique alors plus
|
||||
# du tout tant que ce quiz reste plein écran.
|
||||
s["any_quiz_box_fullscreen"] = any(
|
||||
o["kind"] == "quiz_box" and o.get("quiz_config", {}).get("fullscreen") for o in objects
|
||||
)
|
||||
payload_screens.append({**s, "elements": objects})
|
||||
|
||||
# Le graphe de logique et les animations sont, eux, lus pour TOUS les
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import json
|
||||
|
||||
# Réglages "❓ Boîte à quiz" propres au quiz AUTONOME/plein écran (demande
|
||||
# explicite : "étendre les fonctionnalités de la boîte à quiz pour les RH
|
||||
# qui veulent juste faire un quiz") — DISTINCT de "🎨 Style"
|
||||
# (dialogue_box_style.py, partagé avec dialogue_box/score_widget) : ce
|
||||
# fichier ne gère QUE plein écran/minuteur/modèle, jamais police/couleur.
|
||||
#
|
||||
# DEUX catégories de modèles bien distinctes (demande explicite, précisée
|
||||
# après coup — les 5 premiers modèles construits avant cette précision
|
||||
# étaient tous de la première catégorie) :
|
||||
# - "dialog_template" (modèles "boîte de dialogue", visibles SEULEMENT
|
||||
# quand fullscreen=False) : variantes de forme/couleur/interaction sur
|
||||
# une structure HTML qui reste globalement la même que "Classique".
|
||||
# - "page_template" (modèles "page de quiz", visibles SEULEMENT quand
|
||||
# fullscreen=True) : thème complet et autonome — police, formes,
|
||||
# couleurs ET structure HTML totalement propres à CHAQUE modèle,
|
||||
# aucune structure partagée entre eux ni avec les modèles "boîte".
|
||||
# Les deux réglages sont conservés INDÉPENDAMMENT (jamais écrasés l'un par
|
||||
# l'autre) : activer/désactiver le plein écran change juste lequel des
|
||||
# deux s'applique, sans jamais perdre le choix de l'autre.
|
||||
DEFAULT_QUIZ_BOX_CONFIG = {
|
||||
"fullscreen": False,
|
||||
"timer_mode": "aucun",
|
||||
"timer_seconds": 20,
|
||||
"dialog_template": "defaut",
|
||||
"page_template": "classique",
|
||||
}
|
||||
|
||||
# "aucun" : pas de minuteur (réglage par défaut, jamais imposé — demande
|
||||
# explicite du créateur). "question" : redémarre à chaque question.
|
||||
# "quiz" : un seul compte à rebours pour l'ensemble du quiz.
|
||||
TIMER_MODES = ["aucun", "question", "quiz"]
|
||||
|
||||
# "defaut" (Classique) = le CSS/HTML déjà existant, AUCUN changement
|
||||
# visuel pour un quiz non reconfiguré (voir quiz_box_templates.py) —
|
||||
# "Verre dépoli"/"Néon"/"Minimal"/"Ludique" ont été retirés (demande
|
||||
# explicite : "supprime tous les modele de boite sauf le base").
|
||||
# "manga_dialogue" (Manga) : pendant, en petite carte flottante (jamais
|
||||
# plein écran), du modèle "page de quiz" "Manga" — validé par le créateur
|
||||
# via une maquette interactive avant intégration (voir
|
||||
# screens/rendering/quiz_box_templates.py::render_quiz_box_manga_dialogue).
|
||||
# Clé DIFFÉRENTE du modèle "page" "manga" (registre d'effets JS partagé
|
||||
# entre les deux catégories, voir static/js/play/quiz-box-templates.js)
|
||||
# — reste proche de la structure de "Classique" (demande explicite :
|
||||
# les modèles "boîte" ne changent que forme/couleur/interaction).
|
||||
QUIZ_BOX_DIALOG_TEMPLATES = ["defaut", "manga_dialogue"]
|
||||
QUIZ_BOX_DIALOG_TEMPLATE_LABELS = {
|
||||
"defaut": "Classique",
|
||||
"manga_dialogue": "Manga",
|
||||
}
|
||||
|
||||
# "classique" (Classique) : modèle de PAGE DE QUIZ de base — sobre/
|
||||
# professionnel, reprend les couleurs par défaut du modèle "boîte de
|
||||
# dialogue" de base (demande explicite : "respecte les couleurs du modele
|
||||
# de boite de base pour le modele de page de base") plutôt que sa propre
|
||||
# palette, contrairement à "manga" qui est un thème pleinement autonome.
|
||||
# "manga" : thème/typographie/structure entièrement dédiés, validé par le
|
||||
# créateur via une maquette interactive avant intégration (voir
|
||||
# screens/rendering/quiz_box_templates.py::render_quiz_box_page_manga).
|
||||
QUIZ_BOX_PAGE_TEMPLATES = ["classique", "manga"]
|
||||
QUIZ_BOX_PAGE_TEMPLATE_LABELS = {
|
||||
"classique": "Classique",
|
||||
"manga": "Manga",
|
||||
}
|
||||
|
||||
_MIN_TIMER_SECONDS = 3
|
||||
_MAX_TIMER_SECONDS = 600
|
||||
|
||||
|
||||
def sanitize_quiz_box_config(raw_config):
|
||||
config = dict(DEFAULT_QUIZ_BOX_CONFIG)
|
||||
if not isinstance(raw_config, dict):
|
||||
return config
|
||||
config["fullscreen"] = bool(raw_config.get("fullscreen"))
|
||||
if raw_config.get("timer_mode") in TIMER_MODES:
|
||||
config["timer_mode"] = raw_config["timer_mode"]
|
||||
try:
|
||||
config["timer_seconds"] = max(_MIN_TIMER_SECONDS, min(_MAX_TIMER_SECONDS, int(raw_config.get("timer_seconds"))))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if raw_config.get("dialog_template") in QUIZ_BOX_DIALOG_TEMPLATES:
|
||||
config["dialog_template"] = raw_config["dialog_template"]
|
||||
if raw_config.get("page_template") in QUIZ_BOX_PAGE_TEMPLATES:
|
||||
config["page_template"] = raw_config["page_template"]
|
||||
return config
|
||||
|
||||
|
||||
def resolve_quiz_box_config(obj):
|
||||
raw = (obj.get("attributes") or {}).get("_quiz_box_config")
|
||||
if not raw:
|
||||
return dict(DEFAULT_QUIZ_BOX_CONFIG)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return dict(DEFAULT_QUIZ_BOX_CONFIG)
|
||||
return sanitize_quiz_box_config(parsed)
|
||||
@@ -0,0 +1,337 @@
|
||||
import html as html_lib
|
||||
|
||||
# ---------- "❓ Boîte à quiz" — DEUX catégories de modèles bien distinctes
|
||||
# (voir screens/rendering/quiz_box_config.py) :
|
||||
# - "boîte de dialogue" (dialog_template, visibles SEULEMENT hors plein
|
||||
# écran) : "defaut" (Classique) + "manga_dialogue" (Manga), voir
|
||||
# render_quiz_box_manga_dialogue plus bas — "Verre dépoli"/"Néon"/
|
||||
# "Minimal"/"Ludique" ont été retirés (demande explicite : "supprime
|
||||
# tous les modele de boite sauf le base") ;
|
||||
# - "page de quiz" (page_template, visibles SEULEMENT en plein écran,
|
||||
# voir render_quiz_box_page_classique/render_quiz_box_page_manga plus
|
||||
# bas) : thème complet et autonome, structure HTML totalement propre
|
||||
# à CHAQUE modèle, aucune structure partagée avec les modèles
|
||||
# "boîte" ni entre eux.
|
||||
# Chaque modèle a son propre bloc CSS (static/style.css) + éventuellement
|
||||
# son propre effet JS (static/js/play/quiz-box-templates.js), mais TOUS
|
||||
# doivent respecter le même "contrat" de points d'ancrage pour que le
|
||||
# moteur PARTAGÉ (static/js/play/dialogue-box-controller.js) fonctionne
|
||||
# quel que soit le modèle choisi :
|
||||
# - le conteneur racine porte class="quizBoxWidget[ quizBoxWidget--X]",
|
||||
# data-object-id/data-element-id, data-quiz-template, data-fullscreen,
|
||||
# data-timer-mode, data-timer-seconds (voir _quiz_data_attrs) ;
|
||||
# - [data-quiz-role="header"] / "question" / "choices" — JS y écrit
|
||||
# TOUJOURS via .textContent ou remplace .innerHTML en entier (jamais
|
||||
# un ajout) : ne JAMAIS poser de décoration (icône, badge, score...)
|
||||
# comme ENFANT de ces éléments précis, elle serait effacée au premier
|
||||
# rafraîchissement — un ::before CSS est la seule façon sûre d'ajouter
|
||||
# une icône à ces zones-là. C'est pourquoi le score/minuteur (voir
|
||||
# juste en dessous) vit dans un élément FRÈRE du header (jamais son
|
||||
# enfant), même si visuellement il doit apparaître à l'intérieur du
|
||||
# même bandeau (demande explicite : "le score doit être à l'intérieur
|
||||
# de la boîte à quiz, pas à l'extérieur") — voir _quiz_header_row_html,
|
||||
# qui les pose côte à côte dans un même bandeau partageant le même
|
||||
# fond, plutôt qu'en deux bandes empilées de couleurs différentes ;
|
||||
# - [data-quiz-role="score"]/[data-quiz-role="timer"] — un modèle
|
||||
# "boîte" les regroupe dans un [data-quiz-role="topbar"] (masqué hors
|
||||
# plein écran par static/style.css) ; un modèle "page" n'a pas besoin
|
||||
# de ce regroupement/masquage (il n'existe QUE plein écran) et peut
|
||||
# les placer où il veut (voir render_quiz_box_page_manga) — le moteur
|
||||
# JS ne cherche jamais "topbar" lui-même, seulement score/timer.
|
||||
# "defaut" (Classique) reste visuellement identique au style déjà en
|
||||
# place avant cette extension (pas de suffixe de classe) — voir
|
||||
# render_quiz_box_defaut.
|
||||
|
||||
|
||||
def _quiz_data_attrs(obj, config, active_template):
|
||||
return (
|
||||
f'data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" '
|
||||
f'data-quiz-template="{active_template}" '
|
||||
f'data-fullscreen="{"1" if config["fullscreen"] else "0"}" '
|
||||
f'data-timer-mode="{config["timer_mode"]}" data-timer-seconds="{config["timer_seconds"]}"'
|
||||
)
|
||||
|
||||
|
||||
def _quiz_box_style(obj, style):
|
||||
return (
|
||||
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'])};"
|
||||
)
|
||||
|
||||
|
||||
def _quiz_topbar_html():
|
||||
return (
|
||||
'<div class="quizBoxTopBar" data-quiz-role="topbar">'
|
||||
'<span class="quizBoxScoreDisplay" data-quiz-role="score">Score : 0</span>'
|
||||
'<span class="quizBoxTimerDisplay" data-quiz-role="timer"></span>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def _quiz_header_row_html(header_text, header_bg=None):
|
||||
# Le score/minuteur (topbar) est un FRÈRE du header, jamais son enfant
|
||||
# (voir le commentaire d'en-tête) — les deux partagent ici le MÊME
|
||||
# bandeau (fond commun posé sur ".quizBoxHeaderRow", jamais sur le
|
||||
# header seul) pour que le score, une fois visible en plein écran,
|
||||
# apparaisse comme faisant partie du même bloc plutôt qu'une bande à
|
||||
# part au-dessus (demande explicite). `header_bg=None` : aucun fond
|
||||
# posé, le bandeau reste transparent (repli disponible pour un futur
|
||||
# modèle qui n'en aurait pas besoin).
|
||||
bg_style = f' style="background:{html_lib.escape(header_bg)}"' if header_bg else ''
|
||||
return (
|
||||
f'<div class="quizBoxHeaderRow"{bg_style}>'
|
||||
f'<div class="quizBoxHeader" data-quiz-role="header">{header_text}</div>'
|
||||
+ _quiz_topbar_html() +
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_defaut(obj, config, style):
|
||||
return (
|
||||
f'<div class="quizBoxWidget" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
+ _quiz_header_row_html("Quête : Titre de la quête", style["header_bg"]) +
|
||||
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>'
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_manga_dialogue(obj, config, style):
|
||||
# "Manga" (boîte de dialogue) — pendant, en petite carte flottante
|
||||
# JAMAIS plein écran, du modèle "page de quiz" "Manga" (voir
|
||||
# render_quiz_box_page_manga), validé par le créateur via une
|
||||
# maquette interactive avant intégration. Réutilise TELLE QUELLE la
|
||||
# structure partagée des modèles "boîte" (quizBoxHeaderRow/
|
||||
# quizBoxHeader/quizBoxTopBar/quizBoxBody/quizBoxQuestionText/
|
||||
# quizBoxChoices/quizBoxChoiceBtn) — seule la DÉCORATION change (voir
|
||||
# CSS), jamais la structure (demande explicite : les modèles "boîte"
|
||||
# "s'éloignent peu du modèle basic", contrairement aux modèles
|
||||
# "page" qui ont chacun leur structure propre).
|
||||
# .quizMangaDialogueFrame : enveloppe portant la forme dentelée/le
|
||||
# cadre (voir CSS) SANS l'appliquer au conteneur racine (qui doit
|
||||
# rester positionnable/redimensionnable normalement) — même principe
|
||||
# que les anciens modèles "Verre dépoli"/"Néon".
|
||||
# "Titre de la quête" seul ici (pas "Quête : Titre de la quête") : le
|
||||
# préfixe "Quête : " est posé en CSS pur (::before sur .quizBoxHeader,
|
||||
# voir static/style.css) pour survivre à l'écrasement JS du header
|
||||
# (voir le commentaire de classe ci-dessus) — l'écrire aussi ici le
|
||||
# doublerait dans l'aperçu éditeur (jamais réécrit par le JS avant le
|
||||
# premier vrai jeu).
|
||||
return (
|
||||
f'<div class="quizBoxWidget quizBoxWidget--mangaDialogue" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["dialog_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
'<div class="quizMangaDialogueFrame">'
|
||||
+ _quiz_header_row_html("Titre de la quête", style["header_bg"]) +
|
||||
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>'
|
||||
# Repère visuel SEULEMENT (aperçu éditeur, jamais reconfiguré) —
|
||||
# le vrai bouton est désormais TOUJOURS présent dès la question
|
||||
# (voir forgeShowQuizBox, dialogue-box-controller.js — posé
|
||||
# désactivé, activé par forgeQuizBoxAnswer une fois répondu,
|
||||
# demande explicite), donc ce repère est maintenant fidèle au
|
||||
# comportement réel, pas juste à l'aperçu éditeur.
|
||||
'<button type="button" class="quizBoxContinueBtn" disabled>Continuer →</button>'
|
||||
'</div></div>'
|
||||
'</div>'
|
||||
# Bandeau de pied de page TOUJOURS visible (comme la maquette
|
||||
# validée) — purement décoratif (aria-hidden, jamais un
|
||||
# data-quiz-role), le moteur JS partagé ne le touche JAMAIS : le
|
||||
# bouton "Continuer →" qu'il injecte dans .quizBoxChoices se pose
|
||||
# PAR-DESSUS en position:absolute (voir static/style.css), pas
|
||||
# DANS ce bandeau — même principe que le modèle "page" "Classique".
|
||||
# FRÈRE de .quizMangaDialogueFrame (jamais dedans) : .quizMangaDialogueFrame
|
||||
# a overflow:hidden (pour la forme dentelée du corps), qui
|
||||
# rognerait ce bandeau/le bouton s'ils étaient nichés à
|
||||
# l'intérieur au lieu d'être positionnés par-dessus TOUT le
|
||||
# widget (.quizBoxWidget--mangaDialogue est position:relative).
|
||||
'<div class="quizMangaDialogueFooterBand" aria-hidden="true"></div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_page_manga(obj, config, style):
|
||||
# "Manga" — premier modèle "PAGE DE QUIZ" (voir screens/rendering/
|
||||
# quiz_box_config.py::QUIZ_BOX_PAGE_TEMPLATES) : thème complet et
|
||||
# autonome, validé par le créateur via une maquette interactive avant
|
||||
# intégration — structure HTML totalement propre à ce modèle, AUCUNE
|
||||
# réutilisation de _quiz_header_row_html/quizBoxHeader/quizBoxBody des
|
||||
# modèles "boîte" ci-dessus (demande explicite : "la structure du html
|
||||
# qui change totalement"). Respecte quand même le contrat minimal
|
||||
# (data-quiz-role="header"/"question"/"choices"/"score"/"timer") pour
|
||||
# que le moteur JS partagé fonctionne sans modification.
|
||||
#
|
||||
# Score/minuteur : PAS de "topbar" à masquer/afficher ici (ce modèle
|
||||
# n'existe QUE plein écran, il n'y a jamais lieu de les cacher) — deux
|
||||
# plaques séparées de part et d'autre d'un cartouche de titre, comme
|
||||
# dans la maquette validée. Les lettres A/B/C/D + le tampon "正解"/
|
||||
# "不正解" à la révélation sont posés en CSS pur (::before/::after,
|
||||
# voir static/style.css) : les boutons de choix injectés par le
|
||||
# moteur JS (static/js/play/dialogue-box-controller.js) restent de
|
||||
# simples <button class="quizBoxChoiceBtn"> sans balisage
|
||||
# supplémentaire à maintenir ici.
|
||||
# "Temps" reste toujours visible, même sans minuteur configuré (demande
|
||||
# explicite : "si il n'y a pas de chrono marque en dessous de temps
|
||||
# aucun") — le texte initial ici ("Aucun") n'est JAMAIS écrasé par le
|
||||
# moteur JS quand timer_mode="aucun" (voir forgeShowQuizBox,
|
||||
# dialogue-box-controller.js : aucune de ses branches ne touche
|
||||
# [data-quiz-role="timer"] dans ce cas), donc pas besoin d'un effet JS
|
||||
# dédié pour le maintenir à jour question après question.
|
||||
timer_placeholder = "Aucun" if config["timer_mode"] == "aucun" else ""
|
||||
return (
|
||||
f'<div class="quizBoxWidget quizBoxWidget--manga" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} style="{_quiz_box_style(obj, style)}">'
|
||||
'<div class="mangaTopbar">'
|
||||
'<div class="mangaPlate mangaPlate--timer">'
|
||||
'<span class="mangaPlateLabel">Temps</span>'
|
||||
f'<span class="mangaPlateValue quizBoxTimerDisplay" data-quiz-role="timer">{timer_placeholder}</span>'
|
||||
'</div>'
|
||||
'<div class="mangaTitlemark"><span>Manga Quiz</span></div>'
|
||||
'<div class="mangaPlate mangaPlate--score">'
|
||||
'<span class="mangaPlateLabel">Score</span>'
|
||||
'<span class="mangaPlateValue" data-quiz-role="score">Score : 0</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
# Barre d'étapes (voir static/js/play/quiz-box-templates.js::
|
||||
# forgeQuizMangaOnShow) — remplie EN JEU (nombre de questions total
|
||||
# connu seulement à l'exécution, jamais à la pose de l'objet) ;
|
||||
# data-manga-role (pas data-quiz-role) : décoration propre à CE
|
||||
# modèle, jamais du contrat partagé.
|
||||
'<div class="mangaProgress" data-manga-role="progress"></div>'
|
||||
'<div class="mangaPanelQuestion">'
|
||||
'<span class="mangaKicker" data-quiz-role="header">Titre de la quête</span>'
|
||||
'<p class="mangaQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
'</div>'
|
||||
'<div class="mangaChoices" data-quiz-role="choices">'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
'</div>'
|
||||
# Écran de résultat de fin de quiz — voir forgeQuizMangaOnQuizComplete
|
||||
# (static/js/play/quiz-box-templates.js), même mécanique que
|
||||
# "classique" (render_quiz_box_page_classique) mais dans le thème
|
||||
# manga propre à ce modèle. Pas de conteneur "content" à part ici
|
||||
# (contrairement à .classiqueContent) : c'est la RACINE du widget
|
||||
# elle-même qui bascule en classe "is-showing-result" (voir
|
||||
# static/style.css) pour masquer question/choix, en gardant le
|
||||
# bandeau topbar/score + la barre d'étapes visibles. data-manga-
|
||||
# role (pas data-quiz-role) : décoration propre à ce modèle.
|
||||
'<div class="mangaResultZone" data-manga-role="result"></div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
def render_quiz_box_page_classique(obj, config, style):
|
||||
# "Classique" — modèle de PAGE DE QUIZ DE BASE (sobre/professionnel),
|
||||
# validé par le créateur via une maquette interactive (formation
|
||||
# sécurité incendie comme exemple). Reprend VOLONTAIREMENT les
|
||||
# couleurs par défaut du modèle "boîte de dialogue" de base
|
||||
# (header_bg/body_bg/text_color, voir dialogue_box_style.py) — demande
|
||||
# explicite : "respecte les couleurs du modele de boite de base pour
|
||||
# le modele de page de base" — plutôt que d'inventer sa propre
|
||||
# palette comme "manga".
|
||||
#
|
||||
# Titre = le champ "Nom" de CET objet (déjà existant aujourd'hui,
|
||||
# aucun nouveau champ "titre" nécessaire, demande explicite) — rendu
|
||||
# ICI, une bonne fois pour toutes côté serveur, jamais réécrit par le
|
||||
# moteur JS (à l'inverse de [data-quiz-role="header"] plus bas, qui
|
||||
# sert ici à autre chose : voir le commentaire dessus).
|
||||
title = html_lib.escape(obj.get("name") or "Quiz")
|
||||
timer_placeholder = "Aucun" if config["timer_mode"] == "aucun" else ""
|
||||
return (
|
||||
# `--classique-footer-bg` (custom property posée sur la racine) :
|
||||
# lue par .classiqueChoices .quizBoxContinueBtn (voir plus bas) —
|
||||
# le bouton "Continuer →" du moteur partagé (injecté APRÈS coup
|
||||
# dans [data-quiz-role="choices"]) se pose en bandeau bas façon
|
||||
# pied de page, dans la couleur configurée via "🎨 Style"
|
||||
# (footer_bg), jamais une couleur figée en dur.
|
||||
f'<div class="quizBoxWidget quizBoxWidget--classique" role="dialog" aria-live="polite" aria-atomic="true" {_quiz_data_attrs(obj, config, config["page_template"])} style="{_quiz_box_style(obj, style)} --classique-footer-bg:{html_lib.escape(style["footer_bg"])}; --classique-body-bg:{html_lib.escape(style["body_bg"])};">'
|
||||
f'<div class="classiqueHeaderBar" style="background:{html_lib.escape(style["header_bg"])}">'
|
||||
'<div class="classiqueHeaderTitleWrap">'
|
||||
'<span class="classiqueHeaderEyebrow">❓ Quiz</span>'
|
||||
f'<span class="classiqueHeaderTitle">{title}</span>'
|
||||
'</div>'
|
||||
'<div class="classiqueHeaderChips">'
|
||||
'<div class="classiqueChip classiqueChip--timer">'
|
||||
'<span class="classiqueChipLabel">Temps</span>'
|
||||
f'<span class="classiqueChipValue quizBoxTimerDisplay" data-quiz-role="timer">{timer_placeholder}</span>'
|
||||
'</div>'
|
||||
'<div class="classiqueChip classiqueChip--score">'
|
||||
'<span class="classiqueChipLabel">Score</span>'
|
||||
'<span class="classiqueChipValue" data-quiz-role="score">Score : 0</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
f'<div class="classiqueBody" style="background:{html_lib.escape(style["body_bg"])}">'
|
||||
'<div class="classiqueContent">'
|
||||
# Barre d'étapes — même principe que "manga" (voir
|
||||
# forgeQuizProgressOnShow, static/js/play/quiz-box-templates.js),
|
||||
# remplie EN JEU (nombre de questions connu seulement à
|
||||
# l'exécution) ; data-classique-role (pas data-quiz-role) :
|
||||
# décoration propre à CE modèle, jamais du contrat partagé.
|
||||
'<div class="classiqueProgress" data-classique-role="progress"></div>'
|
||||
# [data-quiz-role="header"] sert ICI d'étiquette "Question N/M"
|
||||
# (voir forgeQuizClassiqueOnShow) — JAMAIS le titre du quiz
|
||||
# (classiqueHeaderTitle ci-dessus, statique).
|
||||
'<span class="classiqueKicker" data-quiz-role="header">Question</span>'
|
||||
'<p class="classiqueQuestionText" data-quiz-role="question">La question s\'affiche ici.</p>'
|
||||
'<div class="classiqueChoices" data-quiz-role="choices">'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
|
||||
'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
|
||||
'</div>'
|
||||
# Encart d'explication après réponse — RÉSERVÉ pour une
|
||||
# fonctionnalité FUTURE, PAS ENCORE implémentée (demande
|
||||
# explicite : "tu peut le mettre dans le modele mais ne
|
||||
# fonctionnera pas pour le moment il faudra attendre que la
|
||||
# fonctionnalité sois implémenté") : aucune donnée d'explication
|
||||
# n'existe aujourd'hui sur une question, donc AUCUN code JS
|
||||
# n'écrit ici pour l'instant — reste vide et caché (voir
|
||||
# static/style.css) jusqu'à ce que ce champ existe réellement.
|
||||
'<div class="classiqueExplain" data-quiz-role="explain"></div>'
|
||||
# Écran de résultat de fin de quiz — RESTE VIDE/CACHÉ tant que le
|
||||
# quiz n'est pas terminé (voir forgeQuizClassiqueOnQuizComplete,
|
||||
# static/js/play/quiz-box-templates.js, qui le remplit et bascule
|
||||
# .classiqueContent en classe "is-showing-result" — masquant en
|
||||
# CSS tout le reste : progression/question/choix). data-classique-
|
||||
# role (pas data-quiz-role) : décoration propre à ce modèle.
|
||||
'<div class="classiqueResultZone" data-classique-role="result"></div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
# Bandeau de pied de page TOUJOURS visible (footer_bg, même
|
||||
# esprit que header/body ci-dessus) — purement décoratif
|
||||
# (aria-hidden, jamais un data-quiz-role), le moteur JS partagé
|
||||
# ne le touche JAMAIS : le bouton "Continuer →" qu'il injecte
|
||||
# dans .classiqueChoices se pose PAR-DESSUS en position:absolute
|
||||
# (voir static/style.css), pas DANS ce bandeau — demande
|
||||
# explicite : "il manque le footer foncé avec le bouton".
|
||||
'<div class="classiqueFooterBand" aria-hidden="true"></div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
|
||||
QUIZ_BOX_DIALOG_RENDERERS = {
|
||||
"defaut": render_quiz_box_defaut,
|
||||
"manga_dialogue": render_quiz_box_manga_dialogue,
|
||||
}
|
||||
|
||||
QUIZ_BOX_PAGE_RENDERERS = {
|
||||
"classique": render_quiz_box_page_classique,
|
||||
"manga": render_quiz_box_page_manga,
|
||||
}
|
||||
|
||||
|
||||
def render_quiz_box(obj, config, style):
|
||||
"""Point d'entrée UNIQUE (voir screens/scenes/render_scene_object.py)
|
||||
— bascule entre les deux catégories de modèles selon `fullscreen`
|
||||
(demande explicite : ces modèles ne se voient JAMAIS dans l'autre
|
||||
mode) plutôt que de laisser l'appelant deviner quelle liste
|
||||
consulter."""
|
||||
if config["fullscreen"]:
|
||||
renderer = QUIZ_BOX_PAGE_RENDERERS.get(config["page_template"], render_quiz_box_page_classique)
|
||||
else:
|
||||
renderer = QUIZ_BOX_DIALOG_RENDERERS.get(config["dialog_template"], render_quiz_box_defaut)
|
||||
return renderer(obj, config, style)
|
||||
@@ -2,6 +2,8 @@ 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):
|
||||
@@ -72,31 +74,20 @@ def render_scene_object(obj):
|
||||
)
|
||||
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.
|
||||
# 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)
|
||||
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" role="dialog" aria-live="polite" aria-atomic="true" 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>'
|
||||
)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
|
||||
import db
|
||||
|
||||
from ..rendering.quiz_box_config import sanitize_quiz_box_config
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_quiz_config(slug, object_id, raw_config):
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT attributes FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return None
|
||||
attributes = json.loads(row["attributes"] or "{}")
|
||||
config = sanitize_quiz_box_config(raw_config)
|
||||
attributes["_quiz_box_config"] = json.dumps(config)
|
||||
conn.execute("UPDATE _scene_objects SET attributes = ? WHERE id = ?", (json.dumps(attributes), object_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return config
|
||||
Reference in New Issue
Block a user