· 8 years ago · Jun 29, 2018, 02:54 AM
1/**
2 *
3 * Copyright (C) 2015 Ken L.
4 * Licensed under the GPL Version 3 license.
5 * http://www.gnu.org/licenses/gpl.html
6 *
7 * Contributors:
8 * Andy W.
9 * Shu Zong C.
10 * Carlos R. L. Rodrigues
11 *
12 *
13 * This script is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation, either version 3 of the License, or
16 * (at your option) any later version.
17 *
18 * This script is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
24 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
25 *
26 * This script is designed to parse stat blocks from the pathfinder PRD which are
27 * formatted in a very particular way. Know that there will be edge cases which
28 * are not handled due to the large varity of conditions.
29 *
30 * It is charactersheet agnostic as it simply creates ability fields and marcos for
31 * the provided statblock. It is also light-weight simply (if enabled) linking or
32 * reminding a GM of information or abilities a creature has.
33 *
34 * If copying stat blocks from PDFs or other Paizo or Paizo compatible sources,
35 * ensure that they fit the PRD specified layout (1 line per ability etc), when
36 * in doubt, copy it into a text editor and confirm what you copied fits the PRD
37 * format.
38 *
39 * If all else fails, it's probably an edge case I didn't consider; or the stat
40 * block is malformed. Feel free to patch up the holes and commit to the git-hub to
41 * make the script better for everyone.
42 *
43 * https://github.com/Roll20KenL/Roll20_API_Scripts/blob/master/cgen.js
44 *
45 */
46
47var CreatureGenPF = (function() {
48 'use strict';
49 var version = 1.32,
50 author = "Ken L.",
51 contributers = "Andy W., Shu Zong C., Carlos R. L. Rodrigues",
52 debugLvl = 1,
53 locked = false,
54 unitPixels = 70,
55 workStart = 0,
56 workDelay = 250,
57 workList = [],
58 dmesg,
59 warn,
60 creName,
61 character;
62
63
64 var termEnum = Object.freeze({
65 GENERAL : 1,
66 MONABILITY: 2,
67 SPELL: 3,
68 FEAT: 4,
69 SQ: 5,
70 SA: 6,
71 SKILL: 7
72 });
73
74 var bonusEnum = Object.freeze({
75 SCALAR: 1,
76 SIGN: 2
77 });
78
79 var urlCondEnum = Object.freeze({
80 FULL: "FULL",
81 LABEL: "LABEL"
82 });
83
84 var atkEnum = Object.freeze({
85 TITLE: 'TITLE',
86 ATTACK: 'ATTACK',
87 DAMAGE: 'DAMAGE'
88 });
89
90 /**
91 * Various fields that can be changed for customization
92 *
93 */
94 var fields = {
95 defaultName: "Creature",
96 charDataPrefix: "CGen_",
97 publicName: "@{CGen_name}",
98 publicEm: "/emas ",
99 publicAtkEm: "/as Attack ",
100 publicDmgEm: "/as Damage ",
101 publicAnn: "/desc ",
102 privWhis: "/w GM ",
103 menuWhis: "/w GM ",
104 resultWhis: "/w GM ",
105 urlTermGeneral: '<a href="http://www.google.com/cse?cx=006680642033474972217%3A6zo0hx_wle8&q=<<FULL>>"><span style="color: #FF0000; font-weight: bold;"><<LABEL>></span></a>',
106 //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>',
107 urlTermMonAbility: '<a href="http://paizo.com/pathfinderRPG/prd/additionalMonsters/universalMonsterRules.html#<<FULL>>"><span style="color: #2F2F2F; font-weight: bold;"><<FULL>></span></a>',
108 urlTermSpell: '<a href="http://www.d20pfsrd.com/magic/all-spells/<<0>>/<<FULL>>"><span style="color: #2E39FF; font-weight: bold;"><<LABEL>></span></a>',
109 urlTermFeat: '<a href="http://www.google.com/cse?cx=006680642033474972217%3A6zo0hx_wle8&q=<<FULL>>"><span style="color: #29220A; font-weight: bold;"><<FULL>></span></a>',
110 urlTermSQ: "", // unused
111 urlTermSA: "", // unused
112 summoner: undefined,
113 shortAtkRiders: false // unused
114 };
115
116 /**
117 * div border styles, customize it for your game!
118 *
119 * <span [[format text injected here]]> penguins </span>
120 * <img src="[[image link]]">"
121 */
122 var design = {
123 feedbackName: 'Ken L.',
124 feedbackImg: 'https://s3.amazonaws.com/files.d20.io/images/3466065/uiXt3Zh5EoHDkXmhGUumYQ/thumb.jpg?1395313520',
125 errorImg: 'https://s3.amazonaws.com/files.d20.io/images/7545187/fjEEs0Jvjz1uy3mGN5A_3Q/thumb.png?1423165317',
126 warningImg: 'https://s3.amazonaws.com/files.d20.io/images/7926480/NaBmVmKe94rdzXwVnLq0-w/thumb.png?1424965188',
127 successImg: 'https://s3.amazonaws.com/files.d20.io/images/7545189/5BR2W-XkmeVyXNsk-C8Z6g/thumb.png?1423165325',
128
129 skillTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
130 skillLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
131 skillTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816000/-QKJOJF6UFmDAEypDGeXcw/thumb.png?1424460624',
132 skillMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816002/nKEWcTQ2VjcRJqrfiMHg_w/thumb.png?1424460630',
133 skillBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816003/lcNufMB1JeLmdDVPJ0KFdQ/thumb.png?1424460634',
134
135 spellBookTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
136 spellBookLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
137 spellBookTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7834797/n4P8Ys6gHoKmVlBTRYoi9Q/thumb.png?1424540093',
138 spellBookMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7834799/LOJaPHkZcUDyekpMnm0-Cg/thumb.png?1424540096',
139 spellBookMidOvr: 'https://s3.amazonaws.com/files.d20.io/images/7836432/fi7Bw5cRaMfq0fjBZZ7ggg/thumb.png?1424544220',
140 spellBookBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7834800/ORBbrVuMO7Ns2UoPRmvfRg/thumb.png?1424540099',
141
142 spellTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
143 spellLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
144 spellTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7382439/dJ-jaPdm6kEtl9lE__ve-g/thumb.png?1422405849',
145 spellMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7382437/FYBtfPgkj4b3Q_hLkUk5Gg/thumb.png?1422405847',
146 spellBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7382435/DXm7UUdLKNnF6ktFfVojWA/thumb.png?1422405843',
147
148 specialTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
149 specialLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
150 specialTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816913/PsSBFlCv3IlPzGP0usiVBw/thumb.png?1424464282',
151 specialMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816781/0g1qpdJ80DTvpSHqQ2-7oA/thumb.png?1424463781',
152 specialBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816784/aZ1OFXDsO2BUM5G9oiXpgw/thumb.png?1424463791',
153
154 atkTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
155 atkLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
156 atkTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816913/PsSBFlCv3IlPzGP0usiVBw/thumb.png?1424464282',
157 atkMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816781/0g1qpdJ80DTvpSHqQ2-7oA/thumb.png?1424463781',
158 atkBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816784/aZ1OFXDsO2BUM5G9oiXpgw/thumb.png?1424463791',
159
160 atkMenuTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
161 atkMenuLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
162 atkMenuTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816912/WWQxo9xFhc-vETYw79f0wA/thumb.png?1424464275',
163 atkMenuMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816781/0g1qpdJ80DTvpSHqQ2-7oA/thumb.png?1424463781',
164 atkMenuBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816783/m3UhKz4plb_0TB1kUOAI1Q/thumb.png?1424463787',
165
166 genTitleFmt: 'style="color: #000000; font-weight: bold; font-size: 125%;"',
167 genLabelFmt: 'style="color: #44824C; font-weight: bold; font-size: 110%;"',
168 genTopImg: 'https://s3.amazonaws.com/files.d20.io/images/7816014/jpO1FF9pA7Ipi__sFQAAMw/thumb.png?1424460680',
169 genMidImg: 'https://s3.amazonaws.com/files.d20.io/images/7816016/0XT65Rctt1imgamKQUO6sg/thumb.png?1424460684',
170 genBotImg: 'https://s3.amazonaws.com/files.d20.io/images/7816023/T6uTWEX4aMDL-78lhSLanw/thumb.png?1424460752',
171 };
172
173 var atkTemplate = '<div class="img" style="column-gap: 0; line-height: 0; position: relative; text-align: center;">'
174 + '<img src="'+"https://s3.amazonaws.com/files.d20.io/images/7926600/1gnY_LsAt4UHnkJmI3PAlA/thumb.png?1424966062"+'">'
175 + '</div>'
176 + '<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;">'
177 + '<div>'
178 + '<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">'
179 + '<span style="font-weight: bold;"><<TITLE>></span>'
180 + '</div>'
181 + '<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;">'
182 + '<table style="width: 140px; text-align: left; margin-left: auto; margin-right: auto; border-spacing: 5;">'
183 + '<tr>'
184 + '<td>' + 'Attack:' + '</td>'
185 + '<td>' + '<<ATTACK>>' + '</td>'
186 + '</tr>'
187 + '<tr>'
188 + '<td>' + 'Damage:' + '</td>'
189 + '<td>' + '<<DAMAGE>>' + '</td>'
190 + '</tr>'
191 + '</table>'
192 +' </div>'
193 + '</div>'
194 + '</div>'
195 + '<div class="img" style="column-gap: 0; line-height: 0; position: relative; text-align: center;">'
196 + '<img src="'+"https://s3.amazonaws.com/files.d20.io/images/7926602/lMjFWWNyFpUlbk80HqkeOQ/thumb.png?1424966073"+'">'
197 + '</div>';
198
199 var menuTemplate = {
200 boundryImg: _.template('<div class="img" style="column-gap: 0; line-height: 0; position: relative; text-align: center;">'
201 + '<img src="<%= imgLink %>">'
202 + '</div>'),
203 titleFmt: _.template('<div>'
204 + '<span <%= style %> >'
205 + '<%= title %>'
206 + '</span>'
207 + '</div>'),
208 midDiv: _.template('<div style="background-image: url('
209 + '<%= imgLink %>'
210 + '); background-repeat: repeat-y; background-position: center center; text-align: center;">'),
211 /*
212 midDivOverlay: _.template('<div style="background: url(<%= imgLink %>) center center no-repeat; pointer-events: none;'
213 + 'position: absolute; left: 0; top: 0; width: 200px; height: 200px;'
214 + ' text-align: center;">'),
215 */
216 midDivOverlay: _.template('<div style="background: '
217 + 'url(<%= imgLinkOverlay %>) no-repeat center center,'
218 + 'url(<%= imgLink %>) repeat-y center center;'
219 + ' text-align: center; min-height: 150px;">'
220 + '<table style="width: 100%; height: 100%; text-align: center; vertical-align: middle; min-height: 150px;">'
221 + '<tr><td style="text-align: center; vertical-align: middle;">'),
222 midButton: _.template('<div style="text-align: center;">'
223 + '<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\' %>'
224 + '<%= \'<a href="!\' + \'&\'+\'#37\' + \';{\'+creName+\'|\'+abName+\'}">\'+btnName+\'</a>\' %>'
225 + '</span>'
226 + '</div>'),
227 midButtonFree: _.template('<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\' %>'
228 + '<%= \'<a href="!\' + \'&\'+\'#37\' + \';{\'+creName+\'|\'+abName+\'}">\'+btnName+\'</a>\' %>'
229 + '</span>'),
230 midLink: _.template('<div style="text-align: center;">'
231 + '<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\' %>'
232 + '<%= link %>'
233 + '</span>'
234 + '</div>'),
235 midLinkFree: _.template('<%= \'<span \'+ (riders ? (\'class="showtip tipsy" title="\' + riders + \'"\') : \'\') + \' style="font-weight: bold;">\''
236 + '+ link +'
237 + '\'</span>\' %>'),
238 midLinkCaption: _.template('<div style="text-align: center;">'
239 + '<%= link %>'
240 + '<%= (riders ? (\'<span style="color: #2B2B2B; font-weight: lighter; font-style: italic; font-size: 70%;">\' + riders + \'</span>\'):"") %>'
241 + '</div>'),
242 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;">\') %>'
243 + '<%= label %>'
244 + '</span> <span style="color: #000000;"><%= text %></span>'),
245 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;"\') + \'>\' %>'
246 + '<%= text %>'
247 +'</span>'),
248 };
249
250
251
252 /**
253 * Select design template
254 */
255 var selectDesignTemplate = function(name, quiet) {
256 var flag;
257 if (typeof(CGTmp) !== "undefined") {
258 try {
259 for (var tmp in CGTmp.designTmp) {
260 if (tmp.toLowerCase() === name) {
261 design = CGTmp.designTmp[tmp];
262 if (!quiet) {
263 sendFeedback('<span style="color: #653200; font-weight: bold;">Design template:</span> <b style="color: #009C26;">' + tmp + '</b> has been '
264 + 'configured for future tokens generated.</span>');
265 }
266 state.cgen_design = tmp;
267 flag = true;
268 break;
269 }
270 }
271 if (!flag)
272 {sendFeedback('<span style="color: #990004;">Design template: \'' + name + '\' does not exist</span>');}
273 } catch (e) {
274 log(e);
275 }
276 } else {
277 sendFeedback('<span style="color: #990004;">No CreatureGen templates found, '
278 + 'did you load the additional templates script?</span>');
279 }
280 return flag;
281 };
282
283 /**
284 * Select attack template
285 */
286 var selectAttackTemplate = function(name, quiet) {
287 var flag;
288 if (typeof(CGTmp) !== "undefined") {
289 try {
290 for (var tmp in CGTmp.attackTmp) {
291 if (tmp.toLowerCase() === name) {
292 fields.tmpAtk = CGTmp.attackTmp[tmp];
293 if (!quiet) {
294 sendFeedback('<span style="color: ##155391; font-weight: bold;">Attack template:</span> <b style="color: #009C26;">' + tmp + '</b> has been '
295 + 'configured for future tokens generated.</span>');
296 }
297 state.cgen_attack = tmp;
298 flag = true;
299 break;
300 }
301 }
302 if (!flag)
303 {sendFeedback('<span style="color: #990004;">Attack template: \'' + name + '\' does not exist</span>');}
304 } catch (e) {
305 log(e);
306 }
307 } else {
308 sendFeedback('<span style="color: #990004;">No CreatureGen templates found, '
309 + 'did you load the additional templates script?</span>');
310 }
311 return flag;
312 };
313
314 /**
315 * Object fix to resolve firebase errors
316 *
317 * @author Shu Zong C.
318 */
319 var fixNewObject = function(obj) {
320 var p = obj.changed._fbpath;
321 if(p !== undefined) {
322 var new_p = p.replace(/([^\/]*\/){4}/, "/");
323 obj.fbpath = new_p;
324 }
325 return obj;
326 };
327
328 /**
329 * scan in information from token notes
330 *
331 * @contribuitor Andy W.
332 */
333 var scan = function(token) {
334 var charSheet;
335 var data;
336 var rawData;
337 var dispData = "";
338
339 if (!token) {
340 throw "No Token selected";
341 }
342 rawData = token.get("gmnotes");
343 creLog('RAW: ' + rawData);
344 if (!rawData)
345 {throw "no token notes";}
346 data = rawData.split(/%3Cbr%3E|\\n|<br>/);
347
348 //clean out all other data except text
349 for (var i = data.length; i >= 0; i--) {
350 if (data[i]) {
351 data[i] = cleanString(data[i]).trim();
352 if (!data[i].match(/[^\s]/)) {
353 data.splice(i,1);
354 }
355 }
356 }
357
358 // Essential parameters for object creation (mainly the name)
359 parseEssential(data);
360 // if character name already exists, close out.
361 if (findObjs({
362 _type: "character",
363 name: creName,
364 }).length > 0) {
365 addWarning('Character \''+creName+'\' already exists.');
366 throw 'Character \''+creName+'\' already exists.';
367 }
368
369 dispData = formatDisplay(data);
370
371 charSheet = createObj("character", {
372 avatar: token.get("imgsrc"),
373 name: creName,
374 gmnotes: '',
375 archived: false,
376 inplayerjournals: '',
377 controlledby: ''
378 });
379 charSheet = fixNewObject(charSheet);
380 if (!charSheet) {
381 throw "ERROR: could not create character sheet";
382 }
383 if (fields.summoner) {
384 charSheet.set('bio','<br clear="both">' + dispData);
385 }
386
387 token.set("represents",charSheet.get('_id'));
388 charSheet.set('gmnotes',dispData);
389 character = charSheet;
390
391 // warn on image source
392 if (charSheet.get('avatar') === '') {
393 addWarning('Unable to set avatar to character journal, only images you\'ve '
394 + 'uploaded yourself are viable during creation. Auto-population '
395 + '<i>(drag-drop population)</i> will not be possible without an avatar image.'
396 + ' You can still upload an avatar manually, or drag an image into the avatar field'
397 + ' from the image-search.');
398 }
399
400
401 // parse up our data set.
402 var specials;
403
404 try {
405 parseCore(data);
406 specials = parseSpecials(data);
407 parseAttacks(data,specials);
408 parseSpells(data);
409 parseExtra(data,specials);
410 prepToken(token,charSheet);
411
412 } catch (e) {
413 log("ERROR when parsing");
414 throw e;
415 }
416 }
417
418 /**
419 * Format display of stat-block
420 */
421 var formatDisplay = function(datum) {
422 if (!datum)
423 {return undefined;}
424 var content = '';
425
426 _.each(datum, function(e,i,l) {
427 creLog('('+i+') ' + e,1);
428 if (e.match('DEFENSE')
429 || e.match('OFFENSE')
430 || e.match('TACTICS')
431 || e.match('BASE STATISTICS')
432 || e.match('STATISTICS')
433 || e.match('ECOLOGY')
434 || e.match('SPECIAL ABILITIES'))
435 {content += '<div style="font-size: 112%; border-bottom: 1px solid black; border-top: 1px solid black; margin-top: 8px;">'+e+'</b></div>';}
436 else if (e.match(/\(Ex\)|\(Su\)|\(Sp\)/i))
437 {content += '<div>' + e + '</div>';}
438 else
439 {content += e+'<br>';}
440 });
441 return content;
442 };
443
444 /**
445 * Prep the token.
446 * Asynchronous
447 */
448 var prepToken = function(token,character) {
449 if (!token || !character)
450 {return undefined;}
451 var name,AC,hp,prep;
452 var charId = character.get('_id');
453
454 hp = getAttribute("hp", charId);
455
456 AC = getAttribute("AC", charId);
457
458 prep = getAttribute("CGEN", charId);
459
460 name = character.get('name');
461
462 // Fast vs cb delay
463 if (token.get('gmnotes')) {
464 if (hp && AC && name && prep
465 && (prep.get('current').match(/true/i))) {
466 hp = hp.get('current');
467 AC = AC.get('current');
468 token.set('bar1_value',hp);
469 token.set('bar1_max', hp);
470 token.set('bar3_value',AC);
471 token.set('name',name);
472 token.set('showname',true);
473 token.set('light_hassight',true);
474 resizeToken(token,character);
475 }
476 } else {
477 character.get('gmnotes',function(notes) {
478 if (hp && AC && name && prep
479 && (prep.get('current').match(/true/i))
480 && notes && (notes !== '')) {
481 hp = hp.get('current');
482 AC = AC.get('current');
483 token.set('bar1_value',hp);
484 token.set('bar1_max', hp);
485 token.set('bar3_value',AC);
486 token.set('name',name);
487 token.set('showname',true);
488 token.set('light_hassight',true);
489 token.set('gmnotes', notes
490 .replace(/<div[^<>]*>/g,'')
491 .replace(/<\/div>/g,'<br>'));
492 resizeToken(token,character);
493 }
494 });
495 }
496 };
497
498 /**
499 * Resize Token based on size attribute
500 */
501 var resizeToken = function(token,character) {
502 var charId = character.get('_id');
503 var unitSize = 1;
504 var pageScale = getObj('page',token.get('_pageid')).get('snapping_increment');
505 var tsize = parseInt(unitPixels*(pageScale===0 ? 1:pageScale));
506 var size = getAttribute("Size", charId);
507 if (!size)
508 {return;}
509
510 switch (size.get('current')) {
511 case 'Fine':
512 case 'Diminutive':
513 case 'Tiny':
514 case 'Small':
515 case 'Medium':
516 break;
517 case 'Large':
518 unitSize = 2;
519 break;
520 case 'Huge':
521 unitSize = 3;
522 break;
523 case 'Gargantuan':
524 unitSize = 4;
525 break;
526 case 'Colossal':
527 unitSize = 6;
528 break;
529 default:
530 creLog('resizeToken: Bad size \''+size+'\' ');
531 }
532 token.set('width',tsize*unitSize);
533 token.set('height',tsize*unitSize);
534 };
535
536 var parseEssential = function(data) {
537 /* names are tricky as we delimit on CR, last occurance of CR
538 which has numbers after it TODO use a regex which is shorter*/
539 var namefield = data[0];
540 var delimiter_idx =namefield.lastIndexOf("CR");
541 var fuzzyfield = namefield.substring(delimiter_idx,namefield.length);
542 if ((delimiter_idx <= 0) || !(fuzzyfield.match(/\d+|—/g)))
543 {delimiter_idx = namefield.length;}
544 var name = namefield.substring(0,delimiter_idx);
545 name = name.trim().toLowerCase();
546 creName = (fields.summoner ? (fields.summoner.get('_displayname')+'\'s ' + name):name);
547 fields.publicName = (fields.summoner ? creName:fields.publicName);
548 };
549
550 /**
551 * parse core attributes, AC, HP, etc
552 */
553 var parseCore = function(data) {
554 if (!data)
555 {return;}
556
557 if (fields.summoner) {
558 character.set('controlledby',fields.summoner.get('_id'));
559 character.set('inplayerjournals',fields.summoner.get('_id'));
560 }
561
562 var charId = character.get('_id');
563 var line = "";
564 var lineStartFnd = 0;
565 var lineEndFnd = data.length;
566 var termChars = [';',','];
567 var rc = -1;
568 // core attribute fields
569 var initAttr = "Init";
570 var primeAttr = ["Str","Dex","Con","Int","Wis","Cha"];
571 var minorAttr = ["Base Atk","CMB","CMD"];
572 var defAttr = ["AC","touch","flat-footed","hp"];
573 var saveAttr = ["Fort","Ref","Will"];
574 var hp=0,AC=0,tAC=0,ffAC=0;
575
576
577 //Flag that this token will be prepped, can be removed by the user
578 addAttribute("CGEN",'true','',charId);
579 addAttribute("name",fields.defaultName,'',charId);
580 // Init (TODO mythic Init)
581 line = getLineByName(initAttr,data);
582 rc = getValueByName(initAttr,line,termChars);
583 addAttribute(initAttr,rc,rc,charId);
584 // prime attributes
585 lineStartFnd = getLineNumberByName("STATISTICS",data);
586 lineEndFnd = getLineNumberByName("SPECIAL ABILITIES",data);
587 line = getLineByName("Str",data,lineStartFnd,lineEndFnd);
588 addAttrList(data,line,primeAttr,lineStartFnd,termChars,charId);
589 // minor attributes
590 line = getLineByName("Base Atk",data,lineStartFnd,lineEndFnd);
591 addAttrList(data,line,minorAttr,lineStartFnd,termChars,charId);
592 // defense attributes
593 lineStartFnd = getLineNumberByName("DEFENSE",data);
594 lineEndFnd = getLineNumberByName("OFFENSE",data);
595 line = getLineByName("AC",data,lineStartFnd,lineEndFnd);
596 addAttrList(data,line,defAttr,lineStartFnd,termChars,charId);
597 // save attributes
598 line = getLineByName("Fort",data,lineStartFnd,lineEndFnd);
599 addAttrList(data,line,saveAttr,lineStartFnd,termChars,charId);
600
601 //format attributes:
602 hp = formatAttribute("hp",0,charId);
603 creLog("parsecore: HP: " + hp,1);
604 AC = formatAttribute("AC",0,charId);
605 creLog("parsecore: AC: " + AC,1);
606 tAC = formatAttribute("touch",0,charId);
607 creLog("parsecore: touch AC: " + tAC,1);
608 ffAC = formatAttribute("flat-footed",0,charId);
609 creLog("parsecore: flat-foot AC: " + ffAC,1);
610
611 // determine size
612 var size = parseSize(data);
613 creLog("parsecore: size: " + size,1);
614 addAttribute('Size',size,size,charId);
615
616 // save riders
617 if ((rc=line.indexOf(';')) !== -1) {
618 rc = '('+line.substring(rc+1)+')';
619 } else {rc = "";}
620 addAttributeRoll("INIT",initAttr,false,true,charId,"&{tracker}");
621 addAttributeRoll("F","Fort",false,true,charId,rc);
622 addAttributeRoll("R","Ref",false,true,charId,rc);
623 addAttributeRoll("W","Will",false,true,charId,rc);
624
625
626 };
627
628 /**
629 * Parse Size of the creature
630 * TODO modify getLineByName and getLineNumberByName to allow regex
631 */
632 var parseSize = function(data) {
633 var retval = 'Medium';
634 var lineEndFnd = getLineNumberByName('DEFENSE',data);
635 var space = getLineByName('Space',data,getLineNumberByName('OFFENSE',data),getLineNumberByName('STATISTICS',data));
636 creLog('parseSize: space is ' + space);
637 if (space) {
638 space = getValueByName('Space',space,[';',',']);
639 space = getBonusNumber(space,bonusEnum.SCALAR);
640 space = parseInt(space);
641 if (isNaN(space))
642 {return;}
643 space = space/5;
644 switch(space) {
645 case 1:
646 retval = 'Medium';
647 break;
648 case 2:
649 retval = 'Large';
650 break;
651 case 3:
652 retval = 'Huge';
653 break;
654 case 4:
655 retval = 'Gargantuan';
656 break;
657 case 6:
658 retval = 'Colossal';
659 break;
660 default:
661 retval = 'Medium';
662 break;
663 }
664 } else if (getLineByName('Fine',data,0,lineEndFnd) || getLineByName('fine',data,0,lineEndFnd)) {
665 retval = 'Fine';
666 } else if (getLineByName('Diminutive',data,0,lineEndFnd) || getLineByName('diminutive',data,0,lineEndFnd)) {
667 retval = 'Diminutive';
668 } else if (getLineByName('Tiny',data,0,lineEndFnd) || getLineByName('tiny',data,0,lineEndFnd)) {
669 retval = 'Tiny';
670 } else if (getLineByName('Small',data,0,lineEndFnd) || getLineByName('small',data,0,lineEndFnd)) {
671 retval = 'Small';
672 } else if (getLineByName('Medium',data,0,lineEndFnd) || getLineByName('Medium',data,0,lineEndFnd)) {
673 retval = 'Medium';
674 } else if (getLineByName('Large',data,0,lineEndFnd) || getLineByName('Large',data,0,lineEndFnd)) {
675 retval = 'Large';
676 } else if (getLineByName('Huge',data,0,lineEndFnd) || getLineByName('huge',data,0,lineEndFnd)) {
677 retval = 'Huge';
678 } else if (getLineByName('Gargantuan',data,0,lineEndFnd) || getLineByName('gargantuan',data,0,lineEndFnd)) {
679 retval = 'Gargantuan';
680 } else if (getLineByName('Colossal',data,0,lineEndFnd) || getLineByName('colossal',data,0,lineEndFnd)) {
681 retval = 'Colossal';
682 }
683 return retval;
684 };
685
686 /**
687 * parse special attacks, if the statblock has 'riders' which give
688 * details on the special abilities, then include it as part of the marco.
689 * TODO: add option for verbrocity during generaton.
690 */
691 var parseSpecials = function(data) {
692 var retval = {};
693 var charId = character.get('_id');
694 var line = "";
695 var lineStartFnd = 0;
696 var lineEndFnd = data.length;
697 var re, saName, abName, sAtks, sAtkStr,
698 action, actionStr, spList, hasSAtks;
699
700 line = getLineByName("Special Attacks",data);
701 if (line) {
702 line = line.replace("Special Attacks","");
703 sAtks = line.split(/,(?![^\(\)]*\))/);
704 while (sAtks.length > 0) {
705 if (!sAtks[0] || !sAtks[0].match(/[^\s]+/)) {
706 sAtks.shift();
707 continue;
708 }
709 saName = sAtks[0].match(/\b[^\d\(\)\+]+/g);
710 if (saName) {
711 saName = saName[0].trim();
712 sAtkStr += saName;
713 if (!retval[saName])
714 {retval[saName] = new Array(sAtks[0].trim());}
715 else
716 {retval[saName].push(sAtks[0].trim());}
717 creLog("parseSpecials " + sAtks[0] + " saName: " + saName,1);
718 }
719 sAtks.shift();
720 }
721 }
722
723 /* TODO add in nextLine support for cases where the special ability
724 name is found on the following line(s) */
725 lineStartFnd = getLineNumberByName("SPECIAL ABILITIES",data);
726 if (lineStartFnd) {
727 for (var i = lineStartFnd; i < data.length; ++i) {
728 line = data[i];
729 if (line.match(/\(Su\)|\(Ex\)|\(Sp\)/i)) {
730 saName = line.substring(0,line.indexOf('(')).toLowerCase().trim();
731 re = new RegExp(saName,'ig');
732 action = "!\n" + fields.menuWhis +
733 line.replace(re,getTermLink(saName,termEnum.GENERAL));
734 if (!retval[saName])
735 {retval[saName] = new Array(line.trim());}
736 else
737 {retval[saName].push(line.trim());}
738 addAbility("SA-"+saName,"",action,false,charId);
739 }
740 }
741 }
742
743 /* If there is a special attack, that is a special attack not ability,
744 then it is unique and should get its own ability as well as long-rider
745 if one exists.*/
746 spList = "!\n" + fields.menuWhis
747 + menuTemplate.boundryImg({imgLink: design.specialTopImg})
748 + menuTemplate.midDiv({imgLink: design.specialMidImg})
749 + '<div style="text-align:center">'
750 + menuTemplate.titleFmt({
751 style: design.specialTitleFmt,
752 title: creName
753 })
754 + menuTemplate.titleFmt({
755 style: design.specialLabelFmt,
756 title: 'Special Attacks'
757 })
758 +'</div>';
759 /*
760 // DEPRECATED (exclude special attacks that are melee/ranged riders)
761 // insure we have melee or ranged
762 line = getLineByName("Melee",data)
763 + getLineByName("Ranged",data) + '';
764 */
765 if (sAtkStr) {
766 _.every(_.keys(retval), function(sAtks) {
767 if (/*!line.match(sAtks) &&*/ (sAtkStr.indexOf(sAtks) !== -1)) {
768 hasSAtks = true;
769 abName = "SP-" + sAtks;
770 abName = abName.replace(/\s/g,"-");
771 action = "!\n" + fields.resultWhis;
772 _.every(retval[sAtks], function(rider) {
773 creLog("SP rider: " + rider,3);
774 re = new RegExp(sAtks,"ig");
775 actionStr = "<div>"+getFormattedRoll(rider)+"</div>";
776 action += actionStr.replace(re,getTermLink(sAtks,termEnum.GENERAL));
777 return true;
778 });
779 creLog("SP action: " + action,3);
780 addAbility(abName,"",action,false,charId);
781 spList = spList
782 + menuTemplate.midButton({
783 riders: undefined,
784 creName: creName,
785 abName: abName,
786 btnName: sAtks
787 });
788 }
789 return true;
790 });
791 }
792 if (hasSAtks) {
793 spList = spList
794 + '</div>'
795 + menuTemplate.boundryImg({imgLink: design.specialBotImg});
796 addAbility("Specials",'',spList,false,charId);
797 }
798 return retval;
799 };
800
801 /**
802 * parse melee and ranged attacks, if there are special attack riders,
803 * then we will append the marco text
804 */
805 var parseAttacks = function(data,specials) {
806 var charId = character.get('_id');
807 var line = "";
808 var lineStartFnd = 0;
809 var lineEndFnd = data.length;
810 var atkMenu, hasSAtk = false, CMB, riders;
811
812 atkMenu = fields.menuWhis
813 + menuTemplate.boundryImg({imgLink: design.atkMenuTopImg})
814 + menuTemplate.midDiv({imgLink: design.atkMenuMidImg})
815 + '<div style="text-align:center">'
816 + menuTemplate.titleFmt({
817 style: design.atkMenuTitleFmt,
818 title: creName
819 })
820 + menuTemplate.titleFmt({
821 style: design.atkMenuLabelFmt,
822 title: 'Attacks'
823 })
824 +'</div>';
825
826 lineStartFnd = getLineNumberByName("OFFENSE",data);
827 lineEndFnd = getLineNumberByName("TACTICS",data);
828 if (!lineEndFnd)
829 {lineEndFnd = getLineNumberByName("STATISTICS",data);}
830 try {
831 line = getLineByName("Melee",data,lineStartFnd,lineEndFnd);
832 if (line) {
833 formatAttacks(line,"Melee",charId,"ATK",specials);
834 atkMenu = atkMenu
835 + menuTemplate.midButton({
836 riders: undefined,
837 creName: creName,
838 abName: 'ATK',
839 btnName: 'Melee'
840 });
841 }
842 line = getLineByName("Ranged",data,lineStartFnd,lineEndFnd);
843 if (line) {
844 formatAttacks(line,"Ranged",charId,"RNG",specials);
845 atkMenu = atkMenu
846 + menuTemplate.midButton({
847 riders: undefined,
848 creName: creName,
849 abName: 'RNG',
850 btnName: 'Ranged'
851 });
852 }
853 hasSAtk = findObjs({
854 _type: "ability",
855 name: "Specials",
856 _characterid: charId
857 });
858 if (hasSAtk.length > 0) {
859 atkMenu = atkMenu
860 + menuTemplate.midButton({
861 riders: undefined,
862 creName: creName,
863 abName: 'Specials',
864 btnName: 'Specials'
865 });
866 }
867 lineStartFnd = getLineNumberByName("STATISTICS",data);
868 line = getLineByName("CMB",data,lineStartFnd);
869 if (line) {
870 CMB = getValueByName("CMB",line,[',',';']);
871 riders = CMB.match(/\(.+\)/);
872 addAttributeRoll("CMB","CMB",false,false,charId,(riders ? riders:''));
873 atkMenu = atkMenu
874 + menuTemplate.midButton({
875 riders: riders,
876 creName: creName,
877 abName: 'CMB',
878 btnName: 'CMB'
879 });
880 }
881
882 } catch (e) {
883 log ("ERROR when parsing attacks: ");
884 throw e;
885 }
886 atkMenu = atkMenu
887 + '</div>'
888 + menuTemplate.boundryImg({imgLink: design.atkMenuBotImg});
889 addAbility("Attacks",'',atkMenu,true,charId);
890 };
891
892 /** parse out spells the marco will spit them out, possibly even link
893 * them to a known PRD search engine.
894 */
895 var parseSpells = function(data) {
896 var charId = character.get('_id');
897 var lineEndFnd = data.length;
898 var casterType, attrName;
899 var rc, line = "";
900 var termChars = [';',','];
901
902 lineEndFnd = getLineNumberByName("TACTICS",data);
903 if (!lineEndFnd)
904 {lineEndFnd = getLineNumberByName("STATISTICS",data);}
905 formatSpells("Spell-Like Abilities",data,lineEndFnd,termEnum.GENERAL,"SLA");
906 formatSpells("Spells Known",data,lineEndFnd,termEnum.SPELL);
907 formatSpells("Spells Prepared",data,lineEndFnd,termEnum.SPELL);
908 formatSpells("Extracts Prepared",data,lineEndFnd,termEnum.SPELL);
909 };
910
911 /**
912 * Generic Parse assuming CSV on the line.
913 *
914 */
915 var parseGeneric = function(generic,data,type,start,end) {
916 if (!generic || !type)
917 {return undefined;}
918 if (!start)
919 {start = 0;}
920 if (!end)
921 {end = data.length;}
922 var charId = character.get('_id');
923 var genName, genRiders, genAry,
924 idx, genList, abName, genLabel;
925 var lineStartFnd = 0;
926 var rc, line = "";
927 var termChars = [';'];
928
929 genList = "!\n" + fields.menuWhis
930 + menuTemplate.boundryImg({imgLink: design.genTopImg})
931 + menuTemplate.midDiv({imgLink: design.genMidImg})
932 + '<div style="text-align:center">'
933 + menuTemplate.titleFmt({
934 style: design.genTitleFmt,
935 title: creName
936 })
937 + menuTemplate.titleFmt({
938 style: design.genLabelFmt,
939 title: generic
940 })
941 +'</div>';
942
943 line = getLineByName(generic,data,start,end);
944 line = getValueByName(generic,line,termChars);
945 creLog("parseGeneric: " + line,1);
946 if (line) {
947 line = line.replace(generic,"");
948 genAry = line.split(/,(?![^\(\)]*\))/);
949 if (genAry) {
950 _.every(genAry, function(elemGen) {
951 if ((idx=elemGen.indexOf("(")) !== -1) {
952 genName = elemGen.substring(0,idx).trim();
953 genRiders = elemGen.substring(idx).trim();
954 } else {
955 genName = elemGen.trim();
956 genRiders = undefined;
957 }
958 genName = formatSuperSubScript(genName);
959 genList = genList
960 + menuTemplate.midLink({
961 riders: genRiders,
962 link: getTermLink(genName,type)
963 });
964 return true;
965 });
966 genList = genList
967 + '</div>'
968 + menuTemplate.boundryImg({imgLink: design.genBotImg});
969 addAbility(generic,'',genList,false,charId);
970 }
971 }
972 };
973
974 /** parse out skills the marco will spit them out, possibly even link
975 * them to a known PRD search engine.
976 */
977 var parseSkills = function(data) {
978 var charId = character.get('_id');
979 var lineStartFnd = 0;
980 var lineEndFnd = data.length;
981 var skillName, skillRiders, skillAry,
982 skillList, abName, skillLabel,
983 parts, abStr, racialBonus, racialAry,
984 racialRiders;
985 var rc, line = "";
986 var termChars = [';',','];
987
988 skillList = "!\n" + fields.menuWhis
989 + menuTemplate.boundryImg({imgLink: design.skillTopImg})
990 + menuTemplate.midDiv({imgLink: design.skillMidImg})
991 + '<div style="text-align:center">'
992 + menuTemplate.titleFmt({
993 style: design.skillTitleFmt,
994 title: creName
995 })
996 + menuTemplate.titleFmt({
997 style: design.skillLabelFmt,
998 title: 'Skills'
999 })
1000 +'</div>';
1001
1002 lineStartFnd = getLineNumberByName("STATISTICS",data);
1003 line = getLineByName("Skills",data,lineStartFnd);
1004 if (line) {
1005 line = line.replace("Skills","");
1006 skillAry = line.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1007 if (skillAry.length > 0) {
1008 _.every(skillAry, function(skill) {
1009 if (!skill || !skill.match(/[^\s]+/)) {
1010 creLog("invalid: " + skill,2);
1011 return true;
1012 }
1013 creLog(skill,4);
1014 if ((parts=skill.match(/\b[^\d\+\-/]+/)) !== -1) {
1015 skillName = parts[0].trim();
1016 creLog('parseSkills: skillName: ' + skillName,5);
1017 skillRiders = (skill.substring(skill.indexOf(skillName)+skillName.length)).match(/\(.+\)/);
1018 }
1019 if (skillName.match("Racial Modifiers")) {
1020 creLog("parseSkills: Ending skills, reason : " + skillName,4);
1021 return false;
1022 }
1023 rc = getBonusNumber(skill);
1024 skillName = formatSuperSubScript(skillName);
1025 abName = "SK-" + skillName;
1026 abName = abName.replace(/\s/g,"-");
1027 abStr = "!\n" + fields.resultWhis + creName + ' ' + abName + " [[1d20"+rc+"]]";
1028 addAbility(abName,'',abStr,false,charId);
1029 skillList = skillList
1030 + menuTemplate.midButton({
1031 riders: skillRiders,
1032 creName: creName,
1033 abName: abName,
1034 btnName: skillName
1035 });
1036 return true;
1037 });
1038 }
1039 if (!characterObjExists('SK-Perception','ability',charId)) {
1040 lineEndFnd = getLineNumberByName('DEFENSE',data);
1041 line = getLineByName('Perception',data,0,lineEndFnd);
1042 rc = getValueByName('Perception',line,termChars);
1043 rc = getBonusNumber(rc,bonusEnum.SIGN);
1044 abName = "SK-Perception";
1045 abStr = "!\n" + fields.resultWhis + creName + ' ' + abName + " [[1d20"+rc+"]]";
1046 addAbility(abName,'',abStr,false,charId);
1047 skillList = skillList
1048 + menuTemplate.midButton({
1049 riders: skillRiders,
1050 creName: creName,
1051 abName: abName,
1052 btnName: 'Perception'
1053 });
1054 }
1055 skillList = skillList
1056 + '</div>'
1057 + menuTemplate.boundryImg({imgLink: design.skillBotImg});
1058 addAbility("Skills",'',skillList,false,charId);
1059 }
1060
1061 };
1062
1063 /**
1064 * Given an ability name, augment it (very specifc)
1065 * DEPRECATED
1066 */
1067 var augmentSkillRoll = function(name, augment, charId) {
1068 var ability,abStr,rollExpr,
1069 newExpr,bonus;
1070 ability = characterObjExists(name,'ability',charId);
1071 if (ability) {
1072 abStr = ability.get('action');
1073 rollExpr = getExpandedExpr('1d20',abStr);
1074 newExpr = rollExpr.replace('1d20','').trim();
1075 bonus = parseInt(newExpr);
1076 bonus = bonus + parseInt(augment) + "";
1077 bonus = getBonusNumber(bonus,bonusEnum.SIGN);
1078 abStr = abStr.replace(rollExpr,'1d20'+bonus);
1079 ability.set('action',abStr);
1080 }
1081 };
1082
1083 /**
1084 * Parse defenses
1085 */
1086 var parseDefenses = function(data) {
1087 var retVal;
1088 var charId = character.get('_id');
1089 var line = "";
1090 var lineStartFnd = 0;
1091 var lineEndFnd = data.length;
1092 var termChars = [';'];
1093 var excluded = ['SR','DR','Immune','Resist','Weaknesses'];//TODO compensate for missing delimiters (SR on black dragons vs succubus)
1094 var rc = -1;
1095 var i = 0;
1096 var SR,DR,CMD,defenseAb,riders, name,
1097 resist,immune,weak, senses, speed,
1098 regen, aura, fasthealing, aryList, hasDefense=false;
1099 var defenseList;
1100
1101 var fmtLinkFunc = function(arg) {
1102 arg = arg.trim();
1103 var name = arg.match(/\b[^\(\)]+/);
1104 name = formatSuperSubScript(name[0]);
1105 var riders = arg.match(/\(.+\)/);
1106 return menuTemplate.midLinkFree({
1107 riders: riders,
1108 link: getTermLink(name,termEnum.GENERAL)
1109 });
1110 };
1111 var fmtLeadFunc = function(arg) {
1112 var list = arg.split('%%');
1113 if (list.length !== 2)
1114 {throw "ERROR: Bad Arg";}
1115 var label = list[0];
1116 var riders = list[1].match(/\(.+\)/);
1117 var value = list[1].match(/\b[^\(\)]+/);
1118 if (!value)
1119 {value = '—';}
1120 value = value[0].trim();
1121 return menuTemplate.midLeadText({
1122 riders: riders,
1123 label: label,
1124 text: value
1125 });
1126 };
1127 var fmtTextFunc = function(arg) {
1128 var riders = arg.match(/\(.+\)/);
1129 var value = arg.match(/\b[^\(\)]+/);
1130 if (!value)
1131 {value = '—';}
1132 value = value[0].trim();
1133 return menuTemplate.midText({
1134 riders: riders,
1135 text: value
1136 });
1137 };
1138
1139 defenseList = "!\n" + fields.menuWhis
1140 + menuTemplate.boundryImg({imgLink: design.genTopImg})
1141 + menuTemplate.midDiv({imgLink: design.genMidImg})
1142 + '<div style="text-align:center;">'
1143 + menuTemplate.titleFmt({
1144 style: design.genTitleFmt,
1145 title: creName
1146 })
1147 + menuTemplate.titleFmt({
1148 style: design.genLabelFmt,
1149 title: 'Defenses'
1150 })
1151 +'</div>';
1152
1153 lineStartFnd = getLineNumberByName("DEFENSE",data);
1154 lineEndFnd = getLineNumberByName("OFFENSE",data);
1155 line = getLineByName("DR",data,lineStartFnd,lineEndFnd);
1156 if (line) {DR = getValueByName("DR",line,termChars);}
1157 line = getLineByName("SR",data,lineStartFnd,lineEndFnd);
1158 if (line) {SR = getValueByName("SR",line,termChars);}
1159 line = getLineByName("Immune",data,lineStartFnd,lineEndFnd);
1160 if (line) {immune = getValueByName("Immune",line,termChars);}
1161 line = getLineByName("Resist",data,lineStartFnd,lineEndFnd);
1162 if (line) {resist = getValueByName("Resist",line,termChars);}
1163 line = getLineByName("Defensive Abilities",data,lineStartFnd,lineEndFnd);
1164 if (line) {defenseAb = getValueByName("Defensive Abilities",line,termChars);}
1165 line = getLineByName("Weaknesses",data,lineStartFnd,lineEndFnd);
1166 if (line) {weak = getValueByName("Weaknesses",line,termChars);}
1167 // add Fast Healing
1168 line = getLineByName("fast healing",data,0,lineEndFnd);
1169 if (line) {fasthealing = getValueByName("fast healing",line,termChars);}
1170 // add Regeneration
1171 line = getLineByName("regeneration",data,0,lineEndFnd);
1172 if (line) {regen = getValueByName("regeneration",line,termChars);}
1173 // add CMD
1174 lineStartFnd = getLineNumberByName("STATISTICS",data);
1175 line = getLineByName("CMD",data,lineStartFnd);
1176 if (line) {CMD = getValueByName("CMD",line,[',',';']);}
1177 // add Senses
1178 lineEndFnd = getLineNumberByName("DEFENSE", data);
1179 line = getLineByName("Senses",data,0,lineEndFnd);
1180 if (line) {senses = getValueByName("Senses",line,termChars);}
1181 // add Aura
1182 line = getLineByName("Aura",data,0,lineEndFnd);
1183 if (line) {aura = getValueByName("Aura",line,termChars);}
1184 // add Speed
1185 lineStartFnd = getLineNumberByName("OFFENSE", data);
1186 lineEndFnd = getLineNumberByName("TACTICS",data);
1187 if (!lineEndFnd)
1188 {lineEndFnd = getLineNumberByName("STATISTICS",data);}
1189 line = getLineByName("Speed",data,lineStartFnd,lineEndFnd);
1190 if (line) {speed = getValueByName("Speed",line,termChars);}
1191
1192 if (CMD) {
1193 hasDefense = true;
1194 defenseList = defenseList
1195 + '<div>'
1196 + fmtLeadFunc('CMD:%%'+CMD)
1197 + '</div>';
1198 }
1199
1200 if (regen) {
1201 hasDefense = true;
1202 defenseList = defenseList
1203 + '<div>'
1204 + fmtLeadFunc('Regeneration:%%'+regen)
1205 + '</div>';
1206 }
1207
1208 if (fasthealing) {
1209 hasDefense = true;
1210 defenseList = defenseList
1211 + '<div>'
1212 + fmtLeadFunc('Fast Healing:%%'+fasthealing)
1213 + '</div>';
1214 }
1215
1216 if (DR || SR) {
1217 hasDefense = true;
1218 defenseList += '<div>';
1219 if (DR && SR) {
1220 defenseList = defenseList
1221 + formatTable(
1222 [('DR:%%'+DR),('SR:%%'+SR)],
1223 2,
1224 fmtLeadFunc);
1225 } else if (DR) {
1226 defenseList = defenseList
1227 + fmtLeadFunc('DR:%%'+DR);
1228 } else if (SR) {
1229 defenseList = defenseList
1230 + fmtLeadFunc('SR:%%'+SR);
1231 }
1232 defenseList += '</div>';
1233 }
1234
1235 if (speed) {
1236 hasDefense = true;
1237 aryList = speed.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1238 defenseList = defenseList
1239 + '<div style="text-align:center;">'
1240 + '<span style="font-weight: bold; color: #000000;">' + "Speed:" + '</span>'
1241 + '</div>';
1242 if (aryList.length >= 2) {
1243 defenseList += '<div>' + formatTable(aryList,2,fmtTextFunc) + '</div>';
1244 } else if (aryList.length > 0) {
1245 for (i = 0; i < aryList.length; i++) {
1246 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1247 continue;
1248 }
1249 defenseList = defenseList
1250 + '<div>'
1251 + fmtTextFunc(aryList[i])
1252 + '</div>';
1253 }
1254 }
1255 }
1256
1257 if (senses) {
1258 hasDefense = true;
1259 aryList = senses.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1260 defenseList = defenseList
1261 + '<div style="text-align:center;">'
1262 + '<span style="font-weight: bold; color: #000000;">' + "Senses:" + '</span>'
1263 + '</div>';
1264 if (aryList.length > 3) {
1265 defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
1266 } else if (aryList.length > 0) {
1267 for (i = 0; i < aryList.length; i++) {
1268 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1269 continue;
1270 }
1271 defenseList = defenseList
1272 + '<div>'
1273 + fmtLinkFunc(aryList[i])
1274 + '</div>';
1275 }
1276 }
1277 }
1278
1279 if (aura) {
1280 hasDefense = true;
1281 aryList = aura.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1282 defenseList = defenseList
1283 + '<div style="text-align:center;">'
1284 + '<span style="font-weight: bold; color: #000000;">' + "Aura:" + '</span>'
1285 + '</div>';
1286 if (aryList.length > 3) {
1287 defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
1288 } else if (aryList.length > 0) {
1289 for (i = 0; i < aryList.length; i++) {
1290 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1291 continue;
1292 }
1293 defenseList = defenseList
1294 + '<div>'
1295 + fmtLinkFunc(aryList[i])
1296 + '</div>';
1297 }
1298 }
1299 }
1300
1301 if (immune) {
1302 hasDefense = true;
1303 aryList = immune.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1304 defenseList = defenseList
1305 + '<div style="text-align:center;">'
1306 + '<span style="font-weight: bold; color: #000000;">' + "Immunities:" + '</span>'
1307 + '</div>';
1308 if (aryList.length > 3) {
1309 defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
1310 } else if (aryList.length > 0) {
1311 for (i = 0; i < aryList.length; i++) {
1312 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1313 continue;
1314 }
1315 defenseList = defenseList
1316 + '<div>'
1317 + fmtLinkFunc(aryList[i])
1318 + '</div>';
1319 }
1320 }
1321 }
1322
1323 if (resist) {
1324 hasDefense = true;
1325 aryList = resist.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1326 defenseList = defenseList
1327 + '<div style="text-align:center">'
1328 + '<span style="font-weight: bold; color: #000000;">' + "Resistances:" + '</span>'
1329 + '</div>';
1330 if (aryList.length > 3) {
1331 defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
1332 } else if (aryList.length > 0) {
1333 for (i = 0; i < aryList.length; i++) {
1334 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1335 continue;
1336 }
1337 defenseList = defenseList
1338 + '<div style="text-align:center">'
1339 + fmtLinkFunc(aryList[i])
1340 + '</div>';
1341 }
1342 }
1343 }
1344
1345 if (weak) {
1346 hasDefense = true;
1347 aryList = weak.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1348 defenseList = defenseList
1349 + '<div style="text-align:center">'
1350 + '<span style="font-weight: bold; color: #000000;">' + "Weaknesses:" + '</span>'
1351 + '</div>';
1352 if (aryList.length > 3) {
1353 defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
1354 } else if (aryList.length > 0) {
1355 for (i = 0; i < aryList.length; i++) {
1356 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1357 continue;
1358 }
1359 defenseList = defenseList
1360 + '<div style="text-align:center">'
1361 + fmtLinkFunc(aryList[i])
1362 + '</div>';
1363 }
1364 }
1365 }
1366
1367 if (defenseAb) {
1368 hasDefense = true;
1369 aryList = defenseAb.split(/,(?![^\(\)]*\))|;(?![^\(\)]*\))/);
1370 defenseList = defenseList
1371 + '<div style="text-align:center">'
1372 + '<span style="font-weight: bold; color: #000000;">' + "Defensive Abilities:" + '</span>'
1373 + '</div>';
1374 if (aryList.length > 3) {
1375 defenseList += '<div>' + formatTable(aryList,2,fmtLinkFunc) + '</div>';
1376 } else if (aryList.length > 0) {
1377 for (i = 0; i < aryList.length; i++) {
1378 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
1379 continue;
1380 }
1381 defenseList = defenseList
1382 + '<div style="text-align:center">'
1383 + fmtLinkFunc(aryList[i])
1384 + '</div>';
1385 }
1386 }
1387 }
1388
1389 defenseList = defenseList
1390 + '</div>'
1391 + menuTemplate.boundryImg({imgLink: design.genBotImg});
1392
1393 if (hasDefense) {
1394 addAbility("Defenses",'',defenseList,false,charId);
1395 }
1396
1397 };
1398
1399 /**
1400 * Format a table given a minimum column length and a format function for
1401 * the elements.
1402 */
1403 var formatTable = function(aryList, minCol, fmtFunc) {
1404 if (!aryList || !minCol)
1405 {return undefined;}
1406 if (!fmtFunc)
1407 {fmtFunc = function(arg) {return arg;};}
1408 var retval = '<table style="margin-left: auto; margin-right: auto;">';
1409 var i,j;
1410
1411 for (i = 0; i < aryList.length; i += minCol) {
1412 retval += '<tr>';
1413 if ((aryList.length - i + 1) > minCol) {
1414 for (j = i; j < i + minCol; ++j) {
1415 if (!aryList[j] || !aryList[j].match(/[^\s]+/))
1416 {continue;}
1417 retval += '<td>' + fmtFunc(aryList[j]) + '</td>';
1418 }
1419 } else {
1420 // span the last column
1421 for (j = i; j < aryList.length - 1; ++j) {
1422 if (!aryList[j] || !aryList[j].match(/[^\s]+/))
1423 {continue;}
1424 retval += '<td>' + fmtFunc(aryList[j]) + '</td>';
1425 }
1426 retval += '<td colspan="' + (minCol-(j%minCol)) + '">' + fmtFunc(aryList[aryList.length - 1]) + '</td>';
1427 retval += '</tr>';
1428 break;
1429 }
1430 retval += '</tr>';
1431 }
1432
1433 // clean up remainder here
1434 retval += '</table>';
1435 return retval;
1436};
1437
1438 /**
1439 * Parse out special abilities (not special attacks) and put them in a
1440 * menu, if there are no special abilities, create no such menu
1441 */
1442 var parseSpecialMenu = function(data,specials) {
1443 if (!specials)
1444 {return undefined;}
1445 var charId = character.get('_id');
1446 var spMenu, saName, hasSpecials = false;
1447
1448 spMenu = "!\n" + fields.menuWhis
1449 + menuTemplate.boundryImg({imgLink: design.genTopImg})
1450 + menuTemplate.midDiv({imgLink: design.genMidImg})
1451 + '<div style="text-align:center">'
1452 + menuTemplate.titleFmt({
1453 style: design.genTitleFmt,
1454 title: creName
1455 })
1456 + menuTemplate.titleFmt({
1457 style: design.genLabelFmt,
1458 title: 'Special Abilities'
1459 })
1460 +'</div>';
1461
1462
1463 _.every(_.keys(specials), function(key) {
1464 saName = 'SA-' + key;
1465 if (characterObjExists(saName,'ability',charId)) {
1466 hasSpecials = true;
1467 spMenu = spMenu
1468 + menuTemplate.midButton({
1469 riders: undefined,
1470 creName: creName,
1471 abName: saName,
1472 btnName: key
1473 });
1474 }
1475 return true;
1476 });
1477
1478 spMenu = spMenu
1479 + '</div>'
1480 + menuTemplate.boundryImg({imgLink: design.genBotImg});
1481
1482 if (hasSpecials)
1483 {addAbility("Special Abilities",'',spMenu,false,charId);}
1484 };
1485
1486 /**
1487 * Parses Gear
1488 */
1489 var parseItems = function(data) {
1490 var charId = character.get('_id');
1491 var menu, hasGear = false;
1492 var start, end;
1493
1494 menu = '!\n' + fields.menuWhis
1495 + menuTemplate.boundryImg({imgLink: design.genTopImg})
1496 + menuTemplate.midDiv({imgLink: design.genMidImg})
1497 + '<div style="text-align:center">'
1498 + menuTemplate.titleFmt({
1499 style: design.genTitleFmt,
1500 title: creName
1501 })
1502 + menuTemplate.titleFmt({
1503 style: design.genLabelFmt,
1504 title: 'Items'
1505 })
1506 +'</div>';
1507
1508 start = getLineNumberByName('STATISTICS',data);
1509 end = getLineNumberByName('SPECIAL ABILITIES',data);
1510
1511 // parse Combat Gear
1512 parseGeneric("Combat Gear",termEnum.GENERAL,start,end);
1513 if (characterObjExists("Combat Gear","ability",charId)) {
1514 hasGear = true;
1515 menu = menu
1516 + menuTemplate.midButton({
1517 riders: undefined,
1518 creName: creName,
1519 abName: 'Combat Gear',
1520 btnName: 'Gear-Combat'
1521 });
1522 }
1523
1524 // parse Other Gear
1525 parseGeneric("Other Gear",termEnum.GENERAL,start,end);
1526 if (characterObjExists("Other Gear","ability",charId)) {
1527 hasGear = true;
1528 menu = menu
1529 + menuTemplate.midButton({
1530 riders: undefined,
1531 creName: creName,
1532 abName: 'Other Gear',
1533 btnName: 'Gear-Other'
1534 });
1535 }
1536 if (!hasGear) {
1537 // parse General Gear
1538 parseGeneric("Gear",termEnum.GENERAL,start,end);
1539 if (characterObjExists("Gear","ability",charId)) {
1540 hasGear = true;
1541 menu = menu
1542 + menuTemplate.midButton({
1543 riders: undefined,
1544 creName: creName,
1545 abName: 'Gear',
1546 btnName: 'Gear'
1547 });
1548 }
1549 }
1550
1551 menu = menu
1552 + '</div>'
1553 + menuTemplate.boundryImg({imgLink: design.genBotImg});
1554
1555 if (hasGear)
1556 {addAbility("Items",'',menu,false,charId);}
1557 };
1558
1559 /**
1560 * Parses Tactics
1561 */
1562 var parseTactics = function(data) {
1563 var charId = character.get('_id');
1564 var menu, hasTactics = false;
1565 var start, end, line;
1566
1567 menu = '!\n' + fields.menuWhis
1568 + menuTemplate.boundryImg({imgLink: design.genTopImg})
1569 + menuTemplate.midDiv({imgLink: design.genMidImg})
1570 + '<div style="text-align:center">'
1571 + menuTemplate.titleFmt({
1572 style: design.genTitleFmt,
1573 title: creName
1574 })
1575 + menuTemplate.titleFmt({
1576 style: design.genLabelFmt,
1577 title: 'Tactics'
1578 })
1579 +'</div>';
1580
1581 start = getLineNumberByName('TACTICS',data);
1582 end = getLineNumberByName('STATISTICS',data);
1583
1584 // Before Combat
1585 line = getLineByName('Before Combat',data,start,end);
1586 if (line) {
1587 hasTactics = true;
1588 line = line.replace('Before Combat','<b>Before Combat</b>');
1589 line = '!\n' + fields.privWhis + line;
1590 addAbility('Tactics-Before','',line,false,charId);
1591 menu = menu
1592 + menuTemplate.midButton({
1593 riders: undefined,
1594 creName: creName,
1595 abName: 'Tactics-Before',
1596 btnName: 'Before Combat'
1597 });
1598 }
1599 // During Combat
1600 line = getLineByName('During Combat',data,start,end);
1601 if (line) {
1602 hasTactics = true;
1603 line = line.replace('During Combat','<b>During Combat</b>');
1604 line = '!\n' + fields.privWhis + line;
1605 addAbility('Tactics-During','',line,false,charId);
1606 menu = menu
1607 + menuTemplate.midButton({
1608 riders: undefined,
1609 creName: creName,
1610 abName: 'Tactics-During',
1611 btnName: 'During Combat'
1612 });
1613 }
1614 // Morale
1615 line = getLineByName('Morale',data,start,end);
1616 if (line) {
1617 hasTactics = true;
1618 line = line.replace('Morale','<b>Morale</b>');
1619 line = '!\n' + fields.privWhis + line;
1620 addAbility('Tactics-Morale','',line,false,charId);
1621 menu = menu
1622 + menuTemplate.midButton({
1623 riders: undefined,
1624 creName: creName,
1625 abName: 'Tactics-Morale',
1626 btnName: 'Morale'
1627 });
1628 }
1629
1630 menu = menu
1631 + '</div>'
1632 + menuTemplate.boundryImg({imgLink: design.genBotImg});
1633 if (hasTactics)
1634 {addAbility("Tactics",'',menu,false,charId);}
1635 };
1636
1637 /**
1638 * parse additional, non-combat abilities
1639 */
1640 var parseExtra = function(data,specials) {
1641 var charId = character.get('_id');
1642 var menu, hasExtras = false;
1643
1644 menu = fields.menuWhis
1645 + menuTemplate.boundryImg({imgLink: design.genTopImg})
1646 + menuTemplate.midDiv({imgLink: design.genMidImg})
1647 + '<div style="text-align:center">'
1648 + menuTemplate.titleFmt({
1649 style: design.genTitleFmt,
1650 title: creName
1651 })
1652 + menuTemplate.titleFmt({
1653 style: design.genLabelFmt,
1654 title: 'Abilities'
1655 })
1656 +'</div>';
1657
1658 // parse skills
1659 parseSkills(data);
1660 if (characterObjExists("Skills","ability",charId)) {
1661 hasExtras = true;
1662 menu = menu
1663 + menuTemplate.midButton({
1664 riders: undefined,
1665 creName: creName,
1666 abName: 'Skills',
1667 btnName: 'Skills'
1668 });
1669 }
1670 // parse feats
1671 parseGeneric("Feats",data,termEnum.FEAT);
1672 if (characterObjExists("Feats","ability",charId)) {
1673 hasExtras = true;
1674 menu = menu
1675 + menuTemplate.midButton({
1676 riders: undefined,
1677 creName: creName,
1678 abName: 'Feats',
1679 btnName: 'Feats'
1680 });
1681 }
1682 // parse SQ
1683 parseGeneric("SQ",data,termEnum.GENERAL);
1684 if (characterObjExists("SQ","ability",charId)) {
1685 hasExtras = true;
1686 menu = menu
1687 + menuTemplate.midButton({
1688 riders: undefined,
1689 creName: creName,
1690 abName: 'SQ',
1691 btnName: 'SQ'
1692 });
1693 }
1694 // parse Defenses
1695 parseDefenses(data);
1696 if (characterObjExists("Defenses","ability",charId)) {
1697 hasExtras = true;
1698 menu = menu
1699 + menuTemplate.midButton({
1700 riders: undefined,
1701 creName: creName,
1702 abName: 'Defenses',
1703 btnName: 'Defenses'
1704 });
1705 }
1706 // parse Tactics
1707 parseTactics(data);
1708 if (characterObjExists("Tactics","ability",charId)) {
1709 hasExtras = true;
1710 menu = menu
1711 + menuTemplate.midButton({
1712 riders: undefined,
1713 creName: creName,
1714 abName: 'Tactics',
1715 btnName: 'Tactics'
1716 });
1717 }
1718 // parse special abilities
1719 parseSpecialMenu(data,specials);
1720 if (characterObjExists("Special Abilities","ability",charId)) {
1721 hasExtras = true;
1722 menu = menu
1723 + menuTemplate.midButton({
1724 riders: undefined,
1725 creName: creName,
1726 abName: 'Special Abilities',
1727 btnName: 'Special Abilities'
1728 });
1729 }
1730 // parse items
1731 parseItems(data);
1732 if (characterObjExists("Items","ability",charId)) {
1733 hasExtras = true;
1734 menu = menu
1735 + menuTemplate.midButton({
1736 riders: undefined,
1737 creName: creName,
1738 abName: 'Items',
1739 btnName: 'Items'
1740 });
1741 }
1742
1743
1744 menu = menu
1745 + '</div>'
1746 + menuTemplate.boundryImg({imgLink: design.genBotImg});
1747
1748 if (hasExtras)
1749 {addAbility("Abilities",'',menu,true,charId);}
1750 };
1751
1752 /**
1753 * A messy area were we parse exceptions such as Base-statistics and
1754 * other rare things that mess up the uniformity of PRD stat blocks.
1755 */
1756 var parseOutlier = function() {
1757 // Unimplemented
1758 };
1759
1760 /**
1761 * Format spells
1762 */
1763 var formatSpells = function(type, data, end, termType, suffix) {
1764 if (!type || !end || !termType)
1765 {return undefined;}
1766 var charId = character.get('_id');
1767 var start, casterType, conAttrName, clAttrName,
1768 sLevel, spells, rc, line, casterLevel,
1769 concentration, spellBook, abName, attrStr;
1770 var termChars = [';',','];
1771
1772 start = getLineNumberByName(type,data);
1773 while (start && !!(line=getLineByName(type,data,start,end))) {
1774 if (line) {
1775 line = getLineByName(type,data,start,end);
1776 casterType = line.substring(0,line.indexOf(type)).trim();
1777 casterType = casterType.replace(/\s+/g,"-");
1778 if (casterType === "")
1779 {casterType = "Base";}
1780 casterType += (suffix ? ("-"+suffix) : "");
1781 casterLevel = getValueByName("CL",line,termChars);
1782 casterLevel = getBonusNumber(casterLevel,bonusEnum.SCALAR);
1783 creLog("SpellFormat: casterType: " + casterType,2);
1784 clAttrName = casterType + "-CL";
1785 addAttribute(clAttrName,casterLevel,casterLevel,charId);
1786 attrStr = "!\n" + fields.resultWhis + creName
1787 + ': ' + clAttrName + ': [[1d20+'+casterLevel+']]';
1788 addAbility(clAttrName,'',attrStr,false,charId);
1789 concentration = getValueByName("concentration",line,termChars);
1790 concentration = getBonusNumber(concentration,bonusEnum.SIGN);
1791 conAttrName = casterType + "-CON";
1792 addAttribute(conAttrName,concentration,concentration,charId);
1793 if (!concentration) {
1794 addWarning('No concentration bonus found for \''
1795 + casterType + '\' ' + type + '. A manual'
1796 + ' prompt has been substituted in place.');
1797 }
1798 attrStr = "!\n" + fields.resultWhis + creName
1799 + ': ' + conAttrName + ': [[1d20'+(concentration ? concentration:'+?{concentration-bonus}')+']]';
1800 addAbility(conAttrName,'',attrStr,false,charId);
1801 start++;
1802
1803 spellBook = fields.menuWhis
1804 + menuTemplate.boundryImg({imgLink: design.spellBookTopImg})
1805 + (design.spellBookMidOvr ?
1806 menuTemplate.midDivOverlay({imgLink: design.spellBookMidImg, imgLinkOverlay: design.spellBookMidOvr})
1807 : menuTemplate.midDiv({imgLink: design.spellBookMidImg}))
1808 + '<div style="text-align:center">'
1809 + menuTemplate.titleFmt({
1810 style: design.spellBookTitleFmt,
1811 title: casterType
1812 })
1813 +'</div>';
1814
1815 creLog("ParseSpells: start: " + start + " line: " + line,1);
1816 // mow down some lines wherever we see —, stop when we don't see it.
1817 for (var i = start; i < end; ++i) {
1818 line = data[i];
1819 creLog("formatSpells: line " + line,2);
1820 if (line.match("—")) {
1821 spells = line.split("—");
1822 if (spells.length < 2)
1823 {throw "ERROR: Bad spell list format";}
1824 sLevel = spells[0];
1825 spells = spells[1].split(/,(?![^\(\)]*\))/);
1826 creLog("spells sl: " + sLevel + " sp: " + spells,2);
1827 addSpells(casterLevel,sLevel,spells,termType,casterType,charId);
1828 abName = casterType + " " + sLevel;
1829 spellBook = spellBook
1830 + menuTemplate.midButton({
1831 riders: undefined,
1832 creName: creName,
1833 abName: abName,
1834 btnName: sLevel
1835 });
1836 } else
1837 {break;}
1838 }
1839 if (casterType) {
1840 // put in CL and CON check buttons
1841 spellBook = spellBook
1842 + '<div>##</div>'
1843 + '<div style="font-weight: bold;">'
1844 + menuTemplate.midButtonFree({
1845 riders: undefined,
1846 creName: creName,
1847 abName: clAttrName,
1848 btnName: 'CL'
1849 })
1850 + '\t'
1851 + menuTemplate.midButtonFree({
1852 riders: undefined,
1853 creName: creName,
1854 abName: conAttrName,
1855 btnName: 'CON'
1856 })
1857 + '</div>'
1858 + (design.spellBookMidOvr ? '</td></tr></table>':'') // table-centering
1859 + '</div>' // BG
1860 + menuTemplate.boundryImg({imgLink: design.spellBookBotImg});
1861 abName = casterType;
1862 addAbility(abName,'',spellBook,true,charId);
1863 }
1864 /* Should be safe if we're under the assumption that there is at
1865 lease one ability given the stat block defined that it has abilities
1866 of this type */
1867 start = getLineNumberByName(type,data,start+1,end);
1868 }
1869 }
1870 };
1871
1872 /**
1873 * add spells, be wary of greater/lesser semantics..
1874 */
1875 var addSpells = function(casterLevel,spellLvl, spellAry, termType, setName, charId) {
1876 if (!spellLvl || !spellAry || !setName || !termType || !charId)
1877 {return undefined;}
1878 var spellName, spellRiders, spellLabel, idx,
1879 abName;
1880 var spellList = "";
1881
1882 spellList = "!\n" + fields.menuWhis
1883 + menuTemplate.boundryImg({imgLink: design.spellTopImg})
1884 + menuTemplate.midDiv({imgLink: design.spellMidImg})
1885 + '<div style="text-align:center">'
1886 + menuTemplate.titleFmt({
1887 style: design.spellTitleFmt,
1888 title: setName
1889 })
1890 + menuTemplate.titleFmt({
1891 style: design.spellLabelFmt,
1892 title: spellLvl + " CL("+casterLevel+")"
1893 })
1894 +'</div>';
1895
1896 while (spellAry.length > 0) {
1897 if (!spellAry[0] || !spellAry[0].match(/[^\s]+/)) {
1898 spellAry.shift();
1899 continue;
1900 }
1901 spellAry[0] = spellAry[0].trim();
1902 if ((idx=spellAry[0].indexOf("(")) !== -1) {
1903 spellName = spellAry[0].substring(0,idx).trim();
1904 spellRiders = spellAry[0].substring(idx).trim();
1905 } else {
1906 spellName = spellAry[0].trim();
1907 spellRiders = undefined;
1908 }
1909 // elminate badly formatted trailing commas
1910 if (spellName === "" || !spellName)
1911 {throw 'ERROR: bad spell name: ' + spellName;}
1912 // Invert to treat special names, link to the base spell in the label
1913 spellName = formatSuperSubScript(spellName);
1914 spellLabel = formatSpellStrength(spellName);
1915 creLog("spell name: " + spellName + " spellRiders: " + spellRiders,2);
1916
1917 //d20pfsrd uses - rather than %20
1918 spellLabel = spellLabel.replace(/\s+/g,"-");
1919 spellList = spellList
1920 + menuTemplate.midLinkCaption({
1921 riders: spellRiders,
1922 link: getTermLink(spellLabel,termType,spellName)
1923 });
1924 spellAry.shift();
1925 }
1926 spellList = spellList
1927 + '</div>'
1928 + menuTemplate.boundryImg({imgLink: design.spellBotImg});
1929 abName = setName + " " + spellLvl;
1930 addAbility(abName,'',spellList,false,charId);
1931
1932 };
1933
1934 /**
1935 * A fancy way of saying, 'deal with greater/lesser/(numearl) for spell names'.
1936 * this is for term linking as the PRD and PFSRD refer to a single page for all
1937 * strength varients.
1938 */
1939 var formatSpellStrength = function(spellName) {
1940 if (!spellName)
1941 {return undefined;}
1942 var retval = spellName;
1943 var strengths = ["mass","lesser","greater","IX","VIII","VII","VI","IV","V","III","II","I"];
1944
1945 _.every(strengths, function(level) {
1946 if (spellName.indexOf(level) !== -1) {
1947 retval = spellName.replace(level,"").trim();
1948 return false;
1949 }
1950 return true;
1951 });
1952 return retval;
1953 };
1954
1955 /**
1956 * Given a string which we recognize as a legal name (spell/feat/whatever)
1957 * and, which we suspect may have a superscript or subscript; remove it.
1958 */
1959 var formatSuperSubScript = function(str) {
1960 if (!str)
1961 {return undefined;}
1962 var retval = str;
1963 var cases = ["1st","2nd","3rd","4th","5th","6th","7th","8th","9th",
1964 "UM","TG","UC","APG","MC","D","B"];
1965 var endsWith = function (str, suffix) {
1966 return str.indexOf(suffix, str.length - suffix.length) !== -1;
1967 };
1968
1969 _.every(cases, function(subscript) {
1970 if (endsWith(str,subscript)) {
1971 retval = str.replace(subscript,"").trim();
1972 return false;
1973 }
1974 return true;
1975 });
1976
1977 return retval;
1978 };
1979
1980 /**
1981 * Format attacks
1982 */
1983 var formatAttacks = function(str, type, charId, label, specials) {
1984 if (!str || !type || !charId)
1985 {return undefined;}
1986 if (!label)
1987 {label = type;}
1988 var volley, attacks, atkList;
1989 var cnt = 0;
1990 var alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M',
1991 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
1992
1993 atkList = "!\n" + fields.menuWhis
1994 + menuTemplate.boundryImg({imgLink: design.atkTopImg})
1995 + menuTemplate.midDiv({imgLink: design.atkMidImg})
1996 + '<div style="text-align:center">'
1997 + menuTemplate.titleFmt({
1998 style: design.atkTitleFmt,
1999 title: creName
2000 })
2001 + menuTemplate.titleFmt({
2002 style: design.atkLabelFmt,
2003 title: type
2004 })
2005 +'</div>';
2006
2007 str = str.replace(type,"").trim();
2008 str = str.replace(/\band\b/g,',');
2009 volley = str.split(/\bor\b/g);
2010 if (volley.length > 1) {
2011 while (volley.length > 0) {
2012 if (!volley[0] || !volley[0].match(/[^\s]+/)) {
2013 volley.shift();
2014 continue;
2015 }
2016 if (alphabet.length <= 0)
2017 {alphabet.unshift(++cnt);}
2018 attacks = volley[0].split(/,(?![^\(\)]*\))/);
2019 atkList += addAttacks(attacks,charId,label,specials,alphabet[0]);
2020 volley.shift();
2021 alphabet.shift();
2022 }
2023 } else {
2024 attacks = str.split(/,(?![^\(\)]*\))/);
2025 atkList += addAttacks(attacks,charId,label,specials);
2026 }
2027
2028 atkList = atkList
2029 + '</div>'
2030 + menuTemplate.boundryImg({imgLink: design.atkBotImg});
2031 addAbility(label,'',atkList,false,charId);
2032
2033
2034 };
2035
2036 /**
2037 * Adds attacks
2038 */
2039 var addAttacks = function(aryList, charId, label, specials, volley) {
2040 if (!aryList || !label || !charId)
2041 {return undefined;}
2042 var attack, atkName, atkMod,
2043 atkRiders, atkDamage, atkIter,
2044 critRange, atkStr, dmgStr,
2045 abName, iterCnt = 0, atkList = "",
2046 atkTitle;
2047
2048 creLog('addAttacks: ' + aryList + " " + label + " " + volley,1);
2049
2050 for (var i = 0; i < aryList.length; ++i) {
2051 if (!aryList[i] || !aryList[i].match(/[^\s]+/)) {
2052 continue;
2053 }
2054 attack = aryList[i].trim();
2055 atkName = attack.match(/\b[^\d\(\)\+\/×]+(?=\+|\-)/);
2056 if (!atkName) {
2057 /* if we don't have an attack name, we either don't have
2058 a modifier or there's a modifier with no name..*/
2059 /* if there is no modifier, the name spans from 0,
2060 to the first paren, absorb the rider into the
2061 name if any; if there's no paren cough up the error ghost.
2062 If there is a modifier, then append a default name*/
2063 if (attack.substring(0,attack.indexOf('(')).match(/\+\-/)) {
2064 atkName = "Basic " + label + " ";
2065 attack = atkName + attack;
2066 } else {
2067 atkName = attack.substring(0,attack.indexOf('(')).trim();
2068 }
2069 } else {
2070 atkName = attack.substring(
2071 0,attack.indexOf(atkName[0])+atkName[0].length).trim();
2072 }
2073 // Ensure there's a damage section
2074 if (attack.match(/\(.+\)/)) {
2075 atkMod = attack.substring(atkName.length,attack.indexOf('(')-1).trim();
2076 atkDamage = attack.substring(attack.indexOf('(')+1,attack.indexOf(')'));
2077 atkRiders = atkMod.match(/\b[^\d\(\)\+\-\/×]+/);
2078 } else {
2079 atkMod = getBonusNumber(attack);
2080 atkDamage = "0";
2081 }
2082
2083 critRange =getCritRange(atkDamage);
2084 atkDamage = formatDamage(atkDamage,specials);
2085 creLog('addAttacks: got damage for ' + attack,1);
2086 // do iteratives:
2087 atkIter = atkMod.split('/');
2088 if (atkIter && atkIter.length > 1) {
2089 while(atkIter.length > 0) {
2090 ++iterCnt;
2091
2092 atkTitle = fields.publicName + " attacks with " + atkName + "!";
2093 atkStr = "[[1d20"+(critRange.range<20 ? ("cs>"+critRange.range) : '')
2094 + atkIter[0] + "]]" + ((critRange.range<20||critRange.multi>2) ?
2095 " (" + ((critRange.range<20 ? (critRange.range + "-20"):"")
2096 + (critRange.multi>2 ? ((critRange.range<20 ? '/×':'×') + critRange.multi):"") + ")"):"")
2097 + (!atkRiders ? '' : ( " ["+atkRiders[0].trim() + "]"));
2098 abName = label + (volley ? ("["+volley+"]") : '')
2099 + "_" + atkName + "(" + iterCnt + ")";
2100 atkStr = "!\n" + fields.publicAnn + applyAtkTemp(fields.tmpAtk,atkTitle,atkStr,atkDamage.damage);
2101 atkStr = atkStr + (atkDamage.rider==="" ? '':('\n'+fields.resultWhis+atkDamage.rider));
2102 addAbility(abName,'',atkStr,false,charId);
2103 atkIter.shift();
2104 atkList = atkList
2105 + menuTemplate.midButton({
2106 riders: undefined,
2107 creName: creName,
2108 abName: abName,
2109 btnName: abName
2110 });
2111 }
2112 } else {
2113 atkTitle = fields.publicName + " attacks with " + atkName + "!";
2114 atkStr = (atkMod !== '' ? "[[1d20"+(critRange.range<20 ? ("cs>"+critRange.range) : '')
2115 + atkMod + "]]" : 'auto-hit') + ((critRange.range<20||critRange.multi>2) ?
2116 " (" + ((critRange.range<20 ? (critRange.range + "-20"):"")
2117 + (critRange.multi>2 ? ((critRange.range<20 ? '/×':'×') + critRange.multi):"") + ")"):"")
2118 + (!atkRiders ? '' : ( " ["+atkRiders[0].trim() + "]"));
2119 abName = label + (volley ? ("["+volley+"]") : '')
2120 + "_" + atkName;
2121 atkStr = "!\n" + fields.publicAnn + applyAtkTemp(fields.tmpAtk,atkTitle,atkStr,atkDamage.damage);
2122 atkStr = atkStr + (atkDamage.rider==="" ? '':('\n'+fields.resultWhis+atkDamage.rider));
2123
2124 addAbility(abName,'',atkStr,false,charId);
2125 atkList = atkList
2126 + menuTemplate.midButton({
2127 riders: undefined,
2128 creName: creName,
2129 abName: abName,
2130 btnName: abName
2131 });
2132 }
2133 creLog('addAttacks: ' + attack + " atkname: '" + atkName + "' atkmod: '" + atkMod + "' atkDam: '"
2134 + atkDamage + "' atkRiders: '" + atkRiders + "'",1);
2135 }
2136 creLog('addAttacks: finished attacks for ' + label,1);
2137 return atkList;
2138 };
2139
2140 /**
2141 * Gets the crit range from the damage string.
2142 */
2143 var getCritRange = function(str) {
2144 if (!str)
2145 {return {range: 20, multi: 2};}
2146 var retval, multi = 2,range = 20,tmp = "";
2147 var terms = str.split("/");
2148
2149 if (terms && terms.length > 0) {
2150 for (var i = 0; i < terms.length; ++i) {
2151 if (terms[i].match(/×\d+/)) {
2152 multi = terms[i].match(/\d+/g);
2153 if (!multi)
2154 {multi = 2;}
2155 } else if ((tmp=terms[i].match(/\b\d+\-\d+/))) {
2156 range = tmp[0].match(/\d+/g);
2157 if (!range)
2158 {range = 2;}
2159 }
2160 }
2161 }
2162
2163 retval = {
2164 range: parseInt(range),
2165 multi: parseInt(multi)
2166 };
2167 return retval;
2168 };
2169
2170 /**
2171 * Format the damage string and attach any riders if any. Be careful about
2172 * subrider semantics.
2173 */
2174 var formatDamage = function(str, specials) {
2175 if (!str)
2176 {return {damage: "", rider: ""};}
2177 var retval;
2178 var damage, damageStr="", damageExpr,
2179 damageTypes,riders,riderStr="",ploc = -1,tmp;
2180 var re = /\d+d\d+/;
2181
2182 creLog("formatDamage str: " + str,2);
2183 damage = re.exec(str);
2184 ploc = str.indexOf('plus');
2185
2186 // if flat, or rider only damage section
2187 if (!damage) {
2188 if (ploc !== -1) {
2189 damageStr = str.substring(0,ploc).match(/\b[\d]+\s/);
2190 if (!damageStr) {
2191 damageStr = str.match(/\b[^\d\/\(\)\+×\s]+\b/);
2192 creLog("damageStr: " + damageStr,3);
2193 if (!damageStr) // give up if things get too weird
2194 {throw "ERROR: Bad damage format: '" + str + "'";}
2195 str = str.substring(0,ploc+4)
2196 + ' ' + damageStr[0].trim() + ','
2197 + str.substring(ploc+4);
2198 }
2199 } else if (!!(damageStr = str.match(/\b[^\d\/\(\)\+×\s]+\b/))) {
2200 str = str + " plus " + damageStr[0].trim();
2201 }
2202 damageStr = '[[0d0]]';
2203 } else {
2204 damageExpr = getExpandedExpr(damage[0],str,damage.index).trim();
2205 damageStr = getFormattedRoll(damageExpr);
2206 // handle damage type
2207 damageTypes = str.match(/\b[^\d\/\(\)\+×]+\b/);
2208 if (damageTypes) {
2209 /* In the case where we have a type we need to get rid of the critical
2210 expression */
2211 creLog("formatDamage Types: " + damageTypes,3);
2212 if (!str.match("plus")) {
2213 tmp = str.indexOf('/');
2214 tmp = str.substring(str.indexOf(damageExpr)+damageExpr.length,
2215 (tmp===-1 ? str.length:tmp)).trim();
2216 damageStr += " " + tmp;
2217 } else {
2218 tmp = str.indexOf('/');
2219 tmp = str.substring(str.indexOf(damageExpr)+damageExpr.length,
2220 (tmp===-1 ? ploc:tmp)).trim();
2221 damageStr += " " + tmp;
2222 }
2223 }
2224 }
2225
2226 // handle riders
2227 if (str.match("plus")) {
2228 riders = str.substring(str.indexOf("plus"));
2229 creLog("riders are: " + riders,3);
2230 riders = riders.replace("plus","");
2231 var ary = riders.split(/,(?![^\(\)]*\))/);
2232 var riderName;
2233 var subRiders;
2234 creLog('ary: ' + ary);
2235 while (ary && ary.length > 0) {
2236 if (!ary[0] || !ary[0].match(/[^\s]+/)) {
2237 ary.shift();
2238 continue;
2239 }
2240 riderName = ary[0].match(/\b[^\d\/\(\)\+×]+/);
2241 // resolve unlabed/typed damage riders
2242 if (!riderName) {
2243 riderName = 'untyped';
2244 ary[0] = riderName + ary[0];
2245 } else {
2246 riderName = riderName[0];
2247 }
2248 subRiders = ary[0].replace(riderName,'').trim();
2249 riderName = riderName.toLowerCase().trim();
2250 creLog("ary val: " + ary[0],4);
2251 creLog("subrider: " + subRiders + " riderName: " + riderName,4);
2252 creLog("specials: " + specials[riderName],4);
2253 if (!specials) {
2254 riderStr +=
2255 "<div>"
2256 + getTermLink(riderName,termEnum.GENERAL)
2257 + " "+(subRiders.match(/[^\s]+/) ? (" "+getFormattedRoll(subRiders)) : '')
2258 + "</div>";
2259 } else {
2260 riderStr +=
2261 "<div>"
2262 + ((specials[riderName] ? getFormattedRoll(getDamageRiderInfo(riderName,specials)) : undefined) || getTermLink(riderName,termEnum.GENERAL))
2263 + " "+(subRiders.match(/[^\s]+/) ? (" "+getFormattedRoll(subRiders,termEnum.GENERAL)) : '')
2264 + "</div>";
2265 }
2266 ary.shift();
2267 }
2268 }
2269 creLog("formatDamage: " + damageStr + " riders: " + riderStr,1);
2270 retval = {
2271 damage: damageStr,
2272 rider: riderStr
2273 };
2274 return retval;
2275 };
2276
2277 /**
2278 * Gets all damage rider information
2279 */
2280 var getDamageRiderInfo = function(riderName,specials) {
2281 var retval = "";
2282 var riderInfo;
2283 var re;
2284 if (specials) {
2285 riderInfo = specials[riderName];
2286 creLog("RiderInfo: " + riderInfo,3);
2287 if (riderInfo) {
2288 while(riderInfo.length > 0) {
2289 re = new RegExp("\\b"+riderName+"\\b",'ig');
2290 riderInfo[0] = riderInfo[0].replace(
2291 re,getTermLink(riderName,termEnum.GENERAL)+" ");
2292 retval += "<p>" + riderInfo[0] + "</p>" ;
2293 creLog("RiderInfoLine: " + retval,3);
2294 riderInfo.shift();
2295 }
2296 }
2297 }
2298 return retval;
2299 };
2300
2301 /**
2302 * Format an attribute by the type desired:
2303 * 0 - scalar, grab the first number
2304 * 1 - pos/neg, grab the first number as well as the sign if any
2305 */
2306 var formatAttribute = function(name, type, charId) {
2307 if (!name || !charId)
2308 {return undefined;}
2309 if (!type)
2310 {type = 0;}
2311 var retval = 0;
2312 var attr;
2313
2314 attr = getAttribute(name, charId);
2315
2316 if (!attr) {
2317 throw "Error: no attribute found '" + name + "'";
2318 }
2319 retval = attr.get("current");
2320
2321 switch (type) {
2322 case 0:
2323 retval = retval.match(/\d+/g)[0];
2324 break;
2325 case 1:
2326 retval = getBonusNumber(retval);
2327 break;
2328 }
2329 creLog("formatAttribute: " + retval + " n: " + name + " t: " + type + " c: " + charId,2);
2330
2331 attr.set('current',retval);
2332 attr.set('max',retval);
2333 return retval;
2334 };
2335
2336 /**
2337 * Apply attack template
2338 */
2339 var applyAtkTemp = function(template,titleStr,atkStr,dmgStr) {
2340 if (!template || !titleStr || !atkStr || !dmgStr)
2341 {return undefined;}
2342 creLog("title: " + titleStr + " atk " + atkStr + " dmg " + dmgStr + template,2);
2343 var retval;
2344 if (!template.match(atkEnum.ATTACK) || !template.match(atkEnum.DAMAGE)) {
2345 log("attack template is malformed");
2346 return retval;
2347 }
2348 retval = template.replace('<<'+atkEnum.TITLE+'>>',titleStr);
2349 retval = retval.replace('<<'+atkEnum.ATTACK+'>>',atkStr);
2350 retval = retval.replace('<<'+atkEnum.DAMAGE+'>>',dmgStr);
2351 return retval;
2352 };
2353
2354 /**
2355 * Add an attribute to a character
2356 */
2357 var addAttribute = function(name, curVal, maxVal, charId) {
2358 if (!curVal)
2359 //{throw 'Cannot add ' + name + ' with no value';}
2360 {curVal = '';}
2361 if (!maxVal)
2362 {maxVal = '';}
2363 creLog("addAttribute: " + fields.charDataPrefix + name + " " + curVal + " " + maxVal + " " + charId,2);
2364
2365 createObj("attribute", {
2366 name: fields.charDataPrefix + name,
2367 current: curVal,
2368 max: maxVal,
2369 characterid: charId
2370 });
2371 };
2372
2373 /**
2374 * Get an attribute from a character
2375 */
2376 var getAttribute = function(name, charId) {
2377 return findObjs({
2378 _type: "attribute",
2379 name: fields.charDataPrefix + name,
2380 _characterid: charId
2381 })[0];
2382 };
2383
2384 /**
2385 * Add an ability to a character
2386 */
2387 var addAbility = function(name, desc, action, isTokenAct, charId) {
2388 createObj("ability", {
2389 name: name,
2390 description: desc,
2391 action: action,
2392 istokenaction: isTokenAct,
2393 characterid: charId
2394 });
2395 };
2396
2397 /**
2398 * Add an ability roll from an attribute
2399 */
2400 var addAttributeRoll = function(name, attrName, isPublic, isTokenAction, charId, rider) {
2401 if (!name,!attr,!charId)
2402 {return undefined;}
2403 if (!isPublic)
2404 {isPublic = false;}
2405 var action = "";
2406 var attr = getAttribute(attrName, charId);
2407 if (!attr) {
2408 throw ("Error: no attribute found '" + name + "'");
2409 }
2410 action = "!\n" + (isPublic ? fields.publicAnn : fields.resultWhis) +
2411 creName + ": " + attrName + ": [[ 1d20"
2412 + getBonusNumber(attr.get('current')) + (rider ? " "+rider:'') + "]]";
2413
2414 addAbility(name,"",action,isTokenAction,charId);
2415 };
2416
2417 /**
2418 * Given a line containing the most attributes, add them to the
2419 * character
2420 */
2421 var addAttrList = function(data,line, aryList, startFnd, termChars, charId) {
2422 if (!aryList || !termChars || !charId)
2423 {return undefined;}
2424 if (!line)
2425 {line = data[0];}
2426 if (!startFnd)
2427 {startFnd = 0;}
2428 var rc;
2429
2430 creLog("addAttrList: " + startFnd + " " + termChars + " " + charId + " " + line + " " + (!!aryList),3);
2431 while(aryList.length > 0) {
2432 rc = getValueByName(aryList[0],line,termChars);
2433 if (!rc) {
2434 var nextBestLine = getLineByName(aryList[0],data,startFnd);
2435 rc = getValueByName(aryList[0],nextBestLine,termChars);
2436 if (!rc) {
2437 throw ("ERROR: could not find attribute " + aryList[0]);
2438 }
2439 }
2440 addAttribute(aryList[0],rc,rc,charId);
2441 aryList.shift();
2442 }
2443 };
2444
2445 /**
2446 * Return a link whose format depends on type
2447 */
2448 var getTermLink = function(str,type,label) {
2449 if (!str || !type)
2450 {return undefined;}
2451 if (!label)
2452 {label = str;}
2453 var retval = str;
2454 switch(type) {
2455 case termEnum.GENERAL:
2456 retval = getFormattedUrl(str,fields.urlTermGeneral,label);
2457 break;
2458 case termEnum.SPELL:
2459 retval = getFormattedUrl(str,fields.urlTermSpell,label);
2460 break;
2461 case termEnum.FEAT:
2462 retval = getFormattedUrl(str,fields.urlTermFeat,label);
2463 break;
2464 case termEnum.SQ:
2465 retval = getFormattedUrl(str,fields.urlTermSQ,label);
2466 break;
2467 case termEnum.SA:
2468 retval = getFormattedUrl(str,fields.urlTermSA,label);
2469 break;
2470 default:
2471 retval = str;
2472 }
2473 creLog("getTermLink: " + retval,3);
2474 return retval;
2475 };
2476
2477 /**
2478 * Return a formatted URL by filling in placeholders. with str and label
2479 */
2480 var getFormattedUrl = function(str,url,label) {
2481 if (!str || !url)
2482 {return undefined;}
2483 if (!label)
2484 {label = str;}
2485 var retval;
2486 var re = /<<\w+>>/g;
2487 var matches, cond;
2488
2489 creLog("formatting str: '" + str + "' on url: '" + url + "'",4);
2490 if ((matches=url.match(re))) {
2491 while (matches.length > 0) {
2492 creLog("formaturl: " + matches[0],5);
2493 matches[0] = matches[0].replace(/<<|>>/g,"");
2494 if ((cond=matches[0].match(/\D+/))) {
2495 creLog("formaturl word: " + cond[0],5);
2496 switch(cond[0]) {
2497 case urlCondEnum.FULL:
2498 url=url.replace("<<"+urlCondEnum.FULL+">>",str);
2499 break;
2500 case urlCondEnum.LABEL:
2501 url=url.replace("<<"+urlCondEnum.LABEL+">>",label);
2502 break;
2503 default:
2504 creLog("unsupported url subsitution: " + matches[0],1);
2505 }
2506
2507 } else if ((cond=matches[0].match(/\d+/))) {
2508 cond = parseInt(cond);
2509 creLog("formaturl char: " + cond,5);
2510 if (cond < str.length)
2511 {url=url.replace("<<"+matches[0]+">>",str[cond]);}
2512 else
2513 {creLog("illegal string index of " + cond + " in '" + str + "'",1);}
2514 }
2515 matches.shift();
2516 }
2517 }
2518 retval = url;
2519 creLog("formattedUrl: " + retval,4);
2520 return retval;
2521 };
2522
2523 /**
2524 * Get the bonus number in the string, carefully looking for signs to
2525 * preserve negatives and what not. TODO there's a Regex solution to this
2526 * which is way shorter.. also add type for scalars.
2527 */
2528 var getBonusNumber = function(str,type) {
2529 if (!str)
2530 {return 0;}
2531 if (!type)
2532 {type = bonusEnum.SIGN;}
2533 var retval = 0;
2534 var locStart = 0;
2535 var locEnd = str.length;
2536 var num;
2537
2538 str = str.replace(/\s/g,"");
2539 switch (type) {
2540 case bonusEnum.SCALAR:
2541 num = str.match(/\d+/);
2542 if (num)
2543 {retval = num[0];}
2544 else
2545 {retval = "0";}
2546 break;
2547 case bonusEnum.SIGN:
2548 num = str.match(/\+*\-*\d+/);
2549 if (num) {
2550 if (!num[0].match(/\+|\-/))
2551 {retval = "+" + num[0];}
2552 else
2553 {retval = num[0];}
2554 } else
2555 {retval = "+0";}
2556 break;
2557 default:
2558 // impossible
2559 return undefined;
2560 }
2561 return retval;
2562 };
2563
2564 /**
2565 * Return the string with the roll formatted, this is accomplished by simply
2566 * surrounding roll equations with [[ ]] TODO, should be replaced with a
2567 * single regex
2568 *
2569 */
2570 var getFormattedRoll = function(str) {
2571 if (!str)
2572 {return "";}
2573 var retval = str;
2574 var re = /\d+d\d+/;
2575 var idx, expr, roll, pre, post;
2576
2577 if (!!(roll=re.exec(str))) {
2578 expr = getExpandedExpr(roll[0],str,roll.index);
2579 idx = str.indexOf(expr);
2580 pre = str.substring(0,idx);
2581 post = str.substring(idx+expr.length);
2582 } else { return str;}
2583
2584 return pre+"[["+expr+"]]"+getFormattedRoll(post);
2585 };
2586
2587 /**
2588 * Return the target expression expanded as far as it logically can span
2589 * within the provided line.
2590 *
2591 * ie: target = 1d20
2592 * locHint = 4
2593 * line = "2+1d20+5+2d4 bla (bla 1d20+8 bla) bla (4d8...) bla bla"
2594 *
2595 * result = 2+1d20+5+2d4
2596 */
2597 var getExpandedExpr = function(target, line, locHint) {
2598 if (!target || !line)
2599 {return undefined;}
2600 if (!locHint)
2601 {locHint = 0;}
2602 var retval = target;
2603 var expr = target;
2604 var re = /\d|[\+\-]|d/;
2605 var loc = -1, start = 0, end = 0;
2606
2607 if((loc=line.indexOf(target,locHint)) !== -1) {
2608 start = loc;
2609 while (start > 0) {
2610 if (line[start].match(re))
2611 {start--;}
2612 else
2613 {start++;break;}
2614 }
2615 end = loc;
2616 while (end < line.length) {
2617 if (line[end].match(re))
2618 {end++;}
2619 else
2620 {break;}
2621 }
2622 retval = line.substring(start,end);
2623 creLog("getExpandedExpr: '"
2624 + retval + "' s: " + start + " e: " + end + " t: " + target + " l: " + line,4);
2625 retval = getLegalRollExpr(retval);
2626 }
2627
2628 return retval;
2629 };
2630
2631 /**
2632 * Gets a legal roll expression.
2633 * TODO strip trailing operands +1d6 and such
2634 */
2635 var getLegalRollExpr = function(expr) {
2636 if (!expr)
2637 {return undefined;}
2638 var retval = expr;
2639 var stray = expr.match(/d/g);
2640 var valid = expr.match(/\d+d\d+/g);
2641 var errMsg = "Illegal expression " + expr;
2642
2643 try {
2644 if (expr.match(/[^\s\d\+-d]/g)
2645 || !stray
2646 || !valid
2647 || (stray.length =! valid.length))
2648 {throw errMsg;}
2649 stray = expr.match(/\+/g);
2650 valid = expr.match(/\d+\+\d+/g);
2651 if (stray && valid
2652 && (stray.length !== valid.length))
2653 {throw errMsg;}
2654 stray = expr.match(/-/g);
2655 valid = expr.match(/\d+-\d+/g);
2656 if (stray && valid
2657 && (stray.length !== valid.length))
2658 {throw errMsg;}
2659 } catch (e) {
2660 creLog(e,1);
2661 throw e;
2662 }
2663
2664 //check for leading, trailing, operands
2665 if (retval[0].match(/\+|\-/))
2666 {retval = retval.substring(1);}
2667 if (retval[retval.length-1].match(/\+|\-/))
2668 {retval = retval.substring(0,retval.length-1);}
2669
2670 creLog("getLegalRollExpr: " + retval,4);
2671 return retval;
2672 };
2673
2674 /**
2675 * Given a name, array of lines, and a start/end location, find the first
2676 * line that contains the given name.
2677 */
2678 var getLineByName = function(strName, aryLines, locStart, locEnd) {
2679 creLog('getLineByName: name ' + strName + ' data ' + !!aryLines,5);
2680 if (!strName || !aryLines)
2681 {return undefined;}
2682 if (!locStart)
2683 {locStart = 0;}
2684 if (!locEnd)
2685 {locEnd = aryLines.length;}
2686 var retval;
2687
2688 creLog("getLineByName: " + strName + " " + locStart + " " + locEnd + " " + (!!aryLines),5);
2689 for (var i = locStart; i < locEnd; ++i) {
2690 if (aryLines[i].indexOf(strName) !== -1) {
2691 retval = aryLines[i];
2692 break;
2693 }
2694 }
2695 creLog('getLineByName: name ' + strName + ' value \'' + retval,5);
2696 return retval;
2697 };
2698
2699 /**
2700 * Given a name, array of lines, and a start/end location, find the first
2701 * line # that contains the given name.
2702 */
2703 var getLineNumberByName = function(strName, aryLines, locStart, locEnd) {
2704 if (!strName || !aryLines)
2705 {return undefined;}
2706 if (!locStart)
2707 {locStart = 0;}
2708 if (!locEnd)
2709 {locEnd = aryLines.length;}
2710 var retval;
2711
2712 creLog("getLineNumberByName: " + strName + " " + locStart + " " + locEnd + " " + (!!aryLines),5);
2713 for (var i = locStart; i < locEnd; ++i) {
2714 if (aryLines[i].indexOf(strName) !== -1) {
2715 retval = i;
2716 break;
2717 }
2718 }
2719 return retval;
2720 };
2721
2722 /**
2723 * Given a line, name, and terminators return the value, value is
2724 * the the trimed text after the name and before the terminator.
2725 */
2726 var getValueByName = function(strName, strLine, termChars) {
2727 if (!strLine || !strName || !termChars)
2728 {return undefined;}
2729 var retval;
2730 var loc = -1;
2731 var locTerm = strLine.length;
2732
2733 creLog("getValueByName: " + strName + " " + termChars + " " + strLine,5);
2734 if ((loc=strLine.indexOf(strName)) !== -1) {
2735 for (var i = 0; i < termChars.length; ++i) {
2736 var tmp = strLine.indexOf(termChars[i],loc);
2737 if ((tmp !== -1) && (tmp < locTerm))
2738 {locTerm = tmp;}
2739 }
2740 if (locTerm > loc) {
2741 locTerm = getParenSafeTerm(
2742 strLine,loc,locTerm,termChars);
2743 retval = strLine.substring(loc+strName.length,locTerm);
2744 }
2745 }
2746 return retval;
2747 };
2748
2749 /**
2750 * Get the location of the closest terminator that is paren safe. If there
2751 * are parens. Probably a faster way exists using regex and exec..
2752 */
2753 var getParenSafeTerm = function(strLine, start, end, termChars) {
2754 var newTerm = -1;
2755 var inParen = 0;
2756 var closeLoc = -1;
2757 var i;
2758
2759 if (start >= end)
2760 {return end;}
2761 for (i = start; i < strLine.length; ++i) {
2762 if (strLine[i] === '(')
2763 {inParen++;}
2764 else if (strLine[i] ===')')
2765 {inParen--;}
2766 if (i >= end) {
2767 if (inParen <= 0)
2768 {return end;}
2769 else if (inParen > 0)
2770 {break;}
2771 }
2772 }
2773 if (inParen <= 0)
2774 {return end;}
2775
2776 // if we found we're in parens
2777 creLog("in parens for " + strLine + " openparens: " + inParen,5);
2778 closeLoc = strLine.indexOf(')',start);
2779 end = strLine.length;
2780 if (closeLoc === -1)
2781 {return end;}
2782 for (i = 0; i < termChars.length; ++i) {
2783 if (-1 === (newTerm=strLine.indexOf(termChars[i],closeLoc)))
2784 {newTerm = strLine.length;}
2785 else if (newTerm < end)
2786 {end = newTerm;}
2787 }
2788 return end;
2789 };
2790
2791 /**
2792 * check if the character object exists, return first match
2793 */
2794 var characterObjExists = function(name, type, charId) {
2795 var retval;
2796 var obj = findObjs({
2797 _type: type,
2798 name: name,
2799 _characterid: charId
2800 });
2801 creLog("type: " + type + " name: " + name + " charId: " + charId + " retval: " + obj,5);
2802 if (obj.length > 0)
2803 {retval = obj[0];}
2804 return retval;
2805 };
2806
2807 /** removes all occurence of removeStr in str and replaces them with
2808 * replaceWidth
2809 *
2810 * @author Andy W.
2811 */
2812 var stripString = function(str, removeStr, replaceWith) {
2813 while (str.indexOf(removeStr) !== -1) {
2814 str = str.replace(removeStr, replaceWith);
2815 }
2816 return str;
2817 };
2818
2819 /**
2820 * Cleans the string preserving select special characters and dropping the
2821 * remainder.
2822 *
2823 * @author Andy W.
2824 * @contributor Ken L.
2825 */
2826 var cleanString = function(strSpecials) {
2827 strSpecials = stripString(strSpecials, "%20", ' ');
2828 strSpecials = stripString(strSpecials, "%22", '"');
2829 strSpecials = stripString(strSpecials, "%29", ')');
2830 strSpecials = stripString(strSpecials, "%28", '(');
2831 strSpecials = stripString(strSpecials, "%2C", ',');
2832 strSpecials = stripString(strSpecials, "%42", '');
2833 strSpecials = stripString(strSpecials, "*", '');
2834 strSpecials = stripString(strSpecials, '\n', '');
2835 strSpecials = stripString(strSpecials, '%3Cbr', '');
2836
2837 strSpecials = stripString(strSpecials, "%09", ' ');
2838 strSpecials = stripString(strSpecials, "%3C", '<');
2839 strSpecials = stripString(strSpecials, "%3E", '>');
2840 strSpecials = stripString(strSpecials, "%23", '#');
2841 strSpecials = stripString(strSpecials, "%3A", ':');
2842 strSpecials = stripString(strSpecials, "%3B", ';');
2843 strSpecials = stripString(strSpecials, "%3D", '=');
2844 strSpecials = stripString(strSpecials, "%D7", '×');
2845 strSpecials = stripString(strSpecials, "%u2018", '');
2846 strSpecials = stripString(strSpecials, "%u2019", '');
2847 strSpecials = stripString(strSpecials, "%u2013", '-');
2848 strSpecials = stripString(strSpecials, "%u2014", '—');
2849 strSpecials = stripString(strSpecials, "%u201C", '“');
2850 strSpecials = stripString(strSpecials, "%u201D", 'â€');
2851
2852
2853 while (strSpecials.search(/%../) !== -1) {
2854 strSpecials = strSpecials.replace(/%../, "");
2855 }
2856
2857 strSpecials = strSpecials.replace(/<[^<>]+>|<\/[^<>]+>/g,'');
2858 //strSpecials = strSpecials.replace(/<(?:.|\n)*?>/gm, '');
2859 return strSpecials;
2860 };
2861
2862 /**
2863 * Logging, store it
2864 */
2865 var creLog = function(msg, lvl) {
2866 if (!msg)
2867 {return undefined;}
2868 if (!lvl)
2869 {lvl = 1;}
2870 if (dmesg) {
2871 dmesg.push('['+lvl+']:' + " " + msg);
2872 } else {
2873 dmesg = new Array('['+lvl+']:' + " " + msg);
2874 }
2875 };
2876
2877 /**
2878 * Dump it.
2879 */
2880 var creLogDump = function(lvl) {
2881 if (dmesg) {
2882 log('--- Dumping log at level ['+lvl+'] ---');
2883 _.every(dmesg, function(line) {
2884 var clvl = line.match(/\d+/);
2885 if (clvl && (parseInt(clvl[0].trim()) <= lvl))
2886 {log(line);}
2887 return true;
2888 });
2889 log('--- Log dump at level ['+lvl+'] complete ---');
2890 } else {log("No log found");}
2891 };
2892
2893 /**
2894 * Add warnings
2895 */
2896 var addWarning = function(msg) {
2897 if (warn)
2898 {warn.push(msg);}
2899 else
2900 {warn = new Array(msg);}
2901 };
2902
2903 /**
2904 * Send warnings
2905 */
2906 var sendWarnings = function(token,msg) {
2907 var content = '';
2908 if (warn) {
2909 _.every(warn, function(elem) {
2910 content += '<p>'
2911 + '<span style="color: #FF9100; font-weight: bold">Warning: </span>'
2912 + elem + '</p>';
2913 return true;
2914 });
2915 content += (msg ? msg:'');
2916 sendFeedback(content,design.warningImg,token.get('imgsrc'));
2917 }
2918 };
2919
2920 /**
2921 * get warning block
2922 */
2923 var getWarningBlock = function(msg) {
2924 var content = '';
2925 if (warn) {
2926 _.every(warn, function(elem) {
2927 content += '<p>'
2928 + '<span style="color: #FF9100; font-weight: bold">Warning: </span>'
2929 + elem + '</p>';
2930 return true;
2931 });
2932 content += (msg ? msg:'');
2933 //return getFeedbackBlock(content,design.warningImg,token.get('imgsrc'));
2934 return content;
2935 }
2936 };
2937
2938 /**
2939 * Fake message is fake!
2940 */
2941 var sendFeedback = function(msg,img,tokenImg) {
2942 var content = '/w GM '
2943 + '<div style="position: absolute; top: 4px; left: 5px; width: 26px;">'
2944 + '<img src="' + design.feedbackImg + '">'
2945 + '</div>'
2946 + msg;
2947 if (tokenImg && img) {
2948 content = content
2949 + '<div style="position: relative">'
2950 + '<div style="position: relative; width: 70px; height: 70px; overflow: hidden;">'
2951 + '<img src="' + tokenImg + '" style="position: relative; width: 70px; height: 70px;">'
2952 + '</div>'
2953 + '<div style="position: absolute; top: 0px; width: 70px; height: 70px;">'
2954 + '<img src="' + img + '" style="position: relative;">'
2955 + '</div>'
2956 + '</div>';
2957 }
2958
2959 sendChat(design.feedbackName,content);
2960 };
2961
2962 /**
2963 * Fake message block
2964 */
2965 var getFeedbackBlock = function(msg,img,tokenImg) {
2966 var content = msg;
2967 if (tokenImg && img) {
2968 content +=
2969 '<div>'
2970 + '<div style="display: inline-block; position: relative; width: 70px; height: 70px; overflow: hidden;">'
2971 + '<img src="' + tokenImg + '" style="position: relative; width: 70px; height: 70px;">'
2972 + '</div>'
2973 + '<div style="display: inline-block; position: absolute; left: 45px; width: 70px; height: 70px; overflow: hidden;">'
2974 + '<img src="' + img + '" style="position: relative;">'
2975 + '</div>'
2976 + '</div>';
2977 }
2978 return content;
2979 };
2980
2981 /**
2982 * Performs Genesis with a default name other than 'Creature''
2983 */
2984 var doNameGenesis = function(msg, name) {
2985 if (!msg || !name)
2986 {return undefined;}
2987 var originalDefaultName = fields.defaultName;
2988
2989 fields.defaultName = name;
2990 doGenesis(msg);
2991 fields.defaultName = originalDefaultName;
2992 };
2993
2994 /**
2995 * Performs Genesis granting control to a player
2996 */
2997 var doPlayerGenesis = function(msg, playerName) {
2998 if (!msg || !playerName)
2999 {return undefined;}
3000 var players;
3001 var name;
3002 var targets = [];
3003 var targetName;
3004 var targetPlayer;
3005 var levDiff = 0;
3006 var minDiff = 255; // good enough
3007 var originalMenuWhis = fields.menuWhis;
3008 var originalResultWhis = fields.resultWhis;
3009 var originalName = fields.publicName;
3010 var originalPublicAnn = fields.publicAnn;
3011 var re = new RegExp(playerName,'i');
3012 players = findObjs({
3013 _type: "player",
3014 });
3015
3016 _.every(players, function(elem) {
3017 name = elem.get('_displayname').toLowerCase();
3018 if (name.match(re)) {
3019 targets.push(elem);
3020 }
3021 return true;
3022 });
3023
3024 if (targets.length < 1) {
3025 sendFeedback('Could not find player: ' + playerName);
3026 return undefined;
3027 } else if (targets.length === 1) {
3028 targetName = targets[0].get('_displayname').toLowerCase();
3029 targetPlayer = targets[0];
3030 } else {
3031 targetName = targets[0].get('_displayname').toLowerCase();
3032 _.every(targets, function(elem) {
3033 name = elem.get('_displayname').toLowerCase();
3034 levDiff = getLev(playerName,name);
3035 if (levDiff < minDiff) {
3036 minDiff = levDiff;
3037 targetName = name;
3038 targetPlayer = elem;
3039 }
3040 return true;
3041 });
3042 }
3043 fields.menuWhis = '/w "'+targetName+'" ';
3044 fields.resultWhis = '';
3045 fields.publicAnn = '';
3046 fields.summoner = targetPlayer;
3047 doGenesis(msg);
3048 var pgenWork = function(args) {
3049 fields.summoner = undefined;
3050 fields.publicAnn = originalPublicAnn;
3051 fields.publicName = originalName;
3052 fields.resultWhis = originalResultWhis;
3053 fields.menuWhis = originalMenuWhis;
3054 }
3055 workList.push({workFunc: pgenWork});
3056 };
3057
3058 var getLev = function(s1, s2, cost_ins, cost_rep, cost_del) {
3059 // discuss at: http://phpjs.org/functions/levenshtein/
3060 // original by: Carlos R. L. Rodrigues (http://www.jsfromhell.com)
3061 // bugfixed by: Onno Marsman
3062 // revised by: Andrea Giammarchi (http://webreflection.blogspot.com)
3063 // reimplemented by: Brett Zamir (http://brett-zamir.me)
3064 // reimplemented by: Alexander M Beedie
3065 // reimplemented by: Rafał Kukawski
3066 // example 1: levenshtein('Kevin van Zonneveld', 'Kevin van Sommeveld');
3067 // returns 1: 3
3068 // example 2: levenshtein("carrrot", "carrots");
3069 // returns 2: 2
3070 // example 3: levenshtein("carrrot", "carrots", 2, 3, 4);
3071 // returns 3: 6
3072 var LEVENSHTEIN_MAX_LENGTH = 255; // PHP limits the function to max 255 character-long strings
3073
3074 cost_ins = cost_ins === undefined ? 1 : +cost_ins;
3075 cost_rep = cost_rep === undefined ? 1 : +cost_rep;
3076 cost_del = cost_del === undefined ? 1 : +cost_del;
3077
3078 if (s1 === s2) {
3079 return 0;
3080 }
3081
3082 var l1 = s1.length;
3083 var l2 = s2.length;
3084
3085 if (l1 === 0) {
3086 return l2 * cost_ins;
3087 }
3088 if (l2 === 0) {
3089 return l1 * cost_del;
3090 }
3091
3092 // Enable the 3 lines below to set the same limits on string length as PHP does
3093 /*if (l1 > LEVENSHTEIN_MAX_LENGTH || l2 > LEVENSHTEIN_MAX_LENGTH) {
3094 return -1;
3095 }*/
3096
3097 // BEGIN STATIC
3098 var split = false;
3099 try {
3100 split = !('0')[0];
3101 } catch (e) {
3102 // Earlier IE may not support access by string index
3103 split = true;
3104 }
3105 // END STATIC
3106 if (split) {
3107 s1 = s1.split('');
3108 s2 = s2.split('');
3109 }
3110
3111 var p1 = new Array(l2 + 1);
3112 var p2 = new Array(l2 + 1);
3113
3114 var i1, i2, c0, c1, c2, tmp;
3115
3116 for (i2 = 0; i2 <= l2; i2++) {
3117 p1[i2] = i2 * cost_ins;
3118 }
3119
3120 for (i1 = 0; i1 < l1 ; i1++) {
3121 p2[0] = p1[0] + cost_del;
3122
3123 for (i2 = 0; i2 < l2; i2++) {
3124 c0 = p1[i2] + ((s1[i1] === s2[i2]) ? 0 : cost_rep);
3125 c1 = p1[i2 + 1] + cost_del;
3126
3127 if (c1 < c0) {
3128 c0 = c1;
3129 }
3130
3131 c2 = p2[i2] + cost_ins;
3132
3133 if (c2 < c0) {
3134 c0 = c2;
3135 }
3136
3137 p2[i2 + 1] = c0;
3138 }
3139
3140 tmp = p1;
3141 p1 = p2;
3142 p2 = tmp;
3143 }
3144
3145 c0 = p1[l2];
3146
3147 return c0;
3148 };
3149
3150 /**
3151 * "And then the GM said, let there be monsters!"
3152 *
3153 * Performs Creature Generation
3154 */
3155 var doGenesis = function(msg) {
3156 var token;
3157 var content = '';
3158
3159 if (!(msg.selected && msg.selected.length > 0)) {
3160 sendFeedback("no token selected for creature creation");
3161 return;
3162 }
3163
3164 if (msg.selected.length > 1) {
3165 sendFeedback( '<div style="font-weight: bold; color: #7AB6FF; font-size: 150%">'
3166 + 'Starting group generation</div>');
3167 }
3168 locked = true;
3169 workStart = Date.now();
3170
3171 _.each(msg.selected, function(e) {
3172 var genesisWork = function(e) {
3173 token = getObj('graphic', e._id);
3174 if ((token && (token.get('_subtype') !== 'token')) || !token) {
3175 sendFeedback(
3176 '<span style="font-weight: bold; color: #FF0000;">Invalid Selection</span>'
3177 + (msg.selected.length > 1 ? ('<div style="color: black; font-style: italic; font-weight: bold;">('+(msg.selected.length-workList.length+1)+'/'+msg.selected.length+')</div>'):'')
3178 );
3179 return;
3180 }
3181 try {
3182 dmesg = undefined;
3183 warn = undefined;
3184 scan(token);
3185 sendFeedback(
3186 (warn ? getWarningBlock():'')
3187 + '<span style="font-weight: bold; color: '+(warn ? '#FF9100':'#08AF12')+';">'
3188 + token.get('name')
3189 + '</span>'
3190 + ' has been generated ' + (warn ? 'with warnings.':'successfully!')
3191 + (msg.selected.length > 1 ? ('<div style="color: black; font-style: italic; font-weight: bold;">('+(msg.selected.length-workList.length+1)+'/'+msg.selected.length+')</div>'):''),
3192 (warn ? design.warningImg:design.successImg),
3193 token.get('imgsrc'));
3194 //creLogDump(5);
3195 character = undefined;
3196 } catch (err) {
3197 log("GENESIS ERROR: " + err);
3198 sendFeedback(
3199 (warn ? getWarningBlock():'')
3200 + '<span style="font-weight: bold; color: #FF0000;">'
3201 + 'There was an error during token generation.'
3202 + '</span> '
3203 + 'Please see the log for details, and delete the erroneous journal entry.'
3204 + (msg.selected.length > 1 ? ('<div style="color: black; font-style: italic; font-weight: bold;">('+(msg.selected.length-workList.length+1)+'/'+msg.selected.length+')</div>'):''),
3205 design.errorImg,
3206 token.get("imgsrc"));
3207 creLogDump(debugLvl);
3208 warn = undefined;
3209 character = undefined;
3210 log("-----Please ensure that the statistics block is properly formatted.-----");
3211 }
3212 };
3213
3214 workList.push({workFunc: genesisWork, workData: [e]});
3215 },this);
3216
3217 if (msg.selected.length > 1) {
3218 workList.push({workFunc: function() {
3219 var tTime = (Date.now() - workStart)/1000;
3220 sendFeedback(
3221 '<div style="font-weight: bold; color: #08AF12; font-size: 150%">'
3222 + 'Group generation complete </div>'
3223 + '<div style="color: blue; font-size: 100%; font-style: italic: font-weight: bold">(Time elapsed: '+tTime.toFixed(2)+'s)</div>');
3224 locked = false;
3225 }, workData: []});
3226 } else {
3227 locked = false;
3228 }
3229
3230 doDelayedWork(workList);
3231 };
3232
3233 /**
3234 * Delayed Worktask
3235 */
3236 var doDelayedWork = function() {
3237 if (!workList || !workList.length)
3238 {return;}
3239
3240 var payload = workList.shift();
3241 if (!payload)
3242 {return;}
3243 payload.workFunc.apply(undefined,payload.workData);
3244 setTimeout(doDelayedWork,workDelay);
3245 };
3246
3247
3248 /**
3249 * Show help
3250 */
3251 var showHelp = function() {
3252 var content;
3253 var designTmpList = "";
3254 var attackTmpList = "";
3255
3256 if (typeof(CGTmp) !== "undefined") {
3257 try {
3258 _.every(_.keys(CGTmp.designTmp), function(tmp) {
3259 designTmpList += '<div>' + '<a href="!CreatureGen -set-design ' + tmp + '">' + tmp + ' </a></div>';
3260 return true;
3261 });
3262 _.every(_.keys(CGTmp.attackTmp), function(tmp) {
3263 attackTmpList += '<div>' + '<a href="!CreatureGen -set-attack ' + tmp + '">' + tmp + ' </a></div>';
3264 return true;
3265 });
3266 } catch (ex) {
3267 log("ERROR accessing CGTmp: " + e);
3268 designTmpList = "";
3269 attackTmpList = "";
3270 }
3271 }
3272 content = '<div style="background-color: #FFFFFF; border: 1px solid black; left-margin 5x; right margin 5px; padding-top: 5px; padding-bottom: 5px;;">'
3273 + '<div style="border-bottom: 1px solid black;">'
3274 + '<span style="font-weight: bold; font-size: 150%">CreatureGen v'+version+'</span>'
3275 + '</div>'
3276 + '<div style="padding-left: 10px; padding-right: 10px;">'
3277 + '<div>'
3278 + '<span style="font-weight: bold;">!CreatureGen -help</span>'
3279 + '</div>'
3280 + '<li style="padding-left: 10px;">'
3281 + 'Display this message'
3282 + '</li>'
3283 + '<div>'
3284 + '<span style="font-weight: bold;">!CreatureGen</span>'
3285 + '</div>'
3286 + '<li style="padding-left: 10px;">'
3287 + 'Generate creature'
3288 + '</li>'
3289 + '<div>'
3290 + '<span style="font-weight: bold;">!CreatureGen -name [name]</span>'
3291 + '</div>'
3292 + '<li style="padding-left: 10px;">'
3293 + 'Generate creature with the given default name'
3294 + '</li>'
3295 + '<div>'
3296 + '<span style="font-weight: bold;">!CreatureGen -player [name]</span>'
3297 + '</div>'
3298 + '<li style="padding-left: 10px;">'
3299 + 'Generate player controlled creature (a summon)'
3300 + '</li>'
3301 + '<div>'
3302 + '<span style="font-weight: bold;">!CreatureGen -set-design [name]</span>'
3303 + '</div>'
3304 + '<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;">'
3305 + (state.cgen_design ? state.cgen_design : "Default")
3306 + '</div>'
3307 + '<li style="padding-left: 10px;">'
3308 + 'Set design template'
3309 + '</li>'
3310 + (designTmpList==='' ? '' : '<div style="border: 1px solid blue; text-align: center;"><span style="text-decoration: underline;">Available design templates:</span>')
3311 + designTmpList
3312 + (designTmpList==='' ? '' : '</div>')
3313 + '<div>'
3314 + '<span style="font-weight: bold;">!CreatureGen -set-attack [name]</span>'
3315 + '</div>'
3316 + '<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;">'
3317 + (state.cgen_attack ? state.cgen_attack : "Default")
3318 + '</div>'
3319 + '<li style="padding-left: 10px;">'
3320 + 'Set attack template'
3321 + '</li>'
3322 + (attackTmpList==='' ? '' : '<div style="border: 1px solid blue; text-align: center;"><span style="text-decoration: underline;">Available attack templates:</span>')
3323 + attackTmpList
3324 + (attackTmpList==='' ? '' : '</div>')
3325 + '<div>'
3326 + '<span style="font-weight: bold;">!CreatureGen -dmesg [lvl]</span>'
3327 + '</div>'
3328 + '<li style="padding-left: 10px;">'
3329 + 'Dump to API-output console debug at the specified level (1-5) for the last token generated.'
3330 + '</li>'
3331 + '</div>'
3332
3333 + '</div>'
3334 + '</div>';
3335 sendFeedback(content);
3336 };
3337
3338 /**
3339 * Handle chat messages
3340 */
3341 var handleChatMessage = function(msg) {
3342 var cmdName = "!CreatureGen";
3343 var msgTxt = msg.content;
3344 var args;
3345 if ((msg.type === "api")
3346 && (msgTxt.indexOf(cmdName) !== -1)
3347 && playerIsGM(msg.playerid)) {
3348 args = msgTxt.replace(cmdName,'').trim().toLowerCase();
3349 if (args !== "") {
3350 if (locked) {
3351 sendFeedback('<span style="color: #FF8D0B; font-weight: bold;">'
3352 + ' BUSY </span>');
3353 } else if (args.indexOf('-set-design') === 0) {
3354 args = args.replace('-set-design','').trim();
3355 selectDesignTemplate(args);
3356 } else if(args.indexOf('-set-attack') === 0) {
3357 args = args.replace('-set-attack','').trim();
3358 selectAttackTemplate(args);
3359 } else if (args.indexOf('-help') === 0) {
3360 showHelp();
3361 } else if (args.indexOf('-dmesg') === 0) {
3362 var level = 0;
3363 args = args.replace('-dmesg','').trim();
3364 level = getBonusNumber(args,bonusEnum.SCALAR);
3365 creLogDump(level);
3366 sendFeedback('<span style="color: #FF8D0B;">'
3367 + 'Dumping debug from last <b>GENESIS</b> at level ('+level+')'
3368 + '</span>');
3369 } else if (args.indexOf('-player') === 0) {
3370 args = args.replace('-player','').trim();
3371 doPlayerGenesis(msg,args);
3372 } else if (args.indexOf('-name') === 0) {
3373 args = args.replace('-name','').trim();
3374 doNameGenesis(msg,args);
3375 } else {
3376 sendFeedback("Unknown CreatureGen command '"+args+"'");
3377 showHelp();
3378 }
3379
3380 } else if (locked) {
3381 sendFeedback('<span style="color: #FF8D0B; font-weight: bold;">'
3382 + ' BUSY </span>');
3383 } else {
3384 doGenesis(msg);
3385 }
3386 }
3387 };
3388
3389 /**
3390 * Handle new graphics added
3391 */
3392 var handleAddGraphic = function(obj) {
3393 var type;
3394 var charSheet;
3395 var charId;
3396 if (!!(type=obj.get('_subtype'))) {
3397 if (type === 'token') {
3398 charSheet = obj.get('represents');
3399 if (charSheet) {
3400 charSheet = getObj("character", charSheet);
3401 if (charSheet) {
3402 prepToken(obj,charSheet);
3403 }
3404 }
3405 }
3406 }
3407 };
3408
3409 return {
3410
3411 /**
3412 * Register Roll20 handlers
3413 */
3414 registerAPI : function() {
3415 on('chat:message',handleChatMessage);
3416 on('add:graphic',handleAddGraphic);
3417 },
3418
3419 init: function() {
3420 var rc = true;
3421 fields.tmpAtk = atkTemplate;
3422 if (state.cgen_design)
3423 {rc = selectDesignTemplate(state.cgen_design.toLowerCase(),true);}
3424 if (!rc) {
3425 log("ERROR: state design template no longer exists, defaulting..");
3426 state.cgen_design = undefined;
3427 }
3428 if (state.cgen_attack)
3429 {rc = selectAttackTemplate(state.cgen_attack.toLowerCase(),true);}
3430 if (!rc) {
3431 log("ERROR: state attack template no longer exists, defaulting..");
3432 state.cgen_attack = undefined;
3433 }
3434 }
3435 };
3436
3437}());
3438
3439
3440
3441on("ready", function() {
3442 'use strict';
3443 CreatureGenPF.init();
3444 CreatureGenPF.registerAPI();
3445});