diff --git a/HeroRoller/1.3.0/heroll.js b/HeroRoller/1.3.0/heroll.js new file mode 100644 index 0000000000..ee1cdcc98b --- /dev/null +++ b/HeroRoller/1.3.0/heroll.js @@ -0,0 +1,1589 @@ +/* +========================================================= +Name : Hero Roller (heroll) +Version : 1.3.0 +Last Update : 1/14/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. +========================================================= +*/ +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.0'; + const schemaVersion = 0.1; + const vd = new Date(1736897730); + + 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"; + + // 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, + "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; + } + } + return Number(retValue); + + } else { + // 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; + } + }; + + + 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__: "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 = '