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 ----------
// 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.
// "📣 Événements") plutôt qu'une grille de cartes. Un clic sur une ligne
// ouvre la modale de FICHE (titre/objectif/récompense/statut/résultat —
// "je crée"), qui ouvre à son tour, sur un bouton dédié, la modale
// PLEIN ÉCRAN d'arbre de dialogue ("j'ajoute les dialogues") — deux
// étapes séparées, jamais le même formulaire. Les deux modales sont
// aussi ouvertes 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éées dynamiquement (ensureQuest*Modal)
// plutôt que dupliquées dans deux endroits.
//
// Une quête (voir db/quests/) : id, title, objectif, recompense_score,
// status (nouvelle/en_cours/terminee — statut COURANT, informatif),
@@ -32,7 +34,7 @@ function questById(questId) {
function questListRowHtml(quest) {
var resultatHtml = quest.resultat ? '<span class="questPill questPill-resultat">' + QUEST_RESULT_LABELS[quest.resultat] + '</span>' : '<span class="hint">—</span>';
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>' + (quest.objectif ? quest.objectif.replace(/</g, '&lt;') : '<span class="hint">—</span>') + '</td>' +
'<td><span class="questPill questPill-score">🏆 +' + (quest.recompense_score || 0) + '</span></td>' +
@@ -68,7 +70,7 @@ function createQuestFromListPage() {
.then(function (json) {
QUESTS.unshift(json.quest);
renderQuestListPage();
openQuestDialogueModal(json.quest.id);
openQuestFieldsModal(json.quest.id);
});
}
@@ -82,22 +84,28 @@ function questDeleteFromList(questId) {
});
}
// ---------- 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
// collision, jamais dupliquée dans les templates.
// ---------- Deux modales distinctes ----------
// 1. #questFieldsModal (taille normale) : "je crée" — titre/objectif/
// récompense/statut/résultat, + un bouton qui ouvre...
// 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 _questModalCurrentId = null;
function ensureQuestDialogueModal() {
var modal = document.getElementById('questDialogueModal');
function ensureQuestFieldsModal() {
var modal = document.getElementById('questFieldsModal');
if (modal) return modal;
modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'questDialogueModal';
modal.id = 'questFieldsModal';
modal.innerHTML =
'<div class="modal-background" onclick="closeQuestDialogueModal()"></div>' +
'<div class="modal-content questDialogueModalContent"><div class="box">' +
'<div id="questDialogueModalBody"></div>' +
'<div class="modal-background" onclick="closeQuestFieldsModal()"></div>' +
'<div class="modal-content questFieldsModalContent"><div class="box">' +
'<div id="questFieldsModalBody"></div>' +
'</div></div>';
document.body.appendChild(modal);
return modal;
@@ -105,25 +113,32 @@ function ensureQuestDialogueModal() {
// `onUse(quest)` optionnel : posé par collisionWizardChooseAction('quete', ...)
// 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.
function openQuestDialogueModal(questId, onUse) {
// bouton "✓ Utiliser cette quête" dans les deux modales.
function openQuestFieldsModal(questId, onUse) {
_questModalOnUse = onUse || null;
var modal = ensureQuestDialogueModal();
_questModalCurrentId = questId;
var modal = ensureQuestFieldsModal();
modal.classList.add('is-active');
renderQuestDialogueModalBody(questId);
renderQuestFieldsModalBody(questId);
}
function closeQuestDialogueModal() {
var modal = document.getElementById('questDialogueModal');
// _questModalOnUse/_questModalCurrentId ne sont PAS remis à null ici :
// 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');
_questModalOnUse = null;
_questModalCurrentId = null;
}
function questUseThisQuest(questId) {
var quest = questById(questId);
if (_questModalOnUse && quest) _questModalOnUse(quest);
closeQuestDialogueModal();
closeQuestFieldsModal();
_questModalOnUse = null;
_questModalCurrentId = null;
}
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) {
return (
'<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) {
var quest = questById(questId);
@@ -184,12 +253,14 @@ function renderQuestDialogueModalBody(questId) {
: '';
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>' +
'<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 +
'<button type="button" class="button is-small" onclick="closeQuestDialogueModal()">✕</button>' +
'</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>' +
'<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;
Object.assign(quest, json.quest);
renderQuestListPage();
if (document.getElementById('questDialogueModal') && document.getElementById('questDialogueModal').classList.contains('is-active')) {
renderQuestDialogueModalBody(questId);
}
renderQuestFieldsModalBody(questId); // seule modale à porter des champs éditables
});
}
@@ -263,14 +332,13 @@ function _currentQuestModalId() {
}
// ---------- 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,
// contrairement à quests.html) puis affiche des cartes + "+ Nouvelle
// quête" DANS la modale de collision déjà ouverte (voir
// renderCollisionWizardActionStep) ; choisir/créer une quête ouvre
// ENSUITE la modale de dialogue (celle-ci empilée par-dessus), avec un
// bouton "✓ Utiliser cette quête" qui referme les deux et finalise la
// règle — exactement le comportement demandé : cliquer "Déclencher une
// quête" ouvre l'éditeur de quête, pas un simple champ texte.
// Affiche des cartes + "+ Nouvelle quête" DANS la modale de collision
// déjà ouverte (voir renderCollisionWizardActionStep) ; choisir/créer une
// quête ouvre ENSUITE la modale de FICHE (celle-ci par-dessus), avec un
// bouton "✓ Utiliser cette quête" (disponible aussi depuis la modale de
// dialogue) qui referme tout et finalise la règle — exactement le
// comportement demandé : cliquer "Déclencher une quête" ouvre l'éditeur
// de quête, pas un simple champ texte.
function questPickerHtml(quests) {
if (!quests.length) {
return '<p class="hint mb-2">Aucune quête pour l\'instant.</p>';
@@ -299,7 +367,7 @@ function renderQuestPickerStep(onUse) {
}
function questPickerChoose(questId) {
openQuestDialogueModal(questId, _questPickerOnUse);
openQuestFieldsModal(questId, _questPickerOnUse);
}
function questPickerCreate() {
@@ -309,7 +377,7 @@ function questPickerCreate() {
.then(function (r) { return r.json(); })
.then(function (json) {
QUESTS.unshift(json.quest);
openQuestDialogueModal(json.quest.id, _questPickerOnUse);
openQuestFieldsModal(json.quest.id, _questPickerOnUse);
});
}