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
+21 -4
View File
@@ -9,9 +9,26 @@ from core.flask_app import app
def quest_dialogues(slug, quest_id):
"""Toujours les 3 colonnes ENTIÈRES (voir db.set_quest_dialogues) —
le client (renderQuestDialogueColumns, quest-editor.js) poste l'objet
complet à chaque modification plutôt qu'une réplique isolée."""
body = request.get_json(silent=True) or {}
dialogues = db.set_quest_dialogues(slug, quest_id, body.get("dialogues"))
if dialogues is None:
complet à chaque modification plutôt qu'une réplique isolée.
Refusé (400) si la somme des récompenses des questions ("❓ Question")
dépasse la récompense totale de la quête (recompense_score) — voir
db.quest_dialogues_total_reward. Validé ICI (avant persistance)
plutôt que dans db.set_quest_dialogues, pour renvoyer un message
d'erreur explicite au lieu d'enregistrer silencieusement un état
incohérent."""
quest = db.get_quest(slug, quest_id)
if quest is None:
return jsonify({"error": "quête introuvable"}), 404
body = request.get_json(silent=True) or {}
sanitized = db.sanitize_quest_dialogues(body.get("dialogues"))
allocated = db.quest_dialogues_total_reward(sanitized)
if allocated > quest["recompense_score"]:
return jsonify({
"error": (
f"La somme des récompenses des questions ({allocated} pts) dépasse "
f"la récompense totale de la quête ({quest['recompense_score']} pts)."
),
}), 400
dialogues = db.set_quest_dialogues(slug, quest_id, body.get("dialogues"))
return jsonify({"dialogues": dialogues})
+22
View File
@@ -7,7 +7,29 @@ from core.flask_app import app
@app.route("/game/<slug>/quests/<int:quest_id>", methods=["POST"])
def quest_update(slug, quest_id):
"""Refusé (400) si "recompense_score" est abaissé SOUS le total déjà
réparti entre les questions ("❓ Question") du dialogue de cette
quête — voir db.quest_dialogues_total_reward, même contrainte que
routes/quests/quest_dialogues.py mais dans l'autre sens (ici c'est le
budget total qui bouge, pas la répartition)."""
body = request.get_json(silent=True) or {}
if "recompense_score" in body:
quest = db.get_quest(slug, quest_id)
if quest is None:
return jsonify({"error": "quête introuvable"}), 404
try:
new_score = int(body["recompense_score"])
except (TypeError, ValueError):
new_score = None
if new_score is not None:
allocated = db.quest_dialogues_total_reward(quest["dialogues"])
if new_score < allocated:
return jsonify({
"error": (
f"La récompense totale ({new_score} pts) est inférieure aux "
f"{allocated} pts déjà répartis entre les questions du dialogue."
),
}), 400
quest = db.update_quest(slug, quest_id, body)
if quest is None:
return jsonify({"error": "quête introuvable"}), 404
+4 -3
View File
@@ -28,14 +28,15 @@ def render_scene_edit(slug, screen_id, game):
collision_settings = None
dialogue_box_style = None
selected_obj = next((o for o in objects if o["id"] == selected_id), None) if selected_id else None
_UI_WIDGET_KINDS = ("dialogue_box", "quiz_box", "score_widget")
if selected_obj:
if selected_obj["kind"] != "dialogue_box":
if selected_obj["kind"] not in _UI_WIDGET_KINDS:
collision_settings = screens.resolve_collision_settings(selected_obj)
if selected_obj["kind"] == "personnage":
personnage_data = screens.resolve_personnage_data(selected_obj)
personnage_commands = screens.resolve_personnage_commands(selected_obj)
personnage_role = screens.resolve_personnage_role(selected_obj)
elif selected_obj["kind"] == "dialogue_box":
elif selected_obj["kind"] in _UI_WIDGET_KINDS:
dialogue_box_style = screens.resolve_dialogue_box_style(selected_obj)
_DEFAULT_OBJECT_LABELS = {"personnage": "Personnage", "fond": "Fond"}
@@ -56,7 +57,7 @@ def render_scene_edit(slug, screen_id, game):
# SÉLECTIONNÉ, mais nécessaire ici pour TOUS les personnages).
collision_rule_objects = []
for o in objects:
if o["kind"] in ("fond", "dialogue_box"):
if o["kind"] in ("fond", "dialogue_box", "quiz_box", "score_widget"):
continue
role = screens.resolve_personnage_role(o) if o["kind"] == "personnage" else None
if role == "joueur":