// ---------- É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. // // 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(); openQuestDialogueModal(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(); }); } // ---------- 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(); });