ooshbin

autoButtons API script

Mar 22nd, 2022 (edited)
347
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /* globals log, on, playerIsGM, state, sendChat */ //eslint-disable-line
  2.  
  3. const autoButtons = (() => { //eslint-disable-line no-unused-vars  
  4.  
  5.     const scriptName = 'autoButtons';
  6.  
  7.     const config = {
  8.         version: {
  9.             M: 0,
  10.             m: 4,
  11.             p: 4,
  12.             get: function() { return `${this.M}.${this.m}.${this.p}` },
  13.             getFloat: function() { return parseFloat(`${this.M}.${this.m}${this.p}`) }
  14.         },
  15.         settings: {
  16.             sheet: 'dnd5e_r20',
  17.             templates: {},
  18.             enabledButtons: [],
  19.             gmOnly: 1,
  20.             hpBar: 1,
  21.             ignoreAPI: 1,
  22.             overheal: 0,
  23.             overkill: 0
  24.         },
  25.         fetchFromState: function() { Object.assign(this.settings, state[scriptName].settings); },
  26.         saveToState: function() { Object.assign(state[scriptName].settings, this.settings); },
  27.         // Provide path relative to {config.settings}, e.g. changeSetting('sheet', 'mySheet');
  28.         // supply newValue as 'toggle' to toggle a 1/0 switch
  29.         changeSetting: function(pathString, newValue, pathOptions = { baseObject: config.settings, createPath: false }) {
  30.             if (typeof(pathString) !== 'string' || newValue === undefined) return;
  31.             let keyName = (pathString.match(/[^/]+$/)||[])[0],
  32.                     path = /.+\/.+/.test(pathString) ? pathString.match(/(.+)\/[^/]+$/)[1] : '';
  33.             let configPath = path ? h.getObjectPath(path, pathOptions.baseObject, pathOptions.createPath) : config.settings;
  34.             if (configPath && keyName) {
  35.                 if (/^toggle$/i.test(newValue)) { // toggle 1/0
  36.                     const oldVal = parseInt(this.getSetting(pathString));
  37.                     if (oldVal !== 0 && oldVal !== 1) {
  38.                         h.toChat(`Setting "${pathString}" could not be toggled: current value is "${oldVal}"`);
  39.                         return 0;
  40.                     }
  41.                     newValue = (1 - oldVal);
  42.                 }
  43.                 configPath[keyName] = newValue;
  44.                 this.saveToState();
  45.                 return 1;
  46.             } else {
  47.                 log(`${scriptName}: bad config path ${pathString}`);
  48.                 return 0;
  49.             }
  50.         },
  51.         getSetting: function(pathString) {
  52.             if (typeof(pathString) !== 'string') return null;
  53.             let configValue = h.getObjectPath(pathString);
  54.             return (typeof configValue === 'object') ? JSON.parse(JSON.stringify(configValue)) : configValue;
  55.         },
  56.         loadPreset: function() {
  57.             const currentSheet = this.settings.sheet;
  58.             if (Object.keys(preset).includes(currentSheet)) {
  59.                 this.settings.templates = preset[currentSheet].templates || [];
  60.                 this.settings.enabledButtons = preset[currentSheet].defaultButtons || [];
  61.                 h.toChat(`Loaded preset: ${config.getSetting('sheet')}`);
  62.                 this.saveToState();
  63.                 return 1;
  64.             } else return 0;
  65.         }
  66.     };
  67.  
  68.     const initScript = () => {
  69.         setTimeout(() => { if (!/object/i.test(typeof(['token-mod']))) return sendChat(scriptName, `/w gm <div style="${styles.error}">tokenMod not found - this script requires tokenMod to function! Aborting init...</div>`), 500 });
  70.         if (!state[scriptName] || !state[scriptName].version) {
  71.             log(`autoButtons: first time setup...`);
  72.             state[scriptName] = {
  73.                 version: config.version.getFloat(),
  74.                 settings: config.settings,
  75.             }
  76.         } else if (state[scriptName].version < config.version.getFloat()) {
  77.             let v = state[scriptName].version;
  78.             if (v < 0.13) {
  79.                 Object.assign(state[scriptName].settings, { ignoreAPI: 1 }); // new config key
  80.             }
  81.             if (v < 0.20) {
  82.                 Object.assign(state[scriptName].settings, { overkill: 0, overheal: 0, enabledButtons: [] }); // new config keys
  83.             }
  84.             if (v < 0.30) {
  85.                 config.loadPreset(); // structure of preset has changed - reload
  86.             }
  87.             if (v < 0.40) {
  88.                 state[scriptName].customButtons = {}; // new button store
  89.             }
  90.             state[scriptName].version = config.version.getFloat();
  91.             log(`***UPDATED*** ====> ${scriptName} to v${config.version.get()}`);
  92.         }
  93.         config.fetchFromState();
  94.         if (
  95.             (!config.getSetting('templates/names') || !config.getSetting('templates/names').length)
  96.             || (!config.getSetting('enabledButtons') || !config.getSetting('enabledButtons').length)) {
  97.                 config.loadPreset();
  98.                 h.toChat(`Error fetching config - loaded preset defaults`);
  99.         }
  100.         // Check state of buttons, repair if needed
  101.         state[scriptName].customButtons = state[scriptName].customButtons || {};
  102.         for (let button in state[scriptName].customButtons) {
  103.             log(state[scriptName].customButtons[button]);
  104.             buttons[button] = buttons[button] || state[scriptName].customButtons[button];
  105.             buttons[button].math = customButton.parseActionString(state[scriptName].customButtons[button].mathString||'');
  106.             log(buttons[button]);
  107.         }
  108.         const allButtons = buttons.getNames(),
  109.             enabledButtons = config.getSetting('enabledButtons'),
  110.             validButtons = enabledButtons.filter(v => allButtons.includes(v));
  111.         if (validButtons.length !== enabledButtons.length) config.changeSetting('enabledButtons', validButtons);
  112.         on('chat:message', handleInput);
  113.         log(`- Initialised ${scriptName} - v${config.version.get()} -`);
  114.         // log(state[scriptName]);
  115.     }
  116.  
  117.     // Setting up a sheet:
  118.     // Follow the pattern for the 5e sheet -
  119.     //  names: array of the roll template property names to watch in chat and respond to with buttons
  120.     //  damageProperties: core damage function expects and array for 'damage' and 'crit', but either can be empty if not relevant.
  121.     //  other arrays can be created, but will need custom code in the handleDamageRoll() function to do anything with them
  122.     //  All roll template property names entered into the 'damage' and 'crit' arrays will be available in button math, math: (d,c) => {}
  123.     //
  124.     // defaultButtons: the default buttons to show in the button template
  125.     const preset = {
  126.         dnd5e_r20: {
  127.             templates: {
  128.                 names: ['atkdmg', 'dmg', 'npcfullatk', 'npcdmg'],
  129.                 damageProperties: {
  130.                     damage: ['dmg1', 'dmg2', 'globaldamage'],
  131.                     crit: ['crit1', 'crit2', 'globaldamagecrit'],
  132.                     upcastDamage: ['hldmg'],
  133.                     upcastCrit: ['hldmgcrit'],
  134.                 }
  135.             },
  136.             defaultButtons: ['damageCrit', 'damageFull', 'damageHalf', 'healingFull'],
  137.         },
  138.         custom: {
  139.             templates: {
  140.                 names: [],
  141.                 damageProperties: {
  142.                     damage: [],
  143.                     crit: [],
  144.                 }
  145.             },
  146.             defaultButtons: []
  147.         }
  148.     }
  149.  
  150.     const styles = {
  151.         error: `color: red; font-weight: bold;`,
  152.         outer: `position: relative; vertical-align: middle; font-family: pictos; display: block; background: #f4e6b6; border: 1px solid black; height: auto; line-height: 34px; text-align: center; border-radius: 2px;`,
  153.         rollName: `font-family: arial; font-size: 1.1rem; color: black; font-style:italic; position:relative; overflow: hidden; display: block; line-height: 1rem; margin: 2px 0px 1px 0px; white-space: nowrap; text-align: left; left: 2px;`,
  154.         buttonContainer: `display: inline-block; text-align: center; vertical-align: middle; line-height: 26px; margin: auto 5px auto 5px; height: 26px;    width: 26px; border: #8c6700 1px solid; box-shadow: 0px 0px 3px #805200; border-radius: 5px; background-color: whitesmoke;`,
  155.         buttonShared: `background-color: transparent;   border: none;   padding: 0px;   width: 100%; height: 100%; overflow: hidden;    white-space: nowrap;`,
  156.         crit: `color: red; font-size: 1.5rem;`,
  157.         full: `color: darkred; font-size: 2.1rem;`,
  158.         half: `color: black; font-family: pictos three; font-size: 2rem; padding-top:1px;`,
  159.         healFull: `color: green; font-size: 2rem;`,
  160.         list: {
  161.             container: `background: #626161; border: solid 2px darkgrey; color: white; vertical-align: middle;`,
  162.             header: `text-align: center; font-weight: bold; padding: 6px 0px 6px 0px; border-bottom: solid 1px darkgrey; line-height: 1.5em;`,
  163.             body: `padding: 8px 0px 8px 0px; border-bottom: solid 1px darkgrey;`,
  164.             row: `vertical-align: middle; margin: 0.2em auto 0.2em auto; font-size: 1.2em; line-height: 1.4em;`,
  165.             name: `display: inline-block; vertical-align: middle;   width: 60%; margin-left: 5%; overflow-x: hidden;`,
  166.             buttonContainer: `  display: inline-block; vertical-align: middle; width: 10%; text-align: center; line-height: 1.2em;`,
  167.             controls: {
  168.                 common: `position: relative; font-family: pictos; display: inline-block; background-color: darkgray; padding: 0px; margin: 0px; border: 1px solid #c2c2c2; border-radius: 3px; width: 1.1em; height: 1.1em; line-height: 1.1em; font-size: 1.2em;`,
  169.                 show: `color: #03650b;`,
  170.                 hide: `color: #2a2a2a;`,
  171.                 disabled: `color: gray; cursor: pointer;`,
  172.                 delete: `color: darkred;`,
  173.                 create: `display: inline-block; background-color: darkgray; padding: 0px; margin: 0px; border: 1px solid #c2c2c2; border-radius: 3px;   color: #066a66; padding: 2px 5px 2px 5px;`,
  174.                 no: `position: absolute; left: 0.4em; font-weight: bold; font-family: arial;`
  175.             },
  176.             footer: `text-align: center; font-weight: bold; padding: 6px 0px 6px 0px; border-bottom: solid 1px darkgrey; line-height: 1.5em;`
  177.         }
  178.     }
  179.  
  180.     class customButton {
  181.         constructor(data={}) {
  182.             if (data.math) {
  183.                 this.mathString = data.math;
  184.                 if (typeof data.math === 'string') data.math = customButton.parseActionString(data.mathString || data.math);
  185.                 if (typeof data.math !== 'function') return null;
  186.             } else return null;
  187.             Object.assign(this, {
  188.                 sheets: data.sheets ? h.toArray(data.sheets.split(/\s*,\s*/g)) : [],
  189.                 tooltip: `${data.tooltip || ''}`,
  190.                 style: styles[data.style] || data.style || styles.full,
  191.                 content: `${data.content || 'k'}`,
  192.                 math: data.math,
  193.                 default: false,
  194.             });
  195.             // log(this);
  196.         }
  197.         static parseActionString(inputString) {
  198.             // Convert to JS
  199.             const formulaReplacer = {
  200.                 '$1Math.floor': /([^.]|^)floor/ig,
  201.                 '$1Math.ceil': /([^.]|^)ceil/ig,
  202.                 '$1Math.round': /([^.]|^)round/ig,
  203.                 '($1||0)': /((damage|crit)\.\w+)/ig,
  204.             }
  205.             let newFormula = inputString;
  206.             for (let f in formulaReplacer) newFormula = newFormula.replace(formulaReplacer[f], f);
  207.  
  208.             // Create a test object
  209.             let damageKeys = inputString.match(/(damage|crit)\.(\w+)/g),
  210.                 testKeys = {};
  211.             damageKeys = damageKeys ? damageKeys.map(k => k.replace(/^[^.]*\./, '')) : [];
  212.             damageKeys.forEach(k => testKeys[k] = 5);
  213.  
  214.             let validate = false,
  215.                 newFunc;
  216.             try {
  217.                 newFunc = new Function(`damage`, `crit`, `return (${newFormula})`)
  218.                 validate = isNaN(newFunc(testKeys, testKeys)) ? false : true;
  219.             } catch(e) { log(`${scriptName}: formula failed validation`) }
  220.  
  221.             if (validate) {
  222.                 return newFunc;
  223.             }   else {
  224.                 log(`Failed to create function: syntax error, or function does not return a number: ${inputString}`);
  225.                 return null;
  226.             }
  227.         }
  228.     }
  229.  
  230.    
  231.     // Setting up buttons manually:
  232.     // name - any valid javascript object key. Will be used with the CLI to add and remove the button from the script output
  233.     //  sheets - array of sheets this button will function with
  234.     //  tooltip - mouseover tooltip, % will be replaced with the calculated numerical value
  235.     //  style - link to a style in the styles: {} object above, or write a new one
  236.     //  math - receives two arguments, damage and crit. Both are objects containing a key for each template property, and one for total
  237.     //          e.g. damage = { dmg1: 5, dmg2: 7, globaldamage: 0, total: 12 }  //         
  238.     //  content - the label on the button. Pre-built buttons are all using pictos font
  239.     const buttonKeyNames = ['sheets', 'content', 'tooltip', 'style', 'math', 'default', 'mathString'];
  240.     const buttons = {
  241.         // Default buttons used by presets
  242.         damageCrit: {
  243.             sheets: ['dnd5e_r20'],
  244.             tooltip: `Crit (%)`,
  245.             style: styles.crit,
  246.             math: (d,c) => -(1 * c.total),
  247.             content: 'kk',
  248.             default: true,
  249.         },
  250.         damageFull: {
  251.             sheets: ['dnd5e_r20'],
  252.             tooltip: `Full (%)`,
  253.             style: styles.full,
  254.             math: (d) => -(1 * d.total),
  255.             content: 'k',
  256.             default: true,
  257.         },
  258.         damageHalf: {
  259.             sheets: ['dnd5e_r20'],
  260.             tooltip: `Half (%)`,
  261.             style: styles.half,
  262.             math: (d) => -(Math.floor(0.5 * d.total)),
  263.             content: 'b',
  264.             default: true,
  265.         },
  266.         healingFull: {
  267.             sheets: ['dnd5e_r20'],
  268.             tooltip: `Heal (%)`,
  269.             style: styles.healFull,
  270.             math: (d) => (1 * d.total),
  271.             content: '&',
  272.             default: true,
  273.         },
  274.         createApiButton: function(buttonName, damage, crit) {
  275.             const btn = this[buttonName],
  276.                 bar = config.getSetting('hpBar'),
  277.                 overheal = config.getSetting('overheal'),
  278.                 overkill = config.getSetting('overkill');
  279.             if (!btn || typeof(btn.math) !== 'function') {
  280.                 log(`${scriptName}: error creating API button ${buttonName}`);
  281.                 log(btn.math||'No function found');
  282.                 return ``;
  283.             }
  284.             const modifier = btn.math(damage, crit),
  285.             tooltip = btn.tooltip.replace(/%/, `${modifier} HP`),
  286.                 tokenModCmd = (modifier > 0) ? (!overheal) ? `+${modifier}!` : `+${modifier}` : (modifier < 0 && !overkill) ? `${modifier}!` : modifier;
  287.             return `<div style="${styles.buttonContainer}"  title="${tooltip}"><a href="!token-mod --set bar${bar}_value|${tokenModCmd}" style="${styles.buttonShared}${btn.style}">${btn.content}</a></div>`;
  288.         },
  289.         getNames: function(includeDefault=true) { return Object.entries(this).map(e => { if (typeof(e[1]) !== 'function' && (includeDefault || !e[1].default)) return e[0] }).filter(v=>v); },
  290.         validateButtons: function(buttons) { // Supply button(s) to validate, or supply no parameter to check all currently-shown buttons
  291.             const currentSheet = config.getSetting('sheet'),
  292.                 buttonsToValidate = buttons ? h.toArray(buttons) : config.getSetting('enabledButtons');
  293.             return buttonsToValidate.filter(b => this[b] && (currentSheet === 'custom' || this[b].sheets.length < 1 || this[b].sheets.includes(currentSheet)))
  294.         },
  295.         editButton: function(buttonName, buttonData, isNew) { // supply false as buttonData to delete
  296.             let err;
  297.             if (this[buttonName] && this[buttonName].default) return { res: 0, err: `Cannot modify default buttons` }
  298.             if (buttonData) {
  299.                 if (isNew && this[buttonName]) err = `Button "${buttonName}" already exists, aborting.`;
  300.                 else if (!isNew && !this[buttonName]) err = `Cannot edit button "${buttonName}", button not found.`;
  301.                 else {
  302.                     if (isNew) buttons[buttonName] = {};
  303.                     for (let kv in buttonData) { if (buttonKeyNames.includes(kv)) buttons[buttonName][kv] = buttonData[kv] }
  304.                     this.saveToState();
  305.                 }
  306.             } else if (buttonData === false) {
  307.                 if (!buttons[buttonName]) err = `Cannot delete button "${buttonName}", button not found.`;
  308.                 else {
  309.                     const stores = [state[scriptName].customButtons, buttons];
  310.                     stores.forEach(store => {
  311.                         if (store[buttonName]) {
  312.                             store[buttonName] = null;
  313.                             delete store[buttonName];
  314.                         }
  315.                     });
  316.                 }
  317.             } else err = `Unknown data supplied.`;
  318.             return err ? { res: 0, err: err } : { res: 1 }
  319.         },
  320.         saveToState: function() {
  321.             const customButtonNames = this.getNames(false);
  322.             // log(`Saving ${customButtonNames.join(', ')}`)
  323.             customButtonNames.forEach(v => {
  324.                 state[scriptName].customButtons[v] = buttons[v];
  325.             });
  326.         }
  327.     }
  328.  
  329.     const rx = { on: /\b(1|true|on)\b/i, off: /\b(0|false|off)\b/i };
  330.  
  331.     const sendButtons = (damage, crit, msg) => {
  332.         let gmo = config.getSetting('gmOnly') ? true : false;
  333.         let buttonHtml = '',
  334.             activeButtons = config.getSetting(`enabledButtons`) || [],
  335.             name = h.findName(msg.content);
  336.         name = name || `Apply:`;
  337.         activeButtons.forEach(btn => buttonHtml += buttons.createApiButton(btn, damage, crit));
  338.         const buttonTemplate = `<div class="autobutton" style="${styles.outer}"><div style="${styles.rollName}">${name}</div>${buttonHtml}</div>`;
  339.         h.toChat(`${buttonTemplate}`, gmo);
  340.     }
  341.  
  342.     const handleDamageRoll = (msg) => {
  343.         const dmgFields = config.getSetting('templates/damageProperties/damage')||[],
  344.             critFields = config.getSetting('templates/damageProperties/crit')||[];
  345.         const damage = h.processFields(dmgFields, msg),
  346.             crit = h.processFields(critFields, msg);
  347.         if ('dnd5e_r20' === config.getSetting('sheet')) {
  348.             const isSpell = h5e.is5eAttackSpell(msg.content);
  349.             if (isSpell) {
  350.                 const upcastDamageFields = config.getSetting('templates/damageProperties/upcastDamage')||[],
  351.                     upcastCritFields = config.getSetting('templates/damageProperties/upcastCrit')||[];
  352.                 damage.total += h.processFields(upcastDamageFields, msg).total||0;
  353.                 crit.total += h.processFields(upcastCritFields, msg).total||0;
  354.             }
  355.         }
  356.         crit.total += damage.total;
  357.         sendButtons(damage, crit, msg);
  358.     }
  359.  
  360.     const handleInput = (msg) => {
  361.         const msgIsGM = playerIsGM(msg.playerid);
  362.         if (msg.type === 'api' && msgIsGM && /^!(autobut)/i.test(msg.content)) {
  363.             let cmdLine = (msg.content.match(/^![^\s]+\s+(.+)/i) || [])[1],
  364.                     params = cmdLine ? cmdLine.split(/\s*--\s*/g) : [];
  365.             params.shift();
  366.             params = params.length ? params : [''];
  367.             params.forEach(param => {
  368.                 let cmd = (param.match(/^([^\s]+)/)||[])[1],
  369.                     args = (param.match(/\s+(.+)/)||['',''])[1],
  370.                     changed = [];
  371.                 if (!cmd) return;
  372.                 for (let opt in CLI.options) { if (CLI.options[opt].rx && CLI.options[opt].rx.test(cmd)) changed.push(CLI.options[opt].action(args)) }
  373.                 if (changed.filter(v=>v).length) h.toChat(`*Settings change result:*<br>${changed.join('<br>')}`);
  374.             });
  375.         }
  376.         else if (msg.rolltemplate && config.getSetting('templates/names').includes(msg.rolltemplate)) {
  377.             const ignoreAPI = config.getSetting('ignoreAPI');
  378.             if (ignoreAPI && /^api$/i.test(msg.playerid)) return;
  379.             handleDamageRoll(msg);
  380.         }
  381.     }
  382.  
  383.     // Command line interface data
  384.     const CLI = {
  385.         options: {
  386.             reset: {
  387.                 rx: /^reset/i,
  388.                 description: `Reset configuration from preset`,
  389.                 action: () => {
  390.                     if (config.getSetting('sheet')) {
  391.                         config.loadPreset();
  392.                         return `Config reset from preset: "${config.getSetting('sheet')}"`;
  393.                     } else h.toChat(`No preset found!`);
  394.                 }
  395.             },
  396.             bar: {
  397.                 rx: /^(hp)?bar/i,
  398.                 description: `Select which token bar represents hit points`,
  399.                 action: (args) => {
  400.                     const newVal = parseInt(`${args}`.replace(/\D/g, ''));
  401.                     if (newVal > 0 && newVal < 4) {
  402.                         if (config.changeSetting('hpBar', newVal)) return `hpBar: ${newVal}`;
  403.                     }
  404.                 }
  405.             },
  406.             loadPreset: {
  407.                 rx: /^loadpre/i,
  408.                 description: `Select a preset for a Game System`,
  409.                 action: (args) => {
  410.                     const newVal = args.trim();
  411.                     if (Object.keys(preset).includes(newVal)) {
  412.                         if (config.changeSetting('sheet', newVal)) {
  413.                             config.loadPreset();
  414.                             buttons.validateButtons();
  415.                             return `Preset changed: ${newVal}`;
  416.                         } else h.toChat(`${scriptName}: error changing preset to "${newVal}"`);
  417.                     }
  418.                 }
  419.             },
  420.             listTemplates: {
  421.                 rx: /^(list)?templ/i,
  422.                 description: `List roll templates the script is listening for`,
  423.                 action: () => {
  424.                     const templates = config.getSetting(`templates/names`),
  425.                         templateText = `{{&nbsp;${templates.join(', ')}}}`,
  426.                         chatText = `&{template:default} {{name=Trigger Templates}} ${templateText}`;
  427.                     h.toChat(chatText);
  428.                 }
  429.             },
  430.             addTemplate: {
  431.                 rx: /^addtem/i,
  432.                 description: `Add roll template name to listen list for damage rolls`,
  433.                 action: (args) => CLI.helpers.modifyConfigArray(args, 'templates/names')
  434.             },
  435.             removeTemplate: {
  436.                 rx: /^rem(ove)?tem/i,
  437.                 description: `Remove roll template from listen list`,
  438.                 action: (args) => CLI.helpers.modifyConfigArray(args, 'templates/names', 0)
  439.             },
  440.             listProperties: {
  441.                 rx: /^(list)?(propert|props)/i,
  442.                 description: `List roll template properties inline rolls are grabbed from`,
  443.                 action: () => {
  444.                     const properties = config.getSetting('templates/damageProperties');
  445.                     let templateText = ``;
  446.                     if (typeof properties === 'object') {
  447.                         for (let category in properties) templateText += `{{&nbsp;${category}=${properties[category].join(`, `)}}}`
  448.                     } else return log(`${scriptName}: Error getting damage properties from state`);
  449.                     const chatOutput = `&{template:default} {{name=Roll Template Properties}} ${templateText}`;
  450.                     h.toChat(chatOutput);
  451.                 }
  452.             },
  453.             addProperty: {
  454.                 rx: /^addprop/i,
  455.                 description: `Add a roll template property to the listener`,
  456.                 action: (args) => {
  457.                     const parts = args.match(/([^/]+)\/(.+)/);
  458.                     if (parts && parts.length === 3) {
  459.                         if (config.getSetting(`templates/damageProperties/${parts[1]}`) == null) {
  460.                             h.toChat(`Created new roll template damage property category: ${parts[1]}`);
  461.                             state[scriptName].settings.templates.damageProperties[parts[1]] = [];
  462.                         }
  463.                         CLI.helpers.modifyConfigArray(parts[2], `templates/damageProperties/${parts[1]}`);
  464.                     } else {
  465.                         h.toChat(`Bad property path supplied, must be in the form "category/propertyName". Example: damage/dmg1`);
  466.                     }
  467.                 }
  468.             },
  469.             removeProperty: {
  470.                 rx: /^rem(ove)?prop/i,
  471.                 description: `Remove a roll template property from the listener`,
  472.                 action: (args) => {
  473.                     const parts = args.match(/([^/]+)\/(.+)/);
  474.                     if (parts && parts.length === 3) {
  475.                         const currentArray = config.getSetting(`templates/damageProperties/${parts[1]}`);
  476.                         if (currentArray != null) {
  477.                             const removed = CLI.helpers.modifyConfigArray(parts[2], `templates/damageProperties/${parts[1]}`, 0);
  478.                             if (removed && !/^(damage|crit)$/i.test(parts[1])) { // Clean up category if it's now empty, and isn't a core category
  479.                                 const newArray = config.getSetting(`templates/damageProperties/${parts[1]}`);
  480.                                 if (newArray.length === 0) delete state[scriptName].settings.templates.damageProperties[parts[1]];
  481.                             }
  482.                         } else h.toChat(`Could not find roll template property category: ${parts[1]}`);
  483.                     } else {
  484.                         h.toChat(`Bad property path supplied, must be in the form "category/propertyName". Example: damage/dmg1`);
  485.                     }
  486.                 }
  487.             },
  488.             listButtons:{
  489.                 rx: /^(list)?button/i,
  490.                 description: `List available buttons`,
  491.                 action: () => {
  492.                     const allButtons = buttons.getNames(),
  493.                         removableButtons = buttons.getNames(false),
  494.                         usedButtons = config.getSetting('enabledButtons'),
  495.                         unusedButtons = allButtons.filter(v => !usedButtons.includes(v)),
  496.                         availableButtons = buttons.validateButtons(unusedButtons),
  497.                         reorderedButtons = usedButtons.concat(unusedButtons);
  498.                     const links = {
  499.                         hide: `!autoButton --hideButton %name%`,
  500.                         show: `!autoButton --showButton %name%`,
  501.                         delete: `!autoButton --deleteButton %name%`,
  502.                         disabled: `#`
  503.                     }
  504.                     const labels = { hide: `E<span style="${styles.list.controls.no}">/</span>`, show: 'E', delete: 'D', disabled: '!' };
  505.                     const controls = ['show', 'hide', 'delete'];
  506.                     const listBody = reorderedButtons.map(button => {
  507.                         let rowHtml = `<div class="list-row" style="${styles.list.row}"><div class="button-name" style="${styles.list.name}">${removableButtons.includes(button) ? '' : '&ast;'}%name%</div>`;
  508.                         controls.forEach(control => {
  509.                             const controlType = (
  510.                                 (control === 'show' && availableButtons.includes(button)) ||
  511.                                 (control === 'hide' && usedButtons.includes(button)) ||
  512.                                 (control === 'delete' && removableButtons.includes(button)))
  513.                                 ? control : 'disabled';
  514.                             rowHtml += `<div class="control-${control}" style="${styles.list.buttonContainer}" title="${h.emproper(`${control} button`)}"><a href="${links[controlType]}" style="${styles.list.controls.common}${styles.list.controls[controlType]}">${labels[control]}</a></div>`;
  515.                         });
  516.                         return `${rowHtml.replace(/%name%/g, button)}</div>`;
  517.                     });
  518.                     const fullTemplate = `
  519.                         <div class="autobutton-list" style="${styles.list.container}">
  520.                             <div class="autobutton-header" style="${styles.list.header}">autoButton list (sheet: ${config.getSetting('sheet')})</div>
  521.                             <div class="autobutton-body" style="${styles.list.body}">
  522.                                 ${listBody.join('')}
  523.                             </div>
  524.                             <div class="autobutton-footer" style="${styles.list.footer}">
  525.                                 <div style="${styles.list.buttonContainer}width:auto;"><a style="${styles.list.controls.create}" href="!autobut --createbutton {{name=?{Name?|newButton}}} {{content=?{Pictos Character?|k}}} {{tooltip=?{Tooltip?|This is a button}}} {{math=?{Math function|floor(damage.total/2&rpar;}}}">Create New Button</a></div>
  526.                             </div>
  527.                         </div>
  528.                         `;
  529.                     h.toChat(`/w gm ${fullTemplate.replace(/\n/g, '')}`, false);
  530.                 },
  531.             },
  532.             showButton: {
  533.                 rx: /^showbut/i,
  534.                 description: `Add a button to the template`,
  535.                 action: (args) => {
  536.                     const newVal = args.trim();
  537.                     if (buttons.validateButtons(newVal).length) {
  538.                         let oldVal = config.getSetting('enabledButtons');
  539.                         if (!oldVal.includes(newVal)) {
  540.                             oldVal.push(newVal);
  541.                             config.changeSetting('enabledButtons', oldVal);
  542.                             return `Button "${newVal}" is now visible.`;
  543.                         } else h.toChat(`Button "${newVal}" already exists.`)
  544.                     } else h.toChat(`Unrecognised or incompatible button: "${newVal}"`);
  545.                 }
  546.             },
  547.             hideButton: {
  548.                 rx: /^hidebut/i,
  549.                 description: `Remove a button from the template`,
  550.                 action: (args) => {
  551.                     const newVal = args.trim(),
  552.                     oldVal = config.getSetting('enabledButtons');
  553.                     if (oldVal.length && oldVal.includes(newVal)) {
  554.                         const filtered = oldVal.filter(v=> v !== newVal);
  555.                         config.changeSetting('enabledButtons', filtered);
  556.                         return `Button "${newVal}" is hidden.`;
  557.                     } else log(`${scriptName}: unrecognised button name`);
  558.                 }
  559.             },
  560.             reorderButtons: {
  561.                 rx: /^(re)?order/i,
  562.                 description: `Change order of buttons`,
  563.                 action: (args) => {
  564.                     if (!args) return;
  565.                     const newIndices = args.replace(/[^\d,]/g, '').split(/,/g),
  566.                         currentOrder = config.getSetting('enabledButtons');
  567.                     let newOrder = [];
  568.                     let valid = true;
  569.                     newIndices.forEach(buttonIndex => {
  570.                         const realIndex = buttonIndex - 1;
  571.                         if (realIndex > -1 && realIndex < currentOrder.length) {
  572.                             if (currentOrder[realIndex]) {
  573.                                 newOrder.push(currentOrder[realIndex]);
  574.                                 currentOrder[realIndex] = null;
  575.                             }
  576.                         } else valid = false;
  577.                     });
  578.                     if (!valid) return h.toChat(`Invalid button order input: ${args}. Indices must be between 1 and total number of buttons in use.`);
  579.                     newOrder = newOrder.concat(currentOrder.filter(v=>v));
  580.                     if ((newOrder.length === currentOrder.length) && config.changeSetting('enabledButtons', newOrder)) return `Button order changed to: [ ${newOrder.join(' | ')} ]`;
  581.                 }
  582.             },
  583.             createButton: {
  584.                 rx: /^createbut/i,
  585.                 description: `Create a new button`,
  586.                 action: (args) => {
  587.                     const buttonData = CLI.helpers.splitHandlebars(args);
  588.                     if (buttonData && buttonData.name) {
  589.                         if (/^[^A-Za-z]/.test(buttonData.name)) return `Invalid button name: must start with a letter`;
  590.                         let buttonName = /\s/.test(buttonData.name) ? CLI.helpers.camelise(buttonData.name) : buttonData.name;
  591.                         if (buttons.getNames().includes(buttonName)) return `Invalid button name, already in use: "${buttonName}"`;
  592.                         if (!buttonData.math) return `Button must have an associated function, {{math=...}}`;
  593.                         const newButton = new customButton(buttonData);
  594.                         if (newButton && newButton.math) {
  595.                             const { res, err } = buttons.editButton(buttonName, newButton, 1);
  596.                             if (res) return `Successfully added button "${buttonName}"`;
  597.                             else return `Failed to create button "${buttonName}" - ${err}`;
  598.                         }
  599.                     } else return `Bad input for button creation`
  600.                 }
  601.             },
  602.             editButton: {
  603.                 rx: /^editbut/i,
  604.                 description: `Edit an existing button`,
  605.                 action: (args) => {
  606.                     let buttonData = CLI.helpers.splitHandlebars(args);
  607.                     // log(buttonData);
  608.                     if (buttonData && buttonData.name) {
  609.                         const buttonName = /\s/.test(buttonData.name) ? CLI.helpers.camelise(buttonData.name) : buttonData.name;
  610.                         if (buttonData.math) {
  611.                             buttonData.mathString = buttonData.math;
  612.                             let newFunc = customButton.parseActionString(buttonData.math);
  613.                             // log(newFunc.toString());
  614.                             buttonData.math = newFunc;
  615.                         }
  616.                         if (buttonData.math === null) return `Bad math function supplied in edit.`;
  617.                         // log(buttonData);
  618.                         const { res, err } = buttons.editButton(buttonName, buttonData)
  619.                         if (res) return `Successfully edited button "${buttonName}"`;
  620.                         else return `Failed to save edits to button "${buttonName}" - ${err}`;
  621.                     }
  622.                 }
  623.             },
  624.             deleteButton: {
  625.                 rx: /^del(ete)?but/i,
  626.                 description: `Remove a button`,
  627.                 action: (args) => {
  628.                     const { res, err } = buttons.editButton(args, false);
  629.                     if (res) {
  630.                         CLI.helpers.modifyConfigArray(args, 'enabledButtons', 0);
  631.                         return `Successfully deleted button ${args}`;
  632.                     } else return `Couldn't delete button ${args} - ${err}`;
  633.                 }
  634.             },
  635.             ignoreApi: {
  636.                 rx: /^ignoreapi/i,
  637.                 description: `Ignore anything sent to chat by the API`,
  638.                 action: (args) => CLI.helpers.toggle('ignoreAPI', args)
  639.             },
  640.             overheal: {
  641.                 rx: /^overh/i,
  642.                 description: `Allow healing to push hp above hpMax`,
  643.                 action: (args) => CLI.helpers.toggle('overheal', args)
  644.             },
  645.             overkill: {
  646.                 rx: /^overk/i,
  647.                 description: `Allow healing to push hp above hpMax`,
  648.                 action: (args) => CLI.helpers.toggle('overkill', args)
  649.             },
  650.             gmOnly: {
  651.                 rx: /^gmo/i,
  652.                 description: `Whisper the buttons to GM, or post publicly`,
  653.                 action: (args) => CLI.helpers.toggle('gmOnly', args)
  654.             },
  655.             settings: {
  656.                 rx: /^setting/i,
  657.                 description: `Open settings UI`,
  658.                 action: () => h.toChat(`Not yet implemented.`)
  659.             },
  660.             help: {
  661.                 rx: /^(\?$|h$|help)/i,
  662.                 description: `Display script help`,
  663.                 action: () => showHelp()
  664.             },
  665.             uninstall: {
  666.                 rx: /^uninstall$/i,
  667.                 description: `Remove all script settings from API state`,
  668.                 action: () => {
  669.                     state[scriptName] = null;
  670.                     delete state[scriptName];
  671.                     h.toChat(`Removed all ${scriptName} settings from API state.`)
  672.                 }
  673.             }
  674.         },
  675.         helpers: {
  676.             toggle: (settingPath, args) => {
  677.                 if (!settingPath) return log(`autoButton config error, bad settings change: ${settingPath}`);
  678.                 const newVal = !args ? 'toggle' : rx.off.test(args) ? 0 : rx.on.test(args) ? 1 : null;
  679.                 if (newVal != null && config.changeSetting(settingPath, newVal)) return `${settingPath} set to "${rx.on.test(config.getSetting(settingPath)) ? 'on' : 'off'}"`;
  680.                 else h.toChat(`Failed to set ${settingPath}: "${args}"`);
  681.             },
  682.             // supply falsy 'addOrSub' value for subtract, default is add. Supply a validation function if required.
  683.             modifyConfigArray: (value, targetSettingPath, addOrSub = 1, validation = (v) => v, allowDuplicates = false) => {
  684.                 if (value == null || !targetSettingPath || addOrSub == null) return log(`${scriptName}: Error modifying template, bad arguments.`);
  685.                 const currentSetting = config.getSetting(targetSettingPath);
  686.                 if (!Array.isArray(currentSetting)) return log(`${scriptName}: bad setting path supplied to config "${targetSettingPath}", or target is not an Array.`);
  687.                 let newSetting = [];
  688.                 if (addOrSub) {
  689.                     if (validation(value)) {
  690.                         if (allowDuplicates || !currentSetting.includes(value)) newSetting = currentSetting.concat([value]);
  691.                         else return `Value "${value}" already exists in "${targetSettingPath}"`;
  692.                     }
  693.                 } else {
  694.                     if (currentSetting.includes(value)) newSetting = currentSetting.filter(v => v !== value);
  695.                     else return `Value "${value}" does not exist in "${targetSettingPath}"`;
  696.                 }
  697.                 if (config.changeSetting(targetSettingPath, newSetting)) {
  698.                     h.toChat(`"${value}" added to setting "${targetSettingPath}"`);
  699.                     return true;
  700.                 }
  701.             },
  702.             splitHandlebars: (inputString) => {
  703.                 let output = {},
  704.                     kvArray = inputString.match(/{{[^}]+}}/g)||[];
  705.                 kvArray.forEach(kv => {
  706.                     kv = kv.replace(/({{|}})/g, '');
  707.                     const key = kv.match(/^[^=]+/),
  708.                         value = (kv.match(/=(.+)/)||[])[1] || ``;
  709.                     if (key) output[key] = value;
  710.                 });
  711.                 return Object.keys(output).length ? output : null;
  712.             },
  713.             camelise: (inp, options={enforceCase:false}) => {
  714.                 if (typeof(inp) !== 'string') return null;
  715.                 const words = inp.split(/[\s_]+/g);
  716.                 return words.map((w,i) => {
  717.                     const wPre = i > 0 ? w[0].toUpperCase() : w[0].toLowerCase();
  718.                     const wSuf = options.enforceCase ? w.slice(1).toLowerCase() : w.slice(1);
  719.                     return `${wPre}${wSuf}`;
  720.                 }).join('');
  721.             }
  722.         }
  723.     }
  724.  
  725.     const showHelp = () => h.toChat(`Haaaaalp! ... (Not yet implemented)`);
  726.    
  727.     // Helper functions
  728.     const h = (() => {
  729.         const processFields = (fieldArray, msg) => {
  730.             let output = {}
  731.             const rolls = msg.inlinerolls;
  732.             output.total = fieldArray.reduce((m, v) => {
  733.                 const rxIndex = new RegExp(`{${v}=\\$\\[\\[\\d+`, 'g'),
  734.                     indexResult = msg.content.match(rxIndex);
  735.                 if (indexResult) {
  736.                     const index = indexResult.pop().match(/\d+$/)[0],
  737.                         total = isNaN(rolls[index].results.total) ? 0 : rolls[index].results.total;
  738.                     output[v] = total;
  739.                     return m + total;
  740.                 } else { // if roll template property's inline roll is not found, return 0 to prevent errors down the line
  741.                     output[v] = 0;
  742.                 }
  743.                 return m;
  744.             }, 0);
  745.             return output;
  746.         }
  747.  
  748.         const findName = (msgContent) => {
  749.             const rxName = /name=([^}]+)}/i;
  750.             let name = msgContent.match(rxName);
  751.             return name ? name[1] : null;
  752.         }
  753.    
  754.         const getObjectPath = (pathString, baseObject = config.settings, createPath=false) => {
  755.             let parts = pathString.split(/\/+/g);
  756.             let objRef = parts.reduce((m,v) => {
  757.                 if (m == null) return;
  758.                 if (m[v] == null) {
  759.                     if (createPath) m[v] = {};
  760.                     else return null;
  761.                 }
  762.                 return m[v];}, baseObject)
  763.             return objRef;
  764.         }
  765.         const toChat = (msg, whisper = true) => {
  766.             let prefix = whisper ? `/w gm ` : '';
  767.             sendChat(scriptName, `${prefix}${msg}`, {noarchive: true});
  768.         }
  769.         const toArray = (inp) => Array.isArray(inp) ? inp : [inp];
  770.         const emproper = (inpString) => {
  771.             let words = inpString.split(/\s+/g);
  772.             return words.map(w => `${w[0].toUpperCase()}${w.slice(1)}`).join(` `);
  773.         }
  774.  
  775.         return { processFields, findName, getObjectPath, toChat, toArray, emproper }
  776.     })();
  777.    
  778.     // 5e helpers
  779.     const h5e = (() => {
  780.         const is5eAttackSpell = (msgContent) => {
  781.             const rxSpell = /{spelllevel=(cantrip|\d+)/;
  782.             return rxSpell.test(msgContent) ? 1 : 0;
  783.         }
  784.         return { is5eAttackSpell }
  785.     })();
  786.  
  787.     on('ready', () => initScript());
  788.  
  789. })();
Add Comment
Please, Sign In to add comment