diff --git a/Fix Turn Order/1.0.0/fixTurnOrder.js b/Fix Turn Order/1.0.0/fixTurnOrder.js new file mode 100644 index 000000000..b55dc4eff --- /dev/null +++ b/Fix Turn Order/1.0.0/fixTurnOrder.js @@ -0,0 +1,276 @@ +// Script: Fix Turn Order +// By: Keith Curtis +// Contact: https://app.roll20.net/users/162065/keithcurtis +var API_Meta = API_Meta||{}; //eslint-disable-line no-var +API_Meta.fixTurnOrder={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; +{try{throw new Error('');}catch(e){API_Meta.fixTurnOrder.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}} + + + +on('ready', () => { + + const scriptName = 'Fix Turn Order'; + const version = '1.0.0'; //version number set here + log('-=> Fix Turnorder v' + version + ' is loaded. Use !fixturnorder to scan for orphaned turns.'); + //1.0.0 Debut + + + +on('chat:message', (msg) => { + if (msg.type !== 'api') return; + if (!playerIsGM(msg.playerid)) return; + + + + + + /* ---------- helpers ---------- */ + + const normalizeForChat = (html) => + html.trim().replace(/\r?\n/g, ''); + + const Pictos = (char) => + `${char}`; + +const getCSS = () => ({ + box: "background:#bababa;border:2px solid #666;border-radius:8px;padding:8px;font-size:14px;color:#222;", + playerBanner: "background:#d6d6d6;border:2px solid #555;border-radius:8px;padding:6px 8px;margin-bottom:6px;line-height:24px;white-space:nowrap;", + playerBannerImage: "height:24px;width:auto;vertical-align:middle;margin-right:6px;", + playerBannerText: "font-size:16px;font-weight:bold;vertical-align:middle;", + header: "font-weight:bold;margin-bottom:6px;", + groupBox: "background:#555;border:1px solid #666;border-radius:8px;padding:6px 8px;margin:8px 0;color:#eee;", + groupHeader: "font-weight:bold;margin:4px 0;color:#eee;", + pageRow: "background:#d0d0d0;border:1px solid #777;border-radius:6px;padding:4px 6px;margin:4px 0;", + tokenRow: "background:#e6e6e6;border:1px solid #999;border-radius:6px;padding:4px 6px;margin:3px 0;", + rowItem: "color:#111;display:inline-block;vertical-align:middle;white-space:nowrap;font-weight:bold", + trashButton: "font-weight:bold;display:inline-block;margin-right:6px;padding:2px 6px;background:#a44;color:#eee;text-decoration:none;border-radius:4px;font-size:14px;", + tokenImage: "display:inline-block;max-height:35px;max-width:35px;border-radius:4px;margin-right:6px;vertical-align:middle;", + tokenName: "font-weight:bold;color:#111;display:inline-block;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;", + footer: "margin-top:10px;text-align:right;", + footerLeft: "float:left;", + confirmButton: "font-weight:bold;padding:3px 8px;background:#156616;color:#eee;text-decoration:none;border-radius:4px;font-size:11px;", + messageContainer: "background:#dcdcdc;border:3px solid #666;border-radius:8px;padding:8px;font-size:14px;color:#222;", + messageTitle: "font-size:16px;font-weight:bold;margin-bottom:4px;", + messageButton: "padding:2px 6px;background:#777;color:#eee;text-decoration:none;border-radius:4px;font-size:14px;" +}); + + + const PLAYER_FLAG_SRC = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjkiIGhlaWdodD0iMzUiIHZpZXdCb3g9IjAgMCAyOSAzNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI4IDM0TDE1IDI0LjRMMiAzNFYzLjZDMiAyLjcyIDIuOTc1IDIgNC4xNjY2NyAySDI1LjgzMzNDMjcuMDI1IDIgMjggMi43MiAyOCAzLjZWMzRaIiBzdHJva2U9IiMwQzBDMEMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTI3IDMzTDE0IDIzLjRMMSAzM1YyLjZDMSAxLjcyIDEuOTc1IDEgMy4xNjY2NyAxSDI0LjgzMzNDMjYuMDI1IDEgMjcgMS43MiAyNyAyLjZWMzNaIiBmaWxsPSIjRkZCQzMzIiBzdHJva2U9IiM5OTY3MDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik01LjUgM0M2LjMyODQzIDMgNyAzLjQ0NzcyIDcgNEw3IDIzQzcgMjMuNTUyMyA2LjMyODQzIDI0IDUuNSAyNEM0LjY3MTU3IDI0IDQgMjMuNTUyMyA0IDIzTDQgNEM0IDMuNDQ3NzIgNC42NzE1NyAzIDUuNSAzWiIgZmlsbD0iI0ZGREQ5OSIvPgo8L3N2Zz4K`; + + const sendHTML = (html) => { + sendChat(scriptName, normalizeForChat(html), null, { noarchive: true }); + }; + + const sendStyledMessage = (titleOrMessage, messageOrUndefined, isPublic = false) => { + const css = getCSS(); + let title, message; + + if (messageOrUndefined === undefined) { + title = scriptName; + message = titleOrMessage; + } else { + title = titleOrMessage || scriptName; + message = messageOrUndefined; + } + + message = String(message).replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + (_, label, command) => + `${label}` + ); + + const html = + `
+PinTool provides bulk creation, inspection, and modification of map pins. +It also provides commands for conversion of old-style note tokens to new +map pins. +
+ +Base Command: !pintool
--set — Modify properties on one or more pins (selected pins, or all pins on a page).--convert — Convert map tokens into a handout. Can optionally replace existing token pins upon creation.--place — Places pins on the map based on a specified handout and header level.--purge — Removes all tokens on the map similar to the selected token, or pins similar to the selected pin.--help — Open this help handout.Format:
++!pintool --set property|value [property|value ...] [filter|target] ++ +
All supplied properties apply to every pin matched by the filter.
+ +filter|selected — (default) Selected pinsfilter|all — All pins on the current pagefilter|ID ID ID — Space-separated list of pin IDs
+Values are case-sensitive unless otherwise noted.
+Values indicated by "" mean no value.
+Do not type quotation marks.
+See examples at the end of this document.
+
x — Horizontal position on page, in pixelsy — Vertical position on page, in pixelstitle — Title text displayed on the pinnotes — Notes content associated with the pintooltipImage — Roll20 image identifier (URL)link — ID of the linked handout or objectlinkType — handout or ""subLink — Header identifier within the handoutsubLinkType — headerPlayer, headerGM, or ""visibleTo — Overall visibility: all or ""tooltipVisibleTo — Tooltip visibilitynameplateVisibleTo — Nameplate visibilityimageVisibleTo — Image visibilitynotesVisibleTo — Notes visibilitygmNotesVisibleTo — GM Notes visibilityautoNotesType — Controls blockquote-based player visibility:
+ blockquote or ""
+ scale — Range: 0.25 – 2.0imageDesynced — true / falsenotesDesynced — true / falsegmNotesDesynced — true / false+The convert command builds or updates a handout by extracting data +from map tokens. +
+ +Format:
++!pintool --convert key|value key|value ... ++ +
+A single token must be selected. +All tokens on the same page that represent the +same character are processed. +All note pins must represent a common character. +
+ +name|h1–h5title|stringgmnotes|formattooltip|formatbar1_value|formatbar1_max|formatbar2_value|formatbar2_max|formatbar3_value|formatbar3_max|formatFormat may be:
+h1–h6blockquotecodenormalsupernotesGMText|true-----) in a blockquote.
+ If no separator exists, the entire section is wrapped.
+ imagelinks|true[Image] links after images that send them to chat.
+ replace|truepurge [pins/tokens] command.
+ title| values may contain spaces.+The place command creates or replaces map pins on the current page +based on headers found in an existing handout. +
+ +Format:
++!pintool --place name|h1–h4 handout|Exact Handout Name ++ +
name|h1–h4handout|stringsubLinkType|headerPlayer.subLinkType|headerGM.--convert replace|true.+The purge command removes all tokens on the map similar to the selected token (i.e. that represent the same character), or pins similar to the selected pin (i.e. that are linked to the same handout). +
+ +Format:
++!pintool --purge tokens ++ +
tokens or pins!pintool --set scale|1!pintool --set scale|1 filter|all!pintool --set scale|1 filter|-123456789abcd -123456789abce -123456789abcf !pintool --set title|Camp notesVisibleTo|all!pintool --set autoNotesType|!pintool --convert name|h2 title|Goblin Notes gmnotes|blockquote_type and _pageid) cannot be modified.This will permanently delete ${targets.length} pin(s)
+ linked to handout ${_.escape(handoutName)}.
This cannot be undone.
+ ` + ); + return; + } + } + + + + function normalizeForChat(html) { + return String(html).replace(/\r\n|\r|\n/g, "").trim(); + } + + const sendStyledMessage = (titleOrMessage, messageOrUndefined, isPublic = false) => { + const css = getCSS(); + let title, message; + + if (messageOrUndefined === undefined) { + title = scriptName; + message = titleOrMessage; + } + else { + title = titleOrMessage || scriptName; + message = messageOrUndefined; + } + + message = String(message).replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + (_, label, command) => + `${label}` + ); + + const html = + `/gi); + if (!blocks) return `
${html}`; + + const idx = blocks.findIndex( + b => normalizeVisibleText(b) === "-----" + ); + + // NEW: no separator → everything is player-visible + if (idx === -1) { + return `
${blocks.join("")}`; + } + + // Separator exists → split as before + const player = blocks.slice(0, idx).join(""); + const gm = blocks.slice(idx + 1).join(""); + + return `
${player}\n${gm}`; + } + + + function downgradeHeaders(html) { + return html + .replace(/<\s*h[1-2]\b[^>]*>/gi, "
${content}`; + if (format === "code") return `
${_.escape(content)}`;
+ return content;
+ }
+
+ // ---------------- Build output ----------------
+ const output = [];
+ const tokenByName = {}; // NEW: exact name → token
+ const pinsToCreateCache = new Set();
+
+ let workTokensOnPage = tokensOnPage
+ .sort((a, b) => (a.get("name") || "").localeCompare(b.get("name") || "", undefined,
+ {
+ sensitivity: "base"
+ }));
+
+
+ const finishUp = () => {
+ // ---------------- Handout creation ----------------
+ let h = findObjs(
+ {
+ _type: "handout",
+ name: flags.title
+ })[0];
+ if (!h) h = createObj("handout",
+ {
+ name: flags.title
+ });
+
+ h.set("notes", output.join("\n"));
+ const handoutId = h.id;
+
+ sendChat("PinTool", `/w gm Handout "${flags.title}" updated.`);
+
+ if (!replace) return;
+
+ const skipped = [];
+ // const headerRegex = new RegExp(`Handout: ${_.escape(handoutName)}
+
@@ -197,15 +197,18 @@ All note pins must represent a common character.
imagelinks|true
Adds clickable [Image] links after images that send them to chat.
+
replace|truepurge [pins/tokens] command.
+ --.title| values may contain spaces.This will permanently delete ${targets.length} pin(s)
+ sendStyledMessage(
+ "Confirm Purge",
+ `
This will permanently delete ${targets.length} pin(s)
linked to handout ${_.escape(handoutName)}.
This cannot be undone.
@@ -558,243 +666,239 @@ The purge command removes all tokens on the map similar to the Click here to confirm
` - ); - return; - } + ); + return; } + } - function normalizeForChat(html) - { - return String(html).replace(/\r\n|\r|\n/g, "").trim(); - } + function normalizeForChat(html) { + return String(html).replace(/\r\n|\r|\n/g, "").trim(); + } - const sendStyledMessage = (titleOrMessage, messageOrUndefined, isPublic = false) => - { - const css = getCSS(); - let title, message; + const sendStyledMessage = (titleOrMessage, messageOrUndefined, isPublic = false) => { + const css = getCSS(); + let title, message; - if(messageOrUndefined === undefined) - { - title = scriptName; - message = titleOrMessage; - } - else - { - title = titleOrMessage || scriptName; - message = messageOrUndefined; - } + if (messageOrUndefined === undefined) { + title = scriptName; + message = titleOrMessage; + } + else { + title = titleOrMessage || scriptName; + message = messageOrUndefined; + } - message = String(message).replace( - /\[([^\]]+)\]\(([^)]+)\)/g, - (_, label, command) => - `${label}` - ); + message = String(message).replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + (_, label, command) => + `${label}` + ); - const html = - `/gi); - if(!blocks) return `
${html}`; + if (!blocks) return `
${html}`; const idx = blocks.findIndex( b => normalizeVisibleText(b) === "-----" ); // NEW: no separator → everything is player-visible - if(idx === -1) - { + if (idx === -1) { return `
${blocks.join("")}`; } @@ -937,58 +1029,50 @@ The purge command removes all tokens on the map similar to the } - function downgradeHeaders(html) - { + function downgradeHeaders(html) { return html .replace(/<\s*h[1-2]\b[^>]*>/gi, "
${content}`; - if(format === "code") return `
${_.escape(content)}`;
+ if (format === "blockquote") return `${content}`; + if (format === "code") return `
${_.escape(content)}`;
return content;
}
@@ -1011,7 +1095,7 @@ The purge command removes all tokens on the map similar to the
_type: "handout",
name: flags.title
})[0];
- if(!h) h = createObj("handout",
+ if (!h) h = createObj("handout",
{
name: flags.title
});
@@ -1021,21 +1105,20 @@ The purge command removes all tokens on the map similar to the
sendChat("PinTool", `/w gm Handout "${flags.title}" updated.`);
- if(!replace) return;
+ if (!replace) return;
const skipped = [];
-// const headerRegex = new RegExp(`+The Token Home script allows tokens to store and recall multiple +named locations on the current page. +Each location records an X/Y position and the token’s layer. +
+ ++Tokens can be sent back to saved locations, queried, or summoned to a selected +anchor point based on proximity. +
+ +Base Command: !home
--set — Store the selected token’s current position as a location.--lN — Recall the selected token to a stored location.--summon — Pull tokens to a selected anchor based on proximity.--clear — Remove stored location data from selected tokens.--help — Open this help handout.
+Locations are identified by numbered slots:
+L1, L2, L3, and higher.
+There is no fixed upper limit.
+
+Each stored location records: +
+ +Format:
++!home --set --lN ++ +
+Stores the selected token’s current position and layer into location L N.
+
!home --set --l1 — Set default location!home --set --l2 — Set residence!home --set --l5 — Set custom locationFormat:
++!home --lN ++ +
+Moves the selected token to the stored location L N.
+
!home --l1!home --l3+The summon command pulls tokens toward a selected anchor object +based on proximity to their stored locations. +
+ +Format:
++!home --summon [--lN] [--r pixels or grid squares] ++
+if no value is given, then pixels are assumed. Use 'g' for grid squares.
--r300= 300 pixels,
--r5g= 5 grid squares. + + +
+Exactly one object must be selected: +
+ +graphic)text)pin)+The selected object’s X/Y position is used as the summon target. +
+ +--lN--r pixels70.
+ --lN is supplied, only that location is tested!home --summon!home --summon --r 210!home --summon --l2!home --summon --l4 --r 140Format:
++!home --clear [--lN] ++ +
--lN is supplied, only that location is removed+The Token Home script allows tokens to store and recall multiple +named locations on the current page. +Each location records an X/Y position and the token’s layer. +
+ ++Tokens can be sent back to saved locations, queried, or summoned to a selected +anchor point based on proximity. +
+ +Base Command: !home
--set — Store the selected token’s current position as a location.--lN — Recall the selected token to a stored location.--summon — Pull tokens to a selected anchor based on proximity.--clear — Remove stored location data from selected tokens.--help — Open this help handout.
+Locations are identified by numbered slots:
+L1, L2, L3, and higher.
+There is no fixed upper limit.
+
+Each stored location records: +
+ +Format:
++!home --set --lN ++ +
+Stores the selected token’s current position and layer into location L N.
+
!home --set --l1 — Set default location!home --set --l2 — Set residence!home --set --l5 — Set custom locationFormat:
++!home --lN ++ +
+Moves the selected token to the stored location L N.
+
!home --l1!home --l3+The summon command pulls tokens toward a selected anchor object +based on proximity to their stored locations. +
+ +Format:
++!home --summon [--lN] [--r pixels or grid squares] ++
+if no value is given, then pixels are assumed. Use 'g' for grid squares.
--r300= 300 pixels,
--r5g= 5 grid squares. + + +
+Exactly one object must be selected: +
+ +graphic)text)pin)+The selected object’s X/Y position is used as the summon target. +
+ +--lN--r pixels70.
+ --lN is supplied, only that location is tested!home --summon!home --summon --r 210!home --summon --l2!home --summon --l4 --r 140Format:
++!home --clear [--lN] ++ +
--lN is supplied, only that location is removed