diff --git a/db/quests/create_quest.py b/db/quests/create_quest.py index 83522dc3..e58748a5 100644 --- a/db/quests/create_quest.py +++ b/db/quests/create_quest.py @@ -5,7 +5,7 @@ from .get_quest import get_quest def create_quest(slug, title="Nouvelle quête"): """Titre par défaut plutôt qu'un refus — créée d'un clic depuis la - liste (voir templates/quests.html) ou depuis l'assistant "+ Action" + liste (voir l'onglet "🗺️ Quêtes", templates/scene_edit.html) ou depuis l'assistant "+ Action" de l'éditeur de collision (voir collision-rules-editor.js), avant même que l'auteur ait choisi un titre.""" ensure_quests_schema(slug) diff --git a/routes/quests/__init__.py b/routes/quests/__init__.py index 0bbd7d75..95519fe5 100644 --- a/routes/quests/__init__.py +++ b/routes/quests/__init__.py @@ -1 +1 @@ -from . import quests_view, quests_list_json, quest_new, quest_update, quest_delete, quest_dialogues +from . import quests_list_json, quest_new, quest_update, quest_delete, quest_dialogues diff --git a/routes/quests/quests_list_json.py b/routes/quests/quests_list_json.py index 94dbed50..35fd9167 100644 --- a/routes/quests/quests_list_json.py +++ b/routes/quests/quests_list_json.py @@ -9,6 +9,7 @@ from core.flask_app import app def quests_list_json(slug): """Utilisé par l'assistant "+ Action" de l'éditeur de collision (voir collision-rules-editor.js) pour lister les quêtes existantes sans - quitter la modale déjà ouverte — même contenu que quests_view, en JSON - seul plutôt qu'une page complète.""" + quitter la modale déjà ouverte (l'onglet "🗺️ Quêtes" lui-même reçoit + déjà sa liste via QUESTS_JSON, voir routes/scenes/scene_edit_view.py — + cette route ne sert qu'à la RAFRAÎCHIR depuis le contexte collision).""" return jsonify({"quests": db.list_quests(slug)}) diff --git a/routes/quests/quests_view.py b/routes/quests/quests_view.py deleted file mode 100644 index f9afc130..00000000 --- a/routes/quests/quests_view.py +++ /dev/null @@ -1,20 +0,0 @@ -import json - -from flask import render_template - -import db - -from core.flask_app import app - - -@app.route("/game//quests") -def quests_view(slug): - """Éditeur de quêtes (voir db/quests/) — page dédiée game-wide, dans le - même esprit que game_dashboard.html : une liste de cartes + un bouton - "+ Nouvelle quête" au-dessus, chaque carte ouvrant une modale avec - l'arbre de dialogue (static/js/quests/quest-editor.js — même module - JS que celui rendu depuis l'assistant "+ Action" de l'éditeur de - collision, voir collision-rules-editor.js).""" - game = db.game_meta(slug) - quests = db.list_quests(slug) - return render_template("quests.html", game=game, quests_json=json.dumps(quests)) diff --git a/static/js/quests/quest-editor.js b/static/js/quests/quest-editor.js index 5b820af9..ce52df40 100644 --- a/static/js/quests/quest-editor.js +++ b/static/js/quests/quest-editor.js @@ -1,56 +1,63 @@ // ---------- Éditeur de quêtes ---------- -// Module partagé entre templates/quests.html (page dédiée : liste de -// cartes + "+ Nouvelle quête") et static/js/scenes/collision-rules-editor.js -// (l'assistant "+ Action" de l'éditeur de collision ouvre CETTE MÊME -// modale de dialogue quand l'auteur choisit "Déclencher une quête", -// plutôt qu'un simple champ texte pour un id). La modale (arbre de -// dialogue) est créée dynamiquement (ensureQuestDialogueModal) plutôt que -// dupliquée dans deux templates. +// Onglet "🗺️ Quêtes" à côté de "🧩 Collision" (templates/scene_edit.html, +// jamais une page à part) : une LISTE (mêmes tableau/lignes que l'onglet +// "📣 Événements") plutôt qu'une grille de cartes, chaque ligne portant +// toutes les infos de la quête + un clic dessus ouvrant la modale +// d'arbre de dialogue. Cette même modale est aussi ouverte depuis +// l'assistant "+ Action" de l'éditeur de collision (voir +// static/js/scenes/collision-rules-editor.js) quand l'auteur choisit +// "Déclencher une quête" — créée dynamiquement (ensureQuestDialogueModal) +// plutôt que dupliquée dans deux endroits. // // Une quête (voir db/quests/) : id, title, objectif, recompense_score, // status (nouvelle/en_cours/terminee — statut COURANT, informatif), // resultat (reussi/echoue), dialogues (une liste de répliques PAR -// STATUT — 3 colonnes dans l'éditeur : quel dialogue jouer selon l'état +// STATUT — 3 colonnes dans la modale : quel dialogue jouer selon l'état // de la quête au moment où le joueur parle au PNJ). var QUEST_STATUS_LABELS = { nouvelle: 'Nouvelle', en_cours: 'En cours', terminee: 'Terminée' }; var QUEST_RESULT_LABELS = { reussi: '✅ Réussi', echoue: '❌ Échoué' }; var QUEST_STATUS_ORDER = ['nouvelle', 'en_cours', 'terminee']; -// Quêtes connues côté client — préremplies par QUESTS_JSON (quests.html) -// si présent, sinon vide tant qu'on n'a pas encore fetch (contexte -// éditeur de collision, voir questPickerEnsureLoaded). +// Quêtes connues côté client — préremplies par QUESTS_JSON (voir +// templates/scene_edit.html), toujours présentes puisque l'onglet +// Quêtes vit dans le même éditeur que l'onglet Collision qui en a +// besoin (collisionRuleActionSummary). var QUESTS = (typeof QUESTS_JSON !== 'undefined') ? QUESTS_JSON : []; function questById(questId) { return QUESTS.find(function (q) { return q.id === questId; }); } -function questCardHtml(quest, i) { - var resultatHtml = quest.resultat ? '' + QUEST_RESULT_LABELS[quest.resultat] + '' : ''; +function questListRowHtml(quest) { + var resultatHtml = quest.resultat ? '' + QUEST_RESULT_LABELS[quest.resultat] + '' : '—'; return ( - '
' + - '
' + - '

' + (quest.title || '').replace(/' + - '' + QUEST_STATUS_LABELS[quest.status] + '' + - '

' + - '

' + (quest.objectif ? quest.objectif.replace(/Aucun objectif défini.') + '

' + - '
' + - '🏆 +' + (quest.recompense_score || 0) + '' + - resultatHtml + - '
' + - '
' + '' + + '' + (quest.title || '').replace(/' + + '' + (quest.objectif ? quest.objectif.replace(/—') + '' + + '🏆 +' + (quest.recompense_score || 0) + '' + + '' + QUEST_STATUS_LABELS[quest.status] + '' + + '' + resultatHtml + '' + + '' + + '' + + '' + + '' ); } function renderQuestListPage() { - var container = document.getElementById('questsGrid'); + var container = document.getElementById('questsTabPanel'); if (!container) return; + var wrap = document.getElementById('questsListWrap'); if (!QUESTS.length) { - container.innerHTML = '

Aucune quête pour l\'instant — crée-en une avec le bouton ci-dessus.

'; + wrap.innerHTML = '

Aucune quête pour l\'instant — crée-en une avec le bouton ci-dessus.

'; return; } - container.innerHTML = QUESTS.map(questCardHtml).join(''); + wrap.innerHTML = + '
' + + '' + + '' + QUESTS.map(questListRowHtml).join('') + '' + + '
TitreObjectifRécompenseStatutRésultat
'; } function createQuestFromListPage() { @@ -65,6 +72,16 @@ function createQuestFromListPage() { }); } +function questDeleteFromList(questId) { + var quest = questById(questId); + if (!quest || !confirm('Supprimer la quête « ' + quest.title + ' » ? Toute règle de collision qui la référence perdra sa cible.')) return; + fetch('/game/' + GAME_SLUG + '/quests/' + questId + '/delete', { method: 'POST' }) + .then(function () { + QUESTS = QUESTS.filter(function (q) { return q.id !== questId; }); + renderQuestListPage(); + }); +} + // ---------- Modale "arbre de dialogue" ---------- // Créée une seule fois (au premier appel), réutilisée ensuite — partagée // entre la page dédiée et l'assistant "+ Action" de l'éditeur de diff --git a/static/style.css b/static/style.css index 8944360b..63fb727d 100644 --- a/static/style.css +++ b/static/style.css @@ -760,20 +760,11 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d .collisionInteractBubble{ display:none; position:absolute; left:50%; bottom:100%; transform:translateX(-50%); margin-bottom:6px; background:#111; color:#fff; border-radius:10px; padding:4px 10px; font-size:12px; white-space:nowrap; pointer-events:none; z-index:20; } -/* ---- Éditeur de quêtes (templates/quests.html + modale de dialogue - partagée avec l'assistant "+ Action" de l'éditeur de collision, voir +/* ---- Éditeur de quêtes (onglet "🗺️ Quêtes" à côté de "🧩 Collision", + templates/scene_edit.html + modale de dialogue partagée avec + l'assistant "+ Action" de l'éditeur de collision, voir static/js/quests/quest-editor.js) ---- */ -.questsGrid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:18px; margin-top:14px; } -.questCard{ - background:linear-gradient(165deg, var(--panel), var(--panel2)); border:1px solid var(--border); - border-radius:16px; padding:16px; cursor:pointer; box-shadow:0 6px 18px rgba(0,0,0,.18); - animation:collisionCardIn .35s ease backwards; transition:transform .18s ease, box-shadow .18s ease; -} -.questCard:hover{ transform:translateY(-3px); box-shadow:0 10px 26px rgba(0,0,0,.28); border-color:var(--accent); } -.questCardHeader{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; } -.questCardTitle{ margin:0; font-size:15px; font-weight:700; } -.questCardObjectif{ font-size:12.5px; color:var(--forge-text-muted, #8b95a7); margin-bottom:12px; min-height:1.4em; } -.questCardFooter{ display:flex; gap:8px; flex-wrap:wrap; } +.questListRow{ cursor:pointer; } .questPill{ display:inline-flex; align-items:center; border-radius:999px; padding:3px 10px; font-size:11.5px; font-weight:700; } .questPill-score{ background:rgba(242,153,74,.18); color:#f2994a; } .questPill-resultat{ background:rgba(23,162,184,.18); color:#17a2b8; } diff --git a/templates/base.html b/templates/base.html index cc21362c..3392f0ef 100644 --- a/templates/base.html +++ b/templates/base.html @@ -53,7 +53,6 @@ une version simplifiée (juste les écrans) pour un type "restreint" (quiz/embranchement/rpg), la version complète pour "custom". --> 📊 Tableau de bord - 🗺️ Quêtes ▶️ Jouer +
+
+

🗺️ Quêtes

+ +
+

Titre, objectif, récompense, statut, résultat — clique sur une ligne pour écrire son enchaînement de dialogue.

+
+
+

Événements personnalisés

diff --git a/tests/test_quests.py b/tests/test_quests.py index cf96b276..49559c17 100644 --- a/tests/test_quests.py +++ b/tests/test_quests.py @@ -1,5 +1,5 @@ -"""Éditeur de quêtes (voir db/quests/, routes/quests/, nouveau lien -"🗺️ Quêtes" dans templates/base.html) : id, titre, objectif, récompense +"""Éditeur de quêtes (voir db/quests/, routes/quests/, onglet "🗺️ Quêtes" +de l'éditeur de scène 2D, templates/scene_edit.html) : id, titre, objectif, récompense (score), statut (nouvelle/en_cours/terminee), résultat (reussi/echoue), et un dialogue par statut (3 colonnes — voir static/js/quests/ quest-editor.js et sanitize_quest_dialogues). Une quête vit pour TOUT le @@ -108,14 +108,6 @@ def test_set_quest_dialogues_returns_none_for_missing_quest(tmp_game_slug_cleanu # ---------- Routes ---------- -def test_quests_view_route_renders_the_list(client, tmp_game_slug_cleanup): - slug = _make_game(tmp_game_slug_cleanup) - db.create_quest(slug, "Trouver la clé") - resp = client.get(f"/game/{slug}/quests") - assert resp.status_code == 200 - assert b"Trouver la cl" in resp.data - - def test_quest_new_route_creates_and_returns_json(client, tmp_game_slug_cleanup): slug = _make_game(tmp_game_slug_cleanup) resp = client.post(f"/game/{slug}/quests/new", data='{"title": "Nouvelle quête du donjon"}', content_type="application/json")