diff --git a/ScriptCards/3.0.01/scriptcards.js b/ScriptCards/3.0.01/scriptcards.js
new file mode 100644
index 000000000..00c867b2e
--- /dev/null
+++ b/ScriptCards/3.0.01/scriptcards.js
@@ -0,0 +1,7178 @@
+/* eslint-disable no-undef */
+/* eslint-disable no-useless-escape */
+/* eslint-disable no-redeclare */
+// Github: https://gist.github.com/kjaegers/515dff0f04c006d7192e0fec534d96bf
+// By: Kurt Jaegers
+// Contact: https://app.roll20.net/users/2365448/kurt-j
+if (typeof MarkStart === "function") MarkStart('ScriptCards');
+var API_Meta = API_Meta || {};
+API_Meta.ScriptCards = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 };
+{ try { throw new Error(''); } catch (e) { API_Meta.ScriptCards.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - 10); } }
+
+var scriptCardsStashedScripts = {};
+
+const ScriptCards = (async () => { // eslint-disable-line no-unused-vars
+ /*
+
+ ScriptCards implements a run-time scripting language interpreter for the Roll20 system. It contains no system-specific code, and can process scripts
+ entered into the chat window, either directly, through cut/paste, or executed from macros, character abilities, etc.
+
+ A ScriptCard script consists of one or more lines, each delimited by a double dash (--) starting the line, followed by a statement type identifier.
+
+ After the identifier, is a line tag, followed by a vertical bar (|) character, followed by the line content. The scripting language supports
+ end-inclusion of function libraries with the +++libname+++ directive, which will be pre-parsed and removed from the script. Any number of libraries
+ can be specified by separating library names (case sensitive) with semicolons (;).
+
+ Please see the ScriptCards Wiki Entry on Roll20 at https://wiki.roll20.net/Script:ScriptCards for details.
+ */
+
+ const APINAME = "ScriptCards";
+ const APIVERSION = "3.0.01";
+ const NUMERIC_VERSION = "300010"
+ const APIAUTHOR = "Kurt Jaegers";
+ const debugMode = false;
+
+ const parameterAliases = {
+ "tablebackgroundcolor": "tablebgcolor",
+ "titlecardbackgroundcolor": "titlecardbackground",
+ "nominmaxhilight": "nominmaxhighlight",
+ "norollhilight": "norollhilight",
+ "buttonbackgroundcolor": "buttonbackground",
+ "concatentioncharacter": "concatenationcharacter",
+ "reentry": "reentrant"
+ }
+
+ var lastExecutedByID;
+ var lastExecutedDisplayName;
+
+ // These are the parameters that all cards will start with. This table is copied to the
+ // cardParameters table inside the processing loop and that table is updated with settings
+ // from --# lines in the script.
+ const defaultParameters = {
+ reentrant: "0",
+ tableborder: "2px solid #000000;",
+ tablebgcolor: "#EEEEEE",
+ tableborderradius: "6px;",
+ tableshadow: "5px 3px 3px 0px #aaa;",
+ title: "ScriptCards",
+ titlecardbackground: "#1C6EA4",
+ titlecardgradient: "0",
+ titlecardbackgroundimage: "",
+ titlecardbottomborder: "2px solid #444444;",
+ titlefontface: "Contrail One",
+ titlefontsize: "1.2em",
+ titlefontlineheight: "1.2em",
+ titlefontweight: "strong",
+ titlefontstyle: "normal",
+ titlefontshadow: "-1px 1px 0 #000, 1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000;",
+ lineheight: "normal",
+ rollhilightlineheight: "1.0em",
+ rollhilightcolornormal: "#FFFEA2",
+ rollhilightcolorcrit: "#88CC88",
+ rollhilightcolorfumble: "#FFAAAA",
+ rollhilightcolorboth: "#8FA4D4",
+ titlefontcolor: "#FFFFFF",
+ subtitlefontsize: "13px",
+ subtitlefontface: "Tahoma",
+ subtitlefontcolor: "#FFFFFF",
+ subtitleseparator: " &" + "#x2666; ",
+ tooltip: "Sent by ScriptCards",
+ bodyfontsize: "14px;",
+ bodyfontface: "Helvetica",
+ oddrowbackground: "#D0E4F5",
+ evenrowbackground: "#eeeeee",
+ oddrowfontcolor: "#000000",
+ evenrowfontcolor: "#000000",
+ bodybackgroundimage: "",
+ oddrowbackgroundimage: "",
+ evenrowbackgroundimage: "",
+ whisper: "",
+ emotetext: "",
+ sourcetoken: "",
+ targettoken: "",
+ activepage: "",
+ emotebackground: "#f5f5ba",
+ emotefont: "Georgia",
+ emotefontweight: "bold",
+ emotefontsize: "14px",
+ emotestate: "visible",
+ emotefontcolor: "",
+ emotesourcetokensize: "50",
+ emotetargettokensize: "50",
+ emotesourcetokenoverride: "0",
+ emotetargettokenoverride: "0",
+ rollfontface: "helvetica",
+ leftsub: "",
+ rightsub: "",
+ sourcecharacter: "",
+ targetcharacter: "",
+ activepageobject: undefined,
+ debug: "0",
+ hidecard: "0",
+ hidetitlecard: "0",
+ dontcheckbuttonsforapi: "0",
+ roundup: "0",
+ buttonbackground: "#1C6EA4",
+ buttonbackgroundimage: "",
+ buttontextcolor: "White",
+ buttonbordercolor: "#999999",
+ buttonfontsize: "x-small",
+ buttonfontface: "Tahoma",
+ buttonpadding: "5px",
+ parameterdelimiter: ";",
+ concatenationcharacter: "+",
+ formatoutputforobjectmodification: "0",
+ dicefontcolor: "#1C6EA4",
+ dicefontsize: "3.0em",
+ usehollowdice: "0",
+ allowplaintextinrolls: "0",
+ showfromfornonwhispers: "0",
+ allowinlinerollsinoutput: "0",
+ nominmaxhighlight: "0",
+ norollhighlight: "0",
+ disablestringexpansion: "0",
+ disablerollvariableexpansion: "0",
+ disableparameterexpansion: "0",
+ disablerollprocessing: "0",
+ disableattributereplacement: "0",
+ attemptattributeparsing: "0",
+ disableinlineformatting: "0",
+ executionlimit: "40000",
+ inlineconditionseparator: "|",
+ deferralcharacter: "^",
+ locale: "en-US", //apparently not supported by Roll20's Javascript implementation...
+ timezone: "America/New_York",
+ hpbar: "3",
+ outputtagprefix: "",
+ outputcontentprefix: " ",
+ enableattributesubstitution: "0",
+ formatinforequesttext: "0",
+ overridetemplate: "none",
+ explodingonesandaces: "0",
+ functionbenchmarking: "0",
+ limitmaxbarvalues: "0",
+ gmoutputtarget: "gm",
+ storagecharid: "",
+ beaconsheet: "0",
+ styleTableTag: " border-collapse:separate; border: solid black 2px; border-radius: 6px; -moz-border-radius: 6px; ",
+ stylenone: " text-align: center; font-size: 100%; display: inline-block; font-weight: bold; height: !{rollhilightlineheight}; min-width: 1.75em; margin-top: -1px; margin-bottom: 1px; padding: 0px 2px; ",
+ stylenormal: " text-align: center; font-size: 100%; display: inline-block; font-weight: bold; height: !{rollhilightlineheight}; min-width: 1.75em; margin-top: -1px; margin-bottom: 1px; padding: 0px 2px; border: 1px solid; border-radius: 3px; background-color: !{rollhilightcolornormal}; border-color: #87850A; color: #000000;",
+ stylefumble: " text-align: center; font-size: 100%; display: inline-block; font-weight: bold; height: !{rollhilightlineheight}; min-width: 1.75em; margin-top: -1px; margin-bottom: 1px; padding: 0px 2px; border: 1px solid; border-radius: 3px; background-color: !{rollhilightcolorfumble}; border-color: #660000; color: #660000;",
+ stylecrit: " text-align: center; font-size: 100%; display: inline-block; font-weight: bold; height: !{rollhilightlineheight}; min-width: 1.75em; margin-top: -1px; margin-bottom: 1px; padding: 0px 2px; border: 1px solid; border-radius: 3px; background-color: !{rollhilightcolorcrit}; border-color: #004400; color: #004400;",
+ styleboth: " text-align: center; font-size: 100%; display: inline-block; font-weight: bold; height: !{rollhilightlineheight}; min-width: 1.75em; margin-top: -1px; margin-bottom: 1px; padding: 0px 2px; border: 1px solid; border-radius: 3px; background-color: !{rollhilightcolorboth}; border-color: #061539; color: #061539;",
+
+ // These settings can be used freely and are stored with the format storage commands
+ usersetting0: "",
+ usersetting1: "",
+ usersetting2: "",
+ usersetting3: "",
+ usersetting4: "",
+ usersetting5: "",
+ usersetting6: "",
+ usersetting7: "",
+ usersetting8: "",
+ usersetting9: "",
+
+ critd20: "20",
+ critd100: "100",
+ critd10: "10",
+ critd8: "8",
+ critd6: "6",
+ critd4: "4",
+ fumbled20: "1",
+ fumbled100: "1",
+ fumbled10: "1",
+ fumbled8: "1",
+ fumbled6: "1",
+ fumbled4: "1",
+ };
+
+ const SettingsThatAreColors = [
+ "tablebgcolor",
+ "titlecardbackground",
+ "rollhilightcolornormal",
+ "rollhilightcolorcrit",
+ "rollhilightcolorfumble",
+ "rollhilightcolorboth",
+ "titlefontcolor",
+ "subtitlefontcolor",
+ "oddrowbackground",
+ "evenrowbackground",
+ "oddrowfontcolor",
+ "evenrowfontcolor",
+ "emotebackground",
+ "buttonbackground",
+ "buttonbordercolor",
+ "dicefontcolor"
+ ];
+
+ const SettingsThatAreBooleans = [
+ "debug",
+ "hidecard",
+ "hidetitlecard",
+ "dontcheckbuttonsforapi",
+ "roundup",
+ "usehollowdice",
+ "allowplaintextinrolls",
+ "showfromfornonwhispers",
+ "allowinlinerollsinoutput",
+ "nominmaxhighlight",
+ "norollhighlight",
+ "disablestringexpansion",
+ "disablerollvariableexpansion",
+ "disableparameterexpansion",
+ "disablerollprocessing",
+ "disableattributereplacement",
+ "attemptattributeparsing",
+ "disableinlineformatting",
+ "enableattributesubstitution",
+ "formatinforequesttext",
+ "explodingonesandaces",
+ "functionbenchmarking",
+ "limitmaxbarvalues",
+ "beaconsheet",
+ ];
+
+ const SettingsThatAreNumbers = [
+ "emotesourcetokensize",
+ "emotetargettokensize"
+ ]
+
+ const TokenAttrsThatAreNumbers = [
+ "left", "top", "width", "height", "rotation"
+ ]
+
+ const EncodingReplaements = [
+ "%5B:[",
+ "%5D:]",
+ "%7B:{",
+ "%7C:|",
+ "%7D:}",
+ "%20: ",
+ "%21:!",
+ '%22:"',
+ "%23:#",
+ "%24:$",
+ "%25:%",
+ "%26:&",
+ "%27:'",
+ "%28,(",
+ "%29,)",
+ "%2A:*",
+ "%2B:+",
+ "%2C:,",
+ "%2D:-",
+ "%2E:.",
+ "%2F:/",
+ "%3C:<",
+ "%3D:=",
+ "%3E:>",
+ ]
+
+ //---------------------------------------------------------------------------------------
+ // Handles registering token change events for other api scripts
+ //---------------------------------------------------------------------------------------
+ const observers = {
+ tokenChange: []
+ };
+
+ const observeTokenChange = (handler) => {
+ if (handler && _.isFunction(handler)) {
+ observers.tokenChange.push(handler);
+ }
+ };
+
+ const notifyObservers = (event, obj, prev) => {
+ if (observers[event]) {
+ _.each(observers[event], (handler) => {
+ try {
+ handler(obj, prev);
+ } catch (e) {
+ log(`ScriptCards: An observer threw an exception in handler: ${handler}. Error: ${e.message}`);
+ }
+ });
+ } else {
+ log(`ScriptCards: No observers found for event: ${event}`);
+ }
+ };
+ //---------------------------------------------------------------------------------------
+ //---------------------------------------------------------------------------------------
+
+ //---------------------------------------------------------------------------------------
+ // "borrowed" from token-mod to force lighting updates after modifying tokens
+ //---------------------------------------------------------------------------------------
+
+ const getActivePages = () => [...new Set([
+ Campaign().get('playerpageid'),
+ ...Object.values(Campaign().get('playerspecificpages')),
+ ...findObjs({
+ type: 'player',
+ online: true
+ })
+ .filter((p) => playerIsGM(p.id))
+ .map((p) => p.get('lastpage'))
+ ])
+ ];
+
+ const forceLightUpdateOnPage = (() => {
+ const forPage = (pid) => (getObj('page', pid) || { set: () => { } }).set('force_lighting_refresh', true);
+ let pids = new Set();
+ let t;
+
+ return (pid) => {
+ pids.add(pid);
+ clearTimeout(t);
+ t = setTimeout(() => {
+ let activePages = getActivePages();
+ [...pids].filter(p => activePages.includes(p)).forEach(forPage);
+ pids.clear();
+ }, 100);
+ };
+ })();
+
+ // HTML Templates for the various pieces of the output card. Replaced sections are marked with
+ // !{...} syntax, and will have values substituted in them when the output line is built.
+ var htmlTemplate = `
");
+ outputLine = outputLine.replace(/\[t\]/gi, "");
+ outputLine = outputLine.replace(/\[\/t\]/gi, "
");
+ outputLine = outputLine.replace(/\[p\s+?(.+?)\]/gi, "");
+ outputLine = outputLine.replace(/\[p\]/gi, "
");
+ outputLine = outputLine.replace(/\[\/p\]/gi, "
");
+ outputLine = outputLine.replace(/\[[Ff](\d+)\](.*?)\[\/F\]/gi, "$2
"); // [F8] for font size 8
+ outputLine = outputLine.replace(/\[[Ff]\:([a-zA-Z\s]*)\:?(\d+)?\](.*?)\[\/[Ff]\]/gi, "$3"); // [F8] for font size 8
+ outputLine = outputLine.replace(/\[[Bb]\](.*?)\[\/[Bb]\]/g, "$1"); // [B]...[/B] for bolding
+ outputLine = outputLine.replace(/\[[Ii]\](.*?)\[\/[Ii]\]/g, "$1"); // [I]...[/I] for italics
+ outputLine = outputLine.replace(/\[[Uu]\](.*?)\[\/[Uu]\]/g, "$1"); // [U]...[/u] for underline
+ outputLine = outputLine.replace(/\[[Ss]\](.*?)\[\/[Ss]\]/g, "$1"); // [S]...[/s] for strikethru
+ outputLine = outputLine.replace(/\[[Qq]\](.*?)\[\/[Qq]\]/g, "$1
"); // [S]...[/s] for strikethru
+ outputLine = outputLine.replace(/\[[Cc]\](.*?)\[\/[Cc]\]/g, "$1
"); // [C]..[/C] for center
+ outputLine = outputLine.replace(/\[[Ll]\](.*?)\[\/[Ll]\]/g, "$1
"); // [L]..[/L] for left
+ outputLine = outputLine.replace(/\[[Rr]\](.*?)\[\/[Rr]\]/g, "$1
"); // [R]..[/R] for right
+ outputLine = outputLine.replace(/\[[Jj]\](.*?)\[\/[Jj]\]/g, "$1
"); // [J]..[/J] for justify
+
+ var fakerolls = outputLine.match(/(\[roll(.*?)\](.*?)\[\/roll\])/gi)
+ for (let fakeroll in fakerolls) {
+ var base = fakerolls[fakeroll].replace(/\[roll(.*?)\]/, "").replace(/\[\/roll(.*?)\]/, "")
+ let style = cardParameters.stylenormal
+ if (fakerolls[fakeroll].substring(5, 7).toLowerCase() == ":c") {
+ style = cardParameters.stylecrit
+ }
+ if (fakerolls[fakeroll].substring(5, 7).toLowerCase() == ":f") {
+ style = cardParameters.stylefumble
+ }
+ var work = buildTooltip(base, "Roll: " + base + "
Result: " + base, style)
+ outputLine = outputLine.replace(fakerolls[fakeroll], work)
+ }
+
+ var images = outputLine.match(/(\[img(.*?)\](.*?)\[\/img\])/gi);
+ for (var image in images) {
+ var work = images[image].replace("[img", "
").replace("]", " src=");
+ outputLine = outputLine.replace(images[image], work);
+ }
+ var webms = outputLine.match(/(\[webm(.*?)\](.*?)\[\/webm\])/gi);
+ for (var webm in webms) {
+ var work = webms[webm].replace("[webm]", "");
+ outputLine = outputLine.replace(webms[webm], work);
+ }
+ var statusmarkers = outputLine.match(/\[sm(.*?)\](.*?)\[\/sm\]/gi);
+ for (var sm in statusmarkers) {
+ var markername = statusmarkers[sm].substring(statusmarkers[sm].indexOf("]") + 1);
+ markername = markername.substring(0, markername.indexOf("["));
+ var work = statusmarkers[sm].replace("[sm", "
").replace("]", " src=" + tokenMarkerURLs[markername]);
+ outputLine = outputLine.replace(statusmarkers[sm], work);
+ }
+ var buttons = outputLine.match(/\[button(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:([0-9]{1,})PX)?(\:(.*?))?\](.*?)\:\:(.*?)\[\/button\]/gi);
+ for (var button in buttons) {
+ var customTextColor = undefined;
+ var customBackgroundColor = undefined;
+ var customfontsize = undefined;
+ let customHoverText = undefined;
+ var basebutton = buttons[button].replace(/\[button(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:([0-9]{1,})PX)?(\:.+?)?\]/gi, "[button]");
+ //log(basebutton);
+ if (basebutton.toLowerCase() !== buttons[button].toLowerCase()) {
+ var tempbutton = buttons[button].replace("[button:", "").replace("[Button:", "").replace("[BUTTON:", "").split("]")[0];
+ var customs = tempbutton.split(":");
+ var firstColorUsed = false;
+ for (var c in customs) {
+ if (customs[c].startsWith("#")) {
+ if (firstColorUsed) { customBackgroundColor = customs[c]; } else { customTextColor = customs[c]; firstColorUsed = true; }
+ } else {
+ if (customs[c].toLowerCase().endsWith("px")) {
+ customfontsize = customs[c];
+ } else {
+ if (customs[c] !== "[rbutton") customHoverText = customs[c];
+ }
+ }
+ }
+ }
+ var title = basebutton.split("::")[0].replace("[button]", "").replace("[Button]", "").replace("[BUTTON]", "");
+ var action = basebutton.split("::")[1].replace("[/button]", "").replace("[/Button]", "").replace("[/BUTTON]", "");
+ if (cardParameters.dontcheckbuttonsforapi == "0") {
+ action = action.replace(/(^|\ +)_/g, " --");
+ }
+ if (raw == true) {
+ outputLine = outputLine.replace(buttons[button], makeTemplateButton(title, action, cardParameters));
+ } else {
+ outputLine = outputLine.replace(buttons[button], makeButton(title, action, cardParameters, customTextColor, customBackgroundColor, customfontsize, customHoverText));
+ }
+ }
+
+ var sheetbuttons = outputLine.match(/\[sheetbutton(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:([0-9]{1,})PX)?(\:(.*?))?\](.*?)\:\:(.*?)\:\:(.*?)\[\/sheetbutton\]/gi);
+ for (var button in sheetbuttons) {
+ var customTextColor = undefined;
+ var customBackgroundColor = undefined;
+ var customfontsize = undefined;
+ let customHoverText = undefined;
+ var basebutton = sheetbuttons[button].replace(/\[sheetbutton(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:([0-9]{1,})PX)?(\:.+?)?\]/gi, "[sheetbutton]");
+ if (basebutton.toLowerCase() !== sheetbuttons[button].toLowerCase()) {
+ var tempbutton = sheetbuttons[button].replace("[sheetbutton:", "").replace("[Sheetbutton:", "").replace("[SHEETBUTTON:", "").split("]")[0];
+ var customs = tempbutton.split(":");
+ var firstColorUsed = false;
+ for (var c in customs) {
+ if (customs[c].startsWith("#")) {
+ if (firstColorUsed) { customBackgroundColor = customs[c]; } else { customTextColor = customs[c]; firstColorUsed = true; }
+ } else {
+ if (customs[c].toLowerCase().endsWith("px")) {
+ customfontsize = customs[c];
+ } else {
+ if (customs[c] !== "[rbutton") customHoverText = customs[c];
+ }
+ }
+ }
+ }
+ var title = basebutton.split("::")[0].replace("[sheetbutton]", "").replace("[Sheetbutton]", "").replace("[SHEETBUTTON]", "");
+ var actor = "";
+ var tryID = basebutton.split("::")[1];
+ if (getObj("character", tryID)) {
+ actor = tryID;
+ } else {
+ if (getObj("graphic", tryID)) {
+ if (getObj("character", getObj("graphic", tryID).get("represents"))) {
+ actor = getObj("graphic", tryID).get("represents");
+ }
+ }
+ }
+ if (actor == "") {
+ // eslint-disable-next-line no-unused-vars
+ var possible = findObjs({ type: "character" }).filter(function (value, index, arg) { return value.get("name").toLowerCase().trim() == tryID.toLowerCase().trim() });
+ if (possible.length > 0) {
+ actor = possible[0].get("_id");
+ }
+ }
+ if (actor !== "") {
+ var action = "~" + actor + "|" + basebutton.split("::")[2].replace("[/sheetbutton]", "").replace("[/Sheetbutton]", "").replace("[/SHEETBUTTON]", "");
+ if (cardParameters.dontcheckbuttonsforapi == "0") {
+ action = action.replace(/(^|\ +)_/g, " --");
+ }
+ if (raw == true) {
+ outputLine = outputLine.replace(sheetbuttons[button], makeTemplateButton(title, action, cardParameters));
+ } else {
+ outputLine = outputLine.replace(sheetbuttons[button], makeButton(title, action, cardParameters, customTextColor, customBackgroundColor, customfontsize, customHoverText));
+ }
+ }
+ }
+
+ var reentrantbuttons = outputLine.match(/\[rbutton(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:([0-9]{1,})PX)?(\:(.*?))?\](.*?)\:\:(.*?)\[\/rbutton\]/gi);
+ for (var button in reentrantbuttons) {
+ var customTextColor = undefined;
+ var customBackgroundColor = undefined;
+ var customfontsize = undefined;
+ var customHoverText = undefined;
+ var basebutton = reentrantbuttons[button].replace(/\[rbutton(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:\#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}))?(\:([0-9]{1,})PX)?(\:.+?)?\]/gi, "[rbutton]");
+ if (basebutton.toLowerCase() !== reentrantbuttons[button].toLowerCase()) {
+ var tempbutton = reentrantbuttons[button].replace("[rbutton:", "").replace("[Rbutton:", "").replace("[RBUTTON:", "").split("]")[0];
+ var customs = tempbutton.split(":");
+ var firstColorUsed = false;
+ for (var c in customs) {
+ if (customs[c].startsWith("#")) {
+ if (firstColorUsed) { customBackgroundColor = customs[c]; } else { customTextColor = customs[c]; firstColorUsed = true; }
+ } else {
+ if (customs[c].toLowerCase().endsWith("px")) {
+ customfontsize = customs[c];
+ } else {
+ if (customs[c] !== "[rbutton") customHoverText = customs[c];
+ }
+ }
+ }
+ }
+ var title = basebutton.split("::")[0].replace("[rbutton]", "").replace("[Rbutton]", "").replace("[RBUTTON]", "");
+ var reentrylabel = basebutton.split("::")[1].replace("[/rbutton]", "").replace("[/Rbutton]", "").replace("[/RBUTTON]", "");
+ var action = "!sc-reentrant " + cardParameters["reentrant"] + "-|-" + reentrylabel
+ if (raw == true) {
+ outputLine = outputLine.replace(reentrantbuttons[button], makeTemplateButton(title, action, cardParameters));
+ } else {
+ outputLine = outputLine.replace(reentrantbuttons[button], makeButton(title, action, cardParameters, customTextColor, customBackgroundColor, customfontsize, customHoverText));
+ }
+ }
+
+ //DiceFont Stuff
+ var dicefontchars = diceLetters;
+ if (cardParameters.usehollowdice !== "0") { dicefontchars = dicefontchars.toLowerCase(); }
+ outputLine = outputLine.replace(/\[d4\](.*?)\[\/d4\]/g, function (x) { var side = parseInt(x.replace("[d4]", "").replace("[/d4]", "").trim()); return "" + dicefontchars.charAt(side) + "" });
+ outputLine = outputLine.replace(/\[d6\](.*?)\[\/d6\]/g, function (x) { var side = parseInt(x.replace("[d6]", "").replace("[/d6]", "").trim()); return "" + dicefontchars.charAt(side) + "" });
+ outputLine = outputLine.replace(/\[d8\](.*?)\[\/d8\]/g, function (x) { var side = parseInt(x.replace("[d8]", "").replace("[/d8]", "").trim()); return "" + dicefontchars.charAt(side) + "" });
+ outputLine = outputLine.replace(/\[d10\](.*?)\[\/d10\]/g, function (x) { var side = parseInt(x.replace("[d10]", "").replace("[/d10]", "").trim()); return "" + dicefontchars.charAt(side) + "" });
+ outputLine = outputLine.replace(/\[d12\](.*?)\[\/d12\]/g, function (x) { var side = parseInt(x.replace("[d12]", "").replace("[/d12]", "").trim()); return "" + dicefontchars.charAt(side) + "" });
+ outputLine = outputLine.replace(/\[d20\](.*?)\[\/d20\]/g, function (x) { var side = parseInt(x.replace("[d20]", "").replace("[/d20]", "").trim()); return "" + dicefontchars.charAt(side) + "" });
+
+ return outputLine;
+ }
+
+ function makeButton(title, url, parameters, customTextColor, customBackgroundColor, customfontsize, customHoverText) {
+ var thisButtonStyle = buttonStyle;
+ let thisHoverText = "";
+ if (customTextColor) { thisButtonStyle = thisButtonStyle.replace("!{buttontextcolor}", customTextColor) }
+ if (customBackgroundColor) { thisButtonStyle = thisButtonStyle.replace("!{buttonbackground}", customBackgroundColor) }
+ if (customfontsize) { thisButtonStyle = thisButtonStyle.replace("!{buttonfontsize}", customfontsize) }
+ if (customHoverText) { thisHoverText = ` title="${customHoverText}" ` }
+ return `${removeBRs(title)}`;
+ }
+
+ function makeTemplateButton(title, url, parameters) {
+ if (parameters.overridetemplate !== "none") {
+ return `${removeBRs(title)}`;
+ } else {
+ return "Template button without Template"
+ }
+ }
+
+ function removeInlineRolls(text, cardParameters) {
+ if (cardParameters.allowinlinerollsinoutput !== "0") { return text; }
+ return text.replace(/\[\[/g, " ").replace(/\]\]/g, " ");
+ }
+
+ function fillCharAttrs(attrs) {
+ if (!attrs) { return; }
+ repeatingCharAttrs = {};
+ attrs.forEach(function (x) {
+ repeatingCharAttrs[x.get("name")] = x.get("current");
+ });
+ }
+
+ function getRepeatingSectionIDs(charid, prefix) {
+ const repeatingAttrs = {};
+ regExp = new RegExp(`^${prefix}_(-[-A-Za-z0-9]+?|\\d+)_`);
+ let repOrder;
+ // Get attributes
+ findObjs({
+ _type: 'attribute',
+ _characterid: charid
+ }).forEach(o => {
+ const attrName = o.get('name');
+ if (attrName.search(regExp) === 0)
+ repeatingAttrs[attrName] = o;
+ else if (attrName === `_reporder_${prefix}`)
+ repOrder = o.get('current').split(',');
+ });
+ if (!repOrder)
+ repOrder = [];
+ // Get list of repeating row ids by prefix from repeatingAttrs
+ const unorderedIds = [...new Set(Object.keys(repeatingAttrs)
+ .map(n => n.match(regExp))
+ .filter(x => !!x)
+ .map(a => a[1]))];
+ const repRowIds = [...new Set(repOrder.filter(x => unorderedIds.includes(x)).concat(unorderedIds))];
+ return repRowIds;
+ }
+
+ function getSectionAttrs(charid, entryname, sectionname, searchtext, fuzzy) {
+ var return_set = [];
+ var char_attrs = findObjs({ type: "attribute", _characterid: charid });
+ try {
+ if (!fuzzy) {
+ var action_prefix = char_attrs
+ .filter(function (z) {
+ return (z.get("name").startsWith(sectionname) && z.get("name").endsWith(searchtext))
+ })
+ .filter(entry => entry.get("current") == entryname)[0]
+ .get("name").slice(0, -searchtext.length);
+ } else {
+ var thisRegex = new RegExp(entryname, "i")
+ var action_prefix = char_attrs
+ .filter(function (z) {
+ return (z.get("name").startsWith(sectionname) && z.get("name").match(searchtext))
+ })
+ .filter(entry => entry.get("current").match(thisRegex))[0]
+ .get("name").slice(0, -searchtext.length);
+ }
+ } catch {
+ return return_set;
+ }
+
+ try {
+ action_attrs = char_attrs.filter(function (z) { return (z.get("name").startsWith(action_prefix)); })
+ } catch {
+ return return_set;
+ }
+
+ action_attrs.forEach(function (z) {
+ if (z.get("name")) {
+ return_set.push(z.get("name").toString().replace(action_prefix, "") + "|" + z.get("current").toString().replace(/(?:\r\n|\r|\n)/g, "
").replace("@{", "").replace("}", ""));
+ return_set.push(z.get("name").toString().replace(action_prefix, "") + "_max|" + z.get("max").toString());
+ }
+ })
+
+ var PrefixEntry = "xxxActionIDxxxx|" + action_prefix.replace(sectionname + "_", "");
+ PrefixEntry = PrefixEntry.substring(0, PrefixEntry.length - 1);
+
+ return_set.unshift(PrefixEntry);
+
+ return (return_set);
+ }
+
+ function getSectionAttrsByID(charid, sectionname, sectionID) {
+ var return_set = [];
+ var action_prefix = sectionname + "_" + sectionID + "_";
+
+ try {
+ var action_attrs = findObjs({ type: "attribute", _characterid: charid })
+ action_attrs = action_attrs.filter(function (z) { return (z.get("name").startsWith(action_prefix)); })
+ } catch {
+ return return_set;
+ }
+
+ action_attrs.forEach(function (z) {
+ try {
+ return_set.push(z.get("name").replace(action_prefix, "") + "|" + z.get("current").toString().replace(/(?:\r\n|\r|\n)/g, "
"));//.replace(/[\[\]\@]/g, " "));
+ return_set.push(z.get("name").replace(action_prefix, "") + "_max|" + z.get("max").toString());
+ // eslint-disable-next-line no-empty
+ } catch { log(`Attribute lookup error parsing ${z.get("name'")}`) }
+ })
+ return (return_set);
+ }
+
+ function rollOnRollableTable(tableName) {
+ var theTable = findObjs({ type: "rollabletable", name: tableName })[0];
+ if (theTable != null) {
+ var tableItems = findObjs({ type: "tableitem", _rollabletableid: theTable.id });
+ if (tableItems != null) {
+ var rollResults = {};
+ var rollIndex = 0;
+ var lastRollIndex = 0;
+ var itemCount = 0;
+ var maxRoll = 0;
+ var nonOneWeights = 0;
+ tableItems.forEach(function (item) {
+ try {
+ var thisWeight = parseInt(item.get("weight"));
+ if (isNaN(thisWeight)) { thisWeight = 1 }
+ if (thisWeight !== 1) { nonOneWeights += 1; }
+ rollIndex += thisWeight;
+ for (var x = lastRollIndex + 1; x <= rollIndex; x++) {
+ rollResults[x] = itemCount;
+ }
+ itemCount += 1;
+ maxRoll += thisWeight;
+ lastRollIndex += thisWeight;
+ } catch {
+ log(`ScriptCards: Exception attempting to get rollable table item information`)
+ }
+ });
+ var tableRollResult = randomInteger(maxRoll);
+ try {
+ if (nonOneWeights == 0) {
+ return [tableItems[rollResults[tableRollResult]].get("name"), tableItems[rollResults[tableRollResult]].get("avatar"), tableRollResult, tableItems[rollResults[tableRollResult]].get("weight")];
+ } else {
+ return [tableItems[rollResults[tableRollResult]].get("name"), tableItems[rollResults[tableRollResult]].get("avatar"), 0, tableItems[rollResults[tableRollResult]].get("weight")];
+ }
+ } catch {
+ log(`ScriptCards: Exception while reading table results for table item ${tableRollResult}`)
+ return "", ""
+ }
+ } else {
+ return ["", ""];
+ }
+ }
+ }
+
+ function loadLibraryHandounts() {
+ ScriptCardsLibrary = {};
+ var handouts = filterObjs(function (obj) {
+ if (obj.get("type") == "handout" && obj.get("name").startsWith("ScriptCards Library")) { return true; } else { return false; }
+ });
+ if (handouts) {
+ handouts.forEach(function (handout) {
+ var libraryName = handout.get("name").replace("ScriptCards Library", "").trim();
+ //var libraryContent = "";
+ handout.get("notes", function (notes) {
+ if (notes) {
+ notes = notes.replace(/\/g, " ").replace(/\<\/p\>/g, " ").replace(/\
/g, " ").replace(/ /g, " ").replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&");
+ }
+ ScriptCardsLibrary[libraryName] = notes;
+ });
+ });
+ }
+ }
+
+ function insertLibraryContent(cardContent, libraryList) {
+ if (!libraryList) { return cardContent; }
+ cardContent = cardContent.substring(0, cardContent.length - 2);
+ var libs = libraryList.split(";");
+ cardContent += " --X| ";
+ for (var x = 0; x < libs.length; x++) {
+ if (ScriptCardsLibrary[libs[x]]) {
+ cardContent += ScriptCardsLibrary[libs[x]];
+ }
+ }
+ cardContent += " }}";
+ return cardContent;
+ }
+
+ function stashAScript(stashIndex, scriptContent, cardParameters, stringVariables, rollVariables, returnStack, parameterStack, programCounter, outputLines, resultStringName, stashType, arrayVariables, arrayIndexes, gmonlyLines, bareoutputLines) {
+ if (scriptCardsStashedScripts[stashIndex]) { delete scriptCardsStashedScripts[stashIndex]; }
+
+ scriptCardsStashedScripts[stashIndex] = {};
+ scriptCardsStashedScripts[stashIndex].scriptContent = JSON.stringify(scriptContent);
+ scriptCardsStashedScripts[stashIndex].cardParameters = JSON.stringify(cardParameters);
+ scriptCardsStashedScripts[stashIndex].stringVariables = JSON.stringify(stringVariables);
+ scriptCardsStashedScripts[stashIndex].rollVariables = JSON.stringify(rollVariables);
+ scriptCardsStashedScripts[stashIndex].arrayVariables = JSON.stringify(arrayVariables);
+ scriptCardsStashedScripts[stashIndex].arrayIndexes = JSON.stringify(arrayIndexes);
+ scriptCardsStashedScripts[stashIndex].hashTables = JSON.stringify(hashTables);
+ scriptCardsStashedScripts[stashIndex].returnStack = JSON.stringify(returnStack);
+ scriptCardsStashedScripts[stashIndex].parameterStack = JSON.stringify(parameterStack);
+ scriptCardsStashedScripts[stashIndex].outputLines = JSON.stringify(outputLines);
+ scriptCardsStashedScripts[stashIndex].gmonlyLines = JSON.stringify(gmonlyLines);
+ scriptCardsStashedScripts[stashIndex].bareoutputLines = JSON.stringify(bareoutputLines);
+ scriptCardsStashedScripts[stashIndex].repeatingSectionIDs = JSON.stringify(repeatingSectionIDs);
+ scriptCardsStashedScripts[stashIndex].repeatingSection = JSON.stringify(repeatingSection);
+ scriptCardsStashedScripts[stashIndex].repeatingCharAttrs = JSON.stringify(repeatingCharAttrs);
+ scriptCardsStashedScripts[stashIndex].repeatingCharID = repeatingCharID;
+ scriptCardsStashedScripts[stashIndex].repeatingSectionName = repeatingSectionName;
+ scriptCardsStashedScripts[stashIndex].repeatingIndex = repeatingIndex;
+ scriptCardsStashedScripts[stashIndex].programCounter = programCounter;
+ scriptCardsStashedScripts[stashIndex].resultStringName = resultStringName;
+ scriptCardsStashedScripts[stashIndex].loopControl = loopControl;
+ scriptCardsStashedScripts[stashIndex].loopStack = loopStack;
+ scriptCardsStashedScripts[stashIndex].stashType = stashType;
+ }
+
+ function uuidv4() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+
+ function isNumeric(n) {
+ return !isNaN(parseInt(n));
+ }
+
+ function isNumber(n) {
+ return !isNaN(Number(n));
+ }
+
+ // Despite the name, this function takes a semicolon separated value string and returns an
+ // array of objects. Used to parse parameter lists to gosub branches.
+ function CSVtoArray(text) {
+ var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^;'"\s\\]*(?:\s+[^;'"\s\\]+)*)\s*(?:;\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^;'"\s]*(?:\s+[^;'"\s\\]+)*)\s*)*$/;
+ var re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^;'"\s]*(?:\s+[^;'"\s\\]+)*))\s*(?:;|$)/g;
+ // Return NULL if input string is not well formed CSV string.
+ if (!re_valid.test(text)) {
+ log(`ScriptCards Error: Parameter content is not valid. Do you have unescaped quotes or qoutes not surrounding escaped values? (${text})`)
+ return null;
+ }
+
+ var a = []; // Initialize array to receive values.
+ text.replace(re_value, // "Walk" the string using replace with callback.
+ function (m0, m1, m2, m3) {
+
+ // Remove backslash from \' in single quoted values.
+ if (m1 != null) a.push(m1.replace(/\\'/g, "'"));
+
+ // Remove backslash from \" in double quoted values.
+ else if (m2 != null) a.push(m2.replace(/\\"/g, '"'));
+ else if (m3 != null) a.push(m3);
+ return ''; // Return empty string.
+ });
+
+ // Handle special case of empty last value.
+ if (/,\s*$/.test(text)) a.push('');
+ return a;
+ }
+
+ // ScriptCards doesn't directly support inline rolls, but there are cases where some sheets
+ // are so strange that an inline roll is required to retrieve simple values. This routine
+ // is run before processing script lines and replaces the inline roll markers with their
+ // final values as literal strings.
+ function processInlinerolls(msg) {
+ if (_.has(msg, 'inlinerolls')) {
+ return _.chain(msg.inlinerolls)
+ .reduce(function (m, v, k) {
+ var ti = _.reduce(v.results.rolls, function (m2, v2) {
+ if (_.has(v2, 'table')) {
+ m2.push(_.reduce(v2.results, function (m3, v3) {
+ m3.push(v3.tableItem.name);
+ return m3;
+ }, []).join(', '));
+ }
+ return m2;
+ }, []).join(', ');
+ m['$[[' + k + ']]'] = (ti.length && ti) || v.results.total || 0;
+ return m;
+ }, {})
+ .reduce(function (m, v, k) {
+ return m.replace(k, v);
+ }, msg.content)
+ .value();
+ } else {
+ return msg.content;
+ }
+ }
+
+ function debugOutput(msg) {
+ if (debugMode) { log(msg) }
+ }
+
+ function isBlank(str) {
+ return (!str || /^\s*$/.test(str));
+ }
+
+ function removeTags(str) {
+ if ((str === null) || (str === ''))
+ return false;
+ else
+ return str.toString().replace(/(<([^>]+)>)/ig, '');
+ }
+
+ function removeBRs(str) {
+ if ((str === null) || (str === '') || (str === undefined))
+ return false;
+ else
+ return str.toString().replace(/
/ig, '').replace(/
/ig, '');
+ }
+
+ var playJukeboxTrack = function (trackname) {
+ var track = findObjs({ type: 'jukeboxtrack', title: trackname })[0];
+ if (track) {
+ track.set('softstop', false);
+ track.set('playing', true);
+ } else {
+ log(`ScriptCards warning: Jukebox track ${trackname} not found in game.`);
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ function handleDiceFormats(text, rollResult, hadOne, hadAce, currentOperator) {
+ // Split the dice roll into components
+ var matches = text.toLowerCase().match(/^(\d+[dDuUmM][fF\d]+)([eE])?([kK][lLhH]\d+)?([rR][<\>]\d+)?([rR][oO][<\>]\d+)?(![HhLl])?(![<\>]\d+)?(!)?([Ww][Ss][Xx])?([Ww][Ss])?([Ww][Xx])?([Ww])?([\><]\d+)?(f\<\d+)?(\#)?$/);
+
+ var resultSet = {
+ rollSet: [],
+ rollTextSet: [],
+ rollText: "",
+ rollTotal: 0,
+ hadOne: false,
+ hadAce: false,
+ dontHilight: false,
+ highlightasfailure: false,
+ dontBase: false,
+ sides: 6,
+ rawRollSet: [],
+ droppedRollSet: [],
+ keptRollSet: [],
+ diceFontSet: []
+ };
+
+ // Just some defaults
+ var count = 1;
+ var sides = 6;
+ var fudgeDice = false;
+ var fudgeText = ["-", "0", "+"];
+ var keeptype = "a";
+ var keepcount = count;
+ var rerollThreshold = undefined;
+ var rerollType = "x";
+ var rerollUnlimited = true;
+ var rollUnique = false;
+ var explodeValue = 0;
+ var isWildDie = false;
+ var minRollValue = 1;
+ var wildDieDropSelf = false;
+ var wildDieDropHighest = false;
+ var successThreshold = 0;
+ var failureThreshold = 0;
+
+ if (matches) {
+ for (var x = 1; x < matches.length; x++) {
+ if (matches[x]) {
+
+ // Handle XdY
+ if (matches[x].match(/^\d+[dD][fF\d]+$/)) {
+ count = matches[x].split("d")[0]
+ keepcount = count;
+ sides = matches[x].split("d")[1]
+ if (sides == "f") { sides = 3; fudgeDice = true; }
+ }
+
+ // Handle XmY (X dice, always returning the highest possible (sides) roll value)
+ if (matches[x].match(/^\d+[mM]\d+$/)) {
+ count = matches[x].split("m")[0]
+ keepcount = count;
+ sides = matches[x].split("m")[1]
+ minRollValue = Number(sides);
+ if (sides == "f") { sides = 3; fudgeDice = true; }
+ log(`ScriptCards: Player ${lastExecutedDisplayName}(${lastExecutedByID}) used an XmY dice formula in a roll: ${text}`)
+ }
+
+ // Handle XuY (Roll XdY dice, always getting a unique value on the roll)
+ if (matches[x].match(/^\d+[uU]\d+$/)) {
+ count = matches[x].split("u")[0]
+ keepcount = count;
+ sides = matches[x].split("u")[1]
+ if (parseInt(keepcount) > parseInt(sides)) { keepcount = sides; count = sides; log(`ScriptCards: Attempt to roll more than ${sides} unique d${sides}`) }
+ rollUnique = true;
+ }
+
+ // Handle keep highest/lowest
+ if (matches[x].match(/^[kK][lLhH]\d+$/)) {
+ keeptype = matches[x].charAt(1);
+ keepcount = Number(matches[x].substring(2));
+ }
+
+ // Handle keep furthest from center (rolling with emphasis)
+ if (matches[x].match(/^[eE]$/)) {
+ keeptype = "e";
+ keepcount = 1;
+ }
+
+ // Handle reroll thresholds (r>Z, r]\d+$/)) {
+ rerollType = matches[x].charAt(1);
+ rerollThreshold = Number(matches[x].substring(2));
+ rerollUnlimited = true;
+ }
+
+ // Handle reroll once (ro>Z, ro]\d+$/)) {
+ rerollType = matches[x].charAt(2);
+ rerollThreshold = Number(matches[x].substring(3));
+ rerollUnlimited = false;
+ }
+
+ // Handle exploding dice (!h or !l)
+ if (matches[x].match(/^![HhLl]$/)) {
+ keepcount = 1;
+ if (matches[x].charAt(1) == "h") {
+ explodeValue = sides;
+ keeptype = "h";
+ } else {
+ explodeValue = 1;
+ keeptype = "h";
+ }
+ }
+
+ // Handle exploding dice without rerolls
+ if (matches[x].match(/^![\<\>]\d+$/)) {
+ explodeValue = Number(matches[x].substring(2));
+ }
+
+ // Handle exploding dice
+ if (matches[x].match(/^!$/)) {
+ explodingType = "h";
+ explodeValue = sides;
+ }
+
+ // Handle counting successes
+ if (matches[x].match(/^[\><]\d+/)) {
+ successThreshold = Number(matches[x].substring(1));
+ }
+
+ // Handle failure counting
+ if (matches[x].match(/^f[\><]\d+/)) {
+ failureThreshold = Number(matches[x].substring(2));
+ }
+
+ // Handle Wild Dice
+ if (matches[x].match(/^([Ww])?([Xx])?([Ss])?([Xx])?$/)) {
+ isWildDie = true;
+ count--;
+ if (matches[x].indexOf("s") > 0) {
+ wildDieDropSelf = true;
+ }
+ if (matches[x].indexOf("x") > 0) {
+ wildDieDropHighest = true;
+ }
+ }
+
+ // Handle counting successes
+ if (matches[x].match(/^\#$/)) {
+ resultSet.dontHilight = true;
+ resultSet.dontBase = true;
+ }
+ }
+ }
+
+ resultSet.sides = sides;
+
+ // Roll the dice
+ for (var x = 0; x < count; x++) {
+ do {
+ var thisDiceRoll = rollWithReroll(sides, rerollThreshold, rerollType, rerollUnlimited);
+ var thisRoll = Number(thisDiceRoll[1]);
+ } while (resultSet.rollSet.includes(thisRoll) && rollUnique)
+ if (Number(minRollValue) > 1) {
+ thisRoll = Number(minRollValue);
+ thisDiceRoll[0] = sides.toString() + ` {MIN ${minRollValue}}`;
+ }
+ if (fudgeDice) { thisRoll -= 2; resultSet.dontHilight = true }
+ var thisTotal = thisRoll;
+ //var thisText = thisTotal.toString();
+ var thisText = thisDiceRoll[0];
+ if (fudgeDice) {
+ thisText = fudgeText[thisRoll + 1];
+ }
+ while ((explodeValue > 0) && (thisRoll >= explodeValue)) {
+ thisReroll = rollWithReroll(sides, rerollThreshold, rerollType, rerollUnlimited);
+ thisRoll = Number(thisReroll[1]);
+ thisTotal += Number(thisReroll[1]);
+ thisText += "!" + thisReroll[0].toString();
+ }
+ resultSet.rollSet.push(thisTotal);
+ resultSet.rawRollSet.push(thisTotal);
+ resultSet.rollTextSet.push(thisText);
+ switch (sides) {
+ case 20: rollSet.diceFontSet.push("[d20]thisTotal[/d20]"); break;
+ case 12: rollSet.diceFontSet.push("[d12]thisTotal[/d12]"); break;
+ case 10: rollSet.diceFontSet.push("[d10]thisTotal[/d10]"); break;
+ case 8: rollSet.diceFontSet.push("[d8]thisTotal[/d8]"); break;
+ case 6: rollSet.diceFontSet.push("[d6]thisTotal[/d6]"); break;
+ case 4: rollSet.diceFontSet.push("[d4]thisTotal[/d4]"); break;
+ }
+ }
+
+ // If we are keeping highest or lowest number of dice, eliminate the ones to remove
+ if (keepcount !== count) {
+ var removeCount = count - keepcount;
+ for (var x = 0; x < removeCount; x++) {
+ if (keeptype == "h") { removeLowestRoll(resultSet.rollSet, resultSet.rollTextSet, resultSet.droppedRollSet) }
+ if (keeptype == "l") { removeHighestRoll(resultSet.rollSet, resultSet.rollTextSet, resultSet.droppedRollSet) }
+ if (keeptype == "e") { removeClosestRolls(resultSet.rollSet, resultSet.rollTextSet, resultSet.droppedRollSet, sides / 2) }
+ }
+ for (var x = 0; x < count; x++) {
+ if (!resultSet.rollTextSet[x].startsWith("[x")) {
+ resultSet.keptRollSet.push(resultSet.rollSet[x]);
+ }
+ }
+ } else {
+ resultSet.keptRollSet.push(...resultSet.rollSet);
+ }
+
+ // Handle the Wild Die if present
+ if (isWildDie) {
+ var thisRoll = randomInteger(sides);
+ var thisTotal = thisRoll;
+ var thisText = thisTotal.toString();
+ while (thisRoll == sides) {
+ thisRoll = randomInteger(sides);
+ thisTotal += thisRoll;
+ thisText += "!" + thisRoll.toString();
+ }
+ if (thisTotal == 1) { resultSet.hadOne = true; }
+ if (thisTotal >= sides) { resultSet.hadAce = true; }
+ if (thisTotal == 1 && wildDieDropHighest) { removeHighestRoll(resultSet.rollSet, resultSet.rollTextSet) }
+ if (thisTotal > 1 || !wildDieDropSelf) {
+ thisText = "W:" + thisText;
+ } else {
+ thisText = "W:[x" + thisText + "x]";
+ }
+ resultSet.rollSet.push(thisTotal);
+ resultSet.rollTextSet.push(thisText);
+ resultSet.dontHilight = true;
+ }
+ }
+
+ // Compute the totals for the roll
+ var thisResult = 0;
+ var thisResultText = "";
+ if (successThreshold == 0) {
+ for (var x = 0; x < resultSet.rollSet.length; x++) {
+ thisResult += resultSet.rollSet[x];
+ thisResultText += resultSet.rollTextSet[x] + (x == resultSet.rollSet.length - 1 ? "" : ",");
+ }
+ } else {
+ for (var x = 0; x < resultSet.rollSet.length; x++) {
+ if (resultSet.rollSet[x] > successThreshold) {
+ thisResult += 1
+ }
+ if (failureThreshold > 0 && resultSet.rollSet[x] < failureThreshold) {
+ thisResult -= 1
+ resultSet.highlightasfailure = true
+ }
+ thisResultText += resultSet.rollTextSet[x] + (x == resultSet.rollSet.length - 1 ? "" : ",");
+ }
+ resultSet.dontHilight = true;
+ }
+ resultSet.rollTotal = thisResult;
+ resultSet.rollText = thisResultText;
+
+ return resultSet;
+ }
+
+ function rollWithReroll(sides, rerollThreshold, rType, unlimited) {
+ var thisRoll = randomInteger(sides);
+ var rollText = "";
+ var once = false;
+ while (rType == ">" && (unlimited || !once) && thisRoll >= rerollThreshold) { rollText += `[x${thisRoll}x]`; thisRoll = randomInteger(sides); once = true; }
+ while (rType == "<" && (unlimited || !once) && thisRoll <= rerollThreshold) { rollText += `[x${thisRoll}x]`; thisRoll = randomInteger(sides); once = true; }
+ rollText += thisRoll;
+ return [rollText, thisRoll]
+ }
+
+ function removeHighestRoll(rollSet, rollSetText, droppedRollSet) {
+ var highest = -1;
+ var highestIndex = -1;
+ for (var x = 0; x < rollSet.length; x++) {
+ if (rollSet[x] > highest) {
+ highest = rollSet[x];
+ highestIndex = x;
+ }
+ }
+ if (highestIndex > -1) {
+ droppedRollSet.push(rollSet[highestIndex]);
+ rollSet[highestIndex] = 0;
+ rollSetText[highestIndex] = "[x" + rollSetText[highestIndex] + "x]"
+ }
+ }
+
+ function removeLowestRoll(rollSet, rollSetText, droppedRollSet) {
+ var lowest = Number.MAX_SAFE_INTEGER;
+ var lowestIndex = -1;
+ for (var x = 0; x < rollSet.length; x++) {
+ if (rollSet[x] < lowest && rollSet[x] > 0) {
+ lowest = rollSet[x];
+ lowestIndex = x;
+ }
+ }
+ if (lowestIndex > -1) {
+ droppedRollSet.push(rollSet[lowestIndex]);
+ rollSet[lowestIndex] = 0;
+ rollSetText[lowestIndex] = "[x" + rollSetText[lowestIndex] + "x]"
+ }
+ }
+
+ function removeClosestRolls(rollSet, rollSetText, droppedRollSet, centerValue) {
+ var difference = 0;
+ var emphasisIndex = -1;
+ for (var x = 0; x < rollSet.length; x++) {
+ if ((Math.abs(centerValue - rollSet[x]) < difference && rollSet[x] > 0)
+ || (emphasisIndex == -1)
+ || (Math.abs(centerValue - rollSet[x] && rollSet[x] > rollSet[emphasisIndex])
+ )) {
+ difference = Math.abs(centerValue - rollSet[x]);
+ emphasisIndex = x;
+ }
+ }
+ if (emphasisIndex > -1) {
+ for (let x = 0; x < rollSet.length; x++) {
+ if (x !== emphasisIndex) {
+ droppedRollSet.push(rollSet[rollSet[x]])
+ }
+ }
+ rollSet[emphasisIndex] = 0;
+ rollSetText[emphasisIndex] = "[x" + rollSetText[emphasisIndex] + "x]"
+ }
+ }
+
+ function StripAndSplit(content, delimeter) {
+ //log(content)
+ //var work = content.replace("[","").replace(")","").replace("(","").replace(")","")
+ var work = content.replace(/[^a-z0-9áéíóúñü:; \,_-]/gim, "");
+ return work.split(delimeter);
+ }
+
+ const getCleanImgsrc = (imgsrc) => {
+ if (imgsrc.startsWith('https://s3.amazonaws.com/files.d20.io/marketplace')) {
+ log(`ScriptCards: imgsrc property appears to be a marketplace image. ${imgsrc}. imgsrc properties must be from the user's asset library.`);
+ }
+ let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/);
+ if (parts) {
+ return parts[1] + 'thumb' + parts[3] + (parts[4] ? parts[4] : `?${Math.round(Math.random() * 9999999)}`);
+ }
+ return;
+ };
+
+ function DelaySandboxExecution(thisContent) {
+ var now = new Date;
+ var startTime = (now.getHours() * 60 * 60) + (now.getMinutes() * 60) + now.getSeconds();
+ var delay = parseFloat(thisContent);
+ if (delay > 10) { delay = 10; }
+ var endTime = startTime + delay;
+ while (endTime > (now.getHours() * 60 * 60) + (now.getMinutes() * 60) + now.getSeconds()) {
+ now = new Date;
+ }
+ }
+
+ function delayFunction(speaker, output) {
+ return function () {
+ sendChat("ScriptCards", output.trim());
+ }
+ }
+
+ const generateUUID = (() => {
+ let a = 0;
+ let b = [];
+ return () => {
+ let c = (new Date()).getTime() + 0;
+ let f = 7;
+ let e = new Array(8);
+ let d = c === a;
+ a = c;
+ for (; 0 <= f; f--) {
+ e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);
+ c = Math.floor(c / 64);
+ }
+ c = e.join("");
+ if (d) {
+ for (f = 11; 0 <= f && 63 === b[f]; f--) {
+ b[f] = 0;
+ }
+ b[f]++;
+ } else {
+ for (f = 0; 12 > f; f++) {
+ b[f] = Math.floor(64 * Math.random());
+ }
+ }
+ for (f = 0; 12 > f; f++) {
+ c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);
+ }
+ return c;
+ };
+ })();
+
+ const generateRowID = () => generateUUID().replace(/_/g, "Z");
+
+ async function setStringOrArrayElement(varName, varValue, cardParameters) {
+ // Determine if the varName is a string or Array Element
+ if (varName.match(/(.*)\((-?\d*)\)/)) {
+ // It's an array element reference, split into a name and index
+ var match = varName.match(/(.*)\((-?\d*)\)/);
+ var arrayName = match[1];
+ var arrayIndex = match[2];
+
+ if (isNumber(arrayIndex)) {
+ // If the array doesn't exist, create an empty array
+ if (arrayVariables[arrayName] == null) {
+ arrayVariables[arrayName] = [];
+ }
+ if (arrayVariables[arrayName].length >= (arrayIndex - 1) && arrayIndex >= 0) {
+ arrayVariables[arrayName][arrayIndex] = varValue;
+ } else {
+ if (arrayIndex < 0) {
+ arrayVariables[arrayName].unshift(varValue);
+ } else {
+ arrayVariables[arrayName].push(varValue);
+ }
+ }
+ }
+ } else {
+ if (varValue == null) { varValue = "" }
+
+ //if (typeof (varValue) === 'string' && varValue.charAt(0) == "+") {
+ if (typeof (varValue) === 'string' && varValue.charAt(0) == cardParameters.concatenationcharacter) {
+ varValue = (stringVariables[varName] || "") + varValue.substring(1);
+ }
+
+ stringVariables[varName] = await replaceVariableContent(varValue, cardParameters, true);
+ }
+ }
+
+ function reload_template_mule() {
+ templates = {}
+
+ if (typeof Supernotes_Templates != "undefined") {
+ templates = Object.assign({}, Supernotes_Templates);
+ }
+
+ templates["dnd5e"] = {
+ boxcode: ``, //"Bookinsanity",
+ titlecode: `
`, //padding-bottom: .1rem;
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; margin: 0px; font-size: 10px; color:#fff; padding: 2px 1px 1px 2px; background-color: #d00; text-align: center; border-radius: 5px;'`,
+ playerbuttonstyle: `style='display:inline-block; color:#000; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `
• `,
+ handoutbuttonstyle: `style='display:inline-block; color:#13f2fc; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ footer: ""
+ };
+ templates["dnd1e_green"] = {
+ boxcode: `
`,
+ titlecode: `
`, //padding-bottom: .1rem;
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; font-size: 10px; color:#000; padding: 2px 0px 2px 0px; background-color: transparent; border: 1px solid black; text-align: center; border-radius: 0px;'`,
+ playerbuttonstyle: `style='display:inline-block; color:#000; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: `
• `,
+ handoutbuttonstyle: `style='display:inline-block; color:#13f2fc; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ footer: ""
+ };
+ templates["dnd1e_amber"] = {
+ boxcode: `
`,
+ titlecode: `
`, //padding-bottom: .1rem;
+ textcode: "
",
+ buttonwrapper: `
`,
+ buttonstyle: `style='display:inline-block; font-size: 10px; color:#000; padding: 2px 1px 1px 2px; background-color: transparent; border: 1px solid black; text-align: center; border-radius: 0px;'`,
+ playerbuttonstyle: `style='display:inline-block; color:#000; font-weight:normal; background-color: transparent;padding: 0px; border: none;'`,
+ buttondivider: ` • `,
+ handoutbuttonstyle: `style='display:inline-block; color:#13f2fc; font-weight:normal; background-color: transparent; padding: 0px; border: none;'`,
+ footer: ""
+ };
+
+ try {
+ var findTemplateChar = findObjs({ _type: "character", name: "ScriptCards_TemplateMule" })[0];
+ } catch {
+ log(`ScriptCards: TemplateMule not found`)
+ }
+
+ try {
+ if (findTemplateChar) {
+ const muleTemplates = findObjs({ _type: "ability", characterid: findTemplateChar.id });
+ if (muleTemplates) {
+ muleTemplates.forEach(template => {
+ const tempName = template.get("name");
+ templates[tempName] = templates[tempName] || {};
+ const templateText = template.get("action");
+ const templateLines = templateText.split("||");
+
+ templateLines.forEach(line => {
+ const pieces = line.replace(/(\r\n|\n|\r)/gm, "").split("::");
+ if (pieces && pieces.length === 2) {
+ const [key, value] = pieces.map(piece => piece.trim());
+ const formattedValue = value.replace(/\{/g, "<").replace(/\}/g, ">");
+
+ const templateKeys = [
+ 'boxcode', 'titlecode', 'textcode', 'buttonwrapper', 'buttonstyle',
+ 'footer', 'tablestyle', 'thstyle', 'tdstyle', 'trstyle',
+ 'subtitlestyle', 'h1style', 'h2style', 'h3style', 'h4style', 'h5style'
+ ];
+
+ if (templateKeys.includes(key)) {
+ templates[tempName][key] = formattedValue;
+ }
+ }
+ });
+ });
+ }
+ }
+ } catch (error) {
+ log("ScriptCards: Error parsing Templates Mule. Mule templates may not be available");
+ }
+ log(`ScriptCards: ${Object.keys(templates).length} Templates loaded`);
+ }
+
+ function ParseCalculatedAttribute(attribute, character) {
+ while (attribute.match(/\@\{(.*?)\}/g) != null) {
+ var thisMatch = attribute.match(/\@\{(.*?)\}/g)[0];
+ var replacement = "";
+ try {
+ replacement = getAttrByName(character.id, thisMatch.replace("@{", "").replace("}", ""), "current");
+ } catch {
+ log(`Failure looking up attribute ${thisMatch}`)
+ }
+ attribute = attribute.replace(thisMatch, replacement);
+ }
+ attribute = attribute.replace(/floor\((.*?)\)/g, "$1 {FLOOR}"); // Remove double square brackets
+ attribute = attribute.replace(/ceil\((.*?)\)/g, "$1 {CEIL}"); // Remove double square brackets
+ attribute = attribute.replace(/\[\[(.*?)\]\]/g, "$1"); // Remove double square brackets
+ attribute = attribute.replace(/\((.*?)\)/g, "$1"); // Remove double square brackets
+ return attribute;
+ }
+
+ function FillTemplateStyle(piece, cardParameters, raw) {
+ if (!raw || cardParameters.overridetemplate === "none") {
+ return "";
+ }
+
+ const templateStyle = templates[cardParameters.overridetemplate][piece];
+ return templateStyle ? `style='${templateStyle}'` : "";
+ }
+
+ function storeVariable(charid, prefix, varname, type, cardParameters = null) {
+ try {
+ let attributeName;
+ let variableValue;
+
+ switch (type) {
+ case 'roll':
+ attributeName = `SCR_${prefix}-${varname}`;
+ variableValue = JSON.stringify(rollVariables[varname]);
+ break;
+ case 'string':
+ attributeName = `SCS_${prefix}-${varname}`;
+ variableValue = stringVariables[varname];
+ break;
+ case 'array':
+ attributeName = `SCA_${prefix}-${varname}`;
+ variableValue = JSON.stringify(arrayVariables[varname]);
+ break;
+ case 'hash':
+ attributeName = `SCH_${prefix}-${varname}`;
+ variableValue = JSON.stringify(hashTables[varname]);
+ if (variableValue === '{}') {
+ const testObj = findObjs({ type: "attribute", characterid: charid, name: attributeName })[0];
+ if (testObj) {
+ testObj.remove();
+ }
+ return;
+ }
+ break;
+ case 'setting':
+ if (cardParameters && cardParameters[varname] !== undefined) {
+ attributeName = `SCT_${prefix}-${varname}`;
+ variableValue = cardParameters[varname];
+ } else {
+ log(`Attempted to store ${varname} setting, which does not exist`);
+ return;
+ }
+ break;
+ default:
+ log(`Unknown variable type: ${type}`);
+ return;
+ }
+
+ const testObj = findObjs({ type: "attribute", characterid: charid, name: attributeName })[0];
+ if (testObj) {
+ testObj.set("current", variableValue);
+ } else {
+ createObj("attribute", {
+ name: attributeName,
+ current: variableValue,
+ characterid: charid
+ });
+ }
+ } catch (e) {
+ log(`Unable to store ${type} ${varname} on ${charid}, error ${e}`);
+ }
+ }
+
+ function loadVariable(charid, prefix, varname, type, cardParameters = null) {
+ try {
+ let testname = "unknown"
+ switch (type) {
+ case "roll": testname = `SCR_${prefix}-${varname}`; break;
+ case "string": testname = `SCS_${prefix}-${varname}`; break;
+ case "array": testname = `SCA_${prefix}-${varname}`; break;
+ case "hash": testname = `SCH_${prefix}-${varname}`; break;
+ case "setting": testname = `SCT_${prefix}-${varname}`; break;
+ }
+ let charobj = getObj("character", charid)
+ if (charobj) {
+ let attr = findObjs({ type: "attribute", characterid: charid, name: testname })[0];
+ if (attr) {
+ switch (type) {
+ case "roll": rollVariables[varname] = JSON.parse(attr.get("current")); break;
+ case "string": stringVariables[varname] = attr.get("current"); break;
+ case "array": arrayVariables[varname] = JSON.parse(attr.get("current")); break;
+ case "hash": hashTables[varname] = JSON.parse(attr.get("current")); break;
+ case "setting": cardParameters[varname] = attr.get("current"); break;
+ }
+ } else {
+ log(`ScriptCards: Attribute ${testname} not found on Storage Mule ${charid}`)
+ }
+ }
+ } catch (e) {
+ log(`Unable to load ${varname} on ${charid}, error ${e} `)
+ }
+ }
+
+ function handleEmoteCommands(thisTag, thisContent) {
+ /*
+ try {
+ var characterName = thisTag.substring(1);
+ var macroName = thisContent.trim();
+ log("gothere")
+ if (characterName.length >= 1) {
+ log("sendwithname")
+ sendChat("ScriptCards", `%{${characterName}|${macroName}}`);
+ } else {
+ sendChat("ScriptCards", `#${macroName}`);
+ }
+ } catch (e) {
+ log(`Error generating echo command: ${e}. thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ */
+ try {
+ var sendAs = thisTag.substring(1);
+ sendChat(sendAs, thisContent);
+ } catch (e) {
+ log(`Error generating echo command: ${e}. thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleConsoleLogs(thisTag, thisContent) {
+ try {
+ log(thisContent);
+ } catch (e) {
+ log(e);
+ }
+ }
+
+ function handleHasTableCommands(thisTag, hashTables, thisContent) {
+ try {
+ let hashparams = thisTag.substring(2);
+ let tableName = hashparams.split('("')[0];
+ let tableKey = hashparams.split('("')[1];
+ tableKey = tableKey.substring(0, tableKey.indexOf('")'));
+ if (hashTables[tableName] === undefined) {
+ hashTables[tableName] = {};
+ }
+ if (thisContent == null || thisContent.trim() == "") {
+ delete hashTables[tableName][tableKey];
+ } else {
+ hashTables[tableName][tableKey] = thisContent;
+ }
+ } catch (e) {
+ log(`Error setting hash table ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleWaitStatements(thisTag, thisContent, cardParameters) {
+ try {
+ if (thisTag.length == 1) {
+ DelaySandboxExecution(thisContent);
+ } else {
+ if (thisTag.indexOf(":") > 0) {
+ var delayArgs = thisTag.substring(1).split(":");
+ var delayLength = delayArgs[0];
+ delayArgs.shift();
+ var delayCommand = delayArgs.join(":");
+ var hideInfo = "--#hidecard|1"
+ if (delayCommand.charAt(0) == "+" || delayCommand.charAt(0) == "*") {
+ hideInfo = "--#hidetitlecard|1"
+ }
+ setTimeout(delayFunction("", `!script {{ ${hideInfo} --${await replaceVariableContent(delayCommand, cardParameters)}|${await replaceVariableContent(thisContent, cardParameters)} }}`), parseFloat(delayLength) * 1000)
+ }
+ }
+ } catch (e) {
+ log(`Error setting up wait statement ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ /*
+ function handleLoopStatements(thisTag, thisContent, cardParameters, cardLines) {
+ let loopCounter = undefined || thisTag.substring(1);
+ if (loopCounter && loopCounter !== "" && loopCounter !== "!") {
+ if (loopControl[loopCounter]) { log(`ScriptCards: Warning - loop counter ${loopCounter} reused inside itself on line ${lineCounter}.`); }
+ let params = thisContent.split(cardParameters.parameterdelimiter);
+ if (params.length === 2 && params[0].toLowerCase().endsWith("each")) {
+ // This will be a for-each loop, so the first (and only) parameter must be an array name
+ if (arrayVariables[params[1]] && arrayVariables[params[1]].length > 0) {
+ loopControl[loopCounter] = { loopType: "foreach", initial: 0, current: 0, end: arrayVariables[params[1]].length - 1, step: 1, nextIndex: lineCounter, arrayName: params[1] }
+ stringVariables[loopCounter] = arrayVariables[params[1]][0];
+ loopStack.push(loopCounter);
+ if (cardParameters.debug == 1) { log(`ScriptCards: Info - Beginning of loop ${loopCounter}`) }
+ } else {
+ log(`ScriptCards For...Each loop without a defined array or with empty array on line ${lineCounter}`)
+ }
+ }
+ if (params.length === 2 && (params[0].toLowerCase().endsWith("while") || params[0].toLowerCase().endsWith("until"))) {
+ let originalContent = getLineContent(cardLines[lineCounter]);
+ let contentParts = originalContent.split(cardParameters.parameterdelimiter);
+ let isTrue = await processFullConditional(await replaceVariableContent(contentParts[1], cardParameters)) || params[0].toLowerCase().endsWith("until");
+ if (isTrue) {
+ loopControl[loopCounter] = { loopType: params[0].toLowerCase().endsWith("until") ? "until" : "while", initial: 0, current: 0, end: 999999, step: 1, nextIndex: lineCounter, condition: contentParts[1] }
+ stringVariables[loopCounter] = "true";
+ loopStack.push(loopCounter);
+ if (cardParameters.debug == 1) { log(`ScriptCards: Info - Beginning of loop ${loopCounter}`) }
+ } else {
+ let line = lineCounter;
+ for (line = lineCounter + 1; line < cardLines.length; line++) {
+ if (getLineTag(cardLines[line], line, "").trim() == "%") {
+ lineCounter = line;
+ }
+ }
+ if (lineCounter > cardLines.length) {
+ log(`ScriptCards: Warning - no end block marker found for loop block started ${loopCounter}`);
+ lineCounter = cardLines.length + 1;
+ }
+ }
+ }
+ if (params.length === 2 && (!params[0].toLowerCase().endsWith("each")) && (!params[0].toLowerCase().endsWith("until")) && (!params[0].toLowerCase().endsWith("while"))) { params.push("1"); } // Add a "1" as the assumed step value if only two parameters
+ if (params.length === 3) {
+ if (isNumeric(params[0]) && isNumeric(params[1]) && isNumeric(params[2]) && parseInt(params[2]) != 0) {
+ loopControl[loopCounter] = { loopType: "fornext", initial: parseInt(params[0]), current: parseInt(params[0]), end: parseInt(params[1]), step: parseInt(params[2]), nextIndex: lineCounter }
+ stringVariables[loopCounter] = params[0];
+ loopStack.push(loopCounter);
+ if (cardParameters.debug == 1) { log(`ScriptCards: Info - Beginning of loop ${loopCounter}`) }
+ } else {
+ if (parseInt(params[2] == 0)) {
+ log(`ScriptCards: Error - cannot use loop step of 0 at line ${lineCounter}`)
+ } else {
+ log(`ScriptCards: Error - loop initialization contains non-numeric values on line ${lineCounter}`)
+ }
+ }
+ }
+ } else {
+ if (loopStack.length >= 1) {
+ let currentLoop = loopStack[loopStack.length - 1];
+ if (loopControl[currentLoop]) {
+ loopControl[currentLoop].current += loopControl[currentLoop].step;
+ switch (loopControl[currentLoop].loopType) {
+ case "fornext":
+ stringVariables[currentLoop] = loopControl[currentLoop].current.toString();
+ break;
+ case "foreach":
+ try {
+ var beforeLoopEnded = stringVariables[currentLoop]
+ stringVariables[currentLoop] = arrayVariables[loopControl[currentLoop].arrayName][loopControl[currentLoop].current]
+ } catch {
+ stringVariables[currentLoop] = "ArrayError"
+ }
+ break;
+ case "while":
+ var isTrue = await processFullConditional(await replaceVariableContent(loopControl[currentLoop].condition, cardParameters));
+ if (!isTrue) {
+ loopControl[currentLoop].current = loopControl[currentLoop].end + 1;
+ loopControl[currentLoop].step = 1;
+ }
+ break;
+ case "until":
+ var isTrue = await processFullConditional(await replaceVariableContent(loopControl[currentLoop].condition, cardParameters));
+ if (isTrue) {
+ loopControl[currentLoop].current = loopControl[currentLoop].end + 1;
+ loopControl[currentLoop].step = 1;
+ }
+ break;
+
+ }
+ if ((loopControl[currentLoop].step > 0 && loopControl[currentLoop].current > loopControl[currentLoop].end) ||
+ (loopControl[currentLoop].step < 0 && loopControl[currentLoop].current < loopControl[currentLoop].end) ||
+ loopCounter == "!") {
+ stringVariables[currentLoop] = beforeLoopEnded;
+ loopStack.pop();
+ delete loopControl[currentLoop];
+ if (cardParameters.debug == 1) { log(`ScriptCards: Info - End of loop ${currentLoop}`) }
+ if (loopCounter == "!") {
+ let line = lineCounter;
+ for (line = lineCounter + 1; line < cardLines.length; line++) {
+ if (getLineTag(cardLines[line], line, "").trim() == "%") {
+ lineCounter = line;
+ }
+ }
+ if (lineCounter > cardLines.length) {
+ log(`ScriptCards: Warning - no end block marker found for loop block started ${loopCounter}`);
+ lineCounter = cardLines.length + 1;
+ }
+ }
+ } else {
+ lineCounter = loopControl[currentLoop].nextIndex;
+ }
+ }
+ } else {
+ log(`ScriptCards: Error - Loop end statement without an active loop on line ${lineCounter}`);
+ }
+ }
+ }
+ */
+
+ function handleCardSettingsCommands(thisTag, thisContent, cardParameters) {
+ try {
+ let paramName = thisTag.substring(1).toLowerCase();
+ paramName = parameterAliases[paramName] || paramName;
+ if (cardParameters[paramName] != null) {
+ cardParameters[paramName] = thisContent;
+ if (cardParameters.debug == "1") { log(`Setting parameter ${paramName} to value ${thisContent} - ${cardParameters[paramName]}`) }
+ } else {
+ if (cardParameters.debug == "1") { log(`Unable to set parameter ${paramName} to value ${thisContent}`) }
+ }
+
+ switch (paramName) {
+ case "sourcetoken":
+ var charLookup = getObj("graphic", thisContent.trim());
+ if (charLookup != null && charLookup.get("represents") !== "") {
+ cardParameters.sourcecharacter = getObj("character", charLookup.get("represents"));
+ }
+ break;
+
+ case "targettoken":
+ var charLookup = getObj("graphic", thisContent.trim());
+ if (charLookup != null && charLookup.get("represents") !== "") {
+ cardParameters.targetcharacter = getObj("character", charLookup.get("represents"));
+ }
+ break;
+
+ case "activepage":
+ if (thisContent.trim().toLowerCase() === "playerpage") {
+ cardParameters.activepageobject = getObj("page", Campaign().get("playerpageid"));
+ } else {
+ var pageLookup = getObj("page", thisContent.trim());
+ if (pageLookup != null) {
+ cardParameters.activepageobject = pageLookup;
+ }
+ }
+ break;
+
+ case "overridetemplate":
+ if (templates[thisContent.trim()] != null) {
+ cardParameters.overridetemplate = thisContent.trim();
+ } else {
+ if (thisContent.trim() !== "none") {
+ log(`ScriptCards: Unknown template ${thisContent.trim()} specified. Template names are case sensitive. Reverting to "none"`)
+ }
+ cardParameters.overridetemplate = "none";
+ }
+ break;
+
+ case "titlecardgradient":
+ if (thisContent.trim() !== "0") {
+ cardParameters["titlecardbackgroundimage"] = gradientStyle;
+ } else {
+ cardParameters["titlecardbackgroundimage"] = "";
+ }
+ break;
+
+ case "buttontextcolor":
+ if (thisContent.trim().match(/^[0-9a-fA-F]{6}$/)) {
+ //cardParameters["buttontextcolor"] = `#${thisContent.trim()}`;
+ }
+ break;
+
+ case "bodybackgroundimage":
+ if (thisContent.trim() !== "") {
+ cardParameters.oddrowbackground = "#00000000";
+ cardParameters.evenrowbackground = "#00000000";
+ }
+ break;
+
+ case "subtitleseperator":
+ cardParameters.subtitleseparator = thisContent;
+ break;
+
+ case "evenrowbackgroundimage":
+ if (thisContent.trim() !== "") {
+ cardParameters.evenrowbackground = "#00000000";
+ }
+ break;
+
+ case "oddrowbackgroundimage":
+ if (thisContent.trim() !== "") {
+ cardParameters.oddrowbackground = "#00000000";
+ }
+ break;
+ }
+ if (SettingsThatAreColors.includes(paramName)) {
+ if (thisContent.trim().match(/^[0-9a-fA-F]{8}$/)) {
+ cardParameters[paramName] = `#${thisContent.trim()}`;
+ }
+ if (thisContent.trim().match(/^[0-9a-fA-F]{6}$/)) {
+ cardParameters[paramName] = `#${thisContent.trim()}`;
+ }
+ if (thisContent.trim().match(/^[0-9a-fA-F]{3}$/)) {
+ cardParameters[paramName] = `#${thisContent.trim()}`;
+ }
+ if (thisContent.trim() == "") {
+ cardParameters[paramName] = "#00000000"
+ }
+ }
+ if (SettingsThatAreBooleans.includes(paramName)) {
+ if (thisContent.trim().toLowerCase() == "true"
+ || thisContent.trim().toLowerCase() == "yes"
+ || thisContent.trim().toLowerCase() == "on"
+ || thisContent.trim() == "1"
+ || thisContent.trim().toLowerCase() == "show") {
+ cardParameters[paramName] = "1";
+ } else {
+ cardParameters[paramName] = "0";
+ }
+ if (paramName == "beaconsheet") {
+ if (cardParameters[paramName] == "1") {
+ sheetType = "beacon";
+ } else {
+ sheetType = "classic";
+ }
+ }
+ }
+ if (SettingsThatAreNumbers.includes(paramName)) {
+ cardParameters[paramName] = thisContent.match(/\d+/)[0]
+ }
+ } catch (e) {
+ log(`Error setting card parameter ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleZOrderSettingCommands(thisTag, thisContent) {
+ try {
+ let statementParams = thisTag.split(":");
+ let contentParams = thisContent.split(" ");
+ if (statementParams[1].toLowerCase() == "graphic" || statementParams[1].toLowerCase() == "token") {
+ if (contentParams[0].toLowerCase() == "tofront") {
+ let thisObj = getObj("graphic", statementParams[2]);
+ if (thisObj) { toFront(thisObj); }
+ }
+ if (contentParams[0].toLowerCase() == "toback") {
+ let thisObj = getObj("graphic", statementParams[2]);
+ if (thisObj) { toBack(thisObj); }
+ }
+ }
+ } catch (e) {
+ log(`Error setting z-order ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleObjectModificationCommands(thisTag, thisContent, cardParameters) {
+ try {
+ if (thisTag.length > 1) {
+ if (thisTag.charAt(2) == ":" || thisTag.charAt(3) == ":") {
+ let objectType = thisTag.substring(1, 2).toLowerCase();
+ switch (objectType) {
+ case "o":
+ switch (thisTag.substring(2, 3).toLowerCase()) {
+ case "c": {
+ let settings = thisContent.split(cardParameters.parameterdelimiter);
+ let newChar = createObj("character", { name: settings[0] });
+ if (newChar) {
+ stringVariables[thisTag.substring(4)] = newChar.id
+ } else {
+ stringVariables[thisTag.substring(4)] = "OBJECT_CREATION_ERROR";
+ }
+ }
+ break;
+
+ case "t": {
+ let regexSplit = /\|(?=(?:[^"]*"[^"]*")*[^"]*$)/
+ let settings = thisContent.split(regexSplit);
+ let tProps = {}
+ for (let x = 0; x < settings.length; x++) {
+ //log(`Setting ${x} is ${settings[x]}`)
+ let setting = settings[x].match(/(".*?"|[^":\s]+)(?=\s*:|\s*$)/g);
+ if (setting[1]) {
+ if (setting[1].startsWith('"') && setting[1].endsWith('"')) {
+ setting[1] = setting[1].substring(1, setting[1].length - 1);
+ }
+ if (setting[0].startsWith("t-") || setting[0].startsWith("T-")) {
+ setting[0] = setting[0].substring(2);
+ }
+ tProps[setting[0]] = getSafeTokenProperty(setting[0], setting[1]);
+ }
+ }
+ if (tProps["subtype"] == undefined) { tProps["subtype"] = "token"; }
+ if (tProps["layer"] == undefined) { tProps["layer"] = "objects"; }
+ if (tProps["pageid"] == undefined) { tProps["pageid"] = Campaign().get("playerpageid"); }
+ if (tProps["left"] == undefined) { tProps["left"] = 200; }
+ if (tProps["top"] == undefined) { tProps["top"] = 200; }
+ if (tProps["width"] == undefined) { tProps["width"] = 70; }
+ if (tProps["height"] == undefined) { tProps["height"] = 70; }
+ try {
+ var newToken = createObj("graphic", tProps);
+ } catch (e) {
+ log(e)
+ }
+
+ if (newToken) {
+ stringVariables[thisTag.substring(4)] = newToken.id
+ } else {
+ stringVariables[thisTag.substring(4)] = "OBJECT_CREATION_ERROR";
+ }
+ }
+ break;
+
+ case "p": {
+ let regexSplit = /\|(?=(?:[^"]*"[^"]*")*[^"]*$)/
+ let settings = thisContent.split(regexSplit);
+ let tProps = {}
+ for (let x = 0; x < settings.length; x++) {
+ //log(`Setting ${x} is ${settings[x]}`)
+ let setting = settings[x].match(/(".*?"|[^":\s]+)(?=\s*:|\s*$)/g);
+ if (setting[1]) {
+ if (setting[1].startsWith('"') && setting[1].endsWith('"')) {
+ setting[1] = setting[1].substring(1, setting[1].length - 1);
+ }
+ tProps[setting[0]] = getSafeTokenProperty(setting[0], setting[1]);
+ }
+ }
+ if (tProps["subtype"] == undefined) { tProps["subtype"] = "token"; }
+ if (tProps["layer"] == undefined) { tProps["layer"] = "objects"; }
+ if (tProps["pageid"] == undefined) { tProps["pageid"] = Campaign().get("playerpageid"); }
+ if (tProps["left"] == undefined) { tProps["left"] = 200; }
+ if (tProps["top"] == undefined) { tProps["top"] = 200; }
+ if (tProps["width"] == undefined) { tProps["width"] = 70; }
+ if (tProps["height"] == undefined) { tProps["height"] = 70; }
+ try {
+ var newToken = createObj("graphic", tProps);
+ } catch (e) {
+ log(e)
+ }
+
+ if (newToken) {
+ stringVariables[thisTag.substring(4)] = newToken.id
+ } else {
+ stringVariables[thisTag.substring(4)] = "OBJECT_CREATION_ERROR";
+ }
+ }
+ break;
+
+ case "#": {
+ let returnVarName = thisTag.substring(4);
+ let settings = thisContent.split(cardParameters.parameterdelimiter);
+ if (returnVarName && settings[0]) {
+ let showPlayers = false;
+ if (settings[1] && (settings[1].toLowerCase() == "true" || settings[1].toLowerCase() == "yes" || settings[1] == "1")) {
+ showPlayers = true;
+ }
+ var newTable = createObj("rollabletable", { name: settings[0], showplayers: showPlayers })
+ if (newTable) {
+ stringVariables[returnVarName] = newTable.id
+ } else {
+ stringVariables[returnVarName] = "OBJECT_CREATION_ERROR";
+ }
+ }
+ }
+ break;
+
+ case "e": {
+ let returnVarName = thisTag.substring(4);
+ let settings = thisContent.split(cardParameters.parameterdelimiter);
+ if (returnVarName && settings[0] && settings[1]) {
+ let weight = 1;
+ let avatar = "";
+ if (settings[2]) {
+ weight = parseInt(settings[2]) || 1;
+ }
+ if (settings[3]) {
+ avatar = getCleanImgsrc(settings[3]);
+ }
+ var newTableEntry = createObj("tableitem", { _rollabletableid: settings[0], name: settings[1], weight: weight, avatar: avatar })
+ if (newTableEntry) {
+ stringVariables[returnVarName] = newTableEntry.id
+ } else {
+ stringVariables[returnVarName] = "OBJECT_CREATION_ERROR";
+ }
+ }
+ }
+ break;
+
+ case "b": {
+ let attributeInfo = thisTag.substring(4).split(":");
+ if (attributeInfo.length >= 3) {
+ let theCharacter = getObj("character", attributeInfo[1])
+ let isTokenAction = (attributeInfo[3] != null && attributeInfo[3].toLowerCase() == "y") ? true : false
+ if (theCharacter != null) {
+ let newAbility = createObj("ability", {
+ name: attributeInfo[2],
+ _characterid: attributeInfo[1],
+ action: thisContent,
+ istokenaction: isTokenAction
+ });
+ stringVariables[attributeInfo[0]] = newAbility.id;
+ } else {
+ stringVariables[attributeInfo[0]] = "OBJECT_CREATION_ERROR";
+ }
+ } else {
+ stringVariables[attributeInfo[0]] = "OBJECT_CREATION_ERROR";
+ }
+ }
+ break;
+
+ case "r": {
+ let charInfo = thisTag.substring(4).split(":");
+ if (charInfo.length >= 2) {
+ var theCharacter = getObj("character", charInfo[0])
+ var theSection = charInfo[1];
+ var rowID = generateRowID();
+ stringVariables["SC_LAST_CREATED_ROWID"] = rowID;
+ var repeatingInfo = thisContent.split("|");
+ if (theCharacter != null) {
+ for (var x = 0; x < repeatingInfo.length; x++) {
+ var subInfo = repeatingInfo[x].replace(":::").split(":");
+ subInfo.push("");
+ subInfo.push("");
+ subInfo.push("");
+ try {
+ // eslint-disable-next-line no-unused-vars
+ var newAttribute = createObj("attribute",
+ {
+ name: `repeating_${theSection}_${rowID}_${subInfo[0].trim()}`,
+ _characterid: theCharacter.id,
+ //current: subInfo[1].trim(),
+ current: "",
+ max: subInfo[2].replace(/%3A/gi, ":").trim()
+ }
+ )
+ newAttribute.setWithWorker({ current: subInfo[1].replace(/%3A/gi, ":").trim() });
+ } catch {
+ log(`ScriptCards: Error creating repeating section values on character ${theCharacter}, section ${theSection}`)
+ }
+ }
+ }
+
+ }
+ }
+ break;
+ }
+ break;
+
+ case "t": {
+ var tokenID = thisTag.substring(3);
+ if (tokenID.toLowerCase() == "s" && cardParameters.sourcetoken) { tokenID = cardParameters.sourcetoken; }
+ if (tokenID.toLowerCase() == "t" && cardParameters.targettoken) { tokenID = cardParameters.targettoken; }
+ let settings = thisContent.split(/(? theToken.get(sMaxname)) {
+ settingValue = theToken.get(sMaxname);
+ }
+ }
+
+ //if (settingName && settingValue) {
+ if (settingName) {
+ if (TokenAttrsThatAreNumbers.includes(settingName)) {
+ settingValue = Number(settingValue)
+ }
+
+ theToken.set(settingName, settingValue);
+ notifyObservers('tokenChange', theToken, prevTok);
+ }
+
+ if('undefined' !== typeof HealthColors && HealthColors.Update){
+ HealthColors.Update(theToken,prevTok);
+ }
+
+
+ forceLightUpdateOnPage();
+
+ }
+ } else {
+ log(`ScriptCards Error: Modify Token called without valid TokenID`)
+ }
+ }
+ break;
+
+ case "c": {
+ var charID = thisTag.substring(3);
+ if (charID.toLowerCase() == "s") {
+ if (cardParameters.sourcecharacter) {
+ tokenID = cardParameters.sourcecharacter.id;
+ }
+ }
+ if (charID.toLowerCase() == "t") {
+ if (cardParameters.targetcharacter) {
+ tokenID = cardParameters.targetcharacter.id;
+ }
+ }
+ var settings = thisContent.split(/(? 0) {
+ stringVariables[thisTag.substring(1)] = scriptData.shift();
+ } else {
+ stringVariables[thisTag.substring(1)] = "EndOfDataError";
+ }
+ }
+ }
+ } catch (e) {
+ log(`Error reading data ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleAPICallCommands(thisTag, thisContent, cardParameters, msg) {
+ try {
+ let apicmd = thisTag.substring(1);
+ let spacer = " ";
+ let slash = "\\";
+
+ // Replace _ with --
+ let params = thisContent.replace(/(^|\ +)_/g, " --");
+
+ // Remove deferral markers from deferred SelectManager/ZeroFrame calls
+ let regex = new RegExp(`${slash}{${slash}${cardParameters.deferralcharacter}(${slash}&.*?)${slash}}`, "g");
+ params = params.replace(regex, "{$1}");
+
+ // Remove deferral markers from deferred Fetch calls
+ regex = new RegExp(`${slash}@${slash}${cardParameters.deferralcharacter}${slash}((.*?)${slash})`, "g");
+ params = params.replace(regex, "@($1)");
+ regex = new RegExp(`${slash}*${slash}${cardParameters.deferralcharacter}${slash}((.*?)${slash})`, "g");
+ params = params.replace(regex, "*($1)");
+ regex = new RegExp(`get${slash}${cardParameters.deferralcharacter}${slash}.`, "g");
+ params = params.replace(regex, "get.");
+ regex = new RegExp(`set${slash}${cardParameters.deferralcharacter}${slash}.`, "g");
+ params = params.replace(regex, "set.");
+
+ var apiMessage = `!${apicmd}${spacer}${params}`.trim();
+ if (cardParameters.debug !== "0") {
+ log(`ScriptCards: Making API call - ${apiMessage}`);
+ }
+ sendChat(msg.who, apiMessage);
+ } catch (e) {
+ log(`Error calling API command ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleOutputCommands(thisTag, thisContent, cardParameters) {
+ try {
+ let rowData = buildRowOutput(thisTag.substring(1), await replaceVariableContent(thisContent.replace(/\[/g, "["), cardParameters, true), cardParameters.outputtagprefix, cardParameters.outputcontentprefix);
+ let rawRowData = buildRawRowOutput(thisTag.substring(1), await replaceVariableContent(thisContent.replace(/\[/g, "["), cardParameters, true), cardParameters.outputtagprefix, cardParameters.outputcontentprefix);
+
+ tableLineCounter += 1;
+ if (tableLineCounter % 2 == 0) {
+ while (rowData.indexOf("=X=FONTCOLOR=X=") > 0) { rowData = rowData.replace("=X=FONTCOLOR=X=", cardParameters.evenrowfontcolor); }
+ while (rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.evenrowbackground}; background-image: ${cardParameters.evenrowbackgroundimage}; `); }
+ //while(rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.evenrowbackground}; `); }
+ } else {
+ while (rowData.indexOf("=X=FONTCOLOR=X=") > 0) { rowData = rowData.replace("=X=FONTCOLOR=X=", cardParameters.oddrowfontcolor); }
+ while (rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.oddrowbackground}; background-image: ${cardParameters.oddrowbackgroundimage}; `); }
+ //while(rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.oddrowbackground}; `); }
+ }
+ rowData = processInlineFormatting(rowData, cardParameters, false);
+ rawRowData = processInlineFormatting(rawRowData, cardParameters, true);
+
+ thisTag.charAt(0) == "+" ? outputLines.push(rowData) : gmonlyLines.push(rowData)
+ thisTag.charAt(0) == "+" ? bareoutputLines.push(rawRowData) : null
+ } catch (e) {
+ log(`Error adding output/gmoutput line ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleGosubCommands(thisTag, thisContent, cardParameters) {
+ try {
+ parameterStack.push(callParamList);
+ let paramList = CSVtoArray(thisContent.trim());
+ callParamList = {};
+ arrayVariables["args"] = []
+ let paramCount = 1;
+
+ if (paramList) {
+ paramList.forEach(function (item) {
+ arrayVariables["args"].push(item.toString().trim());
+ callParamList[paramCount] = item.toString().trim();
+ paramCount++;
+ });
+ }
+ let jumpTo = thisTag.substring(1);
+ if (cardParameters.functionbenchmarking == "1") {
+ benchmarks[jumpTo] ? benchmarks[jumpTo] += 1 : benchmarks[jumpTo] = 1
+ }
+ if (lineLabels[jumpTo]) {
+ returnStack.push(lineCounter);
+ lineCounter = lineLabels[jumpTo];
+ } else { log(`ScriptCards Error: Label ${jumpTo} is not defined on line ${lineCounter} (${thisTag}, ${thisContent})`) }
+ } catch (e) {
+ log(`Error executing gosub ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleVisualEffectsCommand(thisTag, thisContent, cardParameters) {
+ try {
+ if (thisTag.length > 1) {
+ let effectType = thisTag.substring(1).toLowerCase();
+ let params = thisContent.split(" ");
+ switch (effectType) {
+ case "token":
+ if (params.length >= 2) {
+ let s = getObj("graphic", params[0]);
+ if (s) {
+ var x = s.get("left");
+ var y = s.get("top");
+ var pid = s.get("_pageid");
+ if (params[1].toLowerCase() == "ping") {
+ let moveall = false;
+ if (params[2] && params[2].toLowerCase() == "moveall") {
+ moveall = true;
+ }
+ sendPing(x, y, pid, stringVariables["SendingPlayerID"], moveall);
+ } else {
+ let effectInfo = findObjs({
+ _type: "custfx",
+ name: params[1].trim()
+ });
+ if (!_.isEmpty(effectInfo)) {
+ spawnFxWithDefinition(x, y, effectInfo[0].get('definition'), pid);
+ } else {
+ let t = params[1].trim();
+ if (t !== "" && t !== "none") {
+ spawnFx(x, y, t, pid);
+ }
+ }
+ }
+ }
+ }
+ break;
+ case "betweentokens":
+ if (params.length >= 3) {
+ var s = getObj("graphic", params[0]);
+ var p = getObj("graphic", params[1]);
+ if (s && p) {
+ var x1 = s.get("left");
+ var y1 = s.get("top");
+ var x2 = p.get("left");
+ var y2 = p.get("top");
+ var pid = s.get("_pageid");
+
+ var effectInfo = findObjs({
+ _type: "custfx",
+ name: params[2].trim()
+ });
+ if (!_.isEmpty(effectInfo)) {
+ var angleDeg = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
+ if (angleDeg < 0) {
+ angleDeg += 360;
+ }
+ var definition = effectInfo[0].get('definition');
+ definition.angle = angleDeg;
+ spawnFxWithDefinition(x1, y1, definition, pid);
+ } else {
+ var t = params[2].trim();
+ if (t !== "" && t !== "none") {
+ spawnFxBetweenPoints({ x: x1, y: y1 }, { x: x2, y: y2 }, t, pid);
+ }
+ }
+
+ }
+ }
+ break;
+
+ case "point":
+ var x = params[0];
+ var y = params[1];
+ var pid = Campaign().get("playerpageid");
+ if (cardParameters.activepage !== "") {
+ pid = cardParameters.activepage;
+ }
+ if (params[2].toLowerCase() == "ping") {
+ var moveall = false;
+ if (params[3] && params[3].toLowerCase() == "moveall") {
+ moveall = true;
+ }
+ sendPing(x, y, pid, stringVariables["SendingPlayerID"], moveall);
+ } else {
+ var effectInfo = findObjs({
+ _type: "custfx",
+ name: params[2].trim()
+ });
+ if (!_.isEmpty(effectInfo)) {
+ spawnFxWithDefinition(x, y, effectInfo[0].get('definition'), pid);
+ } else {
+ var t = params[2].trim();
+ if (x && y) {
+ if (t !== "" && t !== "none") {
+ spawnFx(x, y, t, pid);
+ }
+ }
+ }
+ }
+ break;
+
+ case "line":
+ var x1 = params[0];
+ var y1 = params[1];
+ var x2 = params[2];
+ var y2 = params[3];
+ var t = params[4];
+ var pid = Campaign().get("playerpageid");
+ if (cardParameters.activepage !== "") {
+ pid = cardParameters.activepage;
+ }
+ if (x1 && y1 && x2 && y2 && t && pid) {
+ spawnFxBetweenPoints({ x: x1, y: y1 }, { x: x2, y: y2 }, t, pid);
+ }
+ break;
+ }
+ }
+ } catch (e) {
+ log(`Error creating VFX ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleStashLines(thisTag, thisContent, cardParameters) {
+ try {
+ if (thisTag.charAt(0).toLowerCase() == "s") {
+ switch (thisTag.substring(1).toLowerCase()) {
+ case "rollvariables":
+ if (thisContent.trim().length > 0) {
+ state[APINAME].storedVariables[thisContent.trim()] = JSON.parse(JSON.stringify(rollVariables));
+ }
+ break;
+
+ case "stringvariables":
+ if (thisContent.trim().length > 0) {
+ state[APINAME].storedStrings[thisContent.trim()] = JSON.parse(JSON.stringify(stringVariables));
+ }
+ break;
+
+ case "settings":
+ if (thisContent.trim().length > 0) {
+ state[APINAME].storedSettings[thisContent.trim()] = {};
+ for (var key in cardParameters) {
+ if (cardParameters[key] !== defaultParameters[key]) {
+ state[APINAME].storedSettings[thisContent.trim()][key] = cardParameters[key];
+ }
+ }
+ }
+ break;
+ }
+
+ // Handle variable storage and recall
+ if ("$&@#:".includes(thisTag.charAt(1))) {
+ let varType = thisTag.charAt(1)
+ let prefix = ""
+ if (thisTag.length > 2) {
+ prefix = thisTag.substring(2)
+ }
+ let varList = thisContent.split(cardParameters.parameterdelimiter)
+ if (cardParameters.storagecharid && varList) {
+ if (varType == "$") {
+ //varList.forEach((element) => storeRollVar(cardParameters.storagecharid, prefix, element));
+ varList.forEach((element) => storeVariable(storageCharID, prefix, element, "roll", cardParameters));
+ }
+ if (varType == "&") {
+ //varList.forEach((element) => storeStringVar(cardParameters.storagecharid, prefix, element));
+ varList.forEach((element) => storeVariable(storageCharID, prefix, element, "string", cardParameters));
+ }
+ if (varType == "@") {
+ //varList.forEach((element) => storeArray(cardParameters.storagecharid, prefix, element));
+ varList.forEach((element) => storeVariable(storageCharID, prefix, element, "array", cardParameters));
+ }
+ if (varType == ":") {
+ //varList.forEach((element) => storeHashTable(cardParameters.storagecharid, prefix, element));
+ varList.forEach((element) => storeVariable(storageCharID, prefix, element, "hash", cardParameters));
+ }
+ if (varType == "#") {
+ if (thisContent.toLowerCase().trim() == "allsettings") {
+ varList = [];
+ for (var key in cardParameters) {
+ if (cardParameters[key] !== defaultParameters[key]) {
+ if (key != "storagecharid") {
+ varList.push(key);
+ }
+ }
+ }
+ }
+ //varList.forEach((element) => storeSetting(cardParameters.storagecharid, prefix, element.toLowerCase(),
+ varList.forEach((element) => storeVariable(storageCharID, prefix, element, "setting", cardParameters));
+ }
+ }
+ }
+ }
+ } catch (e) {
+ log(`Error stashing content ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleLoadCommands(thisTag, thisContent, cardParameters) {
+ try {
+ if (thisTag.charAt(0).toLowerCase() == "l") {
+ switch (thisTag.substring(1).toLowerCase()) {
+ case "rollvariables":
+ if (thisContent.trim().length > 0 && state[APINAME].storedVariables[thisContent.trim()] != null) {
+ newVariables = state[APINAME].storedVariables[thisContent.trim()];
+ for (var key in newVariables) {
+ rollVariables[key] = JSON.parse(JSON.stringify(newVariables[key]));
+ }
+ }
+ break;
+
+ case "stringvariables":
+ if (thisContent.trim().length > 0 && state[APINAME].storedStrings[thisContent.trim()] != null) {
+ newVariables = state[APINAME].storedStrings[thisContent.trim()];
+ for (var key in newVariables) {
+ stringVariables[key] = JSON.parse(JSON.stringify(newVariables[key]));
+ }
+ }
+ break;
+
+ case "settings":
+ if (thisContent.trim().length > 0) {
+ if (thisContent.trim().length > 0 && state[APINAME].storedSettings[thisContent.trim()] != null) {
+ newSettings = state[APINAME].storedSettings[thisContent.trim()];
+ for (var key in newSettings) {
+ cardParameters[key] = newSettings[key];
+ }
+ } else {
+ log(`Attempt to load stored settings ${thisContent.trim()}, but setting list not found.`)
+ }
+ }
+ break;
+ }
+ if ("$&@#:".includes(thisTag.charAt(1))) {
+ var varType = thisTag.charAt(1)
+ var prefix = ""
+ if (thisTag.length > 2) {
+ prefix = thisTag.substring(2)
+ }
+ let varList = thisContent.split(cardParameters.parameterdelimiter)
+ if (cardParameters.storagecharid && varList) {
+ var thisType = "unknown";
+ switch (varType) {
+ case "$": thisType = "roll"; break;
+ case "&": thisType = "string"; break;
+ case "@": thisType = "array"; break;
+ case ":": thisType = "hash"; break;
+ }
+ if (!(thisType === "unknown")) {
+ varList.forEach((element) => loadVariable(cardParameters.storagecharid, prefix, element, thisType, cardParameters));
+ } else {
+ if (varType == "#") {
+ thisType = "setting";
+ if (thisContent.toLowerCase().trim() == "allsettings") {
+ varList = [];
+ for (var key in cardParameters) {
+ if (key != "storagecharid") {
+ varList.push(key);
+ }
+ }
+ }
+ varList.forEach((element) => loadVariable(cardParameters.storagecharid, prefix, element, thisType, cardParameters));
+ }
+ }
+ }
+ }
+
+ }
+ } catch (e) {
+ log(`Error loading stashed content ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleBlockEndCommand(thisTag, thisContent, cardLines) {
+ try {
+ if (thisContent.charAt(0) === "[") {
+ if (lastBlockAction === "S") {
+ lastBlockAction = "";
+ }
+ if (lastBlockAction === "E") {
+ var line = lineCounter;
+ for (line = lineCounter + 1; line < cardLines.length; line++) {
+ if (getLineTag(cardLines[line], line, "").trim() === "]") {
+ lineCounter = line;
+ break;
+ }
+ }
+ if (lineCounter > cardLines.length) {
+ log(`ScriptCards: Warning - no end block marker found for block started on line ${lineCounter}`);
+ lineCounter = cardLines.length + 1;
+ }
+ lastBlockAction = "";
+ }
+ } else {
+ lastBlockAction = "";
+ }
+ } catch (e) {
+ log(`Error ending block statement ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleFunctionCommands(thisTag, thisContent, cardParameters, msg) {
+ try {
+ var variableName = thisTag.substring(1);
+ var params = thisContent.split(cardParameters.parameterdelimiter);
+ switch (params[0].toLowerCase()) {
+ case "character":
+ if (params.length >= 4) {
+ switch (params[1].toLowerCase()) {
+ case "runability":
+ var charid = undefined
+ var char = getObj("character", params[2]);
+ if (char === undefined) {
+ var actualToken = getObj("graphic", params[2]);
+ if (actualToken != null) {
+ charid = actualToken.get("represents");
+ char = getObj("character", charid);
+ }
+ } else {
+ charid = char.get("_id");
+ }
+ if (char != null) {
+ var abilname = params[3]
+ var ability = findObjs({ type: "ability", _characterid: charid, name: abilname })
+ //if (ability != null && ability !== []) {
+ if (Array.isArray(ability) && ability.length > 0) {
+ ability = ability[0]
+ if (ability != null) {
+ sendChat(char.get("name"), ability.get('action').replace(/@\{([^|]*?|[^|]*?\|max|[^|]*?\|current)\}/g, '@{' + (char.get('name')) + '|$1}'));
+ }
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case "system":
+ if (params.length >= 3) {
+ switch (params[1].toLowerCase()) {
+ case "date":
+ var d = new Date();
+ switch (params[2].toLowerCase()) {
+ case "getdatetime":
+ //log(cardParameters.locale);
+ try {
+ stringVariables[variableName] = d.toLocaleString(cardParameters.locale, { timeZone: cardParameters.timezone });
+ } catch {
+ stringVariables[variableName] = "Unknown/Invalid Locale or TimeZone";
+ }
+ break;
+ case "gettime":
+ try {
+ stringVariables[variableName] = d.toLocaleTimeString(cardParameters.locale, { timeZone: cardParameters.timezone });
+ } catch {
+ stringVariables[variableName] = "Unknown/Invalid Locale or TimeZone";
+ }
+ break;
+ case "getdate":
+ try {
+ stringVariables[variableName] = d.toLocaleDateString(cardParameters.locale, { timeZone: cardParameters.timezone });
+ } catch {
+ stringVariables[variableName] = "Unknown/Invalid Locale or TimeZone";
+ }
+ break;
+ case "getraw":
+ case "gettimestamp":
+ stringVariables[variableName] = d.getTime();
+ break;
+ }
+ break;
+
+ case "playerisgm":
+ stringVariables[variableName] = playerIsGM(params[2] || "") ? 1 : 0
+ break;
+
+ case "runaction":
+ case "runability":
+ var ability = findObjs({ type: "ability", _characterid: params[2], name: params[3] });
+ var metacard = ability[0].get("action")
+ if (Array.isArray(ability) && ability.length > 0) {
+ for (let x = 4; x < params.length; x++) {
+ metacard = metacard.replace(`[REPL${x - 3}]`, params[x])
+ }
+ metacard = metacard.replaceAll("-_-_", "--")
+ sendChat("API", metacard);
+ }
+ break;
+
+ case "readsetting":
+ stringVariables[variableName] = cardParameters[params[2].toLowerCase()] || "UnknownSetting";
+ break;
+
+ case "dumpvariables":
+ switch (params[2].toLowerCase()) {
+ case "rolls":
+ for (var key in rollVariables) {
+ log(`RollVariable: ${key}, Value: ${rollVariables[key]}`)
+ }
+ break;
+
+ case "string":
+ for (var key in stringVariables) {
+ log(`StringVariable: ${key}, Value: ${stringVariables[key]}`)
+ }
+ break;
+
+ case "array":
+ for (var key in arrayVariables) {
+ log(`ArrayVariable: ${key}, Value: ${arrayVariables[key]}`)
+ }
+ break;
+
+ case "hash":
+ case "hashtable":
+ case "hashtables":
+ for (var key in hashTables) {
+ log(`Hash: ${key}, Value: ${JSON.stringify(hashTables[key])}`)
+ }
+ break
+ }
+ break;
+
+ case "findability":
+ // Params: 2-character name, 3-ability name
+ stringVariables[variableName] = "AbilityNotFound";
+ var theChar = findObjs({ _type: "character", name: params[2] });
+ if (theChar[0]) {
+ var theAbility = findObjs({ _type: "ability", _characterid: theChar[0].id, name: params[3] });
+ if (theAbility[0]) {
+ stringVariables[variableName] = theAbility[0].id;
+ }
+ }
+ break;
+
+ case "dropoutputlines":
+ if (params[2].toLowerCase() == "all" || params[2].toLowerCase() == "both" || params[2].toLowerCase() == "direct") {
+ outputLines = [];
+ bareoutputLines = [];
+ }
+ if (params[2].toLowerCase() == "all" || params[2].toLowerCase() == "both" || params[2].toLowerCase() == "gmonly") {
+ gmonlyLines = [];
+ }
+ }
+ }
+ break;
+
+ case "roll":
+ case "rollvar":
+ case "rollvariable":
+ switch (params[1].toLowerCase()) {
+ case "sethilight":
+ case "sethighlight":
+ case "setrollhighlight":
+ case "sethighlightmode":
+ case "sethilightmode":
+ if (params[3].toLowerCase() == "none") {
+ rollVariables[params[2]].Style = cardParameters.stylenormal;
+ }
+ if (params[3].toLowerCase() == "crit" || params[3].toLowerCase() == "critical") {
+ rollVariables[params[2]].Style = cardParameters.stylecrit;
+ }
+ if (params[3].toLowerCase() == "fumble") {
+ rollVariables[params[2]].Style = cardParameters.stylefumble;
+ }
+ if (params[3].toLowerCase() == "both") {
+ rollVariables[params[2]].Style = cardParameters.styleboth;
+ }
+ break;
+ }
+ break;
+
+ case "turnorder":
+ var variableName = thisTag.substring(1);
+ if (params.length >= 2) {
+ if (params[1].toLowerCase() == "clear") {
+ Campaign().set("turnorder", "");
+ }
+ if (params[1].toLowerCase() == "getcurrentactor") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ }
+ //if (turnorder !== []) {
+ if (Array.isArray(turnorder) && turnorder.length > 0) {
+ stringVariables[variableName] = turnorder[0].id
+ }
+ }
+ if (params[1].toLowerCase() == "sort") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ turnorder.sort((a, b) => (Number(a.pr) > Number(b.pr)) ? 1 : ((Number(b.pr) > Number(a.pr)) ? -1 : 0))
+ turnorder.reverse();
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ }
+ }
+ if (params.length == 3) {
+ if (params[1].toLowerCase() == "removetoken") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ }
+ for (var x = turnorder.length - 1; x >= 0; x--) {
+ if (turnorder[x].id == params[2]) {
+ turnorder.splice(x, 1);
+ }
+ }
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ if (params[1].toLowerCase() == "removecustom") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ }
+ for (var x = turnorder.length - 1; x >= 0; x--) {
+ if (turnorder[x].id == -1 && turnorder[x].custom == params[2]) {
+ turnorder.splice(x, 1);
+ }
+ }
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ if (params[1].toLowerCase() == "findtoken") {
+ var turnorder = JSON.parse(Campaign().get("turnorder"));
+ for (var x = turnorder.length - 1; x >= 0; x--) {
+ if (turnorder[x].id.trim() == params[2].trim()) {
+ stringVariables[variableName] = turnorder[x].pr;
+ //log(`Set variable to ${turnorder[x].pr}`)
+ }
+ }
+ }
+ if (params[1].toLowerCase() == "sort") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ turnorder.sort((a, b) => (Number(a.pr) > Number(b.pr)) ? 1 : ((Number(b.pr) > Number(a.pr)) ? -1 : 0))
+ turnorder.reverse();
+ if ((params[2].toLowerCase().startsWith("a"))) { turnorder.reverse(); }
+ if ((params[2].toLowerCase().startsWith("u"))) { turnorder.reverse(); }
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ }
+ }
+ if (params.length == 4 || params.length == 5 || params.length == 6 || params.length == 7) {
+ if (params[1].toLowerCase() == "addtoken") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ }
+ var custom = params[4] || ""
+ var formula = params[5] || ""
+ var t = getObj('graphic', params[2]);
+ if (t) {
+ turnorder.push({
+ id: params[2],
+ pr: params[3],
+ _pageid: t.get('pageid'),
+ custom: custom,
+ formula: formula
+ });
+ }
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ if (params[1].toLowerCase() == "replacetoken") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ }
+ var wasfound = false;
+ var custom = params[4] || ""
+ var formula = params[5] || ""
+ for (var x = turnorder.length - 1; x >= 0; x--) {
+ if (turnorder[x].id.trim() == params[2].trim()) {
+ turnorder[x].pr = params[3];
+ turnorder[x].custom = custom;
+ turnorder[x].formula = formula;
+ wasfound = true;
+ }
+ }
+ if (!wasfound) {
+ var t = getObj('graphic', params[2]);
+ if (t) {
+ turnorder.push({
+ id: params[2],
+ pr: params[3],
+ _pageid: t.get('pageid'),
+ custom: custom,
+ formula: formula
+ });
+ }
+ }
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ if (params[1].toLowerCase() == "addcustom") {
+ var turnorder = [];
+ if (Campaign().get("turnorder") !== "") {
+ turnorder = JSON.parse(Campaign().get("turnorder"));
+ }
+ var custom = undefined || params[2]
+ var pr = undefined || params[3]
+ var formula = undefined || params[4]
+ var insertionPoint = turnorder.length
+ if (params[5]) {
+ var insertion = params[5] || ""
+ if (insertion !== "") {
+ var foundIndex = -1
+ if (insertion.toLowerCase() == "top") { insertionPoint = 0 }
+ if (insertion.toLowerCase() == "bottom") { insertionPoint = turnorder.length }
+ for (let to = 0; to < turnorder.length; to++) {
+ if (turnorder[to].id == insertion.substring(insertion.indexOf(":") + 1)) {
+ foundIndex = to
+ }
+ }
+ if (foundIndex != -1) {
+ if (insertion.toLowerCase().startsWith("before:") || insertion.toLowerCase().startsWith("above:")) {
+ insertionPoint = foundIndex
+ }
+ if (insertion.toLowerCase().startsWith("after:") || insertion.toLowerCase().startsWith("below:")) {
+ insertionPoint = foundIndex + 1
+ }
+ }
+ }
+ }
+ turnorder.splice(insertionPoint, 0, {
+ id: "-1",
+ pr: pr,
+ _pageid: Campaign().get('playerpageid'),
+ custom: custom,
+ formula: formula,
+ });
+
+ Campaign().set("turnorder", JSON.stringify(turnorder));
+ }
+ }
+ break;
+
+ // Chebyshev Unit distance between two tokens (params[1] and params[2]) (4E/5E)
+ case "chebyshevdistance":
+ case "distance":
+ case "euclideandistance":
+ case "manhattandistance":
+ case "taxicabdistance":
+ var result = 0;
+ if (params.length >= 3) {
+ let token1 = getObj("graphic", params[1]);
+ let token2 = getObj("graphic", params[2]);
+ if (token1 != null && token2 != null) {
+ try {
+ let scale = 1.0;
+ let page = getObj("page", token1.get("_pageid"));
+ if (page) { scale = page.get("snapping_increment") }
+ let t1 = getTokenCoords(token1, scale)
+ let t2 = getTokenCoords(token2, scale)
+ if (params[0].toLowerCase() == "distance" || params[0].toLowerCase() == "chebyshevdistance") {
+ result = Math.floor(Math.max(Math.abs(t1.x - t2.x), Math.abs(t1.y - t2.y)));
+ }
+ if (params[0].toLowerCase() == "euclideandistance") {
+ result = Math.floor(Math.sqrt(Math.pow((t1.x - t2.x), 2) + Math.pow((t1.y - t2.y), 2)));
+ }
+ if (params[0].toLowerCase() == "manhattandistance" || params[0].toLowerCase() == "taxicabdistance") {
+ result = Math.abs(t2.x - t1.x) + Math.abs(t2.y - t1.y);
+ }
+ } catch {
+ result = 0;
+ }
+ }
+ }
+ rollVariables[variableName] = await parseDiceRoll(result.toString(), cardParameters);
+ break;
+
+ case "euclideanpixel":
+ case "euclideanlong":
+ var result = 0;
+ if (params.length >= 3) {
+ var token1 = getObj("graphic", params[1]);
+ var token2 = getObj("graphic", params[2]);
+ if (token1 != null && token2 != null) {
+ try {
+ var scale = 1.0;
+ var page = getObj("page", token1.get("_pageid"));
+ if (page) { scale = page.get("snapping_increment") }
+ // Calculate the euclidean unit distance between two tokens (params[1] and params[2])
+ let t1 = getTokenCoords(token1, (1 / 70))
+ let t2 = getTokenCoords(token2, (1 / 70))
+ result = Math.floor(Math.sqrt(Math.pow((t1.x - t2.x), 2) + Math.pow((t1.y - t2.y), 2)));
+ if (params[0].toLowerCase() == "euclideanlong") { result = result / (scale * 70); }
+ } catch {
+ result = 0;
+ }
+ }
+ }
+ rollVariables[variableName] = await parseDiceRoll(result.toString(), cardParameters);
+ break;
+
+ case "getselected":
+ if (msg.selected) {
+ for (var x = 0; x < msg.selected.length; x++) {
+ var obj = getObj(msg.selected[x]._type, msg.selected[x]._id);
+ stringVariables[variableName + (x + 1).toString()] = obj.get("id");
+ }
+ stringVariables[variableName + "Count"] = msg.selected.length.toString();
+ rollVariables[variableName + "Count"] = await parseDiceRoll(msg.selected.length.toString(), cardParameters);
+ } else {
+ stringVariables[variableName + "Count"] = "0";
+ rollVariables[variableName + "Count"] = await parseDiceRoll("0", cardParameters);
+ }
+ break;
+
+ case "stateitem":
+ if (params.length == 3) {
+ switch (params[1].toLowerCase()) {
+ case "write":
+ if (params[2].toLowerCase() == "rollvariable") {
+ if (rollVariables[variableName]) {
+ state[APINAME].storedRollVariable = Object.assign(rollVariables[variableName]);
+ }
+ }
+ if (params[2].toLowerCase() == "stringvariable") {
+ if (stringVariables[variableName]) {
+ state[APINAME].storedStringVariable = Object.assign(stringVariables[variableName]);
+ }
+ }
+ if (params[2].toLowerCase() == "array") {
+ if (arrayVariables[variableName]) {
+ state[APINAME].storedArrayVariable = Object.assign(arrayVariables[variableName]);
+ state[APINAME].storedArrayIndex = Object.assign(arrayIndexes[variableName]);
+ }
+ }
+ break;
+
+ case "read":
+ if (params[2].toLowerCase() == "rollvariable") {
+ if (state[APINAME].storedRollVariable) { rollVariables[variableName] = Object.assign(state[APINAME].storedRollVariable); }
+ }
+ if (params[2].toLowerCase() == "stringvariable") {
+ if (state[APINAME].storedStringVariable) { stringVariables[variableName] = Object.assign(state[APINAME].storedStringVariable); }
+ }
+ if (params[2].toLowerCase() == "array") {
+ if (state[APINAME].storedArrayVariable) {
+ arrayVariables[variableName] = Object.assign(state[APINAME].storedArrayVariable);
+ arrayIndexes[variableName] = Object.assign(state[APINAME].storedArrayIndex);
+ }
+ }
+ break;
+ }
+ }
+ break;
+
+ case "math": //min,max,clamp,round,floor,ceil
+ case "round":
+ case "range":
+ switch (params[1].toLowerCase()) {
+ case "min":
+ case "max":
+ if (params.length == 4) {
+ let val1 = await parseDiceRoll(params[2], cardParameters);
+ let val2 = await parseDiceRoll(params[3], cardParameters);
+ rollVariables[variableName] = params[1].toLowerCase() == "min" ? (val1.Total <= val2.Total ? val1 : val2) :
+ val1.Total >= val2.Total ? val1 : val2;
+ }
+ break;
+
+ case "abs":
+ if (params.length == 3 && !isNaN(parseFloat((params[2])))) {
+ rollVariables[variableName] = await parseDiceRoll(Math.abs(parseFloat(params[2])), cardParameters)
+ }
+ break;
+
+ case "sqrt":
+ case "squareroot":
+ if (params.length == 3 && !isNaN(parseFloat((params[2])))) {
+ rollVariables[variableName] = await parseDiceRoll(Math.sqrt(parseFloat(params[2])), cardParameters)
+ }
+ break;
+
+ case "clamp":
+ if (params.length == 5) {
+ let val = await parseDiceRoll(params[2], cardParameters);
+ let lower = await parseDiceRoll(params[3], cardParameters);
+ let upper = await parseDiceRoll(params[4], cardParameters);
+ val.Total >= lower.Total && val.Total <= upper.Total ? rollVariables[variableName] = val :
+ val.Total < lower.Total ? rollVariables[variableName] = lower : rollVariables[variableName] = upper;
+ }
+ break;
+ }
+
+ if (params.length == 3) {
+ if (params[1].toLowerCase() == "down" || params[1].toLowerCase() == "floor") {
+ rollVariables[variableName] = await parseDiceRoll(Math.floor(Number(params[2])).toString(), cardParameters);
+ }
+ if (params[1].toLowerCase() == "up" || params[1].toLowerCase() == "ceil") {
+ rollVariables[variableName] = await parseDiceRoll(Math.ceil(Number(params[2])).toString(), cardParameters);
+ }
+ if (params[1].toLowerCase() == "closest" || params[1].toLowerCase() == "round") {
+ rollVariables[variableName] = await parseDiceRoll(Math.round(Number(params[2])).toString(), cardParameters);
+ }
+ }
+
+ if (params.length == 4 && params[1].toLowerCase() == "angle") {
+ var token1 = getObj("graphic", params[2]);
+ var token2 = getObj("graphic", params[3]);
+ if (token1 && token2) {
+ var angle = Math.atan2(token2.get("top") - token1.get("top"), token2.get("left") - token1.get("left"));
+ angle *= 180 / Math.PI;
+ angle -= 270;
+ while (angle < 0) { angle = 360 + angle }
+ stringVariables[variableName] = angle;
+ }
+ }
+ break;
+
+ case "attribute":
+ if (params.length > 4) {
+ if (params[1].toLowerCase() == "set") {
+ var theCharacter = getObj("character", params[2]);
+ if (theCharacter) {
+ var oldAttrs = findObjs({ _type: "attribute", _characterid: params[2], name: params[3].trim() });
+ if (oldAttrs.length > 0) {
+ oldAttrs.forEach(function (element) { element.remove(); });
+ }
+ if (params[4] !== "") {
+ createObj("attribute", { _characterid: params[2], name: params[3].trim(), current: params[4].trim() });
+ }
+ }
+ }
+ }
+ break;
+
+ case "stringfuncs": // strlength, substring, replace, split, before, after
+ case "strings":
+ case "string":
+ if (params.length == 3) {
+ switch (params[1].toLowerCase()) {
+ //stringfuncs;strlength;string
+ case "strlength":
+ case "length":
+ rollVariables[variableName] = await parseDiceRoll((params[2].length.toString()), cardParameters)
+ break;
+
+ case "tolowercase":
+ await setStringOrArrayElement(variableName, params[2].toLowerCase(), cardParameters)
+ break;
+
+ case "touppercase":
+ await setStringOrArrayElement(variableName, params[2].toUpperCase(), cardParameters)
+ break;
+
+ case "striphtml":
+ await setStringOrArrayElement(variableName, params[2].replace(/<[^>]*>?/gm, ''), cardParameters)
+ break;
+
+ case "striplinefeeds":
+ case "linefeedstobr":
+ case "linefeedstobrs":
+ await setStringOrArrayElement(variableName, params[2].replace(/\r?\n/gm, "
"), cardParameters)
+ break;
+
+ case "brtolinefeed":
+ case "brtolinefeeds":
+ case "brstolinefeed":
+ case "brstolinefeeds":
+ await setStringOrArrayElement(variableName, params[2].replace(/
/gi, '\n'), cardParameters)
+ break;
+
+
+ case "trim":
+ await setStringOrArrayElement(variableName, params[2].trim(), cardParameters)
+ break;
+
+ case "onlynumbers":
+ var tempvalue = params[2].trim().startsWith("-") ? "-" : "";
+ tempvalue += params[2].replace(/\D/g, '')
+ await setStringOrArrayElement(variableName, tempvalue, cardParameters)
+ break;
+
+ case "nonumbers":
+ await setStringOrArrayElement(variableName, params[2].replace(/\d/g, ''), cardParameters)
+ break;
+
+ case "totitlecase":
+ await setStringOrArrayElement(variableName,
+ params[2].toLowerCase()
+ .split(' ')
+ .map(function (word) {
+ return (word.charAt(0).toUpperCase() + word.slice(1));
+ })
+ .join(" "),
+ cardParameters);
+ break;
+
+ case "bytes":
+ for (let z = 0; z < stringVariables[params[2]].length; z++) {
+ log(stringVariables[params[2]].charCodeAt(z))
+ }
+ break;
+ }
+ }
+
+ if (params.length == 4) {
+ switch (params[1].toLowerCase()) {
+ //stringfuncs;split;delimeter;string
+ case "split":
+ var splits = params[3].split(params[2]);
+ rollVariables[variableName + "Count"] = await parseDiceRoll(splits.length.toString(), cardParameters);
+ for (var x = 0; x < splits.length; x++) {
+ stringVariables[variableName + (x + 1).toString()] = splits[x];
+ }
+ break;
+
+ //stringfuncs;before;delimiter;string
+ case "before":
+ if (params[3].indexOf(params[2]) < 0) {
+ await setStringOrArrayElement(variableName, params[3], cardParameters)
+ } else {
+ await setStringOrArrayElement(variableName, params[3].substring(0, params[3].indexOf(params[2])), cardParameters);
+ }
+ break;
+
+ //stringfuncs;after;delimeter;string
+ case "after":
+ if (params[3].indexOf(params[2]) < 0) {
+ await setStringOrArrayElement(variableName, params[3], cardParameters)
+ } else {
+ await setStringOrArrayElement(variableName, params[3].substring(params[3].indexOf(params[2]) + params[2].length), cardParameters);
+ }
+ break;
+
+ //stringfuncs;left;count;string
+ case "left":
+ if (params[3].length < Number(params[2])) {
+ await setStringOrArrayElement(variableName, params[3], cardParameters)
+ } else {
+ await setStringOrArrayElement(variableName, params[3].substring(0, Number(params[2])), cardParameters);
+ }
+ break;
+
+ //stringfuncs;right;count;string
+ case "right":
+ if (params[3].length < Number(params[2])) {
+ await setStringOrArrayElement(variableName, params[3], cardParameters)
+ } else {
+ await setStringOrArrayElement(variableName, params[3].substring(params[3].length - Number(params[2])), cardParameters);
+ }
+ break;
+
+ case "stripchars":
+ var str = params[3]
+ for (var i = 0; i < params[2].length; i++) {
+ while (str.includes(params[2].substring(i, i + 1))) {
+ str = str.replace(params[2].substring(i, i + 1), "")
+ }
+ }
+ await setStringOrArrayElement(variableName, str, cardParameters);
+ break;
+
+ }
+ }
+
+ if (params.length == 5) {
+ switch (params[1].toLowerCase()) {
+ //stringfuncs0;substring1;start2;length3;string4
+ case "substring":
+ await setStringOrArrayElement(variableName, params[4].substring(Number(params[2]) - 1, Number(params[3]) + Number(params[2]) - 1), cardParameters);
+ break;
+
+ case "replace":
+ await setStringOrArrayElement(variableName, params[4].replace(params[2], params[3]), cardParameters);
+ break;
+
+ case "replaceall":
+ if (!params[3].includes(params[2])) {
+ var str = params[4];
+ while (str.includes(params[2])) { str = str.replace(params[2], params[3]) }
+ await setStringOrArrayElement(variableName, str, cardParameters);
+ }
+ break;
+ }
+ }
+
+ if (params.length > 2) {
+ switch (params[1].toLowerCase()) {
+ case "replaceencoding":
+ var tempparams = Object.assign(params);
+ tempparams.shift();
+ tempparams.shift();
+ var tempString = tempparams.join();
+ for (let zz = 0; zz < EncodingReplaements.length; zz++) {
+ let f = EncodingReplaements[zz].split(":")[0]
+ let r = EncodingReplaements[zz].split(":")[1]
+ if (f && tempString && r) {
+ tempString = tempString.replaceAll(f.toUpperCase(), r);
+ tempString = tempString.replaceAll(f.toLowerCase(), r);
+ }
+ }
+ if (variableName) { await setStringOrArrayElement(variableName, tempString, cardParameters) }
+ break;
+ }
+ }
+ break;
+
+ case "hashtable":
+ case "hash":
+ if (params.length == 3) {
+ if (params[1].toLowerCase() == "clear") {
+ hashTables[params[2]] = {};
+ }
+ }
+
+ if (params.length > 2) {
+ if (params[1].toLowerCase() == "set") {
+ try {
+ let tableName = params[2];
+ if (hashTables[tableName] === undefined) {
+ hashTables[tableName] = {};
+ }
+ for (var x = 3; x < params.length; x++) {
+ if (params[x].indexOf("==") > 0) {
+ let thisKey = params[x].split("==")[0]
+ let thisValue = params[x].split("==")[1]
+ hashTables[tableName][thisKey] = thisValue
+ }
+ }
+ } catch (e) {
+ log(`ScriptCards: Error encounted: ${e}`)
+ }
+ }
+ if (params[1].toLowerCase() == "fromobject") {
+ try {
+ let tableName = params[2];
+ hashTables[tableName] = {};
+ let theObject = getObj(params[3], params[4])
+ let jsonArray = Object.entries(theObject.attributes);
+ for (let j = 0; j < jsonArray.length; j++) {
+ hashTables[tableName][jsonArray[j][0]] = jsonArray[j][1]
+ }
+ } catch (e) {
+ log(`ScriptCards: Error encounted: ${e}`)
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromjson") {
+ try {
+ let tableName = params[2];
+ hashTables[tableName] = {};
+ let parse = [...params];
+ parse.shift();
+ parse.shift();
+ parse.shift();
+ //let jsonData = parse.join("")
+ //let theObject = JSON.parse(jsonData)
+ //let jsonArray = Object.entries(theObject);
+ //let parts = extractKeyValuePairs(theObject)
+ let parts = extractKeyValuePairsFromJson(parse.join(""))
+ log(parts)
+ for (let j = 0; j < parts.length; j++) {
+ let part = parts[j].split(": ")
+ hashTables[tableName][part[0]] = part[1]
+ }
+ /*
+ for (let j = 0; j < jsonArray.length; j++) {
+ log(jsonArray[j][0] + " Type:" + jsonArray[j][1])
+ if (jsonArray[j][1] == "[object Object]") {
+ log (`***** Object Object *****`)
+ let subArray = Object.entries(jsonArray[j][1])
+ for (let q=0; q -1) { rowValue = "Unsupported (AttrRef)" }
+ if (rowValue.indexOf("[[") > -1) { rowValue = "Unsupported (InlineRoll)" }
+ if (rowValue.indexOf("{{") > -1) { rowValue = "Unsupported (TemplateRef)" }
+ if (!rowValue) { rowValue = "" }
+ hashTables[hashname][rowID + "_" + rowName] = rowValue
+ }
+ hashTables[hashname][rowID + "_sectionid"] = sectionIDs[x]
+
+ }
+ } catch (e) {
+ log(`ScriptCards: Error occured converting repeating section to hash table: ${e}`)
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromrepeatingrow") {
+ try {
+ let charid = params[2]
+ let sectionname = params[3]
+ let sectionID = params[4]
+ let hashname = params[5]
+
+ hashTables[hashname] = {};
+
+ let thisSection = getSectionAttrsByID(charid, sectionname, sectionID)
+ for (let y = 0; y < thisSection.length; y++) {
+ let rowName = thisSection[y].split("|")[0]
+ let rowValue = thisSection[y].split("|")[1]
+ if (rowValue.indexOf("@{") > -1) { rowValue = "Unsupported (AttrRef)" }
+ if (rowValue.indexOf("[[") > -1) { rowValue = "Unsupported (InlineRoll)" }
+ if (rowValue.indexOf("{{") > -1) { rowValue = "Unsupported (TemplateRef)" }
+ if (!rowValue) { rowValue = "" }
+ hashTables[hashname][rowName] = rowValue
+ }
+ } catch (e) {
+ log(`ScriptCards: Error occured converting repeating row to hash table: ${e}`)
+ }
+ }
+
+ if (params[1].toLowerCase() == "getjukeboxtracks") {
+ try {
+ hashTables[params[2]] = {};
+ let tracks = findObjs({ type: 'jukeboxtrack' });
+ for (let j = 0; j < tracks.length; j++) {
+ hashTables[params[2]][tracks[j].get("title")] = tracks[j].get("_id")
+ hashTables[params[2]][tracks[j].get("title") + "-playing"] = tracks[j].get("playing")
+ hashTables[params[2]][tracks[j].get("title") + "-loop"] = tracks[j].get("loop")
+ hashTables[params[2]][tracks[j].get("title") + "-volume"] = tracks[j].get("volume")
+ }
+ } catch (e) {
+ log(`ScriptCards: Error encounted: ${e}`)
+ }
+ }
+ if (params[1].toLowerCase() == "getplayerspecificpages") {
+ try {
+ hashTables[params[2]] = {};
+ let pairs = breakObjectIntoPairs(Campaign().get("playerspecificpages"));
+ for (let j = 0; j < pairs.length; j++) {
+ hashTables[params[2]][pairs[j][0]] = pairs[j][1]
+ }
+ } catch (e) {
+ log(e);
+ }
+ }
+ if (params[1].toLowerCase() == "setplayerspecificpages") {
+ try {
+ var tempArray = [];
+ if (hashTables[params[2]]) {
+ Object.keys(hashTables[params[2]]).forEach(function (key) {
+ let thisTemp = [];
+ thisTemp.push(key)
+ thisTemp.push(hashTables[params[2]][key])
+ tempArray.push(thisTemp);
+ });
+ } else { tempArray = undefined }
+ tempArray ? Campaign().set("playerspecificpages", arrayPairsToObject(tempArray)) : Campaign().set("playerspecificpages", false)
+ } catch (e) { log(e) }
+ }
+ }
+ break;
+
+ case "array":
+ if (params.length > 2) {
+ if (params[1].toLowerCase() == "define") {
+ arrayVariables[params[2]] = [];
+ for (var x = 3; x < params.length; x++) {
+ arrayVariables[params[2]].push(params[x]);
+ }
+ arrayIndexes[params[2]] = 0;
+ }
+ if (params[1].toLowerCase() == "sort") {
+ if (arrayVariables[params[2]]) {
+ arrayVariables[params[2]].sort();
+ if (params[3] != null) {
+ if (params[3].toLowerCase().startsWith("desc")) {
+ arrayVariables[params[2]].reverse();
+ }
+ }
+ }
+ }
+ if (params[1].toLowerCase() == "numericsort") {
+ if (arrayVariables[params[2]]) {
+ arrayVariables[params[2]].sort(function (a, b) { return parseInt(a) - parseInt(b) });
+ if (params[3] != null) {
+ if (params[3].toLowerCase().startsWith("desc")) {
+ arrayVariables[params[2]].reverse();
+ }
+ }
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromrollvar") {
+ // param2 = array, param3 = rollvar, param3 = rolled/kept/dropped
+ try {
+ arrayVariables[params[2]] = [];
+ if (rollVariables[params[3]]) {
+ switch (params[4].toLowerCase()) {
+ case "rolled":
+ arrayVariables[params[2]] = [...rollVariables[params[3]].RolledDice];
+ break;
+ case "kept":
+ arrayVariables[params[2]] = [...rollVariables[params[3]].KeptDice];
+ break;
+ case "dropped":
+ arrayVariables[params[2]] = [...rollVariables[params[3]].DroppedDice];
+ break;
+ }
+ }
+ } catch (e) {
+ log(`ScriptCards: array fromrolleddice error: ${e}`)
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromtable") {
+ arrayVariables[params[2]] = [];
+ var theTable = findObjs({ type: "rollabletable", name: params[3] })[0];
+ if (theTable != null) {
+ var tableItems = findObjs({ type: "tableitem", _rollabletableid: theTable.id });
+ if (tableItems != null) {
+ tableItems.forEach(function (item) {
+ arrayVariables[params[2]].push(item.get("name"))
+ })
+ }
+ if (variableName) { stringVariables[variableName] = arrayVariables[params[2]].length; }
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromhashtablekeys" ||
+ params[1].toLowerCase() == "fromkeys") {
+ arrayVariables[params[2]] = [];
+ if (hashTables[params[3]]) {
+ Object.keys(hashTables[params[3]]).forEach(function (key) {
+ arrayVariables[params[2]].push(key);
+ });
+ }
+ var theTable = findObjs({ type: "rollabletable", name: params[3] })[0];
+ if (theTable != null) {
+ var tableItems = findObjs({ type: "tableitem", _rollabletableid: theTable.id });
+ if (tableItems != null) {
+ tableItems.forEach(function (item) {
+ arrayVariables[params[2]].push(item.get("name"))
+ })
+ }
+ if (variableName) { stringVariables[variableName] = arrayVariables[params[2]].length; }
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromtableweighted") {
+ arrayVariables[params[2]] = [];
+ var theTable = findObjs({ type: "rollabletable", name: params[3] })[0];
+ if (theTable != null) {
+ var tableItems = findObjs({ type: "tableitem", _rollabletableid: theTable.id });
+ if (tableItems != null) {
+ tableItems.forEach(function (item) {
+ for (let w = 0; w < Number(item.get("weight")); w++) {
+ arrayVariables[params[2]].push(item.get("name"))
+ }
+ })
+ }
+ if (variableName) { stringVariables[variableName] = arrayVariables[params[2]].length; }
+ }
+ }
+
+ if (params[1].toLowerCase() == "stringify") {
+ if (arrayVariables[params[2]]) {
+ var sep = cardParameters.parameterdelimiter;
+ if (params[3] != null && params[3] != null) {
+ sep = params[3];
+ }
+ await setStringOrArrayElement(variableName, arrayVariables[params[2]].join(sep), cardParameters);
+ } else {
+ await setStringOrArrayElement(variableName, "", cardParameters);
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromcontrolledcharacters") {
+ arrayVariables[params[2]] = [];
+ var chars = findObjs({ type: "character" });
+ if (chars && chars[0]) {
+ for (let x = 0; x < chars.length; x++) {
+ if (chars[x].get("controlledby").includes(params[3])) {
+ arrayVariables[params[2]].push(chars[x].id)
+ }
+ }
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromplayerlist") {
+ arrayVariables[params[2]] = [];
+ var players = findObjs({ type: "player" });
+ for (let x = 0; x < players.length; x++) {
+ if (!playerIsGM(players[x].id)) {
+ arrayVariables[params[2]].push(players[x].id)
+ }
+ }
+ }
+
+ if (params[1].toLowerCase() == "fromgmplayerlist") {
+ arrayVariables[params[2]] = [];
+ var players = findObjs({ type: "player" });
+ for (let x = 0; x < players.length; x++) {
+ if (playerIsGM(players[x].id)) {
+ arrayVariables[params[2]].push(players[x].id)
+ }
+ }
+ }
+
+ if (params[1].toLowerCase() == "pagetokens") {
+ arrayVariables[params[2]] = [];
+ var pageid = params[3];
+ var templateToken = getObj("graphic", params[3]);
+ var foundTokens = []
+ if (templateToken) {
+ pageid = templateToken.get("_pageid");
+ }
+ if (getObj("page", pageid)) {
+ foundTokens = findObjs({ _type: "graphic", _pageid: pageid });
+ }
+ if (params[4]) {
+ for (let p = 4; p < params.length; p++) {
+ for (let t = foundTokens.length - 1; t >= 0; t--) {
+ if (params[p].toLowerCase() == "char" || params[p].toLowerCase() == "chars") {
+ if (isBlank(foundTokens[t].get("represents"))) {
+ foundTokens.splice(t, 1);
+ }
+ }
+
+ if (params[p].toLowerCase() == "graphic" || params[p].toLowerCase() == "graphics") {
+ if (!isBlank(foundTokens[t].get("represents"))) {
+ foundTokens.splice(t, 1);
+ }
+ }
+
+ if (params[p].toLowerCase() == "npc" || params[p].toLowerCase() == "npcs") {
+ if (isBlank(foundTokens[t].get("represents"))) {
+ foundTokens.splice(t, 1);
+ } else {
+ if (!isBlank(getObj("character", foundTokens[t].get("represents")).get("controlledby"))) {
+ foundTokens.splice(t, 1);
+ }
+ }
+ }
+
+ if (params[p].toLowerCase() == "pc" || params[p].toLowerCase() == "pcs") {
+ if (isBlank(foundTokens[t].get("represents"))) {
+ foundTokens.splice(t, 1);
+ } else {
+ if (isBlank(getObj("character", foundTokens[t].get("represents")).get("controlledby"))) {
+ foundTokens.splice(t, 1);
+ }
+ }
+ }
+
+ if (params[p].toLowerCase().startsWith("attr:") ||
+ params[p].toLowerCase().startsWith("prop:") ||
+ params[p].toLowerCase().startsWith("tprop:")) {
+ var attrFilter = "";
+ var attrValue = "";
+ let subfilter = params[p].toLowerCase().split(":")[0];
+ if (subfilter.indexOf("~") > 0) {
+ subfilter = subfilter.slice(0, -1)
+ }
+ try {
+ if (params[p].indexOf("=") >= 0) {
+ attrFilter = params[p].split(":")[1].split("=")[0];
+ attrValue = (params[p].split(":")[1].split("=")[1]).toLowerCase().trim();
+ }
+ if (params[p].indexOf("~=") >= 0) {
+ subfilter = subfilter + "_partial";
+ attrFilter = params[p].split(":")[1].split("~=")[0];
+ attrValue = (params[p].split(":")[1].split("~=")[1]).toLowerCase().trim();
+ }
+ } catch (e) {
+ log('Incorrect pagetokens attribute filter syntax.')
+ }
+ var charobj = getObj("character", foundTokens[t].get("represents"));
+ if (subfilter.startsWith("tp") || charobj) {
+ switch (subfilter) {
+ case "attr":
+ if (!charobj) {
+ foundTokens.splice(t, 1);
+ } else {
+ try {
+ var attrFind = findObjs({ type: 'attribute', characterid: charobj.id, name: attrFilter })[0]
+ if (!attrFind || attrFind.get('current').toLowerCase().trim() !== attrValue) {
+ foundTokens.splice(t, 1);
+ }
+ } catch (e) {
+ foundTokens.splice(t, 1);
+ }
+ }
+ break;
+
+ case "attr_partial":
+ if (!charobj) {
+ foundTokens.splice(t, 1);
+ } else {
+ try {
+ var attrFind = findObjs({ type: 'attribute', characterid: charobj.id, name: attrFilter })[0]
+ if (!attrFind || attrFind.get('current').toLowerCase().trim().indexOf(attrValue) == -1) {
+ foundTokens.splice(t, 1);
+ }
+ } catch (e) {
+ //
+ }
+ }
+ break;
+
+ case "prop":
+ try {
+ var charobj = getObj("character", foundTokens[t].get("represents"));
+ if (charobj.get(attrFilter).toLowerCase().trim() !== attrValue) {
+ foundTokens.splice(t, 1);
+ }
+ } catch (e) {
+ //
+ }
+ break;
+
+ case "prop_partial":
+ try {
+ var charobj = getObj("character", foundTokens[t].get("represents"));
+ if (charobj.get(attrFilter).toLowerCase().trim().indexOf(attrValue) == -1) {
+ foundTokens.splice(t, 1);
+ }
+ } catch (e) {
+ //
+ }
+ break;
+
+ case "tprop":
+ try {
+ if (foundTokens[t].get(attrFilter).toLowerCase().trim() !== attrValue) {
+ foundTokens.splice(t, 1);
+ }
+ } catch (e) {
+ //
+ }
+ break;
+
+ case "tprop_partial":
+ try {
+ if (foundTokens[t].get(attrFilter).toLowerCase().trim().indexOf(attrValue) == -1) {
+ foundTokens.splice(t, 1);
+ }
+ } catch (e) {
+ //
+ }
+ break;
+ }
+
+ } else {
+ foundTokens.splice(t, 1);
+ }
+ }
+ }
+ }
+ }
+ arrayVariables[params[2]] = [];
+ for (let l = 0; l < foundTokens.length; l++) {
+ arrayVariables[params[2]].push(foundTokens[l].id);
+ }
+ if (foundTokens.length > 0) {
+ arrayIndexes[params[2]] = 0;
+ if (variableName) { stringVariables[variableName] = arrayVariables[params[2]].length; }
+ } else {
+ arrayVariables[params[2]] = [];
+ if (variableName) { stringVariables[variableName] = "0"; }
+ }
+ }
+
+ if (params[1].toLowerCase().startsWith("objects:")) {
+ var details = params[1].split(":");
+ var objects = findObjs({ _type: details[1].toLowerCase() });
+ var lookupField = "name";
+ if (details[1].toLowerCase() == "player") { lookupField = "_displayname"; }
+ if (details[1].toLowerCase() == "jukeboxtrack") { lookupField = "title"; }
+ if (details[1].toLowerCase() == "hand") { lookupField = "_type"; }
+ if (details[1].toLowerCase() == "card") { lookupField = "_type"; }
+ if (details[1].toLowerCase() == "campaign") { lookupField = "_type"; }
+ if (details[1].toLowerCase() == "path") { lookupField = "stroke"; }
+ if (details[1].toLowerCase() == "text") { lookupField = "text"; }
+ arrayVariables[params[2]] = [];
+ for (var x = 0; x < objects.length; x++) {
+ if (params[3] != null) {
+ var okFilter = false || params[3] == "";
+ var okChar = !(params[4] != null) || objects[x].get("characterid") == params[4];
+ if (objects[x].get(lookupField).toLowerCase().startsWith(params[3].toLowerCase())) {
+ okFilter = true;
+ }
+ if (okFilter && okChar) {
+ arrayVariables[params[2]].push(objects[x].get("_id"));
+ }
+ } else {
+ arrayVariables[params[2]].push(objects[x].get("_id"));
+ }
+ }
+ }
+
+ if (params[1].toLowerCase() == "attributes") {
+ // Note P1=attributes, P2=array name, P3=character id, P4=Name Starts with
+ try {
+ var foundAttrs = findObjs({ type: "attribute", characterid: params[3] });
+ if (foundAttrs && foundAttrs[0]) {
+ arrayVariables[params[2]] = []
+ for (let x = 0; x < foundAttrs.length; x++) {
+ if (params[4]) {
+ if (foundAttrs[x].get("name").startsWith(params[4])) {
+ arrayVariables[params[2]].push(foundAttrs[x].id)
+ }
+ } else {
+ arrayVariables[params[2]].push(foundAttrs[x].id)
+ }
+ }
+ }
+ } catch (e) { log(e); }
+ }
+
+ if (params[1].toLowerCase() == "selectedtokens") {
+ if (msg.selected) {
+ arrayVariables[params[2]] = [];
+ for (var x = 0; x < msg.selected.length; x++) {
+ var obj = getObj(msg.selected[x]._type, msg.selected[x]._id);
+ arrayVariables[params[2]].push(obj.get("id"));
+ }
+ arrayIndexes[params[2]] = 0;
+ if (variableName) { stringVariables[variableName] = arrayVariables[params[2]].length; }
+ } else {
+ arrayVariables[params[2]] = [];
+ if (variableName) { stringVariables[variableName] = "0"; }
+ }
+ }
+ if (params[1].toLowerCase() == "statusmarkers") {
+ arrayVariables[params[2]] = [];
+ var theToken = getObj("graphic", params[3]);
+ if (theToken) {
+ var markers = theToken.get("statusmarkers").split(",");
+ for (var x = 0; x < markers.length; x++) {
+ if (markers[x].trim() != "") {
+ arrayVariables[params[2]].push(markers[x]);
+ }
+ }
+ arrayIndexes[params[2]] = 0;
+ }
+ }
+ if (params[1].toLowerCase() == "add") {
+ if (!arrayVariables[params[2]]) { arrayVariables[params[2]] = []; arrayIndexes[params[2]] = 0; }
+ for (var x = 3; x < params.length; x++) {
+ arrayVariables[params[2]].push(params[x]);
+ }
+ }
+ if (params[1].toLowerCase() == "remove") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ for (var x = 3; x < params.length; x++) {
+ for (var i = arrayVariables[params[2]].length - 1; i >= 0; i--) {
+ if (arrayVariables[params[2]][i] == params[x]) {
+ arrayVariables[params[2]].splice(i, 1);
+ }
+ }
+ }
+ }
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length == 0) {
+ delete arrayVariables[params[2]];
+ delete arrayIndexes[params[2]];
+ } else {
+ arrayIndexes[params[2]] = 0;
+ }
+ }
+ if (params[1].toLowerCase() == "removeat") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ if (Number(params[3] < arrayVariables[params[2]].length)) {
+ arrayVariables[params[2]].splice(Number(params[3]), 1);
+ }
+ }
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length == 0) {
+ delete arrayVariables[params[2]];
+ delete arrayIndexes[params[2]];
+ } else {
+ arrayIndexes[params[2]] = 0;
+ }
+ }
+ if (params[1].toLowerCase() == "setindex") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ if (arrayVariables[params[2]].length > Number(params[3])) {
+ arrayIndexes[params[2]] = Number(params[3]);
+ }
+ }
+ }
+
+ if (params[1].toLowerCase() == "getindex") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ stringVariables[variableName] = arrayIndexes[params[2]];
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "indexof") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ var wasFound = arrayVariables[params[2]].indexOf(params[3]);
+ if (wasFound >= 0) {
+ stringVariables[variableName] = wasFound.toString();
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "getlength" || params[1].toLowerCase() == "getcount") {
+ if (arrayVariables[params[2]]) {
+ stringVariables[variableName] = arrayVariables[params[2]].length;
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "getcurrent") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ stringVariables[variableName] = arrayVariables[params[2]][arrayIndexes[params[2]]];
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "getfirst") {
+ if (arrayVariables[params[2]] && arrayVariables[params[2]].length > 0) {
+ arrayIndexes[params[2]] = 0;
+ stringVariables[variableName] = arrayVariables[params[2]][arrayIndexes[params[2]]];
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "getlast") {
+ if (arrayVariables[params[2]]) {
+ arrayIndexes[params[2]] = arrayVariables[params[2]].length - 1;
+ stringVariables[variableName] = arrayVariables[params[2]][arrayIndexes[params[2]]];
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "getnext") {
+ if (arrayVariables[params[2]]) {
+ if (arrayIndexes[params[2]] < arrayVariables[params[2]].length - 1) {
+ arrayIndexes[params[2]]++;
+ stringVariables[variableName] = arrayVariables[params[2]][arrayIndexes[params[2]]];
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+
+ if (params[1].toLowerCase() == "getprevious") {
+ if (arrayVariables[params[2]]) {
+ if (arrayIndexes[params[2]] > 0) {
+ arrayIndexes[params[2]]--;
+ stringVariables[variableName] = arrayVariables[params[2]][arrayIndexes[params[2]]];
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ } else {
+ stringVariables[variableName] = "ArrayError";
+ }
+ }
+ }
+ if (params.length == 5) {
+ if (params[1].toLowerCase() == "replace") {
+ if (arrayVariables[params[2]]) {
+ for (var i = 0; i < arrayVariables[params[2]].length; i++) {
+ if (arrayVariables[params[2]][i] == params[3]) {
+ arrayVariables[params[2]][i] = params[4];
+ }
+ }
+
+ }
+ arrayIndexes[params[2]] = 0;
+ }
+ if (params[1].toLowerCase() == "setatindex") {
+ if (arrayVariables[params[2]]) {
+ var index = Number(params[3]);
+ if (arrayVariables[params[2]].length >= index) {
+ arrayVariables[params[2]][index] = params[4];
+ }
+ }
+ }
+ if (params[1].toLowerCase() == "fromstring") {
+ arrayVariables[params[2]] = [];
+ var splitString = params[4].split(params[3]);
+ for (var x = 0; x < splitString.length; x++) {
+ arrayVariables[params[2]].push(splitString[x]);
+ }
+ arrayIndexes[params[2]] = 0;
+ }
+
+ if (params[1].toLowerCase() == "fromrollabletable" || params[1].toLowerCase() == "fromtable") {
+ // params: 1-fromrollabletable, 2-array name, 3-table name, 4-name or avatar or both)
+ if (params[2] !== "") {
+ arrayVariables[params[2]] = [];
+ var theTable = findObjs({ type: "rollabletable", name: params[3] })[0];
+ if (theTable != null) {
+ findObjs({ type: "tableitem", _rollabletableid: theTable.id }).forEach(function (item) {
+ if (item !== null) {
+ switch (params[4].toLowerCase()) {
+ case "avatar":
+ case "image":
+ arrayVariables[params[2]].push(item.get("avatar"));
+ break;
+
+ case "name":
+ case "text":
+ arrayVariables[params[2]].push(item.get("name"));
+ break;
+
+ case "both":
+ arrayVariables[params[2]].push(`${item.get("name")}|${item.get("avatar")}`)
+ break;
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+ if (params.length == 6) {
+ if (params[1].toLowerCase() == "fromrepeatingsection" || params[1].toLowerCase() == "fromrepsection") {
+ if (params[2] !== "") {
+ try {
+ arrayVariables[params[2]] = [];
+ var pushValue = "";
+ var localSectionIDs = getRepeatingSectionIDs(params[3], params[4]);
+ if (localSectionIDs && localSectionIDs.length > 0) {
+ for (var x = 0; x < localSectionIDs.length; x++) {
+ var thisRepeatingSection = getSectionAttrsByID(params[3], params[4], localSectionIDs[x]);
+ pushValue = "";
+ for (var q = 0; q < thisRepeatingSection.length; q++) {
+ if (thisRepeatingSection[q].split("|")[0] == params[5]) {
+ pushValue = thisRepeatingSection[q].split("|")[1];
+ }
+ }
+ arrayVariables[params[2]].push(pushValue);
+ }
+ }
+ } catch {
+ arrayVariables[params[2]] = [];
+ }
+ }
+ }
+ }
+ if (params.length == 7) {
+ if (params[1].toLowerCase() == "fullrepeatingsection" || params[1].toLowerCase() == "fullrepsection") {
+ if (params[2] !== "") {
+ try {
+ arrayVariables[params[2]] = [];
+ var pushValue = "";
+ var localSectionIDs = getRepeatingSectionIDs(params[3], params[4]);
+ var attrList = params[5].split(":");
+ if (localSectionIDs && localSectionIDs.length > 0) {
+ for (var x = 0; x < localSectionIDs.length; x++) {
+ pushValue = [];
+ var thisRepeatingSection = getSectionAttrsByID(params[3], params[4], localSectionIDs[x]);
+ for (var y = 0; y < attrList.length; y++) {
+ var found = false
+ for (var q = 0; q < thisRepeatingSection.length; q++) {
+ if (thisRepeatingSection[q].split("|")[0] == attrList[y]) {
+ if (thisRepeatingSection[q].split("|")[1] != null) {
+ pushValue.push(thisRepeatingSection[q].split("|")[1]);
+ found = true
+ } else {
+ pushValue.push("");
+ }
+
+ }
+ }
+ if (!found) { pushValue.push(""); }
+ }
+ arrayVariables[params[2]].push(pushValue.join(params[6]));
+ }
+ }
+ } catch {
+ arrayVariables[params[2]] = [];
+ }
+ }
+ }
+ }
+ break;
+
+ case "object":
+ if ((params[1].toLowerCase() == "token" || params[1].toLowerCase() == "graphic") &&
+ (params[2].toLowerCase() == "remove" || params[2].toLowerCase() == "delete")) {
+ for (let x = 3; x < params.length; x++) {
+ var tokenID = params[x].trim();
+ try {
+ var theToken = getObj("graphic", tokenID);
+ if (theToken) {
+ theToken.remove();
+ log(`ScriptCards: Token ${tokenID} removed by user ${stringVariables["SendingPlayerID"]} (${stringVariables["SendingPlayerName"]}).`);
+ }
+ } catch (e) {
+ log(e);
+ }
+ }
+ }
+ break;
+ }
+ } catch (e) {
+ log(`Error executing function ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleRollVariableSetCommand(thisTag, thisContent, cardParameters) {
+ try {
+ var rollIDName = thisTag.substring(1).trim();
+ if (rollIDName.indexOf('.') == -1) {
+ rollVariables[rollIDName] = await parseDiceRoll(await replaceVariableContent(thisContent, cardParameters), cardParameters, true);
+ } else {
+ var parts = rollIDName.split(".");
+ if (parts[0] && rollVariables[parts[0]]) {
+ if (parts[1] && rollVariables[parts[0]][parts[1]]) {
+ rollVariables[parts[0]][parts[1]] = await replaceVariableContent(thisContent, cardParameters);
+ }
+ }
+ }
+ } catch (e) {
+ log(`Error setting roll variable ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ function handleRepeatingAttributeCommands(thisTag, thisContent, cardParameters) {
+ try {
+ var command = thisTag.substring(1).toLowerCase();
+ var param = thisContent.split(cardParameters.parameterdelimiter);
+ //log(`Processing Repeating Section Command: ${command}, Params: ${param}`)
+ switch (command.toLowerCase()) {
+ case "find":
+ case "search":
+ var fuzzy = (command.toLowerCase() == "search" ? true : false)
+ repeatingSection = getSectionAttrs(param[0], param[1], param[2], param[3], fuzzy);
+ fillCharAttrs(findObjs({ _type: 'attribute', _characterid: param[0] }));
+ repeatingCharID = param[0];
+ repeatingSectionName = param[2];
+ if (repeatingSection && repeatingSection[0]) {
+ repeatingSectionIDs = [];
+ repeatingSectionIDs.push(repeatingSection[0].split("|")[1]);
+ repeatingIndex = 0;
+ } else {
+ repeatingSectionIDs = [];
+ repeatingSectionIDs[0] = "NoRepeatingAttributeLoaded";
+ repeatingIndex = 0;
+ }
+ break;
+ case "first":
+ repeatingSectionIDs = getRepeatingSectionIDs(param[0], param[1]);
+ if (repeatingSectionIDs) {
+ repeatingIndex = 0;
+ repeatingCharID = param[0];
+ repeatingSectionName = param[1];
+ fillCharAttrs(findObjs({ _type: 'attribute', _characterid: repeatingCharID }));
+ repeatingSection = getSectionAttrsByID(repeatingCharID, repeatingSectionName, repeatingSectionIDs[repeatingIndex]);
+ repeatingIndex = 0;
+ } else {
+ repeatingSection = undefined;
+ }
+ break;
+ case "byindex":
+ if (param[0] && param[1] && param[2]) {
+ repeatingSectionIDs = getRepeatingSectionIDs(param[0], param[1]);
+ if (repeatingSectionIDs) {
+ repeatingIndex = Number(param[2]);
+ repeatingCharID = param[0];
+ repeatingSectionName = param[1];
+ fillCharAttrs(findObjs({ _type: 'attribute', _characterid: repeatingCharID }));
+ repeatingSection = getSectionAttrsByID(repeatingCharID, repeatingSectionName, repeatingSectionIDs[repeatingIndex]);
+ repeatingIndex = Number(param[2]);
+ } else {
+ repeatingSection = undefined;
+ }
+ } else {
+ repeatingSection = undefined;
+ }
+ break;
+ case "bysectionid":
+ if (param[0] && param[1] && param[2]) {
+ let charID = param[0]
+ let secName = param[1]
+ var ci = false
+ if (param[3] && (param[3] == "1" || param[3].toLowerCase() == "i")) {
+ ci = true
+ }
+ repeatingSectionIDs = getRepeatingSectionIDs(charID, secName);
+ if (repeatingSectionIDs) {
+ repeatingIndex = undefined;
+ for (let x = 0; x < repeatingSectionIDs.length; x++) {
+ if (repeatingSectionIDs[x].trim() == param[2].trim()) {
+ repeatingIndex = x;
+ }
+ if (ci && repeatingSectionIDs[x].toLowerCase().trim() == param[2].toLowerCase().trim()) {
+ repeatingIndex = x;
+ }
+ }
+ repeatingCharID = charID;
+ repeatingSectionName = secName;
+ fillCharAttrs(findObjs({ _type: 'attribute', _characterid: repeatingCharID }));
+ repeatingSection = getSectionAttrsByID(repeatingCharID, repeatingSectionName, repeatingSectionIDs[repeatingIndex]);
+ } else {
+ repeatingSection = undefined;
+ }
+ } else {
+ repeatingSection = undefined;
+ }
+ break;
+ case "next":
+ if (repeatingSectionIDs) {
+ if (repeatingSectionIDs[repeatingIndex + 1]) {
+ repeatingIndex++;
+ repeatingSection = getSectionAttrsByID(repeatingCharID, repeatingSectionName, repeatingSectionIDs[repeatingIndex]);
+ } else {
+ repeatingSection = undefined;
+ repeatingSectionIDs = undefined;
+ }
+ } else {
+ repeatingSection = undefined;
+ repeatingSectionIDs = undefined;
+ }
+ break;
+ case "dump":
+ if (repeatingSection) {
+ for (var x = 0; x < repeatingSection.length; x++) {
+ log(repeatingSection[x]);
+ }
+ }
+ break;
+ }
+ } catch (e) {
+ log(`Error processing Repeating Section command ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleConditionalBlock(thisTag, thisContent, cardParameters, cardLines) {
+ try {
+ var isTrue = await processFullConditional(thisTag.substring(1));
+ var trueDest = thisContent.trim();
+ var falseDest = undefined;
+ var varName = undefined;
+ var varValue = undefined;
+ var resultType = "goto";
+ if (trueDest.indexOf("|") >= 0) {
+ falseDest = trueDest.split("|")[1].trim();
+ trueDest = trueDest.split("|")[0].trim();
+ }
+ if (cardParameters.debug == 1) { log(`Condition ${thisTag.substring(1)} evaluation result: ${isTrue}`); }
+ var jumpDest = isTrue ? trueDest : falseDest;
+ var blockSkip = false;
+ var blockChar = "]";
+ if (falseDest == "[" || trueDest == "]") {
+ blockDepth++
+ }
+ if (isTrue && falseDest == "[") { blockSkip = true; }
+ if (!isTrue && trueDest == "[") { blockSkip = true; }
+ if (jumpDest) {
+ switch (jumpDest.charAt(0)) {
+ case ">": resultType = "gosub"; break;
+ case "<": resultType = "return"; break;
+ case "%": resultType = "next"; break;
+ case "[": resultType = "block"; break;
+ case "+": resultType = "directoutput"; break;
+ case "*": resultType = "gmoutput"; break;
+ case "=":
+ case "&":
+ jumpDest.charAt(0) == "=" ? resultType = "rollset" : resultType = "stringset";
+ jumpDest = jumpDest.substring(1);
+ varName = jumpDest.split(cardParameters.parameterdelimiter)[0];
+ varValue = jumpDest.split(cardParameters.parameterdelimiter)[1];
+ break;
+ }
+
+ switch (resultType) {
+ case "goto":
+ if (lineLabels[jumpDest]) {
+ lineCounter = lineLabels[jumpDest];
+ } else {
+ log(`ScriptCards Error: Label ${jumpDest} is not defined on line ${lineCounter} (${thisTag}, ${thisContent})`);
+ }
+ break;
+ case "return":
+ arrayVariables["args"] = []
+ if (returnStack.length > 0) {
+ arrayVariables["args"] = [];
+ callParamList = parameterStack.pop();
+ if (callParamList) {
+ for (const value of Object.values(callParamList)) {
+ arrayVariables["args"].push(value.toString().trim());
+ }
+ }
+ lineCounter = returnStack.pop();
+ }
+ break;
+ case "directoutput":
+ case "gmoutput":
+ if (jumpDest.split(";") != null) {
+ var conditionalTag = jumpDest.split(";")[0];
+ var conditionalContent = jumpDest.substring(jumpDest.indexOf(";") + 1);
+ if (jumpDest.indexOf(";") < 0) {
+ conditionalContent = "";
+ }
+ var rowData = buildRowOutput(conditionalTag.substring(1), await replaceVariableContent(conditionalContent, cardParameters, true), cardParameters.outputtagprefix, cardParameters.outputcontentprefix);
+
+ tableLineCounter += 1;
+ if (tableLineCounter % 2 == 0) {
+ while (rowData.indexOf("=X=FONTCOLOR=X=") > 0) { rowData = rowData.replace("=X=FONTCOLOR=X=", cardParameters.evenrowfontcolor); }
+ while (rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.evenrowbackground}; background-image: ${cardParameters.evenrowbackgroundimage}; `); }
+ } else {
+ while (rowData.indexOf("=X=FONTCOLOR=X=") > 0) { rowData = rowData.replace("=X=FONTCOLOR=X=", cardParameters.oddrowfontcolor); }
+ while (rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.oddrowbackground}; background-image: ${cardParameters.oddrowbackgroundimage}; `); }
+ }
+ rowData = processInlineFormatting(rowData, cardParameters, false);
+ if (resultType == "directoutput") {
+ outputLines.push(rowData);
+ } else {
+ gmonlyLines.push(rowData);
+ }
+ }
+ break;
+ case "gosub":
+ jumpDest = jumpDest.substring(1);
+ parameterStack.push(callParamList);
+ var paramList = CSVtoArray(jumpDest.trim());
+ callParamList = {};
+ var paramCount = 0;
+ arrayVariables["args"] = []
+ if (paramList) {
+ paramList.forEach(function (item) {
+ callParamList[paramCount] = item.toString().trim();
+ arrayVariables["args"].push(item.toString().trim());
+ paramCount++;
+ });
+ }
+ returnStack.push(lineCounter);
+ jumpDest = jumpDest.split(cardParameters.parameterdelimiter)[0];
+ if (lineLabels[jumpDest]) {
+ lineCounter = lineLabels[jumpDest];
+ } else {
+ log(`ScriptCards Error: Label ${jumpDest} is not defined on line ${lineCounter} (${thisTag}, ${thisContent})`);
+ }
+ break;
+ case "rollset":
+ rollVariables[varName] = await parseDiceRoll(await replaceVariableContent(varValue, cardParameters, false), cardParameters);
+ break;
+ case "stringset":
+ if (varName) {
+ await setStringOrArrayElement(varName, varValue, cardParameters);
+ } else {
+ log(`ScriptCards Error: Variable name or value not specified in conditional on line ${lineCounter} (${thisTag}) ${thisContent}`);
+ }
+ break;
+ case "next":
+ if (loopStack.length >= 1) {
+ var currentLoop = loopStack[loopStack.length - 1];
+ var breakLoop = false;
+ if (loopControl[currentLoop]) {
+ loopControl[currentLoop].current += loopControl[currentLoop].step;
+ switch (loopControl[currentLoop].loopType) {
+ case "fornext":
+ stringVariables[currentLoop] = loopControl[currentLoop].current.toString();
+ break;
+ case "foreach":
+ if (jumpDest.charAt(1) !== "!") {
+ try {
+ stringVariables[currentLoop] = arrayVariables[loopControl[currentLoop].arrayName][loopControl[currentLoop].current]
+ } catch {
+ stringVariables[currentLoop] = "ArrayError"
+ }
+ }
+ break;
+ case "while":
+ case "until":
+ breakLoop = true;
+ break;
+ }
+ if ((loopControl[currentLoop].step > 0 && loopControl[currentLoop].current > loopControl[currentLoop].end) ||
+ (loopControl[currentLoop].step < 0 && loopControl[currentLoop].current < loopControl[currentLoop].end) ||
+ jumpDest.charAt(1) == "!" || breakLoop) {
+ loopStack.pop();
+ delete loopControl[currentLoop];
+ blockSkip = true;
+ blockChar = "%";
+ } else {
+ lineCounter = loopControl[currentLoop].nextIndex;
+ }
+ }
+ }
+ break;
+ case "block":
+ lastBlockAction = "E";
+ break;
+ }
+ }
+ if (blockSkip) {
+ var line = lineCounter;
+ if (blockChar === "]") { lastBlockAction = "S"; }
+ for (line = lineCounter + 1; line < cardLines.length; line++) {
+ if (getLineTag(cardLines[line], line, "").trim() == blockChar) {
+ lineCounter = line + (blockChar == "]" ? 0 : 0);
+ break;
+ }
+ }
+ if (lineCounter > cardLines.length) {
+ log(`ScriptCards: Warning - no end block marker found for block started reference on line ${lineCounter}`);
+ lineCounter = cardLines.length + 1;
+ }
+ }
+ } catch (e) {
+ log(`Error processing conditional ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+ }
+
+ async function handleCaseCommand(thisTag, thisContent, cardParameters, cardLines) {
+ try {
+ var testvalue = thisTag.substring(1);
+ var cases = thisContent.split(/(?": resultType = "gosub"; break;
+ case "<": resultType = "return"; break;
+ case "%": resultType = "next"; break;
+ case "+": resultType = "directoutput"; break;
+ case "*": resultType = "gmoutput"; break;
+ case "=":
+ case "&":
+ jumpDest.charAt(0) == "=" ? resultType = "rollset" : resultType = "stringset";
+ jumpDest = jumpDest.substring(1);
+ varName = jumpDest.split(cardParameters.parameterdelimiter)[0];
+ varValue = jumpDest.split(cardParameters.parameterdelimiter)[1];
+ break;
+ }
+
+ switch (resultType) {
+ case "goto":
+ if (lineLabels[jumpDest]) {
+ lineCounter = lineLabels[jumpDest];
+ } else {
+ log(`ScriptCards Error: Label ${jumpDest} is not defined on line ${lineCounter} (${thisTag}, ${thisContent})`);
+ }
+ break;
+ case "return":
+ arrayVariables["args"] = []
+ if (returnStack.length > 0) {
+ callParamList = parameterStack.pop();
+ if (callParamList) {
+ for (const value of Object.values(callParamList)) {
+ arrayVariables["args"].push(value.toString().trim());
+ }
+ }
+ lineCounter = returnStack.pop();
+ }
+ break;
+ case "directoutput":
+ case "gmoutput":
+ if (jumpDest.split(";") != null) {
+ var conditionalTag = jumpDest.split(";")[0];
+ var conditionalContent = jumpDest.substring(jumpDest.indexOf(";") + 1);
+ if (jumpDest.indexOf(";") < 0) {
+ conditionalContent = "";
+ }
+ var rowData = buildRowOutput(conditionalTag.substring(1), await replaceVariableContent(conditionalContent, cardParameters, true), cardParameters.outputtagprefix, cardParameters.outputcontentprefix);
+
+ tableLineCounter += 1;
+ if (tableLineCounter % 2 == 0) {
+ while (rowData.indexOf("=X=FONTCOLOR=X=") > 0) { rowData = rowData.replace("=X=FONTCOLOR=X=", cardParameters.evenrowfontcolor); }
+ while (rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.evenrowbackground}; background-image: ${cardParameters.evenrowbackgroundimage}; `); }
+ } else {
+ while (rowData.indexOf("=X=FONTCOLOR=X=") > 0) { rowData = rowData.replace("=X=FONTCOLOR=X=", cardParameters.oddrowfontcolor); }
+ while (rowData.indexOf("=X=ROWBG=X=") > 0) { rowData = rowData.replace("=X=ROWBG=X=", ` background: ${cardParameters.oddrowbackground}; background-image: ${cardParameters.oddrowbackgroundimage}; `); }
+ }
+
+ rowData = processInlineFormatting(rowData, cardParameters, false);
+ if (resultType == "directoutput") {
+ outputLines.push(rowData);
+ } else {
+ gmonlyLines.push(rowData);
+ }
+ }
+ break;
+ case "gosub":
+ jumpDest = jumpDest.substring(1);
+ parameterStack.push(callParamList);
+ var paramList = CSVtoArray(jumpDest.trim());
+ callParamList = {};
+ var paramCount = 0;
+ arrayVariables["args"] = []
+ if (paramList) {
+ paramList.forEach(function (item) {
+ callParamList[paramCount] = item.toString().trim();
+ arrayVariables["args"].push(item.toString().trim());
+ paramCount++;
+ });
+ }
+ returnStack.push(lineCounter);
+ jumpDest = jumpDest.split(cardParameters.parameterdelimiter)[0];
+ if (lineLabels[jumpDest]) {
+ lineCounter = lineLabels[jumpDest];
+ } else {
+ log(`ScriptCards Error: Label ${jumpDest} is not defined on line ${lineCounter} (${thisTag}, ${thisContent})`);
+ }
+ break;
+ case "rollset":
+ rollVariables[varName] = await parseDiceRoll(await replaceVariableContent(varValue, cardParameters), cardParameters, true);
+ break;
+ case "stringset":
+ if (varName) {
+ await setStringOrArrayElement(varName, varValue, cardParameters);
+ } else {
+ log(`ScriptCards Error: Variable name or value not specified in conditional on line ${lineCounter} (${thisTag}) ${thisContent}`);
+ }
+ break;
+ case "next":
+ if (loopStack.length >= 1) {
+ var currentLoop = loopStack[loopStack.length - 1];
+ var breakLoop = false;
+ if (loopControl[currentLoop]) {
+ loopControl[currentLoop].current += loopControl[currentLoop].step;
+ switch (loopControl[currentLoop].loopType) {
+ case "fornext":
+ stringVariables[currentLoop] = loopControl[currentLoop].current.toString();
+ break;
+ case "foreach":
+ if (jumpDest.charAt(1) !== "!") {
+ try {
+ stringVariables[currentLoop] = arrayVariables[loopControl[currentLoop].arrayName][loopControl[currentLoop].current]
+ } catch {
+ stringVariables[currentLoop] = "ArrayError"
+ }
+ }
+ break;
+ case "while":
+ case "until":
+ breakLoop = true;
+ break;
+ }
+ if ((loopControl[currentLoop].step > 0 && loopControl[currentLoop].current > loopControl[currentLoop].end) ||
+ (loopControl[currentLoop].step < 0 && loopControl[currentLoop].current < loopControl[currentLoop].end) ||
+ jumpDest.charAt(1) == "!" || breakLoop) {
+ loopStack.pop();
+ delete loopControl[currentLoop];
+ blockSkip = true;
+ blockChar = "%";
+ } else {
+ lineCounter = loopControl[currentLoop].nextIndex;
+ }
+ }
+ }
+ break;
+ }
+ x = cases.length + 1;
+ }
+ }
+ }
+ }
+ if (blockSkip) {
+ var line = lineCounter;
+ for (line = lineCounter + 1; line < cardLines.length; line++) {
+ if (getLineTag(cardLines[line], line, "").trim() == blockChar) {
+ lineCounter = line;
+ break;
+ }
+ }
+ if (lineCounter > cardLines.length) {
+ log(`ScriptCards: Warning - no end block marker found for block started reference on line ${lineCounter}`);
+ lineCounter = cardLines.length + 1;
+ }
+ }
+ } catch (e) {
+ log(`Error processing case statement ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+ }
+
+ }
+
+ function getSafeTokenProperty(propName, propValue) {
+ let ret = propValue;
+
+ // Convert numeric properties
+ const numericProps = [
+ "left", "top", "width", "height", "light_radius", "light_dimradius",
+ "light_angle", "light_losangle", "light_multiplier", "adv_fow_view_distance",
+ "light_sensitivity_radius", "rotation"
+ ];
+ if (numericProps.includes(propName)) {
+ let numValue = Number(propValue);
+ if (isNaN(numValue)) {
+ numValue = parseFloat(propValue);
+ }
+ if (isNaN(numValue)) {
+ numValue = 0;
+ }
+ return numValue;
+ }
+
+ // Convert boolean properties
+ const booleanProps = [
+ "isdrawing", "flipv", "fliph", "aura1_square", "aura2_square", "showname",
+ "showplayers_name", "showplayers_bar1", "showplayers_bar2", "showplayers_bar3",
+ "showplayers_aura1", "showplayers_aura2", "playersedit_name", "playersedit_bar1",
+ "playersedit_bar2", "playersedit_bar3", "playersedit_aura1", "playersedit_aura2",
+ "light_otherplayers", "light_hassight", "lockmovement"
+ ];
+ if (booleanProps.includes(propName)) {
+ return ["true", "yes", "on", "1"].includes(propValue.toLowerCase());
+ }
+
+ // Clean image source
+ if (propName === "imgsrc") {
+ return getCleanImgsrc(propValue);
+ }
+
+ // Clean sides property
+ if (propName === "sides") {
+ const sides = propValue.split("|").map(side => getCleanImgsrc(side));
+ return sides.join("|");
+ }
+
+ return ret;
+ }
+
+ function reportBenchmarkingData() {
+ log(benchmarks);
+ let difference = Date.now() - scriptStartTimestamp
+ log(`Script Execution Time: ${difference} ms`)
+ log(`Executed script lines: ${executionCounter}`)
+ }
+
+ /*
+ const getNotes = function (prop, obj) {
+ return new Promise((resolve, reject) => {
+ obj.get(prop, (p) => {
+ resolve(p);
+ });
+ });
+ };
+ */
+
+ /*
+ function getSafeTriggerString(prefix, property) {
+ try {
+ let prop = property
+ prop == null ? prop = "" : prop = prop.toString()
+ if (prop.toString().indexOf("--") < 0) {
+ return ` --&${prefix}|${prop} `
+ }
+ let split = prop.split("--")
+ let ret = ""
+ for (let x = 0; x < split.length; x++) {
+ ret += ` --&${prefix}|${x == 0 ? "" : "+-"}${split[x]}${x < (split.length - 1) ? "-" : ""}`
+ }
+ return ret;
+ } catch (e) {
+ log(`Error creating safe trigger string: ${e.message}`)
+ }
+ }
+ */
+
+ function getSafeTriggerString(prefix, property) {
+ try {
+ let prop = property ? property.toString() : "";
+
+ if (!prop.includes("--")) {
+ return ` --&${prefix}|${prop} `;
+ }
+
+ const split = prop.split("--");
+ let result = split.map((part, index) => {
+ const separator = index === 0 ? "" : "+-";
+ const suffix = index < split.length - 1 ? "-" : "";
+ return ` --&${prefix}|${separator}${part}${suffix}`;
+ }).join("");
+
+ return result;
+ } catch (error) {
+ log(`Error creating safe trigger string: ${error.message}`);
+ return "";
+ }
+ }
+
+ return {
+ ObserveTokenChange: observeTokenChange
+ };
+
+ function breakObjectIntoPairs(obj) {
+ return Object.entries(obj);
+ }
+
+ function arrayPairsToObject(arrayPairs) {
+ if (!arrayPairs) {
+ return undefined;
+ }
+
+ const obj = {};
+ arrayPairs.forEach(pair => {
+ const [key, value] = pair;
+ obj[key] = value;
+ });
+
+ return obj;
+ }
+
+ function getTokenCoords(token, scale) {
+ try {
+ const left = token.get("left");
+ const top = token.get("top");
+ const scaledLeft = left / (scale * 70);
+ const scaledTop = top / (scale * 70);
+ //log(`Token ID: ${token.id}, Scale: ${scale}, Left: ${left}, Top: ${top}, Scaled Left: ${scaledLeft}, Scaled Top: ${scaledTop}`);
+
+ return { x: scaledLeft, y: scaledTop };
+ } catch (error) {
+ log(`Error in getTokenCoords for token ID ${token.id}: ${error.message}`);
+ return { x: 0, y: 0 };
+ }
+ }
+
+ function onChangeCampaignTurnorder(triggerCharID) {
+ try {
+ const abilities = findObjs({ type: "ability", _characterid: triggerCharID, name: "change:campaign:turnorder" });
+ if (Array.isArray(abilities) && abilities.length > 0) {
+ const replacement = ` `;
+ abilities.forEach(ability => {
+ const action = ability.get("action");
+ const metacard = action.replace("--/|TRIGGER_REPLACEMENTS", replacement);
+ sendChat("API", metacard);
+ });
+ } else {
+ log(`No abilities found for character ID: ${triggerCharID}`);
+ }
+ } catch (error) {
+ log(`Error in onChangeCampaignTurnorder: ${error.message}`);
+ }
+ }
+
+ function checkForMessageTriggers(triggerCharID) {
+ const abilities = findObjs({ type: "ability", _characterid: triggerCharID });
+ if (Array.isArray(abilities) && abilities.length > 0) {
+ for (let x = 0; x < abilities.length; x++) {
+ let abname = abilities[x].get("name")
+ if (abname) {
+ if (abname.startsWith("chat:message:")) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ function onChatMessagTrigger(triggerCharID, msg) {
+ if (msg && msg.content && msg.content.indexOf("SC_TRIGGER_GENERATED") < 0) {
+ const abilities = findObjs({ type: "ability", _characterid: triggerCharID });
+ if (Array.isArray(abilities) && abilities.length > 0) {
+ for (let x = 0; x < abilities.length; x++) {
+ let abname = abilities[x].get("name")
+ if (abname) {
+ if (abname.startsWith("chat:message:")) {
+ let testVal = abname.replace("chat:message:", "").replaceAll("-", " ");
+ if (msg.content.replace("-", " ").indexOf(testVal) >= 0) {
+ replacement = " --+| "
+ replacement += `--&TriggerWho|${msg.who} `;
+ replacement += `--&TriggerPlayerID|${msg.playerid} `;
+ replacement += `--&TriggerType|${msg.type} `;
+ replacement += `--&TriggerContent|${msg.content}`;
+ const action = abilities[x].get("action");
+ if (action.indexOf("--/|TRIGGER_REPLACEMENTS") >= 0) {
+ const metacard = action.replace("--/|TRIGGER_REPLACEMENTS", replacement);
+ sendChat("API", metacard);
+ //log(msg)
+ } else {
+ log(`ScriptCards Error : message-based triggers MUST be ScriptCards with a replacement section`)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ async function getBioField(charobj, field) {
+ return new Promise((resolve) => {
+ charobj.get(field, function (resp) {
+ resolve(resp);
+ })
+ })
+ }
+
+ function extractKeyValuePairsFromJson(jsonString) {
+ let obj;
+ try {
+ obj = JSON.parse(jsonString);
+ } catch (e) {
+ console.error("Invalid JSON string provided:", e);
+ return [];
+ }
+
+ return extractKeyValuePairs(obj);
+ }
+
+ function extractKeyValuePairs(obj, prefix = '') {
+ let result = [];
+
+ for (const [key, value] of Object.entries(obj)) {
+ const newKey = prefix ? `${prefix}_${key}` : key;
+
+ if (Array.isArray(value)) {
+ value.forEach((item, index) => {
+ const arrayKey = `${newKey}_${index}`;
+ if (typeof item === 'object' && item !== null) {
+ result = result.concat(extractKeyValuePairs(item, arrayKey));
+ } else {
+ result.push(`${arrayKey}: ${item}`);
+ }
+ });
+ } else if (typeof value === 'object' && value !== null) {
+ result = result.concat(extractKeyValuePairs(value, newKey));
+ } else {
+ result.push(`${newKey}: ${value}`);
+ }
+ }
+
+ return result;
+ }
+
+})();
+
+// log(`Error setting z-order ${e.message}, thisTag: ${thisTag}, thisContent: ${thisContent}`)
+
+// Meta marker for the end of ScriptCards
+{ try { throw new Error(''); } catch (e) { API_Meta.ScriptCards.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.ScriptCards.offset); } }
+
+// Support for AirBag Crash Handler (if installed)
+if (typeof MarkStop === "function") MarkStop('ScriptCards');
\ No newline at end of file
diff --git a/ScriptCards/script.json b/ScriptCards/script.json
index a71efe8da..dc5b16b34 100644
--- a/ScriptCards/script.json
+++ b/ScriptCards/script.json
@@ -1,8 +1,8 @@
{
"name": "ScriptCards",
"script": "scriptcards.js",
- "version": "3.0.0",
- "previousversions": ["2.7.35","2.7.31","2.7.27","2.7.25","2.7.10","2.7.8","2.6.6","2.6.2","2.4.9","2.4.3","2.3.9","2.3.5","2.3.4","2.2.9","2.2.4","2.2.0","2.1.19","2.1.11","2.1.7","2.0.2","1.7.7","1.7.6","1.6.4","1.6.2","1.4.10","1.4.0","1.3.9","1.3.1","1.1.19","1.0.9","1.0.0","0.0.15","0.0.11","0.0.5"],
+ "version": "3.0.01",
+ "previousversions": ["3.0.0","2.7.35","2.7.31","2.7.27","2.7.25","2.7.10","2.7.8","2.6.6","2.6.2","2.4.9","2.4.3","2.3.9","2.3.5","2.3.4","2.2.9","2.2.4","2.2.0","2.1.19","2.1.11","2.1.7","2.0.2","1.7.7","1.7.6","1.6.4","1.6.2","1.4.10","1.4.0","1.3.9","1.3.1","1.1.19","1.0.9","1.0.0","0.0.15","0.0.11","0.0.5"],
"description": "**ScriptCards** allows you to create powerful macros utilizing a built-in scripting language to output nicely formatted display cards for PC and NPC actions, spells, abilities, statistics, or almost any other purpose you can come up with.\n\n Please see the Roll20 API Scripts WIKI for detailed documentation, and the Roll20 API Forum Thread for the most recent details and discussion\n\nScriptCards was designed as a spiritual successor to the PowerCards API script, and addresses some of the often requested abilities that the structure of PowerCards precludes, such as true variables, looping and branching, and the ability to make formatting changes based on conditional results. The default output format for ScriptCards is similar to the power blocks from the official D&D 4E products, but the format works well for any system. ScriptCards are output to the chat window with full HTML/CSS formatting. There is no game-system specific requirements for ScriptCards.",
"authors": "Kurt Jaegers",
"patreon": "https://www.patreon.com/KurtJaegers",