Quiz jouable : budget de récompense, boîte à quiz, widget score
Build and deploy / test-python (push) Failing after 1m34s
Build and deploy / test-js (push) Successful in 1m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

- Récompense d'une quête = budget MAXIMUM pour ses questions : la somme
  des points des "❓ Question" ne peut plus dépasser recompense_score
  (routes/quests/quest_dialogues.py à l'enregistrement des dialogues,
  quest_update.py si on abaisse la récompense sous ce qui est déjà
  réparti) — message d'erreur explicite (400) dans les deux sens,
  affiché via alert() côté éditeur (quest-editor.js), jamais enregistré
  silencieusement dans un état incohérent.
- Nombre de choix par question plafonné à 4 (au lieu de 8).

- Deux nouveaux widgets "🖥️ Interface" (mêmes fondations que "💬 Boîte
  de dialogue" — screens/rendering/dialogue_box_style.py, même panneau
  "🎨 Style") :
  - "❓ Boîte à quiz" : affiche la question et ses choix, sans pied (une
    question se résout au clic sur un choix, pas de "Suivant").
  - "🏆 Score" : affiche en continu les points gagnés, TOUJOURS visible
    une fois posé (contrairement aux boîtes de dialogue/quiz, masquées
    par défaut).

- Moteur d'exécution (static/js/play/dialogue-box-controller.js,
  refonte) : une conversation de quête alterne maintenant répliques
  (boîte de dialogue) et questions (boîte à quiz) selon le type de
  chaque ligne. Bonne réponse -> crédite le score (compteur runtime
  dédié, jamais lié au score du moteur document) et avance ; mauvaise
  réponse -> rien, la question reste affichée pour réessayer ; aucune
  boîte à quiz posée -> la question est ignorée plutôt que de bloquer
  la conversation. Le dialogue "en_cours" épuisé (donc, s'il contenait
  des questions, toutes répondues) fait passer la quête "terminee" (en
  mémoire seulement, comme le reste de ce système — voir accepter/
  refuser une offre de quête) : le joueur peut alors quitter la scène
  normalement, ces widgets n'étant jamais des obstacles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-03 15:42:42 +02:00
co-authored by Claude Sonnet 5
parent e972acf9e6
commit b5546df044
19 changed files with 628 additions and 76 deletions
+11 -4
View File
@@ -5,11 +5,14 @@ import db
from .ensure_scene_schema import ensure_scene_schema
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
_KINDS = {"personnage", "decor", "fond", "dialogue_box"}
# Widget d'interface (voir "🖥️ Interface", screens/rendering/dialogue_box_style.py) :
# assez grand pour lire une réplique sans redimensionner, jamais confondu
# visuellement avec un personnage/décor (128×128 par défaut).
_KINDS = {"personnage", "decor", "fond", "dialogue_box", "quiz_box", "score_widget"}
# Widgets d'interface (voir "🖥️ Interface", screens/rendering/dialogue_box_style.py) :
# assez grands pour lire une réplique/question sans redimensionner,
# jamais confondus visuellement avec un personnage/décor (128×128 par
# défaut). "score_widget" reste petit — juste un nombre.
_DIALOGUE_BOX_DEFAULT_SIZE = (320, 150)
_QUIZ_BOX_DEFAULT_SIZE = (340, 240)
_SCORE_WIDGET_DEFAULT_SIZE = (140, 50)
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None):
@@ -46,6 +49,10 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
width, height = background["width"], background["height"]
elif kind == "dialogue_box":
width, height = _DIALOGUE_BOX_DEFAULT_SIZE
elif kind == "quiz_box":
width, height = _QUIZ_BOX_DEFAULT_SIZE
elif kind == "score_widget":
width, height = _SCORE_WIDGET_DEFAULT_SIZE
conn = db.connect(slug)
if kind == "fond":
z_index = -1000
+44
View File
@@ -65,6 +65,50 @@ def render_scene_object(obj):
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 = la question, corps = les choix de réponse
# (boutons 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"])}">La question s\'affiche ici.</div>'
f'<div class="quizBoxChoices" data-quiz-role="choices" style="background:{html_lib.escape(style["body_bg"])}">'
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix A</button>'
f'<button type="button" class="quizBoxChoiceBtn" disabled>Choix B</button>'
f'</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 ""