// ---------- É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. // // 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 // 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). 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 ? '' + QUEST_RESULT_LABELS[quest.resultat] + '' : ''; return ( '
' + '
' + '

' + (quest.title || '').replace(/' + '' + QUEST_STATUS_LABELS[quest.status] + '' + '

' + '

' + (quest.objectif ? quest.objectif.replace(/Aucun objectif défini.') + '

' + '
' + '🏆 +' + (quest.recompense_score || 0) + '' + resultatHtml + '
' + '
' ); } function renderQuestListPage() { var container = document.getElementById('questsGrid'); if (!container) return; if (!QUESTS.length) { container.innerHTML = '

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

'; return; } container.innerHTML = QUESTS.map(questCardHtml).join(''); } 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(); openQuestDialogueModal(json.quest.id); }); } // ---------- 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. var _questModalOnUse = null; function ensureQuestDialogueModal() { var modal = document.getElementById('questDialogueModal'); if (modal) return modal; modal = document.createElement('div'); modal.className = 'modal'; modal.id = 'questDialogueModal'; 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" épinglé en haut de la modale. function openQuestDialogueModal(questId, onUse) { _questModalOnUse = onUse || null; var modal = ensureQuestDialogueModal(); modal.classList.add('is-active'); renderQuestDialogueModalBody(questId); } function closeQuestDialogueModal() { var modal = document.getElementById('questDialogueModal'); if (modal) modal.classList.remove('is-active'); _questModalOnUse = null; _questModalCurrentId = null; } function questUseThisQuest(questId) { var quest = questById(questId); if (_questModalOnUse && quest) _questModalOnUse(quest); closeQuestDialogueModal(); } function questFieldRowHtml(quest) { return ( '
' + '" + '" + '" + '' + '' + '
' ); } 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 + '
' + '' + '
' ); } var _questModalCurrentId = null; 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 + '' + '
' + '

' + questFieldRowHtml(quest) + '

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(); if (document.getElementById('questDialogueModal') && document.getElementById('questDialogueModal').classList.contains('is-active')) { renderQuestDialogueModalBody(questId); } }); } // 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) ---------- // 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. 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) { openQuestDialogueModal(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); openQuestDialogueModal(json.quest.id, _questPickerOnUse); }); } document.addEventListener('DOMContentLoaded', function () { if (typeof QUESTS_JSON !== 'undefined') renderQuestListPage(); });