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 ]}
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 ], []);
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);
debug.warn(newButton);
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 `
`;
},
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); } }
/* */