Quêtes : dialogue dans une modale à part, en plein écran
Build and deploy / test-python (push) Successful in 6m17s
Build and deploy / test-js (push) Successful in 1m14s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Sépare la modale unique en deux étapes distinctes demandées par
l'utilisateur : #questFieldsModal ("je crée" — titre/objectif/
récompense/statut/résultat, taille normale) puis, sur un bouton dédié
"💬 Ajouter les dialogues", #questDialogueModal ("j'ajoute les
dialogues" — les 3 colonnes de répliques SEULES, occupant tout
l'écran). Un bouton "←" ramène à la fiche ; "✓ Utiliser cette quête"
reste disponible dans les deux, pour l'intégration avec l'éditeur de
collision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 20:54:00 +02:00
co-authored by Claude Sonnet 5
parent 78c1aba8a0
commit 390ee831ca
2 changed files with 120 additions and 44 deletions
+111 -43
View File
@@ -1,13 +1,15 @@
// ---------- Éditeur de quêtes ---------- // ---------- Éditeur de quêtes ----------
// Onglet "🗺️ Quêtes" à côté de "🧩 Collision" (templates/scene_edit.html, // Onglet "🗺️ Quêtes" à côté de "🧩 Collision" (templates/scene_edit.html,
// jamais une page à part) : une LISTE (mêmes tableau/lignes que l'onglet // 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 // "📣 Événements") plutôt qu'une grille de cartes. Un clic sur une ligne
// toutes les infos de la quête + un clic dessus ouvrant la modale // ouvre la modale de FICHE (titre/objectif/récompense/statut/résultat —
// d'arbre de dialogue. Cette même modale est aussi ouverte depuis // "je crée"), qui ouvre à son tour, sur un bouton dédié, la modale
// l'assistant "+ Action" de l'éditeur de collision (voir // PLEIN ÉCRAN d'arbre de dialogue ("j'ajoute les dialogues") — deux
// static/js/scenes/collision-rules-editor.js) quand l'auteur choisit // étapes séparées, jamais le même formulaire. Les deux modales sont
// "Déclencher une quête" — créée dynamiquement (ensureQuestDialogueModal) // aussi ouvertes depuis l'assistant "+ Action" de l'éditeur de collision
// plutôt que dupliquée dans deux endroits. // (voir static/js/scenes/collision-rules-editor.js) quand l'auteur
// choisit "Déclencher une quête" — créées dynamiquement (ensureQuest*Modal)
// plutôt que dupliquées dans deux endroits.
// //
// Une quête (voir db/quests/) : id, title, objectif, recompense_score, // Une quête (voir db/quests/) : id, title, objectif, recompense_score,
// status (nouvelle/en_cours/terminee — statut COURANT, informatif), // status (nouvelle/en_cours/terminee — statut COURANT, informatif),
@@ -32,7 +34,7 @@ function questById(questId) {
function questListRowHtml(quest) { function questListRowHtml(quest) {
var resultatHtml = quest.resultat ? '<span class="questPill questPill-resultat">' + QUEST_RESULT_LABELS[quest.resultat] + '</span>' : '<span class="hint">—</span>'; var resultatHtml = quest.resultat ? '<span class="questPill questPill-resultat">' + QUEST_RESULT_LABELS[quest.resultat] + '</span>' : '<span class="hint">—</span>';
return ( return (
'<tr class="questListRow" onclick="openQuestDialogueModal(' + quest.id + ')">' + '<tr class="questListRow" onclick="openQuestFieldsModal(' + quest.id + ')">' +
'<td><strong>' + (quest.title || '').replace(/</g, '&lt;') + '</strong></td>' + '<td><strong>' + (quest.title || '').replace(/</g, '&lt;') + '</strong></td>' +
'<td>' + (quest.objectif ? quest.objectif.replace(/</g, '&lt;') : '<span class="hint">—</span>') + '</td>' + '<td>' + (quest.objectif ? quest.objectif.replace(/</g, '&lt;') : '<span class="hint">—</span>') + '</td>' +
'<td><span class="questPill questPill-score">🏆 +' + (quest.recompense_score || 0) + '</span></td>' + '<td><span class="questPill questPill-score">🏆 +' + (quest.recompense_score || 0) + '</span></td>' +
@@ -68,7 +70,7 @@ function createQuestFromListPage() {
.then(function (json) { .then(function (json) {
QUESTS.unshift(json.quest); QUESTS.unshift(json.quest);
renderQuestListPage(); renderQuestListPage();
openQuestDialogueModal(json.quest.id); openQuestFieldsModal(json.quest.id);
}); });
} }
@@ -82,22 +84,28 @@ function questDeleteFromList(questId) {
}); });
} }
// ---------- Modale "arbre de dialogue" ---------- // ---------- Deux modales distinctes ----------
// Créée une seule fois (au premier appel), réutilisée ensuite — partagée // 1. #questFieldsModal (taille normale) : "je crée" — titre/objectif/
// entre la page dédiée et l'assistant "+ Action" de l'éditeur de // récompense/statut/résultat, + un bouton qui ouvre...
// collision, jamais dupliquée dans les templates. // 2. #questDialogueModal (PLEIN ÉCRAN) : "j'ajoute les dialogues" — les
// 3 colonnes de répliques, rien d'autre. Étapes séparées demandées
// explicitement par l'utilisateur (pas un seul gros formulaire).
// Créées une seule fois chacune (au premier appel), réutilisées ensuite —
// partagées avec l'assistant "+ Action" de l'éditeur de collision,
// jamais dupliquées dans les templates.
var _questModalOnUse = null; var _questModalOnUse = null;
var _questModalCurrentId = null;
function ensureQuestDialogueModal() { function ensureQuestFieldsModal() {
var modal = document.getElementById('questDialogueModal'); var modal = document.getElementById('questFieldsModal');
if (modal) return modal; if (modal) return modal;
modal = document.createElement('div'); modal = document.createElement('div');
modal.className = 'modal'; modal.className = 'modal';
modal.id = 'questDialogueModal'; modal.id = 'questFieldsModal';
modal.innerHTML = modal.innerHTML =
'<div class="modal-background" onclick="closeQuestDialogueModal()"></div>' + '<div class="modal-background" onclick="closeQuestFieldsModal()"></div>' +
'<div class="modal-content questDialogueModalContent"><div class="box">' + '<div class="modal-content questFieldsModalContent"><div class="box">' +
'<div id="questDialogueModalBody"></div>' + '<div id="questFieldsModalBody"></div>' +
'</div></div>'; '</div></div>';
document.body.appendChild(modal); document.body.appendChild(modal);
return modal; return modal;
@@ -105,25 +113,32 @@ function ensureQuestDialogueModal() {
// `onUse(quest)` optionnel : posé par collisionWizardChooseAction('quete', ...) // `onUse(quest)` optionnel : posé par collisionWizardChooseAction('quete', ...)
// pour finaliser la règle de collision avec cette quête, affiche alors un // pour finaliser la règle de collision avec cette quête, affiche alors un
// bouton "✓ Utiliser cette quête" épinglé en haut de la modale. // bouton "✓ Utiliser cette quête" dans les deux modales.
function openQuestDialogueModal(questId, onUse) { function openQuestFieldsModal(questId, onUse) {
_questModalOnUse = onUse || null; _questModalOnUse = onUse || null;
var modal = ensureQuestDialogueModal(); _questModalCurrentId = questId;
var modal = ensureQuestFieldsModal();
modal.classList.add('is-active'); modal.classList.add('is-active');
renderQuestDialogueModalBody(questId); renderQuestFieldsModalBody(questId);
} }
function closeQuestDialogueModal() { // _questModalOnUse/_questModalCurrentId ne sont PAS remis à null ici :
var modal = document.getElementById('questDialogueModal'); // openQuestDialogueModal() ferme cette modale en s'ouvrant (voir plus
// bas) et a besoin de les garder pour son propre bouton "✓ Utiliser
// cette quête" — chaque point d'entrée (openQuestFieldsModal) les fixe
// de toute façon fraîchement à chaque ouverture.
function closeQuestFieldsModal() {
var modal = document.getElementById('questFieldsModal');
if (modal) modal.classList.remove('is-active'); if (modal) modal.classList.remove('is-active');
_questModalOnUse = null;
_questModalCurrentId = null;
} }
function questUseThisQuest(questId) { function questUseThisQuest(questId) {
var quest = questById(questId); var quest = questById(questId);
if (_questModalOnUse && quest) _questModalOnUse(quest); if (_questModalOnUse && quest) _questModalOnUse(quest);
closeQuestDialogueModal(); closeQuestDialogueModal();
closeQuestFieldsModal();
_questModalOnUse = null;
_questModalCurrentId = null;
} }
function questFieldRowHtml(quest) { function questFieldRowHtml(quest) {
@@ -144,6 +159,25 @@ function questFieldRowHtml(quest) {
); );
} }
function renderQuestFieldsModalBody(questId) {
var quest = questById(questId);
var body = document.getElementById('questFieldsModalBody');
if (!quest || !body) return;
var useBtn = _questModalOnUse
? '<button type="button" class="collisionRuleAddBtn" onclick="questUseThisQuest(' + quest.id + ')">✓ Utiliser cette quête</button>'
: '';
body.innerHTML =
'<div class="fieldRow questDialogueModalHeader" style="align-items:center; justify-content:space-between;">' +
'<h2 class="title is-5" style="margin:0;">🗺️ ' + (quest.title || '').replace(/</g, '&lt;') + '</h2>' +
'<button type="button" class="button is-small" onclick="closeQuestFieldsModal()">✕</button>' +
'</div>' +
questFieldRowHtml(quest) +
'<div class="fieldRow" style="justify-content:space-between; align-items:center; margin-top:6px;">' +
'<button type="button" class="collisionRuleAddBtn" onclick="openQuestDialogueModal(' + quest.id + ')">💬 Ajouter les dialogues</button>' +
useBtn +
'</div>';
}
function questDialogueBubbleHtml(line, status, index) { function questDialogueBubbleHtml(line, status, index) {
return ( return (
'<div class="questBubbleRow questBubbleRow-' + line.speaker + '">' + '<div class="questBubbleRow questBubbleRow-' + line.speaker + '">' +
@@ -172,7 +206,42 @@ function questDialogueColumnHtml(quest, status) {
); );
} }
var _questModalCurrentId = null; // Modale de dialogue — PLEIN ÉCRAN (voir .questDialogueModal en CSS),
// ouverte DEPUIS la modale de fiche (bouton "💬 Ajouter les dialogues"),
// jamais les deux à la fois : referme la fiche en s'ouvrant plutôt que de
// les empiler, "← Fiche de la quête" y ramène.
function ensureQuestDialogueModal() {
var modal = document.getElementById('questDialogueModal');
if (modal) return modal;
modal = document.createElement('div');
modal.className = 'modal questDialogueModal';
modal.id = 'questDialogueModal';
modal.innerHTML =
'<div class="modal-background" onclick="closeQuestDialogueModal()"></div>' +
'<div class="modal-content questDialogueModalContent"><div class="box">' +
'<div id="questDialogueModalBody"></div>' +
'</div></div>';
document.body.appendChild(modal);
return modal;
}
function openQuestDialogueModal(questId) {
_questModalCurrentId = questId;
closeQuestFieldsModal();
var modal = ensureQuestDialogueModal();
modal.classList.add('is-active');
renderQuestDialogueModalBody(questId);
}
function closeQuestDialogueModal() {
var modal = document.getElementById('questDialogueModal');
if (modal) modal.classList.remove('is-active');
}
function questBackToFieldsModal(questId) {
closeQuestDialogueModal();
openQuestFieldsModal(questId, _questModalOnUse);
}
function renderQuestDialogueModalBody(questId) { function renderQuestDialogueModalBody(questId) {
var quest = questById(questId); var quest = questById(questId);
@@ -184,12 +253,14 @@ function renderQuestDialogueModalBody(questId) {
: ''; : '';
body.innerHTML = body.innerHTML =
'<div class="fieldRow questDialogueModalHeader" style="align-items:center; justify-content:space-between;">' + '<div class="fieldRow questDialogueModalHeader" style="align-items:center; justify-content:space-between;">' +
'<h2 class="title is-5" style="margin:0;">🗺️ ' + (quest.title || '').replace(/</g, '&lt;') + '</h2>' + '<div style="display:flex; align-items:center; gap:10px;">' +
'<button type="button" class="button is-small" onclick="questBackToFieldsModal(' + quest.id + ')" title="Retour à la fiche">←</button>' +
'<h2 class="title is-5" style="margin:0;">🗺️ ' + (quest.title || '').replace(/</g, '&lt;') + ' — dialogues</h2>' +
'</div>' +
'<div style="display:flex; gap:8px; align-items:center;">' + useBtn + '<div style="display:flex; gap:8px; align-items:center;">' + useBtn +
'<button type="button" class="button is-small" onclick="closeQuestDialogueModal()">✕</button>' + '<button type="button" class="button is-small" onclick="closeQuestDialogueModal()">✕</button>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
questFieldRowHtml(quest) +
'<p class="hint mt-2 mb-2">Dialogue joué selon l\'état de la quête au moment où le joueur parle au PNJ :</p>' + '<p class="hint mt-2 mb-2">Dialogue joué selon l\'état de la quête au moment où le joueur parle au PNJ :</p>' +
'<div class="questDialogueColumns">' + QUEST_STATUS_ORDER.map(function (s) { return questDialogueColumnHtml(quest, s); }).join('') + '</div>'; '<div class="questDialogueColumns">' + QUEST_STATUS_ORDER.map(function (s) { return questDialogueColumnHtml(quest, s); }).join('') + '</div>';
} }
@@ -207,9 +278,7 @@ function questUpdateField(questId, field, value) {
if (!json.quest) return; if (!json.quest) return;
Object.assign(quest, json.quest); Object.assign(quest, json.quest);
renderQuestListPage(); renderQuestListPage();
if (document.getElementById('questDialogueModal') && document.getElementById('questDialogueModal').classList.contains('is-active')) { renderQuestFieldsModalBody(questId); // seule modale à porter des champs éditables
renderQuestDialogueModalBody(questId);
}
}); });
} }
@@ -263,14 +332,13 @@ function _currentQuestModalId() {
} }
// ---------- Sélecteur de quête (assistant "+ Action" de l'éditeur de collision) ---------- // ---------- Sélecteur de quête (assistant "+ Action" de l'éditeur de collision) ----------
// Récupère la liste à jour (jamais présente d'avance dans scene_edit.html, // Affiche des cartes + "+ Nouvelle quête" DANS la modale de collision
// contrairement à quests.html) puis affiche des cartes + "+ Nouvelle // déjà ouverte (voir renderCollisionWizardActionStep) ; choisir/créer une
// quête" DANS la modale de collision déjà ouverte (voir // quête ouvre ENSUITE la modale de FICHE (celle-ci par-dessus), avec un
// renderCollisionWizardActionStep) ; choisir/créer une quête ouvre // bouton "✓ Utiliser cette quête" (disponible aussi depuis la modale de
// ENSUITE la modale de dialogue (celle-ci empilée par-dessus), avec un // dialogue) qui referme tout et finalise la règle — exactement le
// bouton "✓ Utiliser cette quête" qui referme les deux et finalise la // comportement demandé : cliquer "Déclencher une quête" ouvre l'éditeur
// règle — exactement le comportement demandé : cliquer "Déclencher une // de quête, pas un simple champ texte.
// quête" ouvre l'éditeur de quête, pas un simple champ texte.
function questPickerHtml(quests) { function questPickerHtml(quests) {
if (!quests.length) { if (!quests.length) {
return '<p class="hint mb-2">Aucune quête pour l\'instant.</p>'; return '<p class="hint mb-2">Aucune quête pour l\'instant.</p>';
@@ -299,7 +367,7 @@ function renderQuestPickerStep(onUse) {
} }
function questPickerChoose(questId) { function questPickerChoose(questId) {
openQuestDialogueModal(questId, _questPickerOnUse); openQuestFieldsModal(questId, _questPickerOnUse);
} }
function questPickerCreate() { function questPickerCreate() {
@@ -309,7 +377,7 @@ function questPickerCreate() {
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (json) { .then(function (json) {
QUESTS.unshift(json.quest); QUESTS.unshift(json.quest);
openQuestDialogueModal(json.quest.id, _questPickerOnUse); openQuestFieldsModal(json.quest.id, _questPickerOnUse);
}); });
} }
+9 -1
View File
@@ -773,7 +773,15 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
.questPill-status-en_cours{ background:rgba(242,153,74,.18); color:#f2994a; border-color:transparent; } .questPill-status-en_cours{ background:rgba(242,153,74,.18); color:#f2994a; border-color:transparent; }
.questPill-status-terminee{ background:rgba(46,204,113,.18); color:#2ecc71; border-color:transparent; } .questPill-status-terminee{ background:rgba(46,204,113,.18); color:#2ecc71; border-color:transparent; }
.questDialogueModalContent{ max-width:min(92vw, 1080px); } .questFieldsModalContent{ max-width:min(92vw, 640px); }
/* Modale "j'ajoute les dialogues" — PLEIN ÉCRAN, demandé explicitement
(distincte de la modale de fiche ci-dessus) : couvre tout le viewport
plutôt qu'une boîte centrée, pour laisser toute la place aux 3
colonnes de répliques. */
.questDialogueModal .modal-content{ width:100vw; height:100vh; max-width:100vw; max-height:100vh; margin:0; }
.questDialogueModal .box{ width:100%; height:100%; border-radius:0; overflow-y:auto; }
.questDialogueModalContent{ width:100%; height:100%; max-width:100%; }
.questDialogueModalHeader{ margin-bottom:10px; } .questDialogueModalHeader{ margin-bottom:10px; }
.questFieldsGrid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:10px; margin-bottom:14px; } .questFieldsGrid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:10px; margin-bottom:14px; }
.questField{ display:flex; flex-direction:column; gap:4px; font-size:12px; font-weight:600; } .questField{ display:flex; flex-direction:column; gap:4px; font-size:12px; font-weight:600; }