// ChatSetAttr version 1.0.1 // Last Updated: 2016-10-10 // A script to create, modify, or delete character attributes from the chat area or macros. // If you don't like my choices for --replace, you can edit the replacers variable at your own peril to change them. var chatSetAttr = chatSetAttr || (function() { 'use strict'; const version = '1.0.1', feedback = true, caseSensitive = false, replacers = [ ['<', '[', /',']' , />/g, /\]/g], ['#','|', /#/g, /\|/g], ['~','-', /\~/g, /\-/g], [';','?', /\;/g, /\?/g], ['`','@', /`/g, /@/g]], checkInstall = function() { log(`-=> ChatSetAttr v${version} <=-`); }, handleError = function(who, errorMsg) { let output = '/w "' + who + '"
' + '

Error

'+'

'+errorMsg+'

'+'
'; sendChat('ChatSetAttr', output, null, {noarchive:true}); }, getPlayerName = function(who) { let match = who.match(/(.*) \(GM\)/); return (match) ? match[1] : who; }, processInlinerolls = function (msg) { // Input: msg - chat message // Output: msg.content, with all inline rolls evaluated if (_.has(msg, 'inlinerolls')) { return _.chain(msg.inlinerolls) .reduce(function(previous, current, index) { previous['$[[' + index + ']]'] = current.results.total || 0; return previous; },{}) .reduce(function(previous, current, index) { return previous.replace(index, current); }, msg.content) .value(); } else { return msg.content; } }, deleteAttributes = function (allAttrs) { _.each(allAttrs, function(ch) { _.each(ch, function(attr) { attr.remove(); }); }); }, // Getting attributes from parsed options. Repeating attributes need special treatment // in order to parse row index and not create defective repeating rows. getRepeatingAttributes = function(who, list, setting, createMissing, failSilently) { let allAttrs = {}, allKeys = _.keys(setting), indexMatch, attrNameSplit, allSectionAttrs, id, name, repSectionIds, rowNum, rowId, idMatch; list.forEach(function(charid) { allAttrs[charid] = {}; }); allKeys.forEach(function(attrName) { indexMatch = attrName.match(/_\$\d+?_/); allSectionAttrs = {}, repSectionIds = {}; list.forEach(function(charid) { allSectionAttrs[charid] = {}; }); if (indexMatch) { rowNum = parseInt(indexMatch[0].replace('$','').replace(/_/g,'')); attrNameSplit = attrName.split(indexMatch[0]); } else { idMatch = attrName.match(/_(-[-A-Za-z0-9]+?)_/)[0], rowId = idMatch.replace(/_/g,''); attrNameSplit = attrName.split(idMatch); } filterObjs(function(o) { if (o.get('_type') === 'attribute') { id = o.get('_characterid'); name = o.get('name'); if (_.contains(list,id) && name.search('^' + attrNameSplit[0] + '_(-[-A-Za-z0-9]+?)_') !== -1) { allSectionAttrs[id][name] = o; return true; } } }); list.forEach(function(charid) { repSectionIds[charid] = _.chain(allSectionAttrs[charid]) .map((o,n) => n.match('^' + attrNameSplit[0] + '_(-[-A-Za-z0-9]+?)_')) .compact() .map(a => a[1]) .uniq() .value(); }); list.forEach(function(charid) { if (indexMatch && !_.isUndefined(repSectionIds[charid][rowNum])) { let realRepName = attrNameSplit[0] + '_' + repSectionIds[charid][rowNum] + '_' + attrNameSplit[1]; if (_.has(allSectionAttrs[charid], realRepName)) { allAttrs[charid][attrName] = allSectionAttrs[charid][realRepName] } else if (createMissing) { allAttrs[charid][attrName] = createObj('attribute', {characterid: charid , name: realRepName}); } else { handleError(who, 'Missing attribute '+realRepName+' not created for character '+getAttrByName(charid,'character_name')+'.'); } } else if (indexMatch) { handleError(who, 'Row number '+rowNum+' invalid for character '+getAttrByName(charid,'character_name')+' and repeating section '+attrNameSplit[0]+'.'); } else if (_.contains(repSectionIds[charid], rowId)) { let realRepName = attrNameSplit[0] + '_' + rowId + '_' + attrNameSplit[1]; if (_.has(allSectionAttrs[charid], realRepName)) { allAttrs[charid][attrName] = allSectionAttrs[charid][realRepName] } else if (createMissing) { allAttrs[charid][attrName] = createObj('attribute', {characterid: charid , name: realRepName}); } else if (!failSilently) { handleError(who, 'Missing attribute '+realRepName+' not created for character '+getAttrByName(charid,'character_name')+'.'); } } else if (!failSilently) { handleError(who, 'Repeating section id '+rowId+' invalid for character '+getAttrByName(charid,'character_name')+' and repeating section '+attrNameSplit[0]+'.'); } }); }); return allAttrs; }, getStandardAttributes = function(who, list, setting, createMissing, failSilently) { let allAttrs = {}, allKeys = _.keys(setting), allKeysUpper, id, name; if (!caseSensitive) { allKeysUpper = allKeys.map(x => x.toUpperCase()); } list.forEach(function(charid) { allAttrs[charid] = {}; }); filterObjs(function(o) { if (o.get('_type') === 'attribute') { id = o.get('_characterid'); name = o.get('name'); if (caseSensitive && _.contains(list,id) && _.contains(allKeys,name)) { allAttrs[id][name] = o; return true; } else if (_.contains(list,id) && _.contains(allKeysUpper,name.toUpperCase())) { allAttrs[id][allKeys[_.indexOf(allKeysUpper, name.toUpperCase())]] = o; return true; } } }); list.forEach(function(charid) { _.each(_.difference(allKeys, _.keys(allAttrs[charid])), function (key) { if (createMissing) { allAttrs[charid][key] = createObj('attribute', {characterid: charid , name: key}); } else if (!failSilently) { handleError(who, 'Missing attribute '+key+' not created for character '+getAttrByName(charid,'character_name')+'.'); } }); }); return allAttrs; }, getAllAttributes = function(who, list, setting, createMissing, failSilently) { let settingRepeating = _.pick(setting, (v,k) => (k.search(/^repeating_/) !== -1)); let settingStandard = _.omit(setting, _.keys(settingRepeating)); let standardAttrs = getStandardAttributes(who, list, settingStandard, createMissing, failSilently); let repeatingAttrs = getRepeatingAttributes(who, list, settingRepeating, createMissing, failSilently); let allAttrs = {}; list.forEach(function(charid) { allAttrs[charid] = _.defaults(standardAttrs[charid],repeatingAttrs[charid]); }); return allAttrs; }, // Setting attributes happens in a delayed recursive way to prevent the sandbox // from overheating. delayedSetAttributes = function(who, list, setting, allAttrs, mod) { let cList = _.clone(list), dWork = function(charid) { setCharAttributes(who, charid, setting, allAttrs[charid], mod); if (cList.length) { _.delay(dWork, 50, cList.shift()); } } dWork(cList.shift()); }, setCharAttributes = function(who, charid, setting, attrs, mod) { let current, max; _.each(_.pick(setting, _.keys(attrs)), function (attrValue,attrName) { let attr = attrs[attrName]; if (_.has(attrValue,'current')) { if (mod) { current = parseFloat(attrValue.current) + parseFloat(attr.get('current') || '0'); if (!_.isNaN(current)) { attr.set('current',current); } else { handleError(who,'Attribute '+attrName+' is not integer-valued for character '+getAttrByName(charid,'character_name')+'. Attribute left unchanged.'); } } else { attr.set('current', attrValue.current); } } if (_.has(attrValue,'max')) { if (mod) { max = parseFloat(attrValue.max) + parseFloat(attr.get('max') || '0'); if (!_.isNaN(max)) { attr.set('max',max); } else { handleError(who,'Attribute maximum '+attrName+' is not integer-valued for character '+getAttrByName(charid,'character_name')+'. Attribute maximum left unchanged.'); } } else { attr.set('max', attrValue.max); } } }); }, // These functions parse the chat input. parseOpts = function(content, hasValue) { // Input: content - string of the form command --opts1 --opts2 value --opts3. // values come separated by whitespace. // hasValue - array of all options which come with a value // Output: object containing key:true if key is not in hasValue. and containing // key:value otherwise let args, kv, opts = {}; args = _.rest(content.replace(/\n/g, ' ') .replace(/\s*$/g, '') .replace(/({{(.*?)\s*}}$)/g, "$2") .split(/\s+--/)); for (let k in args) { kv = args[k].split(/\s(.+)/); if (_.contains(hasValue, kv[0])) { opts[kv[0]] = kv[1]; } else { opts[args[k]] = true; } } return opts; }, parseAttributes = function(args, replace) { // Input: args - array containing comma-separated list of strings, every one of which contains // an expression of the form key|value or key|value|maxvalue // replace - true if characters from the replacers array should be replaced // Output: Object containing key|value for all expressions. let setting = _.chain(args) .map(str => str.split(/\s*\|\s*/)) .reject(a => a.length === 0) .map(sanitizeAttributeArray) .object() .value(); if (replace) { setting = _.mapObject(setting, function(obj) { return _.mapObject(obj, function (str) { for (let rep in replacers) { str = str.replace(replacers[rep][2],replacers[rep][1]); } return str; }); }); } return setting; }, sanitizeAttributeArray = function (arr) { if (arr.length === 1) return [arr[0],{current : ''}]; if (arr.length === 2) return [arr[0],{current : arr[1].replace(/^'/,'').replace(/\'$/,'')}]; if (arr.length === 3 && arr[1] === '') return [arr[0], {max : arr[2].replace(/^'/,'').replace(/\'$/,'')}]; if (arr.length === 3 && arr[1] === "''") return [arr[0], {current : '', max : arr[2].replace(/^'/,'').replace(/\'$/,'')}]; else if (arr.length === 3) return [arr[0], {current : arr[1].replace(/^'/,'').replace(/\'$/,''), max : arr[2].replace(/^'/,'').replace(/\'$/,'')}]; if (arr.length > 3) return sanitizeAttributeArray(_.first(arr,3)); }, // These functions are used to get a list of character ids from the input, // and check for permissions. checkPermissions = function (who, list, playerid) { let control, character; for (let k in list) { character = getObj('character',list[k]); if (character) { control = character.get('controlledby').split(/,/); if(!(playerIsGM(playerid) || _.contains(control,'all') || _.contains(control,playerid))) { list.splice(k,1); handleError(who, 'Permission error. Name: ' + character.get('name')); } } else { handleError(who, 'Invalid character id ' + list[k]); list.splice(k,1); } } return _.uniq(list); }, getIDsFromTokens = function (selected) { let charIDList = [], characterId, token; selected.forEach(function(a) { token = getObj('graphic', a._id); if (token) { characterId = token.get('represents'); if (characterId) { charIDList.push(characterId); } } }); return _.uniq(charIDList); }, getIDsFromNames = function(who, charNames, playerid) { let charIDList = _.chain(charNames.split(/\s*,\s*/)) .map(function (n) { let character = findObjs({type: 'character', name: n}, {caseInsensitive: true})[0]; if (character) return character.id; else return '';}) .compact() .value(); return checkPermissions(who, charIDList, playerid); }, getIDsFromList = function(who, charid, playerid) { return checkPermissions(who, charid.split(/\s*,\s*/), playerid); }, sendFeedback = function (who, list, setting, replace, mod) { let charNames = list.map(id => getAttrByName(id, "character_name")).join(', '); let values = _.chain(setting).values() .map(function (o) { return _.mapObject(o,function (str) { if (replace) { for (let rep in replacers) { str = str.replace(replacers[rep][3],replacers[rep][0]); } } return str; });}) .map(function (o) { if (o.max !== undefined && o.current !== undefined) return `${o.current} / ${o.max}`; if (o.max === undefined) return o.current; if (o.current === undefined) return `${o.max} (max)`; return '';}) .value() .join(', '); let output = '/w "'+ who; output += '"
'; if (mod) { output += `

Modifying ${_.keys(setting).join(', ')} by ${values} `; } else { output += `

Setting ${_.keys(setting).join(', ')} to ${values} `; } output += `for characters ${charNames}`; if (replace) { output += ' (replacing ' + _.map(replacers, arr => arr[0]).join() + ' by ' + _.map(replacers, arr => arr[1]).join() + ')'; } output += '.

'; sendChat('ChatSetAttr', output); }, sendDeleteFeedback = function (who, list, setting) { let charNames = list.map(id => getAttrByName(id, 'character_name')).join(', '); let output = '/w "'+ who + '"
' + '

Deleting attributes ' + _.keys(setting).join(', ') + ' for characters ' + charNames + '.

'; sendChat('ChatSetAttr', output); }, // Main function, called after chat message input handleInput = function(msg) { if (msg.type === 'api' && msg.content.search(/^!(set|del)attr\b/) !== -1) { // Parsing input let charIDList; const hasValue = ['charid','name'], optsArray = ['all','allgm','charid','name','silent','sel','replace', 'nocreate','mod'], who = getPlayerName(msg.who), opts = parseOpts(processInlinerolls(msg),hasValue), setting = parseAttributes(_.chain(opts).omit(optsArray).keys().value(),opts.replace), deleteMode = (msg.content.search(/^!delattr\b/) !== -1); if (_.isEmpty(setting)) { handleError(who, 'No attributes supplied.'); return; } // Get list of character IDs if (opts.all && playerIsGM(msg.playerid)) { charIDList = _.map(findObjs({_type: 'character'}), c => c.id); } else if (opts.allgm && playerIsGM(msg.playerid)) { charIDList = _.chain(findObjs({_type: 'character'})) .filter(c => c.get('controlledby') === '') .map(c => c.id) .value(); } else if (opts.charid) { charIDList = getIDsFromList(who, opts.charid, msg.playerid); } else if (opts.name) { charIDList = getIDsFromNames(who, opts.name, msg.playerid); } else if (opts.sel && msg.selected) { charIDList = getIDsFromTokens(msg.selected); } else { handleError(who,'No target characters. You need to supply one of --all, --allgm, --sel, --charid, or --name.'); return; } // Get attributes let allAttrs = getAllAttributes(who, charIDList, setting, !opts.nocreate && !deleteMode, deleteMode); // Set or delete attributes if (deleteMode) { deleteAttributes(allAttrs); if (feedback && !opts.silent && !_.isEmpty(charIDList)) { sendDeleteFeedback(who, charIDList, setting); } } else { delayedSetAttributes(who, charIDList, setting, allAttrs, opts.mod); if (feedback && !opts.silent && !_.isEmpty(charIDList)) { sendFeedback(who, charIDList, setting, opts.replace, opts.mod); } } } return; }, registerEventHandlers = function() { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready',function() { 'use strict'; chatSetAttr.CheckInstall(); chatSetAttr.RegisterEventHandlers(); });