mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-09-01 12:42:32 +00:00
3710 lines
130 KiB
JavaScript
3710 lines
130 KiB
JavaScript
import _ from 'underscore';
|
|
import {PFLog, PFConsole} from './PFLog';
|
|
import TAS from './TheAaronSheet.js';
|
|
import * as SWUtils from './SWUtils';
|
|
import PFConst from './PFConst';
|
|
import * as PFSheet from './PFSheet';
|
|
import PFDB from './PFDB';
|
|
import * as PFMigrate from './PFMigrate';
|
|
import * as PFUtils from './PFUtils';
|
|
import * as PFSize from './PFSize';
|
|
import * as PFSkills from './PFSkills';
|
|
import * as PFAbilityScores from './PFAbilityScores';
|
|
import * as PFBuffs from './PFBuffs';
|
|
|
|
let npcCompendiumAttributesPlayer = [
|
|
'npc-spellike-ability-text',
|
|
'npc-spells-known-text',
|
|
'character_name',
|
|
'cr_compendium',
|
|
'xp_compendium',
|
|
'alignment',
|
|
'size_compendium',
|
|
'type_compendium',
|
|
'init_compendium',
|
|
'senses_compendium',
|
|
'npc-aura',
|
|
'ac_compendium',
|
|
'npc_hp_compendium',
|
|
'fort_compendium',
|
|
'ref_compendium',
|
|
'will_compendium',
|
|
'dr_compendium',
|
|
'sr_compendium',
|
|
'npc-defensive-abilities',
|
|
'immunities',
|
|
'resistances',
|
|
'weaknesses',
|
|
'speed_compendium',
|
|
'space_compendium',
|
|
'reach_compendium',
|
|
'npc-melee-attacks-text',
|
|
'npc-ranged-attacks-text',
|
|
'npc-special-attacks',
|
|
'str_compendium',
|
|
'dex_compendium',
|
|
'con_compendium',
|
|
'int_compendium',
|
|
'wis_compendium',
|
|
'cha_compendium',
|
|
'bab_compendium',
|
|
'cmb_compendium',
|
|
'cmd_compendium',
|
|
'class_compendium',
|
|
'npc-feats-text',
|
|
'skills_compendium',
|
|
'racial_mods_compendium',
|
|
'environment',
|
|
'organization',
|
|
'other_items_treasure',
|
|
'languages',
|
|
'SQ_compendium',
|
|
'content_compendium',
|
|
'npc-spells-prepared_compendium',
|
|
];
|
|
|
|
/* ******************************** PARSING ******************************** */
|
|
|
|
/** returns number from a string, first looks at end of string, then beginning, then anywhere in middle
|
|
* so it works with both compendium (number at end) and SRD ("init " number at beginning) or just a string number
|
|
*@param {string} initstring from the compendium entry
|
|
*@returns {int} the initiative modifier
|
|
*/
|
|
function getNPCInit(initstring) {
|
|
let numberInit, matches;
|
|
//Init +0;
|
|
initstring = PFUtils.convertDashToMinus(initstring);
|
|
initstring = SWUtils.trimBoth(initstring);
|
|
if (/^Init/i.test(initstring) || /^[\+\-]{0,1}\d+/.test(initstring)) {
|
|
//number at front
|
|
numberInit = PFUtils.getIntFromString(initstring, true);
|
|
} else if ((matches = initstring.match(/[\+\-]{0,1}\d+$/)) !== null) {
|
|
//number at end
|
|
initstring = initstring.slice(matches.index);
|
|
if (initstring[0] !== '+') {
|
|
initstring = '-' + initstring;
|
|
}
|
|
numberInit = parseInt(initstring.match(/[\-\+]\d+$/), 10) || 0;
|
|
} else {
|
|
numberInit = PFUtils.getIntFromString(initstring, true);
|
|
}
|
|
return numberInit;
|
|
}
|
|
/*buildImportantFeatObj - saves feats that require updates to the sheet in an object, no spaces and all lowercase.
|
|
* returns sub objects for feats that only apply to certain attacks, and a criticaldamage subobject.
|
|
* for instance::: obj.weaponfinesse=1 obj.criticaldamage.bleedingcritical:1 obj.longsword.weaponfocus:1
|
|
* @returns object of feats as {featname:1,feat2name:1, attacks:{attack1name:{featname:1}}, criticaldamage:{featname:1}}
|
|
*/
|
|
function buildImportantFeatObj(featlist) {
|
|
TAS.debug('buildImportantFeatObj is ', featlist);
|
|
let importantFeats = _.chain(featlist)
|
|
.filter(function (feat) {
|
|
if (!feat) {
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.filter(function (feat) {
|
|
return PFDB.importantFeatRegExp.test(feat);
|
|
})
|
|
.map(function (feat) {
|
|
//TAS.debug("checking <" + feat + "> for ending letter");
|
|
//if there is an "endnote" letter indicator at the end then remove it
|
|
//some end with B for instance
|
|
feat = SWUtils.trimBoth(feat);
|
|
if (/\b[A-Z]$/i.test(feat)) {
|
|
feat = feat.slice(0, -2);
|
|
feat = SWUtils.trimBoth(feat);
|
|
}
|
|
return feat;
|
|
})
|
|
.reduce(function (memo, feat) {
|
|
let origfeat = feat,
|
|
atktype = '',
|
|
matches,
|
|
attacks = {},
|
|
attack = {},
|
|
crits = {},
|
|
skills = [],
|
|
skill = '';
|
|
try {
|
|
if (feat.indexOf('(') >= 0) {
|
|
matches = /(.*?)\((.*)\)/.exec(feat);
|
|
TAS.debug('looking at ' + feat + ', split into ', matches);
|
|
feat = matches[1];
|
|
atktype = matches[2];
|
|
feat = SWUtils.trimBoth(feat);
|
|
atktype = SWUtils.trimBoth(atktype);
|
|
}
|
|
feat = feat.replace(/\s/g, '').toLowerCase();
|
|
if (feat === 'improvedcritical' || feat === 'criticalmastery') {
|
|
return memo;
|
|
}
|
|
memo[feat] = 1;
|
|
if (feat.indexOf('critical') > 0) {
|
|
atktype = feat;
|
|
feat = 'criticaldamage';
|
|
crits = memo.criticaldamage || {};
|
|
crits[atktype] = 1;
|
|
memo.criticaldamage = crits;
|
|
} else if (feat.indexOf('skillfocus') >= 0) {
|
|
skill = atktype.replace(' ', '-');
|
|
if (skill) {
|
|
skill = skill[0].toUpperCase() + skill.slice(1);
|
|
skills = memo.skillfocuses || [];
|
|
skills.push(skill);
|
|
memo.skillfocuses = skills;
|
|
}
|
|
} else if (feat === 'weaponfinesse' || feat === 'improvedcritical') {
|
|
attacks = memo.attacks || {};
|
|
attack = attacks[atktype] || {};
|
|
attack[feat] = 1;
|
|
attacks[atktype] = attack;
|
|
memo.attacks = attacks;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('buildImportantFeatObj error:', err);
|
|
memo[feat] = 1;
|
|
} finally {
|
|
return memo;
|
|
}
|
|
}, {})
|
|
.value();
|
|
TAS.debug('returning important feats:', importantFeats);
|
|
return importantFeats;
|
|
}
|
|
/** parseNPChp - parses statblock hp string such as 203 (14d10+126)
|
|
* @param {string} hpstring - string format: "15 (3d8 + 4) Fast Healing 5" can have multiple xdy, and any string left after ) is considered healing note.
|
|
* @param {int} abilityMod: number representing ability score mod (normally CON-mod)
|
|
* @returns {{hp:number,hdie1:number,hdice1:number,hdie2:number,hdice2:number,misc:number,heal:string}
|
|
*/
|
|
function parseNPChp(hpstring, abilityMod) {
|
|
let hparray = {
|
|
hp: 0,
|
|
hdie1: 0,
|
|
hdice1: 0,
|
|
basehp: 0,
|
|
misc: 0,
|
|
heal: '',
|
|
},
|
|
totalAbility = 0,
|
|
tempInt = 0,
|
|
dice,
|
|
calcHP = 0;
|
|
try {
|
|
abilityMod = abilityMod || 0;
|
|
if (/^hp/i.test(hpstring)) {
|
|
hpstring = hpstring.slice(2);
|
|
}
|
|
//this line should fix flying squirrel:
|
|
hpstring = PFUtils.replaceMissingNegatives_BadDice(hpstring);
|
|
hpstring = PFUtils.convertDashToMinus(hpstring);
|
|
hpstring = hpstring.replace('plus', '+');
|
|
//TAS.debug"parseNPChp", hpstring, abilityMod);
|
|
hparray.hp = parseInt(hpstring, 10) || 0;
|
|
hpstring = hpstring.slice(hpstring.indexOf('(') + 1);
|
|
dice = PFUtils.getDiceDieFromString(hpstring, true);
|
|
if (dice.dice !== 0) {
|
|
hparray.hdice1 = dice.dice;
|
|
hparray.hdie1 = dice.die;
|
|
totalAbility = abilityMod * hparray.hdice1;
|
|
hparray.misc = dice.plus - totalAbility;
|
|
//set the base hp (for class/race) to only the hd average, so will be less than what is in statblock
|
|
hparray.basehp = PFUtils.getAvgHP(hparray.hdice1, hparray.hdie1);
|
|
//check total, if does not match, change 'misc' attr
|
|
calcHP = hparray.basehp + dice.plus;
|
|
if (calcHP !== hparray.hp) {
|
|
TAS.warn('parseNPChp, hp not adding right, should be:' + hparray.hp + ' but getNPCHP returns ' + calcHP, hparray);
|
|
hparray.misc += hparray.hp - calcHP;
|
|
}
|
|
hpstring = hpstring.slice(dice.spaces + 1);
|
|
}
|
|
if (hpstring) {
|
|
hpstring = SWUtils.trimBoth(hpstring.replace(/[\);]/g, ''));
|
|
hparray.heal = hpstring || '';
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseNPChp', err);
|
|
} finally {
|
|
return hparray;
|
|
}
|
|
}
|
|
/** parseNPCAC - parses AC string from statblock
|
|
* @param {string} acstring - format: "24, Touch 24, Flat-footed 16 (+6 Deflection, +7 Dex, +1 Dodge, +1 Armor, +1 Shield, +1 Size, +6 Natural) some note can go here"
|
|
* can start with "AC " or not.
|
|
* if it doesn't add up then the bonus will be added to misc.
|
|
* (others include: Luck, Sacred/Profane, Circumstance, Enhancement, Insight, Morale) - these ALL go to CMD too (and dodge, deflection).
|
|
* @param {string} cmdStr string for cmd , just checks for a number in the string
|
|
* @param {int} abilityMod - to apply, usually dex.
|
|
* @param {int} sizeMod - ac mod due to size.
|
|
* @returns {ac:10,touch:10,ff:10,armor:0,shield:0,deflect:0,dex:0,dodge:0,natural:0,misc:0,note:,size:0,acbuff:0,altability:""}
|
|
*/
|
|
function parseNPCAC(acstring, cmdStr, abilityMod, sizeMod) {
|
|
let matches,
|
|
tempnum = 0,
|
|
tempstr = '',
|
|
acMap = {
|
|
ac: 10,
|
|
touch: 10,
|
|
ff: 10,
|
|
armor: 0,
|
|
shield: 0,
|
|
deflect: 0,
|
|
dex: 0,
|
|
dodge: 0,
|
|
natural: 0,
|
|
misc: 0,
|
|
note: '',
|
|
size: 0,
|
|
altability: '',
|
|
acbuff: 0,
|
|
uncanny: 0,
|
|
cmd: 10,
|
|
notes: '',
|
|
cmdnotes: '',
|
|
};
|
|
abilityMod = abilityMod || 0;
|
|
sizeMod = sizeMod || 0;
|
|
//TAS.debug"parseNPCAC: string:" + acstring + ", ability:" + abilityMod + ", size:" + sizeMod);
|
|
try {
|
|
if (/^ac\s/i.test(acstring)) {
|
|
acstring = acstring.slice(3);
|
|
}
|
|
acMap.ac = parseInt(acstring, 10) || 0;
|
|
|
|
matches = cmdStr.match(/(\d+)/); //get first match
|
|
if (matches && matches[1]) {
|
|
//TAS.debug("getting cmd matches is cmd is : "+matches[1],matches);
|
|
acMap.cmd = parseInt(matches[1], 10) || 0;
|
|
tempstr = cmdStr.slice(matches.index + matches[0].length);
|
|
if (tempstr) {
|
|
tempstr = SWUtils.trimBoth(tempstr);
|
|
acMap.cmdnotes = tempstr;
|
|
}
|
|
}
|
|
|
|
//get other AC totals
|
|
matches = acstring.match(/Touch\s*?(\d+)/i);
|
|
if (matches && matches[1]) {
|
|
acMap.touch = parseInt(matches[1], 10);
|
|
}
|
|
matches = acstring.match(/Flat\-footed\s*?(\d+)/i);
|
|
if (matches && matches[1]) {
|
|
acMap.ff = parseInt(matches[1], 10);
|
|
}
|
|
//get modifiers compendium has all negatives as "1" instead of "-1"
|
|
matches = acstring.match(/([+\-]??\d+)\s*?Deflect[,\i\s]/i);
|
|
if (matches && matches[1]) {
|
|
acMap.deflect = parseInt(matches[1], 10);
|
|
}
|
|
matches = acstring.match(/([+\-]??\d+)\s*?Nat[,u\s]/i);
|
|
if (matches && matches[1]) {
|
|
acMap.natural = parseInt(matches[1], 10);
|
|
}
|
|
matches = acstring.match(/([+\-]??\d+)\s*?Dodge/i);
|
|
if (matches && matches[1]) {
|
|
acMap.dodge = parseInt(matches[1], 10);
|
|
}
|
|
matches = acstring.match(/([+\-]??\d+)\s*?Size/i);
|
|
if (matches && matches[1]) {
|
|
acMap.size = parseInt(matches[1], 10);
|
|
}
|
|
//compendium size wrong: missing minus sign.
|
|
// see Marilith
|
|
if (acMap.size !== sizeMod) {
|
|
acMap.size = sizeMod;
|
|
}
|
|
matches = acstring.match(/([+\-]??\d+)\s*?armor/i);
|
|
if (matches && matches[1]) {
|
|
acMap.armor = parseInt(matches[1], 10);
|
|
}
|
|
matches = acstring.match(/([+\-]??\d+)\s*?shield/i);
|
|
if (matches && matches[1]) {
|
|
acMap.shield = parseInt(matches[1], 10);
|
|
}
|
|
matches = acstring.match(/\)\s*?(.*)/);
|
|
if (matches && matches[1]) {
|
|
acMap.note = matches[1];
|
|
}
|
|
//get ability modifier, should be Dex by default.
|
|
matches = acstring.match(/([+\-]??\d+)\s*?Dex/i);
|
|
if (matches && matches[1]) {
|
|
acMap.dex = parseInt(matches[1], 10) || 0;
|
|
//if different then set, compendium error no minus
|
|
// see Fire Giant.
|
|
if (abilityMod !== acMap.dex) {
|
|
acMap.dex = abilityMod;
|
|
}
|
|
} else {
|
|
matches = acstring.match(/([+\-]??\d+)\s*?(Wis|Int|Str|Con|Cha)/i);
|
|
if (matches && matches[1] && matches[2]) {
|
|
acMap.dex = parseInt(matches[1], 10) || 0;
|
|
//should not happen anymore since 6th printing of PRD they removed abilities that change ability to AC, now
|
|
// just add dodge instead.
|
|
acMap.altability = matches[2].toUppercase();
|
|
}
|
|
}
|
|
//check total for any other (untyped, Luck, Sacred/Profane, Circumstance, Enhancement, Insight, Morale)
|
|
//touch - if touch does not add up put difference in misc. (AC not match we'll put in a buff row)
|
|
// we need to track a separate ac misc buff/penalty. we can put it in buffs.
|
|
tempnum = acMap.dodge + acMap.dex + acMap.deflect + acMap.size + 10;
|
|
if (acMap.touch !== tempnum) {
|
|
acMap.misc = acMap.touch - tempnum;
|
|
}
|
|
//if AC does not add up, even including misc found above, then put it in ac buff row.
|
|
tempnum = acMap.armor + acMap.shield + acMap.dodge + acMap.dex + acMap.natural + acMap.deflect + acMap.size + acMap.misc + 10;
|
|
if (acMap.ac !== tempnum) {
|
|
acMap.acbuff = acMap.ac - tempnum;
|
|
}
|
|
//check for not caught flat footed
|
|
if (acMap.ac === acMap.ff && (acMap.dex > 0 || acMap.dodge > 0)) {
|
|
acMap.uncanny = 1;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseNPCAC', err);
|
|
} finally {
|
|
return acMap;
|
|
}
|
|
}
|
|
/* parseSpeed -returns object with speeds {land:base,fly:xx,swim:xx} etc*/
|
|
function parseSpeed(speedstr) {
|
|
let speeds = speedstr.split(/,\s*/),
|
|
retobj;
|
|
retobj = _.reduce(
|
|
speeds,
|
|
function (memo, speedComponent, idx) {
|
|
let matches,
|
|
speedNum = 0;
|
|
try {
|
|
if (idx === 0) {
|
|
speedNum = parseInt(speedComponent.match(/(\d+)/)[1], 10) || 0;
|
|
if (speedNum) {
|
|
memo['land'] = speedNum;
|
|
}
|
|
} else {
|
|
matches = speedComponent.match(/([\w]+)\s*(\d+)/);
|
|
if (matches) {
|
|
speedNum = parseInt(matches[2], 10) || 0;
|
|
if (speedNum) {
|
|
memo[matches[1].toLowerCase()] = speedNum;
|
|
if (/fly/i.test(matches[1])) {
|
|
matches = speedComponent.match(/\(([\w]+)\)/);
|
|
if (matches && matches[1].length > 0) {
|
|
memo['flyability'] = matches[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseSped', err);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
{},
|
|
);
|
|
return retobj;
|
|
}
|
|
/* getAtkNameFromStr get names of an attack or special attack
|
|
* { Name :(full str up to first parens) , abilityName (without pluses the base ability ), basename (ability name lower case no spaces)}
|
|
* for instance: Mwk Longsword +6/+1 would be : {name:Mwk longsword +6/+1, abilityName:Longsword, basename: longsword}
|
|
*/
|
|
function getAtkNameFromStr(abilitystr) {
|
|
let matches = abilitystr.match(/^\s*([^\(]+)/),
|
|
name = '',
|
|
abilityName = '',
|
|
basename = '';
|
|
if (matches && matches[1]) {
|
|
name = matches[1];
|
|
name = SWUtils.trimBoth(name);
|
|
abilityName = name.replace(/\d+d\d+|\-\d+|\+|\d+|\//g, '');
|
|
abilityName = SWUtils.trimBoth(abilityName);
|
|
abilityName = abilityName[0].toUpperCase() + abilityName.slice(1);
|
|
basename = abilityName.toLowerCase();
|
|
basename = basename.replace(/ray|cone|aura|mwk/gi, '');
|
|
basename = basename.replace(/\s+/g, '');
|
|
}
|
|
return {
|
|
name: name,
|
|
basename: basename,
|
|
abilityName: abilityName,
|
|
};
|
|
}
|
|
/*parseReach - parses reach string from compendium or statblock: giant frog: Reach 5 ft. (15 ft. with tongue)
|
|
* returns the default reach, rest of the string (if any), and an array of exceptions and reaches if any.
|
|
* @returns = {reach:number (5,10,15 etc), reachNotes:"rest of string", reachExceptions:[['Bite':10],['Claw':5]]}
|
|
*/
|
|
function parseReach(reachStr) {
|
|
let numerator = 0,
|
|
denominator = 1,
|
|
tempInt = 0,
|
|
tempFloat = 0.0,
|
|
tempstr,
|
|
restOf = '',
|
|
matches,
|
|
exceptionstr = '',
|
|
tempArray = [],
|
|
reachExceptions = [],
|
|
retobj = {
|
|
reach: 5,
|
|
reachNotes: '',
|
|
reachExceptions: [],
|
|
};
|
|
if (!reachStr) {
|
|
return retobj;
|
|
}
|
|
try {
|
|
//to handle PRD, SRD:
|
|
matches = reachStr.match(/reach/i);
|
|
if (matches) {
|
|
reachStr = reachStr.slice(matches.index + matches[0].length);
|
|
}
|
|
reachStr = SWUtils.trimBoth(reachStr);
|
|
reachStr = PFUtils.convertDashToMinus(reachStr);
|
|
reachStr = reachStr.replace('21/2', '2-1/2');
|
|
reachStr = reachStr.replace('2.5', '2-1/2');
|
|
if (reachStr.slice(0, 5) === '2-1/2') {
|
|
retobj.reach = 2.5;
|
|
exceptionstr = reachStr.slice(5);
|
|
} else {
|
|
retobj.reach = parseInt(reachStr, 10) || 0;
|
|
exceptionstr = PFUtils.getNoteAfterNumber(reachStr);
|
|
}
|
|
if (exceptionstr && exceptionstr.indexOf('(') >= 0) {
|
|
retobj.reachNotes = exceptionstr;
|
|
exceptionstr = SWUtils.trimBoth(
|
|
exceptionstr
|
|
.replace('(', '')
|
|
.replace(')', '')
|
|
.replace(/with\s/gi, '')
|
|
.replace(';', '')
|
|
.replace(/ft[\.\s]*/gi, '')
|
|
.replace(/,\s*/g, ','),
|
|
);
|
|
if (exceptionstr) {
|
|
tempArray = exceptionstr.split(',');
|
|
reachExceptions = _.reduce(
|
|
tempArray,
|
|
function (memo, exceptioninstance) {
|
|
let reachExceptions = [],
|
|
matches;
|
|
try {
|
|
if (exceptioninstance.slice(0, 5) === '2-1/2') {
|
|
reachExceptions.push(SWUtils.trimBoth(exceptioninstance.slice(5)));
|
|
if (reachExceptions[0]) {
|
|
reachExceptions.push(2.5);
|
|
memo.push(reachExceptions);
|
|
}
|
|
} else {
|
|
reachExceptions.push(PFUtils.getNoteAfterNumber(reachStr) || '');
|
|
if (reachExceptions[0]) {
|
|
reachExceptions.push(parseInt(reachStr, 10) || 0);
|
|
memo.push(reachExceptions);
|
|
}
|
|
}
|
|
} catch (erri) {
|
|
TAS.error('parseReach inner error erri:', erri);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
if (reachExceptions && reachExceptions.length > 0) {
|
|
retobj.reachExceptions = reachExceptions;
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseReach error:', err);
|
|
} finally {
|
|
return retobj;
|
|
}
|
|
}
|
|
function getCreatureClassSkills(creatureType) {
|
|
let typeToCheck = creatureType.toLowerCase().replace(/\s/g, ''),
|
|
classSkills,
|
|
subSkills;
|
|
try {
|
|
subSkills = _.find(PFDB.creatureTypeClassSkills, function (skills, mainType) {
|
|
let reg = new RegExp(mainType);
|
|
return reg.test(typeToCheck);
|
|
});
|
|
if (subSkills && subSkills.length > 0) {
|
|
classSkills = subSkills;
|
|
}
|
|
subSkills = _.find(PFDB.creatureSubtypeClassSkills, function (skills, mainType) {
|
|
let reg = new RegExp(mainType);
|
|
return reg.test(typeToCheck);
|
|
});
|
|
if (subSkills) {
|
|
if (classSkills) {
|
|
classSkills = classSkills.concat(subSkills);
|
|
} else {
|
|
classSkills = subSkills;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseCreatureClassSkills', err);
|
|
} finally {
|
|
if (classSkills) {
|
|
return classSkills;
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
/* assignPrimarySecondary
|
|
* to each attack in array, assigns attack.naturaltype='primary|secondary' and sometimes attack.dmgMult=1.5
|
|
* returns attacks for chaining.
|
|
*/
|
|
function assignPrimarySecondary(attacks) {
|
|
let attackGroups, attacksToCheck;
|
|
try {
|
|
attacksToCheck = _.filter(attacks, function (attack) {
|
|
return attack.type === 'natural';
|
|
});
|
|
if (_.size(attacksToCheck) <= 0) {
|
|
return attacks;
|
|
}
|
|
if (_.size(attacksToCheck) === 1) {
|
|
attacksToCheck[0].naturaltype = 'primary';
|
|
if ((attacksToCheck[0].iter && attacksToCheck[0].iter.length === 1) || isNaN(parseInt(attacksToCheck[0].iter, 10))) {
|
|
attacksToCheck[0].dmgMult = 1.5;
|
|
}
|
|
} else {
|
|
attackGroups = _.groupBy(attacksToCheck, function (attack) {
|
|
return PFDB.primaryNaturalAttacksRegExp.exec(attack.name);
|
|
});
|
|
if (_.size(attackGroups) === 1) {
|
|
_.each(attacksToCheck, function (attack) {
|
|
attack.naturaltype = 'primary';
|
|
});
|
|
} else {
|
|
_.each(attacksToCheck, function (attack) {
|
|
if (PFDB.primaryNaturalAttacksRegExp.test(attack.name)) {
|
|
attack.naturaltype = 'primary';
|
|
} else {
|
|
attack.naturaltype = 'secondary';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('assignPrimarySecondary', err);
|
|
} finally {
|
|
return attacks;
|
|
}
|
|
}
|
|
function parseAttack(atkstr, atktypestr, addgroups, groupidx, isUndead) {
|
|
let matches,
|
|
currpos = 0,
|
|
name = '',
|
|
iteratives,
|
|
i = 0,
|
|
tempInt = 0,
|
|
dice,
|
|
beforeBetweenAfterParens,
|
|
bonus = '',
|
|
origStr = atkstr,
|
|
countspaces = 0,
|
|
specCMB = 0,
|
|
abilityBaseName = '',
|
|
tempstr = '',
|
|
tempidx = 0,
|
|
names,
|
|
attackdescs,
|
|
retobj = {
|
|
enh: 0,
|
|
mwk: 0,
|
|
name: '',
|
|
basename: '',
|
|
atktype: 'melee',
|
|
type: '',
|
|
range: '',
|
|
countFullBAB: 1,
|
|
iter: [],
|
|
dmgdice: 0,
|
|
dmgdie: 0,
|
|
dmgtype: '',
|
|
crit: 20,
|
|
critmult: 2,
|
|
dmgbonus: 0,
|
|
plus: '',
|
|
plusamount: '',
|
|
plustype: '',
|
|
note: '',
|
|
};
|
|
try {
|
|
TAS.debug('parseAttack: ' + atkstr);
|
|
if (addgroups) {
|
|
//retobj.name += "Group " + groupidx + ": ";
|
|
retobj.group = 'Full attack ' + groupidx;
|
|
}
|
|
names = getAtkNameFromStr(atkstr);
|
|
retobj.name += names.name;
|
|
retobj.basename = names.basename;
|
|
atkstr = SWUtils.trimBoth(atkstr);
|
|
//if starts with number, it means number of attacks
|
|
matches = atkstr.match(/^(\d+)\s*/);
|
|
if (matches && matches[1]) {
|
|
retobj.countFullBAB = parseInt(matches[1], 10) || 1;
|
|
//move up
|
|
atkstr = atkstr.slice(matches[0].length);
|
|
}
|
|
//starts with '+/-' number or 'mwk': enh or mwk
|
|
matches = atkstr.match(/^([+\-]\d+|mwk)\s*/i);
|
|
if (matches) {
|
|
//starts with +n, is weapon
|
|
//retobj.name += matches[0];
|
|
if (matches[1].toLowerCase() === 'mwk') {
|
|
retobj.mwk = 1;
|
|
} else {
|
|
retobj.enh = parseInt(matches[1], 10) || 0;
|
|
}
|
|
retobj.type = 'weapon'; //for sure is a weapon
|
|
//move up
|
|
atkstr = atkstr.slice(matches[0].length);
|
|
}
|
|
if (PFDB.cmbPlusStrsrch.test(retobj.basename)) {
|
|
retobj.atktype = 'cmb';
|
|
retobj.vs = 'cmd';
|
|
retobj.type = 'natural';
|
|
specCMB = 1;
|
|
} else if (atktypestr === 'melee' && PFDB.combatManeuversRegExp.test(retobj.basename)) {
|
|
retobj.atktype = 'cmb';
|
|
retobj.vs = 'cmd';
|
|
} else if (PFDB.cmbMonsterSrch.test(retobj.basename)) {
|
|
retobj.atktype = 'cmb';
|
|
retobj.type = 'natural';
|
|
retobj.vs = 'cmd';
|
|
} else if (/web/i.test(retobj.basename)) {
|
|
retobj.atktype = 'ranged';
|
|
retobj.type = 'special';
|
|
retobj.vs = 'touch';
|
|
retobj.range = 10;
|
|
} else if (/touch/i.test(retobj.basename)) {
|
|
if (/range/i.test(retobj.basename)) {
|
|
retobj.atktype = 'ranged';
|
|
} else {
|
|
retobj.atktype = 'melee';
|
|
}
|
|
retobj.vs = 'touch';
|
|
} else if (/special/i.test(atktypestr)) {
|
|
retobj.atktype = 'special';
|
|
retobj.type = 'special';
|
|
} else {
|
|
retobj.atktype = atktypestr;
|
|
}
|
|
if (!retobj.type) {
|
|
if (PFDB.naturalAttackRegExp.test(retobj.basename)) {
|
|
retobj.type = 'natural';
|
|
} else if (PFDB.unarmedAttacksRegExp.test(name)) {
|
|
retobj.type = 'unarmed';
|
|
} else {
|
|
retobj.type = 'weapon';
|
|
}
|
|
}
|
|
if (!retobj.vs) {
|
|
if (/touch|web/i.test(retobj.name)) {
|
|
retobj.vs = 'touch';
|
|
if (/ranged|web/i.test(retobj.name)) {
|
|
retobj.atktype = 'ranged';
|
|
if (/web/i.test(retobj.basename)) {
|
|
retobj.range = 10;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//skip past name
|
|
atkstr = SWUtils.trimBoth(atkstr);
|
|
matches = atkstr.match(/^[a-z\s]+/i);
|
|
//matches = atkstr.match(/\s*([^0-9\/\+\-\(]+)/);
|
|
if (matches && matches[0]) {
|
|
if (matches.index) {
|
|
tempidx = matches.index;
|
|
}
|
|
atkstr = atkstr.slice(tempidx + matches[0].length);
|
|
}
|
|
TAS.debug('PFNPCParse atkstr is now ' + atkstr);
|
|
if (atkstr) {
|
|
//after name split rest by parenthesis
|
|
// format: name attack bonus ( damage ) plus additional
|
|
beforeBetweenAfterParens = atkstr.split(/\(|\)/);
|
|
//attack amounts before paren
|
|
iteratives = beforeBetweenAfterParens[0].split(/\//);
|
|
if (/\d/.test(iteratives[0])) {
|
|
retobj.iter = _.map(iteratives, function (iter, index) {
|
|
if (/^[+\-]/.test(iter)) {
|
|
return parseInt(iter, 10) || 0;
|
|
}
|
|
//minus missing assume minus
|
|
return -1 * (parseInt(iter, 10) || 0);
|
|
});
|
|
} else if (retobj.atktype === 'cmb') {
|
|
retobj.iter[0] = 0;
|
|
}
|
|
//damage between parens
|
|
if (beforeBetweenAfterParens[1]) {
|
|
attackdescs = beforeBetweenAfterParens[1].split(/,\s*/);
|
|
//TAS.debug("pfattackparse split on commas: ",attackdescs);
|
|
//split on commas and strip out non damage, put damage in tempstr
|
|
tempstr = _.reduce(
|
|
attackdescs,
|
|
function (memo, subattack) {
|
|
//TAS.debug("current comma is "+subattack);
|
|
if (/ft\./i.test(subattack)) {
|
|
retobj.range = subattack;
|
|
} else if (/D[Cc]\s\d+/.test(subattack)) {
|
|
matches = subattack.match(/(D[Cc]\s\d+)/);
|
|
retobj.DC = matches[1].toUpperCase();
|
|
retobj.DCability = PFDB.specialAttackDCAbilityBase[retobj.basename] || 'CON';
|
|
if (isUndead && retobj.DCability === 'CON') {
|
|
retobj.DCability = 'CHA';
|
|
}
|
|
retobj.DCEquation = PFUtils.getDCString(retobj.DCability, 'npc-hd-num', isUndead);
|
|
//TAS.debug("PFNPCParser.parseAttack looking for dc save:"+subattack);
|
|
matches = subattack.match(/(Will|Fort|Ref|Fortitude|Reflex)\s*D[Cc]\s*\d+([^),.])/i);
|
|
if (matches) {
|
|
if (matches[1]) {
|
|
tempstr = matches[1][0].toUpperCase() + matches[1].slice(1).toLowerCase();
|
|
retobj.save = tempstr;
|
|
}
|
|
if (matches[2]) {
|
|
retobj.save += ' ' + matches[2];
|
|
}
|
|
} else {
|
|
matches = subattack.match(/(Will|Fort|Ref|Fortitude|Reflex)/i);
|
|
if (matches) {
|
|
if (matches[1]) {
|
|
tempstr = matches[1][0].toUpperCase() + matches[1].slice(1).toLowerCase();
|
|
retobj.save = tempstr;
|
|
}
|
|
}
|
|
}
|
|
} else if (/freq|day|constant|at.will/i.test(subattack)) {
|
|
retobj.frequency = subattack;
|
|
} else if (/AC|hp/.test(subattack) || !/\d|plus/.test(subattack)) {
|
|
//if no number or 'plus' don't know what to do so stick it in note.
|
|
retobj.note += subattack + ', ';
|
|
} else {
|
|
memo += subattack + ' ';
|
|
}
|
|
return memo;
|
|
},
|
|
'',
|
|
);
|
|
// find damage
|
|
//damage dice and die
|
|
tempstr = SWUtils.trimBoth(tempstr);
|
|
dice = PFUtils.getDiceDieFromString(tempstr, true, true);
|
|
TAS.debug('##### Attack dice parse of ' + tempstr + ' is ', dice);
|
|
if (dice && dice.dice) {
|
|
retobj.dmgdice = dice.dice;
|
|
retobj.dmgdie = dice.die;
|
|
retobj.dmgbonus = dice.plus;
|
|
tempstr = tempstr.slice(dice.spaces); //no plus 1 why?
|
|
}
|
|
|
|
//look for crit
|
|
TAS.debug('PFParseAttack e3:' + tempstr);
|
|
if (tempstr) {
|
|
//--does not find dash in crit check for different types of minus
|
|
let critter = PFUtils.getCritFromString(tempstr, true);
|
|
TAS.debug('PFParseAttack e4:' + tempstr + ', crits:', critter);
|
|
retobj.crit = critter.crit;
|
|
retobj.critmult = critter.critmult;
|
|
tempstr = tempstr.slice(critter.spaces);
|
|
}
|
|
bonus = SWUtils.trimBoth(tempstr);
|
|
|
|
//any text after damage is 'plus' or damage type
|
|
if (bonus) {
|
|
//if engulf or swallowwhole, there will be inner AC and hp to put in notes
|
|
if (specCMB) {
|
|
matches = bonus.match(/\sac\s\d+/i);
|
|
if (matches) {
|
|
retobj.note += bonus.slice(matches.index);
|
|
bonus = SWUtils.trimBoth(bonus.slice(0, matches.index));
|
|
}
|
|
}
|
|
//look for plus
|
|
matches = bonus.match(/plus(.*)/i);
|
|
if (matches) {
|
|
tempstr = SWUtils.trimBoth(matches[1]);
|
|
bonus = SWUtils.trimBoth(bonus.slice(0, matches.index));
|
|
if (/\d+d\d+/i.test(tempstr)) {
|
|
matches = tempstr.match(/(\d+d\d+)\s*([\w\s]*)/);
|
|
retobj.plusamount = matches[1];
|
|
if (matches[2]) {
|
|
retobj.plustype = matches[2].replace(/^\s+|\s+$/g, '');
|
|
}
|
|
} else {
|
|
retobj.plus = tempstr;
|
|
}
|
|
}
|
|
if (bonus && bonus.length > 0) {
|
|
retobj.dmgtype += bonus;
|
|
}
|
|
}
|
|
//TAS.debug("Parse e5 retobj:",retobj);
|
|
if (
|
|
retobj.atktype !== 'cmb' &&
|
|
!retobj.iter[0] &&
|
|
retobj.dmgtype &&
|
|
retobj.dmgdice &&
|
|
retobj.dmgdie &&
|
|
!retobj.plusamount &&
|
|
!retobj.plustype &&
|
|
!/bludg|slash|pierc/i.test(retobj.dmgtype)
|
|
) {
|
|
retobj.plustype = retobj.dmgtype;
|
|
tempstr = String(retobj.dmgdice) + 'd' + String(retobj.dmgdie);
|
|
if (retobj.dmgbonus) {
|
|
if (retobj.dmgbonus > 0) {
|
|
tempstr += '+' + retobj.dmgbonus;
|
|
} else {
|
|
tempstr += '-' + Math.abs(retobj.dmgbonus);
|
|
}
|
|
}
|
|
retobj.plusamount = tempstr;
|
|
retobj.dmgtype = '';
|
|
retobj.dmgdice = 0;
|
|
retobj.dmgdie = 0;
|
|
}
|
|
}
|
|
//any notes at end
|
|
i = 2;
|
|
while (i < beforeBetweenAfterParens.length) {
|
|
//can use filter then reduce, or use each, or use easy for loop.
|
|
retobj.note += SWUtils.trimBoth(beforeBetweenAfterParens[i]);
|
|
i++;
|
|
}
|
|
}
|
|
if (retobj.note) {
|
|
retobj.note = SWUtils.trimBoth(retobj.note);
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseAttack: error parsing:' + atkstr, err);
|
|
if (retobj.name) {
|
|
retobj.name += ' ';
|
|
}
|
|
retobj.name += 'Could not parse attack!';
|
|
retobj.note = origStr + ' , error: ';
|
|
retobj.note += err;
|
|
} finally {
|
|
TAS.debug('parse attack returning parse of ' + atkstr, retobj);
|
|
return retobj;
|
|
}
|
|
}
|
|
/** parseAttacks parse atttack string one at a time, returns arrays grouped by full attacks
|
|
* attacks split by commas, full attack groups split by 'or'
|
|
* the name of the attack starts with Group 0, Group 1, etc.
|
|
* @param {string} atkstr
|
|
* @param {string} atktypestr "melee" or "ranged"
|
|
* @returns {[{enh:0,mwk:0,name:"",atktype:"melee",type:"",countFullBAB:1,plus:"",plusamount:"",plustype:"",note:"",iter:[],dmgdice:0,dmgdie:0,crit:20,critmult:2,dmgbonus:0}]}
|
|
*/
|
|
function parseAttacks(atkstr, atktypestr, cmbval) {
|
|
let atkarrayout,
|
|
atkarraysub,
|
|
attacksouter = [],
|
|
matches,
|
|
addgroups = false;
|
|
try {
|
|
if (!atkstr) {
|
|
return null;
|
|
}
|
|
if (!atktypestr) {
|
|
atktypestr = 'melee';
|
|
}
|
|
if (atkstr.slice(0, atktypestr.length).toLowerCase() === atktypestr) {
|
|
atkstr = atkstr.slice(atktypestr.length);
|
|
}
|
|
if (atkstr[0] === '*' || atkstr[0] === ':') {
|
|
atkstr = atkstr.slice(1);
|
|
}
|
|
atkstr = SWUtils.trimBoth(atkstr);
|
|
atkstr = PFUtils.convertDashToMinus(atkstr);
|
|
atkstr = PFUtils.replaceMissingNegatives_BadDice(atkstr);
|
|
atkstr = PFUtils.replaceMissingNegatives_CritRange(atkstr);
|
|
//TAS.debug("########","Now the attack is ",atkstr);
|
|
atkarrayout = atkstr.split(/\bor\b/i);
|
|
if (atkarrayout.length > 1) {
|
|
addgroups = true;
|
|
}
|
|
attacksouter = _.reduce(
|
|
atkarrayout,
|
|
function (memoout, atkstrout, groupidx) {
|
|
let atkarray, attacks;
|
|
try {
|
|
atkarray = SWUtils.splitByCommaIgnoreParens(atkstrout); //.split(/,\s*(?![^\(\)]*\))/),
|
|
if (atkarray.length > 1) {
|
|
addgroups = true;
|
|
}
|
|
//TAS.debug('parseattacks outer group: ' + groupidx);
|
|
attacks = _.reduce(
|
|
atkarray,
|
|
function (memo, atkstr) {
|
|
let retobj;
|
|
try {
|
|
//TAS.debug('parseattacks: ' + atkstr);
|
|
retobj = parseAttack(atkstr, atktypestr, addgroups, groupidx, cmbval);
|
|
//TAS.debug("parseAttacks return for this attack: ",retobj);
|
|
if (retobj) {
|
|
memo.push(retobj);
|
|
}
|
|
} catch (erri) {
|
|
TAS.error('parseAttacks', erri);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
} catch (erro) {
|
|
TAS.error('parseAttacks', erro);
|
|
} finally {
|
|
return memoout.concat(attacks);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
} catch (err3) {
|
|
TAS.error('parseAttacks', err3);
|
|
}
|
|
return attacksouter;
|
|
}
|
|
function parseSkillRacialBonuses(racialstr) {
|
|
//abilitymods = modify default ability score for a skill
|
|
let abilitieslower = _.map(PFAbilityScores.abilities, function (ab) {
|
|
return ab.toLowerCase();
|
|
}),
|
|
allCoreSkillsLower = _.map(PFSkills.allCoreSkills, function (skill) {
|
|
return skill.toLowerCase();
|
|
}),
|
|
skillsWithSubSkillsLower = _.map(PFSkills.skillsWithSubSkills, function (skill) {
|
|
return skill.toLowerCase();
|
|
}),
|
|
skillsWithSpaces = PFSkills.skillsWithSpaces,
|
|
temparray,
|
|
modifiers = [],
|
|
abilitymodstr = '',
|
|
abilitymodlower = '',
|
|
ability = '',
|
|
setability = false,
|
|
tempskill = '',
|
|
matches,
|
|
skillmods = {},
|
|
skillnotes = [],
|
|
abilitymods = {},
|
|
retobj = {
|
|
skillmods: skillmods,
|
|
skillnotes: skillnotes,
|
|
abilitymods: abilitymods,
|
|
};
|
|
if (!racialstr) {
|
|
return retobj;
|
|
}
|
|
temparray = racialstr.split(';');
|
|
if (temparray.length > 1) {
|
|
racialstr = temparray[0];
|
|
abilitymodstr = temparray[1];
|
|
}
|
|
if (abilitymodstr) {
|
|
try {
|
|
abilitymodlower = abilitymodstr.toLowerCase();
|
|
ability = _.find(abilitieslower, function (ab) {
|
|
return abilitymodlower.indexOf(ab) >= 0;
|
|
});
|
|
if (ability) {
|
|
tempskill = _.find(allCoreSkillsLower, function (skill) {
|
|
return abilitymodlower.indexOf(skill) >= 0;
|
|
});
|
|
if (tempskill) {
|
|
abilitymods[tempskill[0].toUpperCase() + tempskill.slice(1)] = ability.toLowerCase();
|
|
setability = true;
|
|
}
|
|
}
|
|
} catch (err1) {
|
|
TAS.error('parseSkillRacialBonuses inner', err1);
|
|
}
|
|
if (!setability) {
|
|
skillnotes.push(abilitymodstr);
|
|
}
|
|
}
|
|
modifiers = racialstr.split(/,\s*/);
|
|
_.each(modifiers, function (modstr) {
|
|
let modstrlower = modstr.toLowerCase(),
|
|
mod = 0,
|
|
moddedTitle,
|
|
modded = '',
|
|
tempstr = '',
|
|
exceptionstr = '',
|
|
conditionmod = 0,
|
|
conditionstr = '',
|
|
hasSubSkill = false,
|
|
matches;
|
|
try {
|
|
matches = modstr.match(/\s*([+\-]\d+)\s*(?:on|to)?\s*([\w]+)\s*([\w\s]+)?\s*(\([^)]*\))?/);
|
|
if (!matches) {
|
|
//is an exception or note
|
|
tempskill = _.find(allCoreSkillsLower, function (skill) {
|
|
return modstrlower.indexOf(skill) >= 0;
|
|
});
|
|
if (tempskill) {
|
|
ability = _.find(abilitieslower, function (ab) {
|
|
return modstrlower.indexOf(ab) >= 0;
|
|
});
|
|
if (ability) {
|
|
abilitymods[tempskill.toLowerCase()] = ability;
|
|
} else {
|
|
skillnotes.push(modstr);
|
|
}
|
|
} else {
|
|
skillnotes.push(modstr);
|
|
}
|
|
return;
|
|
}
|
|
exceptionstr = matches[3];
|
|
mod = parseInt(matches[1], 10) || 0;
|
|
modded = matches[2];
|
|
if (!_.contains(allCoreSkillsLower, modded.toLowerCase())) {
|
|
//TAS.warn("does not match " + modded);
|
|
// +8 Sleight of Hand
|
|
tempskill = _.find(skillsWithSpaces, function (skill) {
|
|
return modstrlower.indexOf(skill) >= 0;
|
|
});
|
|
if (!tempskill || tempskill.length < 1) {
|
|
//not sure what this is
|
|
skillnotes.push(modstr);
|
|
return;
|
|
}
|
|
temparray = tempskill.split(/\s/);
|
|
temparray = _.map(temparray, function (part) {
|
|
if (part === 'of') {
|
|
return 'of';
|
|
}
|
|
return part[0].toUpperCase() + part.slice(1);
|
|
});
|
|
modded = temparray.join('-');
|
|
exceptionstr = exceptionstr.slice(tempskill.length - tempskill.indexOf(' ') + 1);
|
|
//TAS.debug("found skill with space converted to modded:"+modded+", exceptionstr:"+exceptionstr);
|
|
}
|
|
if (exceptionstr) {
|
|
//entire thing is a "when" exception
|
|
skillnotes.push(modstr);
|
|
return;
|
|
}
|
|
moddedTitle = modded[0].toUpperCase() + modded.slice(1);
|
|
if (!matches[4]) {
|
|
skillmods[moddedTitle] = mod;
|
|
return;
|
|
}
|
|
//if craft, knowledge, etc
|
|
exceptionstr = matches[4].replace(/^\s+|\(|\)|\s+$/g, '');
|
|
if (_.contains(skillsWithSubSkillsLower, modded.toLowerCase())) {
|
|
exceptionstr = exceptionstr[0].toUpperCase() + exceptionstr.slice(1);
|
|
if (modded.toLowerCase() === 'knowledge') {
|
|
moddedTitle += '-' + exceptionstr;
|
|
} else {
|
|
moddedTitle += '[' + exceptionstr + ']';
|
|
}
|
|
skillmods[moddedTitle] = mod;
|
|
} else {
|
|
//has bonus
|
|
matches = exceptionstr.match(/([+\-]\d+)\s(.*)$/);
|
|
if (matches && matches[1]) {
|
|
conditionmod = parseInt(matches[1], 10) || 0;
|
|
if (matches[2]) {
|
|
conditionstr = matches[2];
|
|
}
|
|
conditionmod = conditionmod - mod;
|
|
skillmods[moddedTitle] = mod;
|
|
tempstr = (conditionmod > 0 ? '+' : '') + conditionmod + ' ' + moddedTitle + ' ' + conditionstr;
|
|
skillnotes.push(tempstr);
|
|
} else {
|
|
skillnotes.push(modstr);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseSkillRacialBonuses outer error', err);
|
|
skillnotes.push(modstr);
|
|
}
|
|
});
|
|
return retobj;
|
|
}
|
|
function parseSkills(skillstr) {
|
|
let rawSkills = skillstr.match(/[\w][\w\s]+\s*(?:\([\w\s,]+\))?\s*[+\-]\d+[,]??/g),
|
|
skills = _.reduce(
|
|
rawSkills,
|
|
function (memo, skill) {
|
|
let matches = skill.match(/^([\w][\w\s]+[\w])\s*(\([\w\s,]+\))??([+\s]+\d+)$/),
|
|
tempskill = '',
|
|
tempval = 0,
|
|
tempskill2 = '',
|
|
subskills;
|
|
if (matches) {
|
|
tempval = parseInt(matches[3], 10) || 0;
|
|
tempskill = matches[1].replace(/^\s+|\s+$/g, '');
|
|
tempskill = tempskill[0].toUpperCase() + tempskill.slice(1);
|
|
tempskill = tempskill.replace(/\s/g, '-');
|
|
if (matches[2]) {
|
|
subskills = matches[2].split(/,\s*/);
|
|
_.each(subskills, function (subskill) {
|
|
subskill = subskill.replace(/^\s+|,|\(|\)|\s+$/g, '');
|
|
subskill = subskill[0].toUpperCase() + subskill.slice(1);
|
|
if (tempskill === 'Knowledge') {
|
|
subskill = '-' + subskill;
|
|
} else {
|
|
subskill = '[' + subskill + ']';
|
|
}
|
|
memo[tempskill + subskill] = tempval;
|
|
});
|
|
} else {
|
|
memo[tempskill] = tempval;
|
|
}
|
|
}
|
|
return memo;
|
|
},
|
|
{},
|
|
);
|
|
return skills || {};
|
|
}
|
|
function parseAbilityScores(v) {
|
|
let aS = {};
|
|
aS.str = PFAbilityScores.getAbilityAndMod(v.str_compendium);
|
|
aS.dex = PFAbilityScores.getAbilityAndMod(v.dex_compendium);
|
|
aS.con = PFAbilityScores.getAbilityAndMod(v.con_compendium);
|
|
aS.wis = PFAbilityScores.getAbilityAndMod(v.wis_compendium);
|
|
aS['int'] = PFAbilityScores.getAbilityAndMod(v.int_compendium);
|
|
aS.cha = PFAbilityScores.getAbilityAndMod(v.cha_compendium);
|
|
return aS;
|
|
}
|
|
function parseSpecialAttack(setter, sastr) {
|
|
let origsastr,
|
|
names,
|
|
tempstr,
|
|
tempstr2,
|
|
match,
|
|
matches,
|
|
parensplit,
|
|
atktyp = 'special',
|
|
baseability = '',
|
|
abilitytype = '',
|
|
isAttack = false,
|
|
retobj = {};
|
|
try {
|
|
origsastr = sastr;
|
|
names = getAtkNameFromStr(sastr);
|
|
if (sastr.indexOf('(') >= 0) {
|
|
if (PFDB.spAttackAttacksPreProcess.test(names.basename)) {
|
|
//preprocess
|
|
if (/rake/i.test(names.basename)) {
|
|
sastr = PFUtils.removeUptoFirstComma(sastr, true);
|
|
} else if (/rend/i.test(names.basename)) {
|
|
sastr = PFUtils.removeUptoFirstComma(sastr);
|
|
} else if (/web/i.test(names.basename)) {
|
|
sastr = PFUtils.removeUptoFirstComma(sastr, true);
|
|
sastr = 'web ' + sastr;
|
|
atktyp = 'ranged';
|
|
}
|
|
isAttack = true;
|
|
} else if (PFDB.spAttackAttacks.test(names.basename)) {
|
|
isAttack = true;
|
|
}
|
|
} else if (/damage|drain|dmg/i.test(names.basename) && !/blood|energy/i.test(names.basename) && PFDB.abilitySrch.test(names.basename)) {
|
|
match = names.basename.match(/damage|drain/i);
|
|
names.AbilityName = 'Ability ' + match[0];
|
|
sastr = names.AbilityName + ' (' + sastr + ')';
|
|
isAttack = true;
|
|
}
|
|
|
|
if (isAttack) {
|
|
retobj = parseAttack(sastr, atktyp, false, 0);
|
|
retobj.specialtype = 'attack';
|
|
retobj.group = 'Special';
|
|
retobj.name = names.AbilityName && names.AbilityName.slice(0, 7) === 'Ability' ? names.AbilityName : names.name;
|
|
retobj.basename = names.basename;
|
|
}
|
|
if (!isAttack) {
|
|
retobj.name = names.abilityName || names.name;
|
|
retobj.basename = names.basename;
|
|
retobj.specialtype = 'ability';
|
|
retobj.rule_category = 'special-attacks';
|
|
matches = /usable\severy/i.exec(origsastr);
|
|
if (matches) {
|
|
retobj.frequency = 'everyrounds';
|
|
tempstr = origsastr.slice(matches.index + matches[0].length);
|
|
tempstr2 = PFUtils.getDiceDieString(tempstr);
|
|
if (tempstr2) {
|
|
retobj.used = tempstr2;
|
|
matches = tempstr.match(/rounds|days|minutes/i);
|
|
if (matches) {
|
|
retobj.used += ' ' + matches[0];
|
|
}
|
|
}
|
|
}
|
|
if (PFDB.specialAttackDCAbilityBase[retobj.basename]) {
|
|
retobj.DCability = PFDB.specialAttackDCAbilityBase[retobj.basename];
|
|
if (parseInt(setter['is_undead'], 10) === 1 && retobj.DCability === 'CON') {
|
|
retobj.DCability = 'CHA';
|
|
}
|
|
}
|
|
retobj.shortdesc = PFUtils.replaceDCString(PFUtils.replaceDiceDieString(origsastr), retobj.DCability, 'npc-hd-num', setter.is_undead);
|
|
}
|
|
abilitytype = PFUtils.getSpecialAbilityTypeFromString(sastr);
|
|
if (abilitytype) {
|
|
retobj.ability_type = abilitytype;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseSpecialAttack', err);
|
|
} finally {
|
|
return retobj;
|
|
}
|
|
}
|
|
function parseSpecialAttacks(setter, saString, cmb) {
|
|
let retarray;
|
|
if (!saString) {
|
|
return {};
|
|
}
|
|
retarray = SWUtils.splitByCommaIgnoreParens(saString); //.split(/,\s*(?![^\(\)]*\))/);
|
|
return _.reduce(
|
|
retarray,
|
|
function (memo, sa) {
|
|
let retobj, tempstr, names;
|
|
try {
|
|
retobj = parseSpecialAttack(setter, sa);
|
|
} catch (err) {
|
|
TAS.error('parseSpecialAttacks', err);
|
|
retobj = {};
|
|
retobj.name = sa;
|
|
retobj.specialtype = 'ability';
|
|
retobj.rule_category = 'special-attacks';
|
|
} finally {
|
|
memo.push(retobj);
|
|
return memo;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
}
|
|
function parseSpecialAbilities(str) {
|
|
let saObj = {},
|
|
initiallines,
|
|
lines,
|
|
extralines,
|
|
contentstr,
|
|
tempstr,
|
|
lastLineIndex = 0;
|
|
saObj.description = [];
|
|
saObj.specialAbilities = [];
|
|
//need to remove newlines that are right after an (Su) this is necessary for PRD
|
|
str = str.replace(/\((Ex|Sp|Su)\)\s*(?:\r\n|[\n\v\f\r\x85\u2028\u2029])/gi, '($1) ');
|
|
//break on newlines
|
|
//We break 3 spaces, or on last period before a (Ex|Sp|Su)
|
|
//because sometimes special abilities do not have newlines between them.
|
|
lines = str.split(/\s\s\s|\r\n|[\n\v\f\r\x85\u2028\u2029]|special abilities|\.(?=[^\.]+\((?:Ex|Sp|Su)\))/i);
|
|
//here is the one that grabs period before (su)
|
|
// initiallines = str.split(/(?:\s\s\s|\r\n|^|[\.\n\v\f\r\x85\u2028\u2029])(?=\s*spells[:\s]|\s*[\w\s]+:|[^\.\v\r\n\x85\u2028\u2029]+(?:\(Su\):??|\(Ex\):??|\(Sp\):??))/i);
|
|
lines = SWUtils.trimBoth(lines).filter(function (line) {
|
|
return line && !/^special abilities$/i.test(line);
|
|
});
|
|
//TAS.debug("PFNPCParser.parseSpecialAbilities split into ",lines);
|
|
saObj = _.reduce(
|
|
lines,
|
|
function (memo, line) {
|
|
let spObj = {},
|
|
splitter = '',
|
|
tempstr = '',
|
|
startIdx,
|
|
endIdx = -1,
|
|
matches,
|
|
abilitytype = '';
|
|
try {
|
|
//TAS.debug("PFNPCParser.parseSpecialAbilities on line:"+line);
|
|
//why am i removing non word characters from the ends? what would be there?
|
|
matches = line.match(/\((Su|Ex|Sp)\)|^(\w+):/i);
|
|
if (!matches) {
|
|
//this is just part of the description
|
|
memo.description.push(line + '\r\n');
|
|
} else if (matches[2]) {
|
|
spObj.name = matches[2];
|
|
spObj.description = SWUtils.trimBoth(line.slice(matches[0].length + 1));
|
|
memo.specialAbilities.push(spObj);
|
|
} else {
|
|
tempstr = line.slice(0, matches.index);
|
|
spObj.name = tempstr.replace(/^[^\w]+|[^\w]$/, '');
|
|
spObj.basename = spObj.name.replace(/\s/g, '').toLowerCase();
|
|
spObj.rule_category = 'special-abilities';
|
|
spObj.ability_type = matches[1][0].toUpperCase() + matches[1][1].toLowerCase();
|
|
spObj.description = SWUtils.trimBoth(line.slice(matches.index + matches[0].length + 1));
|
|
matches = spObj.description.match(/(\d+d\d+) (?:points of){0,1}(.*?) damage/i);
|
|
if (matches) {
|
|
if (matches[1]) {
|
|
spObj.extraDamage = '[[' + matches[1] + ']]';
|
|
}
|
|
if (matches[2]) {
|
|
spObj.extraDamageType = matches[2];
|
|
}
|
|
} else {
|
|
matches = spObj.description.match(/([a-z]) for (\d+d\d+) (rounds|minutes|hours|days)/i);
|
|
if (matches) {
|
|
if (matches[2]) {
|
|
spObj.extraDamage = '[[' + matches[2] + ']] ' + matches[3] || '';
|
|
}
|
|
if (matches[1]) {
|
|
spObj.extraDamageType = matches[1];
|
|
}
|
|
}
|
|
}
|
|
//before dc is usually 'the save'
|
|
matches = spObj.description.match(/dc is (cha|con|wis|int|str|dex)[a-zA-Z]*.based/i);
|
|
//TAS.debug"parseSpecialAbilities looking for DC ability it is: ",matches);
|
|
if (matches && matches[1]) {
|
|
tempstr = matches[1].toUpperCase();
|
|
spObj.DCability = tempstr;
|
|
//TAS.debug"parseSpecialAbilities setting DC ability to "+tempstr);
|
|
} else if (PFDB.specialAttackDCAbilityBase[spObj.basename]) {
|
|
spObj.DCability = PFDB.specialAttackDCAbilityBase[spObj.basename];
|
|
//TAS.debug"parseSpecialAbilities setting DC ability to "+spObj.DCability+" based on "+ spObj.basename);
|
|
}
|
|
//before dc could be 'must make a', 'fails a'
|
|
matches = spObj.description.match(/DC (\d+) (Will|Fort|Ref)[a-zA-Z]* save/i);
|
|
if (matches) {
|
|
if (matches[1]) {
|
|
spObj.DC = matches[1];
|
|
}
|
|
if (matches[2]) {
|
|
tempstr = matches[2][0].toUpperCase() + matches[2].slice(1).toLowerCase();
|
|
spObj.save = tempstr;
|
|
}
|
|
} else {
|
|
matches = spObj.description.match(/(Will|Fort|Ref)[a-zA-Z]* DC (\d+) ([^),.])/i);
|
|
if (matches) {
|
|
if (matches[1]) {
|
|
tempstr = matches[1][0].toUpperCase() + matches[1].slice(1).toLowerCase();
|
|
spObj.save = tempstr;
|
|
if (matches[3]) {
|
|
spObj.save += ' ' + matches[3];
|
|
}
|
|
}
|
|
if (matches[2]) {
|
|
spObj.DC = matches[2];
|
|
}
|
|
}
|
|
}
|
|
memo.specialAbilities.push(spObj);
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseSpecialAbilities error parsing: ' + line + ' error is' + err);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
saObj,
|
|
);
|
|
//TAS.debug("parseSpecialAbilities returning",saObj);
|
|
return saObj;
|
|
}
|
|
function parseSpecialQualities(str) {
|
|
let matches,
|
|
rawAbilities,
|
|
saObjs = [];
|
|
if (str) {
|
|
//TAS.debug("PFNPCParser.parseSpecialQualities: "+str);
|
|
//skip over "SQ" in front
|
|
matches = str.match(/^SQ[\s:]*/i);
|
|
if (matches) {
|
|
str = str.slice(matches[0].length);
|
|
}
|
|
rawAbilities = SWUtils.splitByCommaIgnoreParens(str);
|
|
//TAS.debug("found the following:", rawAbilities);
|
|
_.each(rawAbilities, function (ability) {
|
|
let saAb = {},
|
|
type = '';
|
|
saAb.name = ability;
|
|
type = PFUtils.getSpecialAbilityTypeFromString(ability);
|
|
if (type) {
|
|
saAb.ability_type = type;
|
|
}
|
|
saAb.rule_category = 'special-qualities';
|
|
saObjs.push(saAb);
|
|
});
|
|
//TAS.debug"returning ", saObjs);
|
|
return saObjs;
|
|
}
|
|
return null;
|
|
}
|
|
function parseSLAs(spLAstr) {
|
|
let lines,
|
|
clname = '',
|
|
lastFreq = '',
|
|
tempstr = '',
|
|
lastPerDay = 0,
|
|
slas = {};
|
|
try {
|
|
slas.spellLikeAbilities = [];
|
|
slas.CL = 0;
|
|
slas.concentration = 0;
|
|
slas.classname = '';
|
|
lines = spLAstr.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/);
|
|
_.each(lines, function (line) {
|
|
let matches,
|
|
slatdivider,
|
|
SLAArray,
|
|
freqStr = '',
|
|
slaofTypeStr = '',
|
|
thisSlaObj = {},
|
|
rawDC = 0,
|
|
tempstr2 = '',
|
|
slatype = '',
|
|
numPerDay = 0,
|
|
slasOfType,
|
|
header = 0,
|
|
row = 0,
|
|
hasSpellLevel = 0,
|
|
freqIsPer = 0,
|
|
tempsplit;
|
|
try {
|
|
//TAS.debug"parsing "+line);
|
|
if (/CL\s*\d+/i.test(line) || /concentrat/i.test(line) || /psychic\smagic/i.test(line) || /spell.like.abilit/i.test(line)) {
|
|
header = 1;
|
|
} else if (/\u2013|\u2014|-/.test(line)) {
|
|
row = 1;
|
|
}
|
|
if (header) {
|
|
if (/CL\s*\d+/i.test(line)) {
|
|
matches = line.match(/CL\s*(\d+)/i);
|
|
if (matches[1]) {
|
|
slas.CL = parseInt(matches[1], 10) || 0;
|
|
}
|
|
}
|
|
if (/concentrat/i.test(line)) {
|
|
matches = line.match(/concentrat[\w]*\s*[+\-]??(\d+)/i);
|
|
if (matches[1]) {
|
|
slas.concentration = parseInt(matches[1], 10) || 0;
|
|
}
|
|
}
|
|
if (/psychic\smagic/i.test(line)) {
|
|
slas.classname = 'Psychic Magic';
|
|
} else {
|
|
slas.classname = 'Spell-like abilities';
|
|
}
|
|
} else if (row) {
|
|
//TAS.debug"splitting line "+line);
|
|
matches = line.match(/\u2013|\u2014|\-/);
|
|
slaofTypeStr = line.slice(matches.index + 1);
|
|
freqStr = SWUtils.trimBoth(line.slice(0, matches.index)).toLowerCase();
|
|
matches = freqStr.match(/constant|will|day|month/i);
|
|
if (matches && matches[0]) {
|
|
slatype = matches[0].toLowerCase();
|
|
thisSlaObj.type = slatype;
|
|
if (slatype === 'day' || slatype === 'month') {
|
|
freqIsPer = 1;
|
|
matches = freqStr.match(/\d+/);
|
|
if (matches && matches[0]) {
|
|
numPerDay = parseInt(matches[0], 10) || 0;
|
|
thisSlaObj.perDay = numPerDay;
|
|
}
|
|
}
|
|
} else {
|
|
tempsplit = freqStr.split('/');
|
|
if (tempsplit.length >= 2) {
|
|
freqIsPer = 1;
|
|
matches = tempsplit[0].match(/\d+/);
|
|
if (matches && matches[0]) {
|
|
numPerDay = parseInt(matches[0], 10) || 0;
|
|
thisSlaObj.perDay = numPerDay;
|
|
}
|
|
slatype = 'other';
|
|
thisSlaObj.type = slatype;
|
|
thisSlaObj.otherPer = tempsplit[1];
|
|
}
|
|
}
|
|
//TAS.debug"the frequency is " + slatype + " and are " + numPerDay + " per that");
|
|
slasOfType = SWUtils.splitByCommaIgnoreParens(slaofTypeStr);
|
|
//slaofTypeStr.split(/,\s*(?![^\(\)]*\))/);
|
|
SLAArray = _.reduce(
|
|
slasOfType,
|
|
function (memo, sla) {
|
|
let thissla = {},
|
|
dcstr = '';
|
|
try {
|
|
thissla.type = slatype;
|
|
if (freqIsPer && numPerDay > 0) {
|
|
thissla.perDay = numPerDay;
|
|
}
|
|
//look for spell level.
|
|
matches = sla.match(/level\s*(\d+)/i);
|
|
if (matches) {
|
|
if (matches[1]) {
|
|
//TAS.debug"spell level match on "+ sla+ " Is " + matches[1]);
|
|
thissla.spell_level = parseInt(matches[1], 10) || 0;
|
|
hasSpellLevel = 1;
|
|
}
|
|
sla = sla.replace(matches[0], '');
|
|
}
|
|
|
|
matches = sla.match(/D[Cc]\s*\d+/);
|
|
if (matches) {
|
|
tempstr2 = sla.replace(matches[0], '');
|
|
tempstr = matches[0].match(/\d+/);
|
|
rawDC = parseInt(tempstr, 10) || 0;
|
|
thissla.DC = rawDC;
|
|
matches = tempstr2.match(/\b(fortitude|willpower|reflex|fort|will|ref)\b([^,]+,)/i);
|
|
if (matches) {
|
|
thissla.save = matches[0]; //type of save up to first comma after it
|
|
}
|
|
}
|
|
//if parenthesis, name should be only what is in parens,
|
|
if (sla.indexOf('(') > 0) {
|
|
thissla.name = sla.slice(0, sla.indexOf('(') - 1);
|
|
tempstr = sla.slice(sla.indexOf('(') - 1);
|
|
//sla= tempstr;
|
|
//summon spells have levels
|
|
thissla.shortdesc = tempstr;
|
|
} else {
|
|
thissla.name = sla;
|
|
}
|
|
if (thissla.spell_level && /^summon/i.test(thissla.name)) {
|
|
thissla.name += ' Level ' + String(thissla.spell_level);
|
|
}
|
|
memo.push(thissla);
|
|
} catch (errslain) {
|
|
TAS.error('parseSLAs, error reducing to SLAArray for: ' + sla, errslain);
|
|
if (!thissla.name) {
|
|
thissla.name = sla;
|
|
} else {
|
|
thissla.description = sla;
|
|
}
|
|
memo.push(thissla);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
if (SLAArray && _.size(SLAArray) > 0) {
|
|
thisSlaObj.type = slatype;
|
|
if (freqIsPer && numPerDay > 0) {
|
|
thisSlaObj.perDay = numPerDay;
|
|
}
|
|
thisSlaObj.SLAs = SLAArray;
|
|
slas.spellLikeAbilities.push(thisSlaObj);
|
|
}
|
|
} else {
|
|
TAS.warn('Cannot parse ' + line);
|
|
return;
|
|
}
|
|
} catch (ierr) {
|
|
TAS.error('parseSLAs error parsing' + line, ierr);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
TAS.error('parseSLAs', err);
|
|
} finally {
|
|
if (slas.spellLikeAbilities && _.size(slas.spellLikeAbilities) > 0) {
|
|
return slas;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
/** parseSpells - parses spell string from compendium and returns js object
|
|
*@param {string} spellstr the block of spells known text ex: "Sorcerer Spells Known (CL 8th)\r\n3rd (3/day)-Fireball (DC12)," etc
|
|
*@returns {{classname:string,classLevel:number,concentration:number,domains:[string],oppositionschools:string, spellsByLevel:[{spelllevel:number,perDay:number,spells:[{name:string,DC:number,isDomain:number}]}]}
|
|
*/
|
|
function parseSpells(spellstr) {
|
|
let lines,
|
|
spells = {};
|
|
if (!spellstr) {
|
|
return null;
|
|
}
|
|
spells.classLevel = -1;
|
|
spells.concentration = -1;
|
|
spells.classname = '';
|
|
spells.spellsByLevel = [];
|
|
spells.spellnotes = '';
|
|
lines = spellstr.split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/);
|
|
spells = _.reduce(
|
|
lines,
|
|
function (omemo, line) {
|
|
let matches,
|
|
spellarray,
|
|
slatdivider,
|
|
splittedSpells,
|
|
dcstr,
|
|
tempstr,
|
|
temparray = [],
|
|
match,
|
|
thislvl = {},
|
|
slasOfType;
|
|
try {
|
|
if (/C[Ll]\s*\d+/i.test(line)) {
|
|
if (omemo.classLevel === -1) {
|
|
matches = line.match(/C[Ll]\s*(\d+)/i);
|
|
if (matches && matches[1]) {
|
|
omemo.classLevel = parseInt(matches[1], 10) || 0;
|
|
}
|
|
matches = line.match(/concentrat[\w]*\s*[+\-]??(\d+)/i);
|
|
if (matches && matches[1]) {
|
|
omemo.concentration = parseInt(matches[1], 10) || 0;
|
|
}
|
|
matches = line.match(/([\w\s]*)spells\s(?:known|prepared)/i);
|
|
if (matches && matches[1]) {
|
|
omemo.classname = matches[1].replace(/^\s|\s$/g, '');
|
|
omemo.classname = omemo.classname.toLowerCase();
|
|
} else if (/spells\sprepared/i.test(line)) {
|
|
omemo.classname = 'cleric'; //if prep caster then not a sorcerer
|
|
} else {
|
|
omemo.classname = 'sorcerer'; //default
|
|
}
|
|
}
|
|
} else if (/opposition schools|domains/i.test(line)) {
|
|
match = line.match(/opposition schools/i);
|
|
if (match) {
|
|
tempstr = line.slice(match.index + match[0].length);
|
|
omemo.oppositionschools = SWUtils.trimBoth(tempstr);
|
|
} else {
|
|
matches = line.match(/domains/i);
|
|
line = line.slice(matches.index + matches[0].length);
|
|
slatdivider = line.split(',');
|
|
temparray = slatdivider.map(function (d) {
|
|
return SWUtils.trimBoth(d);
|
|
});
|
|
omemo.domains = temparray;
|
|
}
|
|
} else if (/\u2013|\u2014|-/.test(line)) {
|
|
thislvl.perDay = -1;
|
|
thislvl.spellLevel = -1;
|
|
//look for endash, emdash, or dash default is emdash
|
|
slatdivider = line.split(/\u2013|\u2014|-/);
|
|
if (slatdivider && slatdivider[0]) {
|
|
matches = slatdivider[0].match(/^(\d+)/);
|
|
if (matches && matches[1]) {
|
|
thislvl.spellLevel = parseInt(matches[1], 10) || 0;
|
|
matches = slatdivider[0].match(/(\d+)\/day/i);
|
|
if (matches && matches[1]) {
|
|
thislvl.perDay = parseInt(matches[1], 10) || 0;
|
|
}
|
|
}
|
|
}
|
|
if (slatdivider && slatdivider[1]) {
|
|
splittedSpells = slatdivider[1].split(',');
|
|
spellarray = _.reduce(
|
|
splittedSpells,
|
|
function (memo, spell) {
|
|
let thisspell = {};
|
|
try {
|
|
matches = spell.split(/\(dc/i);
|
|
thisspell.name = matches[0].replace(/^\s|\s$/g, '');
|
|
if (matches[1]) {
|
|
dcstr = matches[1];
|
|
matches = dcstr.match(/\d+/);
|
|
if (matches && matches[0]) {
|
|
thisspell.DC = parseInt(matches[0], 10) || 0;
|
|
}
|
|
}
|
|
if (thisspell.name.slice(-1) === 'D') {
|
|
thisspell.name = SWUtils.trimBoth(thisspell.name.slice(0, -1));
|
|
thisspell.isDomain = 1;
|
|
if (!omemo.classname || omemo.classname !== 'cleric') {
|
|
omemo.classname = 'cleric';
|
|
}
|
|
}
|
|
memo.push(thisspell);
|
|
} catch (errinner) {
|
|
TAS.error('PFNPCParser.parseSpells errinner on spell: ' + spell + ' on line' + line, errinner);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
}
|
|
if (thislvl.spellLevel >= 0 && spellarray && spellarray.length > 0) {
|
|
thislvl.spells = spellarray;
|
|
omemo.spellsByLevel.push(thislvl);
|
|
}
|
|
} else {
|
|
//stuff is here but what? add to notes
|
|
omemo.spellnotes += line;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('PFNPCParser.parseSpells error on line ' + line, err);
|
|
} finally {
|
|
return omemo;
|
|
}
|
|
},
|
|
spells,
|
|
);
|
|
return spells;
|
|
}
|
|
function parseSpace(spaceStr) {
|
|
let retstr = spaceStr,
|
|
matches,
|
|
tempFloat;
|
|
try {
|
|
matches = spaceStr.match(/\s*(\d*\.?\d*)?/);
|
|
if (matches) {
|
|
tempFloat = parseFloat(matches[1]);
|
|
if (!isNaN) {
|
|
retstr = String(tempFloat);
|
|
}
|
|
}
|
|
} finally {
|
|
return retstr;
|
|
}
|
|
}
|
|
/** Gets info on caster from the spells known/prepared section.
|
|
* @param {Map<string,any>} spellObj output from parseSpells
|
|
* @param {Map<string,number>} abilityScores ability score base and modifiers
|
|
* @param {Map<string,any>} healthObj output from parseNPChp
|
|
* @param {boolean} isSLA if spell-like-ability then 1 else 0
|
|
* @returns {{'classname':string,'ability':string,'abilityMod':number,'CL':number,'concentrationBonus':number,'oppositionschools':string,'domains':[string],'spellnotes':string}}
|
|
*/
|
|
function getCasterObj(spellObj, abilityScores, healthObj, isSLA, forceCleric) {
|
|
let caster = {};
|
|
if (!spellObj || !abilityScores || !healthObj) {
|
|
return null;
|
|
}
|
|
try {
|
|
TAS.debug('getCasterObj spellObj,abilities,health are:', spellObj, abilityScores, healthObj);
|
|
caster.abilityMod = 0;
|
|
caster.CL = 0;
|
|
caster.concentrationBonus = 0;
|
|
if (forceCleric) {
|
|
caster.classname = 'Cleric';
|
|
caster.ability = 'WIS';
|
|
caster.abilityMod = abilityScores.wis.mod;
|
|
} else if (isSLA || !spellObj.classname) {
|
|
caster.classname = isSLA ? 'Spell-like abilities' : 'Sorcerer';
|
|
caster.ability = 'CHA';
|
|
caster.abilityMod = abilityScores.cha.mod;
|
|
} else {
|
|
caster.classname = spellObj.classname[0].toUpperCase() + spellObj.classname.slice(1).toLowerCase();
|
|
if (PFDB.casterDefaultAbility[spellObj.classname] && abilityScores[PFDB.casterDefaultAbility[spellObj.classname]]) {
|
|
caster.ability = PFDB.casterDefaultAbility[spellObj.classname].toUpperCase();
|
|
caster.abilityMod = abilityScores[PFDB.casterDefaultAbility[spellObj.classname]].mod;
|
|
} else {
|
|
caster.ability = 'CHA';
|
|
caster.abilityMod = abilityScores.cha.mod;
|
|
}
|
|
}
|
|
if (spellObj.classLevel) {
|
|
caster.CL = spellObj.classLevel;
|
|
} else {
|
|
//assume HD
|
|
caster.CL = healthObj.hdice1;
|
|
}
|
|
if (spellObj.concentration) {
|
|
caster.concentrationBonus = spellObj.concentration - caster.abilityMod - caster.CL;
|
|
}
|
|
if (spellObj.oppositionschools) {
|
|
caster.oppositionschools = spellObj.oppositionschools;
|
|
spellObj.oppositionschools = null;
|
|
}
|
|
if (spellObj.spellnotes) {
|
|
caster.spellnotes = spellObj.spellnotes;
|
|
spellObj.spellnotes = null;
|
|
}
|
|
if (spellObj.domains) {
|
|
caster.domains = spellObj.domains;
|
|
spellObj.domains = null;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('getCasterObj error trying to create obj returning null', err);
|
|
caster = null;
|
|
} finally {
|
|
//TAS.debug"returning ", caster);
|
|
return caster;
|
|
}
|
|
}
|
|
function setCasterFields(casterObj, classidx, attrs, setter) {
|
|
let alreadyPresent = false,
|
|
tempint = 0;
|
|
try {
|
|
//TAS.debug"setCasterFields");
|
|
setter = setter || {};
|
|
classidx = classidx || 0;
|
|
if (classidx < 0) {
|
|
classidx = 0;
|
|
}
|
|
if (attrs) {
|
|
if (attrs['spellclass-' + classidx + '-name'] || attrs['spellclass-' + classidx + '-level']) {
|
|
if (
|
|
!(
|
|
parseInt(attrs['spellclass-' + classidx + '-level'], 10) === parseInt(casterObj.CL, 10) &&
|
|
PFUtils.findAbilityInString(attrs['Concentration-' + classidx + '-ability']) === casterObj.ability.toUpperCase()
|
|
)
|
|
) {
|
|
classidx++;
|
|
} else {
|
|
alreadyPresent = true;
|
|
}
|
|
}
|
|
}
|
|
if (classidx > 2) {
|
|
TAS.error('Could not setCasterFields, 0,1,2 spellclasses already defined:');
|
|
casterObj.pageClassIdx = -1;
|
|
} else if (alreadyPresent) {
|
|
setter['spellclass-' + classidx + '-name'] = setter['spellclass-' + classidx + '-name'] + ' and ' + casterObj.classname;
|
|
casterObj.pageClassIdx = classidx;
|
|
} else {
|
|
setter['spellclass-' + classidx + '-name'] = casterObj.classname;
|
|
//should add class here ? setter['class-'+what+'-name']
|
|
setter['spellclass-' + classidx + '-level'] = casterObj.CL; //if they have hit dice, this will make it increase? not if we don't do class-x-level
|
|
setter['spellclass-' + classidx + '-level-total'] = casterObj.CL;
|
|
if (/wizard|cleric|druid|paladin|ranger|investigator|shaman|witch|alchemist|warpriest/i.test(casterObj.classname)) {
|
|
setter['spellclass-' + classidx + '-casting_type'] = 2; //prepared
|
|
} else {
|
|
setter['spellclass-' + classidx + '-casting_type'] = 1; //spontaneous
|
|
}
|
|
if (casterObj.ability) {
|
|
setter['Concentration-' + classidx + '-ability'] = casterObj.ability + '-mod';
|
|
}
|
|
setter['Concentration-' + classidx + '-mod'] = casterObj.abilityMod;
|
|
if (casterObj.concentrationBonus) {
|
|
setter['Concentration-' + classidx + '-misc'] = casterObj.concentrationBonus;
|
|
}
|
|
casterObj.pageClassIdx = classidx;
|
|
if (casterObj.oppositionschools) {
|
|
setter['spellclass-' + classidx + '-oppositionschool-0'] = casterObj.oppositionschools;
|
|
}
|
|
if (casterObj.spellnotes) {
|
|
setter['spellclass-' + classidx + '-notes'] = casterObj.spellnotes;
|
|
}
|
|
if (casterObj.domains) {
|
|
setter['spellclass-' + classidx + '-domains-show'] = 1;
|
|
tempint = 0;
|
|
_.each(casterObj.domains, function (domain) {
|
|
setter['spellclass-' + classidx + '-domain-' + tempint] = domain;
|
|
setter['spellclass-' + classidx + '-domaintype-' + tempint] = 'Domain';
|
|
tempint++;
|
|
if (tempint > 1) {
|
|
setter['domain0' + tempint + '_show'] = 1;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('setCasterFields', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
/** createSpellEntries
|
|
*@param {jsobject} setter - map to pass to SWUtils.setWrapper
|
|
*@param {jsobject} spellObj obj like: {classname:"name",CL:#,concentration:#,
|
|
* spells:{
|
|
* 0:[{name:spellname,DC:#}],
|
|
* 1:[{name:spellname},{name:spellname}]
|
|
* }}
|
|
*@param {?} casterObj ?
|
|
*@param {?} section ?
|
|
*@returns {jsobject} setter
|
|
*/
|
|
function createSpellEntries(spellObj, casterObj, setter) {
|
|
let section = 'spells';
|
|
setter = setter || {};
|
|
if (!spellObj || !casterObj) {
|
|
return setter;
|
|
}
|
|
_.each(spellObj.spellsByLevel, function (spellLevel) {
|
|
let thisSpellLevel = parseInt(spellLevel.spellLevel, 10) || 0,
|
|
baseDC = 0,
|
|
perdayPrefix = '';
|
|
try {
|
|
//TAS.debug"now look at level " + thisSpellLevel + " spells", spellLevel);
|
|
perdayPrefix = 'spellclass-' + casterObj.pageClassIdx + '-level-' + thisSpellLevel;
|
|
if (spellLevel.perDay) {
|
|
setter[perdayPrefix + '-class'] = spellLevel.perDay;
|
|
setter[perdayPrefix + '-spells-per-day_max'] = spellLevel.perDay;
|
|
setter[perdayPrefix + '-spells-per-day'] = spellLevel.perDay;
|
|
}
|
|
baseDC = 10 + thisSpellLevel + (parseInt(casterObj.abilityMod, 10) || 0);
|
|
} catch (errlvl) {
|
|
TAS.error('createSpellEntries error setting spells per day', errlvl);
|
|
}
|
|
setter = _.reduce(
|
|
spellLevel.spells,
|
|
function (memo, spell) {
|
|
let newRowId = generateRowID(),
|
|
thisDC = 0,
|
|
prefix = 'repeating_' + section + '_' + newRowId + '_';
|
|
try {
|
|
setter[prefix + 'name'] = spell.name[0].toUpperCase() + spell.name.slice(1);
|
|
setter[prefix + 'classnumber'] = casterObj.pageClassIdx;
|
|
setter[prefix + 'spell_class_r'] = casterObj.pageClassIdx;
|
|
setter[prefix + 'spellclass'] = casterObj.classname;
|
|
setter[prefix + 'spell_level'] = thisSpellLevel;
|
|
setter[prefix + 'spell_level_r'] = thisSpellLevel;
|
|
if (spell.DC) {
|
|
thisDC = parseInt(spell.DC, 10) || 0;
|
|
if (thisDC !== baseDC) {
|
|
setter[prefix + 'DC_misc'] = thisDC - baseDC;
|
|
}
|
|
setter[prefix + 'savedc'] = thisDC;
|
|
}
|
|
if (casterObj.concentration) {
|
|
setter[prefix + 'Concentration-mod'] = casterObj.concentration;
|
|
}
|
|
if (spell.isDomain) {
|
|
setter[prefix + 'isDomain'] = 1;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createSpellEntries error setting spell :', spell, err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
},
|
|
setter,
|
|
);
|
|
});
|
|
return setter;
|
|
}
|
|
function createSLAEntries(slaObj, casterObj, race, level, setter) {
|
|
let section = 'ability';
|
|
setter = setter || {};
|
|
if (!slaObj || !casterObj) {
|
|
return setter;
|
|
}
|
|
level = level || 0;
|
|
|
|
_.each(slaObj.spellLikeAbilities, function (perDaySLAs) {
|
|
let thisPerDay = parseInt(perDaySLAs.perDay, 10) || 0,
|
|
freqType = perDaySLAs.type;
|
|
//TAS.debug" at one set of SLAs, freq:" + freqType + " and perday:" + thisPerDay, perDaySLAs);
|
|
setter = _.reduce(
|
|
perDaySLAs.SLAs,
|
|
function (memo, SLA) {
|
|
let newRowId,
|
|
prefix = 'repeating_' + section + '_' + newRowId + '_',
|
|
casterAbility,
|
|
dcTot = 0,
|
|
dcMod = 0,
|
|
sdstr = '',
|
|
charlvl = 0,
|
|
clmisc = 0,
|
|
tempint = 0,
|
|
slmisc = 0,
|
|
casterlevel = 0;
|
|
try {
|
|
newRowId = generateRowID();
|
|
prefix = 'repeating_' + section + '_' + newRowId + '_';
|
|
memo[prefix + 'name'] = SLA.name[0].toUpperCase() + SLA.name.slice(1);
|
|
memo[prefix + 'ability_type'] = 'Sp';
|
|
memo[prefix + 'rule_category'] = 'spell-like-abilities';
|
|
memo[prefix + 'showinmenu'] = '1';
|
|
if (casterObj.ability) {
|
|
casterAbility = casterObj.ability;
|
|
memo[prefix + 'ability-basis'] = '@{' + casterObj.ability + '-mod}';
|
|
} else {
|
|
casterAbility = 'CHA';
|
|
memo[prefix + 'ability-basis'] = '@{CHA-mod}';
|
|
}
|
|
memo[prefix + 'CL-basis'] = '@{npc-hd-num}';
|
|
memo[prefix + 'CL-basis-mod'] = level;
|
|
if (race) {
|
|
memo[prefix + 'class-name'] = race;
|
|
}
|
|
//TAS.debug"CREATE SLA casterObj.CL: " + casterObj.CL + ", level:" + setter.level + " when processing "+ SLA );
|
|
if (casterObj.CL) {
|
|
tempint = level || 0;
|
|
if (tempint > 0) {
|
|
memo[prefix + 'CL-misc'] = casterObj.CL - tempint;
|
|
memo[prefix + 'CL-misc-mod'] = casterObj.CL - tempint;
|
|
}
|
|
casterlevel = casterObj.CL;
|
|
} else {
|
|
casterlevel = level;
|
|
}
|
|
|
|
memo[prefix + 'casterlevel'] = casterlevel;
|
|
//assume 1/2? or calc based on DC?
|
|
if (SLA.spell_level) {
|
|
if (SLA.spell_level === level) {
|
|
memo[prefix + 'spell_level-basis'] = '@{casterlevel}';
|
|
} else if (SLA.spell_level === Math.floor(level / 2)) {
|
|
memo[prefix + 'spell_level-basis'] = 'floor(@{casterlevel}/2)';
|
|
} else {
|
|
memo[prefix + 'spell_level-basis'] = '0';
|
|
memo[prefix + 'spell_level-misc'] = SLA.spell_level;
|
|
}
|
|
} else {
|
|
memo[prefix + 'spell_level-basis'] = 'floor(@{casterlevel}/2)';
|
|
}
|
|
//memo[prefix+"classnumber"]=casterObj.pageClassIdx;
|
|
//memo[prefix+"spellclass"]=casterObj.classname;
|
|
switch (freqType) {
|
|
case 'day':
|
|
memo[prefix + 'frequency'] = 'perday';
|
|
memo[prefix + 'used'] = thisPerDay;
|
|
memo[prefix + 'used_max'] = thisPerDay;
|
|
memo[prefix + 'max-calculation'] = thisPerDay;
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
memo[prefix + 'hasuses'] = '1';
|
|
break;
|
|
case 'will':
|
|
memo[prefix + 'frequency'] = 'atwill';
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
break;
|
|
case 'constant':
|
|
memo[prefix + 'frequency'] = 'constant';
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
break;
|
|
case 'month':
|
|
memo[prefix + 'frequency'] = 'permonth';
|
|
memo[prefix + 'used'] = thisPerDay;
|
|
memo[prefix + 'used_max'] = thisPerDay;
|
|
memo[prefix + 'max-calculation'] = thisPerDay;
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
memo[prefix + 'hasuses'] = '1';
|
|
break;
|
|
case 'everyrounds':
|
|
memo[prefix + 'frequency'] = 'everyrounds';
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
memo[prefix + 'rounds_between'] = SLA.used || '';
|
|
break;
|
|
case 'other':
|
|
memo[prefix + 'frequency'] = 'other';
|
|
memo[prefix + 'used'] = thisPerDay;
|
|
memo[prefix + 'used_max'] = thisPerDay;
|
|
memo[prefix + 'max-calculation'] = thisPerDay;
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
memo[prefix + 'hasuses'] = '1';
|
|
if (slaObj.otherPer) {
|
|
sdstr = 'Frequency per :' + slaObj.otherPer;
|
|
}
|
|
break;
|
|
default:
|
|
memo[prefix + 'frequency'] = 'not-applicable';
|
|
memo[prefix + 'used'] = 0;
|
|
memo[prefix + 'used_max'] = 0;
|
|
memo[prefix + 'max-calculation'] = '';
|
|
memo[prefix + 'hasfrequency'] = '';
|
|
memo[prefix + 'hasuses'] = '';
|
|
break;
|
|
}
|
|
if (SLA.save) {
|
|
memo[prefix + 'save'] = SLA.save;
|
|
}
|
|
if (SLA.DC) {
|
|
try {
|
|
if (!SLA.save) {
|
|
memo[prefix + 'save'] = 'See Text';
|
|
}
|
|
if (casterObj.abilityMod) {
|
|
tempint = 0;
|
|
if (SLA.spell_level) {
|
|
tempint = 10 + casterObj.abilityMod + SLA.spell_level;
|
|
} else {
|
|
tempint = 10 + casterObj.abilityMod + Math.floor(casterlevel / 2);
|
|
}
|
|
if (tempint !== SLA.DC) {
|
|
memo[prefix + 'spell_level-misc'] = SLA.DC - tempint;
|
|
memo[prefix + 'spell_level-misc-mod'] = SLA.DC - tempint;
|
|
}
|
|
}
|
|
} catch (err3) {
|
|
TAS.error('createSLAentries, error trying to calculate DC: ' + SLA, err3);
|
|
}
|
|
}
|
|
if (SLA.description) {
|
|
memo[prefix + 'description'] = SLA.description;
|
|
}
|
|
if (SLA.shortdesc) {
|
|
if (sdstr) {
|
|
sdstr = SLA.shortdesc + ', ' + sdstr;
|
|
} else {
|
|
sdstr = SLA.shortdesc;
|
|
}
|
|
}
|
|
if (sdstr) {
|
|
memo[prefix + 'short-description'] = sdstr;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createSLAEntries error setting SLA :', SLA, err);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
setter,
|
|
);
|
|
});
|
|
return setter;
|
|
}
|
|
/**createAttacks - creates rows in repeating_weapon
|
|
* @param {[{enh:number,name:string,type:string,countFullBAB:number,plus:string,note:string,iter:[number],dmgdice:number,dmgdie:number,crit:number,critmult:number,dmgbonus:number}]} attacklist
|
|
* @param {Map<string,number>} attackGrid populated as out param by parseAndCreateAttacks
|
|
* @param {Map<string,number>} abilityScores output of parseAbilityScores
|
|
* @param {[string]} importantFeats list of attack-affecting feats this char has
|
|
* @param {number} defaultReach the default reach for melee attacks
|
|
* @param {[[string,number]]} exceptionReaches list of attack names and reach numbers
|
|
* @param {Map<string,number>} sizeMap output from PFSize.getSizeFromText
|
|
* @param {Map<string,any>} setter the map to pass to SWUtils.setWrapper
|
|
* @returns {Map<string,any>} setter
|
|
*/
|
|
function createAttacks(attacklist, attackGrid, abilityScores, importantFeats, defaultReach, exceptionReaches, sizeMap, setter) {
|
|
setter = setter || {};
|
|
if (!attacklist || _.size(attacklist) === 0) {
|
|
return setter;
|
|
}
|
|
|
|
//TAS.debug("##################","create attacks:", attacklist, attackGrid, abilityScores, importantFeats, defaultReach, exceptionReaches);
|
|
setter = _.reduce(
|
|
attacklist,
|
|
function (memo, attack) {
|
|
let newRowId = '',
|
|
prefix = '',
|
|
dmgAbilityStr = false,
|
|
specCMB = false,
|
|
i = 0,
|
|
iterativeNum = 0,
|
|
basebonus = 0,
|
|
tempInt = 0,
|
|
dmgMult = 1,
|
|
dmgMod = 0,
|
|
tohitbonus = 0,
|
|
name = '',
|
|
tempstr = '',
|
|
basename = '',
|
|
iterZero = NaN,
|
|
reach,
|
|
newRowId2,
|
|
prefix2;
|
|
//TAS.debug"creating attack row id:" + newRowId);
|
|
try {
|
|
newRowId = generateRowID();
|
|
prefix = 'repeating_weapon_' + newRowId + '_';
|
|
//TAS.debug"looking at attack:", attack);
|
|
tohitbonus = Math.max(attack.enh, attack.mwk);
|
|
basename = attack.basename;
|
|
//basename.replace(/^group.*?:\s*/,'');
|
|
name += attack.name;
|
|
if (attack.plus) {
|
|
name += ' Plus ' + attack.plus;
|
|
}
|
|
memo[prefix + 'name'] = name;
|
|
memo[prefix + 'default_size'] = sizeMap.size;
|
|
if (attack.atktype === 'ranged') {
|
|
basebonus = attackGrid.ranged;
|
|
memo[prefix + 'attack-type'] = 'attk-ranged';
|
|
memo[prefix + 'attack-type-mod'] = attackGrid.ranged;
|
|
memo[prefix + 'isranged'] = 1;
|
|
} else if (PFDB.cmbPlusStrsrch.test(basename)) {
|
|
basebonus = attackGrid.cmb;
|
|
memo[prefix + 'attack-type'] = 'CMB';
|
|
memo[prefix + 'attack-type-mod'] = attackGrid.cmb;
|
|
dmgAbilityStr = true;
|
|
dmgMult = 1.5;
|
|
specCMB = true;
|
|
} else if (attack.atktype === 'cmb') {
|
|
basebonus = attackGrid.cmb;
|
|
memo[prefix + 'attack-type'] = 'CMB';
|
|
memo[prefix + 'attack-type-mod'] = attackGrid.cmb;
|
|
} else if (attack.atktype === 'special') {
|
|
basebonus = 0;
|
|
memo[prefix + 'attack-type-mod'] = 0;
|
|
memo[prefix + 'total-attack'] = 0;
|
|
if (attack.basename === 'trample') {
|
|
dmgAbilityStr = true;
|
|
dmgMult = 1.5;
|
|
TAS.debug('CREATEATTACKS trample the attack is : ', attack);
|
|
}
|
|
} else {
|
|
dmgAbilityStr = true;
|
|
//melee
|
|
if (importantFeats.weaponfinesse) {
|
|
//assume all attacks use weapon finesse
|
|
basebonus = attackGrid.melee2;
|
|
memo[prefix + 'attack-type'] = 'attk-melee2';
|
|
memo[prefix + 'attack-type-mod'] = attackGrid.melee2;
|
|
} else {
|
|
basebonus = attackGrid.melee;
|
|
memo[prefix + 'attack-type'] = 'attk-melee';
|
|
memo[prefix + 'attack-type-mod'] = attackGrid.melee;
|
|
}
|
|
if (attack.type === 'natural') {
|
|
if (attack.naturaltype === 'secondary') {
|
|
dmgMult = 0.5;
|
|
} else if (attack.dmgMult && attack.dmgMult === 1.5) {
|
|
memo[prefix + 'damage_ability_mult'] = '1.5';
|
|
dmgMult = 1.5;
|
|
}
|
|
}
|
|
}
|
|
//if(specCMB){
|
|
// TAS.debug("############ SPEC CMB ###############","dmgAbilityStr:"+dmgAbilityStr+" attackbonus:"+basebonus +", mult:"+ dmgMult);
|
|
//}
|
|
if (dmgAbilityStr || specCMB) {
|
|
if (specCMB && attack.dmgbonus === 0) {
|
|
memo[prefix + 'damage-ability'] = '0';
|
|
dmgMod = 0;
|
|
} else {
|
|
memo[prefix + 'damage-ability'] = 'STR-mod';
|
|
if (dmgMult !== 1) {
|
|
dmgMod = Math.floor(dmgMult * abilityScores.str.mod);
|
|
memo[prefix + 'damage_ability_mult'] = dmgMult;
|
|
} else {
|
|
dmgMod = abilityScores.str.mod;
|
|
}
|
|
}
|
|
memo[prefix + 'damage-ability-mod'] = dmgMod;
|
|
}
|
|
if (attack.enh) {
|
|
memo[prefix + 'enhance'] = attack.enh;
|
|
}
|
|
if (attack.mwk) {
|
|
memo[prefix + 'masterwork'] = '1';
|
|
}
|
|
if (!specCMB && attack.iter && attack.iter.length > 0) {
|
|
iterZero = parseInt(attack.iter[0], 10);
|
|
} else if (specCMB) {
|
|
iterZero = basebonus;
|
|
}
|
|
if (!isNaN(iterZero)) {
|
|
if (specCMB) {
|
|
memo[prefix + 'attack'] = 0;
|
|
memo[prefix + 'attack-mod'] = 0;
|
|
memo[prefix + 'total-attack'] = basebonus;
|
|
} else {
|
|
memo[prefix + 'attack'] = iterZero - tohitbonus - basebonus;
|
|
memo[prefix + 'attack-mod'] = iterZero - tohitbonus - basebonus;
|
|
memo[prefix + 'total-attack'] = iterZero;
|
|
}
|
|
} else if (attack.atktype === 'cmb') {
|
|
if (/swallowwhole|pin/i.test(attack.basename)) {
|
|
//if confirming crit add +5
|
|
memo[prefix + 'attack'] = 5;
|
|
memo[prefix + 'attack-mod'] = 5;
|
|
memo[prefix + 'total-attack'] = attackGrid.cmb + 5;
|
|
} else {
|
|
memo[prefix + 'total-attack'] = attackGrid.cmb;
|
|
}
|
|
} else {
|
|
memo[prefix + 'total-attack'] = 0;
|
|
}
|
|
if (attack.crit !== 20) {
|
|
memo[prefix + 'crit-target'] = attack.crit;
|
|
}
|
|
if (attack.critmult !== 2 && attack.critmult) {
|
|
memo[prefix + 'crit-multiplier'] = attack.critmult;
|
|
}
|
|
if (importantFeats.criticalfocus) {
|
|
memo[prefix + 'crit_conf_mod'] = 4;
|
|
}
|
|
//somewhere this is getting lost: just bandaid it:
|
|
if (!memo[prefix + 'total-attack']) {
|
|
memo[prefix + 'total-attack'] = 0;
|
|
}
|
|
TAS.debug('Create attack e6 ' + attack.name + ', enhance:' + attack.enh + ' dmg from attack:' + attack.dmgbonus + ', from str:' + dmgMod);
|
|
memo[prefix + 'damage-dice-num'] = attack.dmgdice;
|
|
memo[prefix + 'default_damage-dice-num'] = attack.dmgdice;
|
|
memo[prefix + 'damage-die'] = attack.dmgdie;
|
|
memo[prefix + 'default_damage-die'] = attack.dmgdie;
|
|
memo[prefix + 'damage'] = attack.dmgbonus - attack.enh - dmgMod;
|
|
memo[prefix + 'damage-mod'] = attack.dmgbonus - attack.enh - dmgMod;
|
|
memo[prefix + 'total-damage'] = attack.dmgbonus;
|
|
if (attack.note) {
|
|
memo[prefix + 'notes'] = '(' + attack.type + ') ' + attack.note;
|
|
} else {
|
|
memo[prefix + 'notes'] = '(' + attack.type + ')';
|
|
}
|
|
if (attack.iter.length > 1) {
|
|
for (i = 1; i < attack.iter.length; i++) {
|
|
iterativeNum = i + 1;
|
|
//TAS.debug"at iteration " + iterativeNum + ", difference is :" + (attack.iter[i] - attack.iter[0]));
|
|
memo[prefix + 'toggle_iterative_attack' + iterativeNum] = '@{var_iterative_attack' + iterativeNum + '_macro}';
|
|
memo[prefix + 'iterative_attack' + iterativeNum + '_value'] = attack.iter[i] - attack.iter[0];
|
|
}
|
|
} else if (attack.countFullBAB > 1) {
|
|
for (i = 1; i < attack.countFullBAB; i++) {
|
|
iterativeNum = i + 1;
|
|
memo[prefix + 'toggle_iterative_attack' + iterativeNum] = '@{var_iterative_attack' + iterativeNum + '_macro}';
|
|
memo[prefix + 'iterative_attack' + iterativeNum + '_value'] = 0;
|
|
}
|
|
}
|
|
// plus extra damage **********************
|
|
if (attack.plusamount) {
|
|
memo[prefix + 'precision_dmg_macro'] = '[[' + attack.plusamount + ']]';
|
|
if (attack.plustype) {
|
|
memo[prefix + 'precision_dmg_type'] = attack.plustype;
|
|
}
|
|
} else if (attack.plus) {
|
|
memo[prefix + 'precision_dmg_type'] = 'Plus';
|
|
memo[prefix + 'precision_dmg_macro'] = attack.plus;
|
|
}
|
|
if (attack.dmgtype) {
|
|
memo[prefix + 'notes'] = memo[prefix + 'notes'] + ', damage type:' + attack.dmgtype;
|
|
}
|
|
//reach **************************
|
|
if (attack.range) {
|
|
tempInt = parseInt(attack.range, 10);
|
|
if (isNaN(tempInt)) {
|
|
memo[prefix + 'notes'] = memo[prefix + 'notes'] + ', range:' + attack.range;
|
|
}
|
|
} else if (/tongue/i.test(attack.name)) {
|
|
reach = defaultReach * 3;
|
|
memo[prefix + 'range'] = reach;
|
|
memo[prefix + 'vs'] = 'touch';
|
|
} else if (attack.atktype === 'melee') {
|
|
if (exceptionReaches && exceptionReaches.length > 0) {
|
|
//TAS.log("looking for match",exceptionReaches);
|
|
reach = _.filter(exceptionReaches, function (reacharray) {
|
|
//TAS.log("matching "+basename+" with "+reacharray[0]);
|
|
if (basename.indexOf(reacharray[0]) >= 0) {
|
|
//TAS.log("it matches!"+reacharray[0]);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
//TAS.log(reach);
|
|
if (reach && reach[0] && reach[0][1]) {
|
|
memo[prefix + 'range'] = reach[0][1];
|
|
} else if (defaultReach) {
|
|
memo[prefix + 'range'] = defaultReach;
|
|
}
|
|
} else if (defaultReach) {
|
|
memo[prefix + 'range'] = defaultReach;
|
|
}
|
|
}
|
|
if (attack.vs && !/tongue/i.test(attack.name)) {
|
|
memo[prefix + 'vs'] = attack.vs;
|
|
}
|
|
if (attack.group) {
|
|
memo[prefix + 'group'] = attack.group;
|
|
}
|
|
if (attack.DC) {
|
|
TAS.debug('PFNPCParser has attack dc', attack);
|
|
memo[prefix + 'notes'] = memo[prefix + 'notes'] + ' ' + (attack.save || '') + ' ' + attack.DC + attack.DCEquation ? ' ' + attack.DCEquation : '';
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createattacks error on:', attack, err);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
},
|
|
setter,
|
|
);
|
|
//TAS.debug("end of create attacks returning:", setter);
|
|
return setter;
|
|
}
|
|
function createACEntries(acMap, abilityScores, importantFeats, hpMap, bab, level, setter) {
|
|
let acAbility = 'DEX',
|
|
acDexDef = abilityScores.dex.mod,
|
|
calcCMD = 0,
|
|
altbab = 0;
|
|
try {
|
|
setter = setter || {};
|
|
//TAS.debug("acMap", acMap);
|
|
if (acMap.altability) {
|
|
//this should no longer happen.
|
|
//TAS.debug("different ability score for AC!");
|
|
acAbility = acMap.altability.toUpperCase();
|
|
if (acAbility !== 'DEX') {
|
|
setter['AC-ability'] = acAbility + '-mod';
|
|
setter['CMD-ability2'] = acAbility + '-mod';
|
|
switch (acMap.altability.toLowerCase()) {
|
|
case 'wis':
|
|
acDexDef = abilityScores.wis.mod;
|
|
break;
|
|
case 'int':
|
|
acDexDef = abilityScores['int'].mod;
|
|
break;
|
|
case 'cha':
|
|
acDexDef = abilityScores.cha.mod;
|
|
break;
|
|
case 'con':
|
|
acDexDef = abilityScores.con.mod;
|
|
break;
|
|
default:
|
|
acDexDef = abilityScores.dex.mod;
|
|
break;
|
|
}
|
|
setter['AC-ability-mod'] = acDexDef;
|
|
}
|
|
}
|
|
//has uncanny dodge
|
|
if (acMap.uncanny) {
|
|
setter['FF-ability'] = acAbility + '-mod';
|
|
setter['FF-ability-mod'] = acDexDef;
|
|
setter['CMD-ability'] = acAbility + '-mod';
|
|
setter['CMD-ability'] = acDexDef;
|
|
setter['uncanny_dodge'] = 1;
|
|
setter['uncanny_cmd_dodge'] = 1;
|
|
}
|
|
altbab = bab;
|
|
if (importantFeats.defensivecombattraining) {
|
|
setter['hd_not_bab'] = 1;
|
|
altbab = Math.max((hpMap.hdice1 || 0) + (hpMap.hdice2 || 0), level || 0);
|
|
}
|
|
try {
|
|
calcCMD = 10 + altbab + abilityScores.str.mod + acDexDef + -1 * acMap.size;
|
|
//TAS.debug("bab:"+altbab+"+ str:"+ abilityScores.str.mod + "+ dex" + acDexDef + " - size: " +acMap.size + ", calcCMD:"+calcCMD+", cmdparsed:"+acMap.cmd);
|
|
if (isNaN(acMap.cmd) || calcCMD === acMap.cmd) {
|
|
setter['CMD'] = calcCMD;
|
|
} else {
|
|
setter['CMD'] = acMap.cmd;
|
|
setter['CMD-misc'] = acMap.cmd - calcCMD;
|
|
setter['CMD-misc-mod'] = acMap.cmd - calcCMD;
|
|
}
|
|
} catch (err2) {
|
|
TAS.error('createACEntries error trying to calculate CMD', err2);
|
|
}
|
|
|
|
setter['AC'] = acMap.ac;
|
|
setter['Touch'] = acMap.touch;
|
|
setter['Flat-Footed'] = acMap.ff;
|
|
setter['AC-deflect'] = acMap.deflect;
|
|
setter['AC-dodge'] = acMap.dodge;
|
|
setter['AC-misc'] = acMap.misc;
|
|
setter['AC-misc-mod'] = acMap.misc;
|
|
setter['AC-natural'] = acMap.natural;
|
|
if (acMap.armor) {
|
|
setter['armor3-equipped'] = '1';
|
|
setter['armor3-acbonus'] = acMap.armor;
|
|
setter['armor3'] = 'Armor bonus';
|
|
setter['AC-armor'] = acMap.armor;
|
|
}
|
|
if (acMap.shield) {
|
|
setter['shield3-equipped'] = '1';
|
|
setter['shield3-acbonus'] = acMap.shield;
|
|
setter['shield3'] = 'Shield bonus';
|
|
setter['AC-shield'] = acMap.shield;
|
|
}
|
|
if (acMap.notes) {
|
|
setter['defense-notes'] = acMap.notes;
|
|
}
|
|
if (acMap.cmdnotes) {
|
|
setter['cmd-notes'] = acMap.cmdnotes;
|
|
}
|
|
} catch (err) {
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createSkillEntries(skills, racial, abilityScores, importantFeats, classSkills, sizeMap, isUndead, setter) {
|
|
let npcSkillsWithFillInNames = ['Craft', 'Perform', 'Profession'],
|
|
craftLevel = -1,
|
|
performLevel = -1,
|
|
professionLevel = -1,
|
|
runningTot = 0,
|
|
counter = 0,
|
|
tempAbilities = PFSkills.coreSkillAbilityDefaults,
|
|
tempstr = '';
|
|
try {
|
|
setter = setter || {};
|
|
TAS.info('PFNPC createSkillEntries sizemap is: ', skills, racial, abilityScores, importantFeats, classSkills, sizeMap, isUndead);
|
|
if (racial) {
|
|
if (racial.abilitymods && _.size(racial.abilitymods) > 0) {
|
|
//set default ability for skill and substitute adjustments, make sure to use copy not original
|
|
tempAbilities = _.extend({}, PFSkills.coreSkillAbilityDefaults, racial.abilitymods);
|
|
/*setter = _.reduce(racial.abilitymods, function (memo, ability, skill) {
|
|
//CBTEST 20170601
|
|
///memo[skill + "-ability"] = + ability.toUpperCase() ; //can we do without setting this?
|
|
memo[skill + "-ability"] = ability+'-mod';
|
|
memo[skill + "-ability-mod"] = abilityScores[ability].mod;
|
|
return memo;
|
|
}, setter);*/
|
|
}
|
|
if (racial.skillmods && _.size(racial.skillmods) > 0) {
|
|
_.each(racial.skillmods, function (mod, skill) {
|
|
if (skill === 'Knowledge') {
|
|
_.each(PFSkills.knowledgeSkills, function (kSkill) {
|
|
setter[kSkill + '-racial'] = mod;
|
|
});
|
|
} else if (_.contains(PFSkills.coreSkillsWithFillInNames, skill)) {
|
|
_.each(PFSkills.allFillInSkillInstances[skill], function (subskill) {
|
|
setter[subskill + '-racial'] = mod;
|
|
});
|
|
} else {
|
|
setter[skill + '-racial'] = mod;
|
|
}
|
|
});
|
|
}
|
|
if (racial.skillnotes && racial.skillnotes.length > 0) {
|
|
tempstr = '';
|
|
_.each(racial.skillnotes, function (note) {
|
|
tempstr += note + ', ';
|
|
});
|
|
tempstr.replace(/,\s$/, '');
|
|
if (tempstr) {
|
|
setter['Skill-notes'] = tempstr;
|
|
}
|
|
}
|
|
}
|
|
if (importantFeats && _.size(importantFeats) > 0) {
|
|
if (importantFeats.intimidatingprowess) {
|
|
setter['Intimidate-feat'] = abilityScores.str.mod;
|
|
//setter["Intimidate-misc"] = '@{STR-mod}';
|
|
//setter["Intimidate-misc-mod"] = abilityScores.str.mod;
|
|
}
|
|
if (importantFeats.skillfocuses) {
|
|
_.each(importantFeats.skillfocuses, function (skill) {
|
|
if (skill === 'Knowledge') {
|
|
_.each(PFSkills.knowledgeSkills, function (kSkill) {
|
|
setter[kSkill + '-feat'] = 3;
|
|
});
|
|
} else if (_.contains(PFSkills.coreSkillsWithFillInNames, skill)) {
|
|
_.each(PFSkills.allFillInSkillInstances[skill], function (subskill) {
|
|
setter[subskill + '-feat'] = 3;
|
|
});
|
|
} else {
|
|
setter[skill + '-feat'] = 3;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (classSkills && _.size(classSkills) > 0) {
|
|
_.each(classSkills, function (skill) {
|
|
try {
|
|
if (skill === 'Knowledge') {
|
|
_.each(PFSkills.knowledgeSkills, function (kSkill) {
|
|
setter[kSkill + '-cs'] = 3;
|
|
});
|
|
} else if (_.contains(PFSkills.coreSkillsWithFillInNames, skill)) {
|
|
_.each(PFSkills.allFillInSkillInstances[skill], function (subskill) {
|
|
setter[subskill + '-cs'] = 3;
|
|
});
|
|
} else {
|
|
setter[skill + '-cs'] = 3;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createSkillEntries', err);
|
|
}
|
|
});
|
|
}
|
|
_.each(skills, function (tot, skill) {
|
|
let ability = '',
|
|
tempint = 0,
|
|
abilitymod = 0,
|
|
ranks = 0;
|
|
try {
|
|
tot = parseInt(tot, 10) || 0;
|
|
if (tempAbilities[skill]) {
|
|
ability = tempAbilities[skill];
|
|
abilitymod = abilityScores[ability] ? abilityScores[ability].mod : 0;
|
|
setter[skill + '-ability'] = ability.toUpperCase() + '-mod';
|
|
setter[skill + '-ability-mod'] = abilitymod;
|
|
setter[skill] = tot;
|
|
ranks = tot;
|
|
if (skill === 'Stealth') {
|
|
TAS.debug('skilltot 1:' + ranks);
|
|
}
|
|
ranks -= abilitymod;
|
|
if (skill === 'Stealth') {
|
|
TAS.debug('skilltot 2 minus ' + abilitymod + ' =:' + ranks);
|
|
}
|
|
if (skill === 'Stealth') {
|
|
if (sizeMap.skillSize !== 0) {
|
|
ranks -= 2 * sizeMap.skillSize;
|
|
}
|
|
TAS.debug('skilltot 3 minus ' + 2 * sizeMap.skillSize + ' =:' + ranks);
|
|
} else if (skill === 'Fly') {
|
|
if (sizeMap.skillSize !== 0) {
|
|
ranks -= sizeMap.skillSize;
|
|
}
|
|
}
|
|
if (racial && racial.skillmods && racial.skillmods[skill]) {
|
|
ranks -= parseInt(racial.skillmods[skill], 10) || 0;
|
|
if (skill === 'Stealth') {
|
|
TAS.debug('skilltot 4 minus ' + (parseInt(racial.skillmods[skill], 10) || 0) + ' =:' + ranks);
|
|
}
|
|
}
|
|
// if(importantFeats && importantFeats.skillfocuses && _.contains(importantFeats.skillfocuses,skill)){
|
|
if (parseInt(setter[skill + '-feat'], 10) > 0) {
|
|
ranks -= 3;
|
|
//ranks -= (parseInt(memo[skill + "-feat"], 10)||0);
|
|
if (skill === 'Stealth') {
|
|
TAS.debug('skilltot 5 minus 3 =:' + ranks);
|
|
}
|
|
}
|
|
if (ranks > 0) {
|
|
if (parseInt(setter[skill + '-cs'], 10) > 0) {
|
|
ranks -= 3;
|
|
if (skill === 'Stealth') {
|
|
TAS.debug('skilltot 6 minus 3 =:' + ranks);
|
|
}
|
|
}
|
|
}
|
|
if (skill === 'Stealth') {
|
|
TAS.debug('skilltot 7 final ranks are :' + ranks);
|
|
}
|
|
setter[skill + '-ranks'] = ranks;
|
|
if (ranks < 0) {
|
|
setter[skill + '-note'] = 'Ranks less than 0, possible parse error or error in statblock';
|
|
}
|
|
runningTot++;
|
|
} else {
|
|
TAS.warn('createSkillEntries, skill ' + skill + ' not found');
|
|
setter[skill + '-note'] = (setter[skill + '-note'] || '') + ', ' + skill + ':' + tot;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createSkillEntries inner reduce', err);
|
|
}
|
|
});
|
|
} catch (errouter) {
|
|
TAS.error('at createskillEntries OUTER error', errouter);
|
|
} finally {
|
|
TAS.info('leaving createskills:', setter);
|
|
return setter;
|
|
}
|
|
}
|
|
/**createInitEntries adds init,init-misc,init-misc-mod,init-ability-mod values to setter
|
|
* @param {Map<string,any>} setter the map to pass to SWUtils.setWrapper
|
|
* @param {number} baseInit the total initiative bonus
|
|
* @param {Map<string,{Map<string,number>>}} abilityScores output of parseAbilityScores
|
|
* @param {[string]} importantFeats list of attack-affecting feats this char has, IGNORED
|
|
* @returns {Map<string,any>} setter
|
|
*/
|
|
function createInitEntries(baseInit, abilityScores, importantFeats, setter) {
|
|
let initMisc = 0;
|
|
try {
|
|
setter = setter || {};
|
|
initMisc = baseInit - abilityScores.dex.mod;
|
|
setter['init'] = baseInit;
|
|
setter['init-misc'] = initMisc;
|
|
setter['init-misc-mod'] = initMisc;
|
|
setter['init-ability-mod'] = abilityScores.dex.mod;
|
|
} catch (err) {
|
|
TAS.error('createInitEntries', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createHPAbilityModEntry(abilityScores, isUndead, setter) {
|
|
try {
|
|
setter = setter || {};
|
|
if (isUndead || abilityScores.con.base === '-') {
|
|
setter['HP-ability'] = 'CHA-mod';
|
|
setter['HP-ability-mod'] = abilityScores.cha.mod;
|
|
} else {
|
|
setter['HP-ability-mod'] = abilityScores.con.mod;
|
|
}
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createHealthEntries(abilityScores, isUndead, hpMap, setter) {
|
|
let currlevel = 0;
|
|
try {
|
|
setter = setter || {};
|
|
setter['npc-hd-num'] = hpMap.hdice1;
|
|
setter['level'] = hpMap.hdice1;
|
|
setter['npc-hd'] = hpMap.hdie1;
|
|
setter['HP'] = hpMap.hp;
|
|
setter['HP_max'] = hpMap.hp;
|
|
setter['non-lethal-damage_max'] = hpMap.hp;
|
|
setter['auto_calc_hp'] = '1';
|
|
setter['both_whisper_show'] = '1';
|
|
//NPC: add to race row of class/race grid
|
|
if (hpMap.basehp) {
|
|
setter['NPC-HP'] = hpMap.basehp;
|
|
}
|
|
//bonuses
|
|
if (hpMap.misc) {
|
|
setter['HP-formula-macro-text'] = hpMap.misc;
|
|
setter['HP-formula-mod'] = hpMap.misc;
|
|
}
|
|
if (hpMap.heal) {
|
|
setter['npc-heal-conditions'] = hpMap.heal;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createHealthEntries', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createSpeedEntries(speedMap, importantFeats, setter) {
|
|
let tempstr = '';
|
|
try {
|
|
setter = setter || {};
|
|
_.each(speedMap, function (speed, stype) {
|
|
switch (stype) {
|
|
case 'land':
|
|
setter['speed-base'] = speed;
|
|
setter['speed-modified'] = speed;
|
|
break;
|
|
case 'fly':
|
|
setter['speed-fly'] = speed;
|
|
break;
|
|
case 'climb':
|
|
setter['speed-climb'] = speed;
|
|
break;
|
|
case 'swim':
|
|
setter['speed-swim'] = speed;
|
|
break;
|
|
case 'flyability':
|
|
setter['speed-fly-maneuverability'] = speed;
|
|
break;
|
|
default:
|
|
setter['speed-misc'] = speed;
|
|
if (tempstr.length > 0) {
|
|
tempstr += ', ';
|
|
}
|
|
tempstr += stype + ' ' + speed;
|
|
break;
|
|
}
|
|
});
|
|
if (tempstr) {
|
|
setter['speed-notes'] = tempstr;
|
|
}
|
|
if (importantFeats.run) {
|
|
setter['run-mult'] = 5;
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseAndSetSpeed error, speedMap', speedMap, err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createSaveEntries(abilityScores, isUndead, baseSaves, v, importantFeats, setter) {
|
|
let fortMisc,
|
|
refMisc,
|
|
willMisc,
|
|
tempNote = '',
|
|
tempstr = '';
|
|
try {
|
|
setter = setter || {};
|
|
fortMisc = baseSaves.baseFort - abilityScores.con.mod;
|
|
refMisc = baseSaves.baseRef - abilityScores.dex.mod;
|
|
willMisc = baseSaves.baseWill - abilityScores.wis.mod;
|
|
if (isUndead || abilityScores.con.base === '-') {
|
|
fortMisc = baseSaves.baseFort - abilityScores.cha.mod;
|
|
setter['Fort-ability'] = 'CHA-mod';
|
|
setter['Fort-ability-mod'] = abilityScores.cha.mod;
|
|
} else {
|
|
setter['Fort-ability-mod'] = abilityScores.con.mod;
|
|
}
|
|
if (importantFeats.greatfortitude) {
|
|
setter['Fort-misc'] = 2;
|
|
setter['Fort-misc-mod'] = 2;
|
|
fortMisc -= 2;
|
|
}
|
|
setter['total-Fort'] = fortMisc;
|
|
setter['npc-Fort'] = fortMisc;
|
|
setter['Fort'] = baseSaves.baseFort;
|
|
tempNote = '';
|
|
tempstr = PFUtils.getNoteAfterNumber(v['fort_compendium']);
|
|
if (tempstr) {
|
|
tempNote += 'Fortitude ' + tempstr;
|
|
}
|
|
if (importantFeats.lightningreflexes) {
|
|
setter['Ref-misc'] = 2;
|
|
setter['Ref-misc-mod'] = 2;
|
|
refMisc -= 2;
|
|
}
|
|
setter['total-Ref'] = refMisc;
|
|
setter['npc-Ref'] = refMisc;
|
|
setter['Ref'] = baseSaves.baseRef;
|
|
if (abilityScores.dex.mod !== 0) {
|
|
setter['Ref-ability-mod'] = abilityScores.dex.mod;
|
|
}
|
|
tempstr = PFUtils.getNoteAfterNumber(v['ref_compendium']);
|
|
if (tempstr) {
|
|
tempNote += 'Reflex ' + tempstr;
|
|
}
|
|
if (importantFeats.ironwill) {
|
|
setter['Will-misc'] = 2;
|
|
setter['Will-misc-mod'] = 2;
|
|
willMisc -= 2;
|
|
}
|
|
setter['total-Will'] = willMisc;
|
|
setter['npc-Will'] = willMisc;
|
|
setter['Will'] = baseSaves.baseWill;
|
|
if (abilityScores.wis.mod !== 0) {
|
|
setter['Will-ability-mod'] = abilityScores.wis.mod;
|
|
}
|
|
tempstr = PFUtils.getNoteAfterNumber(v['will_compendium']);
|
|
if (tempstr) {
|
|
tempNote += tempstr;
|
|
}
|
|
if (tempNote) {
|
|
setter['saves_notes'] = tempNote;
|
|
setter['toggle_save_notes'] = '1';
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createSaveEntries', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createAbilityScoreEntries(abilityScores, setter) {
|
|
try {
|
|
setter = setter || {};
|
|
setter['STR-base'] = abilityScores.str.base;
|
|
setter['DEX-base'] = abilityScores.dex.base;
|
|
setter['CON-base'] = abilityScores.con.base;
|
|
setter['WIS-base'] = abilityScores.wis.base;
|
|
setter['INT-base'] = abilityScores['int'].base;
|
|
setter['CHA-base'] = abilityScores.cha.base;
|
|
setter['STR'] = abilityScores.str.base;
|
|
setter['DEX'] = abilityScores.dex.base;
|
|
setter['CON'] = abilityScores.con.base;
|
|
setter['WIS'] = abilityScores.wis.base;
|
|
setter['INT'] = abilityScores['int'].base;
|
|
setter['CHA'] = abilityScores.cha.base;
|
|
setter['STR-mod'] = abilityScores.str.mod;
|
|
setter['DEX-mod'] = abilityScores.dex.mod;
|
|
setter['CON-mod'] = abilityScores.con.mod;
|
|
setter['WIS-mod'] = abilityScores.wis.mod;
|
|
setter['INT-mod'] = abilityScores['int'].mod;
|
|
setter['CHA-mod'] = abilityScores.cha.mod;
|
|
} catch (err) {
|
|
TAS.error('createAbilityScoreEntries', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function parseAndCreateAttackGrid(abilityScores, sizeMap, importantFeats, bab, level, cmb_compendium, setter) {
|
|
let matches,
|
|
tempstr = '',
|
|
tempCMB,
|
|
tempBab = 0,
|
|
attackGrid = {},
|
|
miscCMB = 0,
|
|
calcCMB = 0;
|
|
try {
|
|
setter = setter || {};
|
|
attackGrid.melee = abilityScores.str.mod + bab + sizeMap.size;
|
|
setter['bab'] = bab;
|
|
setter['npc-bab'] = bab;
|
|
setter['melee-ability-mod'] = abilityScores.str.mod;
|
|
setter['attk-melee'] = attackGrid.melee;
|
|
setter['melee-ability'] = 'STR-mod';
|
|
setter['melee-ability-mod'] = abilityScores.str.mod;
|
|
setter['melee_bab'] = 'bab';
|
|
setter['melee_bab-mod'] = bab;
|
|
|
|
attackGrid.melee2 = abilityScores.dex.mod + bab + sizeMap.size;
|
|
setter['melee2-ability'] = 'DEX-mod';
|
|
setter['melee2-ability-mod'] = abilityScores.dex.mod;
|
|
setter['attk-melee2'] = attackGrid.melee2;
|
|
setter['attk_melee2_note'] = 'Weapon Finesse';
|
|
setter['melee2_bab'] = 'bab';
|
|
setter['melee2_bab-mod'] = bab;
|
|
|
|
attackGrid.ranged = abilityScores.dex.mod + bab + sizeMap.size;
|
|
attackGrid.ranged2 = attackGrid.ranged;
|
|
setter['ranged-ability'] = 'DEX-mod';
|
|
setter['ranged-ability-mod'] = abilityScores.dex.mod;
|
|
setter['attk-ranged'] = attackGrid.ranged;
|
|
setter['ranged_bab'] = 'bab';
|
|
setter['ranged_bab-mod'] = bab;
|
|
setter['ranged2-ability'] = 'DEX-mod';
|
|
setter['ranged2-ability-mod'] = abilityScores.dex.mod;
|
|
setter['attk-ranged2'] = attackGrid.ranged;
|
|
setter['ranged2_bab'] = 'bab';
|
|
setter['ranged2_bab-mod'] = bab;
|
|
|
|
if (importantFeats.defensivecombattraining) {
|
|
setter['cmb_bab'] = 'level';
|
|
setter['cmb_bab-mod'] = level;
|
|
setter['cmb2_bab'] = 'level';
|
|
setter['cmb2_bab-mod'] = level;
|
|
tempBab = level;
|
|
} else {
|
|
setter['cmb_bab'] = 'bab';
|
|
setter['cmb_bab-mod'] = bab;
|
|
setter['cmb2_bab'] = 'bab';
|
|
setter['cmb2_bab-mod'] = bab;
|
|
tempBab = bab;
|
|
}
|
|
|
|
if (importantFeats.agilemaneuvers) {
|
|
setter['CMB-ability'] = 'DEX-mod';
|
|
setter['CMB-ability-mod'] = abilityScores.dex.mod;
|
|
setter['CMB2-ability'] = 'DEX-mod';
|
|
setter['CMB2-ability-mod'] = abilityScores.dex.mod;
|
|
calcCMB = abilityScores.dex.mod + tempBab - sizeMap.size;
|
|
setter['cmb_desc'] = 'Agile Maneuvers';
|
|
} else {
|
|
setter['CMB-ability'] = 'STR-mod';
|
|
setter['CMB-ability-mod'] = abilityScores.str.mod;
|
|
setter['CMB2-ability'] = 'STR-mod';
|
|
setter['CMB2-ability-mod'] = abilityScores.str.mod;
|
|
calcCMB = abilityScores.str.mod + tempBab - sizeMap.size;
|
|
}
|
|
tempCMB = calcCMB;
|
|
if (cmb_compendium) {
|
|
matches = cmb_compendium.match(/\d+/);
|
|
if (matches) {
|
|
tempCMB = parseInt(matches[0], 10);
|
|
tempstr = cmb_compendium.slice(matches.index + matches[0].length);
|
|
if (tempstr) {
|
|
attackGrid.cmbnotes = tempstr;
|
|
setter['CMB-notes'] = tempstr;
|
|
}
|
|
}
|
|
}
|
|
setter['CMB'] = tempCMB;
|
|
attackGrid.cmb = tempCMB;
|
|
setter['CMB2'] = tempCMB;
|
|
attackGrid.cmb2 = tempCMB;
|
|
miscCMB = tempCMB - calcCMB;
|
|
if (miscCMB) {
|
|
setter['attk-CMB-misc'] = miscCMB;
|
|
setter['attk-CMB-misc-mod'] = miscCMB;
|
|
setter['attk-CMB2-misc'] = miscCMB;
|
|
setter['attk-CMB2-misc-mod'] = miscCMB;
|
|
}
|
|
|
|
if (importantFeats.criticalfocus) {
|
|
setter['cmb_crit_conf'] = 4;
|
|
setter['ranged_crit_conf'] = 4;
|
|
setter['melee_crit_conf'] = 4;
|
|
setter['cmb2_crit_conf'] = 4;
|
|
setter['ranged2_crit_conf'] = 4;
|
|
setter['melee2_crit_conf'] = 4;
|
|
}
|
|
} catch (errC) {
|
|
TAS.error('parseAndCreateAttackGrid error creating CMB attack types', errC);
|
|
} finally {
|
|
return attackGrid;
|
|
}
|
|
}
|
|
function parseAndCreateAttacks(abilityScores, sizeMap, importantFeats, bab, attackGrid, reachObj, meleeAtkStr, rangedAtkStr, setter) {
|
|
let attacklist = [],
|
|
matches,
|
|
tempstr = '';
|
|
try {
|
|
setter = setter || {};
|
|
// Attacks *****************************
|
|
if (meleeAtkStr) {
|
|
try {
|
|
attacklist = parseAttacks(meleeAtkStr, 'melee');
|
|
assignPrimarySecondary(attacklist);
|
|
createAttacks(attacklist, attackGrid, abilityScores, importantFeats, reachObj.reach, reachObj.reachExceptions, sizeMap, setter);
|
|
} catch (errM) {
|
|
TAS.error('parseAndCreateAttacks error creating melee attacks', errM);
|
|
}
|
|
}
|
|
if (rangedAtkStr) {
|
|
try {
|
|
attacklist = parseAttacks(rangedAtkStr, 'ranged');
|
|
createAttacks(attacklist, attackGrid, abilityScores, importantFeats, null, null, sizeMap, setter);
|
|
} catch (errR) {
|
|
TAS.error('parseAndCreateAttacks error creating ranged attacks', errR);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('parseAndCreateAttacks', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
function createFeatEntries(featlist, race, level, setter) {
|
|
return _.reduce(
|
|
featlist,
|
|
function (memo, feat) {
|
|
let newRowId = generateRowID(),
|
|
prefix = 'repeating_ability_' + newRowId + '_';
|
|
memo[prefix + 'name'] = feat;
|
|
memo[prefix + 'rule_category'] = 'feats';
|
|
memo[prefix + 'showinmenu'] = '1';
|
|
memo[prefix + 'CL-basis'] = '@{npc-hd-num}';
|
|
memo[prefix + 'CL-basis-mod'] = level || 0;
|
|
if (race) {
|
|
memo[prefix + 'class-name'] = race;
|
|
}
|
|
memo[prefix + 'row_id'] = newRowId;
|
|
memo[prefix + 'frequency'] = 'not-applicable'; //'not-applicable';
|
|
memo[prefix + 'ability_type'] = ''; //'not-applicable';
|
|
return memo;
|
|
},
|
|
setter,
|
|
);
|
|
}
|
|
function parseFeats(featstring2) {
|
|
if (featstring2.slice(0, 5).toLowerCase() === 'feats') {
|
|
featstring2 = featstring2.slice(5);
|
|
}
|
|
return SWUtils.splitByCommaIgnoreParens(featstring2);
|
|
}
|
|
function createFeatureEntries(abilitylist, abilityScoreMap, race, level, setter) {
|
|
let attrs = {},
|
|
creatureRace = '',
|
|
tempint = 0,
|
|
dc = 0,
|
|
abilityMod = 0,
|
|
charlevel = 0,
|
|
calcDC = 0;
|
|
try {
|
|
//TAS.debug("at createFeatureEntries:", abilitylist);
|
|
charlevel = Math.floor(level / 2);
|
|
creatureRace = race;
|
|
attrs = _.chain(abilitylist)
|
|
.map(function (ability) {
|
|
let match = null,
|
|
tempstr;
|
|
//copy only settings we want to keep and return them in a new obj.
|
|
//TAS.debug("first iter: ", ability);
|
|
try {
|
|
ability.description = ability.description || '';
|
|
if (ability.note) {
|
|
if (ability.description) {
|
|
ability.description += ', ';
|
|
}
|
|
ability.description += ability.note.replace(/,\s$/, '');
|
|
}
|
|
if (ability.other) {
|
|
if (ability.description) {
|
|
ability.description += ', ';
|
|
}
|
|
ability.description += ability.other.replace(/,\s$/, '');
|
|
ability.other = null;
|
|
}
|
|
if (!ability.ability_type) {
|
|
if (ability.name) {
|
|
tempstr = PFUtils.getSpecialAbilityTypeFromString(ability.name);
|
|
if (tempstr) {
|
|
ability.ability_type = tempstr;
|
|
ability.name = ability.name.replace(/\b(Su|Ex|Sp)\b/i, '').replace('()', '');
|
|
}
|
|
}
|
|
}
|
|
} catch (err3) {
|
|
TAS.error('createFeatureEntries err3', err3);
|
|
} finally {
|
|
//TAS.debug("this ability is:", ability);
|
|
return ability;
|
|
}
|
|
})
|
|
.filter(function (ability) {
|
|
if (ability.name) {
|
|
return true;
|
|
}
|
|
return false;
|
|
})
|
|
.reduce(function (memo, ability) {
|
|
let newRowId, prefix;
|
|
try {
|
|
newRowId = generateRowID();
|
|
prefix = 'repeating_ability_' + newRowId + '_';
|
|
memo[prefix + 'name'] = ability.name;
|
|
memo[prefix + 'row_id'] = newRowId;
|
|
memo[prefix + 'showinmenu'] = '1';
|
|
if (ability.shortdesc) {
|
|
memo[prefix + 'short-description'] = ability.shortdesc;
|
|
}
|
|
if (ability.description) {
|
|
memo[prefix + 'description'] = ability.description;
|
|
}
|
|
if (ability.used) {
|
|
if (ability.frequency && ability.frequency === 'everyrounds') {
|
|
memo[prefix + 'frequency'] = ability.frequency;
|
|
memo[prefix + 'rounds_between'] = ability.used;
|
|
memo[prefix + 'hasfrequency'] = '1';
|
|
memo[prefix + 'used'] = 0;
|
|
memo[prefix + 'used_max'] = 0;
|
|
memo[prefix + 'max-calculation'] = '';
|
|
} else {
|
|
if (ability.frequency) {
|
|
memo[prefix + 'frequency'] = ability.frequency;
|
|
memo[prefix + 'hasfrequency'] = ability.frequency;
|
|
} else {
|
|
memo[prefix + 'frequency'] = 'perday';
|
|
memo[prefix + 'hasfrequency'] = 'perday';
|
|
}
|
|
memo[prefix + 'hasuses'] = '1';
|
|
memo[prefix + 'used'] = ability.used;
|
|
memo[prefix + 'used_max'] = ability.used;
|
|
memo[prefix + 'max-calculation'] = ability.used;
|
|
}
|
|
} else {
|
|
memo[prefix + 'frequency'] = 'not-applicable'; //'not-applicable';
|
|
memo[prefix + 'used'] = 0;
|
|
memo[prefix + 'used_max'] = 0;
|
|
memo[prefix + 'max-calculation'] = '';
|
|
memo[prefix + 'hasfrequency'] = '';
|
|
memo[prefix + 'hasuses'] = '';
|
|
}
|
|
if (ability.dmgtype) {
|
|
memo[prefix + 'damage-type'] = ability.dmgtype;
|
|
}
|
|
if (ability.rule_category) {
|
|
memo[prefix + 'rule_category'] = ability.rule_category;
|
|
}
|
|
if (ability.ability_type) {
|
|
memo[prefix + 'ability_type'] = ability.ability_type;
|
|
} else {
|
|
memo[prefix + 'ability_type'] = ''; //'not-applicable';
|
|
}
|
|
memo[prefix + 'CL-basis'] = '@{npc-hd-num}';
|
|
memo[prefix + 'CL-basis-mod'] = setter.level || 0;
|
|
if (creatureRace) {
|
|
memo[prefix + 'class-name'] = creatureRace;
|
|
}
|
|
if (ability.save) {
|
|
memo[prefix + 'save'] = ability.save;
|
|
}
|
|
|
|
if (ability.DCability) {
|
|
memo[prefix + 'ability-basis'] = '@{' + ability.DCability.toUpperCase() + '-mod}';
|
|
abilityMod = abilityScoreMap[ability.DCability.toLowerCase()].mod;
|
|
} else if (ability.ability_type === 'Sp' || setter.is_undead) {
|
|
memo[prefix + 'ability-basis'] = '@{CHA-mod}';
|
|
abilityMod = abilityScoreMap.cha.mod;
|
|
} else {
|
|
memo[prefix + 'ability-basis'] = '@{CON-mod}';
|
|
abilityMod = abilityScoreMap.con.mod;
|
|
}
|
|
if (ability.extraDamage) {
|
|
memo[prefix + 'damage-macro-text'] = ability.extraDamage;
|
|
}
|
|
if (ability.extraDamageType) {
|
|
memo[prefix + 'damage-type'] = ability.extraDamageType;
|
|
}
|
|
memo[prefix + 'spell_level-basis'] = 'floor(@{casterlevel}/2)';
|
|
if (ability.DC) {
|
|
dc = parseInt(ability.DC, 10) || 0;
|
|
calcDC = abilityMod + charlevel + 10;
|
|
tempint = dc - calcDC;
|
|
if (tempint !== 0) {
|
|
memo[prefix + 'spell_level-misc'] = tempint;
|
|
memo[prefix + 'spell_level-misc-mod'] = tempint;
|
|
}
|
|
}
|
|
} catch (ierr2) {
|
|
TAS.error('createFeatureEntries', ierr2);
|
|
} finally {
|
|
return memo;
|
|
}
|
|
}, {})
|
|
.value();
|
|
//TAS.debug"createFeatureAttrs adding " + _.size(attrs) + " to " + _.size(setter), attrs);
|
|
setter = _.extend(setter, attrs);
|
|
} catch (err) {
|
|
TAS.error('createFeatureEntries', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
/** appends values of objects in sa2 to sa1 if name already exists in sa1
|
|
* by reference
|
|
* @param {Array} sa1 Array of {} js objects:list of special abilities maps. Must have 'name' property to compare
|
|
* @param {Array} sa2 Array of {} js objects:list of special abilities maps. Must have 'name' property to compare
|
|
* @returns {Array} sa2 concatenated with sa2, for any duplicates, we add properties from the sa2 version to sa1, but do not overwrite.
|
|
*/
|
|
function combineSpecialAbilities(sa1, sa2) {
|
|
let combined;
|
|
combined = _.map(sa1, function (sa) {
|
|
let existingSA;
|
|
try {
|
|
existingSA = _.findWhere(sa2, {name: sa.name});
|
|
if (existingSA) {
|
|
_.each(_.keys(existingSA), function (key) {
|
|
//TAS.debug("combining abilities: "+sa[key]+ " plus "+ existingSA[key]);
|
|
if (key === 'description') {
|
|
sa.description = (sa.description ? sa.description + ', ' : '') + (existingSA.description || '');
|
|
} else if (key === 'shortdesc') {
|
|
sa.shortdesc = (sa.shortdesc ? sa.shortdesc + ', ' : '') + (existingSA.shortdesc || '');
|
|
} else if (!sa[key] && existingSA[key]) {
|
|
sa[key] = existingSA[key];
|
|
}
|
|
});
|
|
}
|
|
} catch (err1) {
|
|
TAS.error('combineSpecialAbilities err1', err1);
|
|
} finally {
|
|
return sa;
|
|
}
|
|
});
|
|
sa2 = _.reject(sa2, function (sa) {
|
|
if (_.findWhere(sa1, {name: sa.name})) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
combined = _.union(combined, sa2);
|
|
return combined;
|
|
}
|
|
function createClassEntries(characterClass, attrs, setter) {
|
|
let sumlvls = 0,
|
|
currlvls = 0,
|
|
i = 0,
|
|
startidx = 0,
|
|
alreadyPresent = false;
|
|
try {
|
|
setter = setter || {};
|
|
currlvls = setter.level || 0;
|
|
if (characterClass.CL && characterClass.classname) {
|
|
for (i = 0; i < 7; i++) {
|
|
if (attrs['class-' + i + '-name'] || attrs['class-' + i + '-level'] > 0) {
|
|
startidx = i;
|
|
if (attrs['class-' + i + '-name'].toLowerCase() === characterClass.classname.toLowerCase()) {
|
|
alreadyPresent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (startidx >= 6) {
|
|
TAS.warning('too many classes, cannot add ' + characterClass.classname);
|
|
} else {
|
|
setter['class-' + startidx + '-name'] = characterClass.classname || '';
|
|
setter['class-' + startidx + '-level'] = characterClass.CL || 0;
|
|
}
|
|
if (characterClass.CL) {
|
|
currlvls += characterClass.CL || 0;
|
|
}
|
|
if ((setter.level || 0) !== currlvls) {
|
|
setter.level = currlvls;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
TAS.error('createClassEntries', err);
|
|
} finally {
|
|
return setter;
|
|
}
|
|
}
|
|
/**************************** THE BIG ONE ***********************/
|
|
/*importFromCompendium - imports all stuff*/
|
|
export function importFromCompendium(eventInfo, callback, errorCallback) {
|
|
let done = function (shouldupdate) {
|
|
TAS.info('##############################################');
|
|
TAS.info('Leaving importFromCompendium');
|
|
if (shouldupdate) {
|
|
PFSheet.checkForUpdate(true);
|
|
}
|
|
if (typeof callback === 'function') {
|
|
callback();
|
|
}
|
|
},
|
|
errorDone = _.once(function () {
|
|
//TAS.info("##############################################");
|
|
TAS.warn('Leaving importFromCompendium NOTHING DONE');
|
|
if (typeof errorCallback === 'function') {
|
|
errorCallback();
|
|
}
|
|
}),
|
|
fields = npcCompendiumAttributesPlayer.concat(['is_npc', 'alignment', 'npc_parse_no_recalc']);
|
|
getAttrs(fields, function (v) {
|
|
let setter = {},
|
|
abilityScores = {},
|
|
sizeMap = {},
|
|
speedMap = {},
|
|
hpMap = {},
|
|
acMap = {},
|
|
importantFeats = {},
|
|
reachObj = {},
|
|
racialModsMap = {},
|
|
skillsMap = {},
|
|
attackGrid = {},
|
|
baseFort = 0,
|
|
baseRef = 0,
|
|
baseWill = 0,
|
|
bab = 0,
|
|
featlist = [],
|
|
level = 0,
|
|
isUndead = false,
|
|
forceCleric = false,
|
|
specAbilObj = {},
|
|
npcdesc = '',
|
|
spellStr = '',
|
|
tempNote = '',
|
|
tempstr = '',
|
|
tempInt = 0,
|
|
tempFloat = 0.0,
|
|
tempobj = null,
|
|
baseInit = 0,
|
|
initMisc = 0,
|
|
spellcastingclass = -1,
|
|
cr,
|
|
attacklist,
|
|
hpMod,
|
|
tempArray,
|
|
spellObj,
|
|
casterObj,
|
|
matches,
|
|
attackArray,
|
|
classSkillArray,
|
|
specialAttacks,
|
|
SLAs,
|
|
attackArrays,
|
|
specialAbilities = {},
|
|
reachExceptions = [],
|
|
allSoFar = {},
|
|
specialQualities = [],
|
|
recalcWhenDone = 0,
|
|
match,
|
|
baseSaves = {};
|
|
//TAS.debug("importFromCompendium", v);
|
|
try {
|
|
baseFort = parseInt(v.fort_compendium, 10) || 0;
|
|
baseRef = parseInt(v.ref_compendium, 10) || 0;
|
|
baseWill = parseInt(v.will_compendium, 10) || 0;
|
|
bab = parseInt(v['bab_compendium'], 10) || 0;
|
|
//some basics ***************************************************
|
|
setter['level'] = 0;
|
|
if (!parseInt(v.npc_parse_no_recalc, 10)) {
|
|
recalcWhenDone = 1;
|
|
}
|
|
setter['is_npc'] = '1';
|
|
setter['is_v1'] = '1';
|
|
setter['PFSheet_Version'] = String(PFConst.version.toFixed(3));
|
|
setter['max-dex-source'] = 3;
|
|
PFMigrate.getAllMigrateFlags(setter);
|
|
if (v.xp_compendium) {
|
|
setter['npc-xp'] = v.xp_compendium;
|
|
}
|
|
if (v.cr_compendium) {
|
|
cr = v.cr_compendium.replace(/\s*cr\s*/i, '');
|
|
cr = SWUtils.trimBoth(cr);
|
|
setter['npc-cr'] = cr;
|
|
}
|
|
//setter["PC-Whisper"] = "/w gm";
|
|
//Creature Race and Type *****************************************************
|
|
//undead means use CHA instead of CON
|
|
if (v.type_compendium) {
|
|
setter['npc-type'] = v.type_compendium;
|
|
}
|
|
isUndead = /undead/i.test(v.type_compendium) || /undead/i.test(v.character_name);
|
|
if (isUndead) {
|
|
setter['is_undead'] = '1';
|
|
}
|
|
if (v.character_name) {
|
|
setter['race'] = v['character_name'];
|
|
}
|
|
/****************** class(es)******************************/
|
|
if (v.class_compendium) {
|
|
setter['add_class'] = 1;
|
|
tempInt = 0;
|
|
matches = v.class_compendium.split(',');
|
|
_.each(matches, function (classstr) {
|
|
let lvl = 0,
|
|
newclassstr = '',
|
|
localmatch;
|
|
try {
|
|
localmatch = classstr.match(/\d+/);
|
|
if (localmatch) {
|
|
lvl = parseInt(localmatch[0], 10) || 0;
|
|
newclassstr = SWUtils.trimBoth(classstr.replace(/\s*\d+\s*/, ' '));
|
|
}
|
|
createClassEntries({classname: newclassstr, CL: lvl}, {}, setter);
|
|
tempInt++;
|
|
} catch (cerr) {
|
|
TAS.error('PFNPCparser error trying to parse class' + v.class_compendium);
|
|
}
|
|
});
|
|
if (tempInt > 1) {
|
|
setter['multiclassed'] = 1;
|
|
setter['class1_show'] = 1;
|
|
}
|
|
tempInt = 0;
|
|
level = setter.level;
|
|
}
|
|
// Ability Scores *****************************************************************
|
|
abilityScores = parseAbilityScores(v, isUndead);
|
|
createAbilityScoreEntries(abilityScores, setter);
|
|
// Size **********************************************************************
|
|
sizeMap = PFSize.setSizeFromString(v.size_compendium, setter);
|
|
|
|
// Feats *********************************************************************
|
|
featlist = parseFeats(v['npc-feats-text']);
|
|
//TAS.debug("parseNPC featlist is ",featlist);
|
|
importantFeats = buildImportantFeatObj(featlist);
|
|
|
|
// Initiative *****************************************************************
|
|
baseInit = getNPCInit(v.init_compendium);
|
|
createInitEntries(baseInit, abilityScores, importantFeats, setter);
|
|
/********************** Saves and defense ************************/
|
|
if (v.dr_compendium) {
|
|
setter['DR'] = v.dr_compendium;
|
|
}
|
|
if (v.sr_compendium) {
|
|
setter['SR'] = v.sr_compendium;
|
|
setter['SR-macro-text'] = v.sr_compendium;
|
|
}
|
|
baseSaves = {
|
|
baseFort: baseFort,
|
|
baseRef: baseRef,
|
|
baseWill: baseWill,
|
|
};
|
|
createSaveEntries(abilityScores, isUndead, baseSaves, v, importantFeats, setter);
|
|
//hit points ****************************
|
|
createHPAbilityModEntry(abilityScores, isUndead, setter);
|
|
hpMod = parseInt(setter['HP-ability-mod'], 10);
|
|
//TAS.debug("calling parse hp with con mod of :" + hpMod);
|
|
hpMap = parseNPChp(v['npc_hp_compendium'], hpMod);
|
|
level += hpMap.hdice1 || 0;
|
|
setter.level = level;
|
|
createHealthEntries(abilityScores, isUndead, hpMap, setter);
|
|
//AC ************************************************
|
|
acMap = parseNPCAC(v['ac_compendium'], v.cmd_compendium, abilityScores.dex.mod, sizeMap.size);
|
|
|
|
createACEntries(acMap, abilityScores, importantFeats, hpMap, bab, level, setter);
|
|
|
|
// Misc *********************************************
|
|
if (!v.senses_compendium) {
|
|
matches = v.init_compendium.match(/senses/i);
|
|
if (matches && matches[0]) {
|
|
v.senses_compendium = SWUtils.trimBoth(v.init_compendium.slice(matches.index + 7));
|
|
}
|
|
}
|
|
if (v.senses_compendium) {
|
|
matches = v.senses_compendium.match(/perception/i);
|
|
if (matches) {
|
|
setter['vision'] = v.senses_compendium.slice(0, matches.index - 1);
|
|
} else {
|
|
setter['vision'] = v.senses_compendium;
|
|
}
|
|
}
|
|
if (v.speed_compendium) {
|
|
speedMap = parseSpeed(v.speed_compendium);
|
|
createSpeedEntries(speedMap, importantFeats, setter);
|
|
}
|
|
if (v.alignment) {
|
|
setter['alignment'] = v.alignment.toUpperCase();
|
|
}
|
|
if (v.space_compendium) {
|
|
setter['space'] = parseSpace(v.space_compendium);
|
|
}
|
|
|
|
// Reach *******************************************
|
|
reachObj = parseReach(v.reach_compendium);
|
|
if (reachObj) {
|
|
setter.reach = reachObj.reach;
|
|
if (reachObj.reachNotes) {
|
|
setter['reach-notes'] = reachObj.reachNotes;
|
|
}
|
|
} else {
|
|
reachObj = {};
|
|
reachObj.reach = 5;
|
|
reachObj.reachExceptions = [];
|
|
}
|
|
// Attacks *********************************************************
|
|
attackGrid = parseAndCreateAttackGrid(abilityScores, sizeMap, importantFeats, bab, level, v.cmb_compendium, setter);
|
|
//TAS.debug("PFNPCParser attack Grid is: ",attackGrid);
|
|
//#######set non list fields
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 1');
|
|
});
|
|
setter = {};
|
|
|
|
//######### set feats in list
|
|
createFeatEntries(featlist, allSoFar.race, allSoFar.level, setter);
|
|
if (_.size(setter)) {
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 2');
|
|
});
|
|
setter = {};
|
|
}
|
|
|
|
parseAndCreateAttacks(abilityScores, sizeMap, importantFeats, bab, attackGrid, reachObj, v['npc-melee-attacks-text'], v['npc-ranged-attacks-text'], setter);
|
|
if (_.size(setter)) {
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 3');
|
|
});
|
|
setter = {};
|
|
}
|
|
|
|
//spells***************************************************
|
|
//TAS.debug("checking for spells");
|
|
|
|
if (v['npc-spells-prepared_compendium']) {
|
|
spellStr = v['npc-spells-prepared_compendium'];
|
|
forceCleric = true;
|
|
} else if (v['npc-spells-known-text']) {
|
|
spellStr = v['npc-spells-known-text'];
|
|
}
|
|
if (spellStr) {
|
|
//advance index
|
|
spellcastingclass = 0;
|
|
setter['use_spells'] = 1;
|
|
//TAS.debug("has some spells");
|
|
spellObj = parseSpells(spellStr);
|
|
//TAS.info("the spells are:",spellObj);
|
|
if (spellObj) {
|
|
setter['use_spells'] = 1;
|
|
casterObj = getCasterObj(spellObj, abilityScores, hpMap, false, forceCleric);
|
|
//TAS.info("the caster object is",casterObj);
|
|
//do not add caster levels to hit dice or it gets screwed up
|
|
//setter = createClassEntries (setter,casterObj);
|
|
setCasterFields(casterObj, spellcastingclass, allSoFar, setter);
|
|
createSpellEntries(spellObj, casterObj, setter);
|
|
if (_.size(setter)) {
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 4');
|
|
});
|
|
setter = {};
|
|
}
|
|
}
|
|
}
|
|
//Spell-like-abilities***************************************************
|
|
//TAS.debug("checking for SLAs");
|
|
if (v['npc-spellike-ability-text']) {
|
|
SLAs = parseSLAs(v['npc-spellike-ability-text']);
|
|
if (SLAs) {
|
|
//TAS.debug("the SLAs are:", SLAs);
|
|
casterObj = getCasterObj(SLAs, abilityScores, hpMap, true);
|
|
setter = createSLAEntries(SLAs, casterObj, allSoFar.race, allSoFar.level, setter);
|
|
if (_.size(setter)) {
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 5');
|
|
});
|
|
setter = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
//TAS.debug("after parseAndCreateAttacks attrnum:" + _.size(setter));
|
|
//special Attacks ***************************************************
|
|
specialAttacks = parseSpecialAttacks(setter, v['npc-special-attacks'], attackGrid.cmb);
|
|
if (specialAttacks && specialAttacks.length > 0) {
|
|
attackArrays = _.groupBy(specialAttacks, 'specialtype');
|
|
createAttacks(attackArrays.attack, attackGrid, abilityScores, importantFeats, null, null, sizeMap, setter);
|
|
specialAbilities = attackArrays.ability;
|
|
if (_.size(setter)) {
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 6');
|
|
});
|
|
setter = {};
|
|
}
|
|
//TAS.debug("after createSpecialAttackEntries attrnum:" + _.size(setter));
|
|
}
|
|
//TAS.debug("before parsing special abilities are:", specialAbilities);
|
|
// content and special abilities ***************************
|
|
if (v.content_compendium) {
|
|
//TAS.debug("before parseSpecialAbilities attrnum:"+_.size(setter));
|
|
specAbilObj = parseSpecialAbilities(v.content_compendium);
|
|
|
|
//TAS.debug("returned from parse special abilities with", specAbilObj);
|
|
if (specAbilObj) {
|
|
if (specAbilObj.description && _.size(specAbilObj.description) > 0) {
|
|
npcdesc = _.reduce(
|
|
specAbilObj.description,
|
|
function (memo, line) {
|
|
memo += ' ';
|
|
memo += line;
|
|
return memo;
|
|
},
|
|
'',
|
|
);
|
|
setter['character_description'] = npcdesc;
|
|
}
|
|
if (specAbilObj.specialAbilities) {
|
|
specialAbilities = combineSpecialAbilities(specialAbilities, specAbilObj.specialAbilities);
|
|
}
|
|
} else {
|
|
setter['character_description'] = v.content_compendium;
|
|
}
|
|
//TAS.debug("now special abilities are:", specialAbilities);
|
|
}
|
|
if (v.SQ_compendium) {
|
|
//TAS.debug("found special qualities");
|
|
specialQualities = parseSpecialQualities(v.SQ_compendium);
|
|
if (specialQualities) {
|
|
specialAbilities = combineSpecialAbilities(specialAbilities, specialQualities);
|
|
}
|
|
}
|
|
if (specialAbilities && _.size(specialAbilities) > 0) {
|
|
createFeatureEntries(specialAbilities, abilityScores, allSoFar.race, allSoFar.level, setter);
|
|
//look for sneak attack
|
|
tempobj = _.find(specialAbilities, function (atkobj) {
|
|
return /sneak.attack/i.test(atkobj.name);
|
|
});
|
|
if (tempobj) {
|
|
setter['global_precision_dmg_macro'] = '[[[[floor((@{level}+1)/2)]]d6]]';
|
|
setter['global_precision_dmg_type'] = tempobj.name;
|
|
}
|
|
if (_.size(setter)) {
|
|
_.extend(allSoFar, setter);
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('saved 7');
|
|
});
|
|
setter = {};
|
|
}
|
|
//TAS.debug("after createFeatureEntries attrnum:" + _.size(setter));
|
|
}
|
|
|
|
//TAS.debug("before skills attrnum:" + _.size(setter));
|
|
// skills *********************************************************
|
|
if (v.skills_compendium) {
|
|
skillsMap = parseSkills(v.skills_compendium);
|
|
classSkillArray = getCreatureClassSkills(v.type_compendium);
|
|
if (v.racial_mods_compendium) {
|
|
racialModsMap = parseSkillRacialBonuses(v.racial_mods_compendium);
|
|
}
|
|
if (skillsMap && _.size(skillsMap) > 0) {
|
|
createSkillEntries(skillsMap, racialModsMap, abilityScores, importantFeats, classSkillArray, sizeMap, isUndead, setter);
|
|
//TAS.debug("after createSkillEntries attrnum:" + _.size(setter));
|
|
}
|
|
}
|
|
//if(_.size(setter)){
|
|
// _.extend(allSoFar,setter);
|
|
// SWUtils.setWrapper(setter,PFConst.silentParams,function(){TAS.notice("saved 8 SKILLS");});
|
|
// setter={};
|
|
//}
|
|
} catch (err2) {
|
|
TAS.error('importFromCompendium outer at end', err2);
|
|
} finally {
|
|
if (_.size(setter) || _.size(allSoFar)) {
|
|
setter['npc_import_now'] = 0;
|
|
setter['npc-compimport-show'] = 0;
|
|
setter['modify_dmg_by_size'] = 1;
|
|
setter['use_buff_bonuses'] = 1;
|
|
//TAS.info("##############################################","END OF importFromCompendium");
|
|
//TAS.debug("setting",setter);
|
|
if (recalcWhenDone) {
|
|
setter['is_newsheet'] = 1;
|
|
setter['recalc1'] = 1;
|
|
}
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, function () {
|
|
TAS.notice('##### finished settings now done');
|
|
done(recalcWhenDone);
|
|
});
|
|
} else {
|
|
setter['npc_import_now'] = 0;
|
|
setter['npc-compimport-show'] = 0;
|
|
SWUtils.setWrapper(setter, PFConst.silentParams, errorDone);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// PARSE CREATE NPC MONSTER
|
|
// change:npc_compendium_category
|
|
on(
|
|
'change:npc_import_now',
|
|
TAS.callback(function eventParseMonsterImport(eventInfo) {
|
|
if (eventInfo.sourceType === 'player' || eventInfo.sourceType === 'api') {
|
|
TAS.debug('caught ' + eventInfo.sourceAttribute + ' event: ' + eventInfo.sourceType);
|
|
getAttrs(['npc_import_now'], function (v) {
|
|
if (parseInt(v.npc_import_now, 10) === 1) {
|
|
importFromCompendium(eventInfo);
|
|
}
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
/*
|
|
on("sheet:compendium-drop", TAS.callback(function eventCompendiumDrop(eventInfo) {
|
|
if (eventInfo.sourceType === "player" ) {
|
|
TAS.debug("caught " + eventInfo.sourceAttribute + " event: " + eventInfo.sourceType);
|
|
importFromCompendium(eventInfo,function(){
|
|
getAttrs(['vision'],function(v){
|
|
var setter={}, matches,tempint=0;
|
|
setter.bar1_link = 'HP';
|
|
//setter.light_hassight = 1;
|
|
if(v.vision){
|
|
matches= v.vision.match(/darkvision (\d+)/i);
|
|
if (matches && matches[1]){
|
|
tempint = parseInt(matches[1],10)||60;
|
|
setter.light_radius=tempint;
|
|
setter.light_dimradius=tempint;
|
|
}
|
|
}
|
|
//setDefaultToken(setter);
|
|
});
|
|
});
|
|
}
|
|
}));
|
|
|
|
*/
|