Guest User

Untitled

a guest
Nov 2nd, 2025
33
0
4 days
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 13.24 KB | Gaming | 0 0
  1. // === Mass Actor and Token Renamer ===
  2. // Key used to save settings for the current user
  3. const SETTINGS_KEY = "macro-token-renamer-settings";
  4.  
  5. async function main() {
  6.   // === Pre-checks ===
  7.   const controlled = canvas.tokens?.controlled ?? [];
  8.   if (controlled.length === 0) {
  9.     return ui.notifications.warn("Select at least one token first.");
  10.   }
  11.   const rollTables = game.tables?.contents ?? [];
  12.  
  13.   // Load previously saved settings for the user
  14.   const savedSettings = game.user.getFlag('world', SETTINGS_KEY) ?? {};
  15.  
  16.   // Build table options using IDs (safe) and escaped labels
  17.   const tableOptions = rollTables.map(t =>
  18.     `<option value="${t.id}" ${savedSettings.tables?.includes(t.id) ? "selected" : ""}>${foundry.utils.escapeHTML(t.name)}</option>`
  19.   ).join("");
  20.  
  21.   // === UI HELPER FUNCTION ===
  22.   const updateUI = (html) => {
  23.     // Read all form inputs
  24.     const numTables = parseInt(html.find('#numtables').val(), 10) || 0;
  25.     const appendWords = html.find('#appendWords').val().trim();
  26.     const useAsTitle = html.find('#useAsTitle').is(':checked');
  27.     const appendStart = html.find('[name="appendStart"]').is(':checked');
  28.     const appendEnd = html.find('[name="appendEnd"]').is(':checked');
  29.     const changeDoc = html.find('[name="changeDoc"]').is(':checked');
  30.     const changeActor = html.find('[name="changeActor"]').is(':checked');
  31.  
  32.     // Show/hide roll table selectors
  33.     html.find('.table-selector').hide();
  34.     for (let i = 1; i <= numTables; i++) {
  35.       html.find(`#table-selector-${i}`).show();
  36.     }
  37.  
  38.     // Disable "Use as title" if tables are selected
  39.     const noTables = numTables === 0;
  40.     html.find('[name="useAsTitle"]').prop('disabled', !noTables);
  41.  
  42.     // Determine if other options should be disabled
  43.     const disableAppend = appendWords.length === 0 || (noTables && useAsTitle);
  44.     const disableKeeps = noTables && useAsTitle;
  45.  
  46.     html.find('[name="appendStart"], [name="appendEnd"]').prop('disabled', disableAppend);
  47.     html.find('[name="keepDocName"], [name="keepActorName"]').prop('disabled', disableKeeps);
  48.  
  49.     // --- LIVE PREVIEW LOGIC ---
  50.     const prefix = appendStart && !disableAppend ? `${appendWords} ` : "";
  51.     const suffix = appendEnd && !disableAppend ? ` ${appendWords}` : "";
  52.     const tablePart = noTables ? "" : Array.from({ length: numTables }, (_, i) => `[Table ${i + 1}]`).join(" ");
  53.  
  54.     const generatePreview = (original, keep) => {
  55.       if (noTables && useAsTitle) return appendWords || "[Additional Words]";
  56.       const core = [(keep && !disableKeeps ? `[${original}]` : ""), tablePart].filter(Boolean).join(" ");
  57.       return `${prefix}${core}${suffix}`.trim() || "[No Change]";
  58.     };
  59.  
  60.     let tokenPreview = generatePreview("Original Token", html.find('[name="keepDocName"]').is(':checked'));
  61.     let actorPreview = generatePreview("Original Actor", html.find('[name="keepActorName"]').is(':checked'));
  62.  
  63.     if (!changeDoc) tokenPreview = "[No Change]";
  64.     if (!changeActor) actorPreview = "[No Change]";
  65.    
  66.     html.find('#token-name-preview').text(tokenPreview);
  67.     html.find('#actor-name-preview').text(actorPreview);
  68.  
  69.     // --- VALIDATION LOGIC ---
  70.     const isInvalidState = appendWords.length > 0 && noTables && !useAsTitle && !appendStart && !appendEnd;
  71.     const applyButton = html.closest('.dialog').find('.dialog-button.apply');
  72.     const warningDiv = html.find('#preview-warning');
  73.  
  74.     if (isInvalidState) {
  75.       applyButton.prop('disabled', true);
  76.       warningDiv.text("Choose Append at Start/End, or check β€œUse as full name”.").show();
  77.     } else {
  78.       applyButton.prop('disabled', false);
  79.       warningDiv.hide().text("");
  80.     }
  81.   };
  82.  
  83.   // === RENDER DIALOG ===
  84.   new Dialog({
  85.     title: "Token Rename Helper",
  86.     content: `
  87.       <form>
  88.         <div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
  89.           <!-- Left Column -->
  90.           <div style="flex: 2; min-width: 250px;">
  91.             <fieldset>
  92.               <legend>Name Source</legend>
  93.               <div class="form-group">
  94.                 <label for="numtables">Number of Roll Tables:</label>
  95.                 <input id="numtables" name="numtables" type="number" min="0" max="${rollTables.length}" value="${savedSettings.numTables ?? 0}">
  96.               </div>
  97.               ${Array.from({ length: rollTables.length }, (_, i) => `
  98.                 <div class="form-group table-selector" id="table-selector-${i + 1}" style="display:none;">
  99.                   <label for="table${i + 1}">Roll Table ${i + 1}</label>
  100.                   <select id="table${i + 1}" name="table${i + 1}">${tableOptions}</select>
  101.                 </div>
  102.               `).join("")}
  103.               <hr/>
  104.               <div class="form-group">
  105.                 <label for="appendWords">Additional Words:</label>
  106.                 <!-- MODIFIED: Always starts empty -->
  107.                 <input id="appendWords" name="appendWords" type="text" placeholder="e.g., Elite, of the North" value="">
  108.               </div>
  109.               <div class="form-group">
  110.                 <label for="useAsTitle">Use β€œAdditional Words” as full name (if no tables)</label>
  111.                 <!-- MODIFIED: Always starts unchecked -->
  112.                 <input id="useAsTitle" name="useAsTitle" type="checkbox">
  113.               </div>
  114.             </fieldset>
  115.           </div>
  116.  
  117.           <!-- Right Column -->
  118.           <div style="flex: 1; min-width: 200px;">
  119.             <fieldset>
  120.               <legend>Fine-Tuning</legend>
  121.               <div class="form-group">
  122.                 <label for="keepDocName">Keep Original Token Name</label>
  123.                 <input id="keepDocName" name="keepDocName" type="checkbox" ${savedSettings.keepDocName ? "checked" : ""}>
  124.               </div>
  125.               <div class="form-group">
  126.                 <label for="keepActorName">Keep Original Actor Name</label>
  127.                 <input id="keepActorName" name="keepActorName" type="checkbox" ${savedSettings.keepActorName ?? true ? "checked" : ""}>
  128.               </div>
  129.               <div class="form-group">
  130.                 <label for="appendStart">Append Words at Start</label>
  131.                 <!-- MODIFIED: Always starts unchecked -->
  132.                 <input id="appendStart" name="appendStart" type="checkbox">
  133.               </div>
  134.               <div class="form-group">
  135.                 <label for="appendEnd">Append Words at End</label>
  136.                 <!-- MODIFIED: Always starts unchecked -->
  137.                 <input id="appendEnd" name="appendEnd" type="checkbox">
  138.               </div>
  139.             </fieldset>
  140.             <fieldset>
  141.               <legend>Target</legend>
  142.               <div class="form-group">
  143.                 <label for="changeDoc">Change Token Name</label>
  144.                 <input id="changeDoc" name="changeDoc" type="checkbox" ${savedSettings.changeDoc ?? true ? "checked" : ""}>
  145.               </div>
  146.               <div class="form-group">
  147.                 <label for="changeActor">Change Actor Name</label>
  148.                 <input id="changeActor" name="changeActor" type="checkbox" ${savedSettings.changeActor ?? true ? "checked" : ""}>
  149.               </div>
  150.               <div class="form-group">
  151.                 <label for="skipLinkedActors">Skip linked actors</label>
  152.                 <input id="skipLinkedActors" name="skipLinkedActors" type="checkbox" ${savedSettings.skipLinkedActors ?? true ? "checked" : ""}>
  153.               </div>
  154.             </fieldset>
  155.           </div>
  156.         </div>
  157.         <hr/>
  158.         <fieldset>
  159.           <legend>Live Preview</legend>
  160.           <div id="preview-warning" style="color: #c94c4c; text-align: center; display: none; margin-bottom: 5px; font-weight: bold;"></div>
  161.           <div style="display: flex; flex-direction: column; gap: 5px; text-align: left;">
  162.             <div>
  163.               <strong>Token:</strong>
  164.               <code id="token-name-preview" style="display: block; width: 100%; background: rgba(0,0,0,0.75); padding: 6px 8px; border-radius: 3px; font-size: 1.1em; box-sizing: border-box;"></code>
  165.             </div>
  166.             <div>
  167.               <strong>Actor:</strong>
  168.               <code id="actor-name-preview" style="display: block; width: 100%; background: rgba(0,0,0,0.75); padding: 6px 8px; border-radius: 3px; font-size: 1.1em; box-sizing: border-box;"></code>
  169.             </div>
  170.           </div>
  171.         </fieldset>
  172.       </form>
  173.     `,
  174.     buttons: {
  175.       apply: {
  176.         icon: "<i class='fas fa-check'></i>",
  177.         label: "Apply Changes",
  178.         classes: "apply",
  179.         callback: async (html) => {
  180.           const settings = {
  181.             numTables: parseInt(html.find('#numtables').val(), 10) || 0,
  182.             appendWords: html.find('#appendWords').val().trim(),
  183.             useAsTitle: html.find('#useAsTitle').is(':checked'),
  184.             keepDocName: html.find('#keepDocName').is(':checked'),
  185.             keepActorName: html.find('#keepActorName').is(':checked'),
  186.             appendStart: html.find('#appendStart').is(':checked'),
  187.             appendEnd: html.find('#appendEnd').is(':checked'),
  188.             changeDoc: html.find('#changeDoc').is(':checked'),
  189.             changeActor: html.find('#changeActor').is(':checked'),
  190.             skipLinkedActors: html.find('#skipLinkedActors').is(':checked'),
  191.             tables: Array.from({ length: parseInt(html.find('#numtables').val(), 10) || 0 }, (_, i) => html.find(`#table${i + 1}`).val())
  192.           };
  193.          
  194.           // Create a separate object for saving, excluding the temporary fields
  195.           const settingsToSave = { ...settings };
  196.           delete settingsToSave.appendWords;
  197.           delete settingsToSave.useAsTitle;
  198.           delete settingsToSave.appendStart;
  199.           delete settingsToSave.appendEnd;
  200.          
  201.           await game.user.setFlag('world', SETTINGS_KEY, settingsToSave);
  202.           await executeRename(settings, controlled);
  203.         }
  204.       },
  205.       cancel: {
  206.         icon: "<i class='fas fa-times'></i>",
  207.         label: "Cancel"
  208.       },
  209.     },
  210.     default: "apply",
  211.     render: (html) => {
  212.       html.find('input, select').on('change input', () => updateUI(html));
  213.       updateUI(html);
  214.     },
  215.     close: () => {}
  216.   }).render(true, { width: 600 });
  217. }
  218.  
  219. // === Renaming Logic ===
  220. async function executeRename(settings, controlled) {
  221.   const { numTables, appendWords, useAsTitle, keepDocName, keepActorName, appendStart, appendEnd, changeDoc, changeActor, skipLinkedActors } = settings;
  222.  
  223.   if (numTables === 0 && appendWords.length === 0) {
  224.     return ui.notifications.warn("No roll tables or Additional Words provided.");
  225.   }
  226.  
  227.   const linkedCount = controlled.filter(t => t.document?.actorLink).length;
  228.   if (changeActor && linkedCount > 0) {
  229.     if (skipLinkedActors) ui.notifications.info(`Skipping actor renames for ${linkedCount} linked token${linkedCount !== 1 ? "s" : ""}.`);
  230.     else ui.notifications.warn("Note: Renaming a linked Actor affects all its tokens.");
  231.   }
  232.  
  233.   const effectiveAppendStart = appendStart ? appendWords : null;
  234.   const effectiveAppendEnd = appendEnd ? appendWords : null;
  235.  
  236.   if (numTables === 0) {
  237.     const baseName = useAsTitle ? appendWords : "";
  238.     for (const token of controlled) {
  239.       const isLinked = skipLinkedActors && token.document?.actorLink;
  240.       await renameTokenParts(token, baseName, {
  241.         doTokenDoc: changeDoc,
  242.         doActor: changeActor && !isLinked,
  243.         keepTokenDoc: useAsTitle ? false : keepDocName,
  244.         keepActor: useAsTitle ? false : keepActorName,
  245.         appendStart: useAsTitle ? null : effectiveAppendStart,
  246.         appendEnd: useAsTitle ? null : effectiveAppendEnd,
  247.       });
  248.     }
  249.   } else {
  250.     const tables = settings.tables.map(id => game.tables.get(id));
  251.     if (tables.some(t => !t)) return ui.notifications.error("One or more selected roll tables could not be found.");
  252.  
  253.     for (const token of controlled) {
  254.       const draws = await Promise.all(tables.map(tbl => tbl.draw({ displayChat: false })));
  255.       const newName = draws.map(d => d.results?.[0]?.text ?? "").filter(Boolean).join(" ").trim();
  256.       if (!newName) continue;
  257.  
  258.       const isLinked = skipLinkedActors && token.document?.actorLink;
  259.       await renameTokenParts(token, newName, {
  260.         doTokenDoc: changeDoc,
  261.         doActor: changeActor && !isLinked,
  262.         keepTokenDoc: keepDocName,
  263.         keepActor: keepActorName,
  264.         appendStart: effectiveAppendStart,
  265.         appendEnd: effectiveAppendEnd,
  266.       });
  267.     }
  268.   }
  269.   ui.notifications.info("Renaming complete.");
  270. }
  271.  
  272. async function renameTokenParts(token, newName, { doTokenDoc, doActor, keepTokenDoc, keepActor, appendStart, appendEnd }) {
  273.   const updates = [];
  274.  
  275.   if (doTokenDoc) {
  276.     let tokenDocName = keepTokenDoc ? `${token.document.name} ${newName}`.trim() : newName;
  277.     if (appendStart) tokenDocName = `${appendStart} ${tokenDocName}`;
  278.     if (appendEnd) tokenDocName = `${tokenDocName} ${appendEnd}`;
  279.     updates.push(token.document.update({ name: tokenDocName.trim() }));
  280.   }
  281.  
  282.   if (doActor && token.actor) {
  283.     let actorName = keepActor ? `${token.actor.name} ${newName}`.trim() : newName;
  284.     if (appendStart) actorName = `${appendStart} ${actorName}`;
  285.     if (appendEnd) actorName = `${actorName} ${appendEnd}`;
  286.     updates.push(token.actor.update({ name: actorName.trim() }));
  287.   }
  288.  
  289.   if (updates.length > 0) await Promise.all(updates);
  290. }
  291.  
  292. main();
Advertisement
Add Comment
Please, Sign In to add comment