Skip to content

Instantly share code, notes, and snippets.

@rileydutton
Last active August 9, 2023 15:25
Show Gist options
  • Save rileydutton/f3d099af31773435d27a0d8744af0675 to your computer and use it in GitHub Desktop.
Save rileydutton/f3d099af31773435d27a0d8744af0675 to your computer and use it in GitHub Desktop.
/**
*
* Copyright (C) 2015 Ken L.
* Licensed under the GPL Version 3 license.
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* Andy W.
* Shu Zong C.
* Carlos R. L. Rodrigues
*
*
* This script is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* This script is designed to parse stat blocks from the pathfinder PRD which are
* formatted in a very particular way. Know that there will be edge cases which
* are not handled due to the large varity of conditions.
*
* It is charactersheet agnostic as it simply creates ability fields and marcos for
* the provided statblock. It is also light-weight simply (if enabled) linking or
* reminding a GM of information or abilities a creature has.
*
* If copying stat blocks from PDFs or other Paizo or Paizo compatible sources,
* ensure that they fit the PRD specified layout (1 line per ability etc), when
* in doubt, copy it into a text editor and confirm what you copied fits the PRD
* format.
*
* If all else fails, it's probably an edge case I didn't consider; or the stat
* block is malformed. Feel free to patch up the holes and commit to the git-hub to
* make the script better for everyone.
*
* https://github.com/Roll20KenL/Roll20_API_Scripts/blob/master/cgen.js
*
*/
var CreatureGenPF = (function() {
'use strict';
var version = 1.32,
author = "Ken L.",
contributers = "Andy W., Shu Zong C., Carlos R. L. Rodrigues",
debugLvl = 1,
locked = false,
unitPixels = 70,
workStart = 0,
workDelay = 250,
workList = [],
dmesg,
warn,
creName,
character;
var termEnum = Object.freeze({
GENERAL : 1,
MONABILITY: 2,
SPELL: 3,
FEAT: 4,
SQ: 5,
SA: 6,
SKILL: 7
});
var bonusEnum = Object.freeze({
SCALAR: 1,
SIGN: 2
});
var urlCondEnum = Object.freeze({
FULL: "FULL",
LABEL: "LABEL"
});
var atkEnum = Object.freeze({
TITLE: 'TITLE',
ATTACK: 'ATTACK',
DAMAGE: 'DAMAGE'
});
/**
* Various fields that can be changed for customization
*
*/
var fields = {
defaultName: "Creature",
charDataPrefix: "CGen_",
publicName: "@{CGen_name}",
publicEm: "/emas ",
publicAtkEm: "/as Attack ",
publicDmgEm: "/as Damage ",
publicAnn: "/desc ",
privWhis: "/w GM ",
menuWhis: "/w GM ",
resultWhis: "/w GM ",
urlTermGeneral: '<a href="http://www.google.com/cse?cx=006680642033474972217%3A6zo0hx_wle8&q=<<FULL>>"><span style="color: #FF0000; font-weight: bold;"><<LABEL>></span></a>',
//urlTermGeneral: '<a href="http://www.d20pfsrd.com/system/app/pages/search?scope=search-site&q=<<FULL>>"><span style="color: #FF0000; font-weight: bold;"><<LABEL>></span></a>',
urlTermMonAbility: '<a href="http://paizo.com/pathfinderRPG/prd/additionalMonsters/universalMonsterRules.html#<<FULL>>"><span style="color: #2F2F2F; font-weight: bold;"><<FULL>></span></a>',
urlTermSpell: '<a href="http://www.d20pfsrd.com/magic/all-spells/<<0>>/<<FULL>>"><span style="color: #2E39FF; font-weight: bold;"><<LABEL>></span></a>',
urlTermFeat: '<a href="http://www.google.com/cse?cx=006680642033474972217%3A6zo0hx_wle8&q=<<FULL>>"><span style="color: #29220A; font-weight: bold;"><<FULL>></span></a>',
urlTermSQ: "", // unused
urlTermSA: "", // unused
summoner: undefined,
shortAtkRiders: false // unused
};
/**
* div border styles, customize it for your game!
*
* <span [[format text injected here]]> penguins </span>
* <img src="[[image link]]">"
*/
var design = {
feedbackName: 'Ken L.',
feedbackImg: 'https://s3.amazonaws.com/files.d20.io/images/3466065/uiXt3Zh5EoHDkXmhGUumYQ/thumb.jpg?1395313520',
errorImg: 'https://s3.amazonaws.com/files.d20.io/images/7545187/fjEEs0Jvjz1uy3mGN5A_3Q/thumb.png?1423165317',
warningImg: 'https://s3.amazonaws.com/files.d20.io/images/7926480/NaBmVmKe94rdzXwVnLq0-w/thumb.png?1424965188',
successImg: 'https://s3.amazonaws.com/files.d20.io/images/7545189/5BR2W-XkmeVyXNsk-C8Z6g/thumb.png?1423165325',
skillTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
skillLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
skillTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816000/-QKJOJF6UFmDAEypDGeXcw/thumb.png?1424460624',
skillMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816002/nKEWcTQ2VjcRJqrfiMHg_w/thumb.png?1424460630',
skillBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816003/lcNufMB1JeLmdDVPJ0KFdQ/thumb.png?1424460634',
spellBookTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
spellBookLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
spellBookTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7834797/n4P8Ys6gHoKmVlBTRYoi9Q/thumb.png?1424540093',
spellBookMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7834799/LOJaPHkZcUDyekpMnm0-Cg/thumb.png?1424540096',
spellBookMidOvr: 'https://s3.amazonaws.com/files.d20.io/images/7836432/fi7Bw5cRaMfq0fjBZZ7ggg/thumb.png?1424544220',
spellBookBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7834800/ORBbrVuMO7Ns2UoPRmvfRg/thumb.png?1424540099',
spellTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
spellLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
spellTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7382439/dJ-jaPdm6kEtl9lE__ve-g/thumb.png?1422405849',
spellMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7382437/FYBtfPgkj4b3Q_hLkUk5Gg/thumb.png?1422405847',
spellBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7382435/DXm7UUdLKNnF6ktFfVojWA/thumb.png?1422405843',
specialTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
specialLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
specialTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816913/PsSBFlCv3IlPzGP0usiVBw/thumb.png?1424464282',
specialMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816781/0g1qpdJ80DTvpSHqQ2-7oA/thumb.png?1424463781',
specialBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816784/aZ1OFXDsO2BUM5G9oiXpgw/thumb.png?1424463791',
atkTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
atkLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
atkTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816913/PsSBFlCv3IlPzGP0usiVBw/thumb.png?1424464282',
atkMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816781/0g1qpdJ80DTvpSHqQ2-7oA/thumb.png?1424463781',
atkBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816784/aZ1OFXDsO2BUM5G9oiXpgw/thumb.png?1424463791',
atkMenuTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
atkMenuLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
atkMenuTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816912/WWQxo9xFhc-vETYw79f0wA/thumb.png?1424464275',
atkMenuMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816781/0g1qpdJ80DTvpSHqQ2-7oA/thumb.png?1424463781',
atkMenuBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816783/m3UhKz4plb_0TB1kUOAI1Q/thumb.png?1424463787',
genTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
genLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
genTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816014/jpO1FF9pA7Ipi__sFQAAMw/thumb.png?1424460680',
genMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816016/0XT65Rctt1imgamKQUO6sg/thumb.png?1424460684',
genBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816023/T6uTWEX4aMDL-78lhSLanw/thumb.png?1424460752',
};
var atkTemplate = '<div class="img" style="column-gap: 0; line-height: 0; position: relative; text-align: center;">'
+ '<img src="'+"https://s3.amazonaws.com/files.d20.io/images/7926600/1gnY_LsAt4UHnkJmI3PAlA/thumb.png?1424966062"+'">'
+ '</div>'
+ '<div style="background-image: url('+"https://s3.amazonaws.com/files.d20.io/images/7926601/79qaHPngD_d9MFVlrNTB8w/thumb.png?1424966067"+'); background-repeat: repeat-y; background-position: center center; text-align: center; padding-left: 7px; padding-right: 7px;">'
+ '<div>'
+ '<div style="colspan: 2; color: #FFFFFF; font-style: italic; text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000; padding-top: 4px; padding-bottom: 4px; margin-left: auto; margin-right: auto; width: 160px">'
+ '<span style="font-weight: bold;"><<TITLE>></span>'
+ '</div>'
+ '<div style="color: #FFFFFF; text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000; padding-top: 4px; padding-bottom: 4px; display: block; font-weight: bold;">'
+ '<table style="width: 140px; text-align: left; margin-left: auto; margin-right: auto; border-spacing: 5;">'
+ '<tr>'
+ '<td>' + 'Attack:' + '</td>'
+ '<td>' + '<<ATTACK>>' + '</td>'
+ '</tr>'
+ '<tr>'
+ '<td>' + 'Damage:' + '</td>'
+ '<td>' + '<<DAMAGE>>' + '</td>'
+ '</tr>'
+ '</table>'
+' </div>'
+ '</div>'
+ '</div>'
+ '<div class="img" style="column-gap: 0; line-height: 0; position: relative; text-align: center;">'
+ '<img src="'+"https://s3.amazonaws.com/files.d20.io/images/7926602/lMjFWWNyFpUlbk80HqkeOQ/thumb.png?1424966073"+'">'
+ '</div>';
var menuTemplate = {
boundryImg: _.template('<div class="img" style="column-gap: 0; line-height: 0; position: relative; text-align: center;">'
+ '<img src="<%= imgLink %>">'
+ '</div>'),
titleFmt: _.template('<div>'
+ '<span <%= style %> >'
+ '<%= title %>'
+ '</span>'
+ '</div>'),
midDiv: _.template('<div style="background-image: url('
+ '<%= imgLink %>'
+ '); background-repeat: repeat-y; background-position: center center; text-align: center;">'),
/*
midDivOverlay: _.template('<div style="background: url(<%= imgLink %>) center center no-repeat; pointer-events: none;'
+ 'position: absolute; left: 0; top: 0; width: 200px; height: 200px;'
+ ' text-align: center;">'),
*/
midDivOverlay: _.template('<div style="background: '
+ 'url(<%= imgLinkOverlay %>) no-repeat center center,'
+ 'url(<%= imgLink %>) repeat-y center center;'
+ ' text-align: center; min-height: 150px;">'
+ '<table style="width: 100%; height: 100%; text-align: center; vertical-align: middle; min-height: 150px;">'
+ '<tr><td style="text-align: center; vertical-align: middle;">'),
midButton: _.template('<div style="text-align: center;">'
+ '<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\' %>'
+ '<%= \'<a href="!\' + \'&\'+\'#37\' + \';{\'+creName+\'|\'+abName+\'}">\'+btnName+\'</a>\' %>'
+ '</span>'
+ '</div>'),
midButtonFree: _.template('<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\' %>'
+ '<%= \'<a href="!\' + \'&\'+\'#37\' + \';{\'+creName+\'|\'+abName+\'}">\'+btnName+\'</a>\' %>'
+ '</span>'),
midLink: _.template('<div style="text-align: center;">'
+ '<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\' %>'
+ '<%= link %>'
+ '</span>'
+ '</div>'),
midLinkFree: _.template('<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\''
+ '+ link +'
+ '\'</span>\' %>'),
midLinkCaption: _.template('<div style="text-align: center;">'
+ '<%= link %>'
+ '<%= (riders ? (\'<span style="color: #2B2B2B; font-weight: lighter; font-style: italic; font-size: 70%;">\' + riders + \'</span>\'):"") %>'
+ '</div>'),
midLeadText: _.template('<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'" style="font-weight: bold; color:#FFFFFF; text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000;">\') : \' style="font-weight: bold; color:#000000;">\') %>'
+ '<%= label %>'
+ '</span> <span style="color: #000000;"><%= text %></span>'),
midText: _.template('<%= \'<span \'+ (riders ? (\'style="font-weight: bold; color:#FFFFFF; text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000;" class="showtip tipsy" title="\' + riders + \'"\') : \' style="color: #000000;"\') + \'>\' %>'
+ '<%= text %>'
+'</span>'),
};
/**
* Select design template
*/
var selectDesignTemplate = function(name, quiet) {
var flag;
if (typeof(CGTmp) !== "undefined") {
try {
for (var tmp in CGTmp.designTmp) {
if (tmp.toLowerCase() === name) {
design = CGTmp.designTmp[tmp];
if (!quiet) {
sendFeedback('<span style="color: #653200; font-weight: bold;">Design template:</span> <b style="color: #009C26;">' + tmp + '</b> has been '
+ 'configured for future tokens generated.</span>');
}
state.cgen_design = tmp;
flag = true;
break;
}
}
if (!flag)
{sendFeedback('<span style="color: #990004;">Design template: \'' + name + '\' does not exist</span>');}
} catch (e) {
log(e);
}
} else {
sendFeedback('<span style="color: #990004;">No CreatureGen templates found, '
+ 'did you load the additional templates script?</span>');
}
return flag;
};
/**
* Select attack template
*/
var selectAttackTemplate = function(name, quiet) {
var flag;
if (typeof(CGTmp) !== "undefined") {
try {
for (var tmp in CGTmp.attackTmp) {
if (tmp.toLowerCase() === name) {
fields.tmpAtk = CGTmp.attackTmp[tmp];
if (!quiet) {
sendFeedback('<span style="color: ##155391; font-weight: bold;">Attack template:</span> <b style="color: #009C26;">' + tmp + '</b> has been '
+ 'configured for future tokens generated.</span>');
}
state.cgen_attack = tmp;
flag = true;
break;
}
}
if (!flag)
{sendFeedback('<span style="color: #990004;">Attack template: \'' + name + '\' does not exist</span>');}
} catch (e) {
log(e);
}
} else {
sendFeedback('<span style="color: #990004;">No CreatureGen templates found, '
+ 'did you load the additional templates script?</span>');
}
return flag;
};
/**
* Object fix to resolve firebase errors
*
* @author Shu Zong C.
*/
var fixNewObject = function(obj) {
var p = obj.changed._fbpath;
if(p !== undefined) {
var new_p = p.replace(/([^\/]*\/){4}/, "/");
obj.fbpath = new_p;
}
return obj;
};
/**
* scan in information from token notes
*
* @contribuitor Andy W.
*/
var scan = function(token) {
var charSheet;
var data;
var rawData;
var dispData = "";
if (!token) {
throw "No Token selected";
}
rawData = token.get("gmnotes");
creLog('RAW: ' + rawData);
if (!rawData)
{throw "no token notes";}
data = rawData.split(/%3Cbr%3E|\\n|<br>/);
//clean out all other data except text
for (var i = data.length; i >= 0; i--) {
if (data[i]) {
data[i] = cleanString(data[i]).trim();
if (!data[i].match(/[^\s]/)) {
data.splice(i,1);
}
}
}
// Essential parameters for object creation (mainly the name)
parseEssential(data);
// if character name already exists, close out.
if (findObjs({
_type: "character",
name: creName,
}).length > 0) {
addWarning('Character \''+creName+'\' already exists.');
throw 'Character \''+creName+'\' already exists.';
}
dispData = formatDisplay(data);
charSheet = createObj("character", {
avatar: token.get("imgsrc"),
name: creName,
gmnotes: '',
archived: false,
inplayerjournals: '',
controlledby: ''
});
charSheet = fixNewObject(charSheet);
if (!charSheet) {
throw "ERROR: could not create character sheet";
}
if (fields.summoner) {
charSheet.set('bio','<br clear="both">' + dispData);
}
token.set("represents",charSheet.get('_id'));
charSheet.set('gmnotes',dispData);
character = charSheet;
// warn on image source
if (charSheet.get('avatar') === '') {
addWarning('Unable to set avatar to character journal, only images you\'ve '
+ 'uploaded yourself are viable during creation. Auto-population '
+ '<i>(drag-drop population)</i> will not be possible without an avatar image.'
+ ' You can still upload an avatar manually, or drag an image into the avatar field'
+ ' from the image-search.');
}
// parse up our data set.
var specials;
try {
parseCore(data);
specials = parseSpecials(data);
parseAttacks(data,specials);
parseSpells(data);
parseExtra(data,specials);
prepToken(token,charSheet);
} catch (e) {
log("ERROR when parsing");
throw e;
}
}
/**
* Format display of stat-block
*/
var formatDisplay = function(datum) {
if (!datum)
{return undefined;}
var content = '';
_.each(datum, function(e,i,l) {
creLog('('+i+') ' + e,1);
if (e.match('DEFENSE')
|| e.match('OFFENSE')
|| e.match('TACTICS')
|| e.match('BASE STATISTICS')
|| e.match('STATISTICS')
|| e.match('ECOLOGY')
|| e.match('SPECIAL ABILITIES'))
{content += '<div style="font-size: 112%; border-bottom: 1px solid black; border-top: 1px solid black; margin-top: 8px;">'+e+'</b></div>';}
else if (e.match(/\(Ex\)|\(Su\)|\(Sp\)/i))
{content += '<div>' + e + '</div>';}
else
{content += e+'<br>';}
});
return content;
};
/**
* Prep the token.
* Asynchronous
*/
var prepToken = function(token,character) {
if (!token || !character)
{return undefined;}
var name,AC,hp,prep;
var charId = character.get('_id');
hp = getAttribute("hp", charId);
AC = getAttribute("AC", charId);
prep = getAttribute("CGEN", charId);
name = character.get('name');
// Fast vs cb delay
if (token.get('gmnotes')) {
if (hp && AC && name && prep
&& (prep.get('current').match(/true/i))) {
hp = hp.get('current');
AC = AC.get('current');
token.set('bar1_value',hp);
token.set('bar1_max', hp);
token.set('bar3_value',AC);
token.set('name',name);
token.set('showname',true);
token.set('light_hassight',true);
resizeToken(token,character);
}
} else {
character.get('gmnotes',function(notes) {
if (hp && AC && name && prep
&& (prep.get('current').match(/true/i))
&& notes && (notes !== '')) {
hp = hp.get('current');
AC = AC.get('current');
token.set('bar1_value',hp);
token.set('bar1_max', hp);
token.set('bar3_value',AC);
token.set('name',name);
token.set('showname',true);
token.set('light_hassight',true);
token.set('gmnotes', notes
.replace(/<div[^<>]*>/g,'')
.replace(/<\/div>/g,'<br>'));
resizeToken(token,character);
}
});
}
};
/**
* Resize Token based on size attribute
*/
var resizeToken = function(token,character) {
var charId = character.get('_id');
var unitSize = 1;
var pageScale = getObj('page',token.get('_pageid')).get('snapping_increment');
var tsize = parseInt(unitPixels*(pageScale===0 ? 1:pageScale));
var size = getAttribute("Size", charId);
if (!size)
{return;}
switch (size.get('current')) {
case 'Fine':
case 'Diminutive':
case 'Tiny':
case 'Small':
case 'Medium':
break;
case 'Large':
unitSize = 2;
break;
case 'Huge':
unitSize = 3;
break;
case 'Gargantuan':
unitSize = 4;
break;
case 'Colossal':
unitSize = 6;
break;
default:
creLog('resizeToken: Bad size \''+size+'\' ');
}
token.set('width',tsize*unitSize);
token.set('height',tsize*unitSize);
};
var parseEssential = function(data) {
/* names are tricky as we delimit on CR, last occurance of CR
which has numbers after it TODO use a regex which is shorter*/
var namefield = data[0];
var delimiter_idx =namefield.lastIndexOf("CR");
var fuzzyfield = namefield.substring(delimiter_idx,namefield.length);
if ((delimiter_idx <= 0) || !(fuzzyfield.match(/\d+|—/g)))
{delimiter_idx = namefield.length;}
var name = namefield.substring(0,delimiter_idx);
name = name.trim().toLowerCase();
creName = (fields.summoner ? (fields.summoner.get('_displayname')+'\'s ' + name):name);
fields.publicName = (fields.summoner ? creName:fields.publicName);
};
/**
* parse core attributes, AC, HP, etc
*/
var parseCore = function(data) {
if (!data)
{return;}
if (fields.summoner) {
character.set('controlledby',fields.summoner.get('_id'));
character.set('inplayerjournals',fields.summoner.get('_id'));
}
var charId = character.get('_id');
var line = "";
var lineStartFnd = 0;
var lineEndFnd = data.length;
var termChars = [';',','];
var rc = -1;
// core attribute fields
var initAttr = "Init";
var primeAttr = ["Str","Dex","Con","Int","Wis","Cha"];
var minorAttr = ["Base Atk","CMB","CMD"];
var defAttr = ["AC","touch","flat-footed","hp"];
var saveAttr = ["Fort","Ref","Will"];
var hp=0,AC=0,tAC=0,ffAC=0;
//Flag that this token will be prepped, can be removed by the user
addAttribute("CGEN",'true','',charId);
addAttribute("name",fields.defaultName,'',charId);
// Init (TODO mythic Init)
line = getLineByName(initAttr,data);
rc = getValueByName(initAttr,line,termChars);
addAttribute(initAttr,rc,rc,charId);
// prime attributes
lineStartFnd = getLineNumberByName("STATISTICS",data);
lineEndFnd = getLineNumberByName("SPECIAL ABILITIES",data);
line = getLineByName("Str",data,lineStartFnd,lineEndFnd);
addAttrList(data,line,primeAttr,lineStartFnd,termChars,charId);
// minor attributes
line = getLineByName("Base Atk",data,lineStartFnd,lineEndFnd);
addAttrList(data,line,minorAttr,lineStartFnd,termChars,charId);
// defense attributes
lineStartFnd = getLineNumberByName("DEFENSE",data);
lineEndFnd = getLineNumberByName("OFFENSE",data);
line = getLineByName("AC",data,lineStartFnd,lineEndFnd);
addAttrList(data,line,defAttr,lineStartFnd,termChars,charId);
// save attributes
line = getLineByName("Fort",data,lineStartFnd,lineEndFnd);
addAttrList(data,line,saveAttr,lineStartFnd,termChars,charId);
//format attributes:
hp = formatAttribute("hp",0,charId);
creLog("parsecore: HP: " + hp,1);
AC = formatAttribute("AC",0,charId);
creLog("parsecore: AC: " + AC,1);
tAC = formatAttribute("touch",0,charId);
creLog("parsecore: touch AC: " + tAC,1);
ffAC = formatAttribute("flat-footed",0,charId);
creLog("parsecore: flat-foot AC: " + ffAC,1);
// determine size
var size = parseSize(data);
creLog("parsecore: size: " + size,1);
addAttribute('Size',size,size,charId);
// save riders
if ((rc=line.indexOf(';')) !== -1) {
rc = '('+line.substring(rc+1)+')';
} else {rc = "";}
addAttributeRoll("INIT",initAttr,false,true,charId,"&{tracker}");
addAttributeRoll("F","Fort",false,true,charId,rc);
addAttributeRoll("R","Ref",false,true,charId,rc);
addAttributeRoll("W","Will",false,true,charId,rc);
};
/**
* Parse Size of the creature
* TODO modify getLineByName and getLineNumberByName to allow regex
*/
var parseSize = function(data) {
var retval = 'Medium';
var lineEndFnd = getLineNumberByName('DEFENSE',data);
var space = getLineByName('Space',data,getLineNumberByName('OFFENSE',data),getLineNumberByName('STATISTICS',data));
creLog('parseSize: space is ' + space);
if (space) {
space = getValueByName('Space',space,[';',',']);
space = getBonusNumber(space,bonusEnum.SCALAR);
space = parseInt(space);
if (isNaN(space))
{return;}
space = space/5;
switch(space) {
case 1:
retval = 'Medium';
break;
case 2:
retval = 'Large';
break;
case 3:
retval = 'Huge';
break;
case 4:
retval = 'Gargantuan';
break;
case 6:
retval = 'Colossal';
break;
default:
retval = 'Medium';
break;
}
} else if (getLineByName('Fine',data,0,lineEndFnd) || getLineByName('fine',data,0,lineEndFnd)) {
retval = 'Fine';
} else if (getLineByName('Diminutive',data,0,lineEndFnd) || getLineByName('diminutive',data,0,lineEndFnd)) {
retval = 'Diminutive';
} else if (getLineByName('Tiny',data,0,lineEndFnd) || getLineByName('tiny',data,0,lineEndFnd)) {
retval = 'Tiny';
} else if (getLineByName('Small',data,0,lineEndFnd) || getLineByName('small',data,0,lineEndFnd)) {
retval = 'Small';
} else if (getLineByName('Medium',data,0,lineEndFnd) || getLineByName('Medium',data,0,lineEndFnd)) {
retval = 'Medium';
} else if (getLineByName('Large',data,0,lineEndFnd) || getLineByName('Large',data,0,lineEndFnd)) {
retval = 'Large';
} else if (getLineByName('Huge',data,0,lineEndFnd) || getLineByName('huge',data,0,lineEndFnd)) {
retval = 'Huge';
} else if (getLineByName('Gargantuan',data,0,lineEndFnd) || getLineByName('gargantuan',data,0,lineEndFnd)) {
retval = 'Gargantuan';
} else if (getLineByName('Colossal',data,0,lineEndFnd) || getLineByName('colossal',data,0,lineEndFnd)) {
retval = 'Colossal';
}
return retval;
};
/**
* parse special attacks, if the statblock has 'riders' which give
* details on the special abilities, then include it as part of the marco.
* TODO: add option for verbrocity during generaton.
*/
var parseSpecials = function(data) {
var retval = {};
var charId = character.get('_id');
var line = "";
var lineStartFnd = 0;
var lineEndFnd = data.length;
var re, saName, abName, sAtks, sAtkStr,
action, actionStr, spList, hasSAtks;
line = getLineByName("Special Attacks",data);
if (line) {
line = line.replace("Special Attacks","");
sAtks = line.split(/,(?![^\(\)]*\))/);
while (sAtks.length > 0) {
if (!sAtks[0] || !sAtks[0].match(/[^\s]+/)) {
sAtks.shift();
continue;
}
saName = sAtks[0].match(/\b[^\d\(\)\+]+/g);
if (saName) {
saName = saName[0].trim();
sAtkStr += saName;
if (!retval[saName])
{retval[saName] = new Array(sAtks[0].trim());}
else
{retval[saName].push(sAtks[0].trim());}
creLog("parseSpecials " + sAtks[0] + " saName: " + saName,1);
}
sAtks.shift();
}
}
/* TODO add in nextLine support for cases where the special ability
name is found on the following line(s) */
lineStartFnd = getLineNumberByName("SPECIAL ABILITIES",data);
if (lineStartFnd) {
for (var i = lineStartFnd; i < data.length; ++i) {
line = data[i];
if (line.match(/\(Su\)|\(Ex\)|\(Sp\)/i)) {
saName = line.substring(0,line.indexOf('(')).toLowerCase().trim();
re = new RegExp(saName,'ig');
action = "!\n" + fields.menuWhis +
line.replace(re,getTermLink(saName,termEnum.GENERAL));
if (!retval[saName])
{retval[saName] = new Array(line.trim());}
else
{retval[saName].push(line.trim());}
addAbility("SA-"+saName,"",action,false,charId);
}
}
}
/* If there is a special attack, that is a special attack not ability,
then it is unique and should get its own ability as well as long-rider
if one exists.*/
spList = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.specialTopImg})
+ menuTemplate.midDiv({imgLink: design.specialMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.specialTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.specialLabelFmt,
title: 'Special Attacks'
})
+'</div>';
/*
// DEPRECATED (exclude special attacks that are melee/ranged riders)
// insure we have melee or ranged
line = getLineByName("Melee",data)
+ getLineByName("Ranged",data) + '';
*/
if (sAtkStr) {
_.every(_.keys(retval), function(sAtks) {
if (/*!line.match(sAtks) &&*/ (sAtkStr.indexOf(sAtks) !== -1)) {
hasSAtks = true;
abName = "SP-" + sAtks;
abName = abName.replace(/\s/g,"-");
action = "!\n" + fields.resultWhis;
_.every(retval[sAtks], function(rider) {
creLog("SP rider: " + rider,3);
re = new RegExp(sAtks,"ig");
actionStr = "<div>"+getFormattedRoll(rider)+"</div>";
action += actionStr.replace(re,getTermLink(sAtks,termEnum.GENERAL));
return true;
});
creLog("SP action: " + action,3);
addAbility(abName,"",action,false,charId);
spList = spList
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: abName,
btnName: sAtks
});
}
return true;
});
}
if (hasSAtks) {
spList = spList
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.specialBotImg});
addAbility("Specials",'',spList,false,charId);
}
return retval;
};
/**
* parse melee and ranged attacks, if there are special attack riders,
* then we will append the marco text
*/
var parseAttacks = function(data,specials) {
var charId = character.get('_id');
var line = "";
var lineStartFnd = 0;
var lineEndFnd = data.length;
var atkMenu, hasSAtk = false, CMB, riders;
atkMenu = fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.atkMenuTopImg})
+ menuTemplate.midDiv({imgLink: design.atkMenuMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.atkMenuTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.atkMenuLabelFmt,
title: 'Attacks'
})
+'</div>';
lineStartFnd = getLineNumberByName("OFFENSE",data);
lineEndFnd = getLineNumberByName("TACTICS",data);
if (!lineEndFnd)
{lineEndFnd = getLineNumberByName("STATISTICS",data);}
try {
line = getLineByName("Melee",data,lineStartFnd,lineEndFnd);
if (line) {
formatAttacks(line,"Melee",charId,"ATK",specials);
atkMenu = atkMenu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'ATK',
btnName: 'Melee'
});
}
line = getLineByName("Ranged",data,lineStartFnd,lineEndFnd);
if (line) {
formatAttacks(line,"Ranged",charId,"RNG",specials);
atkMenu = atkMenu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'RNG',
btnName: 'Ranged'
});
}
hasSAtk = findObjs({
_type: "ability",
name: "Specials",
_characterid: charId
});
if (hasSAtk.length > 0) {
atkMenu = atkMenu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Specials',
btnName: 'Specials'
});
}
lineStartFnd = getLineNumberByName("STATISTICS",data);
line = getLineByName("CMB",data,lineStartFnd);
if (line) {
CMB = getValueByName("CMB",line,[',',';']);
riders = CMB.match(/\(.+\)/);
addAttributeRoll("CMB","CMB",false,false,charId,(riders ? riders:''));
atkMenu = atkMenu
+ menuTemplate.midButton({
riders: riders,
creName: creName,
abName: 'CMB',
btnName: 'CMB'
});
}
} catch (e) {
log ("ERROR when parsing attacks: ");
throw e;
}
atkMenu = atkMenu
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.atkMenuBotImg});
addAbility("Attacks",'',atkMenu,true,charId);
};
/** parse out spells the marco will spit them out, possibly even link
* them to a known PRD search engine.
*/
var parseSpells = function(data) {
var charId = character.get('_id');
var lineEndFnd = data.length;
var casterType, attrName;
var rc, line = "";
var termChars = [';',','];
lineEndFnd = getLineNumberByName("TACTICS",data);
if (!lineEndFnd)
{lineEndFnd = getLineNumberByName("STATISTICS",data);}
formatSpells("Spell-Like Abilities",data,lineEndFnd,termEnum.GENERAL,"SLA");
formatSpells("Spells Known",data,lineEndFnd,termEnum.SPELL);
formatSpells("Spells Prepared",data,lineEndFnd,termEnum.SPELL);
formatSpells("Extracts Prepared",data,lineEndFnd,termEnum.SPELL);
};
/**
* Generic Parse assuming CSV on the line.
*
*/
var parseGeneric = function(generic,data,type,start,end) {
if (!generic || !type)
{return undefined;}
if (!start)
{start = 0;}
if (!end)
{end = data.length;}
var charId = character.get('_id');
var genName, genRiders, genAry,
idx, genList, abName, genLabel;
var lineStartFnd = 0;
var rc, line = "";
var termChars = [';'];
genList = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.genTopImg})
+ menuTemplate.midDiv({imgLink: design.genMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.genTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.genLabelFmt,
title: generic
})
+'</div>';
line = getLineByName(generic,data,start,end);
line = getValueByName(generic,line,termChars);
creLog("parseGeneric: " + line,1);
if (line) {
line = line.replace(generic,"");
genAry = line.split(/,(?![^\(\)]*\))/);
if (genAry) {
_.every(genAry, function(elemGen) {
if ((idx=elemGen.indexOf("(")) !== -1) {
genName = elemGen.substring(0,idx).trim();
genRiders = elemGen.substring(idx).trim();
} else {
genName = elemGen.trim();
genRiders = undefined;
}
genName = formatSuperSubScript(genName);
genList = genList
+ menuTemplate.midLink({
riders: genRiders,
link: getTermLink(genName,type)
});
return true;
});
genList = genList
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.genBotImg});
addAbility(generic,'',genList,false,charId);
}
}
};
/** parse out skills the marco will spit them out, possibly even link
* them to a known PRD search engine.
*/
var parseSkills = function(data) {
var charId = character.get('_id');
var lineStartFnd = 0;
var lineEndFnd = data.length;
var skillName, skillRiders, skillAry,
skillList, abName, skillLabel,
parts, abStr, racialBonus, racialAry,
racialRiders;
var rc, line = "";
var termChars = [';',','];
skillList = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.skillTopImg})
+ menuTemplate.midDiv({imgLink: design.skillMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.skillTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.skillLabelFmt,
title: 'Skills'
})
+'</div>';
lineStartFnd = getLineNumberByName("STATISTICS",data);
line = getLineByName("Skills",data,lineStartFnd);
if (line) {
line = line.replace("Skills","");
skillAry = line.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
if (skillAry.length > 0) {
_.every(skillAry, function(skill) {
if (!skill || !skill.match(/[^\s]+/)) {
creLog("invalid: " + skill,2);
return true;
}
creLog(skill,4);
if ((parts=skill.match(/\b[^\d\+\-/]+/)) !== -1) {
skillName = parts[0].trim();
creLog('parseSkills: skillName: ' + skillName,5);
skillRiders = (skill.substring(skill.indexOf(skillName)+skillName.length)).match(/\(.+\)/);
}
if (skillName.match("Racial Modifiers")) {
creLog("parseSkills: Ending skills, reason : " + skillName,4);
return false;
}
rc = getBonusNumber(skill);
skillName = formatSuperSubScript(skillName);
abName = "SK-" + skillName;
abName = abName.replace(/\s/g,"-");
abStr = "!\n" + fields.resultWhis + creName + ' ' + abName + " [[1d20"+rc+"]]";
addAbility(abName,'',abStr,false,charId);
skillList = skillList
+ menuTemplate.midButton({
riders: skillRiders,
creName: creName,
abName: abName,
btnName: skillName
});
return true;
});
}
if (!characterObjExists('SK-Perception','ability',charId)) {
lineEndFnd = getLineNumberByName('DEFENSE',data);
line = getLineByName('Perception',data,0,lineEndFnd);
rc = getValueByName('Perception',line,termChars);
rc = getBonusNumber(rc,bonusEnum.SIGN);
abName = "SK-Perception";
abStr = "!\n" + fields.resultWhis + creName + ' ' + abName + " [[1d20"+rc+"]]";
addAbility(abName,'',abStr,false,charId);
skillList = skillList
+ menuTemplate.midButton({
riders: skillRiders,
creName: creName,
abName: abName,
btnName: 'Perception'
});
}
skillList = skillList
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.skillBotImg});
addAbility("Skills",'',skillList,false,charId);
}
};
/**
* Given an ability name, augment it (very specifc)
* DEPRECATED
*/
var augmentSkillRoll = function(name, augment, charId) {
var ability,abStr,rollExpr,
newExpr,bonus;
ability = characterObjExists(name,'ability',charId);
if (ability) {
abStr = ability.get('action');
rollExpr = getExpandedExpr('1d20',abStr);
newExpr = rollExpr.replace('1d20','').trim();
bonus = parseInt(newExpr);
bonus = bonus + parseInt(augment) + "";
bonus = getBonusNumber(bonus,bonusEnum.SIGN);
abStr = abStr.replace(rollExpr,'1d20'+bonus);
ability.set('action',abStr);
}
};
/**
* Parse defenses
*/
var parseDefenses = function(data) {
var retVal;
var charId = character.get('_id');
var line = "";
var lineStartFnd = 0;
var lineEndFnd = data.length;
var termChars = [';'];
var excluded = ['SR','DR','Immune','Resist','Weaknesses'];//TODO compensate for missing delimiters (SR on black dragons vs succubus)
var rc = -1;
var i = 0;
var SR,DR,CMD,defenseAb,riders, name,
resist,immune,weak, senses, speed,
regen, aura, fasthealing, aryList, hasDefense=false;
var defenseList;
var fmtLinkFunc = function(arg) {
arg = arg.trim();
var name = arg.match(/\b[^\(\)]+/);
name = formatSuperSubScript(name[0]);
var riders = arg.match(/\(.+\)/);
return menuTemplate.midLinkFree({
riders: riders,
link: getTermLink(name,termEnum.GENERAL)
});
};
var fmtLeadFunc = function(arg) {
var list = arg.split('%%');
if (list.length !== 2)
{throw "ERROR: Bad Arg";}
var label = list[0];
var riders = list[1].match(/\(.+\)/);
var value = list[1].match(/\b[^\(\)]+/);
if (!value)
{value = '—';}
value = value[0].trim();
return menuTemplate.midLeadText({
riders: riders,
label: label,
text: value
});
};
var fmtTextFunc = function(arg) {
var riders = arg.match(/\(.+\)/);
var value = arg.match(/\b[^\(\)]+/);
if (!value)
{value = '—';}
value = value[0].trim();
return menuTemplate.midText({
riders: riders,
text: value
});
};
defenseList = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.genTopImg})
+ menuTemplate.midDiv({imgLink: design.genMidImg})
+ '<div style="text-align:center;">'
+ menuTemplate.titleFmt({
style: design.genTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.genLabelFmt,
title: 'Defenses'
})
+'</div>';
lineStartFnd = getLineNumberByName("DEFENSE",data);
lineEndFnd = getLineNumberByName("OFFENSE",data);
line = getLineByName("DR",data,lineStartFnd,lineEndFnd);
if (line) {DR = getValueByName("DR",line,termChars);}
line = getLineByName("SR",data,lineStartFnd,lineEndFnd);
if (line) {SR = getValueByName("SR",line,termChars);}
line = getLineByName("Immune",data,lineStartFnd,lineEndFnd);
if (line) {immune = getValueByName("Immune",line,termChars);}
line = getLineByName("Resist",data,lineStartFnd,lineEndFnd);
if (line) {resist = getValueByName("Resist",line,termChars);}
line = getLineByName("Defensive Abilities",data,lineStartFnd,lineEndFnd);
if (line) {defenseAb = getValueByName("Defensive Abilities",line,termChars);}
line = getLineByName("Weaknesses",data,lineStartFnd,lineEndFnd);
if (line) {weak = getValueByName("Weaknesses",line,termChars);}
// add Fast Healing
line = getLineByName("fast healing",data,0,lineEndFnd);
if (line) {fasthealing = getValueByName("fast healing",line,termChars);}
// add Regeneration
line = getLineByName("regeneration",data,0,lineEndFnd);
if (line) {regen = getValueByName("regeneration",line,termChars);}
// add CMD
lineStartFnd = getLineNumberByName("STATISTICS",data);
line = getLineByName("CMD",data,lineStartFnd);
if (line) {CMD = getValueByName("CMD",line,[',',';']);}
// add Senses
lineEndFnd = getLineNumberByName("DEFENSE", data);
line = getLineByName("Senses",data,0,lineEndFnd);
if (line) {senses = getValueByName("Senses",line,termChars);}
// add Aura
line = getLineByName("Aura",data,0,lineEndFnd);
if (line) {aura = getValueByName("Aura",line,termChars);}
// add Speed
lineStartFnd = getLineNumberByName("OFFENSE", data);
lineEndFnd = getLineNumberByName("TACTICS",data);
if (!lineEndFnd)
{lineEndFnd = getLineNumberByName("STATISTICS",data);}
line = getLineByName("Speed",data,lineStartFnd,lineEndFnd);
if (line) {speed = getValueByName("Speed",line,termChars);}
if (CMD) {
hasDefense = true;
defenseList = defenseList
+ '<div>'
+ fmtLeadFunc('CMD:%%'+CMD)
+ '</div>';
}
if (regen) {
hasDefense = true;
defenseList = defenseList
+ '<div>'
+ fmtLeadFunc('Regeneration:%%'+regen)
+ '</div>';
}
if (fasthealing) {
hasDefense = true;
defenseList = defenseList
+ '<div>'
+ fmtLeadFunc('Fast Healing:%%'+fasthealing)
+ '</div>';
}
if (DR || SR) {
hasDefense = true;
defenseList += '<div>';
if (DR && SR) {
defenseList = defenseList
+ formatTable(
[('DR:%%'+DR),('SR:%%'+SR)],
2,
fmtLeadFunc);
} else if (DR) {
defenseList = defenseList
+ fmtLeadFunc('DR:%%'+DR);
} else if (SR) {
defenseList = defenseList
+ fmtLeadFunc('SR:%%'+SR);
}
defenseList += '</div>';
}
if (speed) {
hasDefense = true;
aryList = speed.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center;">'
+ '<span style="font-weight: bold; color: #000000;">' + "Speed:" + '</span>'
+ '</div>';
if (aryList.length >= 2) {
defenseList += '<div>' + formatTable(aryList,2,fmtTextFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div>'
+ fmtTextFunc(aryList[i])
+ '</div>';
}
}
}
if (senses) {
hasDefense = true;
aryList = senses.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center;">'
+ '<span style="font-weight: bold; color: #000000;">' + "Senses:" + '</span>'
+ '</div>';
if (aryList.length > 3) {
defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div>'
+ fmtLinkFunc(aryList[i])
+ '</div>';
}
}
}
if (aura) {
hasDefense = true;
aryList = aura.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center;">'
+ '<span style="font-weight: bold; color: #000000;">' + "Aura:" + '</span>'
+ '</div>';
if (aryList.length > 3) {
defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div>'
+ fmtLinkFunc(aryList[i])
+ '</div>';
}
}
}
if (immune) {
hasDefense = true;
aryList = immune.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center;">'
+ '<span style="font-weight: bold; color: #000000;">' + "Immunities:" + '</span>'
+ '</div>';
if (aryList.length > 3) {
defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div>'
+ fmtLinkFunc(aryList[i])
+ '</div>';
}
}
}
if (resist) {
hasDefense = true;
aryList = resist.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center">'
+ '<span style="font-weight: bold; color: #000000;">' + "Resistances:" + '</span>'
+ '</div>';
if (aryList.length > 3) {
defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div style="text-align:center">'
+ fmtLinkFunc(aryList[i])
+ '</div>';
}
}
}
if (weak) {
hasDefense = true;
aryList = weak.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center">'
+ '<span style="font-weight: bold; color: #000000;">' + "Weaknesses:" + '</span>'
+ '</div>';
if (aryList.length > 3) {
defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div style="text-align:center">'
+ fmtLinkFunc(aryList[i])
+ '</div>';
}
}
}
if (defenseAb) {
hasDefense = true;
aryList = defenseAb.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
defenseList = defenseList
+ '<div style="text-align:center">'
+ '<span style="font-weight: bold; color: #000000;">' + "Defensive Abilities:" + '</span>'
+ '</div>';
if (aryList.length > 3) {
defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
} else if (aryList.length > 0) {
for (i = 0; i < aryList.length; i++) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
defenseList = defenseList
+ '<div style="text-align:center">'
+ fmtLinkFunc(aryList[i])
+ '</div>';
}
}
}
defenseList = defenseList
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.genBotImg});
if (hasDefense) {
addAbility("Defenses",'',defenseList,false,charId);
}
};
/**
* Format a table given a minimum column length and a format function for
* the elements.
*/
var formatTable = function(aryList, minCol, fmtFunc) {
if (!aryList || !minCol)
{return undefined;}
if (!fmtFunc)
{fmtFunc = function(arg) {return arg;};}
var retval = '<table style="margin-left: auto; margin-right: auto;">';
var i,j;
for (i = 0; i < aryList.length; i += minCol) {
retval += '<tr>';
if ((aryList.length - i + 1) > minCol) {
for (j = i; j < i + minCol; ++j) {
if (!aryList[j] || !aryList[j].match(/[^\s]+/))
{continue;}
retval += '<td>' + fmtFunc(aryList[j]) + '</td>';
}
} else {
// span the last column
for (j = i; j < aryList.length - 1; ++j) {
if (!aryList[j] || !aryList[j].match(/[^\s]+/))
{continue;}
retval += '<td>' + fmtFunc(aryList[j]) + '</td>';
}
retval += '<td colspan="' + (minCol-(j%minCol)) + '">' + fmtFunc(aryList[aryList.length - 1]) + '</td>';
retval += '</tr>';
break;
}
retval += '</tr>';
}
// clean up remainder here
retval += '</table>';
return retval;
};
/**
* Parse out special abilities (not special attacks) and put them in a
* menu, if there are no special abilities, create no such menu
*/
var parseSpecialMenu = function(data,specials) {
if (!specials)
{return undefined;}
var charId = character.get('_id');
var spMenu, saName, hasSpecials = false;
spMenu = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.genTopImg})
+ menuTemplate.midDiv({imgLink: design.genMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.genTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.genLabelFmt,
title: 'Special Abilities'
})
+'</div>';
_.every(_.keys(specials), function(key) {
saName = 'SA-' + key;
if (characterObjExists(saName,'ability',charId)) {
hasSpecials = true;
spMenu = spMenu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: saName,
btnName: key
});
}
return true;
});
spMenu = spMenu
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.genBotImg});
if (hasSpecials)
{addAbility("Special Abilities",'',spMenu,false,charId);}
};
/**
* Parses Gear
*/
var parseItems = function(data) {
var charId = character.get('_id');
var menu, hasGear = false;
var start, end;
menu = '!\n' + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.genTopImg})
+ menuTemplate.midDiv({imgLink: design.genMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.genTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.genLabelFmt,
title: 'Items'
})
+'</div>';
start = getLineNumberByName('STATISTICS',data);
end = getLineNumberByName('SPECIAL ABILITIES',data);
// parse Combat Gear
parseGeneric("Combat Gear",termEnum.GENERAL,start,end);
if (characterObjExists("Combat Gear","ability",charId)) {
hasGear = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Combat Gear',
btnName: 'Gear-Combat'
});
}
// parse Other Gear
parseGeneric("Other Gear",termEnum.GENERAL,start,end);
if (characterObjExists("Other Gear","ability",charId)) {
hasGear = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Other Gear',
btnName: 'Gear-Other'
});
}
if (!hasGear) {
// parse General Gear
parseGeneric("Gear",termEnum.GENERAL,start,end);
if (characterObjExists("Gear","ability",charId)) {
hasGear = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Gear',
btnName: 'Gear'
});
}
}
menu = menu
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.genBotImg});
if (hasGear)
{addAbility("Items",'',menu,false,charId);}
};
/**
* Parses Tactics
*/
var parseTactics = function(data) {
var charId = character.get('_id');
var menu, hasTactics = false;
var start, end, line;
menu = '!\n' + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.genTopImg})
+ menuTemplate.midDiv({imgLink: design.genMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.genTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.genLabelFmt,
title: 'Tactics'
})
+'</div>';
start = getLineNumberByName('TACTICS',data);
end = getLineNumberByName('STATISTICS',data);
// Before Combat
line = getLineByName('Before Combat',data,start,end);
if (line) {
hasTactics = true;
line = line.replace('Before Combat','<b>Before Combat</b>');
line = '!\n' + fields.privWhis + line;
addAbility('Tactics-Before','',line,false,charId);
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Tactics-Before',
btnName: 'Before Combat'
});
}
// During Combat
line = getLineByName('During Combat',data,start,end);
if (line) {
hasTactics = true;
line = line.replace('During Combat','<b>During Combat</b>');
line = '!\n' + fields.privWhis + line;
addAbility('Tactics-During','',line,false,charId);
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Tactics-During',
btnName: 'During Combat'
});
}
// Morale
line = getLineByName('Morale',data,start,end);
if (line) {
hasTactics = true;
line = line.replace('Morale','<b>Morale</b>');
line = '!\n' + fields.privWhis + line;
addAbility('Tactics-Morale','',line,false,charId);
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Tactics-Morale',
btnName: 'Morale'
});
}
menu = menu
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.genBotImg});
if (hasTactics)
{addAbility("Tactics",'',menu,false,charId);}
};
/**
* parse additional, non-combat abilities
*/
var parseExtra = function(data,specials) {
var charId = character.get('_id');
var menu, hasExtras = false;
menu = fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.genTopImg})
+ menuTemplate.midDiv({imgLink: design.genMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.genTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.genLabelFmt,
title: 'Abilities'
})
+'</div>';
// parse skills
parseSkills(data);
if (characterObjExists("Skills","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Skills',
btnName: 'Skills'
});
}
// parse feats
parseGeneric("Feats",data,termEnum.FEAT);
if (characterObjExists("Feats","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Feats',
btnName: 'Feats'
});
}
// parse SQ
parseGeneric("SQ",data,termEnum.GENERAL);
if (characterObjExists("SQ","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'SQ',
btnName: 'SQ'
});
}
// parse Defenses
parseDefenses(data);
if (characterObjExists("Defenses","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Defenses',
btnName: 'Defenses'
});
}
// parse Tactics
parseTactics(data);
if (characterObjExists("Tactics","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Tactics',
btnName: 'Tactics'
});
}
// parse special abilities
parseSpecialMenu(data,specials);
if (characterObjExists("Special Abilities","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Special Abilities',
btnName: 'Special Abilities'
});
}
// parse items
parseItems(data);
if (characterObjExists("Items","ability",charId)) {
hasExtras = true;
menu = menu
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: 'Items',
btnName: 'Items'
});
}
menu = menu
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.genBotImg});
if (hasExtras)
{addAbility("Abilities",'',menu,true,charId);}
};
/**
* A messy area were we parse exceptions such as Base-statistics and
* other rare things that mess up the uniformity of PRD stat blocks.
*/
var parseOutlier = function() {
// Unimplemented
};
/**
* Format spells
*/
var formatSpells = function(type, data, end, termType, suffix) {
if (!type || !end || !termType)
{return undefined;}
var charId = character.get('_id');
var start, casterType, conAttrName, clAttrName,
sLevel, spells, rc, line, casterLevel,
concentration, spellBook, abName, attrStr;
var termChars = [';',','];
start = getLineNumberByName(type,data);
while (start && !!(line=getLineByName(type,data,start,end))) {
if (line) {
line = getLineByName(type,data,start,end);
casterType = line.substring(0,line.indexOf(type)).trim();
casterType = casterType.replace(/\s+/g,"-");
if (casterType === "")
{casterType = "Base";}
casterType += (suffix ? ("-"+suffix) : "");
casterLevel = getValueByName("CL",line,termChars);
casterLevel = getBonusNumber(casterLevel,bonusEnum.SCALAR);
creLog("SpellFormat: casterType: " + casterType,2);
clAttrName = casterType + "-CL";
addAttribute(clAttrName,casterLevel,casterLevel,charId);
attrStr = "!\n" + fields.resultWhis + creName
+ ': ' + clAttrName + ': [[1d20+'+casterLevel+']]';
addAbility(clAttrName,'',attrStr,false,charId);
concentration = getValueByName("concentration",line,termChars);
concentration = getBonusNumber(concentration,bonusEnum.SIGN);
conAttrName = casterType + "-CON";
addAttribute(conAttrName,concentration,concentration,charId);
if (!concentration) {
addWarning('No concentration bonus found for \''
+ casterType + '\' ' + type + '. A manual'
+ ' prompt has been substituted in place.');
}
attrStr = "!\n" + fields.resultWhis + creName
+ ': ' + conAttrName + ': [[1d20'+(concentration ? concentration:'+?{concentration-bonus}')+']]';
addAbility(conAttrName,'',attrStr,false,charId);
start++;
spellBook = fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.spellBookTopImg})
+ (design.spellBookMidOvr ?
menuTemplate.midDivOverlay({imgLink: design.spellBookMidImg, imgLinkOverlay: design.spellBookMidOvr})
: menuTemplate.midDiv({imgLink: design.spellBookMidImg}))
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.spellBookTitleFmt,
title: casterType
})
+'</div>';
creLog("ParseSpells: start: " + start + " line: " + line,1);
// mow down some lines wherever we see —, stop when we don't see it.
for (var i = start; i < end; ++i) {
line = data[i];
creLog("formatSpells: line " + line,2);
if (line.match("—")) {
spells = line.split("—");
if (spells.length < 2)
{throw "ERROR: Bad spell list format";}
sLevel = spells[0];
spells = spells[1].split(/,(?![^\(\)]*\))/);
creLog("spells sl: " + sLevel + " sp: " + spells,2);
addSpells(casterLevel,sLevel,spells,termType,casterType,charId);
abName = casterType + " " + sLevel;
spellBook = spellBook
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: abName,
btnName: sLevel
});
} else
{break;}
}
if (casterType) {
// put in CL and CON check buttons
spellBook = spellBook
+ '<div>##</div>'
+ '<div style="font-weight: bold;">'
+ menuTemplate.midButtonFree({
riders: undefined,
creName: creName,
abName: clAttrName,
btnName: 'CL'
})
+ '\t'
+ menuTemplate.midButtonFree({
riders: undefined,
creName: creName,
abName: conAttrName,
btnName: 'CON'
})
+ '</div>'
+ (design.spellBookMidOvr ? '</td></tr></table>':'') // table-centering
+ '</div>' // BG
+ menuTemplate.boundryImg({imgLink: design.spellBookBotImg});
abName = casterType;
addAbility(abName,'',spellBook,true,charId);
}
/* Should be safe if we're under the assumption that there is at
lease one ability given the stat block defined that it has abilities
of this type */
start = getLineNumberByName(type,data,start+1,end);
}
}
};
/**
* add spells, be wary of greater/lesser semantics..
*/
var addSpells = function(casterLevel,spellLvl, spellAry, termType, setName, charId) {
if (!spellLvl || !spellAry || !setName || !termType || !charId)
{return undefined;}
var spellName, spellRiders, spellLabel, idx,
abName;
var spellList = "";
spellList = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.spellTopImg})
+ menuTemplate.midDiv({imgLink: design.spellMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.spellTitleFmt,
title: setName
})
+ menuTemplate.titleFmt({
style: design.spellLabelFmt,
title: spellLvl + " CL("+casterLevel+")"
})
+'</div>';
while (spellAry.length > 0) {
if (!spellAry[0] || !spellAry[0].match(/[^\s]+/)) {
spellAry.shift();
continue;
}
spellAry[0] = spellAry[0].trim();
if ((idx=spellAry[0].indexOf("(")) !== -1) {
spellName = spellAry[0].substring(0,idx).trim();
spellRiders = spellAry[0].substring(idx).trim();
} else {
spellName = spellAry[0].trim();
spellRiders = undefined;
}
// elminate badly formatted trailing commas
if (spellName === "" || !spellName)
{throw 'ERROR: bad spell name: ' + spellName;}
// Invert to treat special names, link to the base spell in the label
spellName = formatSuperSubScript(spellName);
spellLabel = formatSpellStrength(spellName);
creLog("spell name: " + spellName + " spellRiders: " + spellRiders,2);
//d20pfsrd uses - rather than %20
spellLabel = spellLabel.replace(/\s+/g,"-");
spellList = spellList
+ menuTemplate.midLinkCaption({
riders: spellRiders,
link: getTermLink(spellLabel,termType,spellName)
});
spellAry.shift();
}
spellList = spellList
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.spellBotImg});
abName = setName + " " + spellLvl;
addAbility(abName,'',spellList,false,charId);
};
/**
* A fancy way of saying, 'deal with greater/lesser/(numearl) for spell names'.
* this is for term linking as the PRD and PFSRD refer to a single page for all
* strength varients.
*/
var formatSpellStrength = function(spellName) {
if (!spellName)
{return undefined;}
var retval = spellName;
var strengths = ["mass","lesser","greater","IX","VIII","VII","VI","IV","V","III","II","I"];
_.every(strengths, function(level) {
if (spellName.indexOf(level) !== -1) {
retval = spellName.replace(level,"").trim();
return false;
}
return true;
});
return retval;
};
/**
* Given a string which we recognize as a legal name (spell/feat/whatever)
* and, which we suspect may have a superscript or subscript; remove it.
*/
var formatSuperSubScript = function(str) {
if (!str)
{return undefined;}
var retval = str;
var cases = ["1st","2nd","3rd","4th","5th","6th","7th","8th","9th",
"UM","TG","UC","APG","MC","D","B"];
var endsWith = function (str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
};
_.every(cases, function(subscript) {
if (endsWith(str,subscript)) {
retval = str.replace(subscript,"").trim();
return false;
}
return true;
});
return retval;
};
/**
* Format attacks
*/
var formatAttacks = function(str, type, charId, label, specials) {
if (!str || !type || !charId)
{return undefined;}
if (!label)
{label = type;}
var volley, attacks, atkList;
var cnt = 0;
var alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
atkList = "!\n" + fields.menuWhis
+ menuTemplate.boundryImg({imgLink: design.atkTopImg})
+ menuTemplate.midDiv({imgLink: design.atkMidImg})
+ '<div style="text-align:center">'
+ menuTemplate.titleFmt({
style: design.atkTitleFmt,
title: creName
})
+ menuTemplate.titleFmt({
style: design.atkLabelFmt,
title: type
})
+'</div>';
str = str.replace(type,"").trim();
str = str.replace(/\band\b/g,',');
volley = str.split(/\bor\b/g);
if (volley.length > 1) {
while (volley.length > 0) {
if (!volley[0] || !volley[0].match(/[^\s]+/)) {
volley.shift();
continue;
}
if (alphabet.length <= 0)
{alphabet.unshift(++cnt);}
attacks = volley[0].split(/,(?![^\(\)]*\))/);
atkList += addAttacks(attacks,charId,label,specials,alphabet[0]);
volley.shift();
alphabet.shift();
}
} else {
attacks = str.split(/,(?![^\(\)]*\))/);
atkList += addAttacks(attacks,charId,label,specials);
}
atkList = atkList
+ '</div>'
+ menuTemplate.boundryImg({imgLink: design.atkBotImg});
addAbility(label,'',atkList,false,charId);
};
/**
* Adds attacks
*/
var addAttacks = function(aryList, charId, label, specials, volley) {
if (!aryList || !label || !charId)
{return undefined;}
var attack, atkName, atkMod,
atkRiders, atkDamage, atkIter,
critRange, atkStr, dmgStr,
abName, iterCnt = 0, atkList = "",
atkTitle;
creLog('addAttacks: ' + aryList + " " + label + " " + volley,1);
for (var i = 0; i < aryList.length; ++i) {
if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
continue;
}
attack = aryList[i].trim();
atkName = attack.match(/\b[^\d\(\)\+\/×]+(?=\+|\-)/);
if (!atkName) {
/* if we don't have an attack name, we either don't have
a modifier or there's a modifier with no name..*/
/* if there is no modifier, the name spans from 0,
to the first paren, absorb the rider into the
name if any; if there's no paren cough up the error ghost.
If there is a modifier, then append a default name*/
if (attack.substring(0,attack.indexOf('(')).match(/\+\-/)) {
atkName = "Basic " + label + " ";
attack = atkName + attack;
} else {
atkName = attack.substring(0,attack.indexOf('(')).trim();
}
} else {
atkName = attack.substring(
0,attack.indexOf(atkName[0])+atkName[0].length).trim();
}
// Ensure there's a damage section
if (attack.match(/\(.+\)/)) {
atkMod = attack.substring(atkName.length,attack.indexOf('(')-1).trim();
atkDamage = attack.substring(attack.indexOf('(')+1,attack.indexOf(')'));
atkRiders = atkMod.match(/\b[^\d\(\)\+\-\/×]+/);
} else {
atkMod = getBonusNumber(attack);
atkDamage = "0";
}
critRange =getCritRange(atkDamage);
atkDamage = formatDamage(atkDamage,specials);
creLog('addAttacks: got damage for ' + attack,1);
// do iteratives:
atkIter = atkMod.split('/');
if (atkIter && atkIter.length > 1) {
while(atkIter.length > 0) {
++iterCnt;
atkTitle = fields.publicName + " attacks with " + atkName + "!";
atkStr = "[[1d20"+(critRange.range<20 ? ("cs>"+critRange.range) : '')
+ atkIter[0] + "]]" + ((critRange.range<20||critRange.multi>2) ?
" (" + ((critRange.range<20 ? (critRange.range + "-20"):"")
+ (critRange.multi>2 ? ((critRange.range<20 ? '/×':'×') + critRange.multi):"") + ")"):"")
+ (!atkRiders ? '' : ( " ["+atkRiders[0].trim() + "]"));
abName = label + (volley ? ("["+volley+"]") : '')
+ "_" + atkName + "(" + iterCnt + ")";
atkStr = "!\n" + fields.publicAnn + applyAtkTemp(fields.tmpAtk,atkTitle,atkStr,atkDamage.damage);
atkStr = atkStr + (atkDamage.rider==="" ? '':('\n'+fields.resultWhis+atkDamage.rider));
addAbility(abName,'',atkStr,false,charId);
atkIter.shift();
atkList = atkList
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: abName,
btnName: abName
});
}
} else {
atkTitle = fields.publicName + " attacks with " + atkName + "!";
atkStr = (atkMod !== '' ? "[[1d20"+(critRange.range<20 ? ("cs>"+critRange.range) : '')
+ atkMod + "]]" : 'auto-hit') + ((critRange.range<20||critRange.multi>2) ?
" (" + ((critRange.range<20 ? (critRange.range + "-20"):"")
+ (critRange.multi>2 ? ((critRange.range<20 ? '/×':'×') + critRange.multi):"") + ")"):"")
+ (!atkRiders ? '' : ( " ["+atkRiders[0].trim() + "]"));
abName = label + (volley ? ("["+volley+"]") : '')
+ "_" + atkName;
atkStr = "!\n" + fields.publicAnn + applyAtkTemp(fields.tmpAtk,atkTitle,atkStr,atkDamage.damage);
atkStr = atkStr + (atkDamage.rider==="" ? '':('\n'+fields.resultWhis+atkDamage.rider));
addAbility(abName,'',atkStr,false,charId);
atkList = atkList
+ menuTemplate.midButton({
riders: undefined,
creName: creName,
abName: abName,
btnName: abName
});
}
creLog('addAttacks: ' + attack + " atkname: '" + atkName + "' atkmod: '" + atkMod + "' atkDam: '"
+ atkDamage + "' atkRiders: '" + atkRiders + "'",1);
}
creLog('addAttacks: finished attacks for ' + label,1);
return atkList;
};
/**
* Gets the crit range from the damage string.
*/
var getCritRange = function(str) {
if (!str)
{return {range: 20, multi: 2};}
var retval, multi = 2,range = 20,tmp = "";
var terms = str.split("/");
if (terms && terms.length > 0) {
for (var i = 0; i < terms.length; ++i) {
if (terms[i].match(/×\d+/)) {
multi = terms[i].match(/\d+/g);
if (!multi)
{multi = 2;}
} else if ((tmp=terms[i].match(/\b\d+\-\d+/))) {
range = tmp[0].match(/\d+/g);
if (!range)
{range = 2;}
}
}
}
retval = {
range: parseInt(range),
multi: parseInt(multi)
};
return retval;
};
/**
* Format the damage string and attach any riders if any. Be careful about
* subrider semantics.
*/
var formatDamage = function(str, specials) {
if (!str)
{return {damage: "", rider: ""};}
var retval;
var damage, damageStr="", damageExpr,
damageTypes,riders,riderStr="",ploc = -1,tmp;
var re = /\d+d\d+/;
creLog("formatDamage str: " + str,2);
damage = re.exec(str);
ploc = str.indexOf('plus');
// if flat, or rider only damage section
if (!damage) {
if (ploc !== -1) {
damageStr = str.substring(0,ploc).match(/\b[\d]+\s/);
if (!damageStr) {
damageStr = str.match(/\b[^\d\/\(\)\+×\s]+\b/);
creLog("damageStr: " + damageStr,3);
if (!damageStr) // give up if things get too weird
{throw "ERROR: Bad damage format: '" + str + "'";}
str = str.substring(0,ploc+4)
+ ' ' + damageStr[0].trim() + ','
+ str.substring(ploc+4);
}
} else if (!!(damageStr = str.match(/\b[^\d\/\(\)\+×\s]+\b/))) {
str = str + " plus " + damageStr[0].trim();
}
damageStr = '[[0d0]]';
} else {
damageExpr = getExpandedExpr(damage[0],str,damage.index).trim();
damageStr = getFormattedRoll(damageExpr);
// handle damage type
damageTypes = str.match(/\b[^\d\/\(\)\+×]+\b/);
if (damageTypes) {
/* In the case where we have a type we need to get rid of the critical
expression */
creLog("formatDamage Types: " + damageTypes,3);
if (!str.match("plus")) {
tmp = str.indexOf('/');
tmp = str.substring(str.indexOf(damageExpr)+damageExpr.length,
(tmp===-1 ? str.length:tmp)).trim();
damageStr += " " + tmp;
} else {
tmp = str.indexOf('/');
tmp = str.substring(str.indexOf(damageExpr)+damageExpr.length,
(tmp===-1 ? ploc:tmp)).trim();
damageStr += " " + tmp;
}
}
}
// handle riders
if (str.match("plus")) {
riders = str.substring(str.indexOf("plus"));
creLog("riders are: " + riders,3);
riders = riders.replace("plus","");
var ary = riders.split(/,(?![^\(\)]*\))/);
var riderName;
var subRiders;
creLog('ary: ' + ary);
while (ary && ary.length > 0) {
if (!ary[0] || !ary[0].match(/[^\s]+/)) {
ary.shift();
continue;
}
riderName = ary[0].match(/\b[^\d\/\(\)\+×]+/);
// resolve unlabed/typed damage riders
if (!riderName) {
riderName = 'untyped';
ary[0] = riderName + ary[0];
} else {
riderName = riderName[0];
}
subRiders = ary[0].replace(riderName,'').trim();
riderName = riderName.toLowerCase().trim();
creLog("ary val: " + ary[0],4);
creLog("subrider: " + subRiders + " riderName: " + riderName,4);
creLog("specials: " + specials[riderName],4);
if (!specials) {
riderStr +=
"<div>"
+ getTermLink(riderName,termEnum.GENERAL)
+ " "+(subRiders.match(/[^\s]+/) ? (" "+getFormattedRoll(subRiders)) : '')
+ "</div>";
} else {
riderStr +=
"<div>"
+ ((specials[riderName] ? getFormattedRoll(getDamageRiderInfo(riderName,specials)) : undefined) || getTermLink(riderName,termEnum.GENERAL))
+ " "+(subRiders.match(/[^\s]+/) ? (" "+getFormattedRoll(subRiders,termEnum.GENERAL)) : '')
+ "</div>";
}
ary.shift();
}
}
creLog("formatDamage: " + damageStr + " riders: " + riderStr,1);
retval = {
damage: damageStr,
rider: riderStr
};
return retval;
};
/**
* Gets all damage rider information
*/
var getDamageRiderInfo = function(riderName,specials) {
var retval = "";
var riderInfo;
var re;
if (specials) {
riderInfo = specials[riderName];
creLog("RiderInfo: " + riderInfo,3);
if (riderInfo) {
while(riderInfo.length > 0) {
re = new RegExp("\\b"+riderName+"\\b",'ig');
riderInfo[0] = riderInfo[0].replace(
re,getTermLink(riderName,termEnum.GENERAL)+" ");
retval += "<p>" + riderInfo[0] + "</p>" ;
creLog("RiderInfoLine: " + retval,3);
riderInfo.shift();
}
}
}
return retval;
};
/**
* Format an attribute by the type desired:
* 0 - scalar, grab the first number
* 1 - pos/neg, grab the first number as well as the sign if any
*/
var formatAttribute = function(name, type, charId) {
if (!name || !charId)
{return undefined;}
if (!type)
{type = 0;}
var retval = 0;
var attr;
attr = getAttribute(name, charId);
if (!attr) {
throw "Error: no attribute found '" + name + "'";
}
retval = attr.get("current");
switch (type) {
case 0:
retval = retval.match(/\d+/g)[0];
break;
case 1:
retval = getBonusNumber(retval);
break;
}
creLog("formatAttribute: " + retval + " n: " + name + " t: " + type + " c: " + charId,2);
attr.set('current',retval);
attr.set('max',retval);
return retval;
};
/**
* Apply attack template
*/
var applyAtkTemp = function(template,titleStr,atkStr,dmgStr) {
if (!template || !titleStr || !atkStr || !dmgStr)
{return undefined;}
creLog("title: " + titleStr + " atk " + atkStr + " dmg " + dmgStr + template,2);
var retval;
if (!template.match(atkEnum.ATTACK) || !template.match(atkEnum.DAMAGE)) {
log("attack template is malformed");
return retval;
}
retval = template.replace('<<'+atkEnum.TITLE+'>>',titleStr);
retval = retval.replace('<<'+atkEnum.ATTACK+'>>',atkStr);
retval = retval.replace('<<'+atkEnum.DAMAGE+'>>',dmgStr);
return retval;
};
/**
* Add an attribute to a character
*/
var addAttribute = function(name, curVal, maxVal, charId) {
if (!curVal)
//{throw 'Cannot add ' + name + ' with no value';}
{curVal = '';}
if (!maxVal)
{maxVal = '';}
creLog("addAttribute: " + fields.charDataPrefix + name + " " + curVal + " " + maxVal + " " + charId,2);
createObj("attribute", {
name: fields.charDataPrefix + name,
current: curVal,
max: maxVal,
characterid: charId
});
};
/**
* Get an attribute from a character
*/
var getAttribute = function(name, charId) {
return findObjs({
_type: "attribute",
name: fields.charDataPrefix + name,
_characterid: charId
})[0];
};
/**
* Add an ability to a character
*/
var addAbility = function(name, desc, action, isTokenAct, charId) {
createObj("ability", {
name: name,
description: desc,
action: action,
istokenaction: isTokenAct,
characterid: charId
});
};
/**
* Add an ability roll from an attribute
*/
var addAttributeRoll = function(name, attrName, isPublic, isTokenAction, charId, rider) {
if (!name,!attr,!charId)
{return undefined;}
if (!isPublic)
{isPublic = false;}
var action = "";
var attr = getAttribute(attrName, charId);
if (!attr) {
throw ("Error: no attribute found '" + name + "'");
}
action = "!\n" + (isPublic ? fields.publicAnn : fields.resultWhis) +
creName + ": " + attrName + ": [[ 1d20"
+ getBonusNumber(attr.get('current')) + (rider ? " "+rider:'') + "]]";
addAbility(name,"",action,isTokenAction,charId);
};
/**
* Given a line containing the most attributes, add them to the
* character
*/
var addAttrList = function(data,line, aryList, startFnd, termChars, charId) {
if (!aryList || !termChars || !charId)
{return undefined;}
if (!line)
{line = data[0];}
if (!startFnd)
{startFnd = 0;}
var rc;
creLog("addAttrList: " + startFnd + " " + termChars + " " + charId + " " + line + " " + (!!aryList),3);
while(aryList.length > 0) {
rc = getValueByName(aryList[0],line,termChars);
if (!rc) {
var nextBestLine = getLineByName(aryList[0],data,startFnd);
rc = getValueByName(aryList[0],nextBestLine,termChars);
if (!rc) {
throw ("ERROR: could not find attribute " + aryList[0]);
}
}
addAttribute(aryList[0],rc,rc,charId);
aryList.shift();
}
};
/**
* Return a link whose format depends on type
*/
var getTermLink = function(str,type,label) {
if (!str || !type)
{return undefined;}
if (!label)
{label = str;}
var retval = str;
switch(type) {
case termEnum.GENERAL:
retval = getFormattedUrl(str,fields.urlTermGeneral,label);
break;
case termEnum.SPELL:
retval = getFormattedUrl(str,fields.urlTermSpell,label);
break;
case termEnum.FEAT:
retval = getFormattedUrl(str,fields.urlTermFeat,label);
break;
case termEnum.SQ:
retval = getFormattedUrl(str,fields.urlTermSQ,label);
break;
case termEnum.SA:
retval = getFormattedUrl(str,fields.urlTermSA,label);
break;
default:
retval = str;
}
creLog("getTermLink: " + retval,3);
return retval;
};
/**
* Return a formatted URL by filling in placeholders. with str and label
*/
var getFormattedUrl = function(str,url,label) {
if (!str || !url)
{return undefined;}
if (!label)
{label = str;}
var retval;
var re = /<<\w+>>/g;
var matches, cond;
creLog("formatting str: '" + str + "' on url: '" + url + "'",4);
if ((matches=url.match(re))) {
while (matches.length > 0) {
creLog("formaturl: " + matches[0],5);
matches[0] = matches[0].replace(/<<|>>/g,"");
if ((cond=matches[0].match(/\D+/))) {
creLog("formaturl word: " + cond[0],5);
switch(cond[0]) {
case urlCondEnum.FULL:
url=url.replace("<<"+urlCondEnum.FULL+">>",str);
break;
case urlCondEnum.LABEL:
url=url.replace("<<"+urlCondEnum.LABEL+">>",label);
break;
default:
creLog("unsupported url subsitution: " + matches[0],1);
}
} else if ((cond=matches[0].match(/\d+/))) {
cond = parseInt(cond);
creLog("formaturl char: " + cond,5);
if (cond < str.length)
{url=url.replace("<<"+matches[0]+">>",str[cond]);}
else
{creLog("illegal string index of " + cond + " in '" + str + "'",1);}
}
matches.shift();
}
}
retval = url;
creLog("formattedUrl: " + retval,4);
return retval;
};
/**
* Get the bonus number in the string, carefully looking for signs to
* preserve negatives and what not. TODO there's a Regex solution to this
* which is way shorter.. also add type for scalars.
*/
var getBonusNumber = function(str,type) {
if (!str)
{return 0;}
if (!type)
{type = bonusEnum.SIGN;}
var retval = 0;
var locStart = 0;
var locEnd = str.length;
var num;
str = str.replace(/\s/g,"");
switch (type) {
case bonusEnum.SCALAR:
num = str.match(/\d+/);
if (num)
{retval = num[0];}
else
{retval = "0";}
break;
case bonusEnum.SIGN:
num = str.match(/\+*\-*\d+/);
if (num) {
if (!num[0].match(/\+|\-/))
{retval = "+" + num[0];}
else
{retval = num[0];}
} else
{retval = "+0";}
break;
default:
// impossible
return undefined;
}
return retval;
};
/**
* Return the string with the roll formatted, this is accomplished by simply
* surrounding roll equations with [[ ]] TODO, should be replaced with a
* single regex
*
*/
var getFormattedRoll = function(str) {
if (!str)
{return "";}
var retval = str;
var re = /\d+d\d+/;
var idx, expr, roll, pre, post;
if (!!(roll=re.exec(str))) {
expr = getExpandedExpr(roll[0],str,roll.index);
idx = str.indexOf(expr);
pre = str.substring(0,idx);
post = str.substring(idx+expr.length);
} else { return str;}
return pre+"[["+expr+"]]"+getFormattedRoll(post);
};
/**
* Return the target expression expanded as far as it logically can span
* within the provided line.
*
* ie: target = 1d20
* locHint = 4
* line = "2+1d20+5+2d4 bla (bla 1d20+8 bla) bla (4d8...) bla bla"
*
* result = 2+1d20+5+2d4
*/
var getExpandedExpr = function(target, line, locHint) {
if (!target || !line)
{return undefined;}
if (!locHint)
{locHint = 0;}
var retval = target;
var expr = target;
var re = /\d|[\+\-]|d/;
var loc = -1, start = 0, end = 0;
if((loc=line.indexOf(target,locHint)) !== -1) {
start = loc;
while (start > 0) {
if (line[start].match(re))
{start--;}
else
{start++;break;}
}
end = loc;
while (end < line.length) {
if (line[end].match(re))
{end++;}
else
{break;}
}
retval = line.substring(start,end);
creLog("getExpandedExpr: '"
+ retval + "' s: " + start + " e: " + end + " t: " + target + " l: " + line,4);
retval = getLegalRollExpr(retval);
}
return retval;
};
/**
* Gets a legal roll expression.
* TODO strip trailing operands +1d6 and such
*/
var getLegalRollExpr = function(expr) {
if (!expr)
{return undefined;}
var retval = expr;
var stray = expr.match(/d/g);
var valid = expr.match(/\d+d\d+/g);
var errMsg = "Illegal expression " + expr;
try {
if (expr.match(/[^\s\d\+-d]/g)
|| !stray
|| !valid
|| (stray.length =! valid.length))
{throw errMsg;}
stray = expr.match(/\+/g);
valid = expr.match(/\d+\+\d+/g);
if (stray && valid
&& (stray.length !== valid.length))
{throw errMsg;}
stray = expr.match(/-/g);
valid = expr.match(/\d+-\d+/g);
if (stray && valid
&& (stray.length !== valid.length))
{throw errMsg;}
} catch (e) {
creLog(e,1);
throw e;
}
//check for leading, trailing, operands
if (retval[0].match(/\+|\-/))
{retval = retval.substring(1);}
if (retval[retval.length-1].match(/\+|\-/))
{retval = retval.substring(0,retval.length-1);}
creLog("getLegalRollExpr: " + retval,4);
return retval;
};
/**
* Given a name, array of lines, and a start/end location, find the first
* line that contains the given name.
*/
var getLineByName = function(strName, aryLines, locStart, locEnd) {
creLog('getLineByName: name ' + strName + ' data ' + !!aryLines,5);
if (!strName || !aryLines)
{return undefined;}
if (!locStart)
{locStart = 0;}
if (!locEnd)
{locEnd = aryLines.length;}
var retval;
creLog("getLineByName: " + strName + " " + locStart + " " + locEnd + " " + (!!aryLines),5);
for (var i = locStart; i < locEnd; ++i) {
if (aryLines[i].indexOf(strName) !== -1) {
retval = aryLines[i];
break;
}
}
creLog('getLineByName: name ' + strName + ' value \'' + retval,5);
return retval;
};
/**
* Given a name, array of lines, and a start/end location, find the first
* line # that contains the given name.
*/
var getLineNumberByName = function(strName, aryLines, locStart, locEnd) {
if (!strName || !aryLines)
{return undefined;}
if (!locStart)
{locStart = 0;}
if (!locEnd)
{locEnd = aryLines.length;}
var retval;
creLog("getLineNumberByName: " + strName + " " + locStart + " " + locEnd + " " + (!!aryLines),5);
for (var i = locStart; i < locEnd; ++i) {
if (aryLines[i].indexOf(strName) !== -1) {
retval = i;
break;
}
}
return retval;
};
/**
* Given a line, name, and terminators return the value, value is
* the the trimed text after the name and before the terminator.
*/
var getValueByName = function(strName, strLine, termChars) {
if (!strLine || !strName || !termChars)
{return undefined;}
var retval;
var loc = -1;
var locTerm = strLine.length;
creLog("getValueByName: " + strName + " " + termChars + " " + strLine,5);
if ((loc=strLine.indexOf(strName)) !== -1) {
for (var i = 0; i < termChars.length; ++i) {
var tmp = strLine.indexOf(termChars[i],loc);
if ((tmp !== -1) && (tmp < locTerm))
{locTerm = tmp;}
}
if (locTerm > loc) {
locTerm = getParenSafeTerm(
strLine,loc,locTerm,termChars);
retval = strLine.substring(loc+strName.length,locTerm);
}
}
return retval;
};
/**
* Get the location of the closest terminator that is paren safe. If there
* are parens. Probably a faster way exists using regex and exec..
*/
var getParenSafeTerm = function(strLine, start, end, termChars) {
var newTerm = -1;
var inParen = 0;
var closeLoc = -1;
var i;
if (start >= end)
{return end;}
for (i = start; i < strLine.length; ++i) {
if (strLine[i] === '(')
{inParen++;}
else if (strLine[i] ===')')
{inParen--;}
if (i >= end) {
if (inParen <= 0)
{return end;}
else if (inParen > 0)
{break;}
}
}
if (inParen <= 0)
{return end;}
// if we found we're in parens
creLog("in parens for " + strLine + " openparens: " + inParen,5);
closeLoc = strLine.indexOf(')',start);
end = strLine.length;
if (closeLoc === -1)
{return end;}
for (i = 0; i < termChars.length; ++i) {
if (-1 === (newTerm=strLine.indexOf(termChars[i],closeLoc)))
{newTerm = strLine.length;}
else if (newTerm < end)
{end = newTerm;}
}
return end;
};
/**
* check if the character object exists, return first match
*/
var characterObjExists = function(name, type, charId) {
var retval;
var obj = findObjs({
_type: type,
name: name,
_characterid: charId
});
creLog("type: " + type + " name: " + name + " charId: " + charId + " retval: " + obj,5);
if (obj.length > 0)
{retval = obj[0];}
return retval;
};
/** removes all occurence of removeStr in str and replaces them with
* replaceWidth
*
* @author Andy W.
*/
var stripString = function(str, removeStr, replaceWith) {
while (str.indexOf(removeStr) !== -1) {
str = str.replace(removeStr, replaceWith);
}
return str;
};
/**
* Cleans the string preserving select special characters and dropping the
* remainder.
*
* @author Andy W.
* @contributor Ken L.
*/
var cleanString = function(strSpecials) {
strSpecials = stripString(strSpecials, "%20", ' ');
strSpecials = stripString(strSpecials, "%22", '"');
strSpecials = stripString(strSpecials, "%29", ')');
strSpecials = stripString(strSpecials, "%28", '(');
strSpecials = stripString(strSpecials, "%2C", ',');
strSpecials = stripString(strSpecials, "%42", '');
strSpecials = stripString(strSpecials, "*", '');
strSpecials = stripString(strSpecials, '\n', '');
strSpecials = stripString(strSpecials, '%3Cbr', '');
strSpecials = stripString(strSpecials, "%09", ' ');
strSpecials = stripString(strSpecials, "%3C", '<');
strSpecials = stripString(strSpecials, "%3E", '>');
strSpecials = stripString(strSpecials, "%23", '#');
strSpecials = stripString(strSpecials, "%3A", ':');
strSpecials = stripString(strSpecials, "%3B", ';');
strSpecials = stripString(strSpecials, "%3D", '=');
strSpecials = stripString(strSpecials, "%D7", '×');
strSpecials = stripString(strSpecials, "%u2018", '');
strSpecials = stripString(strSpecials, "%u2019", '');
strSpecials = stripString(strSpecials, "%u2013", '-');
strSpecials = stripString(strSpecials, "%u2014", '—');
strSpecials = stripString(strSpecials, "%u201C", '“');
strSpecials = stripString(strSpecials, "%u201D", '”');
while (strSpecials.search(/%../) !== -1) {
strSpecials = strSpecials.replace(/%../, "");
}
strSpecials = strSpecials.replace(/<[^<>]+>|<\/[^<>]+>/g,'');
//strSpecials = strSpecials.replace(/<(?:.|\n)*?>/gm, '');
return strSpecials;
};
/**
* Logging, store it
*/
var creLog = function(msg, lvl) {
if (!msg)
{return undefined;}
if (!lvl)
{lvl = 1;}
if (dmesg) {
dmesg.push('['+lvl+']:' + " " + msg);
} else {
dmesg = new Array('['+lvl+']:' + " " + msg);
}
};
/**
* Dump it.
*/
var creLogDump = function(lvl) {
if (dmesg) {
log('--- Dumping log at level ['+lvl+'] ---');
_.every(dmesg, function(line) {
var clvl = line.match(/\d+/);
if (clvl && (parseInt(clvl[0].trim()) <= lvl))
{log(line);}
return true;
});
log('--- Log dump at level ['+lvl+'] complete ---');
} else {log("No log found");}
};
/**
* Add warnings
*/
var addWarning = function(msg) {
if (warn)
{warn.push(msg);}
else
{warn = new Array(msg);}
};
/**
* Send warnings
*/
var sendWarnings = function(token,msg) {
var content = '';
if (warn) {
_.every(warn, function(elem) {
content += '<p>'
+ '<span style="color: #FF9100; font-weight: bold">Warning: </span>'
+ elem + '</p>';
return true;
});
content += (msg ? msg:'');
sendFeedback(content,design.warningImg,token.get('imgsrc'));
}
};
/**
* get warning block
*/
var getWarningBlock = function(msg) {
var content = '';
if (warn) {
_.every(warn, function(elem) {
content += '<p>'
+ '<span style="color: #FF9100; font-weight: bold">Warning: </span>'
+ elem + '</p>';
return true;
});
content += (msg ? msg:'');
//return getFeedbackBlock(content,design.warningImg,token.get('imgsrc'));
return content;
}
};
/**
* Fake message is fake!
*/
var sendFeedback = function(msg,img,tokenImg) {
var content = '/w GM '
+ '<div style="position: absolute; top: 4px; left: 5px; width: 26px;">'
+ '<img src="' + design.feedbackImg + '">'
+ '</div>'
+ msg;
if (tokenImg && img) {
content = content
+ '<div style="position: relative">'
+ '<div style="position: relative; width: 70px; height: 70px; overflow: hidden;">'
+ '<img src="' + tokenImg + '" style="position: relative; width: 70px; height: 70px;">'
+ '</div>'
+ '<div style="position: absolute; top: 0px; width: 70px; height: 70px;">'
+ '<img src="' + img + '" style="position: relative;">'
+ '</div>'
+ '</div>';
}
sendChat(design.feedbackName,content);
};
/**
* Fake message block
*/
var getFeedbackBlock = function(msg,img,tokenImg) {
var content = msg;
if (tokenImg && img) {
content +=
'<div>'
+ '<div style="display: inline-block; position: relative; width: 70px; height: 70px; overflow: hidden;">'
+ '<img src="' + tokenImg + '" style="position: relative; width: 70px; height: 70px;">'
+ '</div>'
+ '<div style="display: inline-block; position: absolute; left: 45px; width: 70px; height: 70px; overflow: hidden;">'
+ '<img src="' + img + '" style="position: relative;">'
+ '</div>'
+ '</div>';
}
return content;
};
/**
* Performs Genesis with a default name other than 'Creature''
*/
var doNameGenesis = function(msg, name) {
if (!msg || !name)
{return undefined;}
var originalDefaultName = fields.defaultName;
fields.defaultName = name;
doGenesis(msg);
fields.defaultName = originalDefaultName;
};
/**
* Performs Genesis granting control to a player
*/
var doPlayerGenesis = function(msg, playerName) {
if (!msg || !playerName)
{return undefined;}
var players;
var name;
var targets = [];
var targetName;
var targetPlayer;
var levDiff = 0;
var minDiff = 255; // good enough
var originalMenuWhis = fields.menuWhis;
var originalResultWhis = fields.resultWhis;
var originalName = fields.publicName;
var originalPublicAnn = fields.publicAnn;
var re = new RegExp(playerName,'i');
players = findObjs({
_type: "player",
});
_.every(players, function(elem) {
name = elem.get('_displayname').toLowerCase();
if (name.match(re)) {
targets.push(elem);
}
return true;
});
if (targets.length < 1) {
sendFeedback('Could not find player: ' + playerName);
return undefined;
} else if (targets.length === 1) {
targetName = targets[0].get('_displayname').toLowerCase();
targetPlayer = targets[0];
} else {
targetName = targets[0].get('_displayname').toLowerCase();
_.every(targets, function(elem) {
name = elem.get('_displayname').toLowerCase();
levDiff = getLev(playerName,name);
if (levDiff < minDiff) {
minDiff = levDiff;
targetName = name;
targetPlayer = elem;
}
return true;
});
}
fields.menuWhis = '/w "'+targetName+'" ';
fields.resultWhis = '';
fields.publicAnn = '';
fields.summoner = targetPlayer;
doGenesis(msg);
var pgenWork = function(args) {
fields.summoner = undefined;
fields.publicAnn = originalPublicAnn;
fields.publicName = originalName;
fields.resultWhis = originalResultWhis;
fields.menuWhis = originalMenuWhis;
}
workList.push({workFunc: pgenWork});
};
var getLev = function(s1, s2, cost_ins, cost_rep, cost_del) {
// discuss at: http://phpjs.org/functions/levenshtein/
// original by: Carlos R. L. Rodrigues (http://www.jsfromhell.com)
// bugfixed by: Onno Marsman
// revised by: Andrea Giammarchi (http://webreflection.blogspot.com)
// reimplemented by: Brett Zamir (http://brett-zamir.me)
// reimplemented by: Alexander M Beedie
// reimplemented by: Rafał Kukawski
// example 1: levenshtein('Kevin van Zonneveld', 'Kevin van Sommeveld');
// returns 1: 3
// example 2: levenshtein("carrrot", "carrots");
// returns 2: 2
// example 3: levenshtein("carrrot", "carrots", 2, 3, 4);
// returns 3: 6
var LEVENSHTEIN_MAX_LENGTH = 255; // PHP limits the function to max 255 character-long strings
cost_ins = cost_ins === undefined ? 1 : +cost_ins;
cost_rep = cost_rep === undefined ? 1 : +cost_rep;
cost_del = cost_del === undefined ? 1 : +cost_del;
if (s1 === s2) {
return 0;
}
var l1 = s1.length;
var l2 = s2.length;
if (l1 === 0) {
return l2 * cost_ins;
}
if (l2 === 0) {
return l1 * cost_del;
}
// Enable the 3 lines below to set the same limits on string length as PHP does
/*if (l1 > LEVENSHTEIN_MAX_LENGTH || l2 > LEVENSHTEIN_MAX_LENGTH) {
return -1;
}*/
// BEGIN STATIC
var split = false;
try {
split = !('0')[0];
} catch (e) {
// Earlier IE may not support access by string index
split = true;
}
// END STATIC
if (split) {
s1 = s1.split('');
s2 = s2.split('');
}
var p1 = new Array(l2 + 1);
var p2 = new Array(l2 + 1);
var i1, i2, c0, c1, c2, tmp;
for (i2 = 0; i2 <= l2; i2++) {
p1[i2] = i2 * cost_ins;
}
for (i1 = 0; i1 < l1 ; i1++) {
p2[0] = p1[0] + cost_del;
for (i2 = 0; i2 < l2; i2++) {
c0 = p1[i2] + ((s1[i1] === s2[i2]) ? 0 : cost_rep);
c1 = p1[i2 + 1] + cost_del;
if (c1 < c0) {
c0 = c1;
}
c2 = p2[i2] + cost_ins;
if (c2 < c0) {
c0 = c2;
}
p2[i2 + 1] = c0;
}
tmp = p1;
p1 = p2;
p2 = tmp;
}
c0 = p1[l2];
return c0;
};
/**
* "And then the GM said, let there be monsters!"
*
* Performs Creature Generation
*/
var doGenesis = function(msg) {
var token;
var content = '';
if (!(msg.selected && msg.selected.length > 0)) {
sendFeedback("no token selected for creature creation");
return;
}
if (msg.selected.length > 1) {
sendFeedback( '<div style="font-weight: bold; color: #7AB6FF; font-size: 150%">'
+ 'Starting group generation</div>');
}
locked = true;
workStart = Date.now();
_.each(msg.selected, function(e) {
var genesisWork = function(e) {
token = getObj('graphic', e._id);
if ((token && (token.get('_subtype') !== 'token')) || !token) {
sendFeedback(
'<span style="font-weight: bold; color: #FF0000;">Invalid Selection</span>'
+ (msg.selected.length > 1 ? ('<div style="color: black; font-style: italic; font-weight: bold;">('+(msg.selected.length-workList.length+1)+'/'+msg.selected.length+')</div>'):'')
);
return;
}
try {
dmesg = undefined;
warn = undefined;
scan(token);
sendFeedback(
(warn ? getWarningBlock():'')
+ '<span style="font-weight: bold; color: '+(warn ? '#FF9100':'#08AF12')+';">'
+ token.get('name')
+ '</span>'
+ ' has been generated ' + (warn ? 'with warnings.':'successfully!')
+ (msg.selected.length > 1 ? ('<div style="color: black; font-style: italic; font-weight: bold;">('+(msg.selected.length-workList.length+1)+'/'+msg.selected.length+')</div>'):''),
(warn ? design.warningImg:design.successImg),
token.get('imgsrc'));
//creLogDump(5);
character = undefined;
} catch (err) {
log("GENESIS ERROR: " + err);
sendFeedback(
(warn ? getWarningBlock():'')
+ '<span style="font-weight: bold; color: #FF0000;">'
+ 'There was an error during token generation.'
+ '</span> '
+ 'Please see the log for details, and delete the erroneous journal entry.'
+ (msg.selected.length > 1 ? ('<div style="color: black; font-style: italic; font-weight: bold;">('+(msg.selected.length-workList.length+1)+'/'+msg.selected.length+')</div>'):''),
design.errorImg,
token.get("imgsrc"));
creLogDump(debugLvl);
warn = undefined;
character = undefined;
log("-----Please ensure that the statistics block is properly formatted.-----");
}
};
workList.push({workFunc: genesisWork, workData: [e]});
},this);
if (msg.selected.length > 1) {
workList.push({workFunc: function() {
var tTime = (Date.now() - workStart)/1000;
sendFeedback(
'<div style="font-weight: bold; color: #08AF12; font-size: 150%">'
+ 'Group generation complete </div>'
+ '<div style="color: blue; font-size: 100%; font-style: italic: font-weight: bold">(Time elapsed: '+tTime.toFixed(2)+'s)</div>');
locked = false;
}, workData: []});
} else {
locked = false;
}
doDelayedWork(workList);
};
/**
* Delayed Worktask
*/
var doDelayedWork = function() {
if (!workList || !workList.length)
{return;}
var payload = workList.shift();
if (!payload)
{return;}
payload.workFunc.apply(undefined,payload.workData);
setTimeout(doDelayedWork,workDelay);
};
/**
* Show help
*/
var showHelp = function() {
var content;
var designTmpList = "";
var attackTmpList = "";
if (typeof(CGTmp) !== "undefined") {
try {
_.every(_.keys(CGTmp.designTmp), function(tmp) {
designTmpList += '<div>' + '<a href="!CreatureGen -set-design ' + tmp + '">' + tmp + ' </a></div>';
return true;
});
_.every(_.keys(CGTmp.attackTmp), function(tmp) {
attackTmpList += '<div>' + '<a href="!CreatureGen -set-attack ' + tmp + '">' + tmp + ' </a></div>';
return true;
});
} catch (ex) {
log("ERROR accessing CGTmp: " + e);
designTmpList = "";
attackTmpList = "";
}
}
content = '<div style="background-color: #FFFFFF; border: 1px solid black; left-margin 5x; right margin 5px; padding-top: 5px; padding-bottom: 5px;;">'
+ '<div style="border-bottom: 1px solid black;">'
+ '<span style="font-weight: bold; font-size: 150%">CreatureGen v'+version+'</span>'
+ '</div>'
+ '<div style="padding-left: 10px; padding-right: 10px;">'
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen -help</span>'
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Display this message'
+ '</li>'
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen</span>'
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Generate creature'
+ '</li>'
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen -name [name]</span>'
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Generate creature with the given default name'
+ '</li>'
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen -player [name]</span>'
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Generate player controlled creature (a summon)'
+ '</li>'
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen -set-design [name]</span>'
+ '</div>'
+ '<div style="float: right; margin-left 2px; padding-top: 2px; padding-bottom: 2px; padding-left: 2px; padding-right: 2px; border: 1px solid black; background-color: #89FEBA; text-align: center; font-weight: bold;">'
+ (state.cgen_design ? state.cgen_design : "Default")
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Set design template'
+ '</li>'
+ (designTmpList==='' ? '' : '<div style="border: 1px solid blue; text-align: center;"><span style="text-decoration: underline;">Available design templates:</span>')
+ designTmpList
+ (designTmpList==='' ? '' : '</div>')
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen -set-attack [name]</span>'
+ '</div>'
+ '<div style="float: right; margin-left 2px; padding-top: 2px; padding-bottom: 2px; padding-left: 2px; padding-right: 2px; border: 1px solid black; background-color: #89FEBA; text-align: center; font-weight: bold;">'
+ (state.cgen_attack ? state.cgen_attack : "Default")
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Set attack template'
+ '</li>'
+ (attackTmpList==='' ? '' : '<div style="border: 1px solid blue; text-align: center;"><span style="text-decoration: underline;">Available attack templates:</span>')
+ attackTmpList
+ (attackTmpList==='' ? '' : '</div>')
+ '<div>'
+ '<span style="font-weight: bold;">!CreatureGen -dmesg [lvl]</span>'
+ '</div>'
+ '<li style="padding-left: 10px;">'
+ 'Dump to API-output console debug at the specified level (1-5) for the last token generated.'
+ '</li>'
+ '</div>'
+ '</div>'
+ '</div>';
sendFeedback(content);
};
/**
* Handle chat messages
*/
var handleChatMessage = function(msg) {
var cmdName = "!CreatureGen";
var msgTxt = msg.content;
var args;
if ((msg.type === "api")
&& (msgTxt.indexOf(cmdName) !== -1)
&& playerIsGM(msg.playerid)) {
args = msgTxt.replace(cmdName,'').trim().toLowerCase();
if (args !== "") {
if (locked) {
sendFeedback('<span style="color: #FF8D0B; font-weight: bold;">'
+ ' BUSY </span>');
} else if (args.indexOf('-set-design') === 0) {
args = args.replace('-set-design','').trim();
selectDesignTemplate(args);
} else if(args.indexOf('-set-attack') === 0) {
args = args.replace('-set-attack','').trim();
selectAttackTemplate(args);
} else if (args.indexOf('-help') === 0) {
showHelp();
} else if (args.indexOf('-dmesg') === 0) {
var level = 0;
args = args.replace('-dmesg','').trim();
level = getBonusNumber(args,bonusEnum.SCALAR);
creLogDump(level);
sendFeedback('<span style="color: #FF8D0B;">'
+ 'Dumping debug from last <b>GENESIS</b> at level ('+level+')'
+ '</span>');
} else if (args.indexOf('-player') === 0) {
args = args.replace('-player','').trim();
doPlayerGenesis(msg,args);
} else if (args.indexOf('-name') === 0) {
args = args.replace('-name','').trim();
doNameGenesis(msg,args);
} else {
sendFeedback("Unknown CreatureGen command '"+args+"'");
showHelp();
}
} else if (locked) {
sendFeedback('<span style="color: #FF8D0B; font-weight: bold;">'
+ ' BUSY </span>');
} else {
doGenesis(msg);
}
}
};
/**
* Handle new graphics added
*/
var handleAddGraphic = function(obj) {
var type;
var charSheet;
var charId;
if (!!(type=obj.get('_subtype'))) {
if (type === 'token') {
charSheet = obj.get('represents');
if (charSheet) {
charSheet = getObj("character", charSheet);
if (charSheet) {
prepToken(obj,charSheet);
}
}
}
}
};
return {
/**
* Register Roll20 handlers
*/
registerAPI : function() {
on('chat:message',handleChatMessage);
on('add:graphic',handleAddGraphic);
},
init: function() {
var rc = true;
fields.tmpAtk = atkTemplate;
if (state.cgen_design)
{rc = selectDesignTemplate(state.cgen_design.toLowerCase(),true);}
if (!rc) {
log("ERROR: state design template no longer exists, defaulting..");
state.cgen_design = undefined;
}
if (state.cgen_attack)
{rc = selectAttackTemplate(state.cgen_attack.toLowerCase(),true);}
if (!rc) {
log("ERROR: state attack template no longer exists, defaulting..");
state.cgen_attack = undefined;
}
}
};
}());
on("ready", function() {
'use strict';
CreatureGenPF.init();
CreatureGenPF.registerAPI();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment