Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Pathfinder Statblock Import to Character Sheet Script

Hi All, I upgraded to Mentor in the last week, and this weekend i've been cutting my javascript teeth on a stat block importer. I've managed to get it to the stage where its working for a fair few characters/monsters, so i think its time to get some feedback and maybe some beta testers :-) I realise the code is probably pretty awful by a lot of your standards, so my apologies, its my first attempt at using JavaScript, so bear with me! More importantly, big thanks to Peter W, Kevin and HoneyBadger who have done some solid work on import scripts already- i borrowed code and learned from a lot of your work, so thanks a ton! So i guess if you feel like looking over the code, any feedback would be greatly appreciated (there is a *lot* of error/ exception handling yet to put in). Also i am a complete noob at these forums, so i'm not even sure if the code will display... made a GitHub Repository and whacked the code into a file there, and link is below.... here goes nothing.... Pathfinder Importer
1421563438

Edited 1422853494
*** Edit V1.02 - Minor changes to how AC is calculated, tokens are now populated with HP showing as a bar on bar 3 which players can see, AC as number on bar 2 which they can't. Token name added and displayed. *** Edit V1.04 - Added in a statement to catch "undefined" variables being written to the character sheet and causing errors. Returns an error message and ignores the attribute. Also corrected an error in how the AC of creatures with negative dex bonus was being added up. *** Edit V2.01 - Replaced several functions with those provided by Aaron above! Should be more robust / efficient. *** Edit V2.02 - Minor change, fixed bug with "Other Gear" *** Edit V2.10 - Updated so that the script now parses Melee Attacks. It creates abilities (Attack Roll + Damage) and also partially completes the "Repeating Weapon X" section. This is still in testing - user beware! *** Edit V2.20 - Adapted to parse both Melee and Range. Also deals better with a few things like leading pluses (eg +4 longsword attack) *** Edit V2.21 - Added in error handling to ignore attacks for creatures without Melee or Ranged (the dragons were broke haha) *** Edit V2.22 - Added the Race, Class & Level, Weaknesses, DR, Resistances, Immunities, SR that were missing. *** Edit V2.23 - Fixed a bug where damage with no modifier (eg 1d6 flat, no +/-) in the damage strings would case errors. *** Edit V2.24 - Fixed a bug where attacks without enhancements were still trying to assign null values and throwing errors. ***Edit V2.25 - Completed the repeating weapons section and made it sum correctly (Thanks Kevin!) Also fixed the crit multipliers 3 and above that weren't getting picked up. Please note the Pathfinder Reference Document works the best because of its format, where as the D20pfsrd tends to do bothersome things with tables and need copying via notepad etc first. **** Troubleshooting! Script doesn't deal well with duplicates, especially for uniques that it searches for (size eg the word "Gargantuan" or "Large" appearing in a special ability is a common one- just abbreviate it in stat block to fix) See notes in this thread on Dire Rat for troubleshooting tips! /* PATHFINDER STAT BLOCK IMPORTER FOR ROLL20 API Author Jason.P 18/1/2015 Version 2.25 Updated to Parse both Ranged and Melee attacks (Aaron you will see a lot of your influence here! :-D) This script was written to import as much detail as possible from Pathfinder Reference Document's Stat Blocks into the Pathfinder NPC sheets. (may work a HeroLab stat blocks too, need to test) ****** Huge shout out first of all to Aaron for all his help and feedback. Wouldn't be able to do this without the tips! ****** Also Peter W for his original layout and the initial parsing. Kevin and HoneyBadger too for their work of this kind. IT IMPORTS: Name CR XP Race class name and level Alignment Type and Subtype (string) Size Initiative Bonus (so total Init adds up with dex mod correctly) Senses (string) Auras (string) AC bonuses broken down: Armour, Shield, Deflect, Dodge, Natural (sheet does Size, Dex)- all others report to Misc HP Fort, Ref, Will saves (bonuses that add up correctly with releant ability mods) + save notes Defensive Abiliites (string, only if the line leads with "Defensive Abilities") Weaknesses,DR,Resistances,Immunities,SR Speed (base, burrow, climb, fly, maneuverability, swim) Special Attacks (string) Tactics (Before combat, during combat as string) Abilities (STR - > CHA) Base Attack Bonus Feats Languages (as string) Special Qualities (as string) Gear (combat, Other as string) Melee and Ranged Attacks (Parses, writes attack roll and damage roll macros, partially updates Repeating Weapon slots in char sheet) IT DOES NOT AT THIS POINT IMPORT: special abilities spells CMB,CMD specifics and Notes Slowly working through the list of 'does nots'. next on the chopping block- Special abilities! INSTRUCTIONS 1. Go to PRD (PFSRD does work, but beware formatting!) website, find yourself some baddies (or NPC's) 2. Copy the stat block from *Name CRX* to Combat/Other Gear (or SQ, whatever is last. Can copy more, just doesn't get used) 3. Paste the stat block into the GM Notes Section of a token in your roll20 campaign. Clean up the title as you want it to appear in your Journal - like "Valeros CR12" 4. in the chat box, type the command "!PathfinderImport". Happy gaming! Hope this makes things easier for you to throw hoards of baddies at your players! Let me know if you have any feedback, be it tips, improvement ideas, or requests (keep in mind i was introduced to Javascript 2 days ago!) */ var RegExpEscapeSpecial =/([\/\\\/\[\]\(\)\{\}\?\+\*\|\.\^\$])/g; var AddAttribute = AddAttribute || {}; function AddAttribute(attr, value, charID) { if (value === undefined ) { log(attr + " has returned an undefined value."); sendChat("Error on " + attr + " attribute", "This attribute has been ignored."); } else { createObj("attribute", { name: attr, current: value, characterid: charID }); //use the line below for diagnostics! //log(attr + ", " + value); return; } } // function that adds the various abilities var AddAbility = AddAbility || {}; function addAbility(ability, text, charID) { createObj("ability", { name: ability, description: "", action: text, istokenaction: true, characterid: charID }); } function stripString(str, removeStr, replaceWith) { var r= new RegExp(removeStr.replace(RegExpEscapeSpecial,"\\$1"),'g'); return str.replace(r,replaceWith); } /*Cleans up the string leaving text and hyperlinks */ function cleanUpString(strSpecials) { strSpecials = stripString(strSpecials, "%20", " "); strSpecials = stripString(strSpecials, "%22", "\""); strSpecials = stripString(strSpecials, "%29", ")"); strSpecials = stripString(strSpecials, "%28", "("); strSpecials = stripString(strSpecials, "%2C", ","); var inParens = 0; for (var i = 0; i < strSpecials.length; i++) { if (strSpecials[i]==="(") inParens++; if (strSpecials[i]===")") inParens--; if ((inParens > 0) && (strSpecials[i]===",")) { var post = strSpecials.slice(i); strSpecials = strSpecials.replace(post,"") ; post = post.replace(","," "); strSpecials = strSpecials + post; } } strSpecials = stripString(strSpecials, "%3C", "<"); strSpecials = stripString(strSpecials, "%3E", ">"); strSpecials = stripString(strSpecials, "%23", "#"); strSpecials = stripString(strSpecials, "%3A", ":"); strSpecials = stripString(strSpecials, "%3B", ","); strSpecials = stripString(strSpecials, "%3D", "="); strSpecials = stripString(strSpecials, "</strong>", ""); strSpecials = stripString(strSpecials, "<strong>", ""); strSpecials = stripString(strSpecials, "</em>", ""); strSpecials = stripString(strSpecials, "<em>", ""); strSpecials = stripString(strSpecials, "%u2013", "-"); strSpecials = stripStringRegEx(strSpecials, "<b", ">"); strSpecials = stripString(strSpecials, "</b>", ""); strSpecials = stripStringRegEx(strSpecials, "<h", ">"); strSpecials = stripStringRegEx(strSpecials, "</h", ">"); strSpecials = stripString(strSpecials, "</a>", ""); strSpecials = stripStringRegEx(strSpecials, "<t", ">"); strSpecials = stripStringRegEx(strSpecials, "</t", ">"); while (strSpecials.search(/%../) != -1) { strSpecials = strSpecials.replace(/%../, ""); } return strSpecials; } /* Deletes any characters between the character a and b in incstr */ function stripStringRegEx(incstr, a, b) { var ea = a.replace(RegExpEscapeSpecial,"\\$1"), eb = b.replace(RegExpEscapeSpecial,"\\$1"), r = new RegExp( ea+'.*?'+eb , 'g'); return incstr.replace(r,''); } /* Deletes the links from the string str */ function removeLinks(str) { return stripStringRegEx(str, "<", ">"); } //looks for an occurrence of str in the array strArray, if found returns that element // on doConcat, strips a trailing "and" and concatenates with the next line. function findString(strArray, str, doConcat) { var retr, r = new RegExp(str.replace(RegExpEscapeSpecial,"\\$1")); _.find(strArray,function(v,k,l){ if(v.match(r)){ retr = v; if(doConcat && v.match(/and$/) && l[k+1]) { retr=retr.replace(/and$/,'')+', '+l[k+1]; } return true; } return false; }); return retr; }; /* returns the string between two characters a/b */ function getSubStr(str, a, b) { var ea = a.replace(RegExpEscapeSpecial,"\\$1"), eb = b.replace(RegExpEscapeSpecial,"\\$1"), r = new RegExp( ea+'(.*?)'+eb), m = str.match(r); return m && m[1]; } /* returns every string between two characters a/b */ function getAllSubStr(str, a, b) { var ea = a.replace(RegExpEscapeSpecial,"\\$1"), eb = b.replace(RegExpEscapeSpecial,"\\$1"), r = new RegExp( ea+'(.*?)'+eb,'g'), m = str.match(r); return m; } //removes numbers from array and trims white space on ends of elements function removeNumbersFromArray (strArray) { return _.map(strArray,function(s){ return s.replace(/\d+/g,'').trim(); }); } function removeNonNumericFromArray (strArray) { return _.map(strArray,function(s){ return parseInt(s.replace(/\D+/g,''),10 || 0); }); } function sumArray(numArray) { return _.reduce(numArray,function(acc,n){ return acc + ( parseInt(n,10) || 0 ); }, 0); } function getAbilityMod(ability) { return Math.floor((ability-10)/2); } function parseAttack(data,searchString, attackBonus, dmgBonus, reach,repeatStartNum, charID) { // start with the whole attack line var attackLine = findString(data, searchString, true); if (attackLine === false || attackLine === undefined) { return 0 } else { attackLine = attackLine.replace(searchString,""); attackLine = attackLine.trim(); //separate the attack line into two arrays, one with content from outside brackets, one inside. var attackBrackets = getAllSubStr(attackLine,"(", ")"); var attackNoBrackets = stripStringRegEx(attackLine,"(", ")"); attackNoBrackets = stripString(attackNoBrackets," and ",","); attackNoBrackets = stripString(attackNoBrackets," or ",","); attackNoBrackets = attackNoBrackets.split(","); //initialise the variables outside the loops var attackName = "", dmgDiceNum = "", dmgDiceSides = "", dmgDiceAdd = "", threatenString = "", critMultString = "", attackString = "" attackValues = [""] attack = 0 damage = 0 enhance = [0] mwk = [0] abilityAttackString = "" abiStrAttackHeader = "" abiStrAttack = "" abiStrAttackFooter = "" extraDice = ""; // cycle through each element in the array of attacks for ( i=0; i<attackNoBrackets.length; i++) { attackNoBrackets[i] = attackNoBrackets[i].trim(); //if there are any spaces inside brackets (indicating there is an extra effect) // then set the extraDice = everything after the first space, else "" if(attackBrackets[i].match(/\S+\s/) === null) { extraDice = "" } else { extraDice = attackBrackets[i].replace(/\S+\s/, ""); } extraDice = extraDice.replace(")",""); //adds the hits and crits mod tag for use in the damage macro later, as well as surrounding //anything in format XdX with [[ ]] brackets for an inline roll. extraDice = extraDice.replace(/(\d+d\d)/g,"[[(?{Hits-Landed|0}+?{Crits-Landed|0})*$1]]"); //search for anything followed by a + in the attack, store as name. ( ) save name separate to + attackName = attackNoBrackets[i].match(/(.*) \+/); //search for XdX(+/-)X and store as damage string. dmgString = attackBrackets[i].match(/(\d+)d(\d+)\+*?(\-*?\d+)/); //This if handles the case where the damage is just XdX (no addition or subtraction) if (dmgString === null) { dmgString = attackBrackets[i].match(/(\d+)d(\d+)/); dmgDiceAdd = 0; } else { dmgDiceAdd = parseInt(dmgString[3],10); } dmgDiceNum = parseInt(dmgString[1],10); dmgDiceSides = parseInt(dmgString[2],10); //search for X- as threaten (eg 19-20 = 19), "/x"X as crit multiplier threatenString = attackBrackets[i].match(/\/(\d+)-/); critMultString = attackBrackets[i].match(/\/(\d+)\)/); //if the first character is a + (eg +4 longsword) then remove the +, store the enhancement enhance[i] = 0 if (attackNoBrackets[i].charAt(0)=== "+") { enhance[i] = parseInt(attackNoBrackets[i].charAt(1),10); attackNoBrackets[i] = attackNoBrackets[i].slice(1); } if (enhance[i] > 0) { mwk[i] = 1 } else { mwk[i] = 0 } attackString = attackNoBrackets[i].match(/\+(\d+)/g); attackValues = [""] for (n=0; n<attackString.length; n++) { attackValues[n] = parseInt(attackString[n].replace("+",""),10); } if (threatenString === null) { threatenString = [20,20] } if (critMultString === null) { critMultString = ["/x2",2] } //define the parts of the attack formula (header, body, footer) abiStrAttackHeader = "/e @{Selected|Token_Name} attacks with "+attackName[1]+"!!!"; abiStrAttack = ""; for (j = 0; j< attackString.length;j++) { abiStrAttack = abiStrAttack + "\nAttack "+(j+1)+": [[1d20 "+attackString[j]+"]]" } abiStrAttackFooter = "\nCrit on "+threatenString[1]+critMultString[0]+", "+reach+"ft Range)"; //add the ability with the concatenated formula string abilityAttackString = abiStrAttackHeader + abiStrAttack + abiStrAttackFooter; addAbility(attackName[1], abilityAttackString, charID) //define the parts of the damage formula (Header, Body, Footer) abiStrDamageHeader = "/e @{Selected|Token_name}'s "+ attackName[1] + " damage"; abiStrDamage = "\nTotal: [[(?{Hits-Landed|0}*"+dmgDiceNum+")d"+dmgDiceSides+"+?{Hits-Landed|0}*"+dmgDiceAdd+"+(?{Crits-Landed|0}*"+dmgDiceNum*critMultString[1]+")d"+dmgDiceSides+"+?{Crits-Landed|0}*"+dmgDiceAdd*critMultString[1]+")]] in ?{Hits-Landed|0} Hits and ?{Crits-Landed|0} Criticals."; abiStrDamageFooter = "\n"+extraDice abilityDamageString = abiStrDamageHeader + abiStrDamage + abiStrDamageFooter; addAbility(attackName[1]+"-DMG", abilityDamageString, charID); var attackType = 0 var damageAbility = 0 if (searchString === "Melee") { attackType = "@{attk-melee}" damageAbility = "@{STR-mod}" } else if (searchString === "Ranged") { attackType = "@{attk-ranged}" } attack = attackValues[0]-attackBonus - enhance[i] //assumes all melee attacks use full str bonus... damage = dmgDiceAdd -dmgBonus - enhance[i] // add repeating weapon X attributes (enhance = 1 and masterwork by default at this point var repeatNum = i+repeatStartNum; AddAttribute("repeating_weapon_"+repeatNum+"_enhance",enhance[i],charID); AddAttribute("repeating_weapon_"+repeatNum+"_masterwork",mwk[i],charID); AddAttribute("repeating_weapon_"+repeatNum+"_name",attackName[1],charID); AddAttribute("repeating_weapon_"+repeatNum+"_attack",attack,charID); AddAttribute("repeating_weapon_"+repeatNum+"_attack-type",attackType,charID); AddAttribute("repeating_weapon_"+repeatNum+"_damage-dice-num",dmgDiceNum,charID); AddAttribute("repeating_weapon_"+repeatNum+"_damage-die",dmgDiceSides,charID); AddAttribute("repeating_weapon_"+repeatNum+"_damage",damage,charID); AddAttribute("repeating_weapon_"+repeatNum+"_damage-ability",damageAbility,charID); AddAttribute("repeating_weapon_"+repeatNum+"_crit-target",threatenString[1],charID); AddAttribute("repeating_weapon_"+repeatNum+"_crit-multiplier",critMultString[1],charID); AddAttribute("repeating_weapon_"+repeatNum+"_range",reach,charID); AddAttribute("repeating_weapon_"+repeatNum+"_proficiency","Yes",charID); AddAttribute("repeating_weapon_"+repeatNum+"_notes","TestNotes",charID); } return attackNoBrackets.length } } on('chat:message', function (msg) { // Only run when message is an api type and contains "!PathfinderImport" if (msg.type == 'api' && msg.content.indexOf('!PathfinderImport') !== -1) { if (!(msg.selected && msg.selected.length > 0)) return; // Make sure there's a selected object var token = getObj('graphic', msg.selected[0]._id); if (token.get('subtype') != 'token') return; // Don't try to set the light radius of a drawing or card //************* START CREATING CHARACTER**************** // get notes from token var originalGmNotes = token.get('gmnotes'); var gmNotes = token.get('gmnotes'); //strip string with function gmNotes = stripString(gmNotes, "%3C/table%3E", "%3Cbr"); gmNotes = stripString(gmNotes, "%3C/h1%3E", "%3Cbr"); gmNotes = stripString(gmNotes, "%3C/h2%3E", "%3Cbr"); gmNotes = stripString(gmNotes, "%3C/h3%3E", "%3Cbr"); gmNotes = stripString(gmNotes, "%3C/h4%3E", "%3Cbr"); //break the string down by line returns var data = gmNotes.split("%3Cbr"); //clean any characters excepting text and hyperlinks for (var i = 0; i < data.length; i++) { data[i] = cleanUpString(data[i]); data[i] = removeLinks(data[i]); if (data[i][0]===">") { data[i] = data[i].replace(">",""); } } for (var i = 0; i < data.length; i++) { if (data[i] !== null){ data[i] = data[i].trim(); } } var charName = data[0].trim(); // check if the character entry already exists, if so error and exit. var CheckSheet = findObjs({ _type: "character", name: charName }); if (CheckSheet.length > 0) { sendChat("ERROR", "This character already exists."); return; }; //Create character entry in journal, assign token var character = createObj("character", { avatar: token.get("imgsrc"), name: charName, bio: token.get('gmnotes'), gmnotes: token.get('gmnotes'), archived: false }); var charID = character.get('_id'); token.set("represents", charID); //Determine and enter CR var Header = data[0].split("CR"); var tokenName = Header[0].trim(); var CR = Header[1]; AddAttribute("npc-cr",CR,charID); //split and enter XP var xpHeader = data[1].split(" "); var XP = xpHeader[1]; AddAttribute("npc-xp",XP,charID); //race, class, level var raceMatch = data[2].match(/(\w+)\s(\w+)\s(\d+)/) if( raceMatch!= null) { var race = raceMatch[1], className = raceMatch[2], classLevel = raceMatch[3]; AddAttribute("race",race,charID) AddAttribute("class-0-name",className +" "+ classLevel,charID) } // Alignment, Size, Type var sizesWithSpace = "Fine ,Diminutive ,Tiny ,Small ,Medium ,Large ,Huge ,Gargantuan ,Colossal "; var sizesArray = sizesWithSpace.split(","); for (var i = 0; i < 9; i++) { if (findString(data, sizesArray[i], true) !== undefined) { var sizeLine = findString(data, sizesArray[i], true); break; } } //get subtype before destroying string var subType = getSubStr(sizeLine, "(", ")"); //remove the brackets and anything between them, trim the string, //create the array split at spaces, then assign the alignment to the sheet. var typeArray = stripStringRegEx(sizeLine, "(", ")"); typeArray = typeArray.trim(); typeArray = typeArray.split(" "); AddAttribute("alignment",typeArray[0],charID); // apparently i have to convert size into a value? var sizes = ["Fine","Diminutive","Tiny","Small","Medium","Large","Huge","Gargantuan","Colossal"]; var sizeTable = [8,4,2,1,0,-1,-2,-4,-8]; var sizeNum = sizeTable[sizes.indexOf(typeArray[1])]; AddAttribute("size",sizeNum,charID); // concatenate type and subtype to put into the text box var bothTypes= typeArray[2].concat(" (" , subType , ")"); AddAttribute("npc-type",bothTypes,charID); //*****ATTRIBUTE SCORES*************** //find the element in the data array that the title "Statistics" occurs in var statsElementNumber = data.indexOf("STATISTICS"); //the actual attribute scores are in the element after the heading var stats = data[statsElementNumber+1]; stats = stats.split(","); //assign attribute scores by removing non numerical characters from the stats array elements var strength = stats[0].replace(/\D/g,""); var dexterity = stats[1].replace(/\D/g,""); var constitution = stats[2].replace(/\D/g,""); var intelligence = stats[3].replace(/\D/g,""); var wisdom = stats[4].replace(/\D/g,""); var charisma = stats[5].replace(/\D/g,""); // define attribute modifiers used in other sections var strMod = getAbilityMod(strength); var dexMod = getAbilityMod(dexterity); var conMod = getAbilityMod(constitution); var intMod = getAbilityMod(intelligence); var wisMod = getAbilityMod(wisdom); var chaMod = getAbilityMod(charisma); // place attribute scores in NPC sheet AddAttribute("STR-base",strength,charID); AddAttribute("DEX-base",dexterity,charID); AddAttribute("CON-base",constitution,charID); AddAttribute("INT-base",intelligence,charID); AddAttribute("WIS-base",wisdom,charID); AddAttribute("CHA-base",charisma,charID); //find and store initiative bonus, Senses var initArray = findString(data, "Init", true); initArray = initArray.split(","); var initiative = initArray[0]; initiative = initiative.replace(/\D/g,""); var initBonus = initiative - dexMod; AddAttribute("init-misc",initBonus,charID); initArray.splice(0,1); initArray[0] = initArray[0].replace(" Senses ",""); initArray.toString(); AddAttribute("npc-senses",initArray,charID); //************ Auras **************** var aurasLine = findString(data, "Aura", true); if (aurasLine != null) { aurasLine = aurasLine.replace("Aura ",""); AddAttribute("npc-aura",aurasLine,charID); } //*****AC Breakdown************** var acLine = findString(data, "AC ", true); var acBreakdown = getSubStr(acLine,"(",")" ); acBreakdown = acBreakdown.slice(1); acBreakdown = stripString(acBreakdown,"-","+") //if there is a size or dex penalty with -, change to + for time being, remove later var acSeparate = acBreakdown.split("+"); var acNumOnly = acBreakdown.split("+"); acNumOnly = removeNonNumericFromArray(acNumOnly); var acSeparateNames = removeNumbersFromArray (acSeparate); var armorIndex = acSeparateNames.indexOf("armor"); var shieldIndex = acSeparateNames.indexOf("shield"); var deflectIndex = acSeparateNames.indexOf("deflection"); var dodgeIndex = acSeparateNames.indexOf("dodge"); var naturalIndex = acSeparateNames.indexOf("natural"); var acSizeIndex = acSeparateNames.indexOf("size"); //If the search found that armour in the breakdown, put that value in the //NPC sheet and add the values used to acFromNamed in order to determine // the total miscellenous armour bonus var acFromNamed = 0 if (armorIndex != -1) { AddAttribute("armor-acbonus",acNumOnly[armorIndex],charID); acFromNamed = acFromNamed + acNumOnly[armorIndex]; } if (shieldIndex != -1) { AddAttribute("shield-acbonus",acNumOnly[shieldIndex],charID); acFromNamed = acFromNamed + acNumOnly[shieldIndex]; } if (deflectIndex != -1) { AddAttribute("AC-deflect",acNumOnly[deflectIndex],charID); acFromNamed = acFromNamed + acNumOnly[deflectIndex]; } if (dodgeIndex != -1) { AddAttribute("AC-dodge",acNumOnly[dodgeIndex],charID); acFromNamed = acFromNamed + acNumOnly[dodgeIndex]; } if (naturalIndex != -1) { AddAttribute("AC-natural",acNumOnly[naturalIndex],charID); acFromNamed = acFromNamed + acNumOnly[naturalIndex]; } var natural = acNumOnly[naturalIndex]; //puts any other AC bonuses than the named into MISC var ac = 0; if (sizeNum >= 0) { ac = sumArray(acNumOnly)+10; // if creature has + size AC bonus then simple } else { ac = sumArray(acNumOnly)+10 + 2*sizeNum; // if creature has - size AC bonus then the array summed incorrectly (added instead of subtracted) // correct by subtracting double } if (dexMod <= 0){ ac = ac + dexMod*2; //correct for negative dex bonuses } // every other type of AC bonus than those named reports to misc var miscAC = ac - (10 + acFromNamed +sizeNum + dexMod); AddAttribute("AC-misc",miscAC,charID); //**************** Health ************************ var hpArray = findString(data, "hp ", true); hpArray = stripStringRegEx(hpArray, "(", ")"); hpArray = hpArray.split(" "); var HP = hpArray[1]; AddAttribute("NPC-HP",HP,charID); AddAttribute("npc-hd-misc",HP,charID); //**************** Saves ************************ var savesLine = findString(data, "Fort ", true); var savesArray = savesLine.split(","); savesNum = removeNonNumericFromArray(savesArray); var fortitude = savesNum[0]; var reflex = savesNum[1]; var willpower = savesNum[2]; var savesArrayExtra = savesLine.split(","); savesArrayExtra.splice(0,3); var fortBonus = fortitude - conMod; var refBonus = reflex - dexMod; var willBonus = willpower - wisMod; AddAttribute("Fort-misc",fortBonus,charID); AddAttribute("Ref-misc",refBonus,charID); AddAttribute("Will-misc",willBonus,charID); AddAttribute("Save-notes",savesArrayExtra,charID); //************ Defensive Abilities **************** var defenseLine = findString(data, "Defensive Abilities", true); if (defenseLine != null) { defenseLine = defenseLine.replace("Defensive Abilities ",""); AddAttribute("npc-defensive-abilities",defenseLine,charID); } //**************** Weaknesses ******************* var weakLine = findString(data, "Weaknesses ", true); if (weakLine != null) { var weaknesses = weakLine.replace("Weaknesses ",""); AddAttribute("weaknesses",weaknesses,charID); } //************ Damage Resistance **************** var drLine = findString(data, "DR ", true); if (drLine != null) { var damageResist = drLine.match(/DR (\d+\/\w+)/); AddAttribute("DR",damageResist[1],charID); } //************ Immunities ********************** var immuneLine = findString(data, "Immune ", true); if (immuneLine != null) { immuneLine = immuneLine.replace(/SR .*/,""); var immune = immuneLine.match(/Immune (.*);*?/) AddAttribute("immunities",immune[1],charID); } //************ Spell resistance **************** var srLine = findString(data, "SR ", true); if (srLine != null) { var sr = srLine.match(/SR (\d+)/) AddAttribute("SR",sr[1],charID); } //*************** Speed *********************** var speedStr = findString(data, "Speed ", true); if (speedStr != null) { //make two arrays, one with values and the other with speed types speedStr = speedStr.replace("Speed ",""); var maneuver = getSubStr(speedStr, "(", ")"); var speedArray = stripStringRegEx(speedStr, "(", ")"); speedArray = speedArray.replace(/ft./g,""); speedArray = speedArray.replace(/ /g,""); var speedNums = speedArray.split(","); var speedTypes = speedArray.split(","); speedNums = removeNonNumericFromArray (speedNums); speedTypes = removeNumbersFromArray (speedTypes); //determine the index of each speed type (-1 if type not found) var burrowSpeedIndex = speedTypes.indexOf("burrow"); var climbSpeedIndex = speedTypes.indexOf("climb"); var flySpeedIndex = speedTypes.indexOf("fly"); var swimSpeedIndex = speedTypes.indexOf("swim"); AddAttribute("speed-base",speedNums[0],charID); if (burrowSpeedIndex != -1) { AddAttribute("speed-burrow",speedNums[burrowSpeedIndex],charID); } if (climbSpeedIndex != -1) { AddAttribute("speed-climb",speedNums[climbSpeedIndex],charID); } if (flySpeedIndex != -1) { AddAttribute("speed-fly",speedNums[flySpeedIndex],charID); AddAttribute("speed-fly-maneuverability",maneuver,charID); } if (swimSpeedIndex != -1) { AddAttribute("speed-swim",speedNums[swimSpeedIndex],charID); } } //*********** Space, Reach & Reach Notes ********** // find line containing "Space" var space = "", reach = ""; var reachLine = findString(data, "Space ", true); if (reachLine != null) { //get subtype before destroying string var reachNotes = getSubStr(reachLine, "(", ")"); var reachArray = stripStringRegEx(reachLine, "(", ")"); var reachNums = reachArray.split(","); reachNums = removeNonNumericFromArray (reachNums); space = reachNums[0]; reach = reachNums[1]; AddAttribute("reach-notes",reachNotes,charID); } else { space = 5 reach = 5 } AddAttribute("space",space,charID); AddAttribute("reach",reach,charID); //*********** BASE ATTACK BONUS ************** var babArray = findString(data, "Base Atk", true); babArray = babArray.split(","); var babNum = removeNonNumericFromArray(babArray); AddAttribute("class-0-bab",babNum[0],charID); //************ MELEE ATTACK ******************* //********************************************* // ParseAttack syntax: // ParseAttack(text to search, string to look for, attack bonus, damage bonus, reach, repeat number, charID) var numMeleeAttacks = parseAttack(data,"Melee", (babNum[0] + strMod), strMod, reach, 0, charID); //************ RANGED ATTACK ******************* //********************************************** var numRangedAttacks = parseAttack(data,"Ranged",(babNum[0] + dexMod),0, reach,numMeleeAttacks,charID); //*********** Special Attacks **************** var specAtks = findString(data, "Special Attacks", true); if (specAtks != null) { specAtks = specAtks.replace("Special Attacks ",""); AddAttribute("npc-special-attacks",specAtks,charID); } //*********** Before and During Combat ************ var beforeCombat = findString(data, "Before Combat", true); if (duringCombat != null) { beforeCombat = beforeCombat.replace("Before Combat ",""); AddAttribute("npc-before-combat",beforeCombat,charID); } var duringCombat = findString(data, "During Combat", true); if (duringCombat != null) { duringCombat = duringCombat.replace("During Combat ",""); AddAttribute("npc-during-combat",duringCombat,charID); } //**************** FEATS ********************** var feats = findString(data, "Feats ", true); if (feats!= null){ feats = feats.replace("Feats ",""); feats = feats.trim(); feats = feats.split(","); for (i=0; i<feats.length ; i++) { AddAttribute("repeating_feat_"+i+"_name",feats[i],charID); } } //****************SKILLS************************ var skillsLine = findString(data, "Skills", true); if (skillsLine != null) { skillsLine = skillsLine.replace("Skills ",""); skillsLine = stripString(skillsLine, " +", ""); var skillsBrackets = getSubStr(skillsLine,"(", ")"); var skillsNoBrackets = stripStringRegEx(skillsLine,"(", ")"); var skillsTotal = skillsNoBrackets.split(",") var skillsName = skillsNoBrackets.split(",") skillsTotal = removeNonNumericFromArray (skillsTotal); skillsName = removeNumbersFromArray (skillsName); //make the arrays that are used to find out the relevant skill attribute var skillsAll = "Acrobatics,Appraise,Bluff,Climb,Craft,Diplomacy,Disable Device,Disguise,Escape Artist,Fly,Handle Animal,Heal,Intimidate,Knowledge (Arcana),Knowledge (Dungeoneering),Knowledge (Engineering),Knowledge (Geography),Knowledge (History),Knowledge (Local),Knowledge (Nature),Knowledge (Nobility),Knowledge (Planes),Knowledge (Religion),Linguistics,Perception,Perform,Profession,Ride,Sense Motive,Sleight of Hand,Spellcraft,Stealth,Survival,Swim,Use Magic Device"; skillsAll = skillsAll.split(","); var modsAll = "dex,int,cha,str,int,cha,dex,cha,dex,dex,cha,wis,cha,int,int,int,int,int,int,int,int,int,int,int,wis,cha,wis,dex,wis,dex,int,dex,wis,str,cha"; modsAll = modsAll.split(","); //go through each skill, determine its total score, then determine the total ranks using the relevant modifer. for (var i = 0; i < skillsTotal.length; i++) { var nameStr = "" if (skillsName[i] != "Craft" && skillsName[i] != "Knowledge" && skillsName[i] != "Perform" && skillsName[i] != "Profession") { var skillAtr = modsAll[skillsAll.indexOf(skillsName[i])]; //look up the corresponding attribute for the current skill var mod = 0; switch (skillAtr) { case "str": mod = strMod; break; case "dex": mod = dexMod; break; case "con": mod = conMod; break; case "int": mod = intMod; break; case "wis": mod = wisMod; break; case "cha": mod = chaMod; break; } var skillRank = skillsTotal[i] - mod; nameStr = stripString(skillsName[i], " ", "-"); var fullNameStr = nameStr.concat("-misc"); //output skill to char sheet AddAttribute(fullNameStr,skillRank,charID); } } } //********** LANGUAGES, SQ, GEAR***************** var languageStr = findString(data, "Languages", true); if (languageStr != null) { languageStr = languageStr.replace("Languages ",""); AddAttribute("languages",languageStr,charID); } var sqStr = findString(data, "SQ ", true); if (sqStr != null) { sqStr = sqStr.replace("SQ ","") AddAttribute("SQ",sqStr,charID); } var gearStr = findString(data, "Combat Gear", true); if (gearStr != null) { gearStr = gearStr.replace("Combat Gear ",""); gearStr = gearStr.split("Other Gear"); AddAttribute("npc-combat-gear",gearStr[0],charID); AddAttribute("npc-other-gear",gearStr[1],charID); } //**************** sets Token Name, Health, linked AC ****************** token.set("name", tokenName||''); token.set("showname", true); token.set("bar3_value", HP||0); token.set("bar3_max", HP||0); token.set("bar2_value", ac||0); token.set("showplayers_bar3", true); token.set("status_blue",true); } });
1421564965

Edited 1421565285
vÍnce
Pro
Sheet Author
Hi Jason. Curious to try this out, but I'm having an issue getting it to do anything...? Do you need to link a token to a sheet prior to running the script, and do you need to use something like @{target|token_id}, or @{selected|token_id} to let the script know which token to parse? Thanks Nevermind . I just realized I was testing this on a custom (powercarded) PF sheet... :-)
oh my bad, you have to have the token with the statblock in GMnotes selected, and the command to start is !PathfinderImport The macro creates a new character sheet if there isn't another by that name.
1421565498
vÍnce
Pro
Sheet Author
Jason P. said: oh my bad, you have to have the token with the statblock in GMnotes selected, and the command to start is !PathfinderImport The macro creates a new character sheet if there isn't another by that name. Yep. It works. I like that. It saves me a little time going thru and adding a bunch of the nitty gritty statblock stuff. I have another script that generates my powercarded attacks. I think I might be using this one. Thank you for creating this and sharing Jason.
1421565635

Edited 1421565842
vÍnce
Pro
Sheet Author
I bet it would be a pain to have it try and parse the attacks into the repeatable weapon attacks of the NPC section. If at all possible... Observation, this script actually doesn't matter if you are using a "modified" PF sheet as long as the NPC related attributes are the same.
1421566295
vÍnce
Pro
Sheet Author
I noticed that it outputs the entire statblock into the console window. Is that normal/necessary? ["Orc CR 1/3","XP 135","Orc warrior 1","CE Medium humanoid","Init +0, Senses darkvision 60 ft., Perception -1","Weakness light sensitivity","DEFENSE","AC 13, touch 10, flat-footed 13 (+3 armor)","hp 6 (1d10+1)","Fort +3, Ref +0, Will -1","Defensive Abilities ferocity","OFFENSE","Speed 30 ft.","Melee falchion +5 (2d4+4/18-20)","Ranged javelin +1 (1d6+3)","TACTICS","Before Combat Orcs make few preparations before combat, preferring to charge headlong at any foe that presents itself.","During Combat Orcs prefer to use two-handed weapons to maximize the effectiveness of their great strength. They attack in ambushes from concealment to take an enemy off-guard and cause as much fear and confusion as possible.","Morale Orcs are bullies and cowards. They flee when the odds have turned against them and any nearby leaders are dead014 or have already fled. They are prone to surrender and truces if such actions save their skins, although they honor such terms only as long as it is to their benefit to do so. Exceptions to this are dwarves and elves, from whom they neither ask nor give quarter.","STATISTICS","Str 17, Dex 11, Con 12, Int 7, Wis 8, Cha 6","Base Atk +1, CMB +4, CMD 14","Feats Weapon Focus (falchion)","Skills Intimidate +2","Languages Common, Orc","SQ weapon familiarity","SPECIAL ABILITIES","Ferocity (Ex)","An orc remains conscious and can continue fighting even if its hit point total is below 0. It is still staggered and loses 1 hit point each round. A creature with ferocity still dies when its hit point total reaches a negative amount equal to its Constitution score.",""]
Ahhh i was using that to debug, i'll comment it out when i work out this github thing
1421585015

Edited 1421587254
Hey, I love this - or the idea of it - but I can't get it to do anything. It's also my first use of script (you're partially to thank that I got a subscription today), so I'd appreciate some help. I'm getting this: TypeError: Cannot call method 'replace' of undefined at Sandbox.<anonymous> (evalmachine.<anonymous>:410:30) at eval ( Edit: Did get it to work eventually. Didn't read carefully enough and had the token linked with a sheet or something... working now though! Amazing!
1421651298
vÍnce
Pro
Sheet Author
Jason P. said: No idea about the GitHub thing, however: *** Edit V1.02 - Minor changes to how AC is calculated, tokens are now populated with HP showing as a bar on bar 3 which players can see, AC as number on bar 2 which they can't. Token name added and displayed. //**************** sets Token Name, Health, linked AC ****************** token.set("name", tokenName); token.set("showname", true); token.set("bar3_value", HP); token.set("bar3_max", HP); token.set("bar2_value", ac); token.set("showplayers_bar3", true); Thanks for adding these. Although I use AC on bar1 and I don't show the hp bar since I use another script that tracks/shows health changes to a token, it was easy to make personal changes to your script. Thank you.
1421680232

Edited 1421681225
Error: Firebase.set failed: First argument contains undefined in property 'current' at Error ( ) at Aa (/home/symbly/www/d20-api-server/node_modules/firebase/lib/firebase-node.js:9:49) at Aa (/home/symbly/www/d20-api-server/node_modules/firebase/lib/firebase-node.js:10:196) at za (/home/symbly/www/d20-api-server/node_modules/firebase/lib/firebase-node.js:8:468) at G.W.set (/home/symbly/www/d20-api-server/node_modules/firebase/lib/firebase-node.js:128:98) at createObj ( It doesn't seem to work for me for this one: Is this working for you? <a href="http://www.d20pfsrd.com/bestiary/monster-listings/animals/rat/dire-rat" rel="nofollow">http://www.d20pfsrd.com/bestiary/monster-listings/animals/rat/dire-rat</a>
1421684039
The Aaron
Pro
API Scripter
You could guard against that error by doing this: //**************** sets Token Name, Health, linked AC ****************** token.set("name", tokenName ||'' ); token.set("showname", true); token.set("bar3_value", HP ||0 ); token.set("bar3_max", HP ||0 ); token.set("bar2_value", ac ||0 ); token.set("showplayers_bar3", true); The || will pick the second argument if the first is undefined (or falsey in general).
excellent! the lessons begin :-)
1421707790

Edited 1421707826
Did some testing with it. Weird formats (as the case with the dire rat, but not the common rat on pfsrd) throw it off -- but also after Racial modifiers "uses Dex to modify Climb and Swim " throws it off completely! kind of interesting no? just errors it. - of course without letting you know that it's deactivated the script by doing so - in roll20 no error is visible
awesome, if you let me know all of the mobs it throws errors for i'll have a look some time this week. In the mean time you should be able to work around it by doing some pre-import cleanup of the stat blocks, should still be saving you time hopefully .
1421740970

Edited 1421761255
sure! - though I must say it was quite hard to find out what exactly was causing the trouble. I ended up using small rat statblock and replacing it bit by bit with dire rat until it made an error. Happened to get lucky and got that part of the block in quite soon. The formatting stuff will be much harder to fix, I think. But I'm saying that knowing next to nothing of java. - it was putting it in boxes like tables (they must have done that so the add gets displayed nicely on their website) -- in the moment I copy everything in a notepad document to kill formatting. Is there a function that can do that? Edit: just found this one: <a href="https://www.youtube.com/watch?v=48O8FyPTp64" rel="nofollow">https://www.youtube.com/watch?v=48O8FyPTp64</a> I'll give that a try too. Edit: I'm back. Seems to do better on not causing errors (it even managed the dire rat with the terrible formatting - but it doesn't do the new pathfinder sheets, which I like. So maybe we can find something in its code to help yours? Edit2: Tried looking into it, I'm not good at reading code yet.
Ok, so i added in a statement to the script in V1.04 that should catch "undefined" values attempting to be written to the char sheet, this was causing the majority of errors i had and is usually caused by duplicates. It should spit out a message now telling you what attribute to check in the stat block. As for the Dire Rat, i believe that was happening because "Climb" and "Swim" appeared twice in the skills list, not quite sure how to account for things like that unless they are always separated by ";".... Also some notes on bug hunting! the script uses a number of searches for text strings to work out which line of the stat block to manipulate to get the various attributes- so the following is a list of what it looks for: Name/ CR : Uses first line, splits into two strings divided by "CR" XP : Uses 2nd line, splits by space Size, Alignment, Type : Searches for a line that contains "Medium" or another size (responsible for the most errors! Will cause a problem if it finds a size on the wrong line - particularly in special abilities - eg, "A cloud giant can wield Gargantuan weapons without penalty" will cause an issue. Alter the string to "wield Garg. " or similar to work around. Attribute Scores (Str etc) : Uses the line underneath "STATISTICS", separates by ",". Initiative bonus, Senses : Searches for "Init " (note the space) Auras: Searches for "Aura" (prone to duplicates but won't cause failure) AC: Searches for "AC " Health : Searches for "hp ", separates by space Saves: Searches for "Fort " (thought this would cause issues but haven't had any yet) Defensive abilities : Searches for "Defensive Abilities" Movement speeds: Searches for "Speed ". Stores and removes anything in brackets as maneuverability, separates rest by "," and relates to each type by words, "burrow", "climb", "fly" and "swim" Space and Reach notes: Searches for "Space ", separates notes by brackets. Special attacks : Searches for "Special Attacks" Before Combat : searches for "Before Combat" During Combat: Searches for "During Combat" Base Attack Bonus: Searches for "Base Atk" Skills: Searches for "Skills", removes anything in brackets, separates by "," stores each skill in order of appearance by its name. Languages: Searches for "Languages" Special Qualities: Searches for "SQ" Gear: Searches for "Combat Gear" Give the updated script a try, and the above should help with troubleshooting. Also if any guru's read this and can think of suggestions for alternatives to the more error prone logic, i'm all ears!
I have a creature generator for pathfinder, but i'm currently fixing some edge cases here and there. Julix, that's Peter W. 's monster parser, the original author which Jason gets a few functions from (in fact my code uses 2 of his functions). Peter W. definitely deserves major props for figuring out how to parse the PRD stat blocks. I didn't use much of his logic in my generator, but many of his ideas and approaches saved a lot of time that would have been spent spinning on a chair thinking "but how do I handle x format case?" I'll release it maybe in a week or two, maybe sooner when I think it handles most cases. Currently it can parse d20pfsrd and PRD stat blocks without issues. I've been adding automatic spell linking so you could press a button for 1st level spells to get a spellbook link list of what spells that creature has; same with SLAs and things. Also due to the new roll-templates I'm trying my hand at adding those as well. What it doesn't do and what Jason's does, is fill into a character sheet. @Jason, be careful about the name as CRYSTAL DRAGON would end up with a blank name. It's another one of those edge cases that popped up when I was still using Peter W.'s script.
Mate, I apologise, i didn't mean to step on anyone's toes. Peter W's CleanUpString, stripStringRegEx, findString and getSubStr functions do the majority of the heavy lifting in my script, and without them i wouldn't have had a clue. Like i said before Friday i had never even touched Javascript so the learning curve was pretty steep. To clarify, is it a faux pas to use someone elses functions, or is there some more formal way of recognition? Again, my apologies, after just recently upgrading to mentor i was keen to contribute something. I wanted to cut my prep time for campaigns and figured others might benefit from the share.
Nah, it's no biggie, usually credits in the script which you did. I just highlighted Peter's name since he pretty much was the backbone for both of our scripts.
1421818840
vÍnce
Pro
Sheet Author
Props to anyone and everyone that has contributed to this. There's such a plethora of srd out resources out there. Having a parser to import ANY of it is a time saver. Much appreciated.
What did I Ken L. said: be careful about the name as CRYSTAL DRAGON would end up with a blank name. Why is that? What's special about the name? the all-caps?
1421859476
The Aaron
Pro
API Scripter
Jason P. said: Also if any guru's read this and can think of suggestions for alternatives to the more error prone logic, i'm all ears! Here are a few simplifications (As a bonus, stripStringRegEx actually uses regular expressions now. =D It also works for strings instead of just characters.): var RegExpEscapeSpecial =/([\/\\\/\[\]\(\)\{\}\?\+\*\|\.\^\$])/g; function stripString(str, removeStr, replaceWith) { var r= new RegExp(removeStr.replace(RegExpEscapeSpecial,"\\$1"),'g'); return str.replace(r,replacewith); } /* Deletes any characters between the character a and b in incstr */ function stripStringRegEx(incstr, a, b) { var ea = a.replace(RegExpEscapeSpecial,"\\$1"), eb = b.replace(RegExpEscapeSpecial,"\\$1"), r = new RegExp( ea+'.*?'+eb , 'g'); return incstr.replace(r,''); } /* returns the string between two characters a/b */ function getSubStr(str, a, b) { var ea = a.replace(RegExpEscapeSpecial,"\\$1"), eb = b.replace(RegExpEscapeSpecial,"\\$1"), r = new RegExp( ea+'(.*?)'+eb), m = str.match(r); return m && m[1]; } RegExpEscapeSpecial is a regular expression to match all the characters that need to be escaped in a regular expression. It's used in each of the above 3 rewritten functions. Using a regular expression to do the replace lets you do it in a single command instead of looping. While there is technically a loop somewhere, it will be in the javascript virtual machine, and thus more efficient than hand coding one. It's also likely to be less error prone than dealing directly with indexes; if there were bugs in it, someone already found them. =D .* is the match for anything of any length, adding the ? to the end of it makes it the lazy match, and thus it won't match the b part if more than one b exists. Adding ( and ) around something in a regular expression saves the contents, so getSubStr() is really the same as stripStringRegEx() but with an opposite operation. If the string is found, it will return an array containing the fully matched string at 0, then each of the saved substrings at 1, 2, etc.. The first matching substring is the part we're interested in, so m && m[1] checks if m is defined and if it is, returns the value of m[1] . Also this one. The original was a bit hard to understand, but this duplicates it somewhat more efficiently (added comment about doConcat): //looks for an occurrence of str in the array strArray, if found returns that element // on doConcat, strips a trailing "and" and concatenates with the next line. function findString(strArray, str, doConcat) { var retr, r = new RegExp(str.replace(RegExpEscapeSpecial,"\\$1")); _.find(strArray,function(v,k,l){ if(v.match(r)){ retr = v; if(doConcat && v.match(/and$/) && l[k+1]) { retr=retr.replace(/and$/,'')+', '+l[k+1]; } return true; } return false; }); return retr; }; I'm not completely sure that this behaves identically to the original, but it seemed to for each of the test strings I tried. _.find() will execute a function for each of elements of an array, and return the element that the function first returns true for. Here I'm using it more for the side effect of minimal traversal of an array. The function that you pass to _.find() is passed 3 arguments: The value of the element, the index of the element, the full array. Often, you only care about the first argument, but in this case I'm using all 3. I build a regular expression to do the match using the same escaping method as above. The first element that matches will cause true to be returned and traversal stops. The value of that element is stored in the variable retr (which is defined in the closure that _.find() is being called in). If doConcat is true, it will additionally check if the element ends in and ($ means end of line), and make sure there is a next element. If that is the case, it will remove the and and append a comma and the next element. Underscore.js provides all the functions that are off of the _ special variable. They have some serious time savers in there. Math stuff. Summing arrays is pretty much the poster child example for reduce, this makes it a bit more fault tolerant.: function sumArray(numArray) { return _.reduce(numArray,function(acc,n){ return acc + ( parseInt(n,10) || 0 ); }, 0); } function getAbilityMod(ability) { return Math.floor((ability-10)/2); } The parseInt prevents this problem with the original: #&gt; sumArray(["1","2","3","4"]); "01234" _.reduce() takes an array and then passes each of it's elements to a function, along with a base state. The base state above is the 0. The function receives the same arguments as the one for _.find() above, except for one extra on the beginning, which is the current state (referred to often as the memo ). Each execution of the function must return the new state for the next exectuion Summation is simply returning the current state plus the value of the current element. parseInt() converts a string to a number (specifically, it returns the first sequence of digits it finds as a number). The ten tells it what base to convert from, so you could use 16 if you were converting hexadecimal, such as for html colors). If the argument to parseInt is a number already, you just get that number back. || 0 substitutes 0 if parseInt can't find a number in the input. for getAbilityMod(), I just nested all the calls and returned the result, no need to create a variable there. Map is great for transforming arrays (\d matches a digit, + means 1 or more. \D is the inverse of \d, so not digits (which your code already used. =D)): //removes numbers from array and trims white space on ends of elements function removeNumbersFromArray (strArray) { return _.map(strArray,function(s){ return s.replace(/\d+/g,'').trim(); }); } function removeNonNumericFromArray (strArray) { return _.map(strArray,function(s){ return s.replace(/\D+/g,''); }); } _.map() takes a function with the same arguments as the function for _.find(). The result of calling _.map() on an array is an array of the result of calling the function on each element of the array. cleanUpString() could be cleaned up quite a bit, but I'll tackle that another day. =D
Aaron you're a legend! I'm excited to implement these :-D going to get stuck in as soon as this coffee kicks in. I really appreciate you taking the time mate, improves my very basic knowledge by leaps and bounds. also Julix Why is that? What's special about the name? the all-caps? This is because the string.split() method is targeting "CR" to split by, and will return anything on either side of a CR as separate parts of an array, which i then call by the number they occur in using stringArray[0] etc so the first array element of string.split("CR") of CRYSTAL DRAGON CRXX would be nothing. I generally clean up the names to be something like Crystal Dragon CRXX though so that helps. (look for something like: var stringArray = string.split("CR") to find an example in the code)
Ohhhhhhhhhhhh <a href="http://www.regular-expressions.info/characters.html" rel="nofollow">http://www.regular-expressions.info/characters.html</a> *Penny drops* Its beautiful..... they should have sent a poet...
.* is the match for anything of any length, adding the ? to the end of it makes it the lazy match, and thus it won't match the b part if more than one b exists. Aaron does this mean it would deal with a string like "Melee attack (first set of brackets) ranged attack (second set of brackets)" if you were attempting to remove both sets of brackets?
1421868438

Edited 1421869225
The Aaron
Pro
API Scripter
Correct. /\(.*\)/ would cause your string to become "Melee attack " because * alone is a greedy match, matching everything it can until it can match no more. /\(.*?\)/ would individually match each parenthesized element because the ? turns the * into a lazy match, causing it to match the minimum number of characters before returning. Your string then becomes "Melee attack ranged attack ". For your CRYSTAL DRAGON, you could extract the CR with /CR(\d+)/, which would match CR followed by 1 or more digits. /CR\s*(\d+)/ would allow 0 or more whitespace between the CR and the digits. ( and ) allow you to easily get the number #&gt; "CRYSTAL DRAGON CR12".match(/CR(\d+)/) ["CR12", "12"] #&gt; "CRYSTAL DRAGON CR12".match(/CR\s*(\d+)/) ["CR12", "12"] #&gt; "CRYSTAL DRAGON CR 12".match(/CR\s*(\d+)/) ["CR 12", "12"]
1421868753
The Aaron
Pro
API Scripter
BTW, if you're looking to expand your javascript ability, I highly recommend Javascript: The Good Parts by Douglas Crockford . It's full of great information on writing good Javascript. I think it's a fast easy read, but YMMV. Suffice it to say, if you grok that book, you're JS will improve. =D
Thats perfect! i didn't realise match() worked like that! You've given me so many ideas haha, wonder how long i can go without sleep....
1421871405

Edited 1421873111
The Aaron
Pro
API Scripter
Yup! The implementation of getSubStr() I posted above uses that feature of match() to pull things out. Depending on what you're doing, you can get some pretty complicated and interesting data: #&gt; "Str10 Dex12 Con15 Int9 Wis20 CHA5".match(/\b(str|dex|con|int|wis|ch[ar])\s*(\d+)/ig) ["Str10", "Dex12", "Con15", "Int9", "Wis20", "CHA5"] #&gt; " Int9 Wis20 Str10 Dex12 Con15 CHA5".match(/\b(str|dex|con|int|wis|ch[ar])\s*(\d+)/ig) ["Int9", "Wis20", "Str10", "Dex12", "Con15", "CHA5"] #&gt; "All Int9 these Wis 20 tacos Str10 are Dex12 making me Con15 hungry CHA5!".match(/\b(str|dex|con|int|wis|ch[ar])\s*(\d+)/ig) ["Int9", "Wis 20", "Str10", "Dex12", "Con15", "CHA5"] #&gt; _.reduce("All Int9 these Wis 20 tacos Str10 are Dex12 making me Con15 hungry CHA5!".match(/\b(str|dex|con|int|wis|ch[ar])\s*(\d+)/ig), function(m,a){ var parts = a.match(/(\D+)(\d+)/), stat = parts[1].toUpperCase(), val = parseInt(parts[2],10) || 0; stat = ('CHA' === stat ? 'CHR' : stat); m[stat]=val; return m; },{}); { CHR: 5, WIS: 20, INT: 9, STR: 10, DEX: 12, CON: 15 } =D
so with the var RegExpEscapeSpecial =/([\/\\\/\[\]\(\)\{\}\?\+\*\|\.\^\$])/g; function stripString(str, removeStr, replaceWith) { var r= new RegExp(removeStr.replace(RegExpEscapeSpecial,"\\$1"),'g'); return str.replace(r,replacewith); } I think i understand that its replacing any of the characters in RegExpEscapeSpecial that appear removeStr, but i'm confused as to the effect replacing them has on the final return. can you explain the "\\$1" and the 'g' ?
1421876150
vÍnce
Pro
Sheet Author
Is there any API magic that could be added to this script in order to create and add information to a repeating section of the sheet? Attacks, Spell-Like Abilities, Feats...? Maybe if the NPC section already included the repeatable items first so there would be a field/attriute to populate?
1421876410
vÍnce
Pro
Sheet Author
How about rolling HP as well? I know Aaron has a little code that does this. :-) Is there a way to use a macro or whatever within a journal character to give random hit points? I know... I'm just a lazy DM is all.
Vince i've been thinking about adding the attacks in, i'll slowly tinker until i get something that works, but it might take a week or so taking into account the time i'll spend experimenting with the revelations provided by Aaron haha Also what would you actually want from the spell-like abilities and Feats? just a list? Or an ability that rolls specifics?
1421883431
The Aaron
Pro
API Scripter
Jason P. said: so with the var RegExpEscapeSpecial =/([\/\\\/\[\]\(\)\{\}\?\+\*\|\.\^\$])/g; function stripString(str, removeStr, replaceWith) { var r= new RegExp(removeStr.replace(RegExpEscapeSpecial,"\\$1"),'g'); return str.replace(r,replacewith); } I think i understand that its replacing any of the characters in RegExpEscapeSpecial that appear removeStr, but i'm confused as to the effect replacing them has on the final return. can you explain the "\\$1" and the 'g' ? So, when you write a literal regular expression, you'd write it like this: /bananas/ But if you needed to say, match a price, you might do this: /bananas \$(\d+\.\d?\d?)/ Which gives you this: "bananas $2.23".match(/bananas \$(\d+\.\d?\d?)/) ["bananas $2.23", "2.23"] You must escape the $ , because $ means end of line. Adding the escapes is somewhat obvious when you are writing a literal regular expression. However, when you are building one dynamically, as in that stripString() function, you don't know what the string contains. The RegExp() constructor converts a string to a regular expression, so the contents must be escaped to prevent accidentally matching the wrong thing. The RegExpEscapeSpecial is really just /([.])/g , but where . is a list of all the characters that must be escaped, which are then all escaped. =) The ( and ) save the match, and the g says to apply it multiple times. "\\$1" is the string that each matching element will be replaced by. \\ is the escape code in a javascript (and most c-like languages) for a backslash character, \ . For example, if you just wrote "\now", you'd get a carriage return character (0x13) followed by "ow". replace() supports a bunch of interesting embedded parameters. $1 is is replaced by the first saved sequence in the regular expression. $2 is the second one, etc. (Similar to \\ for a \, if you want a literal $, you have to put $$.) The practical upshot is that it replaces any of the characters that need to be escaped with the escape sequence for them. So, removeStr.replace(RegExpEscapeSpecial,"\\$1") is the first argument to the RegExp() constructor, 'g' is the second argument. It serves the same purpose as the trailing g on a literal regular expression, it causes the returned regular expression to match multiple instances, thus letting str.replace() replace every occurrence. Probably I should rewrite that so that the removeStr.replace(RegExpEscapeSpecial,"\\$1") is in a function called by each instead of how I have it. I'll revise later tonight. =D
1421883466
vÍnce
Pro
Sheet Author
Jason P. said: Vince i've been thinking about adding the attacks in, i'll slowly tinker until i get something that works, but it might take a week or so taking into account the time i'll spend experimenting with the revelations provided by Aaron haha Also what would you actually want from the spell-like abilities and Feats? just a list? Or an ability that rolls specifics? Just creating the repeatable item and inserting the name(not really any other info in the stat-block for these...) would be nice. I understand that this may not be doable since repeatable items actually don't exit until created... (thats sounds profound) Anyhow, it would be nice for the script to make the entry which is one less step for the DM. I can decide if I want/need to add some more description.
Hey Aaron, does the new stripStringRegEx work with arrays? getting a " has no method 'replace' at stripStringRegEx" when it runs, full error is: TypeError: Object Valeros CR12,XP 19,200,Human fighter 12,NG Medium humanoid (human),Init +8, Senses Perception +0,DEFENSE,AC 29, touch 17, flat-footed 24 (+9 armor +2 deflection +4 Dex +1 dodge +2 natural +1 shield),hp 130 (12d10+60),Fort +14, Ref +11, Will +7, +3 vs. fear,Defensive Abilities bravery +3, 25 chance to negate critical hits and sneak attacks,OFFENSE,Speed 30 ft.,Melee +2 keen longsword +21/+16/+11 (1d8+13/17-20), +2 short sword +18/+13 (1d6+8/19-20) or +2 keen longsword +23/+18/+13 (1d8+13/17-20),Ranged +1 shortbow +17/+12/+7 (1d6+1/3),Special Attacks weapon training (heavy blades +2 light blades +1),TACTICS,During Combat Valeros activates his boots of speed, using the extra movement to get into a position from which he can take a full attack action in the following round, and maintaining the effects of haste as long as he can continue to benefit from an additional attack each round. When hes unable to make more than one attack, he prefers his longsword and utilizes Vital Strike to increase his damage output. Unless he absolutely must wield a weapon in two hands to gain the additional damage potential, he wields both swords to take advantage of his Two-Weapon Defense feat.,STATISTICS,Str 20, Dex 18, Con 16, Int 12, Wis 10, Cha 11,Base Atk +12, CMB +17, CMD 34,Feats Combat Reflexes, Dodge, Double Slice, Greater Weapon Focus (longsword), Greater Weapon Specialization (longsword), Improved Initiative, Improved Two-Weapon Fighting, Toughness, Two-Weapon Defense, Two-Weapon Fighting, Two-Weapon Rend, Vital Strike, Weapon Focus (longsword), Weapon Specialization (longsword),Skills Climb +18, Intimidate +13, Knowledge (dungeoneering) +12, Ride +17, Swim +18,Languages Common, Goblin,SQ armor training 3,Combat Gear necklace of fireballs (type V), potions of cure serious wounds (2), potion of fly, potion of heroism, acid flask, alchemists fire (2), holy water, Other Gear +3 light fortification breastplate, +2 keen longsword, +2 short sword,+1 shortbow with 20 arrows, masterwork heavy mace, amulet of natural armor +2, belt of physical perfection +2, boots of speed, cloak of resistance +3, ring of protection +2, antitoxin, backpack, bedroll, crowbar, everburning torch, grappling hook, hemp rope (50 ft.), tankard, trail rations (2), waterskin, 288 gp, has no method 'replace' at stripStringRegEx (evalmachine. :1391:19) at Sandbox. (evalmachine. :1495:12) at eval ( kinda confusing because the only time it runs the RegEx version on the whole block like that is when it takes out anything between &lt; &gt; for the links. Unless the findstring is returning everything? i'll keep investigating...
1421893136
The Aaron
Pro
API Scripter
As written, it only works on strings. I don't think I realized it was being used on arrays (as strings using index notation look the same as arrays). I'll look into it as well. Worst case, I can provide you one that works on arrays. =D
Don't worry just yet, i seem to be working around it! Will keep plodding along and let you know if i get stumped
Version 2.01 up :-) using Aarons suggestions above! Might handle a few cases that it didn't before, let me know how it goes!
1421927683

Edited 1422215523
Was just testing it - the giant frog still doesn't work directly "TypeError: Cannot call method 'replace' of undefined at evalmachine.:340:30 at eval (" But if I copy it into a format free notepad first, thereby getting rid of that table it makes between name and CR and then back - it works - despite the same situation that we had with the dire rat with racial modifiers to skills. So that's great! :) --- In the moment you need to *not* have a character sheet with that name already existing, when you run the script, right? Would there be a way to import into a pre-existing charactersheet? It would need to check for a link to a charactersheet and proceed as it normally does if nothing is associated with the token, and pop an error if there is already a sheet with that name. Then if there is a character associated with it, it would have to pop up a ? querry thing to ask if you want to pull stats and risk replacing anything already in the character -- then run as normal except don't replace the name with that from the gm notes but leave it as is. This would allow me to set up special characters first (names pictures etc.) then place their token and add the gm notes and pull the stats. --- One Important use I could see is for character level ups. Many of my players hate fiddling in the pathfinder sheet since it's very slow for them (bad internet I'm guessing). They prefer to work in notepad even! This way I can tell them they may do that, if they use the pfsrd format and then I just re-import the info when the character levels up! :) Instead of having to use that new stat block to create a new character (dude level 2) and then copy all the attacks and things manually from dude level 1. --- Edit: Working on it, and getting somewhere with good help. I'll share soon what I've got, if I can't get that next step working. Sofar I've found a way to get it to set attributes rather than add, to avoid repeats of attributes and allow overwriting. Just started: <a href="https://www.youtube.com/watch?v=hQVTIJBZook" rel="nofollow">https://www.youtube.com/watch?v=hQVTIJBZook</a> - Sofar I've learned: "If you've gotta use JavaScript, man up and learn JavaScript!" It supposedly is better if you know what you're doing. huh. :)
1422559641
vÍnce
Pro
Sheet Author
I really like this script Jason. Saves GM's a lot of time. Call me lazy, but I would love to see it add/create the repeatable items for attacks ( Melee , Range ), Spell-Like Abilities , Spells Prepared , Feats ... and a non-repeatable item Other Gear (or lump it in with Combat Gear ). and can I have a bag of chips as well? lol Everyone appreciates your efforts and are very thankful for your script as is.
1422564456

Edited 1422566627
The Aaron
Pro
API Scripter
@Julix: That's a pretty good survey video (2009) of the book. Once you've groked that, you should watch this one from 2014 called "The better Parts" <a href="http://www.ustream.tv/recorded/46640057" rel="nofollow">http://www.ustream.tv/recorded/46640057</a> Edit : Same talk, possibly better presentation: <a href="https://www.youtube.com/watch?v=bo36MrBfTk4" rel="nofollow">https://www.youtube.com/watch?v=bo36MrBfTk4</a>
Hey guys, Sorry for the disappearance, had a pretty huge RL week. Vince my efforts with the script have kind of been put on hold at the moment, as my party is considering a switch to Fantasy Grounds and i don't want to sink a lot of time into it if i'm just going to have to switch. If i work out that they will stay i'll happily keep tinkering away with it, but until then its like herding sheep. Also special edit for you! I took the trailing space out in "Other Gear " and now it should deal with that better haha Out of curiosity, Vince do you use Hero Labs?
1422656701

Edited 1422656731
vÍnce
Pro
Sheet Author
Hi Jason. I migrated from FG to roll20 myself. :-) Hope you guys decide to stick around since things just seem to get better here. Thanks for lumping Gear. I'm good at suggestions, but I know next to nothing in regards to javascript. Trying to improve that... I don't use HeroLabs. I've heard of other's importing to roll20 from HL.
1422656917
vÍnce
Pro
Sheet Author
BTW, version on script still says 2.01
1422751845

Edited 1422760224
EDIT Update V2.20 now parses Ranged and Melee Attacks. Let me know how it goes, note it will definitely fail for a few cases at this point- random extra brackets like: Melee:+1 falchion +14/+9 (2d4+7/18-20), or 2 slams +13 (1d6+4 plus 1d6 fire plus burn) (flame form only) will do horrible things. Also any attack that has the same attack name will confuse the ability macro when you call it (and only call the last one?) eg: Melee:Bite +10/+5 (2d4+5), or Bite +9/+4 (2d4+5)
1422759506
vÍnce
Pro
Sheet Author
I applaud your efforts Jason. Thanks for the update.
1422775224

Edited 1422775692
vÍnce
Pro
Sheet Author
First attempt with v2.21 The statblock has two attacks listed Melee mwk quarterstaff +13/+8/+3 (1d4–1) Ranged mwk light crossbow +14 (1d6/19–20) which gives an error TypeError: Cannot read property '1' of null at parseAttack (evalmachine.&lt;anonymous&gt;:4485:31) at evalmachine.&lt;anonymous&gt;:4923:28 at eval ( If I remove the two attacks, the script works as expected. UPDATE Tried an Owlbear and it worked fine. I can see you still need to go in and manually select a few things, but great addition! Melee 2 claws +8 (1d6+4 plus grab), bite +8 (1d6+4)
Awesome, good pickup - it was throwing the error because the damage string for the light crossbow was "1d6" when it was expecting "1d6 + X" fix on the way.
V2.23 Up, also added in Feats.