Éditeur de quêtes : onglet à côté de Collision, pas une page à part
Corrige la mauvaise interprétation de la demande initiale : la page dédiée "🗺️ Quêtes" (nav du jeu) est retirée au profit d'un nouvel onglet "🗺️ Quêtes" DANS l'éditeur de scène 2D, juste à côté de "🧩 Collision" — même liste tabulaire que l'onglet "📣 Événements" (titre/objectif/récompense/statut/résultat par ligne, pas des cartes), un clic sur une ligne ouvrant la modale d'arbre de dialogue déjà construite. L'assistant "+ Action" de l'éditeur de collision ouvre toujours cette même modale pour "Déclencher une quête". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
51427b5297
commit
78c1aba8a0
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import json
|
||||
|
||||
from flask import render_template
|
||||
|
||||
import db
|
||||
|
||||
from core.flask_app import app
|
||||
|
||||
|
||||
@app.route("/game/<slug>/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))
|
||||
@@ -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 ? '<span class="questPill questPill-resultat">' + QUEST_RESULT_LABELS[quest.resultat] + '</span>' : '';
|
||||
function questListRowHtml(quest) {
|
||||
var resultatHtml = quest.resultat ? '<span class="questPill questPill-resultat">' + QUEST_RESULT_LABELS[quest.resultat] + '</span>' : '<span class="hint">—</span>';
|
||||
return (
|
||||
'<div class="questCard" style="animation-delay:' + (i * 50) + 'ms" onclick="openQuestDialogueModal(' + quest.id + ')">' +
|
||||
'<div class="questCardHeader">' +
|
||||
'<h3 class="questCardTitle">' + (quest.title || '').replace(/</g, '<') + '</h3>' +
|
||||
'<span class="questPill questPill-status questPill-status-' + quest.status + '">' + QUEST_STATUS_LABELS[quest.status] + '</span>' +
|
||||
'</div>' +
|
||||
'<p class="questCardObjectif">' + (quest.objectif ? quest.objectif.replace(/</g, '<') : '<span class="hint">Aucun objectif défini.</span>') + '</p>' +
|
||||
'<div class="questCardFooter">' +
|
||||
'<span class="questPill questPill-score">🏆 +' + (quest.recompense_score || 0) + '</span>' +
|
||||
resultatHtml +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
'<tr class="questListRow" onclick="openQuestDialogueModal(' + quest.id + ')">' +
|
||||
'<td><strong>' + (quest.title || '').replace(/</g, '<') + '</strong></td>' +
|
||||
'<td>' + (quest.objectif ? quest.objectif.replace(/</g, '<') : '<span class="hint">—</span>') + '</td>' +
|
||||
'<td><span class="questPill questPill-score">🏆 +' + (quest.recompense_score || 0) + '</span></td>' +
|
||||
'<td><span class="questPill questPill-status questPill-status-' + quest.status + '">' + QUEST_STATUS_LABELS[quest.status] + '</span></td>' +
|
||||
'<td>' + resultatHtml + '</td>' +
|
||||
'<td class="fieldsTableActions">' +
|
||||
'<button type="button" class="button is-small is-danger" title="Supprimer" onclick="event.stopPropagation(); questDeleteFromList(' + quest.id + ')">🗑️</button>' +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
);
|
||||
}
|
||||
|
||||
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 = '<p class="hint">Aucune quête pour l\'instant — crée-en une avec le bouton ci-dessus.</p>';
|
||||
wrap.innerHTML = '<p class="hint">Aucune quête pour l\'instant — crée-en une avec le bouton ci-dessus.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = QUESTS.map(questCardHtml).join('');
|
||||
wrap.innerHTML =
|
||||
'<div class="fieldsTableWrap"><table class="table is-fullwidth is-hoverable fieldsTable">' +
|
||||
'<thead><tr><th>Titre</th><th>Objectif</th><th>Récompense</th><th>Statut</th><th>Résultat</th><th></th></tr></thead>' +
|
||||
'<tbody>' + QUESTS.map(questListRowHtml).join('') + '</tbody>' +
|
||||
'</table></div>';
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+4
-13
@@ -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; }
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
une version simplifiée (juste les écrans) pour un type "restreint"
|
||||
(quiz/embranchement/rpg), la version complète pour "custom". -->
|
||||
<a href="{{ url_for('game_dashboard', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug else '' }}" title="{{ 'Écrans du jeu' if game.onboarding_type in ('quiz', 'embranchement', 'rpg') else 'Écrans, objets, templates, variables' }}">📊 Tableau de bord</a>
|
||||
<a href="{{ url_for('quests_view', slug=game.slug) }}" class="gameNavLink {{ 'active' if request.path == '/game/' ~ game.slug ~ '/quests' else '' }}" title="Quêtes (objectif, récompense, statut, dialogue)">🗺️ Quêtes</a>
|
||||
<a href="{{ url_for('game_play', slug=game.slug) }}" target="_blank" class="gameNavLink" title="Aperçu jouable du jeu (une seule partie partagée, pour la conception)">▶️ Jouer</a>
|
||||
<!-- Phase 1.2 (feuille de route produit) : seul export retenu — paquet
|
||||
STATIQUE (aucun Python/Flask embarqué, voir
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Quêtes — {{ game.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="dashPanelHeader" style="align-items:center; justify-content:space-between;">
|
||||
<h2>🗺️ Quêtes</h2>
|
||||
<button type="button" class="collisionRuleAddBtn" onclick="createQuestFromListPage()">✚ Nouvelle quête</button>
|
||||
</div>
|
||||
<p class="hint">Une quête : un objectif, une récompense (score), un statut, un résultat — et un dialogue par statut, à écrire en cliquant sur sa carte.</p>
|
||||
|
||||
<div id="questsGrid" class="questsGrid"></div>
|
||||
|
||||
<script>
|
||||
var QUESTS_JSON = {{ quests_json|safe }};
|
||||
var GAME_SLUG = "{{ game.slug }}";
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/quests/quest-editor.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -94,6 +94,7 @@
|
||||
<div class="builderTabs" id="builderTabs">
|
||||
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Scène</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="collision-rules" onclick="switchBuilderTab('collision-rules')">🧩 Collision</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="quests" onclick="switchBuilderTab('quests')">🗺️ Quêtes</button>
|
||||
<button type="button" class="builderTabBtn" data-tab="events" onclick="switchBuilderTab('events')">📣 Événements</button>
|
||||
</div>
|
||||
|
||||
@@ -161,6 +162,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Éditeur de quêtes (voir db/quests/) : une LISTE (comme l'onglet
|
||||
Événements juste après), pas une grille de cartes — chaque ligne
|
||||
résume titre/objectif/récompense/statut/résultat, cliquer dessus
|
||||
ouvre la modale d'arbre de dialogue (static/js/quests/
|
||||
quest-editor.js, partagée avec l'assistant "+ Action" de l'onglet
|
||||
Collision). Rendu ENTIÈREMENT en JS depuis QUESTS_JSON, même
|
||||
convention que #collisionRulesCards ci-dessus. -->
|
||||
<div class="builderTabPanel" id="questsTabPanel" data-tab-panel="quests">
|
||||
<div class="dashPanelHeader">
|
||||
<h2>🗺️ Quêtes</h2>
|
||||
<button type="button" class="button is-small primary" onclick="createQuestFromListPage()">+ Créer une quête</button>
|
||||
</div>
|
||||
<p class="hint">Titre, objectif, récompense, statut, résultat — clique sur une ligne pour écrire son enchaînement de dialogue.</p>
|
||||
<div id="questsListWrap"></div>
|
||||
</div>
|
||||
|
||||
<div class="builderTabPanel" id="eventsTabPanel" data-tab-panel="events">
|
||||
<div class="dashPanelHeader">
|
||||
<h2>Événements personnalisés</h2>
|
||||
|
||||
+2
-10
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user