Reset Sheet Settings`;
+ menuOptions.unshift(['Key', 'Setting']);
+ new ChatDialog({
+ title: `${scriptName} settings
v${scriptVersion}`,
+ content: menuOptions,
+ footer: footerContent
+ }, 'table');
+ }
+ }
+
+ /**
+ * Button Manager - Handles CRUD operations, math/query functions and HTML output for all buttons, both internal and Custom Button
+ */
+ class ButtonManager {
+
+ static _buttonKeys = ['sheets', 'content', 'content2', 'content3', 'tooltip', 'style', 'style2', 'style3', 'math', 'default', 'mathString', 'query'];
+ static _editKeys = ['clone', 'rename'];
+ _locator = null;
+ _Config = {};
+ _buttons = {};
+
+ constructor(data = {}) {
+ Object.assign(this, { name: data.name || 'newButtonManager' });
+ // Requires access to a ConfigController
+ this._locator = ServiceLocator.getLocator() || this._locator;
+ this._Config = this._locator ? this._locator.getService('ConfigController') : null;
+ if (!this._Config) return {};
+ for (let button in data.defaultButtons) {
+ this._buttons[button] = new Button(data.defaultButtons[button], styles)
+ }
+ }
+
+ get keys() {
+ return ButtonManager._buttonKeys
+ }
+
+ get editKeys() {
+ return [...ButtonManager._buttonKeys, ...ButtonManager._editKeys]
+ }
+
+ getBeaconButtonNames(beaconSheet) {
+ return Object.values(this._buttons).reduce((output, button) => {
+ return (!button.sheets?.length || button.sheets.includes(beaconSheet)) ? [button.name, ...output] : output
+ }, []);
+ }
+
+ getButtonNames(filters = { default: null, currentSheet: null, shown: null, hidden: null }) {
+ let buttons = Object.entries(this._buttons);
+ const sheet = this._Config.getSetting('sheet'),
+ enabledButtons = this._Config.getSetting('enabledButtons');
+ if (typeof filters.default === 'boolean') buttons = buttons.filter(kv => kv[1].default === filters.default);
+ if (typeof filters.currentSheet === 'boolean') buttons = buttons.filter(kv => (!kv[1].sheets.length || sheet === 'custom' || (kv[1].sheets.includes(sheet) === filters.currentSheet)));
+ if (typeof filters.shown === 'boolean') buttons = buttons.filter(kv => (enabledButtons.includes(kv[0]) === filters.shown));
+ if (typeof filters.hidden === 'boolean') buttons = buttons.filter(kv => (enabledButtons.includes(kv[0]) === !filters.hidden));
+ const output = buttons.map(kv => kv[0]);
+ // debug.log(`button names: ${output.join(', ')}`);
+ return output;
+ }
+
+ static validateMathString(inputString, buttonName) {
+ debug.info(inputString);
+ inputString = `${inputString}`;
+
+ // Default buttons will send in a JS function, remove the declaration part
+ inputString = inputString.replace(/^.*?=>\s*/, '');
+
+ let newFormula = inputString;
+ const mathOpsString = MathOpsTransformer.transformMathString(newFormula);
+ debug.info(mathOpsString);
+
+ // Create a test object
+ const damageKeyMatches = inputString.match(/damage\.(\w+)/g) || [],
+ critKeyMatches = inputString.match(/crit\.(\w+)/g) || [],
+ damageKeys = damageKeyMatches.reduce((output, key) => ({
+ ...output,
+ [key.replace(/^[^.]*\./, '')]: 5
+ }), {}),
+ critKeys = critKeyMatches.reduce((output, key) => ({
+ ...output,
+ [key.replace(/^[^.]*\./, '')]: 5
+ }), {});
+
+ const { config } = ServiceLocator.getLocator().getService('config');
+ const damageProperties = [
+ ...Object.values(config.getSetting('templates/damageProperties')).reduce((output, category) => [...output, ...category], []),
+ 'total',
+ ];
+ const invalidProperties = [...Object.keys(damageKeys), ...Object.keys(critKeys)].filter(key => !(damageProperties.includes(key)));
+
+ const mathOpsKeys = MathOpsTransformer.transformMathOpsPayload(damageKeys, critKeys);
+ debug.info(mathOpsKeys);
+
+ let error;
+ try {
+ const testResult = MathOps.MathProcessor({ code: mathOpsString, known: mathOpsKeys });
+ debug.info(testResult);
+ if (testResult.message) {
+ error = testResult.message;
+ } else if (isNaN(testResult)) {
+ error = `The supplied math did not return a number: ${inputString}`;
+ }
+ } catch (e) {
+ error = `Math failed validation - ${e}`;
+ }
+ if (invalidProperties.length) new ChatDialog({
+ title: `Button Warning: "${buttonName}"`,
+ content: `The following damage properties in the button are not set up in this game: ${invalidProperties.join(', ')}`
+ }, 'error');
+
+ return error
+ ? { success: false, err: error }
+ : { success: true, err: null }
+ }
+
+ addButton(buttonData = {}) {
+ const newButton = buttonData.default === false ? new CustomButton(buttonData) : new Button(buttonData);
+ if (newButton.err || !newButton.math) return {
+ success: 0,
+ err: newButton.err || `Button ${buttonData.name} could not be created.`
+ }
+ if (this._buttons[newButton.name]) return { success: 0, err: `Button "${newButton.name}" already exists` };
+ this._buttons[newButton.name] = newButton;
+ this.saveToStore();
+ return { success: 1, msg: `New Button "${newButton.name}" successfully created` }
+ }
+
+ editButton(buttonData = {}) {
+ const modded = [];
+ if (!this._buttons[buttonData.name]) return {
+ success: 0,
+ err: `Button "${buttonData.name}" does not exist.`
+ }
+ if (this._buttons[buttonData.name].default) return { success: 0, err: `Cannot edit default buttons.` }
+ this.editKeys.forEach(k => {
+ debug.log(k, buttonData[k]);
+ if (buttonData[k] != null) {
+ if (k === 'default') return; // Don't allow reassignment of 'default' property
+ else if (k === 'math') {
+ const { success, err } = ButtonManager.validateMathString(buttonData[k], buttonData.name);
+ if (!success) return { err };
+ else {
+ this._buttons[buttonData.name].mathString = buttonData[k];
+ modded.push(k);
+ }
+ } else if (/^style/.test(k)) {
+ this._buttons[buttonData.name][k] = styles[buttonData[k]] || buttonData[k] || '';
+ modded.push(k);
+ }
+ // else if (k === 'query') {
+ // this._buttons[buttonData.name].query = Button.splitAndEscapeQuery(buttonData.query);
+ // modded.push(k);
+ // }
+ else {
+ this._buttons[buttonData.name][k] = buttonData[k];
+ modded.push(k);
+ }
+ }
+ });
+ if (modded.length) this.saveToStore();
+ return modded.length ? {
+ success: 1,
+ msg: `Modified ${buttonData.name} fields: ${modded.join(', ')}`
+ } : { success: 0, err: `No fields supplied.` }
+ }
+
+ removeButton(buttonName) {
+ if (!this._buttons[buttonName]) return { success: 0, err: `Button "${buttonName}" does not exist.` }
+ if (this._buttons[buttonName].default) return { success: 0, err: `Cannot delete default buttons.` }
+ delete this._buttons[buttonName];
+ this._Config.toStore(`customButtons/${buttonName}`, null);
+ return { success: 1, msg: `Removed "${buttonName}".` }
+ }
+
+ cloneButton(originalButtonName, newButtonName) {
+ if (this._buttons[originalButtonName] && newButtonName) {
+ const cloneName = /\s/.test(newButtonName) ? Helpers.camelise(newButtonName) : newButtonName,
+ cloneData = { ...this._buttons[originalButtonName], name: cloneName, default: false },
+ copyResult = this.addButton(cloneData);
+ return copyResult.success ? {
+ success: 1,
+ msg: `Cloned button ${originalButtonName} => ${cloneName}`
+ } : copyResult;
+ } else return { err: `Could not find button "${originalButtonName}", or bad clone button name "${newButtonName}"` }
+ }
+
+ renameButton(originalButtonName, newButtonName) {
+ if (!this._buttons[originalButtonName]) return {
+ success: 0,
+ err: `Button "${originalButtonName}" could not be found`
+ };
+ if (this._buttons[originalButtonName].default) return {
+ success: 0,
+ err: `Cannot rename a default button.`
+ };
+ const cloneName = /\s/.test(newButtonName) ? Helpers.camelise(newButtonName) : newButtonName,
+ cloneResult = this.cloneButton(originalButtonName, cloneName);
+ if (cloneResult.success) {
+ this.removeButton(originalButtonName);
+ return { success: 1, msg: `Renamed button ${originalButtonName} => ${cloneName}` };
+ } else return cloneResult;
+ }
+
+ showButton(buttonName) {
+ if (this._buttons[buttonName] && !this._Config.getSetting('enabledButtons').includes(buttonName)) {
+ return this._Config.changeSetting('enabledButtons', buttonName)
+ }
+ }
+
+ hideButton(buttonName) {
+ if (this._buttons[buttonName] && this._Config.getSetting('enabledButtons').includes(buttonName)) {
+ return this._Config.changeSetting('enabledButtons', buttonName)
+ }
+ }
+
+ saveToStore() {
+ const customButtons = this.getButtonNames({ default: false });
+ customButtons.forEach(button => this._Config.toStore(`customButtons/${button}`, Helpers.copyObj(this._buttons[button])));
+ }
+
+ _getReportTemplate(barNumber) {
+ const template = `'*({name}) {bar${barNumber}_value;before}HP -> {bar${barNumber}_value}HP*'`;
+ return template;
+ // Styled report template for if Aaron implements decoding in TM
+ // const templateRaw = `'
{name}: {bar1_value:before}HP >> {bar1_value}HP
'`;
+ // return encodeURIComponent(templateRaw);
+
+ // !token-mod --set bar1_value|-[[floor(query*17)]]!
+ }
+
+ _getImageIcon(buttonName, cacheBust, version = '2a') {
+ if (!cacheBusted) {
+ cacheBust = true;
+ }
+ const url = `https://raw.githubusercontent.com/ooshhub/autoButtons/main/assets/imageIcons/${buttonName}.png?${version}`.replace(/%/g, 'P');
+ return cacheBust ?
+ `${url}${Math.floor(Math.random() * 1000000000)}`
+ : url;
+ // May need to switch to this if images move
+ // return styles.imageIcons[buttonName];
+ }
+
+ createApiButton(buttonName, damage, crit) {
+ // debug.info(this._buttons[buttonName]);
+ const btn = this._buttons[buttonName],
+ autoHide = this._Config.getSetting(`autohide`),
+ bar = this._Config.getSetting('hpBar'),
+ overheal = this._Config.getSetting('overheal'),
+ overkill = this._Config.getSetting('overkill'),
+ sendReport = (this._Config.getSetting('report') || ``).toLowerCase(),
+ reportString = ['all', 'gm', 'control'].includes(sendReport)
+ ? ` --report ${sendReport}|${this._getReportTemplate(bar)}`
+ : ``,
+ darkMode = this._Config.getSetting('darkMode');
+ const zeroBound = this._Config.getSetting('allowNegatives') ? false : true,
+ boundingPre = zeroBound ? `{0, ` : ``,
+ boundingPost = zeroBound ? `}kh1` : ``;
+ const queryString = Button.splitAndEscapeQuery(btn.query) || '';
+ if (!btn || typeof (btn.math) !== 'function') {
+ debug.error(`${scriptName}: error creating API button ${buttonName}`);
+ return ``;
+ }
+ const modifier = this.resolveButtonMath(btn, damage, crit),
+ tooltip = btn.tooltip.replace(/%/, `${modifier} HP`),
+ setWithQuery = queryString ? `[[${boundingPre}${queryString.replace(/%%MODIFIER%%/g, Math.abs(modifier))}${boundingPost}]]` : `${Math.abs(modifier)}`,
+ tokenModCmd = (modifier > 0) ? (!overheal) ? `+${setWithQuery}!` : `+${setWithQuery}` : (modifier < 0 && !overkill) ? `-${setWithQuery}!` : `-${setWithQuery}`,
+ selectOrTarget = (this._Config.getSetting('targetTokens') === true) ? `--ids @{target|token_id} ` : ``,
+ buttonHref = `!token-mod ${selectOrTarget}--set bar${bar}_value|${tokenModCmd}${reportString}`,
+ useImageIcon = this._Config.getSetting('imageIcons') && btn.default,
+ buttonContent = useImageIcon ? `
})
`
+ : `
${btn.content}`,
+ buttonContent2 = useImageIcon ? ``
+ : btn.content2 ? `
${btn.content2}` : ``,
+ buttonContent3 = useImageIcon ? ``
+ : btn.content3 ? `
${btn.content3}` : ``;
+ return (autoHide && modifier == 0) ?
+ ``
+ : `
${buttonContent}${buttonContent2}${buttonContent3}
`;
+ }
+
+ verifyButtons() {
+ const currentSheet = this._Config.getSetting('sheet'),
+ currentButtons = this._Config.getSetting('enabledButtons'),
+ validButtons = currentButtons.filter(button => {
+ if (currentSheet === 'custom' || this._buttons[button] && this._buttons[button].sheets.includes(currentSheet)) return 1;
+ });
+ if (validButtons.length !== currentButtons.length) {
+ const { success, msg, err } = this._Config.changeSetting('enabledButtons', validButtons);
+ if (success && msg) new ChatDialog({ content: msg, title: 'Buttons Changed' });
+ else if (err) new ChatDialog({ content: err }, 'error');
+ }
+ }
+
+ resolveButtonMath(button, damage, crit) {
+ const buttonType = button.constructor.name;
+ if (buttonType === 'CustomButton') {
+ debug.info(button.mathString, MathOpsTransformer.transformMathOpsPayload(damage, crit), MathOpsTransformer.transformMathString(button.mathString));
+ let mathOpsString = MathOpsTransformer.transformMathString(button.mathString);
+ const mathOpsDamageKeys = MathOpsTransformer.transformMathOpsPayload(damage, crit);
+ // MathOps zeroed key patch
+ mathOpsString = mathOpsZeroPatch ? this.resolveZeroedKeys(mathOpsString, mathOpsDamageKeys) : mathOpsString;
+ debug.warn(mathOpsString);
+ let result = MathOps.MathProcessor({ code: mathOpsString, known: mathOpsDamageKeys });
+ debug.info(result);
+ return isNaN(result) ? 0 : result;
+ } else if (buttonType === 'Button') {
+ return button.math(damage, crit);
+ }
+ }
+
+ resolveZeroedKeys(mathOpsString, mathOpsDamageKeys) {
+ for (const key in mathOpsDamageKeys) {
+ if (mathOpsDamageKeys[key] === 0) {
+ const rxReplacer = new RegExp(key, 'g');
+ mathOpsString = mathOpsString.replace(rxReplacer, '0');
+ }
+ }
+ return mathOpsString;
+ }
+ }
+
+ /**
+ * Button - Basic schema of a Button object
+ */
+ class Button {
+ constructor(buttonData = {}, styleData = styles) {
+ Object.assign(this, {
+ name: buttonData.name || 'newButton',
+ sheets: Array.isArray(buttonData.sheets) ? buttonData.sheets : [],
+ tooltip: `${buttonData.tooltip || ''}`,
+ style: styleData[buttonData.style] || buttonData.style || '',
+ style2: styleData[buttonData.style2] || buttonData.style2 || '',
+ style3: styleData[buttonData.style3] || buttonData.style3 || '',
+ content: buttonData.content || '?',
+ content2: buttonData.content2 || '',
+ content3: buttonData.content3 || '',
+ math: buttonData.math || null,
+ mathString: buttonData.mathString,
+ query: buttonData.query || ``,
+ default: buttonData.default === false ? false : true,
+ mathBackup: buttonData.mathBackup || '',
+ });
+ debug.log(this);
+ if (typeof (this.math) !== 'function') return { err: `Button "${this.name}" math function failed validation` };
+ }
+
+ static splitAndEscapeQuery(queryString) {
+ if (!queryString || typeof (queryString) !== 'string') return ``;
+ const replacers = {
+ '*': `*`,
+ '+': `+`,
+ }
+ const replacerFunction = (m) => replacers[m],
+ rxQuerySplit = /^[+*/-][+-0]?\|/,
+ rxReplacers = new RegExp(`[${Object.keys(replacers).reduce((out,v) => out += `\\${v}`, ``)}]`, 'g');
+ let operator = (queryString.match(rxQuerySplit) || [])[0] || ``,
+ query = queryString.replace(rxQuerySplit, ''),
+ roundingPre = ``,
+ roundingPost = ``;
+ // Deal with rounding for * and /
+ if (/^[*/]/.test(operator)) {
+ roundingPre = operator[1] === '+' ?
+ `ceil(`
+ : `floor(`
+ roundingPost = `)`;
+ }
+ operator = (operator[0] || ``).replace(rxReplacers, replacerFunction);
+ return query ? `${roundingPre}%%MODIFIER%%${operator}?{${query}}${roundingPost}` : ``;
+ }
+ }
+
+ /**
+ * Custom Button - user-made buttons pass through here for validation before being passed to superclass
+ */
+ class CustomButton extends Button {
+ constructor(buttonData = {}) {
+ debug.info(buttonData);
+ if (!buttonData.mathString) return { err: `Button must contain a math string.` };
+ const { success, err } = ButtonManager.validateMathString(buttonData.mathString, buttonData.name);
+ if (!success) {
+ return { err };
+ }
+ Object.assign(buttonData, {
+ name: buttonData.name || 'newCustomButton',
+ mathString: buttonData.mathString,
+ math: (code, known) => MathOps.MathProcessor({
+ code: MathOpsTransformer.transformMathString(code),
+ known
+ }),
+ style: buttonData.style || 'full',
+ query: buttonData.query || ``,
+ default: false,
+ mathBackup: buttonData.mathBackup || buttonData.mathString,
+ });
+ super(buttonData);
+ }
+ }
+
+ /**
+ * Command Line Interface - handle adding and removing CLI Options, and assess chat input when passed in from HandleInput()
+ */
+ class CommandLineInterface {
+
+ _locator = null;
+ _services = {};
+ _options = {};
+
+ constructor(cliData = {}) {
+ this.name = cliData.name || `Cli`;
+ this._locator = ServiceLocator.getLocator();
+ if (!this._locator) debug.warn(`${this.constructor.name} could not find the service locator. Any commands relying on services will be disabled.`);
+ Object.assign(this._services, {
+ config: this._locator.getService('ConfigController'),
+ buttons: this._locator.getService('ButtonManager'),
+ cli: this,
+ });
+ if (cliData.options && cliData.options.length) this.addOptions(cliData.options);
+ debug.log(`Initialised CLI`);
+ }
+
+ // Add one or more options to the CLI
+ addOptions(optionData) {
+ optionData = Helpers.toArray(optionData);
+ optionData.forEach(data => {
+ if (data.name && !this._options[data.name]) {
+ const suppliedServices = { cli: this }
+ if (data.requiredServices) {
+ for (let service in data.requiredServices) {
+ const svc =
+ service === 'ConfigController' ? this._services.config
+ : service === 'ButtonManager' ? this._services.buttons
+ : this._locator.getService(data.requiredServices[service]);
+ if (svc) suppliedServices[service] = svc;
+ else return debug.warn(`${this.name}: Warning - Service "${service}" could not be found for option ${data.name}. CLI option not registered.`);
+ }
+ }
+ data.services = suppliedServices;
+ this._options[data.name] = new CommandLineOption(data);
+ } else debug.warn(`Bad data supplied to CLI Option constructor`);
+ });
+ }
+
+ assess(commandArray, reportToChat = true) {
+ let changed = [], errs = [];
+ commandArray.forEach(command => {
+ const cmd = (command.match(/^([^\s]+)/) || [])[1],
+ args = (command.match(/\s+(.+)/) || ['', ''])[1];
+ for (let option in this._options) {
+ if (this._options[option].rx.test(cmd)) {
+ const { msg, err } = (this._options[option].action(args) || {});
+ // debug.log(msg||err);
+ if (msg) changed.push(Helpers.toArray(msg).join('
'));
+ if (err) errs.push(err);
+ }
+ }
+ });
+ if (changed.length && reportToChat) {
+ // debug.info(changed);
+ const chatData = {
+ title: `${scriptName} settings changed`,
+ content: changed
+ };
+ new ChatDialog(chatData);
+ }
+ if (errs.length) new ChatDialog({ title: 'Errors', content: errs }, 'error');
+ }
+
+ trigger(option, ...args) {
+ if (this._options[option]) this._options[option].action(...args)
+ }
+ }
+
+ /**
+ * Command Line Option - basic model for a user-facing CLI option
+ */
+ class CommandLineOption {
+
+ constructor(optionData = {}) {
+ for (let service in optionData.services) {
+ this[service] = optionData.services[service];
+ }
+ Object.assign(this, {
+ name: optionData.name || 'newOption',
+ rx: optionData.rx || new RegExp(`${optionData.name}`, 'i'),
+ description: optionData.description || `Description goes here...`,
+ action: optionData.action
+ });
+ }
+
+ }
+
+ /**
+ * Chat Dialog - Short-lived layout class which, by default, is sent straight to chat once constructed.
+ * Can be instantiated and persisted by disabling the default autoSend in the constructor
+ */
+ class ChatDialog {
+
+ static _templates = {
+ none: ({ content }) => `${content}`,
+ default: ({ title, content }) => {
+ const msgArray = content ? Helpers.toArray(content) : [],
+ body = msgArray.map(row => `
${row}
`).join('')
+ return `
+
`;
+ },
+ table: ({ title, content, footer, borders }) => {
+ const rowBorders = borders && borders.row ? styles.table.rowBorders : ``;
+ const msgArray = content ? Helpers.toArray(content) : [],
+ columns = msgArray[0].length || 1,
+ tableRows = msgArray.map((row, i) => {
+ const tc = i === 0 ? 'th' : 'td',
+ tcStyle = i === 0 ? styles.table.headerCell : `${styles.table.cell}${rowBorders}`,
+ trStyle = i === 0 ? styles.table.headerRow : styles.table.row;
+ let cells = ``;
+ for (let i = 0; i < columns; i++) {
+ cells += `<${tc} style="${tcStyle}">${row[i]}${tc}>`
+ }
+ return `
+
+ ${cells}
+
`;
+ }).join(''),
+ footerContent = footer ? `` : ``;
+ return `
+
+ `;
+ },
+ error: ({ title, content }) => {
+ const errArray = content ? Helpers.toArray(content) : [];
+ return `
+
+
+
${errArray.join('
')}
+
`;
+ },
+ listButtons: ({ header, body, footer }) => {
+ return `
+
+ `;
+ }
+ }
+
+ constructor(message, template = 'default', autoSend = true) {
+ this.msg = ChatDialog._templates[template] ? ChatDialog._templates[template](message) : null;
+ if (this.msg) {
+ this.msg = this.msg.replace(/\n/g, '');
+ if (autoSend) Helpers.toChat(this.msg);
+ } else {
+ debug.warn(`${scriptName}: error creating chat dialog, missing template "${template}"`);
+ return {};
+ }
+ }
+ }
+
+ on('ready', startScript);
+
+})();
+{
+ try {
+ throw new Error('');
+ } catch (e) {
+ API_Meta.autoButtons.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.autoButtons.offset);
+ }
+}
+/* */