Here's a bugfix release, which should (crosses fingers) address Keith and Seph's reports. Give it a try, and if it works, I'll update the 2nd post in the thread. var universalChatMenu = universalChatMenu || (function () {
'use strict';
const version = '0.3.7',
scriptName = 'Universal Chat Menu',
lastUpdate = 1559680306108,
//defaultTitle = 'Actions',
defaultHeader = (who, actions = 'Actions') => `${who} ${actions} Menu`,
BUTTONSYMBOL = '!',
MENUPLACE = 'CHATMENU',
SEPARATOR = ' ',
checkInstall = () => {
log('-=> ' + scriptName + ' v' + version + ' <=- [' + (new Date(lastUpdate)) + ']');
},
ch = function (c) {
var entities = {
'<': 'lt',
'>': 'gt',
"'": '#39',
'@': '#64',
'{': '#123',
'|': '#124',
'}': '#125',
'[': '#91',
']': '#93',
'"': 'quot',
'-': 'mdash',
' ': 'nbsp'
};
if (_.has(entities, c)) {
return ('&' + entities[c] + ';');
}
return '';
},
// build attribute name for a repeating section
repname = (section, id, name) => `repeating_${section}_${id}_${name}`,
// get an array of attributes on this character - handy to shorten code elsewhere.
getAttributes = (cid) => {
return findObjs({
_type: 'attribute',
_characterid: cid
});
},
// find a list of all the row ids in a repeating section.
getRepeatingIDs = (cid, section, attribute) => {
const repeating = getAttributes(cid)
.filter((obj) => {
return obj.get('name').startsWith(`repeating_${section}`) && obj.get('name').endsWith(attribute);
})
.map(obj => obj.get('name').replace(`repeating_${section}_`, '').replace('_' + attribute, '').trim());
return repeating;
},
checkFilter = (check, cid, prefix = '') => { // prefix is for repeating stats, supply 'repeating_${section}_${id}_' as the prefix
//log('=== FILTER ====');
//log(`check: ${check} prefix: ${prefix}`);
let tests = check.toString().split(',');
let rules = [];
let pass = 1; // if tests are passed, return 1, otherwise 0;
tests.forEach(test => { // need to check it has the three results, if only one, use rule.match = 0, and rule.type = '>'
let s = test.split(/[!<>=]/);
let rule = {};
rule.stat = s[0];
rule.match = s.length > 1 ? s[1] : '';
rule.type = s.length > 1 ? test.replace(s[0], '').replace(s[1], '') : '#'; // # is used for no filter
rules.push(rule);
// rules is an array of objects. don't need a key since will just be looping through it.
});
//log(`rules: ${JSON.stringify(rules)}`);
// now have a set of rules can use to test the filters
// will return true or false
for (let rule of rules) {
let value = getAttrByName(cid, `${prefix}${rule.stat}`) || ''; // if cell is empty, it is set to null
switch (rule.type) {
case '#': //no filter supplied, just include if attribute exists
pass = (0 === +value || (typeof value === 'string' && value.trim() === '')) ? false : true;
break;
case '=': // if rule.match exists in the target cell; if its a number treat as perfect match. Problem: null ||0 turns empty cells into 0.
if(isNaN(value)) {
pass = value.toString().toLowerCase().includes(rule.match.toString().toLowerCase());
} else {
pass = (+value || 0) === (+rule.match || 0);
}
break;
case '>':
pass = (+value || 0) >= (+rule.match || Infinity);
break;
case '<':
pass = (parseInt(value, 10) || 0) < (parseInt(rule.match, 10) || 0);
break;
case '!':
if(isNaN(value)) {
pass = !value.toString().toLowerCase().includes(rule.match.toString().toLowerCase());
} else {
pass = (+value || 0) !== (+rule.match || 0);
}
break;
}
if (!pass) break;
}
// at this point, pass will be 0 or 1, true or false;
return pass;
},
buildButtons = (cid, who, args) => {
let parameters = {};
let title = '';
parameters[title] = [];
let settings_allowed = ['title', 'footer', 'separator'];
for (let arg of args) {
let settings = arg.split(':');
let setting = settings[0].toLowerCase();
let setting_details = settings.length > 0 ? settings[1] : '';
if (settings_allowed.includes(setting)) {
// this is a config setting; only one supported right now is title
// if new title, create an empty array in parameters to hold the following attributes.
switch (setting) {
case 'title':
title = setting_details; // dont need defaulttitle here any more //|| defaultTitle;
if (!parameters.hasOwnProperty(title)) parameters[title] = [];
break;
case 'footer':
case 'separator':
parameters[setting] = setting_details;
break;
}
} else if (arg.toLowerCase().startsWith('repeating_')) {
// this is a repeating section, need to split on "|"
const repeatingAttribute = arg.split('|');
if (repeatingAttribute.length < 3) {
// not valid, skip it
parameters['error'] = 'Error in Repeating Attributue: ' + arg;
break;
}
const section = repeatingAttribute[0].replace('repeating_', '').trim(); //repeating section name
const display = repeatingAttribute[1].trim() || 'N/A'; //display name
let button = repeatingAttribute[2].trim() || 'N/A';
let buttonAttr = getButtonType(button);
if (buttonAttr) button = button.split(BUTTONSYMBOL)[0].trim();
if (display === 'N/A' || button === 'N/A') {
// this will likely never trigger, it just wont print that section.
parameters['error'] = 'Error in Repeating Attributue: ' + arg;
break;
}
let repeating = getRepeatingIDs(cid, section, display);
let checkAttribute = repeatingAttribute.length > 3 ? repeatingAttribute.slice(3) : [];
// build chat menu buttons
repeating.forEach(id => {
let showButton = 1;
if (checkAttribute.length > 0) {
showButton = checkFilter(checkAttribute,cid, `repeating_${section}_${id}_`);
}
if (showButton > 0) {
parameters[title].push(
getButtonCode(getAttrByName(cid, repname(section, id, display)), who, repname(section, id, button), buttonAttr));
}
});
} else {
// this is a non-repeating set of attributes, split on | and then ,
let singleAttributes = arg.split('|');
singleAttributes.forEach(attr => {
let attr_array = attr.split(',');
let label = attr_array[0];
let button = attr_array[1] || label;
let check = attr_array.length > 2 ? attr_array.slice(2) : [];
let buttonAttr = getButtonType(button);
if (buttonAttr) button = button.split(BUTTONSYMBOL)[0].trim();
// if check doesnt exist, or if it does exist but has a value of zero, print it. Otherwise don';'t
let pass = 1;
if(check.length > 0) {
//log(`${label}: ${check}`);
pass = checkFilter(check,cid,'');
}
if (pass) {
parameters[title].push(
getButtonCode(label, who, button, buttonAttr));
}
});
}
}
return parameters;
},
getButtonType = (button) => button.split(BUTTONSYMBOL).length > 1,
getButtonCode = (label, who, button, attribute = false) => {
let code = `[${label}](`;
if (attribute) {
code += `!&#13;/w gm &amp;${ch('{')}template:default${ch('}')}${ch('{')}${ch('{')}name=${ch('@')}${ch('{')}selected|character_name${ch('}')} ${label}${ch('}')}${ch('}')}${ch('{')}${ch('{')}=${ch('@')}${ch('{')}selected${ch('|')}${button}${ch('}')}${ch('}')}${ch('}')})`;
} else {
code += `~${who}|${button})`;
}
return code;
},
extractFrom = (buttons, field, fallback) => {
let found = fallback;
if (buttons.hasOwnProperty(field)) {
found = buttons[field];
delete buttons[field];
}
return found;
},
handleInput = (msg) => {
// copy the body of your on(chat:message) event to this function
if ('api' === msg.type && msg.content.toLowerCase().startsWith('!chatmenu ')) {
let args = msg.content.split(/\s+--/);
// get character and heading
let parameters = args[0].split(/\s+/);
const cid = parameters[1] || ''; //character id
if (cid === '') {
sendChat(scriptName, 'No recognised character.');
return;
}
const who = getAttrByName(cid, 'character_name');
if (!who) {
sendChat(scriptName, 'No recognised character.');
return;
}
const sender = 'character|' + cid;
const caller = (getObj('player', msg.playerid) || {
get: () => 'API'
}).get('_displayname');
let header = parameters.slice(2).join(' ') || defaultHeader(who);
// if this starts with {template: it is a rolltemplate
// and if it contains CHATMENU, the menu buttons are placed there. Otherwise they are placed at the end.
// get buttons
let combineSections = false; // if false, defaultTemplate sections; if true, combine all sections into one
args.shift();
let buttons = buildButtons(cid, who, args);
// buttons will be an object, each item key is a section title, and each item value is an array of buttons in that section
// build chat menu buttons
if (buttons.hasOwnProperty('error')) {
sendChat(scriptName, buttons['error'].toString());
return;
}
let headerPrint = `&{template:default}{{name=${header}}}`;
if (header.includes('template:')) {
headerPrint = `&${header}`;
}
headerPrint = headerPrint.replace(/(\[)/g, '[[').replace(/(\])/g, ']]'); // make sure no inline rolls present
let footer = extractFrom(buttons, 'footer', '');
let separator = extractFrom(buttons, 'separator', SEPARATOR);
let output = '';
let sections = Object.keys(buttons);
if (headerPrint.includes(MENUPLACE)) combineSections = true;
for (let section of sections) {
if (buttons[section].length > 0) {
output += (combineSections ? `**${section.toUpperCase()}**\n` : `{{**${section}**=`);
output += buttons[section].join(separator);
output += (combineSections ? `\n` : `}}`);
}
}
let print = combineSections ? headerPrint.replace(MENUPLACE, output) : headerPrint + output;
print += footer ? `\n/w ${caller} ${footer}` : '';
sendChat(sender, `/w ${caller} ${print}`);
}
},
registerEventHandlers = () => {
on('chat:message', handleInput);
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers
};
}());
on('ready', () => {
'use strict';
universalChatMenu.CheckInstall();
universalChatMenu.RegisterEventHandlers();
});