- "qui parle" n'est plus réservé au personnage : N'IMPORTE QUEL objet de scène nommé (personnage, décor, fond — voir "ℹ️ Informations", screens/rendering/scene_object_names.py, ex-personnage_names.py) peut parler dans un dialogue. - Confirmé/documenté : aucune limite au nombre de répliques par colonne (bouton "+ Réplique" reste toujours disponible). - Bulle = header (menu déroulant du "qui parle") + body (le texte), centrée dans sa colonne à 70% de largeur, alignée verticalement et reliée à la suivante par un trait — remplace l'ancien alignement gauche/droite façon chat. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
432 lines
20 KiB
JavaScript
432 lines
20 KiB
JavaScript
// ---------- É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 : [];
|
||
|
||
// Noms d'objets de scène nommés (voir "ℹ️ Informations", templates/
|
||
// scene_edit.html) — N'IMPORTE QUEL objet (personnage, décor, fond), pas
|
||
// seulement un personnage — proposés comme "qui parle" pour une réplique,
|
||
// en plus de "Joueur" (toujours disponible, jamais un objet de scène).
|
||
var SCENE_OBJECT_NAMES = (typeof SCENE_OBJECT_NAMES_JSON !== 'undefined') ? SCENE_OBJECT_NAMES_JSON : [];
|
||
|
||
// Une réplique n'a que deux COULEURS possibles (voir .questBubble-joueur/
|
||
// .questBubble-pnj, static/style.css) même si le nom affiché est
|
||
// n'importe quel objet nommé — "Joueur" en vert, n'importe
|
||
// quel autre nom à gauche en bleu (voir demande utilisateur : "les bulles
|
||
// du pnj et du joueur sont d'une couleur différente").
|
||
function questBubbleKind(speaker) {
|
||
return speaker === 'Joueur' ? 'joueur' : 'pnj';
|
||
}
|
||
|
||
function questById(questId) {
|
||
return QUESTS.find(function (q) { return q.id === 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="openQuestFieldsModal(' + quest.id + ')">' +
|
||
'<td><strong>' + (quest.title || '').replace(/</g, '<') + '</strong></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-status questPill-status-' + quest.status + '">' + QUEST_STATUS_LABELS[quest.status] + '</span></td>' +
|
||
'<td>' + resultatHtml + '</td>' +
|
||
'<td class="fieldsTableActions">' +
|
||
'<button type="button" class="button is-small is-danger" title="Supprimer" onclick="event.stopPropagation(); questDeleteFromList(' + quest.id + ')">🗑️</button>' +
|
||
'</td>' +
|
||
'</tr>'
|
||
);
|
||
}
|
||
|
||
function renderQuestListPage() {
|
||
var container = document.getElementById('questsTabPanel');
|
||
if (!container) return;
|
||
var wrap = document.getElementById('questsListWrap');
|
||
if (!QUESTS.length) {
|
||
wrap.innerHTML = '<p class="hint">Aucune quête pour l\'instant — crée-en une avec le bouton ci-dessus.</p>';
|
||
return;
|
||
}
|
||
wrap.innerHTML =
|
||
'<div class="fieldsTableWrap"><table class="table is-fullwidth is-hoverable fieldsTable">' +
|
||
'<thead><tr><th>Titre</th><th>Objectif</th><th>Récompense</th><th>Statut</th><th>Résultat</th><th></th></tr></thead>' +
|
||
'<tbody>' + QUESTS.map(questListRowHtml).join('') + '</tbody>' +
|
||
'</table></div>';
|
||
}
|
||
|
||
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 =
|
||
'<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;
|
||
}
|
||
|
||
// `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 (
|
||
'<div class="questFieldsGrid">' +
|
||
'<label class="questField"><span>Titre</span><input type="text" class="input" value="' + (quest.title || '').replace(/"/g, '"') + '" onchange="questUpdateField(' + quest.id + ", 'title', this.value)\"></label>" +
|
||
'<label class="questField questField-wide"><span>Objectif</span><input type="text" class="input" value="' + (quest.objectif || '').replace(/"/g, '"') + '" onchange="questUpdateField(' + quest.id + ", 'objectif', this.value)\"></label>" +
|
||
'<label class="questField"><span>Récompense (score)</span><input type="number" class="input" value="' + (quest.recompense_score || 0) + '" onchange="questUpdateField(' + quest.id + ", 'recompense_score', this.value)\"></label>" +
|
||
'<label class="questField"><span>Statut</span><select class="input" onchange="questUpdateField(' + quest.id + ", 'status', this.value)\">" +
|
||
QUEST_STATUS_ORDER.map(function (s) { return '<option value="' + s + '"' + (quest.status === s ? ' selected' : '') + '>' + QUEST_STATUS_LABELS[s] + '</option>'; }).join('') +
|
||
'</select></label>' +
|
||
'<label class="questField"><span>Résultat</span><select class="input" onchange="questUpdateField(' + quest.id + ", 'resultat', this.value)\">" +
|
||
'<option value=""' + (!quest.resultat ? ' selected' : '') + '>—</option>' +
|
||
'<option value="reussi"' + (quest.resultat === 'reussi' ? ' selected' : '') + '>Réussi</option>' +
|
||
'<option value="echoue"' + (quest.resultat === 'echoue' ? ' selected' : '') + '>Échoué</option>' +
|
||
'</select></label>' +
|
||
'</div>'
|
||
);
|
||
}
|
||
|
||
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>';
|
||
}
|
||
|
||
// Options du <select> "qui parle" — "Joueur" toujours en premier, puis
|
||
// chaque personnage nommé (voir "ℹ️ Informations"), plus le speaker
|
||
// ACTUEL de la réplique s'il n'est plus dans cette liste (personnage
|
||
// renommé/supprimé depuis) pour ne jamais perdre silencieusement la
|
||
// valeur déjà enregistrée.
|
||
function questSpeakerOptions(currentSpeaker) {
|
||
var options = ['Joueur'].concat(SCENE_OBJECT_NAMES);
|
||
if (currentSpeaker && options.indexOf(currentSpeaker) === -1) options.push(currentSpeaker);
|
||
return options;
|
||
}
|
||
|
||
// Une bulle = header (qui parle, menu déroulant des objets de scène
|
||
// nommés — voir "ℹ️ Informations") + body (le texte) — deux zones
|
||
// distinctes demandées explicitement, pas un seul bloc. Toujours centrée
|
||
// dans sa colonne, 70% de la largeur (voir .questBubble, static/style.css) —
|
||
// aucune limite au nombre de bulles par colonne (voir questAddBubble).
|
||
function questDialogueBubbleHtml(line, status, index) {
|
||
var kind = questBubbleKind(line.speaker);
|
||
var speakerSelect = questSpeakerOptions(line.speaker).map(function (name) {
|
||
return '<option value="' + name.replace(/"/g, '"') + '"' + (name === line.speaker ? ' selected' : '') + '>' + (name === 'Joueur' ? '🧑 Joueur' : '🗨️ ' + name) + '</option>';
|
||
}).join('');
|
||
return (
|
||
'<div class="questBubbleRow">' +
|
||
'<div class="questBubble questBubble-' + kind + '">' +
|
||
'<div class="questBubbleHeader">' +
|
||
'<select class="questBubbleSpeaker" title="Qui parle" onchange="questSetBubbleSpeaker(\'' + status + '\', ' + index + ', this.value)">' + speakerSelect + '</select>' +
|
||
'<button type="button" class="questBubbleDelete" title="Supprimer" onclick="questDeleteBubble(\'' + status + '\', ' + index + ')">✕</button>' +
|
||
'</div>' +
|
||
'<div class="questBubbleBody">' +
|
||
'<textarea class="questBubbleText" rows="1" placeholder="Réplique..." ' +
|
||
'oninput="this.style.height=\'auto\'; this.style.height=this.scrollHeight+\'px\';" ' +
|
||
'onchange="questUpdateBubbleText(\'' + status + '\', ' + index + ', this.value)">' + line.text.replace(/</g, '<') + '</textarea>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>'
|
||
);
|
||
}
|
||
|
||
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 (
|
||
'<div class="questDialogueColumn">' +
|
||
'<h4 class="questDialogueColumnTitle">' + QUEST_STATUS_LABELS[status] + '</h4>' +
|
||
'<div class="questDialogueColumnBubbles" id="questDialogueColumn-' + status + '">' + bubblesHtml + '</div>' +
|
||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn" onclick="questAddBubble(\'' + status + '\')">✚ Réplique</button>' +
|
||
'</div>'
|
||
);
|
||
}
|
||
|
||
// 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);
|
||
var body = document.getElementById('questDialogueModalBody');
|
||
if (!quest || !body) return;
|
||
_questModalCurrentId = questId;
|
||
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;">' +
|
||
'<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 +
|
||
'<button type="button" class="button is-small" onclick="closeQuestDialogueModal()">✕</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<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>';
|
||
}
|
||
|
||
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; });
|
||
}
|
||
|
||
// Alterne "Joueur" <-> le premier personnage nommé (ou "PNJ" faute de
|
||
// mieux, si aucun personnage n'a encore de nom — voir "ℹ️ Informations")
|
||
// — juste un défaut de départ, l'auteur choisit ensuite librement dans
|
||
// le menu déroulant de la réplique.
|
||
function questNextDefaultSpeaker(lastSpeaker) {
|
||
if (lastSpeaker === 'Joueur') return SCENE_OBJECT_NAMES[0] || 'PNJ';
|
||
return 'Joueur';
|
||
}
|
||
|
||
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: questNextDefaultSpeaker(lastSpeaker), text: '' });
|
||
renderQuestDialogueModalBody(quest.id);
|
||
questSaveDialogues(quest);
|
||
}
|
||
|
||
function questSetBubbleSpeaker(status, index, speaker) {
|
||
var quest = questById(_currentQuestModalId());
|
||
if (!quest) return;
|
||
quest.dialogues[status][index].speaker = speaker;
|
||
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 '<p class="hint mb-2">Aucune quête pour l\'instant.</p>';
|
||
}
|
||
return '<div class="collisionWizardChoices">' + quests.map(function (q, i) {
|
||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="questPickerChoose(' + q.id + ')">' +
|
||
'<span class="emoji">🗺️</span><span>' + (q.title || '').replace(/</g, '<') + '</span>' +
|
||
'</button>';
|
||
}).join('') + '</div>';
|
||
}
|
||
|
||
var _questPickerOnUse = null;
|
||
|
||
function renderQuestPickerStep(onUse) {
|
||
_questPickerOnUse = onUse;
|
||
var body = collisionWizardBodyEl();
|
||
body.innerHTML = '<p class="hint mb-2">Quelle quête déclencher ?</p><div id="questPickerList">Chargement…</div>' +
|
||
'<button type="button" class="collisionRuleAddBtn mt-2" onclick="questPickerCreate()">✚ Nouvelle quête</button>';
|
||
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();
|
||
});
|