I made this API script for Abomination vault, cause by default all the level is in one handout ant it's not possible to pinpoint. The script create as many handouts as there are rooms. var DecoupageDonjon = (function () { "use strict"; var NOM_COMMANDE = "!decouper-donjon"; var NOM_COMMANDE_ID = "!decouper-donjon-id"; var NOM_COMMANDE_PAGE = "!decouper-donjon-page"; var HANDOUTS_PAR_PAGE = 12; /* * Affiche un message dans le chat. */ function afficherMessage(message) { sendChat("Découpage Donjon", message); } /* * Envoie un message au joueur qui a lancé la commande. */ function envoyerAuJoueur(playerId, message) { var joueur = getObj("player", playerId); if (joueur) { var nomJoueur = joueur.get("displayname") || ""; sendChat( "Découpage Donjon", '/w "' + nomJoueur.replace(/"/g, '\\"') + '" ' + message ); } else { afficherMessage(message); } } /* * Échappe les caractères HTML. */ function echapperHTML(texte) { return String(texte || "") .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#39;"); } /* * Décode les principales entités HTML. */ function decoderEntites(texte) { return String(texte || "") .replace(/&nbsp;/gi, " ") .replace(/&#160;/gi, " ") .replace(/&amp;/gi, "&") .replace(/&apos;/gi, "'") .replace(/&#39;/gi, "'") .replace(/&#x27;/gi, "'") .replace(/&quot;/gi, '"') .replace(/&lt;/gi, "<") .replace(/&gt;/gi, ">"); } /* * Transforme les liens HTML en liens Markdown. * * Exemple : * <a href="<a href="https://exemple.com">Voir" rel="nofollow">https://exemple.com">Voir</a> le site</a> * * devient : * [Voir le site](<a href="https://exemple.com" rel="nofollow">https://exemple.com</a>) */ function conserverLiensHTML(texte) { return String(texte || "").replace( /<a\b[^>]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a\s*>/gi, function ( correspondance, urlDouble, urlSimple, urlSansGuillemets, libelle ) { var url = urlDouble || urlSimple || urlSansGuillemets || ""; var texteLien = String(libelle || "") .replace(/<[^>]*>/g, ""); texteLien = decoderEntites(texteLien); url = decoderEntites(url); if (!/^https?:\/\//i.test(url)) { return texteLien; } return "[" + texteLien + "](" + url + ")"; } ); } /* * Convertit le contenu HTML en texte pour détecter * les titres de salles et séparer les descriptions. * * Les balises blockquote sont conservées sous la forme * de lignes commençant par >. */ function preparerPourDetection(contenu) { var texte = String(contenu || ""); // Conserver les liens avant de retirer les balises HTML. texte = conserverLiensHTML(texte); // Identifier les citations HTML. texte = texte.replace( /<blockquote\b[^>]*>/gi, "\n__DEBUT_CITATION__\n" ); texte = texte.replace( /<\/blockquote\s*>/gi, "\n__FIN_CITATION__\n" ); // Convertir les séparations HTML en retours à la ligne. texte = texte.replace(/<br\b[^>]*>/gi, "\n"); texte = texte.replace( /<\/(?:p|div|li|h[1-6]|tr|section|article)\s*>/gi, "\n" ); // Supprimer les autres balises HTML. texte = texte.replace(/<[^>]*>/g, ""); // Décoder les entités HTML. texte = decoderEntites(texte); // Uniformiser les fins de ligne. texte = texte.replace(/\r\n?/g, "\n"); /* * Transformer les blocs de citation en lignes Markdown. */ texte = texte.replace( /__DEBUT_CITATION__([\s\S]*?)__FIN_CITATION__/g, function (correspondance, contenuCitation) { var lignes = contenuCitation.split("\n"); lignes = lignes.map(function (ligne) { ligne = ligne.trim(); if (!ligne) { return ""; } if (/^>\s?/.test(ligne)) { return ligne; } return "> " + ligne; }); return "\n" + lignes.join("\n") + "\n"; } ); // Nettoyer les espaces et les lignes vides superflues. texte = texte .replace(/[ \t]+\n/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); return texte; } /* * Détecte les titres de salles. * * Formats reconnus : * A1. Entrée * A21. Atelier de Volluk * B2. Salle des serviteurs * # A1. Entrée */ function detecterSalles(contenu) { var texte = preparerPourDetection(contenu); var regex = /^\s*(?:#{1,6}\s*)?([A-Z]\d+)\.\s*(.+?)\s*$/gm; var salles = []; var resultat; while ((resultat = regex.exec(texte)) !== null) { salles.push({ code: resultat[1], titre: resultat[2].trim(), index: resultat.index, longueur: resultat[0].length }); } return { texte: texte, salles: salles }; } /* * Convertit le texte Markdown simple en HTML. * * Les liens Markdown sont transformés en liens HTML * cliquables. */ function convertirTexte(texte) { var html = echapperHTML(texte || ""); // Liens Markdown. html = html.replace( /\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, function (correspondance, libelle, url) { return '<a href="' + url + '" target="_blank">' + libelle + "</a>"; } ); // Texte en gras : **texte**. html = html.replace( /\*\*([\s\S]+?)\*\*/g, "<strong>$1</strong>" ); // Texte en italique : *texte*. html = html.replace( /(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>" ); // Texte en italique : _texte_. html = html.replace( /(^|[^_])_([^_\n]+)_(?!_)/g, "$1<em>$2</em>" ); // Convertir les retours à la ligne en HTML. html = html.replace(/\n/g, "<br>"); return html; } /* * Sépare la description des joueurs et les notes du MJ. * * Les paragraphes entièrement précédés de > sont * considérés comme des descriptions destinées aux joueurs. * * La description est ensuite enveloppée dans une balise * blockquote pour obtenir un véritable style de citation. */ function separerContenu(texte) { var blocs = String(texte || "") .split(/\n\s*\n/) .map(function (bloc) { return bloc.trim(); }) .filter(function (bloc) { return bloc.length > 0; }); var blocsNotes = []; var blocsGMNotes = []; blocs.forEach(function (bloc) { var lignes = bloc.split("\n"); var estDescriptionJoueurs = lignes.every( function (ligne) { return /^\s*>/.test(ligne); } ); if (estDescriptionJoueurs) { var description = lignes.map(function (ligne) { return ligne.replace(/^\s*>\s?/, ""); }).join("\n").trim(); if (description) { blocsNotes.push(description); } } else { blocsGMNotes.push(bloc); } }); var descriptionHTML = blocsNotes.length ? "<blockquote>" + convertirTexte(blocsNotes.join("\n\n")) + "</blockquote>" : ""; var notesMJHTML = convertirTexte( blocsGMNotes.join("\n\n") ); return { notes: descriptionHTML, gmnotes: notesMJHTML }; } /* * Affiche la liste des handouts disponibles. * La liste est paginée. */ function afficherListeHandouts(playerId, pageDemandee) { var handouts = findObjs({ _type: "handout" }); handouts.sort(function (a, b) { return String(a.get("name") || "").localeCompare( String(b.get("name") || "") ); }); var nombrePages = Math.max( 1, Math.ceil(handouts.length / HANDOUTS_PAR_PAGE) ); var page = parseInt(pageDemandee, 10); if (isNaN(page) || page < 0) { page = 0; } if (page >= nombrePages) { page = nombrePages - 1; } var debut = page * HANDOUTS_PAR_PAGE; var selection = handouts.slice( debut, debut + HANDOUTS_PAR_PAGE ); var html = "<div style='border:1px solid #666;" + "background:#fff;padding:8px;color:#222;'>" + "<h3>Découpage Donjon</h3>" + "<p>Sélectionne le handout contenant les salles.</p>"; if (!selection.length) { html += "<p>Aucun handout trouvé.</p>"; } selection.forEach(function (handout) { var nom = echapperHTML( handout.get("name") || "Sans titre" ); html += "<div style='margin:4px 0;'>" + "<a style='display:inline-block;padding:4px 8px;" + "background:#e8e8e8;border:1px solid #999;" + "border-radius:3px;color:#222;text-decoration:none;' " + "href='" + NOM_COMMANDE_ID + " " + handout.id + "'>" + nom + "</a></div>"; }); html += "<hr>"; if (page > 0) { html += "<a href='" + NOM_COMMANDE_PAGE + " " + (page - 1) + "'>« Page précédente</a> "; } html += " Page " + (page + 1) + " / " + nombrePages + " "; if (page < nombrePages - 1) { html += "<a href='" + NOM_COMMANDE_PAGE + " " + (page + 1) + "'>Page suivante »</a>"; } html += "</div>"; envoyerAuJoueur(playerId, html); } /* * Crée un handout pour une salle. */ function creerHandoutSalle(nom, contenu) { var parties = separerContenu(contenu); var handout = createObj("handout", { name: nom, inplayerjournals: "", archived: false }); if (!handout) { return false; } handout.set("notes", parties.notes); handout.set("gmnotes", parties.gmnotes); return true; } /* * Récupère le contenu du handout source. * * On privilégie GM Notes, comme dans la version précédente. * Si GM Notes est vide, on utilise Notes. */ function decouperHandout(handout, playerId) { var nomSource = handout.get("name") || "Handout sans titre"; handout.get("gmnotes", function (contenuGM) { if (!contenuGM || !String(contenuGM).trim()) { handout.get("notes", function (contenuNotes) { traiterContenuSource( contenuNotes, nomSource, playerId ); }); } else { traiterContenuSource( contenuGM, nomSource, playerId ); } }); } /* * Découpe le contenu et crée les handouts. */ function traiterContenuSource(contenu, nomSource, playerId) { if (!contenu || !String(contenu).trim()) { envoyerAuJoueur( playerId, "Le handout source est vide. Aucun découpage effectué." ); return; } var resultat = detecterSalles(contenu); var texte = resultat.texte; var salles = resultat.salles; if (!salles.length) { envoyerAuJoueur( playerId, "Aucune salle détectée dans <b>" + echapperHTML(nomSource) + "</b>.<br>" + "Vérifie que les titres sont au format " + "<code>A1. Nom de la salle</code>." ); return; } var creees = 0; var erreurs = 0; salles.forEach(function (salle, index) { var debut = salle.index + salle.longueur; var fin = index + 1 < salles.length ? salles[index + 1].index : texte.length; var contenuSalle = texte.substring(debut, fin).trim(); var nomSalle = salle.code + ". " + salle.titre; try { if (creerHandoutSalle(nomSalle, contenuSalle)) { creees++; } else { erreurs++; } } catch (erreur) { erreurs++; log( "Découpage Donjon : erreur lors de la création de " + nomSalle + " : " + erreur ); } }); var message = "<b>Découpage terminé</b><br>" + "Handout source : <b>" + echapperHTML(nomSource) + "</b><br>" + "Salles détectées : " + salles.length + "<br>" + "Handouts créés : " + creees + "<br>" + "Erreurs : " + erreurs + "<br><br>" + "Le handout source n'a pas été modifié."; message += "<br><br><b>Classement :</b> crée ou utilise un dossier " + "<b>" + echapperHTML(nomSource) + "</b> dans le Journal, " + "puis déplace-y les handouts créés. Le script ne peut pas " + "les déplacer automatiquement dans un dossier."; envoyerAuJoueur(playerId, message); } /* * Gestion des commandes du chat. */ function traiterCommande(msg) { if (msg.type !== "api") { return; } var contenu = String(msg.content || "").trim(); // Afficher la liste des handouts. if (contenu === NOM_COMMANDE) { afficherListeHandouts(msg.playerid, 0); return; } // Changer de page. if (contenu.indexOf(NOM_COMMANDE_PAGE + " ") === 0) { var page = contenu.substring( NOM_COMMANDE_PAGE.length ).trim(); afficherListeHandouts(msg.playerid, page); return; } // Découper le handout sélectionné. if (contenu.indexOf(NOM_COMMANDE_ID + " ") === 0) { var id = contenu.substring( NOM_COMMANDE_ID.length ).trim(); var handout = getObj("handout", id); if (!handout) { envoyerAuJoueur( msg.playerid, "Handout introuvable. Relance la commande " + "<code>" + NOM_COMMANDE + "</code>." ); return; } decouperHandout(handout, msg.playerid); } } /* * Initialisation du script. */ on("ready", function () { on("chat:message", traiterCommande); log("Découpage Donjon chargé."); }); return {}; })();