// ---------- É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. 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), // resultat (reussi/echoue), dialogues (une liste de répliques PAR // 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 (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 questListRowHtml(quest) { var resultatHtml = quest.resultat ? '' + QUEST_RESULT_LABELS[quest.resultat] + '' : '—'; return ( '' + '' + (quest.title || '').replace(/' + '' + (quest.objectif ? quest.objectif.replace(/—') + '' + '🏆 +' + (quest.recompense_score || 0) + '' + '' + QUEST_STATUS_LABELS[quest.status] + '' + '' + resultatHtml + '' + '' + '' + '' + '' ); } function renderQuestListPage() { var container = document.getElementById('questsTabPanel'); if (!container) return; var wrap = document.getElementById('questsListWrap'); if (!QUESTS.length) { wrap.innerHTML = '

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

'; return; } wrap.innerHTML = '
' + '' + '' + QUESTS.map(questListRowHtml).join('') + '' + '
TitreObjectifRécompenseStatutRésultat
'; } function createQuestFromListPage() { fetch('/game/' + GAME_SLUG + '/quests/new', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) .then(function (r) { return r.json(); }) .then(function (json) { QUESTS.unshift(json.quest); renderQuestListPage(); openQuestFieldsModal(json.quest.id); }); } 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(); }); } // ---------- 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 ensureQuestFieldsModal() { var modal = document.getElementById('questFieldsModal'); if (modal) return modal; modal = document.createElement('div'); modal.className = 'modal'; modal.id = 'questFieldsModal'; modal.innerHTML = '' + ''; document.body.appendChild(modal); return modal; } // `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" dans les deux modales. function openQuestFieldsModal(questId, onUse) { _questModalOnUse = onUse || null; _questModalCurrentId = questId; var modal = ensureQuestFieldsModal(); modal.classList.add('is-active'); renderQuestFieldsModalBody(questId); } // _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'); } function questUseThisQuest(questId) { var quest = questById(questId); if (_questModalOnUse && quest) _questModalOnUse(quest); closeQuestDialogueModal(); closeQuestFieldsModal(); _questModalOnUse = null; _questModalCurrentId = null; } function questFieldRowHtml(quest) { return ( '
' + '" + '" + '" + '' + '' + '
' ); } function renderQuestFieldsModalBody(questId) { var quest = questById(questId); var body = document.getElementById('questFieldsModalBody'); if (!quest || !body) return; var useBtn = _questModalOnUse ? '' : ''; body.innerHTML = '
' + '

🗺️ ' + (quest.title || '').replace(/' + '' + '

' + questFieldRowHtml(quest) + '
' + '' + useBtn + '
'; } function questDialogueBubbleHtml(line, status, index) { return ( '
' + '
' + '
' + (line.speaker === 'joueur' ? '🧑 Joueur' : '🗨️ PNJ') + '
' + '' + '' + '
' + '
' ); } function questDialogueColumnHtml(quest, status) { var lines = (quest.dialogues && quest.dialogues[status]) || []; var bubblesHtml = lines.map(function (line, i) { return questDialogueBubbleHtml(line, status, i); }).join(''); return ( '
' + '

' + QUEST_STATUS_LABELS[status] + '

' + '
' + bubblesHtml + '
' + '' + '
' ); } // 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 = '' + ''; 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); var body = document.getElementById('questDialogueModalBody'); if (!quest || !body) return; _questModalCurrentId = questId; var useBtn = _questModalOnUse ? '' : ''; body.innerHTML = '
' + '
' + '' + '

🗺️ ' + (quest.title || '').replace(/' + '

' + '
' + useBtn + '' + '
' + '
' + '

Dialogue joué selon l\'état de la quête au moment où le joueur parle au PNJ :

' + '
' + QUEST_STATUS_ORDER.map(function (s) { return questDialogueColumnHtml(quest, s); }).join('') + '
'; } function questUpdateField(questId, field, value) { var quest = questById(questId); if (!quest) return; var body = {}; body[field] = field === 'recompense_score' ? Number(value) : (value || null); fetch('/game/' + GAME_SLUG + '/quests/' + questId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) .then(function (r) { return r.json(); }) .then(function (json) { if (!json.quest) return; Object.assign(quest, json.quest); renderQuestListPage(); renderQuestFieldsModalBody(questId); // seule modale à porter des champs éditables }); } // Dialogues : toujours les 3 colonnes ENTIÈRES postées ensemble (voir // db/quests/set_quest_dialogues.py) — jamais une réplique isolée. function questSaveDialogues(quest) { fetch('/game/' + GAME_SLUG + '/quests/' + quest.id + '/dialogues', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dialogues: quest.dialogues }), }) .then(function (r) { return r.json(); }) .then(function (json) { if (json.dialogues) quest.dialogues = json.dialogues; }); } function questAddBubble(status) { var quest = questById(_currentQuestModalId()); if (!quest) return; quest.dialogues = quest.dialogues || {}; var lines = quest.dialogues[status] || (quest.dialogues[status] = []); var lastSpeaker = lines.length ? lines[lines.length - 1].speaker : 'pnj'; lines.push({ speaker: lastSpeaker === 'pnj' ? 'joueur' : 'pnj', text: '' }); renderQuestDialogueModalBody(quest.id); questSaveDialogues(quest); } function questToggleSpeaker(status, index) { var quest = questById(_currentQuestModalId()); if (!quest) return; var line = quest.dialogues[status][index]; line.speaker = line.speaker === 'joueur' ? 'pnj' : 'joueur'; renderQuestDialogueModalBody(quest.id); questSaveDialogues(quest); } function questUpdateBubbleText(status, index, text) { var quest = questById(_currentQuestModalId()); if (!quest) return; quest.dialogues[status][index].text = text; questSaveDialogues(quest); } function questDeleteBubble(status, index) { var quest = questById(_currentQuestModalId()); if (!quest) return; quest.dialogues[status].splice(index, 1); renderQuestDialogueModalBody(quest.id); questSaveDialogues(quest); } function _currentQuestModalId() { return _questModalCurrentId; } // ---------- Sélecteur de quête (assistant "+ Action" de l'éditeur de collision) ---------- // 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 '

Aucune quête pour l\'instant.

'; } return '
' + quests.map(function (q, i) { return ''; }).join('') + '
'; } var _questPickerOnUse = null; function renderQuestPickerStep(onUse) { _questPickerOnUse = onUse; var body = collisionWizardBodyEl(); body.innerHTML = '

Quelle quête déclencher ?

Chargement…
' + ''; fetch('/game/' + GAME_SLUG + '/quests/list.json') .then(function (r) { return r.json(); }) .then(function (json) { QUESTS = json.quests; var list = document.getElementById('questPickerList'); if (list) list.innerHTML = questPickerHtml(QUESTS); }); } function questPickerChoose(questId) { openQuestFieldsModal(questId, _questPickerOnUse); } function questPickerCreate() { fetch('/game/' + GAME_SLUG + '/quests/new', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) .then(function (r) { return r.json(); }) .then(function (json) { QUESTS.unshift(json.quest); openQuestFieldsModal(json.quest.id, _questPickerOnUse); }); } document.addEventListener('DOMContentLoaded', function () { if (typeof QUESTS_JSON !== 'undefined') renderQuestListPage(); });