From d75d29fc74806fdcd5c028db875052019f5f4e29 Mon Sep 17 00:00:00 2001
From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com>
Date: Wed, 20 Nov 2024 23:28:56 -0700
Subject: [PATCH 1/3] Version 1.3.1 adds compatability with the sheet
HeroSystem6eHeroic.
---
HeroRoller/1.3.1/heroll.js | 1585 ++++++++++++++++++++++++++++++++++++
HeroRoller/README.md | 2 +-
HeroRoller/heroll.js | 1444 --------------------------------
HeroRoller/script.json | 10 +-
4 files changed, 1591 insertions(+), 1450 deletions(-)
create mode 100644 HeroRoller/1.3.1/heroll.js
delete mode 100644 HeroRoller/heroll.js
diff --git a/HeroRoller/1.3.1/heroll.js b/HeroRoller/1.3.1/heroll.js
new file mode 100644
index 0000000000..4146d6de20
--- /dev/null
+++ b/HeroRoller/1.3.1/heroll.js
@@ -0,0 +1,1585 @@
+/*
+=========================================================
+Name : Hero Roller (heroll)
+Version : 1.3.1
+Last Update : 11/20/2024
+GitHub : https://github.com/Roll20/roll20-api-scripts/tree/master/HeroRoller
+Roll20 Contact : timmaugh for general questions.
+ villain-in-glasses (Roll20 Id 633423) for HeroSystem6eHeroic-related questions.
+=========================================================
+*/
+var API_Meta = API_Meta || {};
+API_Meta.HeRoll = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 };
+{
+ try { throw new Error(''); } catch (e) { API_Meta.HeRoll.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (13)); }
+}
+
+/*
+ * ----------- DEVELOPMENT PATH ----------------------------
+ -- skill roll template (mechanic bubble for target, drawn automatically)
+ -- explosion rings (separate BODY/STUN, or BODY/PD for entangles) peeling off dice
+ -- track END spent
+ -- track charges/ammo
+*/
+const HeRoll = (() => {
+
+ // ==================================================
+ // VERSION
+ // ==================================================
+ const apiproject = 'HeRoll';
+ API_Meta[apiproject].version = '1.3.1';
+ const schemaVersion = 0.1;
+ const vd = new Date(1732167196095);
+
+ const versionInfo = () => {
+ log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`);
+ return;
+ };
+
+ const logsig = () => {
+ // initialize shared namespace for all signed projects, if needed
+ state.torii = state.torii || {};
+ // initialize siglogged check, if needed
+ state.torii.siglogged = state.torii.siglogged || false;
+ state.torii.sigtime = state.torii.sigtime || Date.now() - 3001;
+ if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) {
+ const logsig = '\n' +
+ ' _____________________________________________ ' + '\n' +
+ ' )_________________________________________( ' + '\n' +
+ ' )_____________________________________( ' + '\n' +
+ ' ___| |_______________| |___ ' + '\n' +
+ ' |___ _______________ ___| ' + '\n' +
+ ' | | | | ' + '\n' +
+ ' | | | | ' + '\n' +
+ ' | | | | ' + '\n' +
+ ' | | | | ' + '\n' +
+ ' | | | | ' + '\n' +
+ '______________|_|_______________|_|_______________' + '\n' +
+ ' ' + '\n';
+ log(`${logsig}`);
+ state.torii.siglogged = true;
+ state.torii.sigtime = Date.now();
+ }
+ return;
+ };
+
+ // ==================================================
+ // TABLES
+ // ==================================================
+ const radioTargetTable = { // used with the radio_target attr
+ "-1": "none",
+ "1": "random",
+ "2": "focus",
+ "3": "headshot",
+ "4": "highshot",
+ "5": "bodyshot",
+ "6": "lowshot",
+ "7": "legshot",
+ "8": "head",
+ "9": "hand",
+ "10": "shoulder",
+ "11": "chest",
+ "12": "stomach",
+ "13": "vitals",
+ "14": "thigh",
+ "15": "leg",
+ "16": "foot",
+ "17": "arm",
+ };
+
+ const argAliasTable = { // aliases for the various accepted arguments, flattening them to what they map to in the parameters object
+ "p": "template", // the template to use for the other parameter defaults
+ "pow": "template",
+ "pwr": "template",
+ "power": "template",
+ "t": "template",
+ "tmp": "template",
+ "template": "template",
+
+ "rm": "mechanic", // the die-rolling mechanic to use (normal, killing, or luck)
+ "rollmech": "mechanic",
+ "rollmechanic": "mechanic",
+ "m": "mechanic",
+ "mech": "mechanic",
+ "mechanic": "mechanic",
+
+ "db": "dbody", // whether the roll should track a BODY value
+ "dbody": "dbody",
+ "doesbody": "dbody",
+
+ "ds": "dstun", // whether the roll should track a STUN value
+ "dstun": "dstun",
+ "doesstun": "dstun",
+
+ "dkb": "dkb", // whether the roll should track a knockback value
+ "dknockback": "dkb",
+ "doesknockback": "dkb",
+
+ "xkb": "kbdicemod", // knockback modifier; how many dice to add to base 2d6
+ "extrakb": "kbdicemod",
+ "kbdice": "kbdicemod",
+ "kbdicemod": "kbdicemod",
+
+ "xs": "stunmod", // extra STUN modifier; added to base
+ "xstun": "stunmod",
+ "extrastun": "stunmod",
+ "stunmod": "stunmod",
+
+ "l": "loc", // a predefined hit location, or command to generate a hit location
+ "loc": "loc",
+ "location": "loc",
+
+ "pl": "pointslabel", // label for the Points Results (if used)
+ "plbl": "pointslabel",
+ "plabel": "pointslabel",
+ "ptsl": "pointslabel",
+ "ptslbl": "pointslabel",
+ "ptslabel": "pointslabel",
+ "pointsl": "pointslabel",
+ "pointslbl": "pointslabel",
+ "pointslabel": "pointslabel",
+
+ "pn": "powername", // name for the power
+ "pname": "powername",
+ "powername": "powername",
+
+ "a": "act", // if there is activation roll for the power
+ "act": "act",
+ "activation": "act",
+
+ "c": "primarycolor", // color for block elements, also drives secondary color and text color
+ "col": "primarycolor",
+ "color": "primarycolor",
+ "pc": "primarycolor",
+ "primarycolor": "primarycolor",
+
+ "d": "dice", // number of dice
+ "dice": "dice",
+
+ "of": "outputformat", // whether the output should be tall or sidecar
+ "output": "outputformat",
+ "format": "outputformat",
+ "sc": "outputformat", // this one added specifically for value-less args (i.e., "--sc") to trigger the default value
+ "outputformat": "outputformat",
+
+ "mental": "useomcv", // whether to use the mental OCV instead of OCV
+ "um": "useomcv",
+ "omcv": "useomcv",
+ "useomcv": "useomcv",
+
+ "o": "ocv", // OCV override, use this if there is no character sheet to draw from
+ "ocv": "ocv",
+
+ "n": "notes", // user notes
+ "notes": "notes",
+
+ "xd": "extradice", // extra dice to be added to the dice (could be second source)
+ "xdice": "extradice",
+ "extradice": "extradice",
+
+ "v": "verbose", // logging of values to the chat for easier debugging
+ "verbose": "verbose",
+
+ "r": "recall", // for recalling the last roll or the last roll for this user
+ "rc": "recall",
+ "recall": "recall",
+
+ "tgt": "target", // target of attack
+ "target": "target",
+
+ "s": "selective", // whether multiple targets get indiv to-hit rolls
+ "sel": "selective",
+ "selective": "selective",
+
+ "as": "source", // character from which to draw OCV (otherwise will be speaker)
+ "source": "source"
+ };
+
+ const templateAliasTable = { // aliases for the various accepted templates
+ "a": "a", // AID
+ "aid": "a",
+
+ "b": "b", // BLAST
+ "blast": "b",
+
+ "di": "di", // DISPEL
+ "dispel": "di",
+
+ "dr": "dr", // DRAIN
+ "drain": "dr",
+
+ "e": "e", // ENTANGLE
+ "entangle": "e",
+
+ "f": "f", // FLASH
+ "flash": "f",
+
+ "ha": "ha", // HAND ATTACK
+ "hattack": "ha",
+ "handattack": "ha",
+
+ "he": "he", // HEALING
+ "heal": "he",
+ "healing": "he",
+
+ "hk": "ka", // KILLING ATTACK
+ "rk": "ka",
+ "k": "ka",
+ "ka": "ka",
+ "kattack": "ka",
+ "killingattack": "ka",
+
+ "mb": "mb", // MENTAL BLAST
+ "mblast": "mb",
+ "mentalblast": "mb",
+
+ "mi": "mi", // MENTAL ILLUSIONS
+ "millusions": "mi",
+ "mentalillusions": "mi",
+ "illusions": "mi",
+
+ "mc": "mc", // MIND CONTROL
+ "mcontrol": "mc",
+ "mindcontrol": "mc",
+
+ "p": "p", // POINTS
+ "pts": "p",
+ "points": "p",
+
+ "t": "t", // TRANSFORM
+ "transform": "t",
+
+ "l": "l", // LUCK
+ "luck": "l",
+
+ "u": "u", // UNLUCK
+ "unluck": "u",
+
+ "c": "c", // CUSTOM (DEFAULT)
+ "def": "c",
+ "cust": "c",
+ "default": "c",
+ "custom": "c",
+ };
+
+ const noValArgDefaults = { // used with command line arguments for which we don't need a value supplied (just their presence should trigger behavior)
+ dbody: true,
+ dstun: true,
+ dkb: true,
+ outputformat: 'sc',
+ useomcv: true,
+ verbose: true,
+ recall: "", // the default case for this will be the current speaker id, so it is grabbed at the time it is needed
+ selective: true,
+ loc: "random",
+ };
+
+ // ==================================================
+ // UTILITY FUNCTIONS
+ // ==================================================
+ const splitArgs = (a) => { return a.split(":") };
+ const joinVals = (a) => { return [a.slice(0)[0], a.slice(1).join(":").trim()]; };
+ const lookFor = (arg) => (a) => { return a === arg; };
+ const lookForArg = (arg) => (a) => { return a[0] === arg; };
+ const lookForVal = (arg) => (a) => { return a[1] === arg; };
+ const aliasesFrom = (o) => (a) => { return [((a[0] in o) ? o[a[0]] : a[0]), a[1]]; };
+ const getKeyByValue = (object, value) => {
+ return Object.entries(object)
+ .filter(lookForVal(value))
+ .map((a) => { return a[0]; });
+ };
+ const escapeRegExp = (string) => { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); };
+
+ const getChar = (query, pid) => { // find a character where info is an identifying piece of information (id, name, or token id)
+ let character;
+ let qrx = new RegExp(escapeRegExp(query), 'i');
+ let charsIControl = findObjs({ type: 'character' });
+ charsIControl = playerIsGM(pid) ? charsIControl : charsIControl.filter(c => c.get('controlledby').split(',').includes(pid));
+ character = charsIControl.filter(c => c.id === query)[0] ||
+ charsIControl.filter(c => c.id === (getObj('graphic', query) || { get: () => { return '' } }).get('represents'))[0] ||
+ charsIControl.filter(c => c.get('name') === query)[0] ||
+ charsIControl.filter(c => qrx.test(c)).reduce((m, v) => {
+ let d = getEditDistance(query, v);
+ return !m.length || d < m[1] ? [v, d] : m;
+ }, [])[0];
+ return character;
+ };
+ const getEditDistance = (a, b) => {
+ if (a.length === 0) return b.length;
+ if (b.length === 0) return a.length;
+
+ var matrix = [];
+
+ // increment along the first column of each row
+ var i;
+ for (i = 0; i <= b.length; i++) {
+ matrix[i] = [i];
+ }
+
+ // increment each column in the first row
+ var j;
+ for (j = 0; j <= a.length; j++) {
+ matrix[0][j] = j;
+ }
+
+ // Fill in the rest of the matrix
+ for (i = 1; i <= b.length; i++) {
+ for (j = 1; j <= a.length; j++) {
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
+ matrix[i][j] = matrix[i - 1][j - 1];
+ } else {
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
+ Math.min(matrix[i][j - 1] + 1, // insertion
+ matrix[i - 1][j] + 1)); // deletion
+ }
+ }
+ }
+
+ return matrix[b.length][a.length];
+ };
+
+ const extendFromArray = (o, a) => {
+ return a.reduce((o, e) => {
+ o[e[0]] = e.slice(1).join();
+ return o;
+ }, o);
+ };
+
+ const storeState = (thisRoller) => {
+ state.heroll[thisRoller.theSpeaker.id] = {};
+ state.heroll[thisRoller.theSpeaker.id].parameters = thisRoller.parameters;
+ state.heroll[thisRoller.theSpeaker.id].userparameters = thisRoller.userparameters;
+ state.heroll[thisRoller.theSpeaker.id].theResult = thisRoller.theResult;
+ state.heroll.lastSpeaker = thisRoller.theSpeaker.id;
+ return;
+ };
+
+ const recallState = (thisRoller) => {
+ let speakID = thisRoller.recallParameters.speakID || thisRoller.theSpeaker.id;
+ if (typeof state.heroll[speakID] !== undefined) {
+ thisRoller.parameters = state.heroll[speakID].parameters;
+ thisRoller.userparameters = state.heroll[speakID].userparameters;
+ thisRoller.theResult = state.heroll[speakID].theResult;
+
+ // this will drive whether we load a previous roll
+ thisRoller.recallParameters.recall = true;
+ }
+ return;
+ };
+
+ const roll3d6 = () => {
+ return randomInteger(6) + randomInteger(6) + randomInteger(6);
+ };
+
+ const getCountsFromArray = (rolls) => {
+ // takes an array of numbers and returns an object structured as { NUMBER : COUNT }, where COUNT represents the # of appearances of that NUMBER
+ return (_.reduce(rolls || [], (m, r) => {
+ m[r] = (m[r] || 0) + 1;
+ return m;
+ }, {}));
+ };
+
+ const getArrayFromCounts = (c) => {
+ // takes an object with properties structured as { NUMBER : COUNT } and returns an array of those NUMBERs repeated COUNT times
+ return _.reduce(c, (m, v, k) => {
+ _.times(v, () => { m.push(k); });
+ return m;
+ }, []);
+ };
+
+ const hexToRGB = (h) => {
+ let r = 0, g = 0, b = 0;
+
+ // 3 digits
+ if (h.length == 4) {
+ r = "0x" + h[1] + h[1];
+ g = "0x" + h[2] + h[2];
+ b = "0x" + h[3] + h[3];
+ // 6 digits
+ } else if (h.length == 7) {
+ r = "0x" + h[1] + h[2];
+ g = "0x" + h[3] + h[4];
+ b = "0x" + h[5] + h[6];
+ }
+ return [+r, +g, +b];
+ };
+
+ const RGBToHex = (r, g, b) => {
+ r = r.toString(16);
+ g = g.toString(16);
+ b = b.toString(16);
+
+ if (r.length == 1)
+ r = "0" + r;
+ if (g.length == 1)
+ g = "0" + g;
+ if (b.length == 1)
+ b = "0" + b;
+
+ return "#" + r + g + b;
+ };
+
+ const getAltColor = (primarycolor) => {
+ let pc = hexToRGB(primarycolor);
+ let sc = [0, 0, 0];
+
+ for (let i = 0; i < 3; i++) {
+ sc[i] = Math.floor(pc[i] + (.35 * (255 - pc[i])));
+ }
+
+ return RGBToHex(sc[0], sc[1], sc[2]);
+ };
+
+ const getTextColor = (h) => {
+ let hc = hexToRGB(h);
+ return (((hc[0] * 299) + (hc[1] * 587) + (hc[2] * 114)) / 1000 >= 128) ? "#000000" : "#ffffff";
+ };
+
+ const getTheSpeaker = (msg) => {
+ let characters = findObjs({ _type: 'character' });
+ let speaking;
+ let sheetName = "HeroSystem6e";
+
+ characters.forEach(function (chr) { if (chr.get('name') === msg.who) speaking = chr; });
+ if (speaking) {
+
+ speaking.speakerType = "character";
+ speaking.localName = speaking.get("name");
+ speaking.radio_target = getCharacterAttr("radio_target", speaking.id);
+ speaking.ocvFinal = getCharacterAttr("OCV", speaking.id);
+ speaking.ocvBase = getCharacterAttr("ocv_base", speaking.id);
+ speaking.ocvMods = speaking.ocvFinal - speaking.ocvBase;
+
+ } else {
+ speaking = getObj('player', msg.playerid);
+ speaking.speakerType = "player";
+ speaking.localName = speaking.get("displayname");
+ speaking.radio_target = "none";
+ speaking.ocvFinal = 0;
+ speaking.ocvBase = 0;
+ speaking.ocvMods = 0;
+ }
+ speaking.playerid = msg.playerid;
+ speaking.chatSpeaker = `${speaking.speakerType}|${speaking.id}`;
+ return speaking;
+ };
+
+ const getCharacterAttr = (attr, charid) => {
+ // First, identify the sheet as either the original supers (HeroSystem6e) or heroic (HeroSystem6eHeroic).
+ let sheet = getAttrByName(charid, 'sheet_name')||"HeroSystem6e";
+
+ let attrDefaultTable = {
+ "radio_target": -1,
+ "OCV": 0,
+ "ocv_base": 0,
+ "DCV": 0,
+ "dcv_base": 0,
+ "DMCV": 0,
+ "dmcv_base": 0,
+ "OMCV": 0,
+ "omcv_base": 0,
+ }
+
+ let translatedAttr;
+ let defvalue = attrDefaultTable[attr];
+ let retAttr;
+ let retValue;
+
+ if (sheet === "HeroSystem6eHeroic") {
+ // Query attributes in HeroSystem6eHeroic translated to HeroSystem6e.
+ // Some CVs included for possible future use.
+ switch (attr) {
+ case "radio_target": translatedAttr = "targetSelection";
+ break;
+ case "OCV": translatedAttr = "ocvNet";
+ break;
+ case "ocv_base": translatedAttr = "ocv";
+ break;
+ case "DCV": translatedAttr = "dcvNet";
+ break;
+ case "dcv_base": translatedAttr = "dcv";
+ break;
+ case "DMCV": translatedAttr = "dmcvNet";
+ break;
+ case "dmcv_base": translatedAttr = "dmcv";
+ break;
+ case "OMCV": translatedAttr = "omcvNet";
+ break;
+ case "omcv_base": translatedAttr = "omcv";
+ break;
+ }
+
+ retValue = getAttrByName(charid, translatedAttr)||defvalue;
+
+ if (translatedAttr === "targetSelection") {
+ // Translate location ID from HS6eH to HS6e.
+ switch (retValue) {
+ case 1: retValue = 1;
+ break;
+ case 2: retValue = 8;
+ break;
+ case 3: retValue = 9;
+ break;
+ case 4: retValue = 17;
+ break;
+ case 5: retValue = 10;
+ break;
+ case 6: retValue = 11;
+ break;
+ case 7: retValue = 12;
+ break;
+ case 8: retValue = 13;
+ break;
+ case 9: retValue = 14;
+ break;
+ case 10: retValue = 15;
+ break;
+ case 11: retValue = 16;
+ break;
+ case 12: retValue = 3;
+ break;
+ case 13: retValue = 4;
+ break;
+ case 14: retValue = 5;
+ break;
+ case 15: retValue = 6;
+ break;
+ case 16: retValue = 7;
+ break;
+ default: retValue = 1;
+ }
+ }
+ } else {
+ // HeroSystem6e default query.
+ retValue = getAttrByName(charid, attr)||defvalue;
+ }
+
+ return Number(retValue);
+ };
+
+
+ const setTargetAttr = (attr, value, charid) => {
+ // First, identify the sheet as either the original supers (HeroSystem6e) or heroic (HeroSystem6eHeroic).
+ let sheet = getAttrByName(charid, 'sheet_name')||"HeroSystem6e";
+ let setAttr;
+
+ if (sheet === "HeroSystem6eHeroic") {
+ // Translate location variable and ID from HS6e to HS6eH.
+ attr = "targetSelection";
+
+ switch (value) {
+ case 1: value = 1;
+ break;
+ case 2: value = 1;
+ break;
+ case 3: value = 12;
+ break;
+ case 4: value = 13;
+ break;
+ case 5: value = 14;
+ break;
+ case 6: value = 15;
+ break;
+ case 7: value = 16;
+ break;
+ case 8: value = 2;
+ break;
+ case 9: value = 3;
+ break;
+ case 10: value = 5;
+ break;
+ case 11: value = 6;
+ break;
+ case 12: value = 7;
+ break;
+ case 13: value = 8;
+ break;
+ case 14: value = 9;
+ break;
+ case 15: value = 10;
+ break;
+ case 16: value = 11;
+ break;
+ case 17: value = 4;
+ break;
+ default: value = 1;
+ }
+ }
+
+ setAttr = findObjs({ type: 'attribute', characterid: charid, name: attr })[0]||createObj("attribute", { name: attr, current: value, characterid: charid });
+ setAttr.set("current", value);
+ }
+
+
+ const addAttribute = (attr, value, charid) => {
+ let tempAttr = createObj("attribute", { name: attr, current: value, characterid: charid });
+ return tempAttr;
+ };
+
+ const addAbility = (ability, value, charid) => {
+ let tempAbil = createObj("ability", { name: ability, action: value, characterid: charid });
+ return tempAbil;
+ };
+
+ const getLocationData = (loc) => {
+ const locationDataTable = {
+ head: { ocvmod: -8, ksx: 5, nsx: 2, bx: 2, hitlabel: "Head" },
+ hand: { ocvmod: -6, ksx: 1, nsx: 0.5, bx: 0.5, hitlabel: "Hand" },
+ arm: { ocvmod: -5, ksx: 2, nsx: 0.5, bx: 0.5, hitlabel: "Arm" },
+ shoulder: { ocvmod: -5, ksx: 3, nsx: 1, bx: 1, hitlabel: "Shoulder" },
+ chest: { ocvmod: -3, ksx: 3, nsx: 1, bx: 1, hitlabel: "Chest" },
+ stomach: { ocvmod: -7, ksx: 4, nsx: 1.5, bx: 1, hitlabel: "Stomach" },
+ vitals: { ocvmod: -8, ksx: 4, nsx: 1.5, bx: 2, hitlabel: "Vitals" },
+ thigh: { ocvmod: -4, ksx: 2, nsx: 1, bx: 1, hitlabel: "Thigh" },
+ leg: { ocvmod: -6, ksx: 2, nsx: 0.5, bx: 0.5, hitlabel: "Leg" },
+ foot: { ocvmod: -8, ksx: 1, nsx: 0.5, bx: 0.5, hitlabel: "Foot" },
+ headshot: { ocvmod: -4 },
+ highshot: { ocvmod: -2 },
+ bodyshot: { ocvmod: -1 },
+ lowshot: { ocvmod: -2 },
+ legshot: { ocvmod: -4 },
+ focus: { ocvmod: -4, ksx: randomInteger(3), nsx: 1, bx: 1, hitlabel: "Focus" },
+ random: { ocvmod: 0 },
+ none: { ocvmod: 0, ksx: randomInteger(3), hitlabel: "none" },
+ };
+ return locationDataTable[loc] || locationDataTable.none;
+ };
+
+ const specHitLocation = (roll) => {
+ let hit;
+ if (roll < 6) hit = "head";
+ else if (roll === 6) hit = "hand";
+ else if (roll < 9) hit = "arm";
+ else if (roll === 9) hit = "shoulder";
+ else if (roll < 12) hit = "chest";
+ else if (roll === 12) hit = "stomach";
+ else if (roll === 13) hit = "vitals";
+ else if (roll === 14) hit = "thigh";
+ else if (roll < 17) hit = "leg";
+ else hit = "foot";
+
+ return hit;
+ };
+
+ const genHitLocation = (shot) => {
+ let roll;
+ switch (shot) {
+ case "headshot":
+ roll = randomInteger(6) + 3;
+ break;
+
+ case "highshot":
+ roll = randomInteger(6) + randomInteger(6) + 1;
+ break;
+
+ case "bodyshot":
+ roll = randomInteger(6) + randomInteger(6) + 4;
+ break;
+
+ case "lowshot":
+ roll = Math.min(18, randomInteger(6) + randomInteger(6) + 7);
+ break;
+
+ case "legshot":
+ roll = randomInteger(6) + 12;
+ break;
+
+ case "random":
+ case "any":
+ default:
+ roll = roll3d6();
+ break;
+ }
+ return roll;
+ };
+
+ const getDice = (n = 1, s = 6) => { // n is count of dice, s is sides
+ let dice = [];
+ for (let i = 0; i < n; i++) {
+ dice.push(randomInteger(s));
+ }
+ return dice;
+ };
+
+ const normalizeDice = (dice) => {
+ while (dice[2] >= 2) { // adder values at/over 2 should add to a 1d3, instead
+ dice[1]++;
+ dice[2] -= 2;
+ }
+ while (dice[2] <= -2) { // adder values at/below -2 should reduce a 1d3, instead
+ if (dice[1] == 0) { // if d3 is already at 0, it should flow over to the d6
+ if (dice[0] == 0) { // if the d6 is already 0, then the d3 should stay at 0 -- don't do anything
+ } else { // the d6 is positive and can be decremented, meaning that the d3 number can increment
+ dice[0]--;
+ dice[1]++;
+ }
+ } else { // the d3 is positive and can be decremented
+ dice[1]--;
+ }
+ dice[2] += 2;
+ }
+ while (dice[1] >= 2) { // every 2d3 should render 1d6 -- mostly important when combining dice and extradice arrays
+ dice[0]++;
+ dice[1] -= 2;
+ }
+ if (dice[0] != Math.floor(dice[0])) {
+ const diff = Math.floor(dice[0] * 1000 - Math.floor(dice[0]) * 1000);
+ dice[0] = Math.floor(dice[0]);
+ if (diff <= 333) { // fractional dice up to this point should give a +1 to the adder
+ dice[2]++;
+ } else if (diff <= 666) { // fractional dice up to this point should give a half die (1d3)
+ dice[1]++;
+ if (dice[1] == 2) { // 2d3 should render 1d6
+ dice[0]++;
+ dice[1] = 0;
+ }
+ } else if (diff > 666) { // fractional dice up to this point should round to 1d6-1
+ dice[0]++;
+ dice[2]--;
+ }
+ }
+ let d6 = Number(dice[0]) + Number(dice[1]) / 2;
+ dice[4] = d6 + 'd6';
+ if (dice[2] != 0) {
+ dice[4] += (dice[2] > 0 ? "+" : "-") + Math.abs(dice[2]);
+ }
+ return dice;
+ };
+
+ const prioritizeArg = (arg, theFunc, thisRoller, ...passArgs) => {
+ // see if the user supplied this argument
+ if (thisRoller.userparameters[arg] !== "--") {
+ // write sanitized value to the parameters
+ thisRoller.parameters[arg] = validateInput(thisRoller.userparameters[arg].toLowerCase(), arg, thisRoller.userparameters[arg]);
+ // call the process function specific to the argument being processed, spread the arguments that had beeen gathered in the rest syntax
+ theFunc(thisRoller, ...passArgs);
+ }
+ // this argument got prioritized to run before all arguments were processed, so remove it from our set of arguments requiring processing
+ thisRoller.knownparams[thisRoller.knownparams.findIndex(lookFor(arg))] = thisRoller.knownparams.pop();
+ return;
+ };
+
+ const validateInput = (input, test, origCap) => {
+
+ let valInput;
+
+ // if the user didn't supply an argument and the argument has a noVal default, use it
+ if (noValArgDefaults.hasOwnProperty(test) && input === "") return noValArgDefaults[test];
+
+ switch (test) {
+ case "template": // the template to use for the other parameter defaults
+ if (input.toLowerCase() in templateAliasTable) { // if found, supply the value (no need for a not-found test; template is already defaulted to "c", and will only change if this line passes)
+ valInput = templateAliasTable[input.toLowerCase()];
+ }
+ break;
+
+ case "mechanic":
+ switch (input) {
+ case "l":
+ case "luck":
+ valInput = "l";
+ break;
+
+ case "u":
+ case "unluck":
+ valInput = "u";
+ break;
+
+ case "k":
+ case "killing":
+ valInput = "k";
+ break;
+
+ case "n":
+ case "normal":
+ default:
+ // if nothing is defined properly, stay with the default normal mechanic
+ valInput = "n";
+ break;
+ }
+ break;
+
+ case "useomcv":
+ case "verbose":
+ case "dbody":
+ case "dstun":
+ case "dkb":
+ switch (input) {
+ case "y":
+ case "yes":
+ case "t":
+ case "true":
+ valInput = true;
+ break;
+
+ case "n":
+ case "no":
+ case "f":
+ case "false":
+ default:
+ valInput = false;
+ break;
+ }
+ break;
+
+ case "dice":
+ valInput = [1, 0, 0, "--", "1d6"]; // this will represent the dice[] array--> [#d6, #d3, adder, rollmechanic shorthand, rebuilt equation]
+ if (['check', 'skill', 'none'].includes(input)) {
+ valInput = [0, 0, 0, "--", "", "check"]; // extra element to trigger check
+ } else {
+ let nomech = input;
+ var mecharray = ["l", "k", "n", "u"];
+ for (let i = 0, len = mecharray.length; i < len; i++) {
+ if (input.includes(mecharray[i])) {
+ nomech = input.replace(mecharray[i], "");
+ valInput[3] = mecharray[i];
+ }
+ }
+ // check if the roll contains 'd6'
+ if (!nomech.includes("d6")) {
+ if (!isNaN(Number(nomech))) { // if there is no 'd6' in the command line, but the value of the dice argument is a number, just use that as the d6 value and normalize after that
+ valInput[0] = Number(nomech);
+ }
+ } else {
+ valInput[0] = (isNaN(nomech.split("d6")[0]) ? valInput[0] : nomech.split("d6")[0]);
+ valInput[2] = (isNaN(nomech.split("d6")[1]) ? valInput[2] : Number(nomech.split("d6")[1]));
+ }
+ }
+ valInput = normalizeDice(valInput);
+ break;
+
+ case "extradice":
+ valInput = [0, 0, 0, "--", "0d6"]; // set to default values in case no number is provided
+ if (isNaN(Number(input))) {
+ } else {
+ valInput[0] += Number(input);
+ valInput = normalizeDice(valInput);
+ }
+ break;
+
+ case "ocv":
+ case "act":
+ case "kbdicemod":
+ case "stunmod":
+ if (isNaN(Number(input))) {
+ valInput = 0; //default to 0 if no number is provided
+ } else {
+ valInput = Number(input);
+ }
+ break;
+
+ case "notes":
+ case "powername":
+ case "pointslabel":
+ valInput = origCap; // get the original version of the capitalization for anything that will go straight to display
+ break;
+
+ case "primarycolor":
+ var colorRegX = /(^#?[0-9A-F]{6}$)|(^#?[0-9A-F]{3}$)/i;
+ valInput = '#' + (colorRegX.test(input) ? input.replace('#', '') : 'b0c4de');
+ break;
+
+ case "loc": // LOCATION
+ const valLoc = [
+ "head", "hand", "arm", "shoulder", "chest", "stomach", "vitals", "thigh", "leg", "foot", "focus",
+ "headshot", "highshot", "bodyshot", "lowshot", "legshot",
+ "random", "none",
+ "hands", "arms", "shoulders", "thighs", "legs", "feet",
+ ];
+
+ if (valLoc.indexOf(input) === -1) { // not a valid location, but the user tried to set a location, so default should go to "random" instead of "none"
+ valInput = "random";
+ } else { // a valid location, but we need to reduce plural entries; join the keys with a pipe, then use that as the seed for a regexp that will drive a match
+ const toSingular = {
+ "hands": "hand",
+ "arms": "arm",
+ "shoulders": "shoulder",
+ "thighs": "thigh",
+ "legs": "leg",
+ "feet": "foot",
+ };
+
+ valInput = input.replace(new RegExp(Object.keys(toSingular).join("|"), 'gi'), function (matched) { return toSingular[matched]; });
+ }
+ break;
+
+ case "outputformat":
+ switch (input) {
+ case "sc":
+ case "side":
+ case "sidecar":
+ valInput = "sc";
+ break;
+
+ case "t":
+ case "tall":
+ default:
+ valInput = "tall";
+ break;
+ }
+ break;
+ case "target":
+ valInput = origCap.split(/[\s,]/); // split on white space or comma
+ break;
+
+ case "selective":
+ valInput = true;
+ break;
+ case "source":
+ valInput = origCap;
+ break;
+ }
+
+ return valInput;
+ };
+
+ // ==================================================
+ // PROCESS FUNCTIONS
+ // ==================================================
+ const setDefaults = (msg, thisRoller) => { //initializes various parameters
+ // SPEAKER INFO
+ thisRoller.theSpeaker = getTheSpeaker(msg);
+
+ // STATE VARIABLE STORAGE
+ state.heroll = state.heroll || {};
+ state.heroll[thisRoller.theSpeaker.id] = state.heroll[thisRoller.theSpeaker.id] || {};
+ state.heroll.lastSpeaker = state.heroll.lastSpeaker || "none";
+ if (state.heroll.lastSpeaker !== "none") state.heroll[state.heroll.lastSpeaker] = state.heroll[state.heroll.lastSpeaker] || {};
+ thisRoller.recallParameters = {
+ recall: false,
+ speakID: thisRoller.theSpeaker.id,
+ };
+
+ // USER INPUT
+ thisRoller.userparameters = { // record user provided values; default them to "--" for "not provided", set appropriately when the args are evaluated
+ template: "--",
+ powername: "--",
+ mechanic: "--",
+ dbody: "--",
+ dstun: "--",
+ dkb: "--",
+ kbdicemod: "--",
+ stunmod: "--",
+ loc: "--",
+ outputformat: "--",
+ dice: "--",
+ act: "--",
+ primarycolor: "--",
+ pointslabel: "--",
+ useomcv: "--",
+ ocv: "--",
+ extradice: "--",
+ recall: "--",
+ verbose: "--",
+ target: "--",
+ selective: "--",
+ notes: "",
+ source: "--"
+ };
+
+ // KNOWN PARAMETER KEYS // for later iteration
+ thisRoller.knownparams = Object.keys(thisRoller.userparameters);
+
+ // VALIDATED PARAMETERS
+ thisRoller.parameters = { // validated user settings (passed through validateInput() function)
+ template: "c",
+ powername: "Attack",
+ mechanic: "n",
+ dbody: true,
+ dstun: true,
+ dkb: true,
+ kbdicemod: 0,
+ stunmod: 0,
+ loc: "none", // location if specified by user; can include 'shot' locations (i.e., 'headhshot')
+ dice: [1, 0, 0, "--", "1d6"], // d6, d3, adder, rollmechanic shorthand, roll equation without shorthand
+ act: -100, // activation for the try; invalid entry will set this to 0, so -100 is a trip that it isn't set
+ primarycolor: "#b0c4de",
+ pointslabel: "POINTS",
+ outputformat: "tall",
+ useomcv: false,
+ ocv: -100,
+ notes: "",
+ extradice: [0, 0, 0, "--", "0d6"], // d6, d3, adder, shorthand (not used), roll equation
+ recall: "--",
+ target: [], // ids of any targets supplied
+ selective: false, // whether to roll individual to-hits for each supplied target
+ verbose: false,
+ source: "--",
+
+ //inaccessible to user
+ outputas: "attack",
+ actroll: roll3d6(), // activation roll, only used if the activation argument is invoked by the user
+ xdice: [0, 0, 0, "--", "0d6"], // for the combination of dice and extradice
+ dcheck: false, // if this is a check roll, not requiring a result output
+ };
+
+ // OUTPUT PARAMETERS
+ thisRoller.outputParams = { // HTML variables for formatting output
+ __CHARNAME__: thisRoller.theSpeaker.localName, // character name
+ __MECH__: "N", // text in the mechanic bubble
+ __MECH_VIS__: "block", // whether the mechanic bubble is visible (block; none)
+ __MECH_BGC__: "#b0c4de", // background-color for mechanic bubble
+ __POWERNAME__: "Power Name", // power name color text
+ __PRIMARY_BG_COL__: "#b0c4de", // background-color for any element (like power name) that is to match the primarycolor parameter
+ __PRIMARY_TEXT_COL__: "#ffffff", // text color for anything with the primary background color
+ __DS_TALL_VIS__: "block", // whether the die strength (ie, 6d6) in the tall format should be visible (block; none)
+ __DIE_STRENGTH__: "1d6", // value for the die strength, wherever it is used
+ __ACT_VIS__: "none", // whether the Activation block is visible (block; none)
+ __ACT_TGT__: "", // target for the activation roll
+ __ACT_ROLL__: "", // result of the activation roll
+ __TOHIT_VIS__: "block", // whether the To Hit Bar (OCV, ROLL, HIT DCV) should be visible
+ __TOHIT_OCV_LBL__: "OCV", // label for the OCV box
+ __TOHIT_OCV__: "--", // to hit OCV
+ __TOHIT_ROLL__: "", // to hit roll result
+ __TOHIT_DCV_LBL__: "HIT DCV", // label for the DCV box
+ __TOHIT_DCV__: "--", // hit DCV
+ __SECONDARY_BG_COL__: "#d8e1ee", // color for secondary-color elements (like die pool); figured by javascript as shade of the primary color
+ __SECONDARY_TEXT_COL__: "#ffffff", // text color for anything with the secondary background color
+ __LOC_TALL_VIS__: "none", // whether the Hit Location bar in the tall format should be visible (block; none)
+ __LOC_SC_VIS__: "none", // whether the Hit Location bar in the sidecar format should be visible (block; none)
+ __LOC__: "Location", // Hit location
+ __DIEPOOL__: "", // Comma-delimited set of dice resulting from the roll (as well as any adders)
+ __DIEPOOL_TALL_VIS__: "block", // whether the die pool in the tall format should be visible (block for tall; none for sidecar)
+ __RES_MULT_VIS__: "none", // visibility for the Results Bar (with Location), used for hit-location multiples (block; none)
+ __BODY_MULT__: "1", // BODY multiplier (multiplied against the BODY remaining after defenses are applied)
+ __STUN_MULT__: "1", // STUN multiplier (multiplied agianst the STUN remaining after defenses) -- could be NStun if it is a Normal mechanic attack
+ __RES_BODY__: "", // BODY rolled on the dice
+ __RES_STUN__: "", // STUN rolled on the dice
+ __RES_KB__: "", // KB done by attack
+ __RES_BASE_VIS__: "block", // visibility for the Results Bar (without Location), used if location = none (block; none)
+ __RES_PTS_VIS__: "none", // visibility for the Results Bar (Points), used for Points output (block; none)
+ __RES_PTS_LBL__: "POINTS", // text for Points label in Results Bar (Points)
+ __RES_PTS__: "", // Points done (only used for points output)
+ __V_VIS__: "none", // visibility for verbose output (block; none)
+ __V_NOTE__: "", // place for note in verbose output
+ __SIDECAR_VIS__: "none", // visibility for all sidecar elements (works opposite of tall based elements) (block for sidecar; none for tall)
+ __NOTES__: "", // text from the notes argument
+ __TARGET_TABLE_HOOK__: "", // hook for the rows of target information, if one/more is specified
+ };
+
+ // RESULT ROLL
+ thisRoller.theResult = {
+ tohitroll: roll3d6(), // base to-hit roll
+ theroll: [], // dice pool of the roll result
+ dicecounts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }, // count of how many of each was rolled
+ d6counts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }, // count of how many times each was rolled, only for d6 -- used to display results differently (d6 vs d3) in output
+ d3counts: { 1: 0, 2: 0, 3: 0 }, // count of how many times each was rolled, only for d3 -- used to display results differently (d6 vs d3) in output
+ normalstun: 0,
+ normalbody: 0,
+ killingbody: 0,
+ points: { stun: 0, body: 0 },
+ location: { ocvmod: 0, ksx: 1, nsx: 1, bx: 1, hitlabel: "" },
+ kbroll: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 },
+ knockback: 0,
+ targetData: [], // if targets are designated, this will hold any relevant data (pic, to hit roll, location, etc.)
+ };
+ return;
+ };
+
+ const processRecall = (thisRoller, args) => {
+ // the recall speaker was already set to current speaker with the defaults, so we only need to overwrite it if there is a value supplied
+ if (thisRoller.userparameters.recall !== "") {
+ thisRoller.recallParameters.speakID = (thisRoller.userparameters.recall === "last" && state.heroll.lastSpeaker !== "none") ? state.heroll.lastSpeaker : thisRoller.userparameters.recall;
+ }
+
+ // load previous roll
+ recallState(thisRoller);
+
+ if (thisRoller.recallParameters.recall === true) {
+ // that just overwrote our user supplied arguments, so reapply with only those allowed
+ // first, what isn't allowed:
+ const dropProps = ["dice", "extradice", "recall", "template"];
+ extendFromArray(thisRoller.userparameters, args.filter((a) => { return !dropProps.includes(a[0]); }));
+ }
+ return;
+ };
+
+ const setTemplateDefaults = (thisRoller) => {
+ // a 'template' is a slate of parameters to set general behaviors
+ // for instance, an Aid power uses the dice differently from an attack, etc.
+ // the boilerplate attack template is "c" (custom); boilerplate points is "p" (points)
+ // the template is initialized as "c" in the setDefaults function
+ let templates = {
+ a: { template: "a", powername: "Aid", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF AID", useomcv: false, outputas: "points" },
+ b: { template: "b", powername: "Blast", mechanic: "n", dbody: true, dstun: true, dkb: true, primarycolor: "#5ac7ff", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
+ c: { template: "c", powername: "Attack", mechanic: "n", dbody: true, dstun: true, dkb: true, primarycolor: "#b0c4de", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
+ di: { template: "di", powername: "Dispel", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#b0c4de", pointslabel: "POINTS OF DISPEL", useomcv: false, outputas: "points" },
+ dr: { template: "dr", powername: "Drain", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF DRAIN", useomcv: false, outputas: "points" },
+ e: { template: "e", powername: "Entangle", mechanic: "n", dbody: true, dstun: false, dkb: false, primarycolor: "#b0c4de", pointslabel: "ENTANGLE BODY", useomcv: false, outputas: "points" },
+ f: { template: "f", powername: "Flash", mechanic: "n", dbody: true, dstun: false, dkb: false, primarycolor: "#b0c4de", pointslabel: "SEGMENTS OF FLASH", useomcv: false, outputas: "points" },
+ ha: { template: "ha", powername: "Hand Attack", mechanic: "n", dbody: true, dstun: true, dkb: true, primarycolor: "#0289ce", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
+ he: { template: "he", powername: "Healing", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF HEALING", useomcv: false, outputas: "points" },
+ ka: { template: "ka", powername: "Killing Attack", mechanic: "k", dbody: true, dstun: true, dkb: true, primarycolor: "#ff5454", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
+ mb: { template: "mb", powername: "Mental Blast", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#c284ed", pointslabel: "POINTS", useomcv: true, outputas: "attack" },
+ mi: { template: "mi", powername: "Mental Illusions", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#c284ed", pointslabel: "POINTS OF ILLUSION", useomcv: true, outputas: "points" },
+ mc: { template: "mc", powername: "Mind Control", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#c284ed", pointslabel: "POINTS OF MIND CONTROL", useomcv: true, outputas: "points" },
+ p: { template: "p", powername: "Points Power", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#9d41e8", pointslabel: "POINTS", useomcv: false, outputas: "points" },
+ t: { template: "t", powername: "Transform", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF TRANSFORM", useomcv: false, outputas: "points" },
+ l: { template: "l", powername: "Luck", mechanic: "l", dbody: false, dstun: false, dkb: false, primarycolor: "#35e54e", pointslabel: "POINTS OF LUCK", useomcv: false, outputas: "points" },
+ u: { template: "u", powername: "Unluck", mechanic: "u", dbody: false, dstun: false, dkb: false, primarycolor: "#FF453B", pointslabel: "POINTS OF UNLUCK", useomcv: false, outputas: "points" },
+ };
+ Object.assign(thisRoller.parameters, templates[thisRoller.parameters.template]);
+ return;
+ };
+
+ const processArguments = (thisRoller) => {
+ // process each of the known parameters appearing in the command line
+ // if the userparameters version is altered from the default, pass that value through validateInput to return the sanitized version
+ thisRoller.knownparams.map((p) => { thisRoller.parameters[p] = thisRoller.userparameters[p] === "--" ? thisRoller.parameters[p] : validateInput(thisRoller.userparameters[p].toLowerCase(), p, thisRoller.userparameters[p]); });
+
+ // change unrecognized args with no value to have "NA" instead (used in verbose output)
+ Object.keys(thisRoller.userparameters).filter((a) => { return !thisRoller.knownparams.includes(a); })
+ .map((a) => { thisRoller.userparameters[a] === "" ? thisRoller.userparameters[a] = "NA" : thisRoller.userparameters[a]; });
+
+ // set the roll mechanic parameter to the shorthand, if present
+ // the roll mechanic property can be set in three places; the priority should be: template < explicit mech argument < shorthand
+ thisRoller.parameters.mechanic = (thisRoller.parameters.dice[3] != "--" ? thisRoller.parameters.dice[3] : thisRoller.parameters.mechanic);
+
+ // test if the dice parameter came back with the extra element denoting this is a check (should have 5 elements: d6, d3, adder, mechanic, equation)
+ if (thisRoller.parameters.dice.length > 5) {
+ thisRoller.parameters.dcheck = true; // this will trigger no-result output, later
+ thisRoller.parameters.dice.pop(); // remove extra element
+ }
+ return;
+ };
+
+ const processTargets = (thisRoller) => {
+ if (thisRoller.parameters.mechanic === "l" || thisRoller.parameters.mechanic === "u") return; // no targets for luck & unluck
+ let attr = thisRoller.parameters.useomcv ? "DMCV" : "DCV"; // decide which attribute we're using for the defensive value, if character sheet is present
+ if (thisRoller.parameters.target.length > 0) { // if a target was designated -- "target" here is an array of targets
+ thisRoller.theResult.targetData = thisRoller.parameters.target
+ .filter((a) => { return getObj('graphic', a); }) // limit to only those that are properly formatted (filter out the bad)
+ .map((a) => { // each should output an object of key:value pairs for the info we need
+ let loc = getResultLocation(thisRoller);
+ let thr = thisRoller.parameters.selective === true ? roll3d6() : thisRoller.theResult.tohitroll;
+ let token = getObj('graphic', a);
+ let chardcv = "";
+ let charishit = "";
+ if (token.get('represents') !== "") { // check whether token has character sheet
+ chardcv = getCharacterAttr(attr, token.get('represents'));
+ charishit = 11 + thisRoller.theSpeaker.ocvFinal - thr >= chardcv ? "◎" : ""; // if character is hit, load the target character into the string; otherwise, empty
+ }
+ return {
+ __TARGET_IMG__: token.get('imgsrc'),
+ __TARGET_DCV__: chardcv,
+ __TARGET_TOHIT_VIS__: thisRoller.parameters.selective ? "block" : "none",
+ __TARGET_TOHIT_ROLL__: thr,
+ __TARGET_LOC_VIS__: loc.hitlabel !== "none" ? "block" : "none",
+ __TARGET_LOC__: loc.hitlabel + (randomInteger(2) > 1 ? " (R)" : " (L)"),
+ __TARGET_BX__: loc.bx,
+ __TARGET_SX__: thisRoller.parameters.mechanic === "k" ? loc.ksx : loc.nsx,
+ __TARGET_HIT_DCV__: 11 + thisRoller.theSpeaker.ocvFinal - thr,
+ __TARGET_ISHIT__: (thisRoller.parameters.target.length === 1 || !thisRoller.parameters.selective) ? "" : charishit,
+ };
+ });
+ }
+ };
+
+ const processOCV = (thisRoller) => {
+ let sourceChar;
+
+ if (thisRoller.parameters.source !== '--') {
+ sourceChar = getChar(thisRoller.parameters.source, thisRoller.theSpeaker.playerid);
+
+ if (sourceChar) {
+ thisRoller.theSpeaker.radio_target = getCharacterAttr("radio_target", sourceChar.id);
+ thisRoller.theSpeaker.ocvFinal = getCharacterAttr("OCV", sourceChar.id);
+ thisRoller.theSpeaker.ocvBase = getCharacterAttr("ocv_base", sourceChar.id);
+ thisRoller.theSpeaker.ocvMods = thisRoller.theSpeaker.ocvFinal - thisRoller.theSpeaker.ocvBase;
+ thisRoller.theSpeaker.sourceName = sourceChar.get('name');
+ }
+ } else if (thisRoller.theSpeaker.speakerType === 'character') {
+ sourceChar = thisRoller.theSpeaker;
+ }
+ // process using OMCV instead of OCV
+ if (thisRoller.parameters.useomcv === true && sourceChar) { // if there is a character involved, get the ocv and location info from the sheet
+ thisRoller.theSpeaker.ocvFinal = getCharacterAttr("OMCV", sourceChar.id);
+ thisRoller.theSpeaker.ocvBase = getCharacterAttr("omcv_base", sourceChar.id);
+ thisRoller.theSpeaker.ocvMods = thisRoller.theSpeaker.ocvFinal - thisRoller.theSpeaker.ocvBase;
+ }
+
+ // process changing the called location
+ let startingLocation = sourceChar ? radioTargetTable[getCharacterAttr("radio_target", sourceChar.id)] : radioTargetTable[thisRoller.theSpeaker.radio_target];
+ if (startingLocation !== thisRoller.parameters.loc && sourceChar) { //only matters if the location has changed
+
+ setTargetAttr("radio_target", Number(Object.keys(radioTargetTable).find(key => radioTargetTable[key] === thisRoller.parameters.loc)), sourceChar.id);
+
+ if (thisRoller.parameters.useomcv === false) { // only process changes to the OCV if we are using OCV, not if we are using OMCV
+ //apply the new mod, remove the old
+ thisRoller.theSpeaker.ocvMods += (getLocationData(thisRoller.parameters.loc).ocvmod || 0) - (getLocationData(startingLocation).ocvmod || 0);
+ // rebuild the final ocv
+ thisRoller.theSpeaker.ocvFinal = Number(thisRoller.theSpeaker.ocvBase) + Number(thisRoller.theSpeaker.ocvMods);
+ }
+ }
+
+ // process the OCV override
+ if (thisRoller.parameters.ocv !== -100) { // user supplied an OCV override
+ thisRoller.theSpeaker.ocvFinal = Math.floor(thisRoller.parameters.ocv);
+ thisRoller.theSpeaker.ocvMods = 0;
+ }
+ return;
+ };
+
+ const getResultLocation = (thisRoller) => {
+ // if it's a recall and the situation would normally trip a location generation (like random)
+ // we should only roll it if there is no recall version of the location argument passed
+ // original roll generates a random location; once set, that should stay the same for recalls, only changing if the recall explicitly includes new location argument
+ // if (thisRoller.theResult.location.hitlabel !== "" /* default would mean no location, ie: first roll*/ && thisRoller.recallParameters.recall === true )
+
+ let loc = {};
+ // if we need to get a location, get it; run the location through the locationDataTable to get properties
+ if (thisRoller.parameters.loc == "any" || thisRoller.parameters.loc == "random" || thisRoller.parameters.loc.indexOf("shot") > -1) {
+ loc = getLocationData(specHitLocation(genHitLocation(thisRoller.parameters.loc)));
+ } else {
+ loc = getLocationData(thisRoller.parameters.loc);
+ }
+ return loc;
+ };
+
+ const rollResult = (thisRoller) => {
+ var d6Res = [];
+ var d3Res = [];
+ for (let i = 0; i < 3; i++) {
+ thisRoller.parameters.xdice[i] = Number(thisRoller.parameters.dice[i]) + Number(thisRoller.parameters.extradice[i]);
+ }
+ thisRoller.parameters.xdice = normalizeDice(thisRoller.parameters.xdice);
+
+ if (thisRoller.parameters.xdice[0] > 0) { // get the quantity of d6
+ d6Res.push(...getDice(thisRoller.parameters.xdice[0], 6));
+ thisRoller.theResult.theroll = [...thisRoller.theResult.theroll, ...d6Res];
+ }
+ if (thisRoller.parameters.xdice[1] > 0) { // get the quantity of d3
+ d3Res.push(...getDice(thisRoller.parameters.xdice[1], 3));
+ thisRoller.theResult.theroll = [...thisRoller.theResult.theroll, ...d3Res];
+ }
+ thisRoller.theResult.theroll.sort(function (a, b) { return b - a });
+ Object.assign(thisRoller.theResult.dicecounts, getCountsFromArray(thisRoller.theResult.theroll));
+ Object.assign(thisRoller.theResult.d6counts, getCountsFromArray(d6Res)); // needed to output "dice" in the die pool output using the d6 font; d6 in one color, d3 in another
+ Object.assign(thisRoller.theResult.d3counts, getCountsFromArray(d3Res)); // needed to output "dice" in the die pool output using the d6 font; d6 in one color, d3 in another
+
+ // KNOCKBACK
+ if (thisRoller.parameters.dkb === true) {
+ let kbbasedice = 2;
+ if (thisRoller.parameters.mechanic === "k") kbbasedice++;
+ let kbx = Math.max(0, kbbasedice + thisRoller.parameters.kbdicemod);
+ Object.assign(thisRoller.theResult.kbroll, getCountsFromArray(getDice(kbx, 6)));
+ log("Knockback Roll: " + JSON.stringify(thisRoller.theResult.kbroll));
+ for (let i = 1; i < 7; i++) {
+ thisRoller.theResult.knockback += (thisRoller.theResult.kbroll[i] * i);
+ }
+ }
+ return;
+ };
+
+ const calcResult = (thisRoller) => {
+ // reset if we are performing recall
+ if (thisRoller.recallParameters.recall) {
+ Object.assign(thisRoller.theResult, {
+ normalbody: 0,
+ normalstun: 0,
+ });
+ }
+ // nbody maps the amount of BODY per value of a die
+ let nbody = { 1: 0, 2: 1, 3: 1, 4: 1, 5: 1, 6: 2 };
+ for (let i = 1; i < 7; i++) {
+ thisRoller.theResult.normalstun += (thisRoller.theResult.dicecounts[i] * i);
+ thisRoller.theResult.normalbody += (thisRoller.theResult.dicecounts[i] * nbody[i]);
+ }
+ thisRoller.theResult.normalstun += thisRoller.parameters.xdice[2]; //include the adder in the total
+ thisRoller.theResult.killingbody = thisRoller.theResult.normalstun;
+
+ // killing stun multiplier comes from the location in the getLocationData function (including a random d3 for no location and focus location)
+ // if a stunmod is applied, incorporate it with the location stun modifier
+ thisRoller.theResult.location.nsx = Math.max(1, thisRoller.theResult.location.nsx + Math.floor(thisRoller.parameters.stunmod));
+ thisRoller.theResult.location.ksx = Math.max(1, thisRoller.theResult.location.ksx + Math.floor(thisRoller.parameters.stunmod));
+ thisRoller.theResult.killingstun = thisRoller.theResult.killingbody * thisRoller.theResult.location.ksx;
+
+ // if the output is points, determine where the points come from
+ if (thisRoller.parameters.outputas == "points") {
+ thisRoller.theResult.points.stun = thisRoller.theResult.normalstun;
+ thisRoller.theResult.points.body = thisRoller.theResult.normalbody;
+ if (thisRoller.parameters.mechanic === "l") thisRoller.theResult.points.stun = thisRoller.theResult.dicecounts[6];
+ else if (thisRoller.parameters.mechanic === "u") thisRoller.theResult.points.stun = thisRoller.theResult.dicecounts[1];
+ }
+
+ return;
+ };
+
+ const handleInput = (msg) => {
+ if (msg.type !== 'api' || !msg.content.toLowerCase().startsWith('!heroll ')) {
+ return;
+ }
+
+ // reduce all inline rolls - this rewrites the content of the msg to be the output of an inline roll rather than the $[[0]], $[[1]], etc.
+ if (_.has(msg, 'inlinerolls')) {
+ msg.content = _.chain(msg.inlinerolls)
+ .reduce(function (m, v, k) {
+ m['$[[' + k + ']]'] = v.results.total || 0;
+ return m;
+ }, {})
+ .reduce(function (m, v, k) {
+ return m.replace(k, v);
+ }, msg.content)
+ .value();
+ }
+
+ let args = msg.content.split(/\s--/) // split at argument delimiter
+ .slice(1) // drop the api tag
+ .map(splitArgs) // split each arg (foo:bar becomes [foo, bar])
+ .map(joinVals) // if the value included a colon (the delimiter), join the parts that were inadvertently separated
+ .map(aliasesFrom(argAliasTable)); // flatten all argument aliases to the valid, internally tracked args
+
+ let thisRoller = {}; // local object for ephemeral data storage
+ setDefaults(msg, thisRoller); // initializes all parameters, including speaker and template defaults
+ extendFromArray(thisRoller.userparameters, args); // write all args to the userparameters, adding any unrecognized args
+ prioritizeArg("recall", processRecall, thisRoller, args); // look for and process recall argument, if present
+
+ // if (typeof state.heroll[thisRoller.recallParameters.speakID].parameters === 'undefined') { // if no roll is stored for that speaker in the state variable
+ prioritizeArg("template", setTemplateDefaults, thisRoller); // look for and process template argument, if present
+ processArguments(thisRoller); // process the rest of the arguments
+ processOCV(thisRoller); // figure out if OMCV, location, or OCV override should alter the OCV (or replace it)
+ processTargets(thisRoller);
+ // rollActivation(); // no longer needed as 3d6 roll was already generated in the initialization of defaults
+ // rollToHit(); // no longer needed as 3d6 roll was already generated in the initialization of defaults
+ thisRoller.theResult.location = getResultLocation(thisRoller); // generate a location if necessary, then retrieve location information
+ if (!thisRoller.recallParameters.recall) rollResult(thisRoller);// generate dice pool
+ calcResult(thisRoller); // turn dice pool into stun, body, knockback, multipliers, points, etc.
+ storeState(thisRoller); // store in the state variable
+ prepOutput(thisRoller); // assign values to the output parameters
+ sendOutputToChat(thisRoller); // perform replacement using html form and the output parameters to inject the finished values; send to chat
+
+ return;
+ };
+
+ // ==================================================
+ // OUTPUT FUNCTIONS
+ // ==================================================
+ const prepOutput = (thisRoller) => {
+ // SOURCE NAME
+ if (thisRoller.theSpeaker.sourceName) {
+ thisRoller.outputParams.__CHARNAME__ = thisRoller.theSpeaker.sourceName;
+ }
+
+ // OUTPUT FORMAT
+ if (thisRoller.parameters.outputformat != "tall") {
+ thisRoller.outputParams.__DS_TALL_VIS__ = "none";
+ thisRoller.outputParams.__LOC_TALL_VIS__ = "none";
+ thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
+ thisRoller.outputParams.__SIDECAR_VIS__ = "block";
+ }
+
+ // POWER NAME
+ thisRoller.outputParams.__POWERNAME__ = thisRoller.parameters.powername;
+
+ // COLORS
+ thisRoller.outputParams.__PRIMARY_BG_COL__ = thisRoller.parameters.primarycolor;
+ thisRoller.outputParams.__SECONDARY_BG_COL__ = getAltColor(thisRoller.parameters.primarycolor);
+ thisRoller.outputParams.__PRIMARY_TEXT_COL__ = getTextColor(thisRoller.outputParams.__PRIMARY_BG_COL__);
+ thisRoller.outputParams.__SECONDARY_TEXT_COL__ = getTextColor(thisRoller.outputParams.__SECONDARY_BG_COL__);
+
+ // ACTIVATION
+ if (thisRoller.parameters.act != -100) {
+ thisRoller.outputParams.__ACT_VIS__ = "block";
+ thisRoller.outputParams.__ACT_TGT__ = thisRoller.parameters.act;
+ thisRoller.outputParams.__ACT_ROLL__ = thisRoller.parameters.actroll;
+ if (thisRoller.parameters.actroll > thisRoller.parameters.act) { // if activation fails, only show the "CLICK" message
+ thisRoller.parameters.notes = '
----- CLICK! -----
';
+ thisRoller.outputParams.__RES_BASE_VIS__ = "none";
+ thisRoller.outputParams.__RES_MULT_VIS__ = "none";
+ thisRoller.outputParams.__TOHIT_VIS__ = "none";
+ thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
+ thisRoller.outputParams.__DS_TALL_VIS__ = "true";
+ thisRoller.outputParams.__LOC_TALL_VIS__ = "none";
+ thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
+ thisRoller.outputParams.__SIDECAR_VIS__ = "none";
+ }
+ }
+
+ // LOCATION
+ if (thisRoller.parameters.loc != "none" && thisRoller.parameters.target.length === 0) { // if there is a location AND there are no targets (targets show their own locations)
+ if (thisRoller.parameters.outputformat === "tall") {
+ thisRoller.outputParams.__LOC_TALL_VIS__ = "block";
+ } else {
+ thisRoller.outputParams.__LOC_SC_VIS__ = "block";
+ }
+
+ thisRoller.outputParams.__LOC__ = thisRoller.theResult.location.hitlabel + (randomInteger(2) > 1 ? " (R)" : " (L)");
+ thisRoller.outputParams.__BODY_MULT__ = thisRoller.theResult.location.bx;
+ if (thisRoller.parameters.mechanic == "k") thisRoller.outputParams.__STUN_MULT__ = thisRoller.theResult.location.ksx;
+ else if (thisRoller.parameters.mechanic == "n") thisRoller.outputParams.__STUN_MULT__ = thisRoller.theResult.location.nsx;
+
+ } else {
+ thisRoller.outputParams.__BODY_MULT__ = "--"; // this will either be hidden or direct people to the target info
+ thisRoller.outputParams.__STUN_MULT__ = "--";
+ }
+
+ // TO HIT BAR
+ thisRoller.outputParams.__TOHIT_ROLL__ = thisRoller.theResult.tohitroll;
+ if (thisRoller.theSpeaker.speakerType === "character") {
+ // determine how to represent ocv mods (+/-); 0's should make the whole mod portion disappear
+ let outputocvmod = "";
+ if (thisRoller.theSpeaker.ocvMods < 0) outputocvmod = "-";
+ if (thisRoller.theSpeaker.ocvMods > 0) outputocvmod = "+";
+ if (outputocvmod.length) outputocvmod += Math.abs(thisRoller.theSpeaker.ocvMods);
+
+ thisRoller.outputParams.__TOHIT_OCV__ = thisRoller.parameters.ocv !== -100 ? thisRoller.theSpeaker.ocvFinal : thisRoller.theSpeaker.ocvBase + outputocvmod;
+ thisRoller.outputParams.__TOHIT_DCV__ = 11 + thisRoller.theSpeaker.ocvFinal - thisRoller.theResult.tohitroll;
+ }
+ if (thisRoller.parameters.useomcv) {
+ thisRoller.outputParams.__TOHIT_OCV_LBL__ = "OMCV";
+ thisRoller.outputParams.__TOHIT_DCV_LBL__ = "HIT DMCV";
+ }
+
+ // RESULTS BAR
+ if (thisRoller.parameters.outputas == "attack" && thisRoller.parameters.loc != "none") {
+ thisRoller.outputParams.__RES_MULT_VIS__ = "block";
+ thisRoller.outputParams.__RES_BASE_VIS__ = "none";
+ } else if (thisRoller.parameters.outputas == "points") {
+ thisRoller.outputParams.__RES_PTS_VIS__ = "block";
+ thisRoller.outputParams.__RES_BASE_VIS__ = "none";
+ }
+
+ // MECHANIC
+ var mechColors = { L: "#00b8a9", N: "#ff8000", K: "#bf1f2f", U: "#5438AF" };
+ thisRoller.outputParams.__MECH__ = thisRoller.parameters.mechanic.toUpperCase();
+ thisRoller.outputParams.__MECH_BGC__ = mechColors[thisRoller.outputParams.__MECH__];
+
+ // DIE STRENGTH (ROLL EQUATION)
+ thisRoller.outputParams.__DIE_STRENGTH__ = thisRoller.parameters.xdice[4];
+
+ // DIE POOL
+ let numx = { 1: "G", 2: "H", 3: "I", 4: "J", 5: "K", 6: "L" },
+ num3x = { 1: "g", 2: "h", 3: "i" };
+ for (let i = 6; i > 0; i--) {
+ let initlength = thisRoller.outputParams.__DIEPOOL__.length;
+ // processing the d6 roll result, output a single character for each die, mapping the value to the numx object
+ // those characters are what the d6 font requires to display the correct die face
+ for (let j = 0; j < thisRoller.theResult.d6counts[i]; j++) {
+ thisRoller.outputParams.__DIEPOOL__ += i.toString().replace(/1|2|3|4|5|6/gi, function (matched) { return numx[matched] });
+ }
+ // processing the d3 roll result, do the same as above but only for 1-3, and map to the num3x object, which will display the d3 dice in a different color
+ if (i < 4) {
+ for (let j = 0; j < thisRoller.theResult.d3counts[i]; j++) {
+ thisRoller.outputParams.__DIEPOOL__ += i.toString().replace(/1|2|3/gi, function (matched) { return num3x[matched] });
+ }
+ }
+ if (initlength != thisRoller.outputParams.__DIEPOOL__.length && i != 1) thisRoller.outputParams.__DIEPOOL__ += "
";
+
+ }
+
+ if (thisRoller.parameters.xdice[2] != 0) {
+ thisRoller.outputParams.__DIEPOOL__ += " ";
+ thisRoller.outputParams.__DIEPOOL__ += (thisRoller.parameters.xdice[2] > 0 ? "+" : "-");
+ thisRoller.outputParams.__DIEPOOL__ += " ";
+ thisRoller.outputParams.__DIEPOOL__ += Math.abs(thisRoller.parameters.xdice[2]).toString().replace(/1|2|3|4|5|6/gi, function (matched) { return num3x[matched] });
+ }
+
+ // POINTS
+ thisRoller.outputParams.__RES_PTS_LBL__ = thisRoller.parameters.pointslabel;
+ if (thisRoller.parameters.dbody && thisRoller.parameters.dstun) { // if both are true, as for a healing that does both STUN and BODY, then show both values
+ thisRoller.outputParams.__RES_PTS__ = `${thisRoller.theResult.points.body}
${thisRoller.theResult.points.stun}
`;
+ } else if (thisRoller.parameters.dbody) { //just dbody is true (entangle)
+ thisRoller.outputParams.__RES_PTS__ = thisRoller.theResult.points.body;
+ } else { // just dstun or nothing is true, default to showing stun points
+ thisRoller.outputParams.__RES_PTS__ = thisRoller.theResult.points.stun;
+ }
+
+ // RESULTS: BODY, STUN, KNOCKBACK, LUCK
+ if ((thisRoller.parameters.xdice[0] == 0 && thisRoller.parameters.xdice[1] == 0 && thisRoller.parameters.xdice[2] == 0) || thisRoller.parameters.dcheck == true) { // if no dice rolling needs to happen, don't figure BODY/STUN, and hide Results bars
+ thisRoller.outputParams.__DIE_STRENGTH__ = " "; // reset the die strength (roll equation) to not show anything
+ thisRoller.outputParams.__RES_BASE_VIS__ = "none";
+ thisRoller.outputParams.__RES_MULT_VIS__ = "none";
+ thisRoller.outputParams.__TOHIT_VIS__ = "block";
+ thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
+ thisRoller.outputParams.__DS_TALL_VIS__ = "none";
+ thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
+ thisRoller.outputParams.__SIDECAR_VIS__ = "none";
+ } else {
+ if (thisRoller.parameters.mechanic == "l" || thisRoller.parameters.mechanic == "u") {
+ thisRoller.outputParams.__TOHIT_VIS__ = "none";
+ } else {
+ if (thisRoller.parameters.mechanic == "n") {
+ thisRoller.outputParams.__RES_STUN__ = thisRoller.theResult.normalstun;
+ thisRoller.outputParams.__RES_BODY__ = thisRoller.theResult.normalbody;
+ thisRoller.outputParams.__RES_KB__ = Math.max(0, thisRoller.theResult.normalbody - thisRoller.theResult.knockback);
+ } else if (thisRoller.parameters.mechanic == "k") {
+ if (thisRoller.parameters.loc != "none") thisRoller.outputParams.__RES_STUN__ = "--";
+ else thisRoller.outputParams.__RES_STUN__ = thisRoller.theResult.killingstun;
+ thisRoller.outputParams.__RES_BODY__ = thisRoller.theResult.killingbody;
+ thisRoller.outputParams.__RES_KB__ = Math.max(0, thisRoller.theResult.killingbody - thisRoller.theResult.knockback);
+ }
+ if (!thisRoller.parameters.dbody) thisRoller.outputParams.__RES_BODY__ = "--";
+ if (!thisRoller.parameters.dstun) thisRoller.outputParams.__RES_STUN__ = "--";
+ if (!thisRoller.parameters.dkb) thisRoller.outputParams.__RES_KB__ = "--";
+ }
+ }
+
+ // TARGETING
+ if (thisRoller.parameters.target.length > 0) { // if a target was designated
+ let targetTable = '';
+ let targetRow = '';
+ if (thisRoller.parameters.selective || thisRoller.parameters.loc !== "none") { // selective or location damage needed
+ targetTable = '__TABLE-ROWS__';
+ targetRow = ' 
__TARGET_ISHIT__
__TARGET_DCV__
';
+ } else { // only output images of targets
+ targetTable = '';
+ targetRow = '
__TARGET_ISHIT__
__TARGET_DCV__
';
+ }
+ if (thisRoller.theResult.targetData.length > 0) {
+ let targetKeysRegex = new RegExp(Object.keys(thisRoller.theResult.targetData[0]).join("|"), 'gi');
+ let targetAllRows = thisRoller.theResult.targetData.reduce((a, v, i) => {
+ return a + targetRow.replace(targetKeysRegex, (matched) => { return v[matched]; })
+ }, "");
+ thisRoller.outputParams.__TARGET_TABLE_HOOK__ = targetTable.replace("__TABLE-ROWS__", targetAllRows);
+ }
+ }
+
+ // NOTES
+ if (thisRoller.parameters.notes != "") {
+ thisRoller.outputParams.__NOTES__ = thisRoller.parameters.notes;
+ }
+
+ // VERBOSE
+ if (thisRoller.parameters.verbose) {
+ thisRoller.outputParams.__V_VIS__ = "block";
+ // alternating row colors
+ let rowbg = ["#ffffff", "#dedede"];
+ let verbtable = '';
+ let verbheader = 'ARG | USER | EVAL |
';
+ let verbrows = Object.keys(thisRoller.userparameters).filter((p) => { return p !== "notes"; })
+ .reduce((a, v, i) => {
+ return a + '' + v + ' | ' + thisRoller.userparameters[v] + ' | ' + (["act", "ocv"].includes(v) && thisRoller.parameters[v] == -100 ? "none" : thisRoller.parameters[v]) + ' |
'
+ }, verbheader);
+ thisRoller.outputParams.__V_NOTE__ = verbtable.replace("__TABLE-ROWS__", verbrows);
+ }
+ return;
+ };
+
+ const sendOutputToChat = (thisRoller) => {
+ let htmlForm = '__DIE_STRENGTH__
ACT __ACT_TGT__-
__ACT_ROLL__
__TOHIT_OCV_LBL__
__TOHIT_OCV__
__TOHIT_DCV_LBL__
__TOHIT_DCV__
__RES_PTS_LBL__
__RES_PTS__
__TARGET_TABLE_HOOK__
__DIE_STRENGTH__
ACT __ACT_TGT__-
__ACT_ROLL__
';
+
+ // join the output paramaters with a pipe, turn that into a regular expression, and feed that into the replace to modify the html form with our figured values
+ let chatString = htmlForm.replace(new RegExp(Object.keys(thisRoller.outputParams).join("|"), 'gi'),
+ (matched) => { return thisRoller.outputParams[matched]; }
+ );
+
+ sendChat(thisRoller.theSpeaker.chatSpeaker, chatString);
+ return;
+ };
+
+ const registerEventHandlers = () => {
+ on('chat:message', handleInput);
+ };
+
+ on("ready", () => {
+ 'use strict';
+ logsig();
+ versionInfo();
+ registerEventHandlers();
+ });
+
+ return {
+ };
+
+})();
+{ try { throw new Error(''); } catch (e) { API_Meta.HeRoll.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.HeRoll.offset); } }
\ No newline at end of file
diff --git a/HeroRoller/README.md b/HeroRoller/README.md
index d73d4881e1..63aaf57da0 100644
--- a/HeroRoller/README.md
+++ b/HeroRoller/README.md
@@ -1,7 +1,7 @@
# HEROLLER: CARE AND KEEPING
# Introduction
-Hero Roller (or, heroll, for short), works with the Hero character sheet. It takes a command line of arguments to describe the Hero power you want to use, then spits out all of the parameters that the roll would produce in game... hopefully in a visually appealing package.
+Hero Roller (or, heroll, for short), works with HERO 6th Edition Character sheets (*Hero System 6e* and *Hero System 6e Heroic*). It takes a command line of arguments to describe the Hero power you want to use, then spits out all of the parameters that the roll would produce in game... hopefully in a visually appealing package.
## Arguments Are... Our Friends
Each argument is detected by a the presence of a space followed by a double dash in the command line. The following example is 3.5d6 (effectively, 3d6+1d3) with a designation of the normal mechanic and a name of "Doom Smack".
diff --git a/HeroRoller/heroll.js b/HeroRoller/heroll.js
deleted file mode 100644
index 8621bfbd74..0000000000
--- a/HeroRoller/heroll.js
+++ /dev/null
@@ -1,1444 +0,0 @@
-/*
-=========================================================
-Name : Hero Roller (heroll)
-Version : 1.2.1
-Last Update : 4/2/2021
-GitHub : https://github.com/Roll20/roll20-api-scripts/tree/master/HeroRoller
-Roll20 Contact : timmaugh
-=========================================================
-*/
-var API_Meta = API_Meta || {};
-API_Meta.HeRoll = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 };
-{
- try { throw new Error(''); } catch (e) { API_Meta.HeRoll.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (13)); }
-}
-
-/*
- * ----------- DEVELOPMENT PATH ----------------------------
- -- skill roll template (mechanic bubble for target, drawn automatically)
- -- explosion rings (separate BODY/STUN, or BODY/PD for entangles) peeling off dice
- -- track END spent
- -- track charges/ammo
-*/
-const HeRoll = (() => {
-
- // ==================================================
- // VERSION
- // ==================================================
- const apiproject = 'HeRoll';
- API_Meta[apiproject].version = '1.2.1';
- const schemaVersion = 0.1;
- const vd = new Date(1618319851550);
-
- const versionInfo = () => {
- log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`);
- return;
- };
-
- const logsig = () => {
- // initialize shared namespace for all signed projects, if needed
- state.torii = state.torii || {};
- // initialize siglogged check, if needed
- state.torii.siglogged = state.torii.siglogged || false;
- state.torii.sigtime = state.torii.sigtime || Date.now() - 3001;
- if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) {
- const logsig = '\n' +
- ' _____________________________________________ ' + '\n' +
- ' )_________________________________________( ' + '\n' +
- ' )_____________________________________( ' + '\n' +
- ' ___| |_______________| |___ ' + '\n' +
- ' |___ _______________ ___| ' + '\n' +
- ' | | | | ' + '\n' +
- ' | | | | ' + '\n' +
- ' | | | | ' + '\n' +
- ' | | | | ' + '\n' +
- ' | | | | ' + '\n' +
- '______________|_|_______________|_|_______________' + '\n' +
- ' ' + '\n';
- log(`${logsig}`);
- state.torii.siglogged = true;
- state.torii.sigtime = Date.now();
- }
- return;
- };
-
- // ==================================================
- // TABLES
- // ==================================================
- const radioTargetTable = { // used with the radio_target attr
- "-1": "none",
- "1": "random",
- "2": "focus",
- "3": "headshot",
- "4": "highshot",
- "5": "bodyshot",
- "6": "lowshot",
- "7": "legshot",
- "8": "head",
- "9": "hand",
- "10": "shoulder",
- "11": "chest",
- "12": "stomach",
- "13": "vitals",
- "14": "thigh",
- "15": "leg",
- "16": "foot",
- "17": "arm",
- };
-
- const argAliasTable = { // aliases for the various accepted arguments, flattening them to what they map to in the parameters object
- "p": "template", // the template to use for the other parameter defaults
- "pow": "template",
- "pwr": "template",
- "power": "template",
- "t": "template",
- "tmp": "template",
- "template": "template",
-
- "rm": "mechanic", // the die-rolling mechanic to use (normal, killing, or luck)
- "rollmech": "mechanic",
- "rollmechanic": "mechanic",
- "m": "mechanic",
- "mech": "mechanic",
- "mechanic": "mechanic",
-
- "db": "dbody", // whether the roll should track a BODY value
- "dbody": "dbody",
- "doesbody": "dbody",
-
- "ds": "dstun", // whether the roll should track a STUN value
- "dstun": "dstun",
- "doesstun": "dstun",
-
- "dkb": "dkb", // whether the roll should track a knockback value
- "dknockback": "dkb",
- "doesknockback": "dkb",
-
- "xkb": "kbdicemod", // knockback modifier; how many dice to add to base 2d6
- "extrakb": "kbdicemod",
- "kbdice": "kbdicemod",
- "kbdicemod": "kbdicemod",
-
- "xs": "stunmod", // extra STUN modifier; added to base
- "xstun": "stunmod",
- "extrastun": "stunmod",
- "stunmod": "stunmod",
-
- "l": "loc", // a predefined hit location, or command to generate a hit location
- "loc": "loc",
- "location": "loc",
-
- "pl": "pointslabel", // label for the Points Results (if used)
- "plbl": "pointslabel",
- "plabel": "pointslabel",
- "ptsl": "pointslabel",
- "ptslbl": "pointslabel",
- "ptslabel": "pointslabel",
- "pointsl": "pointslabel",
- "pointslbl": "pointslabel",
- "pointslabel": "pointslabel",
-
- "pn": "powername", // name for the power
- "pname": "powername",
- "powername": "powername",
-
- "a": "act", // if there is activation roll for the power
- "act": "act",
- "activation": "act",
-
- "c": "primarycolor", // color for block elements, also drives secondary color and text color
- "col": "primarycolor",
- "color": "primarycolor",
- "pc": "primarycolor",
- "primarycolor": "primarycolor",
-
- "d": "dice", // number of dice
- "dice": "dice",
-
- "of": "outputformat", // whether the output should be tall or sidecar
- "output": "outputformat",
- "format": "outputformat",
- "sc": "outputformat", // this one added specifically for value-less args (i.e., "--sc") to trigger the default value
- "outputformat": "outputformat",
-
- "mental": "useomcv", // whether to use the mental OCV instead of OCV
- "um": "useomcv",
- "omcv": "useomcv",
- "useomcv": "useomcv",
-
- "o": "ocv", // OCV override, use this if there is no character sheet to draw from
- "ocv": "ocv",
-
- "n": "notes", // user notes
- "notes": "notes",
-
- "xd": "extradice", // extra dice to be added to the dice (could be second source)
- "xdice": "extradice",
- "extradice": "extradice",
-
- "v": "verbose", // logging of values to the chat for easier debugging
- "verbose": "verbose",
-
- "r": "recall", // for recalling the last roll or the last roll for this user
- "rc": "recall",
- "recall": "recall",
-
- "tgt": "target", // target of attack
- "target": "target",
-
- "s": "selective", // whether multiple targets get indiv to-hit rolls
- "sel": "selective",
- "selective": "selective",
-
- "as": "source", // character from which to draw OCV (otherwise will be speaker)
- "source": "source"
- };
-
- const templateAliasTable = { // aliases for the various accepted templates
- "a": "a", // AID
- "aid": "a",
-
- "b": "b", // BLAST
- "blast": "b",
-
- "di": "di", // DISPEL
- "dispel": "di",
-
- "dr": "dr", // DRAIN
- "drain": "dr",
-
- "e": "e", // ENTANGLE
- "entangle": "e",
-
- "f": "f", // FLASH
- "flash": "f",
-
- "ha": "ha", // HAND ATTACK
- "hattack": "ha",
- "handattack": "ha",
-
- "he": "he", // HEALING
- "heal": "he",
- "healing": "he",
-
- "hk": "ka", // KILLING ATTACK
- "rk": "ka",
- "k": "ka",
- "ka": "ka",
- "kattack": "ka",
- "killingattack": "ka",
-
- "mb": "mb", // MENTAL BLAST
- "mblast": "mb",
- "mentalblast": "mb",
-
- "mi": "mi", // MENTAL ILLUSIONS
- "millusions": "mi",
- "mentalillusions": "mi",
- "illusions": "mi",
-
- "mc": "mc", // MIND CONTROL
- "mcontrol": "mc",
- "mindcontrol": "mc",
-
- "p": "p", // POINTS
- "pts": "p",
- "points": "p",
-
- "t": "t", // TRANSFORM
- "transform": "t",
-
- "l": "l", // LUCK
- "luck": "l",
-
- "u": "u", // UNLUCK
- "unluck": "u",
-
- "c": "c", // CUSTOM (DEFAULT)
- "def": "c",
- "cust": "c",
- "default": "c",
- "custom": "c",
- };
-
- const noValArgDefaults = { // used with command line arguments for which we don't need a value supplied (just their presence should trigger behavior)
- dbody: true,
- dstun: true,
- dkb: true,
- outputformat: 'sc',
- useomcv: true,
- verbose: true,
- recall: "", // the default case for this will be the current speaker id, so it is grabbed at the time it is needed
- selective: true,
- loc: "random",
- };
-
- // ==================================================
- // UTILITY FUNCTIONS
- // ==================================================
- const splitArgs = (a) => { return a.split(":") };
- const joinVals = (a) => { return [a.slice(0)[0], a.slice(1).join(":").trim()]; };
- const lookFor = (arg) => (a) => { return a === arg; };
- const lookForArg = (arg) => (a) => { return a[0] === arg; };
- const lookForVal = (arg) => (a) => { return a[1] === arg; };
- const aliasesFrom = (o) => (a) => { return [((a[0] in o) ? o[a[0]] : a[0]), a[1]]; };
- const getKeyByValue = (object, value) => {
- return Object.entries(object)
- .filter(lookForVal(value))
- .map((a) => { return a[0]; });
- };
- const escapeRegExp = (string) => { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); };
-
- const getChar = (query, pid) => { // find a character where info is an identifying piece of information (id, name, or token id)
- let character;
- let qrx = new RegExp(escapeRegExp(query), 'i');
- let charsIControl = findObjs({ type: 'character' });
- charsIControl = playerIsGM(pid) ? charsIControl : charsIControl.filter(c => c.get('controlledby').split(',').includes(pid));
- character = charsIControl.filter(c => c.id === query)[0] ||
- charsIControl.filter(c => c.id === (getObj('graphic', query) || { get: () => { return '' } }).get('represents'))[0] ||
- charsIControl.filter(c => c.get('name') === query)[0] ||
- charsIControl.filter(c => qrx.test(c)).reduce((m, v) => {
- let d = getEditDistance(query, v);
- return !m.length || d < m[1] ? [v, d] : m;
- }, [])[0];
- return character;
- };
- const getEditDistance = (a, b) => {
- if (a.length === 0) return b.length;
- if (b.length === 0) return a.length;
-
- var matrix = [];
-
- // increment along the first column of each row
- var i;
- for (i = 0; i <= b.length; i++) {
- matrix[i] = [i];
- }
-
- // increment each column in the first row
- var j;
- for (j = 0; j <= a.length; j++) {
- matrix[0][j] = j;
- }
-
- // Fill in the rest of the matrix
- for (i = 1; i <= b.length; i++) {
- for (j = 1; j <= a.length; j++) {
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
- matrix[i][j] = matrix[i - 1][j - 1];
- } else {
- matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // substitution
- Math.min(matrix[i][j - 1] + 1, // insertion
- matrix[i - 1][j] + 1)); // deletion
- }
- }
- }
-
- return matrix[b.length][a.length];
- };
-
- const extendFromArray = (o, a) => {
- return a.reduce((o, e) => {
- o[e[0]] = e.slice(1).join();
- return o;
- }, o);
- };
-
- const storeState = (thisRoller) => {
- state.heroll[thisRoller.theSpeaker.id] = {};
- state.heroll[thisRoller.theSpeaker.id].parameters = thisRoller.parameters;
- state.heroll[thisRoller.theSpeaker.id].userparameters = thisRoller.userparameters;
- state.heroll[thisRoller.theSpeaker.id].theResult = thisRoller.theResult;
- state.heroll.lastSpeaker = thisRoller.theSpeaker.id;
- return;
- };
-
- const recallState = (thisRoller) => {
- let speakID = thisRoller.recallParameters.speakID || thisRoller.theSpeaker.id;
- if (typeof state.heroll[speakID] !== undefined) {
- thisRoller.parameters = state.heroll[speakID].parameters;
- thisRoller.userparameters = state.heroll[speakID].userparameters;
- thisRoller.theResult = state.heroll[speakID].theResult;
-
- // this will drive whether we load a previous roll
- thisRoller.recallParameters.recall = true;
- }
- return;
- };
-
- const roll3d6 = () => {
- return randomInteger(6) + randomInteger(6) + randomInteger(6);
- };
-
- const getCountsFromArray = (rolls) => {
- // takes an array of numbers and returns an object structured as { NUMBER : COUNT }, where COUNT represents the # of appearances of that NUMBER
- return (_.reduce(rolls || [], (m, r) => {
- m[r] = (m[r] || 0) + 1;
- return m;
- }, {}));
- };
-
- const getArrayFromCounts = (c) => {
- // takes an object with properties structured as { NUMBER : COUNT } and returns an array of those NUMBERs repeated COUNT times
- return _.reduce(c, (m, v, k) => {
- _.times(v, () => { m.push(k); });
- return m;
- }, []);
- };
-
- const hexToRGB = (h) => {
- let r = 0, g = 0, b = 0;
-
- // 3 digits
- if (h.length == 4) {
- r = "0x" + h[1] + h[1];
- g = "0x" + h[2] + h[2];
- b = "0x" + h[3] + h[3];
- // 6 digits
- } else if (h.length == 7) {
- r = "0x" + h[1] + h[2];
- g = "0x" + h[3] + h[4];
- b = "0x" + h[5] + h[6];
- }
- return [+r, +g, +b];
- };
-
- const RGBToHex = (r, g, b) => {
- r = r.toString(16);
- g = g.toString(16);
- b = b.toString(16);
-
- if (r.length == 1)
- r = "0" + r;
- if (g.length == 1)
- g = "0" + g;
- if (b.length == 1)
- b = "0" + b;
-
- return "#" + r + g + b;
- };
-
- const getAltColor = (primarycolor) => {
- let pc = hexToRGB(primarycolor);
- let sc = [0, 0, 0];
-
- for (let i = 0; i < 3; i++) {
- sc[i] = Math.floor(pc[i] + (.35 * (255 - pc[i])));
- }
-
- return RGBToHex(sc[0], sc[1], sc[2]);
- };
-
- const getTextColor = (h) => {
- let hc = hexToRGB(h);
- return (((hc[0] * 299) + (hc[1] * 587) + (hc[2] * 114)) / 1000 >= 128) ? "#000000" : "#ffffff";
- };
-
- const getTheSpeaker = (msg) => {
- let characters = findObjs({ _type: 'character' });
- let speaking;
- characters.forEach(function (chr) { if (chr.get('name') === msg.who) speaking = chr; });
- if (speaking) {
- speaking.speakerType = "character";
- speaking.localName = speaking.get("name");
- speaking.radio_target = getCharacterAttr("radio_target", speaking.id);
- speaking.ocvFinal = getCharacterAttr("OCV", speaking.id);
- speaking.ocvBase = getCharacterAttr("ocv_base", speaking.id);
- speaking.ocvMods = speaking.ocvFinal - speaking.ocvBase;
- } else {
- speaking = getObj('player', msg.playerid);
- speaking.speakerType = "player";
- speaking.localName = speaking.get("displayname");
- speaking.radio_target = "none";
- speaking.ocvFinal = 0;
- speaking.ocvBase = 0;
- speaking.ocvMods = 0;
- }
- speaking.playerid = msg.playerid;
- speaking.chatSpeaker = `${speaking.speakerType}|${speaking.id}`;
- return speaking;
- };
-
- const getCharacterAttr = (attr, charid) => {
- let attrDefaultTable = {
- "radio_target": -1,
- "OCV": 0,
- "ocv_base": 0,
- "DCV": 0,
- "DMCV": 0,
- }
- let defvalue = attrDefaultTable[attr];
- let retAttr = findObjs({ type: 'attribute', characterid: charid, name: attr })[0] || createObj("attribute", { name: attr, current: defvalue, characterid: charid });
- retAttr.currval = retAttr.get('current') || defvalue;
- return retAttr.currval;
- };
-
- const addAttribute = (attr, value, charid) => {
- let tempAttr = createObj("attribute", { name: attr, current: value, characterid: charid });
- return tempAttr;
- };
-
- const addAbility = (ability, value, charid) => {
- let tempAbil = createObj("ability", { name: ability, action: value, characterid: charid });
- return tempAbil;
- };
-
- const getLocationData = (loc) => {
- const locationDataTable = {
- head: { ocvmod: -8, ksx: 5, nsx: 2, bx: 2, hitlabel: "Head" },
- hand: { ocvmod: -6, ksx: 1, nsx: 0.5, bx: 0.5, hitlabel: "Hand" },
- arm: { ocvmod: -5, ksx: 2, nsx: 0.5, bx: 0.5, hitlabel: "Arm" },
- shoulder: { ocvmod: -5, ksx: 3, nsx: 1, bx: 1, hitlabel: "Shoulder" },
- chest: { ocvmod: -3, ksx: 3, nsx: 1, bx: 1, hitlabel: "Chest" },
- stomach: { ocvmod: -7, ksx: 4, nsx: 1.5, bx: 1, hitlabel: "Stomach" },
- vitals: { ocvmod: -8, ksx: 4, nsx: 1.5, bx: 2, hitlabel: "Vitals" },
- thigh: { ocvmod: -4, ksx: 2, nsx: 1, bx: 1, hitlabel: "Thigh" },
- leg: { ocvmod: -6, ksx: 2, nsx: 0.5, bx: 0.5, hitlabel: "Leg" },
- foot: { ocvmod: -8, ksx: 1, nsx: 0.5, bx: 0.5, hitlabel: "Foot" },
- headshot: { ocvmod: -4 },
- highshot: { ocvmod: -2 },
- bodyshot: { ocvmod: -1 },
- lowshot: { ocvmod: -2 },
- legshot: { ocvmod: -4 },
- focus: { ocvmod: -4, ksx: randomInteger(3), nsx: 1, bx: 1, hitlabel: "Focus" },
- random: { ocvmod: 0 },
- none: { ocvmod: 0, ksx: randomInteger(3), hitlabel: "none" },
- };
- return locationDataTable[loc] || locationDataTable.none;
- };
-
- const specHitLocation = (roll) => {
- let hit;
- if (roll < 6) hit = "head";
- else if (roll === 6) hit = "hand";
- else if (roll < 9) hit = "arm";
- else if (roll === 9) hit = "shoulder";
- else if (roll < 12) hit = "chest";
- else if (roll === 12) hit = "stomach";
- else if (roll === 13) hit = "vitals";
- else if (roll === 14) hit = "thigh";
- else if (roll < 17) hit = "leg";
- else hit = "foot";
-
- return hit;
- };
-
- const genHitLocation = (shot) => {
- let roll;
- switch (shot) {
- case "headshot":
- roll = randomInteger(6) + 3;
- break;
-
- case "highshot":
- roll = randomInteger(6) + randomInteger(6) + 1;
- break;
-
- case "bodyshot":
- roll = randomInteger(6) + randomInteger(6) + 4;
- break;
-
- case "lowshot":
- roll = Math.min(18, randomInteger(6) + randomInteger(6) + 7);
- break;
-
- case "legshot":
- roll = randomInteger(6) + 12;
- break;
-
- case "random":
- case "any":
- default:
- roll = roll3d6();
- break;
- }
- return roll;
- };
-
- const getDice = (n = 1, s = 6) => { // n is count of dice, s is sides
- let dice = [];
- for (let i = 0; i < n; i++) {
- dice.push(randomInteger(s));
- }
- return dice;
- };
-
- const normalizeDice = (dice) => {
- while (dice[2] >= 2) { // adder values at/over 2 should add to a 1d3, instead
- dice[1]++;
- dice[2] -= 2;
- }
- while (dice[2] <= -2) { // adder values at/below -2 should reduce a 1d3, instead
- if (dice[1] == 0) { // if d3 is already at 0, it should flow over to the d6
- if (dice[0] == 0) { // if the d6 is already 0, then the d3 should stay at 0 -- don't do anything
- } else { // the d6 is positive and can be decremented, meaning that the d3 number can increment
- dice[0]--;
- dice[1]++;
- }
- } else { // the d3 is positive and can be decremented
- dice[1]--;
- }
- dice[2] += 2;
- }
- while (dice[1] >= 2) { // every 2d3 should render 1d6 -- mostly important when combining dice and extradice arrays
- dice[0]++;
- dice[1] -= 2;
- }
- if (dice[0] != Math.floor(dice[0])) {
- const diff = Math.floor(dice[0] * 1000 - Math.floor(dice[0]) * 1000);
- dice[0] = Math.floor(dice[0]);
- if (diff <= 333) { // fractional dice up to this point should give a +1 to the adder
- dice[2]++;
- } else if (diff <= 666) { // fractional dice up to this point should give a half die (1d3)
- dice[1]++;
- if (dice[1] == 2) { // 2d3 should render 1d6
- dice[0]++;
- dice[1] = 0;
- }
- } else if (diff > 666) { // fractional dice up to this point should round to 1d6-1
- dice[0]++;
- dice[2]--;
- }
- }
- let d6 = Number(dice[0]) + Number(dice[1]) / 2;
- dice[4] = d6 + 'd6';
- if (dice[2] != 0) {
- dice[4] += (dice[2] > 0 ? "+" : "-") + Math.abs(dice[2]);
- }
- return dice;
- };
-
- const prioritizeArg = (arg, theFunc, thisRoller, ...passArgs) => {
- // see if the user supplied this argument
- if (thisRoller.userparameters[arg] !== "--") {
- // write sanitized value to the parameters
- thisRoller.parameters[arg] = validateInput(thisRoller.userparameters[arg].toLowerCase(), arg, thisRoller.userparameters[arg]);
- // call the process function specific to the argument being processed, spread the arguments that had beeen gathered in the rest syntax
- theFunc(thisRoller, ...passArgs);
- }
- // this argument got prioritized to run before all arguments were processed, so remove it from our set of arguments requiring processing
- thisRoller.knownparams[thisRoller.knownparams.findIndex(lookFor(arg))] = thisRoller.knownparams.pop();
- return;
- };
-
- const validateInput = (input, test, origCap) => {
-
- let valInput;
-
- // if the user didn't supply an argument and the argument has a noVal default, use it
- if (noValArgDefaults.hasOwnProperty(test) && input === "") return noValArgDefaults[test];
-
- switch (test) {
- case "template": // the template to use for the other parameter defaults
- if (input.toLowerCase() in templateAliasTable) { // if found, supply the value (no need for a not-found test; template is already defaulted to "c", and will only change if this line passes)
- valInput = templateAliasTable[input.toLowerCase()];
- }
- break;
-
- case "mechanic":
- switch (input) {
- case "l":
- case "luck":
- valInput = "l";
- break;
-
- case "u":
- case "unluck":
- valInput = "u";
- break;
-
- case "k":
- case "killing":
- valInput = "k";
- break;
-
- case "n":
- case "normal":
- default:
- // if nothing is defined properly, stay with the default normal mechanic
- valInput = "n";
- break;
- }
- break;
-
- case "useomcv":
- case "verbose":
- case "dbody":
- case "dstun":
- case "dkb":
- switch (input) {
- case "y":
- case "yes":
- case "t":
- case "true":
- valInput = true;
- break;
-
- case "n":
- case "no":
- case "f":
- case "false":
- default:
- valInput = false;
- break;
- }
- break;
-
- case "dice":
- valInput = [1, 0, 0, "--", "1d6"]; // this will represent the dice[] array--> [#d6, #d3, adder, rollmechanic shorthand, rebuilt equation]
- if (['check', 'skill', 'none'].includes(input)) {
- valInput = [0, 0, 0, "--", "", "check"]; // extra element to trigger check
- } else {
- let nomech = input;
- var mecharray = ["l", "k", "n", "u"];
- for (let i = 0, len = mecharray.length; i < len; i++) {
- if (input.includes(mecharray[i])) {
- nomech = input.replace(mecharray[i], "");
- valInput[3] = mecharray[i];
- }
- }
- // check if the roll contains 'd6'
- if (!nomech.includes("d6")) {
- if (!isNaN(Number(nomech))) { // if there is no 'd6' in the command line, but the value of the dice argument is a number, just use that as the d6 value and normalize after that
- valInput[0] = Number(nomech);
- }
- } else {
- valInput[0] = (isNaN(nomech.split("d6")[0]) ? valInput[0] : nomech.split("d6")[0]);
- valInput[2] = (isNaN(nomech.split("d6")[1]) ? valInput[2] : Number(nomech.split("d6")[1]));
- }
- }
- valInput = normalizeDice(valInput);
- break;
-
- case "extradice":
- valInput = [0, 0, 0, "--", "0d6"]; // set to default values in case no number is provided
- if (isNaN(Number(input))) {
- } else {
- valInput[0] += Number(input);
- valInput = normalizeDice(valInput);
- }
- break;
-
- case "ocv":
- case "act":
- case "kbdicemod":
- case "stunmod":
- if (isNaN(Number(input))) {
- valInput = 0; //default to 0 if no number is provided
- } else {
- valInput = Number(input);
- }
- break;
-
- case "notes":
- case "powername":
- case "pointslabel":
- valInput = origCap; // get the original version of the capitalization for anything that will go straight to display
- break;
-
- case "primarycolor":
- var colorRegX = /(^#?[0-9A-F]{6}$)|(^#?[0-9A-F]{3}$)/i;
- valInput = '#' + (colorRegX.test(input) ? input.replace('#', '') : 'b0c4de');
- break;
-
- case "loc": // LOCATION
- const valLoc = [
- "head", "hand", "arm", "shoulder", "chest", "stomach", "vitals", "thigh", "leg", "foot", "focus",
- "headshot", "highshot", "bodyshot", "lowshot", "legshot",
- "random", "none",
- "hands", "arms", "shoulders", "thighs", "legs", "feet",
- ];
-
- if (valLoc.indexOf(input) === -1) { // not a valid location, but the user tried to set a location, so default should go to "random" instead of "none"
- valInput = "random";
- } else { // a valid location, but we need to reduce plural entries; join the keys with a pipe, then use that as the seed for a regexp that will drive a match
- const toSingular = {
- "hands": "hand",
- "arms": "arm",
- "shoulders": "shoulder",
- "thighs": "thigh",
- "legs": "leg",
- "feet": "foot",
- };
-
- valInput = input.replace(new RegExp(Object.keys(toSingular).join("|"), 'gi'), function (matched) { return toSingular[matched]; });
- }
- break;
-
- case "outputformat":
- switch (input) {
- case "sc":
- case "side":
- case "sidecar":
- valInput = "sc";
- break;
-
- case "t":
- case "tall":
- default:
- valInput = "tall";
- break;
- }
- break;
- case "target":
- valInput = origCap.split(/[\s,]/); // split on white space or comma
- break;
-
- case "selective":
- valInput = true;
- break;
- case "source":
- valInput = origCap;
- break;
- }
-
- return valInput;
- };
-
- // ==================================================
- // PROCESS FUNCTIONS
- // ==================================================
- const setDefaults = (msg, thisRoller) => { //initializes various parameters
- // SPEAKER INFO
- thisRoller.theSpeaker = getTheSpeaker(msg);
-
- // STATE VARIABLE STORAGE
- state.heroll = state.heroll || {};
- state.heroll[thisRoller.theSpeaker.id] = state.heroll[thisRoller.theSpeaker.id] || {};
- state.heroll.lastSpeaker = state.heroll.lastSpeaker || "none";
- if (state.heroll.lastSpeaker !== "none") state.heroll[state.heroll.lastSpeaker] = state.heroll[state.heroll.lastSpeaker] || {};
- thisRoller.recallParameters = {
- recall: false,
- speakID: thisRoller.theSpeaker.id,
- };
-
- // USER INPUT
- thisRoller.userparameters = { // record user provided values; default them to "--" for "not provided", set appropriately when the args are evaluated
- template: "--",
- powername: "--",
- mechanic: "--",
- dbody: "--",
- dstun: "--",
- dkb: "--",
- kbdicemod: "--",
- stunmod: "--",
- loc: "--",
- outputformat: "--",
- dice: "--",
- act: "--",
- primarycolor: "--",
- pointslabel: "--",
- useomcv: "--",
- ocv: "--",
- extradice: "--",
- recall: "--",
- verbose: "--",
- target: "--",
- selective: "--",
- notes: "",
- source: "--"
- };
-
- // KNOWN PARAMETER KEYS // for later iteration
- thisRoller.knownparams = Object.keys(thisRoller.userparameters);
-
- // VALIDATED PARAMETERS
- thisRoller.parameters = { // validated user settings (passed through validateInput() function)
- template: "c",
- powername: "Attack",
- mechanic: "n",
- dbody: true,
- dstun: true,
- dkb: true,
- kbdicemod: 0,
- stunmod: 0,
- loc: "none", // location if specified by user; can include 'shot' locations (i.e., 'headhshot')
- dice: [1, 0, 0, "--", "1d6"], // d6, d3, adder, rollmechanic shorthand, roll equation without shorthand
- act: -100, // activation for the try; invalid entry will set this to 0, so -100 is a trip that it isn't set
- primarycolor: "#b0c4de",
- pointslabel: "POINTS",
- outputformat: "tall",
- useomcv: false,
- ocv: -100,
- notes: "",
- extradice: [0, 0, 0, "--", "0d6"], // d6, d3, adder, shorthand (not used), roll equation
- recall: "--",
- target: [], // ids of any targets supplied
- selective: false, // whether to roll individual to-hits for each supplied target
- verbose: false,
- source: "--",
-
- //inaccessible to user
- outputas: "attack",
- actroll: roll3d6(), // activation roll, only used if the activation argument is invoked by the user
- xdice: [0, 0, 0, "--", "0d6"], // for the combination of dice and extradice
- dcheck: false, // if this is a check roll, not requiring a result output
- };
-
- // OUTPUT PARAMETERS
- thisRoller.outputParams = { // HTML variables for formatting output
- __CHARNAME__: thisRoller.theSpeaker.localName, // character name
- __MECH__: "N", // text in the mechanic bubble
- __MECH_VIS__: "block", // whether the mechanic bubble is visible (block; none)
- __MECH_BGC__: "#b0c4de", // background-color for mechanic bubble
- __POWERNAME__: "Power Name", // power name color text
- __PRIMARY_BG_COL__: "#b0c4de", // background-color for any element (like power name) that is to match the primarycolor parameter
- __PRIMARY_TEXT_COL__: "#ffffff", // text color for anything with the primary background color
- __DS_TALL_VIS__: "block", // whether the die strength (ie, 6d6) in the tall format should be visible (block; none)
- __DIE_STRENGTH__: "1d6", // value for the die strength, wherever it is used
- __ACT_VIS__: "none", // whether the Activation block is visible (block; none)
- __ACT_TGT__: "", // target for the activation roll
- __ACT_ROLL__: "", // result of the activation roll
- __TOHIT_VIS__: "block", // whether the To Hit Bar (OCV, ROLL, HIT DCV) should be visible
- __TOHIT_OCV_LBL__: "OCV", // label for the OCV box
- __TOHIT_OCV__: "--", // to hit OCV
- __TOHIT_ROLL__: "", // to hit roll result
- __TOHIT_DCV_LBL__: "DCV", // label for the DCV box
- __TOHIT_DCV__: "--", // hit DCV
- __SECONDARY_BG_COL__: "#d8e1ee", // color for secondary-color elements (like die pool); figured by javascript as shade of the primary color
- __SECONDARY_TEXT_COL__: "#ffffff", // text color for anything with the secondary background color
- __LOC_TALL_VIS__: "none", // whether the Hit Location bar in the tall format should be visible (block; none)
- __LOC_SC_VIS__: "none", // whether the Hit Location bar in the sidecar format should be visible (block; none)
- __LOC__: "Location", // Hit location
- __DIEPOOL__: "", // Comma-delimited set of dice resulting from the roll (as well as any adders)
- __DIEPOOL_TALL_VIS__: "block", // whether the die pool in the tall format should be visible (block for tall; none for sidecar)
- __RES_MULT_VIS__: "none", // visibility for the Results Bar (with Location), used for hit-location multiples (block; none)
- __BODY_MULT__: "1", // BODY multiplier (multiplied against the BODY remaining after defenses are applied)
- __STUN_MULT__: "1", // STUN multiplier (multiplied agianst the STUN remaining after defenses) -- could be NStun if it is a Normal mechanic attack
- __RES_BODY__: "", // BODY rolled on the dice
- __RES_STUN__: "", // STUN rolled on the dice
- __RES_KB__: "", // KB done by attack
- __RES_BASE_VIS__: "block", // visibility for the Results Bar (without Location), used if location = none (block; none)
- __RES_PTS_VIS__: "none", // visibility for the Results Bar (Points), used for Points output (block; none)
- __RES_PTS_LBL__: "POINTS", // text for Points label in Results Bar (Points)
- __RES_PTS__: "", // Points done (only used for points output)
- __V_VIS__: "none", // visibility for verbose output (block; none)
- __V_NOTE__: "", // place for note in verbose output
- __SIDECAR_VIS__: "none", // visibility for all sidecar elements (works opposite of tall based elements) (block for sidecar; none for tall)
- __NOTES__: "", // text from the notes argument
- __TARGET_TABLE_HOOK__: "", // hook for the rows of target information, if one/more is specified
- };
-
- // RESULT ROLL
- thisRoller.theResult = {
- tohitroll: roll3d6(), // base to-hit roll
- theroll: [], // dice pool of the roll result
- dicecounts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }, // count of how many of each was rolled
- d6counts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }, // count of how many times each was rolled, only for d6 -- used to display results differently (d6 vs d3) in output
- d3counts: { 1: 0, 2: 0, 3: 0 }, // count of how many times each was rolled, only for d3 -- used to display results differently (d6 vs d3) in output
- normalstun: 0,
- normalbody: 0,
- killingbody: 0,
- points: { stun: 0, body: 0 },
- location: { ocvmod: 0, ksx: 1, nsx: 1, bx: 1, hitlabel: "" },
- kbroll: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 },
- knockback: 0,
- targetData: [], // if targets are designated, this will hold any relevant data (pic, to hit roll, location, etc.)
- };
- return;
- };
-
- const processRecall = (thisRoller, args) => {
- // the recall speaker was already set to current speaker with the defaults, so we only need to overwrite it if there is a value supplied
- if (thisRoller.userparameters.recall !== "") {
- thisRoller.recallParameters.speakID = (thisRoller.userparameters.recall === "last" && state.heroll.lastSpeaker !== "none") ? state.heroll.lastSpeaker : thisRoller.userparameters.recall;
- }
-
- // load previous roll
- recallState(thisRoller);
-
- if (thisRoller.recallParameters.recall === true) {
- // that just overwrote our user supplied arguments, so reapply with only those allowed
- // first, what isn't allowed:
- const dropProps = ["dice", "extradice", "recall", "template"];
- extendFromArray(thisRoller.userparameters, args.filter((a) => { return !dropProps.includes(a[0]); }));
- }
- return;
- };
-
- const setTemplateDefaults = (thisRoller) => {
- // a 'template' is a slate of parameters to set general behaviors
- // for instance, an Aid power uses the dice differently from an attack, etc.
- // the boilerplate attack template is "c" (custom); boilerplate points is "p" (points)
- // the template is initialized as "c" in the setDefaults function
- let templates = {
- a: { template: "a", powername: "Aid", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF AID", useomcv: false, outputas: "points" },
- b: { template: "b", powername: "Blast", mechanic: "n", dbody: true, dstun: true, dkb: true, primarycolor: "#5ac7ff", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
- c: { template: "c", powername: "Attack", mechanic: "n", dbody: true, dstun: true, dkb: true, primarycolor: "#b0c4de", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
- di: { template: "di", powername: "Dispel", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#b0c4de", pointslabel: "POINTS OF DISPEL", useomcv: false, outputas: "points" },
- dr: { template: "dr", powername: "Drain", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF DRAIN", useomcv: false, outputas: "points" },
- e: { template: "e", powername: "Entangle", mechanic: "n", dbody: true, dstun: false, dkb: false, primarycolor: "#b0c4de", pointslabel: "ENTANGLE BODY", useomcv: false, outputas: "points" },
- f: { template: "f", powername: "Flash", mechanic: "n", dbody: true, dstun: false, dkb: false, primarycolor: "#b0c4de", pointslabel: "SEGMENTS OF FLASH", useomcv: false, outputas: "points" },
- ha: { template: "ha", powername: "Hand Attack", mechanic: "n", dbody: true, dstun: true, dkb: true, primarycolor: "#0289ce", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
- he: { template: "he", powername: "Healing", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF HEALING", useomcv: false, outputas: "points" },
- ka: { template: "ka", powername: "Killing Attack", mechanic: "k", dbody: true, dstun: true, dkb: true, primarycolor: "#ff5454", pointslabel: "POINTS", useomcv: false, outputas: "attack" },
- mb: { template: "mb", powername: "Mental Blast", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#c284ed", pointslabel: "POINTS", useomcv: true, outputas: "attack" },
- mi: { template: "mi", powername: "Mental Illusions", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#c284ed", pointslabel: "POINTS OF ILLUSION", useomcv: true, outputas: "points" },
- mc: { template: "mc", powername: "Mind Control", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#c284ed", pointslabel: "POINTS OF MIND CONTROL", useomcv: true, outputas: "points" },
- p: { template: "p", powername: "Points Power", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#9d41e8", pointslabel: "POINTS", useomcv: false, outputas: "points" },
- t: { template: "t", powername: "Transform", mechanic: "n", dbody: false, dstun: true, dkb: false, primarycolor: "#ffaa7b", pointslabel: "POINTS OF TRANSFORM", useomcv: false, outputas: "points" },
- l: { template: "l", powername: "Luck", mechanic: "l", dbody: false, dstun: false, dkb: false, primarycolor: "#35e54e", pointslabel: "POINTS OF LUCK", useomcv: false, outputas: "points" },
- u: { template: "u", powername: "Unluck", mechanic: "u", dbody: false, dstun: false, dkb: false, primarycolor: "#FF453B", pointslabel: "POINTS OF UNLUCK", useomcv: false, outputas: "points" },
- };
- Object.assign(thisRoller.parameters, templates[thisRoller.parameters.template]);
- return;
- };
-
- const processArguments = (thisRoller) => {
- // process each of the known parameters appearing in the command line
- // if the userparameters version is altered from the default, pass that value through validateInput to return the sanitized version
- thisRoller.knownparams.map((p) => { thisRoller.parameters[p] = thisRoller.userparameters[p] === "--" ? thisRoller.parameters[p] : validateInput(thisRoller.userparameters[p].toLowerCase(), p, thisRoller.userparameters[p]); });
-
- // change unrecognized args with no value to have "NA" instead (used in verbose output)
- Object.keys(thisRoller.userparameters).filter((a) => { return !thisRoller.knownparams.includes(a); })
- .map((a) => { thisRoller.userparameters[a] === "" ? thisRoller.userparameters[a] = "NA" : thisRoller.userparameters[a]; });
-
- // set the roll mechanic parameter to the shorthand, if present
- // the roll mechanic property can be set in three places; the priority should be: template < explicit mech argument < shorthand
- thisRoller.parameters.mechanic = (thisRoller.parameters.dice[3] != "--" ? thisRoller.parameters.dice[3] : thisRoller.parameters.mechanic);
-
- // test if the dice parameter came back with the extra element denoting this is a check (should have 5 elements: d6, d3, adder, mechanic, equation)
- if (thisRoller.parameters.dice.length > 5) {
- thisRoller.parameters.dcheck = true; // this will trigger no-result output, later
- thisRoller.parameters.dice.pop(); // remove extra element
- }
- return;
- };
-
- const processTargets = (thisRoller) => {
- if (thisRoller.parameters.mechanic === "l" || thisRoller.parameters.mechanic === "u") return; // no targets for luck & unluck
- let attr = thisRoller.parameters.useomcv ? "DMCV" : "DCV"; // decide which attribute we're using for the defensive value, if character sheet is present
- if (thisRoller.parameters.target.length > 0) { // if a target was designated -- "target" here is an array of targets
- thisRoller.theResult.targetData = thisRoller.parameters.target
- .filter((a) => { return getObj('graphic', a); }) // limit to only those that are properly formatted (filter out the bad)
- .map((a) => { // each should output an object of key:value pairs for the info we need
- let loc = getResultLocation(thisRoller);
- let thr = thisRoller.parameters.selective === true ? roll3d6() : thisRoller.theResult.tohitroll;
- let token = getObj('graphic', a);
- let chardcv = "";
- let charishit = "";
- if (token.get('represents') !== "") { // check whether token has character sheet
- chardcv = getCharacterAttr(attr, token.get('represents'));
- charishit = 11 + thisRoller.theSpeaker.ocvFinal - thr >= chardcv ? "◎" : ""; // if character is hit, load the target character into the string; otherwise, empty
- }
- return {
- __TARGET_IMG__: token.get('imgsrc'),
- __TARGET_DCV__: chardcv,
- __TARGET_TOHIT_VIS__: thisRoller.parameters.selective ? "block" : "none",
- __TARGET_TOHIT_ROLL__: thr,
- __TARGET_LOC_VIS__: loc.hitlabel !== "none" ? "block" : "none",
- __TARGET_LOC__: loc.hitlabel + (randomInteger(2) > 1 ? " (R)" : " (L)"),
- __TARGET_BX__: loc.bx,
- __TARGET_SX__: thisRoller.parameters.mechanic === "k" ? loc.ksx : loc.nsx,
- __TARGET_HIT_DCV__: 11 + thisRoller.theSpeaker.ocvFinal - thr,
- __TARGET_ISHIT__: (thisRoller.parameters.target.length === 1 || !thisRoller.parameters.selective) ? "" : charishit,
- };
- });
- }
- };
-
- const processOCV = (thisRoller) => {
- let sourceChar;
- if (thisRoller.parameters.source !== '--') {
- sourceChar = getChar(thisRoller.parameters.source, thisRoller.theSpeaker.playerid);
- if (sourceChar) {
- thisRoller.theSpeaker.radio_target = getCharacterAttr("radio_target", sourceChar.id);
- thisRoller.theSpeaker.ocvFinal = getCharacterAttr("OCV", sourceChar.id);
- thisRoller.theSpeaker.ocvBase = getCharacterAttr("ocv_base", sourceChar.id);
- thisRoller.theSpeaker.ocvMods = thisRoller.theSpeaker.ocvFinal - thisRoller.theSpeaker.ocvBase;
- thisRoller.theSpeaker.sourceName = sourceChar.get('name');
- }
- } else if (thisRoller.theSpeaker.speakerType === 'character') {
- sourceChar = thisRoller.theSpeaker;
- }
- // process using OMCV instead of OCV
- if (thisRoller.parameters.useomcv === true && sourceChar) { // if there is a character involved, get the ocv and location info from the sheet
- thisRoller.theSpeaker.ocvFinal = getCharacterAttr("OMCV", sourceChar.id);
- thisRoller.theSpeaker.ocvBase = getCharacterAttr("omcv_base", sourceChar.id);
- thisRoller.theSpeaker.ocvMods = thisRoller.theSpeaker.ocvFinal - thisRoller.theSpeaker.ocvBase;
- }
-
- // process changing the called location
- let startingLocation = sourceChar ? radioTargetTable[getCharacterAttr("radio_target", sourceChar.id)] : radioTargetTable[thisRoller.theSpeaker.radio_target];
- if (startingLocation !== thisRoller.parameters.loc && sourceChar) { //only matters if the location has changed
- var attr = findObjs({ _type: 'attribute', _characterid: thisRoller.theSpeaker.id, name: "radio_target" })[0];
- attr.set({ current: Object.keys(radioTargetTable).find(key => radioTargetTable[key] === thisRoller.parameters.loc) });
- if (thisRoller.parameters.useomcv === false) { // only process changes to the OCV if we are using OCV, not if we are using OMCV
- //apply the new mod, remove the old
- thisRoller.theSpeaker.ocvMods += (getLocationData(thisRoller.parameters.loc).ocvmod || 0) - (getLocationData(startingLocation).ocvmod || 0);
- // rebuild the final ocv
- thisRoller.theSpeaker.ocvFinal = Number(thisRoller.theSpeaker.ocvBase) + Number(thisRoller.theSpeaker.ocvMods);
- }
- }
-
- // process the OCV override
- if (thisRoller.parameters.ocv !== -100) { // user supplied an OCV override
- thisRoller.theSpeaker.ocvFinal = Math.floor(thisRoller.parameters.ocv);
- thisRoller.theSpeaker.ocvMods = 0;
- }
- return;
- };
-
- const getResultLocation = (thisRoller) => {
- // if it's a recall and the situation would normally trip a location generation (like random)
- // we should only roll it if there is no recall version of the location argument passed
- // original roll generates a random location; once set, that should stay the same for recalls, only changing if the recall explicitly includes new location argument
- // if (thisRoller.theResult.location.hitlabel !== "" /* default would mean no location, ie: first roll*/ && thisRoller.recallParameters.recall === true )
-
- let loc = {};
- // if we need to get a location, get it; run the location through the locationDataTable to get properties
- if (thisRoller.parameters.loc == "any" || thisRoller.parameters.loc == "random" || thisRoller.parameters.loc.indexOf("shot") > -1) {
- loc = getLocationData(specHitLocation(genHitLocation(thisRoller.parameters.loc)));
- } else {
- loc = getLocationData(thisRoller.parameters.loc);
- }
- return loc;
- };
-
- const rollResult = (thisRoller) => {
- var d6Res = [];
- var d3Res = [];
- for (let i = 0; i < 3; i++) {
- thisRoller.parameters.xdice[i] = Number(thisRoller.parameters.dice[i]) + Number(thisRoller.parameters.extradice[i]);
- }
- thisRoller.parameters.xdice = normalizeDice(thisRoller.parameters.xdice);
-
- if (thisRoller.parameters.xdice[0] > 0) { // get the quantity of d6
- d6Res.push(...getDice(thisRoller.parameters.xdice[0], 6));
- thisRoller.theResult.theroll = [...thisRoller.theResult.theroll, ...d6Res];
- }
- if (thisRoller.parameters.xdice[1] > 0) { // get the quantity of d3
- d3Res.push(...getDice(thisRoller.parameters.xdice[1], 3));
- thisRoller.theResult.theroll = [...thisRoller.theResult.theroll, ...d3Res];
- }
- thisRoller.theResult.theroll.sort(function (a, b) { return b - a });
- Object.assign(thisRoller.theResult.dicecounts, getCountsFromArray(thisRoller.theResult.theroll));
- Object.assign(thisRoller.theResult.d6counts, getCountsFromArray(d6Res)); // needed to output "dice" in the die pool output using the d6 font; d6 in one color, d3 in another
- Object.assign(thisRoller.theResult.d3counts, getCountsFromArray(d3Res)); // needed to output "dice" in the die pool output using the d6 font; d6 in one color, d3 in another
-
- // KNOCKBACK
- if (thisRoller.parameters.dkb === true) {
- let kbbasedice = 2;
- if (thisRoller.parameters.mechanic === "k") kbbasedice++;
- let kbx = Math.max(0, kbbasedice + thisRoller.parameters.kbdicemod);
- Object.assign(thisRoller.theResult.kbroll, getCountsFromArray(getDice(kbx, 6)));
- log("Knockback Roll: " + JSON.stringify(thisRoller.theResult.kbroll));
- for (let i = 1; i < 7; i++) {
- thisRoller.theResult.knockback += (thisRoller.theResult.kbroll[i] * i);
- }
- }
- return;
- };
-
- const calcResult = (thisRoller) => {
- // reset if we are performing recall
- if (thisRoller.recallParameters.recall) {
- Object.assign(thisRoller.theResult, {
- normalbody: 0,
- normalstun: 0,
- });
- }
- // nbody maps the amount of BODY per value of a die
- let nbody = { 1: 0, 2: 1, 3: 1, 4: 1, 5: 1, 6: 2 };
- for (let i = 1; i < 7; i++) {
- thisRoller.theResult.normalstun += (thisRoller.theResult.dicecounts[i] * i);
- thisRoller.theResult.normalbody += (thisRoller.theResult.dicecounts[i] * nbody[i]);
- }
- thisRoller.theResult.normalstun += thisRoller.parameters.xdice[2]; //include the adder in the total
- thisRoller.theResult.killingbody = thisRoller.theResult.normalstun;
-
- // killing stun multiplier comes from the location in the getLocationData function (including a random d3 for no location and focus location)
- // if a stunmod is applied, incorporate it with the location stun modifier
- thisRoller.theResult.location.nsx = Math.max(1, thisRoller.theResult.location.nsx + Math.floor(thisRoller.parameters.stunmod));
- thisRoller.theResult.location.ksx = Math.max(1, thisRoller.theResult.location.ksx + Math.floor(thisRoller.parameters.stunmod));
- thisRoller.theResult.killingstun = thisRoller.theResult.killingbody * thisRoller.theResult.location.ksx;
-
- // if the output is points, determine where the points come from
- if (thisRoller.parameters.outputas == "points") {
- thisRoller.theResult.points.stun = thisRoller.theResult.normalstun;
- thisRoller.theResult.points.body = thisRoller.theResult.normalbody;
- if (thisRoller.parameters.mechanic === "l") thisRoller.theResult.points.stun = thisRoller.theResult.dicecounts[6];
- else if (thisRoller.parameters.mechanic === "u") thisRoller.theResult.points.stun = thisRoller.theResult.dicecounts[1];
- }
-
- return;
- };
-
- const handleInput = (msg) => {
- if (msg.type !== 'api' || !msg.content.toLowerCase().startsWith('!heroll ')) {
- return;
- }
-
- // reduce all inline rolls - this rewrites the content of the msg to be the output of an inline roll rather than the $[[0]], $[[1]], etc.
- if (_.has(msg, 'inlinerolls')) {
- msg.content = _.chain(msg.inlinerolls)
- .reduce(function (m, v, k) {
- m['$[[' + k + ']]'] = v.results.total || 0;
- return m;
- }, {})
- .reduce(function (m, v, k) {
- return m.replace(k, v);
- }, msg.content)
- .value();
- }
-
- let args = msg.content.split(/\s--/) // split at argument delimiter
- .slice(1) // drop the api tag
- .map(splitArgs) // split each arg (foo:bar becomes [foo, bar])
- .map(joinVals) // if the value included a colon (the delimiter), join the parts that were inadvertently separated
- .map(aliasesFrom(argAliasTable)); // flatten all argument aliases to the valid, internally tracked args
-
- let thisRoller = {}; // local object for ephemeral data storage
- setDefaults(msg, thisRoller); // initializes all parameters, including speaker and template defaults
- extendFromArray(thisRoller.userparameters, args); // write all args to the userparameters, adding any unrecognized args
- prioritizeArg("recall", processRecall, thisRoller, args); // look for and process recall argument, if present
-
- // if (typeof state.heroll[thisRoller.recallParameters.speakID].parameters === 'undefined') { // if no roll is stored for that speaker in the state variable
- prioritizeArg("template", setTemplateDefaults, thisRoller); // look for and process template argument, if present
- processArguments(thisRoller); // process the rest of the arguments
- processOCV(thisRoller); // figure out if OMCV, location, or OCV override should alter the OCV (or replace it)
- processTargets(thisRoller);
- // rollActivation(); // no longer needed as 3d6 roll was already generated in the initialization of defaults
- // rollToHit(); // no longer needed as 3d6 roll was already generated in the initialization of defaults
- thisRoller.theResult.location = getResultLocation(thisRoller); // generate a location if necessary, then retrieve location information
- if (!thisRoller.recallParameters.recall) rollResult(thisRoller);// generate dice pool
- calcResult(thisRoller); // turn dice pool into stun, body, knockback, multipliers, points, etc.
- storeState(thisRoller); // store in the state variable
- prepOutput(thisRoller); // assign values to the output parameters
- sendOutputToChat(thisRoller); // perform replacement using html form and the output parameters to inject the finished values; send to chat
-
- return;
- };
-
- // ==================================================
- // OUTPUT FUNCTIONS
- // ==================================================
- const prepOutput = (thisRoller) => {
- // SOURCE NAME
- if (thisRoller.theSpeaker.sourceName) {
- thisRoller.outputParams.__CHARNAME__ = thisRoller.theSpeaker.sourceName;
- }
-
- // OUTPUT FORMAT
- if (thisRoller.parameters.outputformat != "tall") {
- thisRoller.outputParams.__DS_TALL_VIS__ = "none";
- thisRoller.outputParams.__LOC_TALL_VIS__ = "none";
- thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
- thisRoller.outputParams.__SIDECAR_VIS__ = "block";
- }
-
- // POWER NAME
- thisRoller.outputParams.__POWERNAME__ = thisRoller.parameters.powername;
-
- // COLORS
- thisRoller.outputParams.__PRIMARY_BG_COL__ = thisRoller.parameters.primarycolor;
- thisRoller.outputParams.__SECONDARY_BG_COL__ = getAltColor(thisRoller.parameters.primarycolor);
- thisRoller.outputParams.__PRIMARY_TEXT_COL__ = getTextColor(thisRoller.outputParams.__PRIMARY_BG_COL__);
- thisRoller.outputParams.__SECONDARY_TEXT_COL__ = getTextColor(thisRoller.outputParams.__SECONDARY_BG_COL__);
-
- // ACTIVATION
- if (thisRoller.parameters.act != -100) {
- thisRoller.outputParams.__ACT_VIS__ = "block";
- thisRoller.outputParams.__ACT_TGT__ = thisRoller.parameters.act;
- thisRoller.outputParams.__ACT_ROLL__ = thisRoller.parameters.actroll;
- if (thisRoller.parameters.actroll > thisRoller.parameters.act) { // if activation fails, only show the "CLICK" message
- thisRoller.parameters.notes = ' ----- CLICK! -----
';
- thisRoller.outputParams.__RES_BASE_VIS__ = "none";
- thisRoller.outputParams.__RES_MULT_VIS__ = "none";
- thisRoller.outputParams.__TOHIT_VIS__ = "none";
- thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
- thisRoller.outputParams.__DS_TALL_VIS__ = "true";
- thisRoller.outputParams.__LOC_TALL_VIS__ = "none";
- thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
- thisRoller.outputParams.__SIDECAR_VIS__ = "none";
- }
- }
-
- // LOCATION
- if (thisRoller.parameters.loc != "none" && thisRoller.parameters.target.length === 0) { // if there is a location AND there are no targets (targets show their own locations)
- if (thisRoller.parameters.outputformat === "tall") {
- thisRoller.outputParams.__LOC_TALL_VIS__ = "block";
- } else {
- thisRoller.outputParams.__LOC_SC_VIS__ = "block";
- }
-
- thisRoller.outputParams.__LOC__ = thisRoller.theResult.location.hitlabel + (randomInteger(2) > 1 ? " (R)" : " (L)");
- thisRoller.outputParams.__BODY_MULT__ = thisRoller.theResult.location.bx;
- if (thisRoller.parameters.mechanic == "k") thisRoller.outputParams.__STUN_MULT__ = thisRoller.theResult.location.ksx;
- else if (thisRoller.parameters.mechanic == "n") thisRoller.outputParams.__STUN_MULT__ = thisRoller.theResult.location.nsx;
-
- } else {
- thisRoller.outputParams.__BODY_MULT__ = "--"; // this will either be hidden or direct people to the target info
- thisRoller.outputParams.__STUN_MULT__ = "--";
- }
-
- // TO HIT BAR
- thisRoller.outputParams.__TOHIT_ROLL__ = thisRoller.theResult.tohitroll;
- if (thisRoller.theSpeaker.speakerType === "character") {
- // determine how to represent ocv mods (+/-); 0's should make the whole mod portion disappear
- let outputocvmod = "";
- if (thisRoller.theSpeaker.ocvMods < 0) outputocvmod = "-";
- if (thisRoller.theSpeaker.ocvMods > 0) outputocvmod = "+";
- if (outputocvmod.length) outputocvmod += Math.abs(thisRoller.theSpeaker.ocvMods);
-
- thisRoller.outputParams.__TOHIT_OCV__ = thisRoller.parameters.ocv !== -100 ? thisRoller.theSpeaker.ocvFinal : thisRoller.theSpeaker.ocvBase + outputocvmod;
- thisRoller.outputParams.__TOHIT_DCV__ = 11 + thisRoller.theSpeaker.ocvFinal - thisRoller.theResult.tohitroll;
- }
- if (thisRoller.parameters.useomcv) {
- thisRoller.outputParams.__TOHIT_OCV_LBL__ = "OMCV";
- thisRoller.outputParams.__TOHIT_DCV_LBL__ = "HIT DMCV";
- }
-
- // RESULTS BAR
- if (thisRoller.parameters.outputas == "attack" && thisRoller.parameters.loc != "none") {
- thisRoller.outputParams.__RES_MULT_VIS__ = "block";
- thisRoller.outputParams.__RES_BASE_VIS__ = "none";
- } else if (thisRoller.parameters.outputas == "points") {
- thisRoller.outputParams.__RES_PTS_VIS__ = "block";
- thisRoller.outputParams.__RES_BASE_VIS__ = "none";
- }
-
- // MECHANIC
- var mechColors = { L: "#00b8a9", N: "#ff8000", K: "#bf1f2f", U: "#5438AF" };
- thisRoller.outputParams.__MECH__ = thisRoller.parameters.mechanic.toUpperCase();
- thisRoller.outputParams.__MECH_BGC__ = mechColors[thisRoller.outputParams.__MECH__];
-
- // DIE STRENGTH (ROLL EQUATION)
- thisRoller.outputParams.__DIE_STRENGTH__ = thisRoller.parameters.xdice[4];
-
- // DIE POOL
- let numx = { 1: "G", 2: "H", 3: "I", 4: "J", 5: "K", 6: "L" },
- num3x = { 1: "g", 2: "h", 3: "i" };
- for (let i = 6; i > 0; i--) {
- let initlength = thisRoller.outputParams.__DIEPOOL__.length;
- // processing the d6 roll result, output a single character for each die, mapping the value to the numx object
- // those characters are what the d6 font requires to display the correct die face
- for (let j = 0; j < thisRoller.theResult.d6counts[i]; j++) {
- thisRoller.outputParams.__DIEPOOL__ += i.toString().replace(/1|2|3|4|5|6/gi, function (matched) { return numx[matched] });
- }
- // processing the d3 roll result, do the same as above but only for 1-3, and map to the num3x object, which will display the d3 dice in a different color
- if (i < 4) {
- for (let j = 0; j < thisRoller.theResult.d3counts[i]; j++) {
- thisRoller.outputParams.__DIEPOOL__ += i.toString().replace(/1|2|3/gi, function (matched) { return num3x[matched] });
- }
- }
- if (initlength != thisRoller.outputParams.__DIEPOOL__.length && i != 1) thisRoller.outputParams.__DIEPOOL__ += "
";
-
- }
-
- if (thisRoller.parameters.xdice[2] != 0) {
- thisRoller.outputParams.__DIEPOOL__ += " ";
- thisRoller.outputParams.__DIEPOOL__ += (thisRoller.parameters.xdice[2] > 0 ? "+" : "-");
- thisRoller.outputParams.__DIEPOOL__ += " ";
- thisRoller.outputParams.__DIEPOOL__ += Math.abs(thisRoller.parameters.xdice[2]).toString().replace(/1|2|3|4|5|6/gi, function (matched) { return num3x[matched] });
- }
-
- // POINTS
- thisRoller.outputParams.__RES_PTS_LBL__ = thisRoller.parameters.pointslabel;
- if (thisRoller.parameters.dbody && thisRoller.parameters.dstun) { // if both are true, as for a healing that does both STUN and BODY, then show both values
- thisRoller.outputParams.__RES_PTS__ = `${thisRoller.theResult.points.body}
${thisRoller.theResult.points.stun}
`;
- } else if (thisRoller.parameters.dbody) { //just dbody is true (entangle)
- thisRoller.outputParams.__RES_PTS__ = thisRoller.theResult.points.body;
- } else { // just dstun or nothing is true, default to showing stun points
- thisRoller.outputParams.__RES_PTS__ = thisRoller.theResult.points.stun;
- }
-
- // RESULTS: BODY, STUN, KNOCKBACK, LUCK
- if ((thisRoller.parameters.xdice[0] == 0 && thisRoller.parameters.xdice[1] == 0 && thisRoller.parameters.xdice[2] == 0) || thisRoller.parameters.dcheck == true) { // if no dice rolling needs to happen, don't figure BODY/STUN, and hide Results bars
- thisRoller.outputParams.__DIE_STRENGTH__ = " "; // reset the die strength (roll equation) to not show anything
- thisRoller.outputParams.__RES_BASE_VIS__ = "none";
- thisRoller.outputParams.__RES_MULT_VIS__ = "none";
- thisRoller.outputParams.__TOHIT_VIS__ = "block";
- thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
- thisRoller.outputParams.__DS_TALL_VIS__ = "none";
- thisRoller.outputParams.__DIEPOOL_TALL_VIS__ = "none";
- thisRoller.outputParams.__SIDECAR_VIS__ = "none";
- } else {
- if (thisRoller.parameters.mechanic == "l" || thisRoller.parameters.mechanic == "u") {
- thisRoller.outputParams.__TOHIT_VIS__ = "none";
- } else {
- if (thisRoller.parameters.mechanic == "n") {
- thisRoller.outputParams.__RES_STUN__ = thisRoller.theResult.normalstun;
- thisRoller.outputParams.__RES_BODY__ = thisRoller.theResult.normalbody;
- thisRoller.outputParams.__RES_KB__ = Math.max(0, thisRoller.theResult.normalbody - thisRoller.theResult.knockback);
- } else if (thisRoller.parameters.mechanic == "k") {
- if (thisRoller.parameters.loc != "none") thisRoller.outputParams.__RES_STUN__ = "--";
- else thisRoller.outputParams.__RES_STUN__ = thisRoller.theResult.killingstun;
- thisRoller.outputParams.__RES_BODY__ = thisRoller.theResult.killingbody;
- thisRoller.outputParams.__RES_KB__ = Math.max(0, thisRoller.theResult.killingbody - thisRoller.theResult.knockback);
- }
- if (!thisRoller.parameters.dbody) thisRoller.outputParams.__RES_BODY__ = "--";
- if (!thisRoller.parameters.dstun) thisRoller.outputParams.__RES_STUN__ = "--";
- if (!thisRoller.parameters.dkb) thisRoller.outputParams.__RES_KB__ = "--";
- }
- }
-
- // TARGETING
- if (thisRoller.parameters.target.length > 0) { // if a target was designated
- let targetTable = '';
- let targetRow = '';
- if (thisRoller.parameters.selective || thisRoller.parameters.loc !== "none") { // selective or location damage needed
- targetTable = '__TABLE-ROWS__';
- targetRow = ' 
__TARGET_ISHIT__
__TARGET_DCV__
';
- } else { // only output images of targets
- targetTable = '';
- targetRow = '
__TARGET_ISHIT__
__TARGET_DCV__
';
- }
- if (thisRoller.theResult.targetData.length > 0) {
- let targetKeysRegex = new RegExp(Object.keys(thisRoller.theResult.targetData[0]).join("|"), 'gi');
- let targetAllRows = thisRoller.theResult.targetData.reduce((a, v, i) => {
- return a + targetRow.replace(targetKeysRegex, (matched) => { return v[matched]; })
- }, "");
- thisRoller.outputParams.__TARGET_TABLE_HOOK__ = targetTable.replace("__TABLE-ROWS__", targetAllRows);
- }
- }
-
- // NOTES
- if (thisRoller.parameters.notes != "") {
- thisRoller.outputParams.__NOTES__ = thisRoller.parameters.notes;
- }
-
- // VERBOSE
- if (thisRoller.parameters.verbose) {
- thisRoller.outputParams.__V_VIS__ = "block";
- // alternating row colors
- let rowbg = ["#ffffff", "#dedede"];
- let verbtable = '';
- let verbheader = 'ARG | USER | EVAL |
';
- let verbrows = Object.keys(thisRoller.userparameters).filter((p) => { return p !== "notes"; })
- .reduce((a, v, i) => {
- return a + '' + v + ' | ' + thisRoller.userparameters[v] + ' | ' + (["act", "ocv"].includes(v) && thisRoller.parameters[v] == -100 ? "none" : thisRoller.parameters[v]) + ' |
'
- }, verbheader);
- thisRoller.outputParams.__V_NOTE__ = verbtable.replace("__TABLE-ROWS__", verbrows);
- }
- return;
- };
-
- const sendOutputToChat = (thisRoller) => {
- let htmlForm = '__DIE_STRENGTH__
ACT __ACT_TGT__-
__ACT_ROLL__
__TOHIT_OCV_LBL__
__TOHIT_OCV__
__TOHIT_DCV_LBL__
__TOHIT_DCV__
__RES_PTS_LBL__
__RES_PTS__
__TARGET_TABLE_HOOK__
__DIE_STRENGTH__
ACT __ACT_TGT__-
__ACT_ROLL__
';
-
- // join the output paramaters with a pipe, turn that into a regular expression, and feed that into the replace to modify the html form with our figured values
- let chatString = htmlForm.replace(new RegExp(Object.keys(thisRoller.outputParams).join("|"), 'gi'),
- (matched) => { return thisRoller.outputParams[matched]; }
- );
-
- sendChat(thisRoller.theSpeaker.chatSpeaker, chatString);
- return;
- };
-
- const registerEventHandlers = () => {
- on('chat:message', handleInput);
- };
-
- on("ready", () => {
- 'use strict';
- logsig();
- versionInfo();
- registerEventHandlers();
- });
-
- return {
- };
-
-})();
-{ try { throw new Error(''); } catch (e) { API_Meta.HeRoll.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.HeRoll.offset); } }
\ No newline at end of file
diff --git a/HeroRoller/script.json b/HeroRoller/script.json
index a0e6a01050..50c2a54497 100644
--- a/HeroRoller/script.json
+++ b/HeroRoller/script.json
@@ -1,11 +1,11 @@
{
"name": "Hero Roller",
"script": "heroll.js",
- "version": "1.2.1",
- "previousversions": [ "1.0", "1.2.0" ],
- "description": "# Hero Roller api script\r\nThis API is designed to be used with the [HeroSystem6e character sheet](https:\/\/github.com\/Roll20\/roll20-character-sheets\/tree\/master\/HeroSystem6e), however its interaction with the sheet is very light. It requests only 3 attributes from the sheet (OCV, ocv_base, and roll_target), and creates them if they are not found. It also allows the over-riding of these values, to the script can be used on its own. Because of these facts, this script should work with nearly any character sheet for a game requiring Hero System roll mechanics.\r\n\r\nThis api script listens to the chat for api calls beginning with \"!heroll \". This script provides templates for powers (hand attack, blast, aid, healing, etc.), along with argument handles to tweak the output to your desire. The script will roll activation, to hit (gathering OCV and figuring the DCV hit), figure damage (BODY, STUN, KB) or points output, count values based on the standard mechanics (normal damage, killing damage, luck, unluck), and output to the resulting roll to the chat in an appealing package (either in a 'tall' format or a 'sidecar' format). The script also includes a 'verbose' parameter that will output the various parameters used in the construction of the roll, show what the user provided and how that input was interpreted by the engine. This can be helpful when debugging why a call to the script produced an unexpected outcome. The verbose output is included below the normal output from the script. The script also includes a 'recall' parameter, to allow you to access the previous roll (all calls), your own last roll, or to designate a speaker to get their last roll. Once retrieved, parameters can be overwritten (for instance, recalling the last roll and applying the verbose parameter to see what happened). \r\n\r\nText description of the output really doesn't do it justice, nor could all of the instructions for use be put in this short blurb. I suggest you read the instructions to see some examples of the script in action.\r\n\r\nHero Roller was originally based on the herodc script written by GiGs (which was based on other work that came before), and I would not have gotten it where it is without the help of GiGs, The Aaron, and Jakob.",
- "authors": "timmaugh",
- "roll20userid": "5962076",
+ "version": "1.3.1",
+ "previousversions": [ "1.0", "1.2.0", "1.2.1" ],
+ "description": "# Hero Roller api script\r\nThis API is designed to be used with the [HeroSystem6e](https:\/\/github.com\/Roll20\/roll20-character-sheets\/tree\/master\/HeroSystem6e) and [HeroSystem6eHeroic](https:\/\/github.com\/Roll20\/roll20-character-sheets\/tree\/master\/HeroSystem6eHeroic) character sheets, however its interaction with these sheets is very light. It requests only five attributes from the sheet (OCV, ocv_base, OMCV, omcv_base, and roll_target), and creates them if they are not found. It also allows the over-riding of these values, to the script can be used on its own. Because of these facts, this script should work with nearly any character sheet for a game requiring Hero System roll mechanics.\r\n\r\nThis api script listens to the chat for api calls beginning with \"!heroll \". This script provides templates for powers (hand attack, blast, aid, healing, etc.), along with argument handles to tweak the output to your desire. The script will roll activation, to hit (gathering OCV and figuring the DCV hit), figure damage (BODY, STUN, KB) or points output, count values based on the standard mechanics (normal damage, killing damage, luck, unluck), and output to the resulting roll to the chat in an appealing package (either in a 'tall' format or a 'sidecar' format). The script also includes a 'verbose' parameter that will output the various parameters used in the construction of the roll, show what the user provided and how that input was interpreted by the engine. This can be helpful when debugging why a call to the script produced an unexpected outcome. The verbose output is included below the normal output from the script. The script also includes a 'recall' parameter, to allow you to access the previous roll (all calls), your own last roll, or to designate a speaker to get their last roll. Once retrieved, parameters can be overwritten (for instance, recalling the last roll and applying the verbose parameter to see what happened). \r\n\r\nText description of the output really doesn't do it justice, nor could all of the instructions for use be put in this short blurb. I suggest you read the instructions to see some examples of the script in action.\r\n\r\nHero Roller was originally based on the herodc script written by GiGs (which was based on other work that came before), and I would not have gotten it where it is without the help of GiGs, The Aaron, and Jakob.",
+ "authors": "timmaugh, Villain In Glasses",
+ "roll20userid": "5962076, 633423",
"useroptions": [],
"dependencies": [],
"modifies": {
From bd4f44dd2a1dc2f5eabaaa4ed5b479ec40e82bcd Mon Sep 17 00:00:00 2001
From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com>
Date: Wed, 8 Jan 2025 12:00:46 -0700
Subject: [PATCH 2/3] Keeping create attribute function for all but
HeroSystem6eHeroic.
---
HeroRoller/1.3.1/heroll.js | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/HeroRoller/1.3.1/heroll.js b/HeroRoller/1.3.1/heroll.js
index 4146d6de20..9e9c3b651a 100644
--- a/HeroRoller/1.3.1/heroll.js
+++ b/HeroRoller/1.3.1/heroll.js
@@ -1,8 +1,8 @@
/*
=========================================================
Name : Hero Roller (heroll)
-Version : 1.3.1
-Last Update : 11/20/2024
+Version : 1.3.2
+Last Update : 1/8/2025
GitHub : https://github.com/Roll20/roll20-api-scripts/tree/master/HeroRoller
Roll20 Contact : timmaugh for general questions.
villain-in-glasses (Roll20 Id 633423) for HeroSystem6eHeroic-related questions.
@@ -29,7 +29,7 @@ const HeRoll = (() => {
const apiproject = 'HeRoll';
API_Meta[apiproject].version = '1.3.1';
const schemaVersion = 0.1;
- const vd = new Date(1732167196095);
+ const vd = new Date(1736361939000);
const versionInfo = () => {
log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`);
@@ -488,6 +488,7 @@ const HeRoll = (() => {
if (sheet === "HeroSystem6eHeroic") {
// Query attributes in HeroSystem6eHeroic translated to HeroSystem6e.
// Some CVs included for possible future use.
+
switch (attr) {
case "radio_target": translatedAttr = "targetSelection";
break;
@@ -549,12 +550,16 @@ const HeRoll = (() => {
default: retValue = 1;
}
}
+ return Number(retValue);
+
} else {
- // HeroSystem6e default query.
- retValue = getAttrByName(charid, attr)||defvalue;
+ // The default query assumes the sheet is HeroSystem6e, however, the sheet could be anything. Heroll will create attributes that don't exist.
+
+ retAttr = findObjs({ type: 'attribute', characterid: charid, name: attr })[0] || createObj("attribute", { name: attr, current: defvalue, characterid: charid });
+ retAttr.currval = retAttr.get('current') || defvalue;
+
+ return retAttr.currval;
}
-
- return Number(retValue);
};
@@ -1263,7 +1268,7 @@ const HeRoll = (() => {
if (thisRoller.parameters.mechanic === "k") kbbasedice++;
let kbx = Math.max(0, kbbasedice + thisRoller.parameters.kbdicemod);
Object.assign(thisRoller.theResult.kbroll, getCountsFromArray(getDice(kbx, 6)));
- log("Knockback Roll: " + JSON.stringify(thisRoller.theResult.kbroll));
+ // log("Knockback Roll: " + JSON.stringify(thisRoller.theResult.kbroll));
for (let i = 1; i < 7; i++) {
thisRoller.theResult.knockback += (thisRoller.theResult.kbroll[i] * i);
}
From ccaaf2054088f453d9efdeaf4c93b7b012560e20 Mon Sep 17 00:00:00 2001
From: Villain1nGlasses <85969638+Villain1nGlasses@users.noreply.github.com>
Date: Tue, 14 Jan 2025 14:26:40 -0700
Subject: [PATCH 3/3] Release candidate.
---
HeroRoller/1.3.1/heroll.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/HeroRoller/1.3.1/heroll.js b/HeroRoller/1.3.1/heroll.js
index 9e9c3b651a..dd7dffff0c 100644
--- a/HeroRoller/1.3.1/heroll.js
+++ b/HeroRoller/1.3.1/heroll.js
@@ -468,6 +468,8 @@ const HeRoll = (() => {
// First, identify the sheet as either the original supers (HeroSystem6e) or heroic (HeroSystem6eHeroic).
let sheet = getAttrByName(charid, 'sheet_name')||"HeroSystem6e";
+ // log( JSON.stringify( Campaign().get('sheetName')) ); // Undefined. When/if this works a sheet's ID won't need a custom attribute.
+
let attrDefaultTable = {
"radio_target": -1,
"OCV": 0,