var ShapedScripts =
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var roll20 = __webpack_require__(1);
var parseModule = __webpack_require__(3);
var mmFormat = __webpack_require__(4);
var myState = roll20.getState('ShapedScripts');
var logger = __webpack_require__(5)(myState.config);
var EntityLookup = __webpack_require__(6);
var JSONValidator = __webpack_require__(8);
var el = new EntityLookup();
var Reporter = __webpack_require__(9);
var reporter = new Reporter(roll20, 'Shaped Scripts');
var ShapedScripts = __webpack_require__(10);
var shaped = new ShapedScripts(logger, myState, roll20, parseModule.getParser(mmFormat, logger), el, reporter);
var _ = __webpack_require__(2);
//logger.wrapModule(el);
logger.wrapModule(roll20);
var jsonValidator = new JSONValidator(__webpack_require__(4));
el.configureEntity('monsters', [
EntityLookup.jsonValidatorAsEntityProcessor(jsonValidator),
el.getSpellHydrator()
], EntityLookup.jsonValidatorAsVersionChecker(jsonValidator));
el.configureEntity('spells', [el.getMonsterSpellUpdater()], EntityLookup.getVersionChecker('0.2'));
roll20.on('ready', function () {
shaped.checkInstall();
shaped.registerEventHandlers();
});
module.exports = {
addEntities: function (entities) {
try {
if (typeof entities === 'string') {
entities = JSON.parse(entities);
}
var result = el.addEntities(entities);
var summary = _.mapObject(result, function (resultObject, type) {
if (type === 'errors') {
return resultObject.length;
}
else {
return _.mapObject(resultObject, function (operationResultArray) {
return operationResultArray.length;
});
}
});
logger.info('Summary of adding entities to the lookup: $$$', summary);
logger.info('Details: $$$', result);
if (!_.isEmpty(result.errors)) {
var message = _.chain(result.errors)
.groupBy('entity')
.mapObject(function (entityErrors) {
return _.chain(entityErrors)
.pluck('errors')
.flatten()
.value();
})
.map(function (errors, entityName) {
return '
' + entityName + ':';
})
.value();
reporter.reportError('JSON import error:');
}
}
catch (e) {
reporter.reportError('JSON parse error, please see log for more information');
logger.error(e.toString());
logger.error(e.stack);
}
}
};
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
/* globals state:false, createObj, findObjs, filterObjs, getObj, getAttrByName, sendChat, on, log, Campaign, playerIsGM, spawnFx, spawnFxBetweenPoints */
'use strict';
var _ = __webpack_require__(2);
//noinspection JSUnusedGlobalSymbols
module.exports = {
getState: function (module) {
if (!state[module]) {
state[module] = {};
}
return state[module];
},
createObj: function (type, attributes) {
return createObj(type, attributes);
},
findObjs: function (attributes) {
return findObjs(attributes);
},
filterObjs: function (callback) {
return filterObjs(callback);
},
getObj: function (type, id) {
return getObj(type, id);
},
getOrCreateObj: function (type, attributes) {
var newAttributes = _.extend(_.clone(attributes), { type: type });
var existing = this.findObjs(newAttributes);
switch (existing.length) {
case 0:
return this.createObj(type, newAttributes);
case 1:
return existing[0];
default:
throw new Error('Asked for a single ' + type + ' but more than 1 was found matching attributes: ' +
JSON.stringify(attributes));
}
},
getAttrByName: function (character, attrName) {
return getAttrByName(character, attrName);
},
getAttrObjectByName: function (character, attrName) {
var attr = this.findObjs({ type: 'attribute', characterid: character, name: attrName });
return attr && attr.length > 0 ? attr[0] : null;
},
getOrCreateAttr: function (characterId, attrName) {
return this.getOrCreateObj('attribute', { characterid: characterId, name: attrName });
},
setAttrByName: function (characterId, attrName, value) {
this.getOrCreateAttr(characterId, attrName).set('current', value);
},
processAttrValue: function (characterId, attrName, cb) {
var attribute = this.getOrCreateAttr(characterId, attrName);
attribute.set('current', cb(attribute.get('current')));
},
getRepeatingSectionAttrs: function (characterId, sectionName) {
var prefix = 'repeating_' + sectionName;
return _.filter(this.findObjs({ type: 'attribute', characterid: characterId }), function (attr) {
return attr.get('name').indexOf(prefix) === 0;
});
},
getRepeatingSectionItemIdsByName: function (characterId, sectionName) {
var re = new RegExp('repeating_' + sectionName + '_([^_]+)_name$');
return _.reduce(this.getRepeatingSectionAttrs(characterId, sectionName),
function (lookup, attr) {
var match = attr.get('name').match(re);
if (match) {
lookup[attr.get('current').toLowerCase()] = match[1];
}
return lookup;
}, {});
},
getCurrentPage: function (playerId) {
var pageId;
if (this.playerIsGM(playerId)) {
pageId = this.getObj('player', playerId).get('lastpage');
}
else {
pageId = this.getCampaign().get('playerspecificpages')[playerId] || this.getCampaign().get('playerpageid');
}
return pageId ? this.getObj('page', pageId) : null;
},
spawnFx: function (pointsArray, fxType, pageId) {
switch (pointsArray.length) {
case 1:
spawnFx(pointsArray[0].x, pointsArray[0].y, fxType, pageId);
break;
case 2:
spawnFxBetweenPoints(pointsArray[0], pointsArray[1], fxType, pageId);
break;
default:
throw new Error('Wrong number of points supplied to spawnFx: $$$', pointsArray);
}
},
playerIsGM: function (playerId) {
return playerIsGM(playerId);
},
getCampaign: function () {
return Campaign(); //jshint ignore: line
},
sendChat: function (sendAs, message, callback, options) {
return sendChat(sendAs, message, callback, options);
},
on: function (event, callback) {
return on(event, callback);
},
log: function (msg) {
return log(msg);
},
logWrap: 'roll20'
};
/***/ },
/* 2 */
/***/ function(module, exports) {
module.exports = _;
/***/ },
/* 3 */
/***/ function(module, exports, __webpack_require__) {
var _ = __webpack_require__(2);
/**
* A specification of a field that can appear
* in the format that this parser processes
* @typedef {Object} FieldSpec
* @property {FieldSpec[]} [contentModel] - list of child fieldSpecs for complex content
* @property {boolean} [bare] - if true this field appears as a bare value with no parseToken in front of it
* @property {string} [parseToken] - the token that defines the beginning of this field (usually case insensitive). Not used for bare tokens.
* @property {string} [pattern] - a pattern that defines the value of this field. For bare properties this will determine
* if the field matches at all, whereas for normal ones this will just be used to validate them
* @property {number} [forNextMatchGroup] - the 1-based index of the match group from the supplied pattern that will contain text that
* should be handed to the next parser rather than used as part of this field.
* @property {number} [forPreviousMatchGroup] - the 1-based index of the match group from the supplied pattern that will contain text that
* should be handed to the previous parser to complete its value rather than used as part of this field.
* Only applicable to
* bare properties, since ones with a token have a clearly defined start based on the parseToken
* @property {number} [matchGroup=0] - the index of the capturing group in the supplied pattern to use as the value for this field. If left at the
* default of 0, the whole match will be used.
* @property {boolean} [caseSensitive=false] - If true, the pattern used for the value of this field will be made case sensitive. Note
* that parseToken matching is always case insensitive.
* @property {string} type - the type of this field. Currently valid values are [orderedContent, unorderedContent, string, enumType, integer, abililty]
* @property {string[]} enumValues - an array of valid values for this field if the type is enumType
* @property {number} [minOccurs=1] - the minimum number of times this field should occur in the parent content model.
* @property {number} [maxOccurs=1] - the maximum number of times this field should occur in the parent content model.
*/
/**
*
* @param {FieldSpec} formatSpec - Root format specification for this parser
* @param logger - Logger object to use for reporting errors etc.
* @returns {{parse:parse}} - A parser that will process text in the format specified by the supplied formatSpec into JSON objects
*/
function getParser(formatSpec, logger) {
'use strict';
//noinspection JSUnusedGlobalSymbols
var parserModule = {
makeContentModelParser: function (fieldSpec, ordered) {
var module = this;
return {
parse: function (stateManager, textLines, resume) {
var parseState = stateManager.enterChildParser(this, resume),
someMatch = false,
canContinue,
stopParser = null;
parseState.subParsers = parseState.subParsers || module.makeParserList(fieldSpec.contentModel);
if (parseState.resumeParser) {
if (!parseState.resumeParser.resumeParse(stateManager, textLines)) {
stateManager.leaveChildParser(this);
return false;
}
someMatch = true;
}
var parseRunner = function (parser, index, parsers) {
if (!parser.parse(stateManager, textLines)) {
if (parser.required === 0 || !ordered) {
//No match but it's ok to keep looking
//through the rest of the content model for one
return false;
}
//No match but one was required here by the content model
}
else {
parser.justMatched = true;
if (parser.required > 0) {
parser.required--;
}
parser.allowed--;
if (ordered) {
//Set all the previous parsers to be exhausted since we've matched
//this one and we're in a strictly ordered content model.
_.each(parsers.slice(0, index), _.partial(_.extend, _, { allowed: 0 }));
}
}
return true;
};
do {
stopParser = _.find(parseState.subParsers, parseRunner);
logger.debug('Stopped at parser $$$', stopParser);
canContinue = stopParser && stopParser.justMatched;
if (stopParser) {
someMatch = someMatch || stopParser.justMatched;
stopParser.justMatched = false;
}
//Lose any parsers that have used up all their cardinality already
parseState.subParsers = _.reject(parseState.subParsers, { allowed: 0 });
} while (!_.isEmpty(parseState.subParsers) && !_.isEmpty(textLines) && canContinue);
stateManager.leaveChildParser(this, someMatch ? parseState : undefined);
return someMatch;
},
resumeParse: function (stateManager, textLines) {
return this.parse(stateManager, textLines, true);
},
complete: function (parseState, finalText) {
var missingContent = _.filter(parseState.subParsers, 'required');
if (!_.isEmpty(missingContent)) {
throw new MissingContentError(missingContent);
}
}
};
},
matchParseToken: function (myParseState, textLines) {
if (_.isEmpty(textLines) || this.bare) {
return !_.isEmpty(textLines);
}
var re = new RegExp('^(.*?)(' + this.parseToken + ')(?:[\\s.]+|$)', 'i');
var match = textLines[0].match(re);
if (match) {
logger.debug('Found match $$$', match[0]);
myParseState.forPrevious = match[1];
myParseState.text = '';
textLines[0] = textLines[0].slice(match[0].length).trim();
if (!textLines[0]) {
textLines.shift();
}
}
return !!match;
},
matchValue: function (myParseState, textLines) {
if (this.pattern && this.bare) {
//If this is not a bare value then we can take all the text up to next
//token and just validate it at the end. If it is, then the pattern itself
//defines whether this field matches and we must run it immediately.
if (_.isEmpty(textLines)) {
return false;
}
textLines[0] = textLines[0].trim();
var matchGroup = this.matchGroup || 0;
var re = new RegExp(this.pattern, this.caseSensitive ? '' : 'i');
logger.debug('$$$ attempting to match value [$$$] against regexp $$$', this.name, textLines[0], re.toString());
var match = textLines[0].match(re);
if (match) {
logger.debug('Successful match! $$$', match);
myParseState.text = match[matchGroup];
if (!myParseState.forPrevious && this.forPreviousMatchGroup) {
logger.debug('Setting forPrevious to $$$', match[this.forPreviousMatchGroup]);
myParseState.forPrevious = match[this.forPreviousMatchGroup];
}
textLines[0] = textLines[0].slice(match.index + match[0].length);
if (this.forNextMatchGroup && match[this.forNextMatchGroup]) {
textLines[0] = match[this.forNextMatchGroup] + textLines[0];
}
if (!textLines[0]) {
myParseState.text += '\n';
textLines.shift();
}
return true;
}
else {
logger.debug('Match failed');
}
return false;
}
else {
logger.debug('$$$ standard string match, not using pattern', this.name);
myParseState.text = '';
return true;
}
},
orderedContent: function (fieldSpec) {
return this.makeContentModelParser(fieldSpec, true);
},
unorderedContent: function (fieldSpec) {
return this.makeContentModelParser(fieldSpec, false);
},
string: function (fieldSpec) {
return this.makeSimpleValueParser();
},
enumType: function (fieldSpec) {
var parser = this.makeSimpleValueParser();
if (fieldSpec.bare) {
parser.matchValue = function (myParseState, textLines) {
var parser = this;
var firstMatch = _.chain(fieldSpec.enumValues)
.map(function (enumValue) {
logger.debug('Attempting to parse as enum property $$$', enumValue);
var pattern = '^(.*?)(' + enumValue + ')(?:[\\s.]+|$)';
var re = new RegExp(pattern, parser.caseSensitive ? '' : 'i');
return textLines[0].match(re);
})
.compact()
.sortBy(function (match) {
return match[1].length;
})
.first()
.value();
if (firstMatch) {
logger.debug('Finished trying to parse as enum property, match: $$$', firstMatch);
myParseState.text = firstMatch[2];
myParseState.forPrevious = firstMatch[1];
textLines[0] = textLines[0].slice(firstMatch.index + firstMatch[0].length);
if (!textLines[0]) {
textLines.shift();
}
return true;
}
return false;
};
}
return parser;
},
number: function (fieldSpec) {
var parser = this.makeSimpleValueParser();
parser.typeConvert = function (textValue) {
var parts = textValue.split('/');
var intVal;
if (parts.length > 1) {
intVal = parts[0] / parts[1];
}
else {
intVal = parseInt(textValue);
}
if (_.isNaN(intVal)) {
throw new BadValueError(fieldSpec.name, textValue, '[Integer]');
}
return intVal;
};
return parser;
},
ability: function (fieldSpec) {
var parser = this.number();
parser.matchValue = function (parseState, textLines) {
if (_.isEmpty(textLines)) {
return false;
}
var re = new RegExp('^([\\sa-z\\(\\)]*)(\\d+(?:\\s?\\([\\-+\\d]+\\))?)', 'i');
logger.debug('Attempting to match value [$$$] against regexp $$$', textLines[0].trim(), re.toString());
var match = textLines[0].trim().match(re);
if (match) {
logger.debug('Successful match $$$', match);
parseState.text = match[2];
textLines[0] = match[1] + textLines[0].slice(match.index + match[0].length);
if (!textLines[0]) {
textLines.shift();
}
return true;
}
return false;
};
return parser;
},
heading: function (fieldSpec) {
fieldSpec.bare = true;
var parser = this.makeSimpleValueParser();
parser.skipOutput = true;
return parser;
},
makeSimpleValueParser: function () {
var module = this;
return {
parse: function (stateManager, textLines) {
var parseState = stateManager.enterChildParser(this);
var match = this.matchParseToken(parseState, textLines) &&
this.matchValue(parseState, textLines);
if (match) {
stateManager.completeCurrentStack(parseState.forPrevious);
delete parseState.forPrevious;
stateManager.leaveChildParser(this, parseState);
}
else {
stateManager.leaveChildParser(this);
}
return match;
},
complete: function (parseState, finalText) {
parseState.text += finalText ? finalText : '';
if (parseState.text) {
parseState.value = this.extractValue(parseState.text);
parseState.value = this.typeConvert(parseState.value);
parseState.setOutputValue();
}
},
extractValue: function (text) {
text = text.trim();
if (this.pattern && !this.bare) {
var regExp = new RegExp(this.pattern, this.caseSensitive ? '' : 'i');
var match = text.match(regExp);
if (match) {
var matchGroup = this.matchGroup || 0;
return match[matchGroup];
}
else {
throw new BadValueError(this.name, text, regExp);
}
}
else {
return text;
}
},
typeConvert: function (textValue) {
return textValue;
},
resumeParse: function (stateManager, textLines) {
if (_.isEmpty(textLines)) {
return false;
}
var parseState = stateManager.enterChildParser(this, true);
parseState.text += textLines.shift() + '\n';
stateManager.leaveChildParser(this, parseState);
return true;
},
matchParseToken: module.matchParseToken,
matchValue: module.matchValue
};
},
makeBaseParseState: function (skipOutput, propertyPath, outputObject, completedObjects) {
return {
text: '',
getObjectValue: function () {
var value = outputObject;
var segments = _.clone(propertyPath);
while (segments.length) {
var prop = segments.shift();
if (prop.flatten) {
continue;
}
value = value[prop.name];
if (_.isArray(value)) {
value = _.last(value);
}
}
return value;
},
setOutputValue: function () {
if (skipOutput) {
return;
}
var outputTo = outputObject;
var segments = _.clone(propertyPath);
while (segments.length > 0) {
var prop = segments.shift();
if (prop.flatten) {
continue;
}
var currentValue = outputTo[prop.name];
var newValue = segments.length === 0 ? this.value : {};
if (_.isUndefined(currentValue) && prop.allowed > 1) {
currentValue = [];
outputTo[prop.name] = currentValue;
}
if (_.isArray(currentValue)) {
var arrayItem = _.find(currentValue, _.partial(_.negate(_.contains), completedObjects));
if (!arrayItem) {
currentValue.push(newValue);
arrayItem = _.last(currentValue);
}
newValue = arrayItem;
}
else if (_.isUndefined(currentValue)) {
outputTo[prop.name] = newValue;
}
else if (segments.length === 0) {
throw new Error('Simple value property somehow already had value when we came to set it');
}
else {
newValue = currentValue;
}
outputTo = newValue;
}
},
logWrap: 'parseState[' + _.pluck(propertyPath, 'name').join('/') + ']',
toJSON: function () {
return _.extend(_.clone(this), { propertyPath: propertyPath });
}
};
},
makeParseStateManager: function () {
var incompleteParserStack = [];
var currentPropertyPath = [];
var completedObjects = [];
var module = this;
return {
outputObject: {},
leaveChildParser: function (parser, state) {
currentPropertyPath.pop();
if (state) {
state.resumeParser = _.isEmpty(incompleteParserStack) ? null : _.last(incompleteParserStack).parser;
incompleteParserStack.push({ parser: parser, state: state });
}
},
completeCurrentStack: function (finalText) {
while (!_.isEmpty(incompleteParserStack)) {
var incomplete = incompleteParserStack.shift();
incomplete.parser.complete(incomplete.state, finalText);
var value = incomplete.state.getObjectValue();
if (_.isObject(value) && !incomplete.parser.flatten) {
//Crude but this list is unlikely to get that big
completedObjects.push(value);
}
}
},
enterChildParser: function (parser, resume) {
currentPropertyPath.push({
name: parser.name,
allowed: parser.allowed,
flatten: parser.flatten
});
if (!resume || _.isEmpty(incompleteParserStack) || parser !== _.last(incompleteParserStack).parser) {
return module.makeBaseParseState(parser.skipOutput, _.clone(currentPropertyPath), this.outputObject, completedObjects);
}
return incompleteParserStack.pop().state;
},
logWrap: 'parserState',
toJSON: function () {
return _.extend(_.clone(this), {
incompleteParsers: incompleteParserStack,
propertyPath: currentPropertyPath
});
}
};
},
parserId: 0,
parserAttributes: [
'forPreviousMatchGroup', 'forNextMatchGroup',
'parseToken', 'flatten', 'pattern', 'matchGroup', 'bare', 'caseSensitive',
'name', 'skipOutput'
],
getParserFor: function (fieldSpec) {
logger.debug('Making parser for field $$$', fieldSpec);
var parserBuilder = this[fieldSpec.type];
if (!parserBuilder) {
throw new Error('Can\'t make parser for type ' + fieldSpec.type);
}
var parser = parserBuilder.call(this, fieldSpec);
parser.required = _.isUndefined(fieldSpec.minOccurs) ? 1 : fieldSpec.minOccurs;
parser.allowed = _.isUndefined(fieldSpec.maxOccurs) ? 1 : fieldSpec.maxOccurs;
_.extend(parser, _.pick(fieldSpec, this.parserAttributes));
_.defaults(parser, {
parseToken: parser.name
});
parser.id = this.parserId++;
parser.logWrap = 'parser[' + parser.name + ']';
return parser;
},
makeParserList: function (contentModelArray) {
var module = this;
return _.chain(contentModelArray)
.reject('noParse')
.reduce(function (parsers, fieldSpec) {
parsers.push(module.getParserFor(fieldSpec));
return parsers;
}, [])
.value();
},
logWrap: 'parseModule'
};
logger.wrapModule(parserModule);
var parser = parserModule.getParserFor(formatSpec);
return {
parse: function (text) {
logger.debug('Text: $$$', text);
var textLines = _.chain(text.split('\n'))
.invoke('trim')
.compact()
.value();
logger.debug(parser);
var stateManager = parserModule.makeParseStateManager();
var success = parser.parse(stateManager, textLines);
while (success && !_.isEmpty(textLines)) {
parser.resumeParse(stateManager, textLines);
}
stateManager.completeCurrentStack(textLines.join('\n'));
if (success && textLines.length === 0) {
stateManager.outputObject.version = formatSpec.formatVersion;
logger.info(stateManager.outputObject);
return stateManager.outputObject;
}
return null;
}
};
}
/**
* @constructor
*/
function ParserError(message) {
'use strict';
//noinspection JSUnusedGlobalSymbols
this.message = message;
}
ParserError.prototype = new Error();
/**
* @constructor
*/
function MissingContentError(missingFieldParsers) {
'use strict';
this.missingFieldParsers = missingFieldParsers;
//noinspection JSUnusedGlobalSymbols
this.message = _.reduce(this.missingFieldParsers, function (memo, parser) {
return memo + 'Field ' + parser.parseToken + ' should have appeared ' + parser.required + ' more times';
}, '';
}
MissingContentError.prototype = new ParserError();
/**
* @constructor
*/
function BadValueError(name, value, pattern) {
'use strict';
this.name = name;
this.value = value;
this.pattern = pattern;
//noinspection JSUnusedGlobalSymbols
this.message = 'Bad value [' + this.value + '] for field [' + this.name + ']. Should have matched pattern: ' +
this.pattern;
}
BadValueError.prototype = new ParserError();
module.exports = {
getParser: getParser,
MissingContentError: MissingContentError,
BadValueError: BadValueError,
ParserError: ParserError
};
/***/ },
/* 4 */
/***/ function(module, exports) {
module.exports = {
"formatVersion": "0.2",
"name": "monsters",
"maxOccurs": "Infinity",
"type": "orderedContent",
"bare": true,
"contentModel": [
{
"name": "coreInfo",
"type": "orderedContent",
"flatten": true,
"contentModel": [
{
"name": "name",
"type": "string",
"bare": "true"
},
{
"name": "size",
"enumValues": [
"Tiny",
"Small",
"Medium",
"Large",
"Huge",
"Gargantuan"
],
"type": "enumType",
"bare": "true"
},
{
"name": "type",
"type": "string",
"bare": "true",
"pattern": "^([\\w\\s\\(\\),-]+),",
"matchGroup": 1
},
{
"name": "alignment",
"type": "enumType",
"enumValues": [
"lawful good",
"lawful neutral",
"lawful evil",
"neutral good",
"neutral evil",
"neutral",
"chaotic good",
"chaotic neutral",
"chaotic evil",
"unaligned",
"any alignment",
"any good alignment",
"any non-good alignment",
"any evil alignment",
"any non-evil alignment",
"any lawful alignment",
"any non-lawful alignment",
"any chaotic alignment",
"any non-chaotic alignment",
"construct",
"(?:lawful|neutral|chaotic) (?:good|neutral|evil) \\(\\d+%\\) or (?:lawful|neutral|chaotic) (?:good|neutral|evil) \\(\\d+%\\)"
],
"bare": true
}
]
},
{
"name": "attributes",
"type": "unorderedContent",
"flatten": true,
"contentModel": [
{
"name": "AC",
"parseToken": "armor class",
"pattern": "\\d+\\s*(?:\\([^)]*\\))?",
"type": "string"
},
{
"name": "HP",
"parseToken": "hit points",
"type": "string",
"pattern": "\\d+(?:\\s?\\(\\s?\\d+d\\d+(?:\\s?[-+]\\s?\\d+)?\\s?\\))?"
},
{
"name": "speed",
"minOccurs": 0,
"type": "string",
"pattern": "^\\d+\\s?ft[\\.]?(,\\s?(fly|swim|burrow|climb)\\s\\d+\\s?ft[\\.]?)*(\\s?\\([^\\)]+\\))?$"
},
{
"name": "strength",
"parseToken": "str",
"type": "ability"
},
{
"name": "dexterity",
"parseToken": "dex",
"type": "ability"
},
{
"name": "constitution",
"parseToken": "con",
"type": "ability"
},
{
"name": "intelligence",
"parseToken": "int",
"type": "ability"
},
{
"name": "wisdom",
"parseToken": "wis",
"type": "ability"
},
{
"name": "charisma",
"parseToken": "cha",
"type": "ability"
},
{
"name": "savingThrows",
"minOccurs": 0,
"parseToken": "saving throws",
"type": "string",
"pattern": "(?:(?:^|,\\s*)(?:Str|Dex|Con|Int|Wis|Cha)\\s+[\\-\\+]\\d+)+"
},
{
"name": "skills",
"minOccurs": 0,
"type": "string",
"pattern": "(?:(?:^|,\\s*)(?:Acrobatics|Animal Handling|Arcana|Athletics|Deception|History|Insight|Intimidation|Investigation|Medicine|Nature|Perception|Performance|Persuasion|Religion|Sleight of Hand|Stealth|Survival)\\s+[\\-\\+]\\d+)+"
},
{
"minOccurs": 0,
"type": "string",
"name": "damageVulnerabilities",
"parseToken": "damage vulnerabilities"
},
{
"minOccurs": 0,
"type": "string",
"name": "damageResistances",
"parseToken": "damage resistances"
},
{
"minOccurs": 0,
"type": "string",
"name": "damageImmunities",
"parseToken": "damage immunities"
},
{
"minOccurs": 0,
"type": "string",
"name": "conditionImmunities",
"parseToken": "condition immunities"
},
{
"name": "senses",
"type": "string",
"minOccurs": 0,
"pattern": "(?:(?:^|,\\s*)(?:blindsight|darkvision|tremorsense|truesight)\\s+\\d+\\s*ft\\.?(?: or \\d+ ft\\. while deafened)?(?:\\s?\\([^\\)]+\\))?)+"
},
{
"name": "passivePerception",
"parseToken": ",?\\s*passive Perception",
"minOccurs": 0,
"type": "number",
"skipOutput": true
},
{
"name": "spells",
"minOccurs": 0,
"type": "string"
},
{
"name": "languages",
"minOccurs": 0,
"type": "string"
}
]
},
{
"name": "challenge",
"type": "string",
"pattern": "^\\s*(\\d+(?:\\s*\\/\\s*\\d)?)\\s*(?:\\(\\s*[\\d,]+\\s*XP\\s*\\)\\s*)?$",
"matchGroup": 1
},
{
"name": "traitSection",
"type": "orderedContent",
"minOccurs": 0,
"maxOccurs": 1,
"flatten": true,
"contentModel": [
{
"name": "traits",
"type": "orderedContent",
"minOccurs": 1,
"maxOccurs": "Infinity",
"contentModel": [
{
"name": "name",
"type": "string",
"pattern": "(^|.*?[a-z]\\.\\s?)((?:[A-Z][\\w\\-']+[,:!]?|A)(?:\\s(?:[A-Z][\\w\\-']+[,:!]?|of|to|in|the|with|and|or|a)+)*)(\\s?\\([^\\)]+\\))?\\.(?!$)",
"matchGroup": 2,
"forPreviousMatchGroup": 1,
"forNextMatchGroup": 3,
"bare": true,
"caseSensitive": true
},
{
"name": "recharge",
"type": "string",
"pattern": "^\\(([^\\)]+)\\)",
"bare": true,
"matchGroup": 1,
"minOccurs": 0
},
{
"name": "text",
"bare": true,
"type": "string"
}
]
}
]
},
{
"name": "actionSection",
"type": "orderedContent",
"minOccurs": 0,
"maxOccurs": 1,
"flatten": true,
"contentModel": [
{
"name": "actionHeader",
"type": "heading",
"bare": true,
"pattern": "^Actions$"
},
{
"name": "actions",
"type": "orderedContent",
"minOccurs": 1,
"maxOccurs": "Infinity",
"contentModel": [
{
"name": "name",
"type": "string",
"pattern": "(^|.*?[a-z]\\.\\s?)((?:\\d+\\.\\s?)?(?:[A-Z][\\w\\-']+[,:!]?|A)(?:\\s(?:[A-Z][\\w\\-']+[,:!]?|of|in|to|with|the|and|or|a|\\+\\d+)+)*)(\\s?\\([^\\)]+\\))?\\.(?!$)",
"matchGroup": 2,
"forPreviousMatchGroup": 1,
"forNextMatchGroup": 3,
"bare": true,
"caseSensitive": true
},
{
"name": "recharge",
"type": "string",
"bare": true,
"pattern": "^\\(([^\\)]+)\\)",
"matchGroup": 1,
"minOccurs": 0
},
{
"name": "text",
"bare": true,
"type": "string"
}
]
}
]
},
{
"name": "reactionSection",
"type": "orderedContent",
"minOccurs": 0,
"maxOccurs": 1,
"flatten": true,
"contentModel": [
{
"name": "reactionHeader",
"type": "heading",
"bare": true,
"pattern": "^Reactions$"
},
{
"name": "reactions",
"type": "orderedContent",
"minOccurs": 1,
"maxOccurs": "Infinity",
"contentModel": [
{
"name": "name",
"type": "string",
"pattern": "(^|.*?[a-z]\\.\\s?)((?:[A-Z][\\w\\-']+[,:!]?|A)(?:\\s(?:[A-Z][\\w\\-']+[,:!]?|of|in|to|with|the|and|or|a)+)*)(\\s?\\([^\\)]+\\))?\\.(?!$)",
"matchGroup": 2,
"forPreviousMatchGroup": 1,
"forNextMatchGroup": 3,
"bare": true,
"caseSensitive": true
},
{
"name": "recharge",
"type": "string",
"bare": true,
"pattern": "^\\(([^\\)]+)\\)",
"matchGroup": 1,
"minOccurs": 0
},
{
"name": "text",
"bare": true,
"type": "string"
}
]
}
]
},
{
"name": "legendaryActionSection",
"type": "orderedContent",
"minOccurs": 0,
"maxOccurs": 1,
"flatten": true,
"contentModel": [
{
"name": "actionHeader",
"type": "heading",
"bare": true,
"pattern": "^Legendary Actions$"
},
{
"name": "legendaryPoints",
"type": "number",
"bare": true,
"pattern": "^The[ \\w]+can take (\\d+) legendary actions.*?start of its turn[.]?",
"matchGroup": 1
},
{
"name": "legendaryActions",
"type": "orderedContent",
"minOccurs": 1,
"maxOccurs": "Infinity",
"contentModel": [
{
"name": "name",
"type": "string",
"bare": true,
"pattern": "(^|.*?[a-z]\\.\\s?)((?:[A-Z][\\w\\-']+[,:!]?|A)(?:\\s(?:[A-Z][\\w\\-']+[,:!]?|of|with|to|the|and|or|a)+)*)(\\s?\\([^\\)]+\\))?\\.(?!$)",
"matchGroup": 2,
"forPreviousMatchGroup": 1,
"forNextMatchGroup": 3,
"caseSensitive": true
},
{
"name": "cost",
"type": "number",
"bare": true,
"pattern": "^\\s*\\(\\s*costs (\\d+) actions\\s*\\)",
"matchGroup": 1,
"minOccurs": 0
},
{
"name": "text",
"bare": true,
"type": "string"
}
]
}
]
},
{
"name": "lairActionSection",
"type": "orderedContent",
"minOccurs": 0,
"maxOccurs": 1,
"flatten": true,
"contentModel": [
{
"name": "actionHeader",
"type": "heading",
"bare": true,
"pattern": "^Lair Actions$"
},
{
"name": "lairActions",
"type": "string",
"bare": true,
"minOccurs": 1,
"maxOccurs": "Infinity"
}
]
},
{
"name": "regionalEffectsSection",
"type": "orderedContent",
"minOccurs": 0,
"maxOccurs": 1,
"flatten": true,
"contentModel": [
{
"name": "actionHeader",
"type": "heading",
"bare": true,
"pattern": "^Regional Effects$"
},
{
"name": "regionalEffects",
"type": "string",
"minOccurs": 1,
"maxOccurs": "Infinity",
"bare": true
},
{
"name": "regionalEffectsFade",
"type": "string",
"bare": true
}
]
}
]
};
/***/ },
/* 5 */
/***/ function(module, exports, __webpack_require__) {
var _ = __webpack_require__(2);
var roll20 = __webpack_require__(1);
/**
*
* @param config
* @returns {{debug:function, error:function, info:function, trace:function, warn:function}}
*/
module.exports = function (config) {
'use strict';
var logger = {
OFF: 0,
ERROR: 1,
WARN: 2,
INFO: 3,
DEBUG: 4,
TRACE: 5,
prefixString: ''
},
stringify = function (object) {
if (object === undefined) {
return object;
}
return typeof object === 'string' ? object : JSON.stringify(object, function (key, value) {
if (key !== 'logWrap' && key !== 'isLogWrapped') {
return value;
}
});
},
shouldLog = function (level) {
var logLevel = logger.INFO;
if (config && config.logLevel) {
logLevel = logger[config.logLevel];
}
return level <= logLevel;
},
outputLog = function (level, message) {
if (!shouldLog(level)) {
return;
}
var args = arguments.length > 2 ? _.toArray(arguments).slice(2) : [];
message = stringify(message);
if (message) {
message = message.replace(/\$\$\$/g, function () {
return stringify(args.shift());
});
}
//noinspection NodeModulesDependencies
roll20.log('ShapedScripts ' + Date.now() + ' ' + logger.getLabel(level) + ' : ' +
(shouldLog(logger.TRACE) ? logger.prefixString : '') +
message);
};
logger.getLabel = function (logLevel) {
var logPair = _.chain(logger).pairs().find(function (pair) {
return pair[1] === logLevel;
}).value();
return logPair ? logPair[0] : 'UNKNOWN';
};
_.each(logger, function (level, levelName) {
logger[levelName.toLowerCase()] = _.partial(outputLog, level);
});
logger.wrapModule = function (modToWrap) {
if (shouldLog(logger.TRACE)) {
_.chain(modToWrap)
.functions()
.each(function (funcName) {
var origFunc = modToWrap[funcName];
modToWrap[funcName] = logger.wrapFunction(funcName, origFunc, modToWrap.logWrap);
});
modToWrap.isLogWrapped = true;
}
};
logger.getLogTap = function (level, messageString) {
return _.partial(outputLog, level, messageString);
};
logger.wrapFunction = function (name, func, moduleName) {
if (shouldLog(logger.TRACE)) {
if (name === 'toJSON' || moduleName === 'roll20' && name === 'log') {
return func;
}
return function () {
logger.trace('$$$.$$$ starting with this value: $$$ and args $$$', moduleName, name, this, arguments);
logger.prefixString = logger.prefixString + ' ';
var retVal = func.apply(this, arguments);
logger.prefixString = logger.prefixString.slice(0, -2);
logger.trace('$$$.$$$ ending with return value $$$', moduleName, name, retVal);
if (retVal && retVal.logWrap && !retVal.isLogWrapped) {
logger.wrapModule(retVal);
}
return retVal;
};
}
return func;
};
//noinspection JSValidateTypes
return logger;
};
/***/ },
/* 6 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
var utils = __webpack_require__(7);
module.exports = EntityLookup;
function EntityLookup() {
var entities = {},
noWhiteSpaceEntities = {},
entityProcessors = {},
versionCheckers = {},
self = this;
this.configureEntity = function (entityName, processors, versionChecker) {
entities[entityName] = {};
noWhiteSpaceEntities[entityName] = {};
entityProcessors[entityName] = processors;
versionCheckers[entityName] = versionChecker;
};
this.addEntities = function (entitiesObject) {
var results = {
errors: []
};
_.chain(entitiesObject)
.omit('version', 'patch')
.each(function (entityArray, type) {
results[type] = {
withErrors: [],
skipped: [],
deleted: [],
patched: [],
added: []
};
if (!entities[type]) {
results.errors.push({ entity: 'general', errors: ['Unrecognised entity type ' + type] });
return;
}
if (!versionCheckers[type](entitiesObject.version, results.errors)) {
return;
}
_.each(entityArray, function (entity) {
var key = entity.name.toLowerCase();
var operation = !!entities[type][key] ? (entitiesObject.patch ? 'patched' : 'skipped') : 'added';
if (operation === 'patched') {
entity = patchEntity(entities[type][key], entity);
if (!entity) {
operation = 'deleted';
delete entities[type][key];
delete noWhiteSpaceEntities[type][key.replace(/\s+/g, '')];
}
}
if (_.contains(['patched', 'added'], operation)) {
var processed = _.reduce(entityProcessors[type], utils.executor, {
entity: entity,
type: type,
version: entitiesObject.version,
errors: []
});
if (!_.isEmpty(processed.errors)) {
processed.entity = processed.entity.name;
results.errors.push(processed);
operation = 'withErrors';
}
else {
if (processed.entity.name.toLowerCase() !== key) {
results[type].deleted.push(key);
delete entities[type][key];
delete noWhiteSpaceEntities[type][key.replace(/\s+/g, '')];
key = processed.entity.name.toLowerCase();
}
entities[type][key] = processed.entity;
noWhiteSpaceEntities[type][key.replace(/\s+/g, '')] = processed.entity;
}
}
results[type][operation].push(key);
});
});
return results;
};
this.findEntity = function (type, name, tryWithoutWhitespace) {
var key = name.toLowerCase();
if (!entities[type]) {
throw new Error('Unrecognised entity type ' + type);
}
var found = entities[type][key];
if (!found && tryWithoutWhitespace) {
found = noWhiteSpaceEntities[type][key.replace(/\s+/g, '')];
}
return found && utils.deepClone(found);
};
this.getAll = function (type) {
if (!entities[type]) {
throw new Error('Unrecognised entity type: ' + type);
}
return utils.deepClone(_.values(entities[type]));
};
/**
* Gets all of the keys for the specified entity type
* @param {string} type - The entity type to retrieve keys for (either 'monster' or 'spell')
* @param {boolean} sort - True if the returned array should be sorted alphabetically; false otherwise
* @function
* @public
* @name EntityLookup#getKeys
* @return {Array} An array containing all keys for the specified entity type
*/
this.getKeys = function (type, sort) {
if (!entities[type]) {
throw new Error('Unrecognised entity type: ' + type);
}
var keys = _.keys(entities[type]);
if (sort) {
keys.sort();
}
return keys;
};
/**
* Gets spell hydrator
* @function
* @public
* @name EntityLookup#getSpellHydrator
* @return {function}
*/
this.getSpellHydrator = function () {
return function (monsterInfo) {
var monster = monsterInfo.entity;
if (monster.spells) {
monster.spells = _.map(monster.spells.split(', '), function (spellName) {
return self.findEntity('spells', spellName) || spellName;
});
}
return monsterInfo;
};
};
this.getMonsterSpellUpdater = function () {
return function (spellInfo) {
var spell = spellInfo.entity;
_.chain(entities.monsters)
.pluck('spells')
.compact()
.each(function (spellArray) {
var spellIndex = _.findIndex(spellArray, function (monsterSpell) {
if (typeof monsterSpell === 'string') {
return monsterSpell.toLowerCase() === spell.name.toLowerCase();
}
else {
return monsterSpell !== spell && monsterSpell.name.toLowerCase() === spell.name.toLowerCase();
}
});
if (spellIndex !== -1) {
spellArray[spellIndex] = spell;
}
});
return spellInfo;
};
};
this.toJSON = function () {
return { monsterCount: _.size(entities.monsters), spellCount: _.size(entities.spells) };
};
}
EntityLookup.prototype.logWrap = 'entityLookup';
EntityLookup.jsonValidatorAsEntityProcessor = function (jsonValidator) {
return function (entityInfo) {
var wrapper = {
version: entityInfo.version
};
wrapper[entityInfo.type] = [entityInfo.entity];
var errors = jsonValidator.validate(wrapper);
var flattenedErrors = _.chain(errors).values().flatten().value();
entityInfo.errors = entityInfo.errors.concat(flattenedErrors);
return entityInfo;
};
};
EntityLookup.jsonValidatorAsVersionChecker = function (jsonValidator) {
return EntityLookup.getVersionChecker(jsonValidator.getVersionNumber());
};
EntityLookup.getVersionChecker = function (requiredVersion) {
return function (version, errorsArray) {
var valid = version === requiredVersion;
if (!valid) {
errorsArray.push({
entity: 'general',
errors: ['Incorrect entity objects version: [' + version + ']. Required is: ' + requiredVersion]
});
}
return valid;
};
};
function patchEntity(original, patch) {
if (patch.remove) {
return undefined;
}
return _.mapObject(original, function (propVal, propName) {
if (propName === 'name' && patch.newName) {
return patch.newName;
}
return patch[propName] || propVal;
});
}
/***/ },
/* 7 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
//noinspection JSUnusedGlobalSymbols
module.exports = {
deepExtend: function (original, newValues) {
var self = this;
if (!original) {
original = _.isArray(newValues) ? [] : {};
}
_.each(newValues, function (value, key) {
if (_.isArray(original[key])) {
if (!_.isArray(value)) {
original[key].push(value);
}
else {
original[key] = _.map(value, function (item, index) {
if (_.isObject(item)) {
return self.deepExtend(original[key][index], item);
}
else {
return item !== undefined ? item : original[key][index];
}
});
}
}
else if (_.isObject(original[key])) {
original[key] = self.deepExtend(original[key], value);
}
else {
original[key] = value;
}
});
return original;
},
createObjectFromPath: function (pathString, value) {
var newObject = {};
_.reduce(pathString.split(/\./), function (object, pathPart, index, pathParts) {
var match = pathPart.match(/([^.\[]*)(?:\[(\d+)\])?/);
var newVal = index === pathParts.length - 1 ? value : {};
if (match[2]) {
object[match[1]] = [];
object[match[1]][match[2]] = newVal;
}
else {
object[match[1]] = newVal;
}
return newVal;
}, newObject);
return newObject;
},
getObjectFromPath: function (obj, path) {
path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
path = path.replace(/^\./, ''); // strip a leading dot
var a = path.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in obj) {
obj = obj[k];
}
else {
return;
}
}
return obj;
},
deepClone: function (object) {
return JSON.parse(JSON.stringify(object));
},
executor: function () {
switch (arguments.length) {
case 0:
return;
case 1:
return arguments[0]();
default:
var args = Array.apply(null, arguments).slice(2);
args.unshift(arguments[0]);
return arguments[1].apply(null, args);
}
},
/**
* Gets a string as 'Title Case' capitalizing the first letter of each word (i.e. 'the grapes of wrath' -> 'The Grapes Of Wrath')
* @param {string} s - The string to be converted
* @return {string} the supplied string in title case
*/
toTitleCase: function (s) {
return s.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
},
/**
* Calculates a contrasting color using YIQ luma value
* @param {string} hexcolor - the color to calculate a contrasting color for
* @return {string} either 'white' or 'black' as determined to be the best contrasting text color for the input color
*/
getContrastYIQ: function (hexcolor) {
hexcolor = hexcolor.replace('#', '');
if (hexcolor.length === 3) {
hexcolor += hexcolor;
}
var r = parseInt(hexcolor.substr(0, 2), 16);
var g = parseInt(hexcolor.substr(2, 2), 16);
var b = parseInt(hexcolor.substr(4, 2), 16);
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? 'black' : 'white';
},
/**
* Builds an html element as a string using the specified options
* @param {string} tag - the html tag type
* @param innerHtml - can be a string to be used as the element inner html, or a {tag, innerHtml, attrs} object
* in order to build a child html element string
* @param attrs - a collection of attributes and their values to be applied to the html element
* @return {string} the full html element as a string
*/
buildHTML: function (tag, innerHtml, attrs) {
var self = this;
if (typeof innerHtml === 'object') {
var res = '';
_.each(innerHtml, function (html) {
if (!_.isUndefined(html)) {
res += '' + self.buildHTML(html.tag, html.innerHtml, html.attrs);
}
});
innerHtml = res;
}
var h = '<' + tag;
for (var attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
if (attrs[attr] === false) {
continue;
}
h += ' ' + attr + '="' + attrs[attr] + '"';
}
}
h += innerHtml ? '>' + innerHtml + '' + tag + '>' : '/>';
return h;
},
versionCompare: function (v1, v2) {
if (v1 === v2) {
return 0;
}
else if (v1 === undefined || v1 === null) {
return -1;
}
else if (v2 === undefined || v2 === null) {
return 1;
}
var v1parts = v1.split('.');
var v2parts = v2.split('.');
var isValidPart = function (x) {
return /^\d+$/.test(x);
};
if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
return NaN;
}
v1parts = _.map(v1parts, Number);
v2parts = _.map(v2parts, Number);
for (var i = 0; i < v1parts.length; ++i) {
if (v2parts.length === i) {
return 1;
}
if (v1parts[i] > v2parts[i]) {
return 1;
}
else if (v1parts[i] < v2parts[i]) {
return -1;
}
}
if (v1parts.length !== v2parts.length) {
return -1;
}
return 0;
}
};
/***/ },
/* 8 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
//noinspection JSUnusedGlobalSymbols
var validatorFactories = {
orderedContent: function (spec) {
return makeContentModelValidator(spec);
},
unorderedContent: function (spec) {
return makeContentModelValidator(spec);
},
string: function (spec) {
if (spec.pattern) {
if (spec.matchGroup) {
return regExValidator(spec.name, extractRegexPart(spec.pattern, spec.matchGroup), spec.caseSensitive);
}
else {
return regExValidator(spec.name, spec.pattern, spec.caseSensitive);
}
}
return function () {
};
},
enumType: function (spec) {
return function (value, errors) {
if (!_.some(spec.enumValues, function (enumVal) {
return new RegExp('^' + enumVal + '$', 'i').test(value);
})) {
errors.add('Value "' + value + '" should have been one of [' + spec.enumValues.join(',') + ']');
}
};
},
ability: function (spec) {
return regExValidator(spec.name, '\\d+');
},
heading: function (spec) {
return function () {
};
},
number: function (spec) {
return function (value, errors) {
if (typeof value !== 'number') {
errors.add('Value "' + value + '" should have been a number');
}
};
}
};
function extractRegexPart(regexp, matchIndex) {
var braceCount = 0;
var startIndex = _.findIndex(regexp, function (character, index) {
if (character === '(' &&
(index < 2 || regexp[index - 1] !== '\\') &&
regexp[index + 1] !== '?') {
return ++braceCount === matchIndex;
}
});
if (startIndex === -1) {
throw new Error('Can\'t find matchgroup ' + matchIndex + ' in regular expression ' + regexp);
}
//Lose the bracket
startIndex++;
var openCount = 1;
var endIndex = _.findIndex(regexp.slice(startIndex), function (character, index, regexp) {
if (character === '(' && regexp[index - 1] !== '\\') {
openCount++;
}
if (character === ')' && regexp[index - 1] !== '\\') {
return --openCount === 0;
}
});
if (endIndex === -1) {
throw new Error('matchgroup ' + matchIndex + ' seems not to have closing brace in regular expression ' + regexp);
}
return regexp.slice(startIndex, startIndex + endIndex);
}
function regExValidator(fieldName, regexp, caseSensitive) {
var re = new RegExp('^' + regexp + '$', caseSensitive ? undefined : 'i');
return function (value, errors) {
if (!re.test(value)) {
errors.add('Value "' + value + '" doesn\'t match pattern /' + regexp + '/');
}
};
}
function makeValidator(spec) {
var validator = validatorFactories[spec.type](spec);
validator.max = _.isUndefined(spec.maxOccurs) ? 1 : spec.maxOccurs;
validator.min = _.isUndefined(spec.minOccurs) ? 1 : spec.minOccurs;
validator.fieldName = spec.name;
return validator;
}
function makeContentModelValidator(spec) {
var parts = _.chain(spec.contentModel)
.reject({ type: 'heading' })
.partition({ flatten: true })
.value();
var flattened = _.map(parts[0], makeValidator);
var subValidators = _.reduce(parts[1], function (subValidators, field) {
subValidators[field.name] = makeValidator(field);
return subValidators;
}, {});
return function (object, errors, isFlatten) {
var completed = _.reduce(object, function (completed, fieldValue, fieldName) {
var validator = subValidators[fieldName];
if (validator) {
completed.push(fieldName);
errors.pushPath(fieldName);
if (_.isArray(fieldValue)) {
if (fieldValue.length > validator.max) {
errors.add('Number of entries [' + fieldValue.length + '] exceeds maximum allowed: ' + validator.max);
}
else if (fieldValue.length < validator.min) {
errors.add('Number of entries [' + fieldValue.length + '] is less than minimum allowed: ' + validator.min);
}
else {
_.each(fieldValue, function (arrayItem, index) {
errors.pushIndex(arrayItem.name ? arrayItem.name : index);
validator(arrayItem, errors);
errors.popIndex();
});
}
}
else {
validator(fieldValue, errors);
}
errors.popPath();
}
return completed;
}, []);
var toValidate = _.omit(object, completed);
_.chain(flattened)
.map(function (validator) {
var subCompleted = validator(toValidate, errors, true);
if (subCompleted.length === 0) {
return validator;
}
else {
completed = completed.concat(subCompleted);
}
toValidate = _.omit(toValidate, completed);
})
.compact()
.each(function (validator) {
if (validator.min > 0) {
errors.pushPath(validator.fieldName);
errors.add('Section is missing');
errors.popPath();
}
});
//If we're a flattened validator (our content is injected directly into the parent content model)
//Then we should only report missing fields if there was some match in our content model - otherwise
//the parent content model will check the cardinality of this model as a whole
if (!isFlatten || !_.isEmpty(completed)) {
_.chain(subValidators)
.omit(completed)
.each(function (validator) {
if (validator.min > 0) {
errors.pushPath(validator.fieldName);
errors.add('Field is missing');
errors.popPath();
}
});
}
//Flattened content models shouldn't check for unrecognised fields since they're only parsing
//part of the current content model.
if (!isFlatten) {
_.chain(object)
.omit(completed)
.each(function (value, key) {
errors.pushPath(key);
errors.add('Unrecognised field');
errors.popPath();
});
}
return completed;
};
}
function Errors() {
var errors = [];
var currentPath = [];
this.pushPath = function (path) {
currentPath.push(path);
};
this.popPath = function () {
currentPath.pop();
};
this.pushIndex = function (index) {
currentPath[currentPath.length - 1] = currentPath[currentPath.length - 1] + '[' + index + ']';
};
this.popIndex = function (index) {
currentPath[currentPath.length - 1] = currentPath[currentPath.length - 1].replace(/\[[^\]]+\]/, '');
};
this.add = function (msg) {
errors.push({ msg: msg, path: _.clone(currentPath) });
};
this.getErrors = function () {
return _.chain(errors)
.groupBy(function (error) {
return error.path[0];
})
.mapObject(function (errorList) {
return _.map(errorList, function (error) {
return error.path.slice(1).join('.') + ': ' + error.msg;
});
})
.value();
};
}
module.exports = JSONValidator;
function JSONValidator(spec) {
var versionProp = {
type: 'string',
name: 'version',
pattern: '^' + spec.formatVersion.replace('.', '\\.') + '$'
};
var contentValidator = makeValidator({ type: 'unorderedContent', contentModel: [spec, versionProp] });
this.validate = function (object) {
var errors = new Errors();
contentValidator(object, errors);
return errors.getErrors();
};
this.getVersionNumber = function () {
return spec.formatVersion;
};
}
/***/ },
/* 9 */
/***/ function(module, exports) {
'use strict';
function Reporter(roll20, scriptName) {
this.report = function (heading, text) {
//Horrible bug with this at the moment - seems to generate spurious chat
//messages when noarchive:true is set
//sendChat('ShapedScripts', '' + msg, null, {noarchive:true});
roll20.sendChat('',
'/w gm ' +
'
' +
scriptName + ' ' + heading +
'
' +
text +
'
');
};
this.reportError = function (text) {
roll20.sendChat('',
'/w gm ' +
'
' +
scriptName +
'
' +
text +
'
');
};
}
module.exports = Reporter;
/***/ },
/* 10 */
/***/ function(module, exports, __webpack_require__) {
/* globals unescape */
'use strict';
var _ = __webpack_require__(2);
var srdConverter = __webpack_require__(11);
var parseModule = __webpack_require__(3);
var cp = __webpack_require__(12);
var utils = __webpack_require__(7);
var mpp = __webpack_require__(13);
var AdvantageTracker = __webpack_require__(14);
var ConfigUI = __webpack_require__(15);
var schemaVersion = 1.0,
configDefaults = {
logLevel: 'INFO',
tokenSettings: {
number: false,
bar1: {
attribute: 'HP',
max: true,
link: false,
showPlayers: false
},
bar2: {
attribute: 'speed',
max: false,
link: true,
showPlayers: false
},
bar3: {
attribute: '',
max: false,
link: false,
showPlayers: false
},
aura1: {
radius: '',
color: '#FFFF99',
square: false
},
aura2: {
radius: '',
color: '#59e594',
square: false
},
light: {
radius: '',
dimRadius: '',
otherPlayers: false,
hasSight: false,
angle: 360,
losAngle: 360,
multiplier: 1
},
showName: true,
showNameToPlayers: false,
showAura1ToPlayers: true,
showAura2ToPlayers: true
},
newCharSettings: {
sheetOutput: '@{output_to_all}',
deathSaveOutput: '@{output_to_all}',
initiativeOutput: '@{output_to_all}',
showNameOnRollTemplate: '@{show_character_name_yes}',
rollOptions: '@{normal}',
initiativeRoll: '@{normal_initiative}',
initiativeToTracker: '@{initiative_to_tracker_yes}',
breakInitiativeTies: '@{initiative_tie_breaker_var}',
showTargetAC: '@{attacks_vs_target_ac_no}',
showTargetName: '@{attacks_vs_target_name_no}',
autoAmmo: '@{ammo_auto_use_var}'
},
advTrackerSettings: {
showMarkers: false
},
rollHPOnDrop: true,
autoHD: true,
genderPronouns: [
{
matchPattern: '^f$|female|girl|woman|feminine',
nominative: 'she',
accusative: 'her',
possessive: 'her',
reflexive: 'herself'
},
{
matchPattern: '^m$|male|boy|man|masculine',
nominative: 'he',
accusative: 'him',
possessive: 'his',
reflexive: 'himself'
},
{
matchPattern: '^n$|neuter|none|construct|thing|object',
nominative: 'it',
accusative: 'it',
possessive: 'its',
reflexive: 'itself'
}
],
defaultGenderIndex: 2
};
var configToAttributeLookup = {
sheetOutput: 'output_option',
deathSaveOutput: 'death_save_output_option',
initiativeOutput: 'initiative_output_option',
showNameOnRollTemplate: 'show_character_name',
rollOptions: 'roll_setting',
initiativeRoll: 'initiative_roll',
initiativeToTracker: 'initiative_to_tracker',
breakInitiativeTies: 'initiative_tie_breaker',
showTargetAC: 'attacks_vs_target_ac',
showTargetName: 'attacks_vs_target_name',
autoAmmo: 'ammo_auto_use'
};
var booleanValidator = function (value) {
var converted = value === 'true' || (value === 'false' ? false : value);
return {
valid: typeof value === 'boolean' || value === 'true' || value === 'false',
converted: converted
};
},
stringValidator = function (value) {
return {
valid: true,
converted: value
};
},
getOptionList = function (options) {
return function (value) {
if (value === undefined) {
return options;
}
return {
converted: options[value],
valid: options[value] !== undefined
};
};
},
integerValidator = function (value) {
var parsed = parseInt(value);
return {
converted: parsed,
valid: !isNaN(parsed)
};
},
colorValidator = function (value) {
return {
converted: value,
valid: /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(value)
};
},
sheetOutputValidator = getOptionList({
public: '@{output_to_all}',
whisper: '@{output_to_gm}'
}),
barValidator = {
attribute: stringValidator,
max: booleanValidator,
link: booleanValidator,
showPlayers: booleanValidator
},
auraValidator = {
radius: stringValidator,
color: colorValidator,
square: booleanValidator
},
lightValidator = {
radius: stringValidator,
dimRadius: stringValidator,
otherPlayers: booleanValidator,
hasSight: booleanValidator,
angle: integerValidator,
losAngle: integerValidator,
multiplier: integerValidator
},
regExpValidator = function (value) {
try {
new RegExp(value, 'i').test('');
return {
converted: value,
valid: true
};
}
catch (e) {
}
return {
converted: null,
valid: false
};
};
/**
* @typedef {Object} ChatMessage
* @property {string} content
* @property {string} type
* @property {SelectedItem[]} selected
* @property {string} rolltemplate
*/
/**
*
* @typedef {Object} SelectedItem
* @property {string} _id
* @property (string _type
*/
module.exports = ShapedScripts;
function ShapedScripts(logger, myState, roll20, parser, entityLookup, reporter) {
var sanitise = logger.wrapFunction('sanitise', __webpack_require__(16), '');
var addedTokenIds = [];
var report = reporter.report.bind(reporter);
var reportError = reporter.reportError.bind(reporter);
var self = this;
var chatWatchers = [];
var at = new AdvantageTracker(logger, myState);
/**
*
* @param {ChatMessage} msg
*/
this.handleInput = function (msg) {
logger.debug(msg);
if (msg.type !== 'api') {
this.triggerChatWatchers(msg);
return;
}
this.getCommandProcessor().processCommand(msg);
};
var configOptionsSpec = {
logLevel: function (value) {
var converted = value.toUpperCase();
return { valid: _.has(logger, converted), converted: converted };
},
tokenSettings: {
number: booleanValidator,
bar1: barValidator,
bar2: barValidator,
bar3: barValidator,
aura1: auraValidator,
aura2: auraValidator,
light: lightValidator,
showName: booleanValidator,
showNameToPlayers: booleanValidator,
showAura1ToPlayers: booleanValidator,
showAura2ToPlayers: booleanValidator
},
newCharSettings: {
sheetOutput: sheetOutputValidator,
deathSaveOutput: sheetOutputValidator,
initiativeOutput: sheetOutputValidator,
showNameOnRollTemplate: getOptionList({
true: '@{show_character_name_yes}',
false: '@{show_character_name_no}'
}),
rollOptions: getOptionList({
normal: '@{roll_1}',
advantage: '@{roll_advantage}',
disadvantage: '@{roll_disadvantage}',
two: '@{roll_2}'
}),
initiativeRoll: getOptionList({
normal: '@{normal_initiative}',
advantage: '@{advantage_on_initiative}',
disadvantage: '@{disadvantage_on_initiative}'
}),
initiativeToTracker: getOptionList({
true: '@{initiative_to_tracker_yes}',
false: '@{initiative_to_tracker_no}'
}),
breakInitiativeTies: getOptionList({
true: '@{initiative_tie_breaker_var}',
false: ''
}),
showTargetAC: getOptionList({
true: '@{attacks_vs_target_ac_yes}',
false: '@{attacks_vs_target_ac_no}'
}),
showTargetName: getOptionList({
true: '@{attacks_vs_target_name_yes}',
false: '@{attacks_vs_target_name_no}'
}),
autoAmmo: getOptionList({
true: '@{ammo_auto_use_var}',
false: ''
})
},
advTrackerSettings: {
showMarkers: booleanValidator,
},
rollHPOnDrop: booleanValidator,
autoHD: booleanValidator,
genderPronouns: [
{
matchPattern: regExpValidator,
nominative: stringValidator,
accusative: stringValidator,
possessive: stringValidator,
reflexive: stringValidator
}
],
defaultGenderIndex: integerValidator
};
/////////////////////////////////////////
// Command handlers
/////////////////////////////////////////
this.configure = function (options) {
utils.deepExtend(myState.config, options);
var cui = new ConfigUI();
logger.debug('options: ' + options);
var menu;
if (options.advTrackerSettings || options.atMenu) {
menu = cui.getConfigOptionGroupAdvTracker(myState.config, configOptionsSpec);
}
else if (options.tokenSettings || options.tsMenu) {
menu = cui.getConfigOptionGroupTokens(myState.config, configOptionsSpec);
}
else if (options.newCharSettings || options.ncMenu) {
menu = cui.getConfigOptionGroupNewCharSettings(myState.config, configOptionsSpec);
}
else {
menu = cui.getConfigOptionsMenu();
}
report('Configuration', menu);
};
this.applyTokenDefaults = function (options) {
var self = this;
_.each(options.selected.graphic, function (token) {
var represents = token.get('represents');
var character = roll20.getObj('character', represents);
if (character) {
self.getTokenConfigurer(token)(character);
}
});
};
this.importStatblock = function (options) {
logger.info('Importing statblocks for tokens $$$', options.selected.graphic);
var self = this;
_.each(options.selected.graphic, function (token) {
var text = token.get('gmnotes');
if (text) {
text = sanitise(unescape(text), logger);
var monsters = parser.parse(text).monsters;
mpp(monsters, entityLookup);
self.importMonsters(monsters, options, token, [
function (character) {
character.set('gmnotes', text.replace(/\n/g, '
'));
}
]);
}
});
};
this.importMonstersFromJson = function (options) {
if (options.all) {
options.monsters = entityLookup.getAll('monsters');
delete options.all;
}
if (_.isEmpty(options.monsters)) {
this.showEntityPicker('monster', 'monsters');
}
else {
this.importMonsters(options.monsters.slice(0, 20), options, options.selected.graphic, []);
options.monsters = options.monsters.slice(20);
var self = this;
if (!_.isEmpty(options.monsters)) {
setTimeout(function () {
self.importMonstersFromJson(options);
}, 200);
}
}
};
this.importMonsters = function (monsters, options, token, characterProcessors) {
var characterRetrievalStrategies = [];
if (token) {
characterProcessors.push(this.getAvatarCopier(token).bind(this));
if (_.size(monsters) === 1) {
characterProcessors.push(this.getTokenConfigurer(token).bind(this));
if (options.replace || options.overwrite) {
characterRetrievalStrategies.push(this.getTokenRetrievalStrategy(token).bind(this));
}
}
}
if (options.replace) {
characterRetrievalStrategies.push(this.nameRetrievalStrategy);
}
characterRetrievalStrategies.push(this.creationRetrievalStrategy.bind(this));
characterProcessors.push(this.monsterDataPopulator.bind(this));
var errors = [];
var importedList = _.chain(monsters)
.map(function (monsterData) {
var character = _.reduce(characterRetrievalStrategies, function (result, strategy) {
return result || strategy(monsterData.name, errors);
}, null);
if (!character) {
logger.error('Failed to find or create character for monster $$$', monsterData.name);
return null;
}
var oldAttrs = roll20.findObjs({ type: 'attribute', characterid: character.id });
_.invoke(oldAttrs, 'remove');
character.set('name', monsterData.name);
_.each(characterProcessors, function (proc) {
proc(character, monsterData);
});
return character && character.get('name');
})
.compact()
.value();
if (!_.isEmpty(importedList)) {
var monsterList = importedList.join('');
report('Import Success', 'Added the following monsters: ');
}
if (!_.isEmpty(errors)) {
var errorList = errors.join('');
reportError('The following errors occurred on import: ');
}
};
this.importSpellsFromJson = function (options) {
if (_.isEmpty(options.spells)) {
this.showEntityPicker('spell', 'spells');
}
else {
this.addSpellsToCharacter(options.selected.character, options.spells);
}
};
this.showEntityPicker = function (entityName, entityNamePlural) {
var list = entityLookup.getKeys(entityNamePlural, true);
if (!_.isEmpty(list)) {
// title case the names for better display
list.forEach(function (part, index) {
list[index] = utils.toTitleCase(part);
});
// create a clickable button with a roll query to select an entity from the loaded json
report(utils.toTitleCase(entityName) + ' Importer', 'Click to select a ' + entityName + '');
}
else {
reportError('Could not find any ' + entityNamePlural + '.
Please ensure you have a properly formatted ' +
entityNamePlural + ' json file.');
}
};
this.addSpellsToCharacter = function (character, spells, noreport) {
var gender = roll20.getAttrByName(character.id, 'gender');
var defaultIndex = Math.min(myState.config.defaultGenderIndex, myState.config.genderPronouns.length);
var defaultPronounInfo = myState.config.genderPronouns[defaultIndex];
var pronounInfo = _.clone(_.find(myState.config.genderPronouns, function (pronounDetails) {
return new RegExp(pronounDetails.matchPattern, 'i').test(gender);
}) || defaultPronounInfo);
_.defaults(pronounInfo, defaultPronounInfo);
var importData = {
spells: srdConverter.convertSpells(spells, pronounInfo)
};
this.getImportDataWrapper(character).mergeImportData(importData);
if (!noreport) {
report('Import Success', 'Added the following spells: - ' + _.map(importData.spells, function (spell) {
return spell.name;
}).join('
- ') + '
');
}
};
this.monsterDataPopulator = function (character, monsterData) {
_.each(myState.config.newCharSettings, function (value, key) {
var attribute = roll20.getOrCreateAttr(character.id, configToAttributeLookup[key]);
attribute.set('current', _.isBoolean(value) ? (value ? 1 : 0) : value);
});
var converted = srdConverter.convertMonster(monsterData);
logger.debug('Converted monster data: $$$', converted);
var expandedSpells = converted.spells;
delete converted.spells;
this.getImportDataWrapper(character).setNewImportData({ npc: converted });
if (expandedSpells) {
this.addSpellsToCharacter(character, expandedSpells, true);
}
return character;
};
this.getTokenRetrievalStrategy = function (token) {
return function (name, errors) {
if (token) {
var character = roll20.getObj('character', token.get('represents'));
if (character && roll20.getAttrByName(character.id, 'locked')) {
errors.push('Character with name ' + character.get('name') + ' and id ' + character.id +
' was locked and cannot be overwritten');
return null;
}
return character;
}
};
};
this.nameRetrievalStrategy = function (name, errors) {
var chars = roll20.findObjs({ type: 'character', name: name });
if (chars.length > 1) {
errors.push('More than one existing character found with name "' + name + '". Can\'t replace');
}
else {
if (chars[0] && roll20.getAttrByName(chars[0].id, 'locked')) {
errors.push('Character with name ' + chars[0].get('name') + ' and id ' + chars[0].id +
' was locked and cannot be overwritten');
return null;
}
return chars[0];
}
};
this.creationRetrievalStrategy = function (name, errors) {
if (!_.isEmpty(roll20.findObjs({ type: 'character', name: name }))) {
errors.push('Can\'t create new character with name "' + name +
'" because one already exists with that name. Perhaps you want --replace?');
}
else {
return roll20.createObj('character', { name: name });
}
};
this.getAvatarCopier = function (token) {
return function (character) {
character.set('avatar', token.get('imgsrc'));
};
};
this.getTokenConfigurer = function (token) {
return function (character) {
token.set('represents', character.id);
token.set('name', character.get('name'));
var settings = myState.config.tokenSettings;
if (settings.number && token.get('name').indexOf('%%NUMBERED%%') === -1) {
token.set('name', token.get('name') + ' %%NUMBERED%%');
}
_.chain(settings)
.pick(['bar1', 'bar2', 'bar3'])
.each(function (bar, barName) {
if (!_.isEmpty(bar.attribute)) {
var attribute = roll20.getOrCreateAttr(character.id, bar.attribute);
if (attribute) {
token.set(barName + '_value', attribute.get('current'));
if (bar.max) {
token.set(barName + '_max', attribute.get('max'));
}
token.set('showplayers_' + barName, bar.showPlayers);
if (bar.link) {
token.set(barName + '_link', attribute.id);
}
}
}
});
// auras
_.chain(settings)
.pick(['aura1', 'aura2'])
.each(function (aura, auraName) {
token.set(auraName + '_radius', aura.radius);
token.set(auraName + '_color', aura.color);
token.set(auraName + '_square', aura.square);
});
logger.debug('Settings for tokens: $$$', settings);
token.set('showname', settings.showName);
token.set('showplayers_name', settings.showNameToPlayers);
token.set('showplayers_aura1', settings.showAura1ToPlayers);
token.set('showplayers_aura2', settings.showAura2ToPlayers);
token.set('light_radius', settings.light.radius);
token.set('light_dimradius', settings.light.dimRadius);
token.set('light_otherplayers', settings.light.otherPlayers);
token.set('light_hassight', settings.light.hasSight);
token.set('light_angle', settings.light.angle);
token.set('light_losangle', settings.light.losAngle);
token.set('light_multiplier', settings.light.multiplier);
};
};
this.getImportDataWrapper = function (character) {
return {
setNewImportData: function (importData) {
if (_.isEmpty(importData)) {
return;
}
roll20.setAttrByName(character.id, 'import_data', JSON.stringify(importData));
roll20.setAttrByName(character.id, 'import_data_present', 'on');
},
mergeImportData: function (importData) {
if (_.isEmpty(importData)) {
return;
}
var attr = roll20.getOrCreateAttr(character.id, 'import_data');
var dataPresentAttr = roll20.getOrCreateAttr(character.id, 'import_data_present');
var current = {};
try {
if (!_.isEmpty(attr.get('current').trim())) {
current = JSON.parse(attr.get('current'));
}
}
catch (e) {
logger.warn('Existing import_data attribute value was not valid JSON: [$$$]', attr.get('current'));
}
_.each(importData, function (value, key) {
var currentVal = current[key];
if (currentVal) {
if (!_.isArray(currentVal)) {
current[key] = [currentVal];
}
current[key] = current[key].concat(value);
}
else {
current[key] = value;
}
});
logger.debug('Setting import data to $$$', current);
attr.set('current', JSON.stringify(current));
dataPresentAttr.set('current', 'on');
return current;
},
logWrap: 'importDataWrapper'
};
};
this.applyAdvantageTracker = function (options) {
var type = 'normal';
if (options.advantage) {
type = 'advantage';
}
else if (options.disadvantage) {
type = 'disadvantage';
}
at.setMarkers(type, at.buildResources(at.getSelectedCharacters(options.selected.character)));
//roll20.log('in AT listener');
//roll20.log(at.buildResources(at.getSelectedCharacters(options.selected.character)));
};
/////////////////////////////////////////////////
// Event Handlers
/////////////////////////////////////////////////
this.handleAddToken = function (token) {
var represents = token.get('represents');
if (_.isEmpty(represents)) {
return;
}
var character = roll20.getObj('character', represents);
if (!character) {
return;
}
addedTokenIds.push(token.id);
var wrappedChangeToken = this.wrapHandler(this.handleChangeToken);
//URGH. Thanks Roll20.
setTimeout((function (id) {
return function () {
var token = roll20.getObj('graphic', id);
if (token) {
wrappedChangeToken(token);
}
};
}(token.id)), 100);
};
this.handleChangeToken = function (token) {
at.updateToken(token);
if (_.contains(addedTokenIds, token.id)) {
addedTokenIds = _.without(addedTokenIds, token.id);
this.rollHPForToken(token);
}
};
this.getHPBar = function () {
return _.chain(myState.config.tokenSettings)
.pick('bar1', 'bar2', 'bar3')
.findKey({ attribute: 'HP' })
.value();
};
this.rollHPForToken = function (token) {
var hpBar = this.getHPBar();
logger.debug('HP bar is $$$', hpBar);
if (!hpBar || !myState.config.rollHPOnDrop) {
return;
}
var represents = token.get('represents');
if (!represents) {
return;
}
var character = roll20.getObj('character', represents);
if (!character) {
return;
}
var hpBarLink = token.get(hpBar + '_link');
if (hpBarLink) {
return;
}
// Guard against characters that aren't properly configured - i.e. ones used for templates and system
// things rather than actual characters
if (_.isEmpty(roll20.getAttrByName(character.id, 'hp_formula'))) {
logger.debug('Ignoring character $$$ for rolling HP - has no hp_formula attribute', character.get('name'));
return;
}
var self = this;
roll20.sendChat('', '%{' + character.get('name') + '|npc_hp}', function (results) {
if (results && results.length === 1) {
var message = self.processInlinerolls(results[0]);
if (!results[0].inlinerolls || !results[0].inlinerolls[0]) {
logger.warn('HP roll didn\'t have the expected structure. This is what we got back: $$$', results[0]);
}
else {
var total = results[0].inlinerolls[0].results.total;
roll20.sendChat('HP Roller', '/w GM &{template:5e-shaped} ' + message);
token.set(hpBar + '_value', total);
token.set(hpBar + '_max', total);
}
}
});
};
this.registerChatWatcher = function (handler, triggerFields) {
var matchers = [];
if (triggerFields && !_.isEmpty(triggerFields)) {
matchers.push(function (msg, options) {
return _.intersection(triggerFields, _.keys(options)).length === triggerFields.length;
});
}
chatWatchers.push({ matchers: matchers, handler: handler.bind(this) });
};
this.triggerChatWatchers = function (msg) {
var options = this.getRollTemplateOptions(msg);
_.each(chatWatchers, function (watcher) {
if (_.every(watcher.matchers, function (matcher) {
return matcher(msg, options);
})) {
watcher.handler(options, msg);
}
});
};
/**
*
* @param options
* @param {ChatMessage} msg
*/
this.handleAmmo = function (options, msg) {
if (!roll20.getAttrByName(options.character.id, 'ammo_auto_use')) {
return;
}
var ammoAttr = _.chain(roll20.findObjs({ type: 'attribute', characterid: options.character.id }))
.filter(function (attribute) {
return attribute.get('name').indexOf('repeating_ammo') === 0;
})
.groupBy(function (attribute) {
return attribute.get('name').replace(/(repeating_ammo_[^_]+).*/, '$1');
})
.find(function (attributes) {
return _.find(attributes, function (attribute) {
return attribute.get('name').match(/.*name$/) && attribute.get('current') === options.ammoName;
});
})
.find(function (attribute) {
return attribute.get('name').match(/.*qty$/);
})
.value();
if (!ammoAttr) {
logger.error('No ammo attribute found corresponding to name $$$', options.ammoName);
return;
}
var ammoUsed = 1;
if (options.ammo) {
var rollRef = options.ammo.match(/\$\[\[(\d+)\]\]/);
if (rollRef) {
var rollExpr = msg.inlinerolls[rollRef[1]].expression;
var match = rollExpr.match(/\d+-(\d+)/);
if (match) {
ammoUsed = match[1];
}
}
}
var val = parseInt(ammoAttr.get('current'), 10) || 0;
ammoAttr.set('current', Math.max(0, val - ammoUsed));
};
this.handleHD = function (options, msg) {
var match = options.title.match(/(\d+)d(\d+) Hit Dice/);
if (match && myState.config.autoHD) {
var hdCount = match[1];
var hdSize = match[2];
var hdAttr = roll20.getAttrObjectByName(options.character.id, 'hd_d' + hdSize);
var hpAttr = roll20.getAttrObjectByName(options.character.id, 'HP');
var newHp = Math.min(parseInt(hpAttr.get('current')) + this.getRollValue(msg, options.roll1), hpAttr.get('max'));
if (hdAttr) {
if (hdCount <= hdAttr.get('current')) {
hdAttr.set('current', hdAttr.get('current') - hdCount);
hpAttr.set('current', newHp);
}
else {
report('HD Police', options.characterName + ' can\'t use ' + hdCount + 'd' + hdSize +
' hit dice because they only have ' + hdAttr.get('current') + ' left');
}
}
}
};
this.handleDeathSave = function (options, msg) {
//TODO: Do we want to output text on death/recovery?
var increment = function (val) {
return ++val;
};
//TODO: Advantage?
if (roll20.getAttrByName(options.character.id, 'roll_setting') !== '@{roll_2}') {
var result = this.getRollValue(msg, options.roll1);
var attributeToIncrement = result >= 10 ? 'death_saving_throw_successes' : 'death_saving_throw_failures';
roll20.processAttrValue(options.character.id, attributeToIncrement, increment);
}
};
this.handleFX = function (options, msg) {
var parts = options.fx.split(' ');
if (parts.length < 2 || _.some(parts.slice(0, 2), _.isEmpty)) {
logger.warn('FX roll template variable is not formated correctly: [$$$]', options.fx);
return;
}
var fxType = parts[0],
pointsOfOrigin = parts[1],
targetTokenId,
//sourceTokenId,
sourceCoords = {},
targetCoords = {},
fxCoords = [],
pageId;
//noinspection FallThroughInSwitchStatementJS
switch (pointsOfOrigin) {
case 'sourceToTarget':
case 'source':
targetTokenId = parts[2]; //jshint ignore: line
fxCoords.push(sourceCoords, targetCoords);
break;
case 'targetToSource':
case 'target':
targetTokenId = parts[2];
fxCoords.push(targetCoords, sourceCoords);
}
if (targetTokenId) {
var targetToken = roll20.getObj('graphic', targetTokenId);
pageId = targetToken.get('_pageid');
targetCoords.x = targetToken.get('left');
targetCoords.y = targetToken.get('top');
}
else {
pageId = roll20.getCurrentPage(msg.playerid).id;
}
var casterTokens = roll20.findObjs({ type: 'graphic', pageid: pageId, represents: options.character.id });
if (casterTokens.length) {
//If there are multiple tokens for the character on this page, then try and find one of them that is selected
//This doesn't work without a selected token, and the only way we can get this is to use @{selected} which is a pain
//for people who want to launch without a token selected
// if(casterTokens.length > 1) {
// var selected = _.findWhere(casterTokens, {id: sourceTokenId});
// if (selected) {
// casterTokens = [selected];
// }
// }
sourceCoords.x = casterTokens[0].get('left');
sourceCoords.y = casterTokens[0].get('top');
}
if (!fxCoords[0]) {
logger.warn('Couldn\'t find required point for fx for character $$$, casterTokens: $$$, fxSpec: $$$ ', options.character.id, casterTokens, options.fx);
return;
}
else if (!fxCoords[1]) {
fxCoords = fxCoords.slice(0, 1);
}
roll20.spawnFx(fxCoords, fxType, pageId);
};
this.getRollValue = function (msg, rollOutputExpr) {
var rollIndex = rollOutputExpr.match(/\$\[\[(\d+)\]\]/)[1];
return msg.inlinerolls[rollIndex].results.total;
};
/**
*
* @returns {*}
*/
this.getRollTemplateOptions = function (msg) {
if (msg.rolltemplate === '5e-shaped') {
var regex = /\{\{(.*?)\}\}/g;
var match;
var options = {};
while (!!(match = regex.exec(msg.content))) {
if (match[1]) {
var splitAttr = match[1].split('=');
var propertyName = splitAttr[0].replace(/_([a-z])/g, function (match, letter) {
return letter.toUpperCase();
});
options[propertyName] = splitAttr.length === 2 ? splitAttr[1] : '';
}
}
if (options.characterName) {
options.character = roll20.findObjs({
_type: 'character',
name: options.characterName
})[0];
}
return options;
}
return {};
};
this.processInlinerolls = function (msg) {
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.toString(), current);
}, msg.content)
.value();
}
else {
return msg.content;
}
};
this.addAbility = function (options) {
if (_.isEmpty(options.abilities)) {
//TODO report some sort of error?
return;
}
var messages = _.map(options.selected.character, function (character) {
var operationMessages = _.chain(options.abilities)
.sortBy('sortKey')
.map(function (maker) {
return maker.run(character, options);
})
.value();
if (_.isEmpty(operationMessages)) {
return '' + character.get('name') + ': Nothing to do';
}
var message;
message = 'Configured the following abilities for character ' + character.get('name') + ':- ';
message += operationMessages.join('
- ');
message += '
';
return message;
});
report('Ability Creation', '' + messages.join('') + '
');
};
var getAbilityMaker = function (character) {
return function (abilitySpec) {
var ability = roll20.getOrCreateObj('ability', { characterid: character.id, name: abilitySpec.name });
ability.set({ action: abilitySpec.action, istokenaction: true }); //TODO configure this
return abilitySpec.name;
};
};
var abilityDeleter = {
run: function (character) {
var abilities = roll20.findObjs({ type: 'ability', characterid: character.id });
var deleted = _.map(abilities, function (obj) {
var name = obj.get('name');
obj.remove();
return name;
});
return 'Deleted: ' + (_.isEmpty(deleted) ? 'None' : deleted.join(', '));
},
sortKey: ''
};
var RepeatingAbilityMaker = function (repeatingSection, abilityName, label, canMark) {
this.run = function (character, options) {
options[`cache${repeatingSection}`] = options[`cache${repeatingSection}`] ||
roll20.getRepeatingSectionItemIdsByName(character.id, repeatingSection);
var configured = _.chain(options[`cache${repeatingSection}`])
.map(function (repeatingId, repeatingName) {
var repeatingAction = '%{' + character.get('name') + '|repeating_' + repeatingSection + '_' + repeatingId +
'_' + abilityName + '}';
if (canMark && options.mark) {
repeatingAction += '\n!mark @{target|token_id}';
}
return { name: utils.toTitleCase(repeatingName), action: repeatingAction };
})
.map(getAbilityMaker(character))
.value();
return label + (_.isEmpty(configured) ? ': Not present for character' : ': ' + configured.join(', '));
};
this.sortKey = 'originalOrder';
};
var RollAbilityMaker = function (abilityName, newName) {
this.run = function (character) {
return getAbilityMaker(character)({
name: newName,
action: '%{' + character.get('name') + '|' + abilityName + '}'
});
};
this.sortKey = 'originalOrder';
};
var staticAbilityOptions = {
DELETE: abilityDeleter,
initiative: new RollAbilityMaker('initiative', 'Init'),
abilitychecks: new RollAbilityMaker('ability_checks_macro', 'Ability Checks'),
abilitychecksquery: new RollAbilityMaker('ability_checks_query_macro', 'Ability Checks'),
abilchecks: new RollAbilityMaker('ability_checks_macro', 'AbilChecks'),
abilchecksquery: new RollAbilityMaker('ability_checks_query_macro', 'AbilChecks'),
savingthrows: new RollAbilityMaker('saving_throw_macro', 'Saving Throws'),
savingthrowsquery: new RollAbilityMaker('saving_throw_query_macro', 'Saving Throws'),
saves: new RollAbilityMaker('saving_throw_macro', 'Saves'),
savesquery: new RollAbilityMaker('saving_throw_query_macro', 'Saves'),
attacks: new RepeatingAbilityMaker('attack', 'attack', 'Attacks', true),
statblock: new RollAbilityMaker('statblock', 'Statblck'),
traits: new RepeatingAbilityMaker('trait', 'trait', 'Traits'),
'traits-macro': new RollAbilityMaker('traits_macro', 'Traits'),
actions: new RepeatingAbilityMaker('action', 'action', 'Actions', true),
'actions-macro': new RollAbilityMaker('actions_macro', 'Actions'),
reactions: new RepeatingAbilityMaker('reaction', 'action', 'Reactions'),
'reactions-macro': new RollAbilityMaker('reactions_macro', 'Reactions'),
legendaryactions: new RepeatingAbilityMaker('legendaryaction', 'action', 'Legendary Actions'),
'legendaryactions-macro': new RollAbilityMaker('legendaryactions_macro', 'Legendary Actions'),
legendarya: new RepeatingAbilityMaker('legendaryaction', 'action', 'LegendaryA'),
lairactions: new RollAbilityMaker('lairactions_macro', 'Lair Actions'),
laira: new RollAbilityMaker('lairactions_macro', 'LairA'),
regionaleffects: new RollAbilityMaker('regionaleffects_macro', 'Regional Effects'),
regionale: new RollAbilityMaker('regionaleffects_macro', 'RegionalE')
};
var abilityLookup = function (optionName, existingOptions) {
var maker = staticAbilityOptions[optionName];
//Makes little sense to add named spells to multiple characters at once
if (!maker && existingOptions.selected.character.length === 1) {
existingOptions.spellToRepeatingIdLookup = existingOptions.spellToRepeatingIdLookup ||
roll20.getRepeatingSectionItemIdsByName(existingOptions.selected.character[0].id, 'spell');
var repeatingId = existingOptions.spellToRepeatingIdLookup[optionName.toLowerCase()];
if (repeatingId) {
maker = new RollAbilityMaker('repeating_spell_' + repeatingId + '_spell', utils.toTitleCase(optionName));
}
}
return maker;
};
this.getCommandProcessor = function () {
return cp('shaped')
.addCommand('config', this.configure.bind(this))
.options(configOptionsSpec)
.option('atMenu', booleanValidator)
.option('tsMenu', booleanValidator)
.option('ncMenu', booleanValidator)
.addCommand('import-statblock', self.importStatblock.bind(self))
.option('overwrite', booleanValidator)
.option('replace', booleanValidator)
.withSelection({
graphic: {
min: 1,
max: Infinity
}
})
.addCommand(['import-monster', 'monster'], this.importMonstersFromJson.bind(this))
.option('all', booleanValidator)
.optionLookup('monsters', entityLookup.findEntity.bind(entityLookup, 'monsters'))
.option('overwrite', booleanValidator)
.option('replace', booleanValidator)
.withSelection({
graphic: {
min: 0,
max: 1
}
})
.addCommand(['import-spell', 'spell'], this.importSpellsFromJson.bind(this))
.optionLookup('spells', entityLookup.findEntity.bind(entityLookup, 'spells'))
.withSelection({
character: {
min: 1,
max: 1
}
})
.addCommand('at', this.applyAdvantageTracker.bind(this))
.option('advantage', booleanValidator)
.option('disadvantage', booleanValidator)
.option('normal', booleanValidator)
.withSelection({
character: {
min: 1,
max: Infinity
}
})
.addCommand('abilities', this.addAbility.bind(this))
.withSelection({
character: {
min: 1,
max: Infinity
}
})
.optionLookup('abilities', abilityLookup)
.option('mark', booleanValidator)
.addCommand('token-defaults', this.applyTokenDefaults.bind(this))
.withSelection({
graphic: {
min: 1,
max: Infinity
}
})
.end();
};
this.checkInstall = function () {
logger.info('-=> ShapedScripts v1.0.2 <=-');
if (myState.version !== schemaVersion) {
logger.info(' > Updating Schema to v$$$ from $$$<', schemaVersion, myState && myState.version);
logger.info('Preupgrade state: $$$', myState);
//noinspection FallThroughInSwitchStatementJS
switch (myState && myState.version) {
case 0.1:
case 0.2:
case 0.3:
_.extend(myState.config.genderPronouns, utils.deepClone(configDefaults.genderPronouns)); //jshint ignore: line
case 0.4:
case 0.5:
case 0.6:
case 0.7:
case 0.8:
case 0.9:
_.defaults(myState.config, utils.deepClone(configDefaults));
_.defaults(myState.config.tokenSettings, utils.deepClone(configDefaults.tokenSettings));
_.defaults(myState.config.newCharSettings, utils.deepClone(configDefaults.newCharSettings));
_.defaults(myState.config.advTrackerSettings, utils.deepClone(configDefaults.advTrackerSettings));
myState.version = schemaVersion;
break;
default:
if (!myState.version) {
_.defaults(myState, {
version: schemaVersion,
config: utils.deepClone(configDefaults)
});
logger.info('Making new state object $$$', myState);
}
else {
logger.error('Unknown schema version for state $$$', myState);
reportError('Serious error attempting to upgrade your global state, please see log for details. ' +
'ShapedScripts will not function correctly until this is fixed');
myState = undefined;
}
break;
}
logger.info('Upgraded state: $$$', myState);
}
};
this.wrapHandler = function (handler) {
var self = this;
return function () {
try {
handler.apply(self, arguments);
}
catch (e) {
if (typeof e === 'string' || e instanceof parseModule.ParserError) {
reportError(e);
logger.error('Error: $$$', e.toString());
}
else {
logger.error(e.toString());
logger.error(e.stack);
reportError('An error occurred. Please see the log for more details.');
}
}
finally {
logger.prefixString = '';
}
};
};
this.registerEventHandlers = function () {
roll20.on('chat:message', this.wrapHandler(this.handleInput));
roll20.on('add:token', this.wrapHandler(this.handleAddToken));
roll20.on('change:token', this.wrapHandler(this.handleChangeToken));
roll20.on('change:attribute', this.wrapHandler(function (msg) {
if (msg.get('name') === 'roll_setting') {
at.updateSetting(msg);
}
}));
this.registerChatWatcher(this.handleDeathSave, ['deathSavingThrow', 'character', 'roll1']);
this.registerChatWatcher(this.handleAmmo, ['ammoName', 'character']);
this.registerChatWatcher(this.handleFX, ['fx', 'character']);
this.registerChatWatcher(this.handleHD, ['character', 'title']);
};
logger.wrapModule(this);
}
ShapedScripts.prototype.logWrap = 'ShapedScripts';
/***/ },
/* 11 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
/* jshint camelcase : false */
function getRenameMapper(newName) {
return function (key, value, output) {
output[newName] = value;
};
}
var identityMapper = function (key, value, output) {
output[key] = value;
},
booleanMapper = function (key, value, output) {
if (value) {
output[key] = 'Yes';
}
},
camelCaseFixMapper = function (key, value, output) {
var newKey = key.replace(/[A-Z]/g, function (letter) {
return '_' + letter.toLowerCase();
});
output[newKey] = value;
},
castingStatMapper = function (key, value, output) {
if (value) {
output.add_casting_modifier = 'Yes';
}
},
componentMapper = function (key, value, output) {
output.components = _.chain(value)
.map(function (value, key) {
if (key !== 'materialMaterial') {
return key.toUpperCase().slice(0, 1);
}
else {
output.materials = value;
}
})
.compact()
.value()
.join(' ');
},
saveAttackMappings = {
ability: getRenameMapper('saving_throw_vs_ability'),
type: identityMapper,
damage: identityMapper,
damageBonus: camelCaseFixMapper,
damageType: camelCaseFixMapper,
saveSuccess: getRenameMapper('saving_throw_success'),
saveFailure: getRenameMapper('saving_throw_failure'),
higherLevelDice: camelCaseFixMapper,
higherLevelDie: camelCaseFixMapper,
secondaryDamage: getRenameMapper('second_damage'),
secondaryDamageBonus: getRenameMapper('second_damage_bonus'),
secondaryDamageType: getRenameMapper('second_damage_type'),
higherLevelSecondaryDice: getRenameMapper('second_higher_level_dice'),
higherLevelSecondaryDie: getRenameMapper('second_higher_level_die'),
condition: getRenameMapper('saving_throw_condition'),
castingStat: castingStatMapper
};
function getObjectMapper(mappings) {
return function (key, value, output) {
_.each(value, function (propVal, propName) {
var mapper = mappings[propName];
if (!mapper) {
throw 'Unrecognised property when attempting to convert to srd format: [' + propName + '] ' +
JSON.stringify(output);
}
mapper(propName, propVal, output);
});
};
}
var spellMapper = getObjectMapper({
name: identityMapper,
duration: identityMapper,
level: getRenameMapper('spell_level'),
school: identityMapper,
emote: identityMapper,
range: identityMapper,
castingTime: camelCaseFixMapper,
target: identityMapper,
description: function (key, value, output) {
output.content = value + (output.content ? '\n' + output.content : '');
},
higherLevel: function (key, value, output) {
output.content = (output.content ? output.content + '\n' : '') + value;
},
ritual: booleanMapper,
concentration: booleanMapper,
save: getObjectMapper(saveAttackMappings),
attack: getObjectMapper(saveAttackMappings),
damage: getObjectMapper(saveAttackMappings),
heal: getObjectMapper({
amount: getRenameMapper('heal'),
castingStat: castingStatMapper,
higherLevelDice: camelCaseFixMapper,
higherLevelDie: camelCaseFixMapper,
higherLevelAmount: getRenameMapper('higher_level_heal'),
bonus: getRenameMapper('heal_bonus')
}),
components: componentMapper,
prepared: function (key, value, output) {
if (value) {
output.is_prepared = 'on';
}
},
classes: _.noop,
aoe: _.noop,
source: _.noop,
effects: _.noop,
domains: _.noop,
oaths: _.noop,
circles: _.noop,
patrons: _.noop
});
var monsterMapper = getObjectMapper({
name: getRenameMapper('character_name'),
size: identityMapper,
type: identityMapper,
alignment: identityMapper,
AC: getRenameMapper('ac_srd'),
HP: getRenameMapper('hp_srd'),
speed: getRenameMapper('npc_speed'),
strength: identityMapper,
dexterity: identityMapper,
constitution: identityMapper,
intelligence: identityMapper,
wisdom: identityMapper,
charisma: identityMapper,
skills: getRenameMapper('skills_srd'),
spells: function (key, value, output) {
var splitSpells = _.partition(value, _.isObject);
if (!_.isEmpty(splitSpells[1])) {
output.spells_srd = splitSpells[1].join(', ');
}
if (!_.isEmpty(splitSpells[0])) {
output.spells = splitSpells[0];
_.each(output.spells, function (spell) {
spell.prepared = true;
});
}
},
savingThrows: getRenameMapper('saving_throws_srd'),
damageResistances: getRenameMapper('damage_resistances'),
damageImmunities: getRenameMapper('damage_immunities'),
conditionImmunities: getRenameMapper('condition_immunities'),
damageVulnerabilities: getRenameMapper('damage_vulnerabilities'),
senses: identityMapper,
languages: identityMapper,
challenge: identityMapper,
traits: identityMapper,
actions: identityMapper,
reactions: identityMapper,
regionalEffects: identityMapper,
regionalEffectsFade: identityMapper,
legendaryPoints: identityMapper,
legendaryActions: identityMapper,
lairActions: identityMapper
});
var pronounTokens = {
'{{GENDER_PRONOUN_HE_SHE}}': 'nominative',
'{{GENDER_PRONOUN_HIM_HER}}': 'accusative',
'{{GENDER_PRONOUN_HIS_HER}}': 'possessive',
'{{GENDER_PRONOUN_HIMSELF_HERSELF}}': 'reflexive'
};
module.exports = {
convertMonster: function (npcObject) {
var output = {};
monsterMapper(null, npcObject, output);
var actionTraitTemplate = _.template('**<%=data.name%><% if(data.recharge) { print(" (" + data.recharge + ")") } %>**: <%=data.text%>', { variable: 'data' });
var legendaryTemplate = _.template('**<%=data.name%><% if(data.cost && data.cost > 1){ print(" (Costs " + data.cost + " actions)") }%>**: <%=data.text%>', { variable: 'data' });
var lairRegionalTemplate = function (item) {
return '**' + item;
};
var simpleSectionTemplate = _.template('<%=data.title%>\n<% print(data.items.join("\\n")); %>', { variable: 'data' });
var legendarySectionTemplate = _.template('<%=data.title%>\nThe <%=data.name%> can take <%=data.legendaryPoints%> legendary actions, ' +
'choosing from the options below. It can take only one legendary action at a time and only at the end of another creature\'s turn.' +
' The <%=data.name%> regains spent legendary actions at the start of its turn.\n<% print(data.items.join("\\n")) %>', { variable: 'data' });
var regionalSectionTemplate = _.template('<%=data.title%>\n<% print(data.items.join("\\n")); %>\n**<%=data.regionalEffectsFade%>', { variable: 'data' });
var srdContentSections = [
{ prop: 'traits', itemTemplate: actionTraitTemplate, sectionTemplate: simpleSectionTemplate },
{ prop: 'actions', itemTemplate: actionTraitTemplate, sectionTemplate: simpleSectionTemplate },
{ prop: 'reactions', itemTemplate: actionTraitTemplate, sectionTemplate: simpleSectionTemplate },
{ prop: 'legendaryActions', itemTemplate: legendaryTemplate, sectionTemplate: legendarySectionTemplate },
{ prop: 'lairActions', itemTemplate: lairRegionalTemplate, sectionTemplate: simpleSectionTemplate },
{ prop: 'regionalEffects', itemTemplate: lairRegionalTemplate, sectionTemplate: regionalSectionTemplate }
];
var makeDataObject = function (propertyName, itemList) {
return {
title: propertyName.replace(/([A-Z])/g, ' $1').replace(/^[a-z]/, function (letter) {
return letter.toUpperCase();
}),
name: output.character_name,
legendaryPoints: output.legendaryPoints,
regionalEffectsFade: output.regionalEffectsFade,
items: itemList
};
};
output.is_npc = 1;
output.edit_mode = 'off';
output.content_srd = _.chain(srdContentSections)
.map(function (sectionSpec) {
var items = output[sectionSpec.prop];
delete output[sectionSpec.prop];
return _.map(items, sectionSpec.itemTemplate);
})
.map(function (sectionItems, sectionIndex) {
var sectionSpec = srdContentSections[sectionIndex];
if (!_.isEmpty(sectionItems)) {
return sectionSpec.sectionTemplate(makeDataObject(sectionSpec.prop, sectionItems));
}
return null;
})
.compact()
.value()
.join('\n');
delete output.legendaryPoints;
return output;
},
convertSpells: function (spellObjects, pronounInfo) {
return _.map(spellObjects, function (spellObject) {
var converted = {};
spellMapper(null, spellObject, converted);
if (converted.emote) {
_.each(pronounTokens, function (pronounType, token) {
var replacement = pronounInfo[pronounType];
converted.emote = converted.emote.replace(token, replacement);
});
}
return converted;
});
}
/* jshint camelcase : true */
};
/***/ },
/* 12 */
/***/ function(module, exports, __webpack_require__) {
var _ = __webpack_require__(2);
var roll20 = __webpack_require__(1);
var utils = __webpack_require__(7);
var getParser = function (optionString, validator) {
'use strict';
return function (arg, errors, options) {
var argParts = arg.split(/\s+/);
if (argParts[0].toLowerCase() === optionString.toLowerCase()) {
if (argParts.length <= 2) {
//Allow for bare switches
var value = argParts.length === 2 ? argParts[1] : true;
var result = validator(value);
if (result.valid) {
options[argParts[0]] = result.converted;
}
else {
errors.push('Invalid value [' + value + '] for option [' + argParts[0] + ']');
}
}
return true;
}
return false;
};
};
var getObjectParser = function (specObject) {
'use strict';
return function (arg, errors, options) {
var argParts = arg.split(/\s+/);
var newObject = utils.createObjectFromPath(argParts[0], argParts.slice(1).join(' '));
var comparison = { spec: specObject, actual: newObject };
while (comparison.spec) {
var key = _.keys(comparison.actual)[0];
var spec = comparison.spec[key];
if (!spec) {
return false;
}
if (_.isFunction(comparison.spec[key])) {
var result = comparison.spec[key](comparison.actual[key]);
if (result.valid) {
comparison.actual[key] = result.converted;
utils.deepExtend(options, newObject);
}
else {
errors.push('Invalid value [' + comparison.actual[key] + '] for option [' + argParts[0] + ']');
}
return true;
}
else if (_.isArray(comparison.actual[key])) {
var newVal = [];
newVal[comparison.actual[key].length - 1] = comparison.spec[key][0];
comparison.spec = newVal;
comparison.actual = comparison.actual[key];
}
else {
comparison.spec = comparison.spec[key];
comparison.actual = comparison.actual[key];
}
}
};
};
/**
* @constructor
*/
function Command(root, handler) {
'use strict';
this.root = root;
this.handler = handler;
this.parsers = [];
}
Command.prototype.option = function (optionString, validator) {
'use strict';
if (_.isFunction(validator)) {
this.parsers.push(getParser(optionString, validator));
}
else if (_.isObject(validator)) {
var dummy = {};
dummy[optionString] = validator;
this.parsers.push(getObjectParser(dummy));
}
else {
throw new Error('Bad validator [' + validator + '] specified for option ' + optionString);
}
return this;
};
Command.prototype.options = function (optsSpec) {
'use strict';
var self = this;
_.each(optsSpec, function (validator, key) {
self.option(key, validator);
});
return this;
};
Command.prototype.optionLookup = function (groupName, lookup) {
'use strict';
if (typeof lookup !== 'function') {
lookup = _.propertyOf(lookup);
}
this.parsers.push(function (arg, errors, options) {
options[groupName] = options[groupName] || [];
var someMatch = false;
var resolved = lookup(arg, options);
if (resolved) {
options[groupName].push(resolved);
someMatch = true;
}
else {
_.each(arg.split(','), function (name) {
var resolved = lookup(name.trim(), options);
if (resolved) {
options[groupName].push(resolved);
someMatch = true;
}
});
}
return someMatch;
});
return this;
};
Command.prototype.handle = function (args, selection) {
'use strict';
var self = this;
var options = { errors: [] };
options.selected = this.selectionSpec && processSelection(selection || [], this.selectionSpec);
options = _.reduce(args, function (options, arg) {
var parser = _.find(self.parsers, function (parser) {
return parser(arg, options.errors, options);
});
if (!parser) {
options.errors.push('Unrecognised or poorly formed option ' + arg);
}
return options;
}, options);
if (options.errors.length > 0) {
throw options.errors.join('\n');
}
delete options.errors;
this.handler(options);
};
Command.prototype.withSelection = function (selectionSpec) {
'use strict';
this.selectionSpec = selectionSpec;
return this;
};
Command.prototype.addCommand = function (cmdString, handler) {
'use strict';
return this.root.addCommand(cmdString, handler);
};
Command.prototype.end = function () {
'use strict';
return this.root;
};
function processSelection(selection, constraints) {
'use strict';
return _.reduce(constraints, function (result, constraintDetails, type) {
var objects = _.chain(selection)
.where({ _type: type === 'character' ? 'graphic' : type })
.map(function (selected) {
return roll20.getObj(selected._type, selected._id);
})
.map(function (object) {
if (type === 'character' && object) {
var represents = object.get('represents');
if (represents) {
return roll20.getObj('character', represents);
}
}
return object;
})
.compact()
.value();
if (_.size(objects) < constraintDetails.min || _.size(objects) > constraintDetails.max) {
throw 'Wrong number of objects of type [' + type + '] selected, should be between ' + constraintDetails.min +
' and ' + constraintDetails.max;
}
switch (_.size(objects)) {
case 0:
break;
case 1:
if (constraintDetails.max === 1) {
result[type] = objects[0];
}
else {
result[type] = objects;
}
break;
default:
result[type] = objects;
}
return result;
}, {});
}
module.exports = function (rootCommand) {
'use strict';
var commands = {};
return {
addCommand: function (cmds, handler) {
var command = new Command(this, handler);
_.each(_.isArray(cmds) ? cmds : [cmds], cmdString => commands[cmdString] = command);
return command;
},
processCommand: function (msg) {
var prefix = '!' + rootCommand + '-';
if (msg.type === 'api' && msg.content.indexOf(prefix) === 0) {
var cmdString = msg.content.slice(prefix.length);
var parts = cmdString.split(/\s+--/);
var cmdName = parts.shift();
var cmd = commands[cmdName];
if (!cmd) {
throw 'Unrecognised command ' + prefix + cmdName;
}
cmd.handle(parts, msg.selected);
}
}
};
};
/***/ },
/* 13 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
var levelStrings = ['Cantrips ', '1st level ', '2nd level ', '3rd level '];
_.each(_.range(4, 10), function (level) {
levelStrings[level] = level + 'th level ';
});
var spellcastingHandler = {
splitRegex: /(Cantrips|(?:1st|2nd|3rd|[4-9]th)\s*level)\.?\s*\(([^\)]+)\)\s*:/i,
makeLevelDetailsObject: function (match) {
var levelMatch = match[1].match(/\d/);
return {
level: levelMatch ? parseInt(levelMatch[0]) : 0,
slots: match[2]
};
},
setLevelDetailsString: function (levelDetails) {
levelDetails.newText = levelStrings[levelDetails.level] + '(' + levelDetails.slots + '): ';
levelDetails.newText += levelDetails.spells.join(', ');
}
};
var innateHandler = {
splitRegex: /(At\s?will|\d\s?\/\s?day)(?:\s?each)?\s?:/i,
makeLevelDetailsObject: function (match) {
var usesMatch = match[1].match(/\d/);
return {
uses: usesMatch ? parseInt(usesMatch[0]) : 0,
slots: match[2]
};
},
setLevelDetailsString: function (levelDetails) {
levelDetails.newText = levelDetails.uses === 0 ? 'At will' : levelDetails.uses + '/day';
if (levelDetails.spells.length > 1) {
levelDetails.newText += ' each';
}
levelDetails.newText += ': ';
levelDetails.newText += levelDetails.spells.join(', ');
}
};
function processSpellcastingTrait(monster, traitName, traitHandler, entityLookup) {
var trait = _.findWhere(monster.traits, { name: traitName });
if (trait) {
var spellList = trait.text.substring(trait.text.indexOf(':') + 1).replace('\n', ' ');
var castingDetails = trait.text.substring(0, trait.text.indexOf(':'));
var levelDetails = [];
var match;
while ((match = traitHandler.splitRegex.exec(spellList)) !== null) {
if (_.last(levelDetails)) {
_.last(levelDetails).spells = spellList.slice(0, match.index);
}
levelDetails.push(traitHandler.makeLevelDetailsObject(match));
spellList = spellList.slice(match.index + match[0].length);
}
if (_.last(levelDetails)) {
_.last(levelDetails).spells = spellList;
}
var hasCastBeforeCombat = false;
var spellDetailsByLevel = _.chain(levelDetails)
.each(function (levelDetails) {
levelDetails.spells = _.chain(levelDetails.spells.replace(',*', '*,').split(','))
.map(_.partial(_.result, _, 'trim'))
.map(function (spellName) {
var match = spellName.match(/([^\(\*]+)(?:\(([^\)]+)\))?(\*)?/);
hasCastBeforeCombat = hasCastBeforeCombat || !!match[3];
return {
name: match[1].trim(),
restriction: match[2],
castBeforeCombat: !!match[3],
toString: function () {
return this.name +
(this.restriction ? ' (' + this.restriction + ')' : '') +
(this.castBeforeCombat ? '*' : '');
},
toSpellArrayItem: function () {
return this.name;
}
};
})
.each(function (spell) {
spell.object = entityLookup.findEntity('spells', spell.name, true);
if (spell.object) {
spell.name = spell.object.name;
spell.toSpellArrayItem = function () {
return this.object;
};
}
})
.value();
})
.each(traitHandler.setLevelDetailsString)
.value();
trait.text = castingDetails + ':\n' + _.pluck(spellDetailsByLevel, 'newText').join('\n');
if (hasCastBeforeCombat) {
trait.text += '\n* The ' + monster.name.toLowerCase() + ' casts these spells on itself before combat.';
}
var spells = _.chain(spellDetailsByLevel)
.pluck('spells')
.flatten()
.map(_.partial(_.result, _, 'toSpellArrayItem'))
.union(monster.spells ? monster.spells : [])
.sortBy('name')
.sortBy('level')
.value();
if (!_.isEmpty(spells)) {
monster.spells = spells;
}
}
return [];
}
module.exports = function (monsters, entityLookup) {
_.each(monsters, function (monster) {
processSpellcastingTrait(monster, 'Spellcasting', spellcastingHandler, entityLookup);
processSpellcastingTrait(monster, 'Innate Spellcasting', innateHandler, entityLookup);
});
};
/***/ },
/* 14 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
var roll20 = __webpack_require__(1);
var utils = __webpack_require__(7);
var advantageMarker = 'green',
disadvantageMarker = 'red';
var ignoreNpc = false;
class AdvantageTracker {
constructor(logger, myState) {
this.logger = logger;
this.myState = myState;
}
getSelectedCharacters(selected) {
return _.chain(selected)
.map(function (s) {
return s.get('_id');
})
.value();
}
updateSetting(msg) {
this.logger.debug('AT: Updating Setting');
var br, setting, isAdvantage, isDisadvantage,
char = [];
char.push(msg.get('_characterid'));
br = this.buildResources(_.uniq(_.union(char)));
setting = msg.get('current');
isAdvantage = '@{roll_advantage}' === setting;
isDisadvantage = '@{roll_disadvantage}' === setting;
if (this.myState.config.advTrackerSettings.showMarkers) {
_.each(br[0].tokens, function (t) {
t.set('status_' + disadvantageMarker, isDisadvantage);
t.set('status_' + advantageMarker, isAdvantage);
});
}
}
updateToken(token) {
this.logger.debug('AT: Updating New Token');
if (!this.myState.config.advTrackerSettings.showMarkers) {
return;
}
var character, setting, isAdvantage, isDisadvantage;
if (token.get('represents') === '') {
return;
}
character = roll20.getObj('character', token.get('represents'));
setting = roll20.getAttrByName(character.id, 'roll_setting');
isAdvantage = '@{roll_advantage}' === setting;
isDisadvantage = '@{roll_disadvantage}' === setting;
if (ignoreNpc) {
if (roll20.getAttrByName(character.id, 'is_npc') === '1') {
return;
}
}
token.set('status_' + disadvantageMarker, isDisadvantage);
token.set('status_' + advantageMarker, isAdvantage);
}
buildResources(ids) {
return _.chain(ids)
.map(function (cid) {
return roll20.getObj('character', cid);
})
.reject(_.isUndefined)
//.filter(this.npcCheckFunc)
.map(function (c) {
return {
character: c,
tokens: roll20.filterObjs(function (o) {
return 'graphic' === o.get('_type') &&
c.id === o.get('represents');
})
};
})
.value();
}
setAttribute(options) {
if (!options.current && options.current !== '') {
roll20.log('Error setting empty value: ');// + name);
return;
}
var attr = roll20.findObjs({
_type: 'attribute',
_characterid: options.characterId,
name: options.name
})[0];
if (!attr) {
roll20.createObj('attribute', {
name: options.name,
current: options.current,
characterid: options.characterId
});
}
else if (!attr.get('current') || attr.get('current').toString() !== options.current) {
attr.set({
current: options.current
});
}
}
setMarkers(type, resources) {
var self = this;
var setting,
rollInfo = '',
preroll = '',
postroll = '',
valByType = {
normal: '@{roll_1}',
advantage: '@{roll_advantage}',
disadvantage: '@{roll_disadvantage}',
roll2: '@{roll_2}'
},
msgByType = {
normal: 'normally',
advantage: 'with advantage',
disadvantage: 'with disadvantage',
roll2: 'two dice'
},
isAdvantage = 'advantage' === type,
isDisadvantage = 'disadvantage' === type;
_.each(resources, function (r) {
if (self.myState.config.advTrackerSettings.showMarkers) {
_.each(r.tokens, function (t) {
t.set('status_' + disadvantageMarker, isDisadvantage);
t.set('status_' + advantageMarker, isAdvantage);
});
}
setting = roll20.getAttrByName(r.character.get('_id'), 'roll_setting');
if (setting === valByType[type]) {
return;
}
self.setAttribute({
characterId: r.character.get('_id'),
name: 'roll_setting',
current: valByType[type]
});
if (valByType[type] === '@{roll_advantage}') {
rollInfo = '{{advantage=1}}';
preroll = 2;
postroll = 'kh1';
}
if (valByType[type] === '@{roll_disadvantage}') {
rollInfo = '{{disadvantage=1}}';
preroll = 2;
postroll = 'kl1';
}
self.setAttribute({
characterId: r.character.get('_id'),
name: 'roll_info',
current: rollInfo
});
self.setAttribute({
characterId: r.character.get('_id'),
name: 'preroll',
current: preroll
});
self.setAttribute({
characterId: r.character.get('_id'),
name: 'postroll',
current: postroll
});
//if (chatOptions(who) === 'disabled') {
// return;
//}
roll20.sendChat('AdvantageTracker',
' &{template:5e-shaped} {{character_name=' + r.character.get('name') + '}} @{' + r.character.get('name') +
'|show_character_name} {{title=' + utils.toTitleCase(type) + '}} {{text_top=' + r.character.get('name') +
' is rolling ' + msgByType[type] + '!}}');
});
}
}
module.exports = AdvantageTracker;
/***/ },
/* 15 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
var _ = __webpack_require__(2);
var utils = __webpack_require__(7);
class ConfigUi {
////////////
// Menus
////////////
getConfigOptionsAll(config, optionsSpec) {
return this.getConfigOptionGroupAdvTracker(config, optionsSpec) +
this.getConfigOptionGroupTokens(config, optionsSpec) +
this.getConfigOptionGroupNewCharSettings(config, optionsSpec);
}
getConfigOptionsMenu() {
var optionRows = this.makeOptionRow('Advantage Tracker', 'atMenu', '', 'view', '', '#02baf2') +
this.makeOptionRow('Token Defaults', 'tsMenu', '', 'view', '', '#02baf2') +
this.makeOptionRow('New Characters', 'ncMenu', '', 'view', '', '#02baf2');
var th = utils.buildHTML('th', 'Main Menu', { colspan: '2' });
var tr = utils.buildHTML('tr', th, { style: 'margin-top: 5px;' });
return utils.buildHTML('table', tr + optionRows, { style: 'width: 100%; font-size: 0.9em;' });
}
getConfigOptionGroupAdvTracker(config, optionsSpec) {
var optionRows = this.makeToggleSetting(config, 'advTrackerSettings.showMarkers', 'Show Markers');
var th = utils.buildHTML('th', 'Advantage Tracker Options', { colspan: '2' });
var tr = utils.buildHTML('tr', th, { style: 'margin-top: 5px;' });
var table = utils.buildHTML('table', tr + optionRows, { style: 'width: 100%; font-size: 0.9em;' });
return table + this.backToMainMenuButton();
}
getConfigOptionGroupTokens(config, optionsSpec) {
var auraButtonWidth = 60;
var ts = 'tokenSettings';
var retVal = '' +
'| Token Options |
' +
this.makeToggleSetting(config, `${ts}.number`, 'Numbered Tokens') +
this.makeToggleSetting(config, `${ts}.showName`, 'Show Name Tag') +
this.makeToggleSetting(config, `${ts}.showNameToPlayers`, 'Show Name to Players');
for (var i = 1; i <= 3; i++) {
retVal += this.makeInputSetting(config, `${ts}.bar${i}.attribute`, `Bar ${i} Attribute`, `Bar ${i} Attribute (empty to unset)`);
retVal += this.makeToggleSetting(config, `${ts}.bar${i}.max`, `Bar ${i} Set Max`);
retVal += this.makeToggleSetting(config, `${ts}.bar${i}.link`, `Bar ${i} Link`);
retVal += this.makeToggleSetting(config, `${ts}.bar${i}.showPlayers`, `Bar ${i} Show Players`);
}
// Build out the aura grids
for (i = 1; i <= 2; i++) {
var currRad = utils.getObjectFromPath(config, `${ts}.aura${i}.radius`);
var currRadEmptyHint = currRad ? currRad : '[not set]';
var currColor = utils.getObjectFromPath(config, `${ts}.aura${i}.color`);
var currSquare = utils.getObjectFromPath(config, `${ts}.aura${i}.square`);
var radBtn = this.makeOptionButton(`${ts}.aura${i}.radius`, `?{Aura ${i} Radius (empty to unset)|${currRad}}`,
this.makeText(currRadEmptyHint), 'click to edit', currRadEmptyHint === '[not set]' ? '#f84545' : '#02baf2',
undefined, auraButtonWidth);
var colorBtn = this.makeOptionButton('tokenSettings.aura' + i + '.color',
'?{Aura ' + i + ' Color (hex colors)' + '|' + currColor + '}',
this.makeText(currColor), 'click to edit', currColor, utils.getContrastYIQ(currColor), auraButtonWidth);
var squareBtn = this.makeOptionButton('tokenSettings.aura' + i + '.square', !currSquare,
this.makeBoolText(currSquare), 'click to toggle', currSquare ? '#65c4bd' : '#f84545',
undefined, auraButtonWidth);
retVal += utils.buildHTML('tr', [
{
tag: 'td', innerHtml: [
{
tag: 'table', innerHtml: [
{
tag: 'tr',
innerHtml: [{ tag: 'th', innerHtml: 'Aura ' + i, attrs: { colspan: 3 } }]
},
{
tag: 'tr',
innerHtml: [
{
tag: 'td',
innerHtml: 'Range'
},
{
tag: 'td',
innerHtml: 'Color'
},
{
tag: 'td',
innerHtml: 'Square'
}
]
},
{
tag: 'tr',
innerHtml: [
{
tag: 'td',
innerHtml: radBtn
},
{
tag: 'td',
innerHtml: colorBtn
},
{
tag: 'td',
innerHtml: squareBtn
}
]
}
],
attrs: { style: 'width: 100%; text-align: center;' }
}
], attrs: { colspan: '2' }
}
], { style: 'border: 1px solid gray;' });
}
// Vision\Light options
retVal += this.makeInputSetting(config, `${ts}.light.radius`, 'Light Radius', 'Light Radius (empty to unset)');
retVal += this.makeInputSetting(config, `${ts}.light.dimRadius`, 'Dim Radius', 'Light Dim Radius (empty to unset)');
retVal += this.makeToggleSetting(config, `${ts}.light.otherPlayers`, 'Show other players');
retVal += this.makeToggleSetting(config, `${ts}.light.hasSight`, 'Has Sight');
retVal += this.makeInputSetting(config, `${ts}.light.angle`, 'Light Angle', 'Light Amgle');
retVal += this.makeInputSetting(config, `${ts}.light.losAngle`, 'LOS Angle', 'LOS Angle');
retVal += this.makeInputSetting(config, `${ts}.light.multiplier`, 'Light Muliplier', 'Light Muliplier');
retVal += '
' + this.backToMainMenuButton();
return retVal;
}
getConfigOptionGroupNewCharSettings(config, optionsSpec) {
var optionRows = this.makeQuerySetting(config, 'newCharSettings.sheetOutput', 'Sheet Output',
optionsSpec.newCharSettings.sheetOutput()) +
this.makeQuerySetting(config, 'newCharSettings.deathSaveOutput',
'Death Save Output', optionsSpec.newCharSettings.deathSaveOutput()) +
this.makeQuerySetting(config, 'newCharSettings.initiativeOutput', 'Initiative Output',
optionsSpec.newCharSettings.initiativeOutput()) +
this.makeToggleSetting(config, 'newCharSettings.showNameOnRollTemplate', 'Show Name on Roll Template',
optionsSpec.newCharSettings.showNameOnRollTemplate()) +
this.makeQuerySetting(config, 'newCharSettings.rollOptions', 'Roll Options',
optionsSpec.newCharSettings.rollOptions()) +
this.makeQuerySetting(config, 'newCharSettings.initiativeRoll', 'Init Roll',
optionsSpec.newCharSettings.initiativeRoll()) +
this.makeToggleSetting(config, 'newCharSettings.initiativeToTracker', 'Init To Tracker',
optionsSpec.newCharSettings.initiativeToTracker()) +
this.makeToggleSetting(config, 'newCharSettings.breakInitiativeTies', 'Break Init Ties',
optionsSpec.newCharSettings.breakInitiativeTies()) +
this.makeToggleSetting(config, 'newCharSettings.showTargetAC', 'Show Target AC',
optionsSpec.newCharSettings.showTargetAC()) +
this.makeToggleSetting(config, 'newCharSettings.showTargetName', 'Show Target Name',
optionsSpec.newCharSettings.showTargetName()) +
this.makeToggleSetting(config, 'newCharSettings.autoAmmo', 'Auto Use Ammo',
optionsSpec.newCharSettings.autoAmmo());
var th = utils.buildHTML('th', 'New Character Sheets', { colspan: '2' });
var tr = utils.buildHTML('tr', th, { style: 'margin-top: 5px;' });
var table = utils.buildHTML('table', tr + optionRows, { style: 'width: 100%; font-size: 0.9em;' });
return table + this.backToMainMenuButton();
}
backToMainMenuButton() {
return utils.buildHTML('a', 'back to main menu', {
href: '!shaped-config',
style: 'text-align: center; margin: 5px 0 0 0; padding: 2px 2px ; border-radius: 10px; white-space: nowrap; ' +
'overflow: hidden; text-overflow: ellipsis; background-color: #02baf2; border-color: #c0c0c0;'
});
}
makeInputSetting(config, path, title, prompt) {
var currentVal = utils.getObjectFromPath(config, path);
var emptyHint = '[not set]';
if (currentVal) {
emptyHint = currentVal;
}
return this.makeOptionRow(title, path, `?{${prompt}|${currentVal}}`, emptyHint, 'click to edit', emptyHint ===
'[not set]' ? '#f84545' : '#02baf2');
}
//noinspection JSUnusedGlobalSymbols
makeColorSetting(config, path, title, prompt) {
var currentVal = utils.getObjectFromPath(config, path);
var emptyHint = '[not set]';
if (currentVal) {
emptyHint = currentVal;
}
var buttonColor = emptyHint === '[not set]' ? '#02baf2' : currentVal;
// return this.makeOptionRow(title, path, '?{' + prompt + '|' + currentVal + '}', emptyHint, 'click to edit', buttonColor, utils.getContrastYIQ(buttonColor));
return this.makeOptionRow(title, path, `?{${prompt}|${currentVal}}`, emptyHint, 'click to edit', buttonColor, utils.getContrastYIQ(buttonColor));
}
makeToggleSetting(config, path, title, optionsSpec) {
var currentVal = utils.getObjectFromPath(config, path);
if (optionsSpec) {
currentVal = _.invert(optionsSpec)[currentVal] === 'true';
}
return this.makeOptionRow(title, path, !currentVal,
this.makeBoolText(currentVal), 'click to toggle', currentVal ? '#65c4bd' : '#f84545');
}
makeQuerySetting(config, path, title, optionsSpec) {
var currentVal = _.invert(optionsSpec)[utils.getObjectFromPath(config, path)];
var optionList = _.keys(optionsSpec);
// move the current option to the front of the list
optionList.splice(optionList.indexOf(currentVal), 1);
optionList.unshift(currentVal);
// return this.makeOptionRow(title, path, '?{' + title + '|' + optionList.join('|') + '}', this.makeText(currentVal), 'click to change', '#02baf2');
return this.makeOptionRow(title, path, `?{${title}|${optionList.join('|')}}`, this.makeText(currentVal), 'click to change', '#02baf2');
}
makeOptionRow(optionTitle, path, command, linkText, tooltip, buttonColor, buttonTextColor) {
var col1 = utils.buildHTML('td', optionTitle);
var col2 = utils.buildHTML('td', this.makeOptionButton(path, command, linkText, tooltip, buttonColor, buttonTextColor),
{ style: 'text-align:right;' });
return utils.buildHTML('tr', col1 + col2, { style: 'border: 1px solid gray;' });
}
makeOptionButton(path, command, linkText, tooltip, buttonColor, buttonTextColor, width) {
if (_.isUndefined(width)) {
width = 80;
}
var css = `text-align: center; width: ${width}px; margin: 5px 0 0 0; ` +
`padding: 2px 2px ; border-radius: 10px; border-color: #c0c0c0;` +
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background-color: ${buttonColor};`;
if (buttonTextColor) {
css += `color: ${buttonTextColor}`; // 'color: ' + buttonTextColor + '; ';
}
return utils.buildHTML('a', linkText, {
style: css,
href: `!shaped-config --${path} ${command}` //'!shaped-config --' + path + ' ' + command
});
}
makeText(value) {
return utils.buildHTML('span', value, { style: 'padding: 0 2px;' });
}
makeBoolText(value) {
return value === true ?
utils.buildHTML('span', 'on', { style: 'padding: 0 2px;' }) :
utils.buildHTML('span', 'off', { style: 'padding: 0 2px;' });
}
}
module.exports = ConfigUi;
/***/ },
/* 16 */
/***/ function(module, exports) {
function sanitise(statblock, logger) {
'use strict';
statblock = statblock
.replace(/\s+([\.,;:])/g, '$1')
.replace(/\n+/g, '#')
.replace(/–/g, '-')
.replace(/
]*>/g, '#')
.replace(/#+/g, '#')
.replace(/\s*#\s*/g, '#')
.replace(/(<([^>]+)>)/gi, '')
.replace(/legendary actions/gi, 'Legendary Actions')
.replace(/(\S)\sACTIONS/, '$1#ACTIONS')
.replace(/#(?=[a-z]|DC)/g, ' ')
.replace(/\s+/g, ' ')
.replace(/#Hit:/gi, 'Hit:')
.replace(/Hit:#/gi, 'Hit: ')
.replace(/#Each /gi, 'Each ')
.replace(/#On a successful save/gi, 'On a successful save')
.replace(/DC#(\d+)/g, 'DC $1')
.replace('LanguagesChallenge', 'Languages -\nChallenge')
.replace('\' Speed', 'Speed')
.replace(/(\w+) s([\s\.,])/g, '$1s$2')
.replace(/#Medium or/gi, ' Medium or')
.replace(/take#(\d+)/gi, 'take $1')
.replace(/#/g, '\n');
logger.debug('First stage cleaned statblock: $$$', statblock);
//Sometimes the texts ends up like 'P a r a l y z i n g T o u c h . M e l e e S p e l l A t t a c k : + 1 t o h i t
//In this case we can fix the title case stuff, because we can find the word boundaries. That will at least meaning
//that the core statblock parsing will work. If this happens inside the lowercase body text, however, there's nothing
//we can do about it because you need to understand the natural language to reinsert the word breaks properly.
statblock = statblock.replace(/([A-Z])(\s[a-z]){2,}/g, function (match, p1) {
return p1 + match.slice(1).replace(/\s([a-z])/g, '$1');
});
//Conversely, sometimes words get mushed together. Again, we can only fix this for title case things, but that's
//better than nothing
statblock = statblock.replace(/([A-Z][a-z]+)(?=[A-Z])/g, '$1 ');
//This covers abilites that end up as 'C O N' or similar
statblock = statblock.replace(/^[A-Z]\s?[A-Z]\s?[A-Z](?=\s|$)/mg, function (match) {
return match.replace(/\s/g, '');
});
statblock = statblock.replace(/^[A-Z ]+$/m, function (match) {
return match.replace(/([A-Z])([A-Z]+)(?=\s|$)/g, function (match, p1, p2) {
return p1 + p2.toLowerCase();
});
});
statblock = statblock.replace(/(\d+)\s*?plus\s*?((?:\d+d\d+)|(?:\d+))/gi, '$2 + $1');
var replaceObj = {
'Jly': 'fly',
',1\'': ',*',
'jday': '/day',
'abol eth': 'aboleth',
'ACT IONS': 'ACTIONS',
'Afrightened': 'A frightened',
'Alesser': 'A lesser',
'Athl etics': 'Athletics',
'blindn ess': 'blindness',
'blind sight': 'blindsight',
'bofh': 'both',
'brea stplate': 'breastplate',
'Can trips': 'Cantrips',
'choos in g': 'choosing',
'com muni cate': 'communicate',
'Constituti on': 'Constitution',
'creatu re': 'creature',
'darkvi sion': 'darkvision',
'dea ls': 'deals',
'di sease': 'disease',
'di stance': 'distance',
'fa lls': 'falls',
'fe et': 'feet',
'exha les': 'exhales',
'ex istence': 'existence',
'lfthe': 'If the',
'Ifthe': 'If the',
'ifthe': 'if the',
'lnt': 'Int',
'magica lly': 'magically',
'Med icine': 'Medicine',
'minlilte': 'minute',
'natura l': 'natural',
'ofeach': 'of each',
'ofthe': 'of the',
'on\'e': 'one',
'on ly': 'only',
'0n': 'on',
'pass ive': 'passive',
'Perce ption': 'Perception',
'radi us': 'radius',
'ra nge': 'range',
'rega ins': 'regains',
'rest.oration': 'restoration',
'savin g': 'saving',
'si lvery': 'silvery',
's lashing': 'slashing',
'slas hing': 'slashing',
'slash in g': 'slashing',
'slash ing': 'slashing',
'Spel/casting': 'Spellcasting',
'successfu l': 'successful',
'ta rget': 'target',
' Th e ': ' The ',
't_urns': 'turns',
'unti l': 'until',
'withi n': 'within',
'tohit': 'to hit',
'At wi ll': 'At will',
'per-son': 'person',
'ab ility': 'ability',
'spe ll': 'spell'
};
var re = new RegExp(Object.keys(replaceObj).join('|'), 'g');
statblock = statblock.replace(re, function (matched) {
return replaceObj[matched];
});
statblock = statblock
.replace(/,\./gi, ',')
.replace(/:\./g, ':')
.replace(/(\W)l(\W)/g, '$11$2')
.replace(/\.([\w])/g, '. $1')
.replace(/1