Quêtes : dialogue dans une modale à part, en plein écran
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:
co-authored by
Claude Sonnet 5
parent
78c1aba8a0
commit
390ee831ca
@@ -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, '<') + '</strong></td>' +
|
'<td><strong>' + (quest.title || '').replace(/</g, '<') + '</strong></td>' +
|
||||||
'<td>' + (quest.objectif ? quest.objectif.replace(/</g, '<') : '<span class="hint">—</span>') + '</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-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, '<') + '</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, '<') + '</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, '<') + ' — 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
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user