diff --git a/_functions/Functions0.js b/_functions/Functions0.js index 19d186c1..9b845414 100644 --- a/_functions/Functions0.js +++ b/_functions/Functions0.js @@ -254,6 +254,10 @@ function setPrototypes() { return index; }; Array.prototype.delete = function (del) { + if ( !/instanceof RegExp/.test(Array.prototype.search.toSource()) ) { + // old version of array.search available due to Adobe bug, so update it + setPrototypes(); + } var iIndx = this.search(del); return iIndx === -1 ? -1 : this.splice(iIndx, 1); }; @@ -1069,7 +1073,7 @@ function isTemplVis(tempNm, returnPrefix) { // A way to return a new, fresh object function newObj(inObj) { - return eval(inObj.toSource()); + return typeof inObj === "string" ? inObj : eval(inObj.toSource()); }; // A way to return an string as an expression (use eval() if it contains a function) diff --git a/_functions/Functions1.js b/_functions/Functions1.js index b24c83a1..ff03da40 100644 --- a/_functions/Functions1.js +++ b/_functions/Functions1.js @@ -2204,9 +2204,6 @@ function levelFieldVal() { UpdateLevelFeatures("all", Math.max(1,lvl)); // update all level features and use the set level - // the following should change to be part UpdateLevelFeatures() once custom companions can be imported - UpdateRangerCompanions(lvl); // update level-dependent things for any ranger companions - IsCharLvlVal = false; // reset global variable // make sure to update the experience points (or similar system) and alert the user @@ -4194,10 +4191,10 @@ function RemoveLanguage(language, tooltip) { RemoveLangTool("language", language function AddTool(tool, toolstooltip, replaceThis) { AddLangTool("tool", tool, toolstooltip, false, replaceThis) }; function RemoveTool(tool, toolstooltip) { RemoveLangTool("tool", tool) }; -function AddWeapon(weapon, partialReplace) { - var QI = !event.target || !event.target.name || event.target.name.indexOf("Comp.") === -1; +function AddWeapon(weapon, partialReplace, prefix) { + if (!prefix && event.target && event.target.name) prefix = getTemplPre(event.target.name, "AScomp", true); + var QI = prefix ? false : !event.target || !event.target.name || event.target.name.indexOf("Comp.") === -1; var Q = QI ? "" : "Comp.Use."; - var prefix = QI ? "" : getTemplPre(event.target.name, "AScomp", true); var maxItems = QI ? FieldNumbers.attacks : 3; var makeWordBoundryRegex = function (inStr) { @@ -5219,6 +5216,68 @@ function FeatDelete(itemNmbr) { thermoM(thermoTxt, true); // Stop progress bar } +// Add a feat to the second/third page or overflow page +function AddFeat(sFeat) { + if (!sFeat) return; + // Check if this feat is recognized and if so, quit if it already exists + var aParsedFeat = ParseFeat(sFeat); + if (aParsedFeat[0] && CurrentFeats.known.indexOf(aParsedFeat[0]) !== -1 && !FeatsList[aParsedFeat[0]].allowDuplicates && !FeatsList[aParsedFeat[0]].choices) { + return; + } else if (aParsedFeat[0]) { + for (var i = 0; i < CurrentFeats.known.length; i++) { + if (CurrentFeats.known[i] === aParsedFeat[0] && CurrentFeats.choices[i] === aParsedFeat[1]) return; + } + } + // Then check if the string isn't already in one of the feat name fields and if not, add it + var sFeatLC = sFeat.toLowerCase(); + var RegExFeat = RegExp("\\b" + sFeat.RegEscape() + "\\b", "i"); + for (var n = 1; n <= 2; n++) { + for (var i = 1; i <= FieldNumbers.feats; i++) { + var sFldNm = "Feat Name " + i; + var sCurFeat = What(sFldNm); + if (n === 1 && (RegExFeat.test(sCurFeat) || sCurFeat.toLowerCase() === sFeatLC)) { + return; // the feat already exists + } else if (n === 2 && sCurFeat === "") { + // if the next empty field on the overflow page and the overflow page is hidden, show it + if (i > FieldNumbers.featsD && !tDoc.getField(BookMarkList["Overflow sheet"])) DoTemplate("ASoverflow", "Add"); + Value(sFldNm, sFeat); + return; // feat was successfully added + } + } + } +} + +// Remove a feat from the second/third page or overflow page +function RemoveFeat(sFeat) { + // First try if this is a recognizable feat and remove that + var aParsedFeat = ParseFeat(sFeat); + var iFeatKnwn = CurrentFeats.known.indexOf(aParsedFeat[0]); + if (aParsedFeat[0] && iFeatKnwn !== -1) { + if (!FeatsList[aParsedFeat[0]].allowDuplicates && !FeatsList[aParsedFeat[0]].choices) { + FeatClear(iFeatKnwn + 1, true); + return; + } else { + for (var i = 0; i < CurrentFeats.known.length; i++) { + if (CurrentFeats.known[i] === aParsedFeat[0] && CurrentFeats.choices[i] === aParsedFeat[1]) { + FeatClear(i + 1, true); + return; + } + } + } + } + // Not recognized, so try it the hard way + var sFeatLC = sFeat.toLowerCase(); + var RegExFeat = RegExp("\\b" + sFeat.RegEscape() + "\\b", "i"); + for (var i = 1; i <= FieldNumbers.feats; i++) { + var sFldNm = "Feat Name " + i; + var sCurFeat = What(sFldNm); + if (RegExFeat.test(sCurFeat) || sCurFeat.toLowerCase() === sFeatLC) { + FeatClear(i, true); + break; + } + } +} + // Clear a feat at the position given function FeatClear(itemNmbr, doAutomation) { var Fflds = ReturnFeatFieldsArray(itemNmbr); @@ -5639,16 +5698,16 @@ function UpdateLevelFeatures(Typeswitch, newLvlForce) { thermoTxt = thermoM("Updating " + cl.fullname + " features...", false); thermoM(1/5); - // process the class header - if (newClassLvl[aClass] == 0) { // remove the header + // process the class heading + if (newClassLvl[aClass] == 0) { // remove the heading var oldHeaderString = cl.fullname + ", level " + oldClassLvl[aClass] + ":"; if (What("Class Features").indexOf("\r\r" + oldHeaderString) !== -1) oldHeaderString = "\r\r" + oldHeaderString; RemoveString("Class Features", oldHeaderString, false); - } else if (oldClassLvl[aClass] == 0) { // add the header + } else if (oldClassLvl[aClass] == 0) { // add the heading var newHeaderString = cl.fullname + ", level " + newClassLvl[aClass] + ":"; if (What("Class Features")) newHeaderString = "\r\r" + newHeaderString; AddString("Class Features", newHeaderString, false); - } else { // update the header + } else { // update the heading var newHeaderString = cl.fullname + ", level " + newClassLvl[aClass] + ":"; var oldHeaderString = !classes.old[aClass] ? "" : classes.old[aClass].fullname.RegEscape() + ".*, level \\d+:"; ReplaceString("Class Features", newHeaderString, false, oldHeaderString, true); @@ -6389,6 +6448,7 @@ function formatACdescr() { function SetToManual_Button(noDialog) { var BackgroundFld = !!CurrentVars.manual.background; + var BackgroundFeatureFld = !!CurrentVars.manual.backgroundFeature; var ClassFld = !!CurrentVars.manual.classes; var FeatFld = !!CurrentVars.manual.feats; var ItemFld = !!CurrentVars.manual.items; @@ -6398,6 +6458,7 @@ function SetToManual_Button(noDialog) { //set the checkboxes in the dialog to starting position SetToManual_Dialog.mAtt = CurrentVars.manual.attacks; SetToManual_Dialog.mBac = BackgroundFld; + SetToManual_Dialog.mBFe = BackgroundFeatureFld; SetToManual_Dialog.mCla = ClassFld; SetToManual_Dialog.mFea = FeatFld; SetToManual_Dialog.mMag = ItemFld; @@ -6423,6 +6484,16 @@ function SetToManual_Button(noDialog) { } } + //do something with the results of background feature checkbox + if (SetToManual_Dialog.mBFe !== BackgroundFeatureFld) { + if (SetToManual_Dialog.mBFe) { + CurrentVars.manual.backgroundFeature = What("Background Feature") + " "; + } else { + CurrentVars.manual.backgroundFeature = false; + ApplyBackgroundFeature(What("Background Feature"), CurrentVars.manual.backgroundFeature); + } + } + //do something with the results of class checkbox if (SetToManual_Dialog.mCla !== ClassFld) { if (SetToManual_Dialog.mCla) { @@ -8142,28 +8213,52 @@ function ParseBackgroundFeature(input) { }; //Add the text of the feature selected -function ApplyBackgroundFeature(input) { - if (IsSetDropDowns) return; // when just changing the dropdowns, don't do anything - if (event.target && event.target.name === "Background Feature" && input.toLowerCase() === event.target.value.toLowerCase()) return; //no changes were made +function ApplyBackgroundFeature(input, inputForceOld) { + if (IsSetDropDowns || CurrentVars.manual.backgroundFeature) return; // when just changing the dropdowns, don't do anything + + var sCurSel = ParseBackgroundFeature(inputForceOld ? inputForceOld : What("Background Feature")); + var sParseFeature = ParseBackgroundFeature(input); + if (sParseFeature === sCurSel) return; // No changes were made, so stop now + + var thermoTxt = thermoM("Applying background feature..."); + calcStop(); var sFldNm = "Background Feature Description"; var sTooltip = stringSource(CurrentBackground, "full,page", "The \"" + CurrentBackground.name + "\" background is found in ", ".\n"); var sNewDescr = input === "" ? "" : What(sFldNm); - var sParseFeature = ParseBackgroundFeature(input); + if (sCurSel) { + // Remove the old background feature common attributes + var Fea = ApplyFeatureAttributes( + "background feature", // type + sCurSel, // fObjName [aParent, fObjName] + [1, 0, false], // lvlA [old-level, new-level, force-apply] + false, // choiceA [old-choice, new-choice, "only"|"change"] + false // forceNonCurrent + ); + } if (sParseFeature) { + // Add the new background feature common attributes + var Fea = ApplyFeatureAttributes( + "background feature", // type + sParseFeature, // fObjName [aParent, fObjName] + [0, 1, false], // lvlA [old-level, new-level, force-apply] + false, // choiceA [old-choice, new-choice, "only"|"change"] + false // forceNonCurrent + ); var sFeaCap = sParseFeature.capitalize(); var oBackFea = BackgroundFeatureList[sParseFeature]; var sNewDescr = What("Unit System") === "imperial" ? oBackFea.description : ConvertToMetric(oBackFea.description, 0.5); sTooltip += stringSource(oBackFea, "full,page", "The \"" + sFeaCap + "\" background feature is found in ", "."); } Value("Background Feature Description", sNewDescr, sTooltip); + thermoM(thermoTxt, true); // Stop progress bar }; //set the dropdown box options for the background features function SetBackgroundFeaturesdropdown(forceTooltips) { var tempArray = [""]; - var tempString = "Select or type in the background feature you want to use and its text will be filled out below automatically.\n\n" + toUni("Background selection") + "\nThe relevant background feature is automatically selected upon selecting a background on the first page. Doing that will always override whatever you wrote here. So, please first fill out a background before you select a alternative feature here."; + var tempString = "Select or type in the background feature you want to use and its text will be filled out below automatically.\n\n" + toUni("Background selection") + "\nThe relevant background feature is automatically selected upon selecting a background on the first page. Doing that will always override whatever you wrote here. So, please first fill out a background before you select an alternative feature here."; for (var feature in BackgroundFeatureList) { if (testSource(feature, BackgroundFeatureList[feature], "backFeaExcl")) continue; diff --git a/_functions/Functions2.js b/_functions/Functions2.js index d684c504..dc524843 100644 --- a/_functions/Functions2.js +++ b/_functions/Functions2.js @@ -39,7 +39,7 @@ function ParseCreature(input) { foundDat = tempDate; } return found; -}; +} // Detects race entered and put information to global CurrentCompRace variable function FindCompRace(inputCreaTxt, aPrefix) { @@ -102,7 +102,7 @@ function setCurrentCompRace(prefix, type, found) { } // set the properties of the CurrentCompRace[prefix] object for (var prop in fObj) { - if ((/^(known|variants?|level)$/i).test(prop)) continue; + if ((/^(typeFound|known|variants?|level|typeCompanion)$/i).test(prop)) continue; CurrentCompRace[prefix][prop] = fObj[prop]; } if (type === "race" && found[1]) { @@ -113,104 +113,66 @@ function setCurrentCompRace(prefix, type, found) { CurrentCompRace[prefix][prop] = RaceSubList[subrace][prop]; } } -} - -//a function to remove the strings added to teh companion page when making a familiar/mount/etc -function resetCompTypes(prefix) { - var theType = What(prefix + "Companion.Remember"); - if (!theType || !compString[theType]) return; - // Start progress bar and stop calculations - var thermoTxt = thermoM("Resetting the companion back to its default..."); - calcStop(); - RunCreatureCallback(prefix, "companion", false); // run callbacks - RemoveString(prefix + "Cnote.Left", compString[theType].string); - RemoveString(prefix + "Comp.Use.Features", compString[theType].featurestring); - for (var i = 0; i < compString[theType].actions.length; i++) { - RemoveAction(compString[theType].actions[i][0], compString[theType].actions[i][1], compString[theType].actionTooltip); - } - - if (theType === "mount" || theType === "mechanicalserv") { - //reset the languages - var removeLangs = What(prefix + "Comp.Use.Features").match(/\u25C6 languages:.*/i); - if (CurrentCompRace[prefix] && CurrentCompRace[prefix].known && removeLangs && CurrentCompRace[prefix].languages) { - removeLangs = removeLangs[0]; - if (CurrentCompRace[prefix].typeFound === "race") { - //make a string of the languages known to the features - var languageString = "\u25C6 " + "Languages: "; - var theEnd = CurrentCompRace[prefix].languages.length - 1; - for (var l = 0; l <= theEnd; l++) { - var divider = l === 0 ? "" : l === theEnd ? " and " : ", "; - languageString += divider + CurrentCompRace[prefix].languages[l]; - languageString += l === theEnd ? "." : ""; + // Make sure these objects are no references + CurrentCompRace[prefix] = newObj(CurrentCompRace[prefix]); + // set the properties from the companion modification, if any + var sCompType = What(prefix + "Companion.Remember"); + if (sCompType) { + CurrentCompRace[prefix].typeCompanion = sCompType; + var oComp = CompanionList[sCompType]; + // process the attributesAdd + if (oComp && oComp.attributesAdd) { + for (var prop in oComp.attributesAdd) { + if (/^(name|nameAlt|source|scores|saves|skills|eval|removeeval|changeeval|companionApply)$/i.test(prop)) continue; + var tProp = oComp.attributesAdd[prop]; + if (!CurrentCompRace[prefix][prop]) { + CurrentCompRace[prefix][prop] = newObj(tProp); + } else if (/^(challengeRating|header|type|subtype)$/i.test(prop)) { + // things that should be replaced instead of amended to + CurrentCompRace[prefix][prop] = tProp; + } else if (isArray(CurrentCompRace[prefix][prop])) { + CurrentCompRace[prefix][prop] = CurrentCompRace[prefix][prop].concat(tProp); + } else if (typeof CurrentCompRace[prefix][prop] === "string" && typeof tProp === "string") { + var sCoupler = CurrentCompRace[prefix][prop].indexOf(";") !== -1 ? "; " : ", "; + CurrentCompRace[prefix][prop] = CurrentCompRace[prefix][prop].replace(/\.$/, "") + sCoupler + tProp; + } else { + // the rest we just overwrite + CurrentCompRace[prefix][prop] = newObj(tProp); } - } else if (CurrentCompRace[prefix].typeFound === "creature") { - var languageString = "\u25C6 Languages: " + CurrentCompRace[prefix].languages + "."; } - ReplaceString(prefix + "Comp.Use.Features", languageString, true, removeLangs, true); - } else if (removeLangs) { - RemoveString(prefix + "Comp.Use.Features", removeLangs[0]); } - } - - if (CurrentCompRace[prefix] && CurrentCompRace[prefix].known && theType === "mount") { - //reset the intelligence if the original creature had less than 6 - if (CurrentCompRace[prefix].typeFound === "creature" && CurrentCompRace[prefix].scores[3] < 6) { - Value(prefix + "Comp.Use.Ability.Int.Score", CurrentCompRace[prefix].scores[3]) + // process the attributesChange + if (oComp && oComp.attributesChange) { + try { + oComp.attributesChange(type === "creature" ? CurrentCompRace[prefix].known : false, CurrentCompRace[prefix]); + } catch (e) { + delete CompanionList[sCompType].attributesChange; + var eText = "The `attributesChange` attribute from the '" + sCompType + "' companion produced an error! Please contact the author of the feature to correct this issue and please include this error message:\n " + error; + for (var e in error) eText += "\n " + e + ": " + error[e]; + console.println(eText); + console.show(); + } } - } else if (theType === "familiar" && CurrentCompRace[prefix] && CurrentCompRace[prefix].typeFound === "creature" && CurrentCompRace[prefix].attacks) { - Value(prefix + "Comp.Use.Attack.perAction", CurrentCompRace[prefix].attacksAction); //set attacks per action - //add any weapons the creature possesses - for (var a = 0; a < CurrentCompRace[prefix].attacks.length; a++) { - AddWeapon(CurrentCompRace[prefix].attacks[a].name); - } - } else if (theType === "companion") { - UpdateRangerCompanions(0, prefix); - } else if (theType === "companionrr") { - UpdateRevisedRangerCompanions(0, prefix); - } else if (theType === "mechanicalserv") { - if (CurrentCompRace[prefix] && CurrentCompRace[prefix].known) { - Value(prefix + "Comp.Desc.MonsterType", CurrentCompRace[prefix].type); - }; - - var removeDamI = What(prefix + "Comp.Use.Features").match(/\u25C6 damage immunities:.*/i); - if (removeDamI && CurrentCompRace[prefix] && CurrentCompRace[prefix].known && CurrentCompRace[prefix].damage_immunities) { - ReplaceString(prefix + "Comp.Use.Features", "\u25C6 Damage Immunities: " + CurrentCompRace[prefix].damage_immunities + ".", true, removeDamI[0], true); - } else if (removeDamI) { - RemoveString(prefix + "Comp.Use.Features", removeDamI[0]); - }; - - var removeConI = What(prefix + "Comp.Use.Features").match(/\u25C6 condition immunities:.*/i); - if (removeConI && CurrentCompRace[prefix] && CurrentCompRace[prefix].known && CurrentCompRace[prefix].condition_immunities) { - ReplaceString(prefix + "Comp.Use.Features", "\u25C6 Damage Immunities: " + CurrentCompRace[prefix].condition_immunities + ".", true, removeConI[0], true); - } else if (removeConI) { - RemoveString(prefix + "Comp.Use.Features", removeConI[0]); - }; - - var removeDarkv = What(prefix + "Comp.Use.Senses").match(/darkvision \d+.?\d*.?(ft|m)/i); - if (removeDarkv && CurrentCompRace[prefix] && CurrentCompRace[prefix].known && (/darkvision \d+.?\d*.?ft/i).test(CurrentCompRace[prefix].vision + CurrentCompRace[prefix].senses)) { - var creaDarkv = (CurrentCompRace[prefix].vision + CurrentCompRace[prefix].senses).match(/darkvision \d+.?\d*.?ft/i)[0]; - if (What("Unit System") === "metric") creaDarkv = ConvertToMetric(creaDarkv, 0.5); - ReplaceString(prefix + "Comp.Use.Senses", creaDarkv, ";", removeDarkv[0], true); - } else if (removeDarkv) { - RemoveString(prefix + "Comp.Use.Senses", removeDarkv[0], ";"); - }; } - Value(prefix + "Companion.Remember", "", ""); - thermoM(thermoTxt, true); // Stop progress bar } //add a creature to the companion page -function ApplyCompRace(newRace) { +function ApplyCompRace(newRace, prefix, sCompType) { if (IsSetDropDowns) return; // when just changing the dropdowns, don't do anything - if (event.target && event.target.name.indexOf("Comp.Race") !== -1 && newRace.toLowerCase() === event.target.value.toLowerCase()) return; //no changes were made + var bIsRaceFld = event.target && event.target.name.indexOf("Comp.Race") !== -1; + if (bIsRaceFld && newRace.toLowerCase() === event.target.value.toLowerCase()) return; //no changes were made // Start progress bar and stop calculations var thermoTxt = thermoM("Applying companion race..."); calcStop(); - var prefix = getTemplPre(event.target.name, "AScomp", true); + if (!prefix) prefix = getTemplPre(event.target.name, "AScomp", true); var hpCalcTxt = " hit points calculation"; var strRaceEntry = clean(newRace).toLowerCase(); + var strRaceEntryCap = strRaceEntry.capitalize() + var sCurrentCompType = What(prefix + "Companion.Remember"); + var iPageNo = tDoc.getField(prefix + 'Comp.Race').page + 1; + if (!sCompType) sCompType = ""; var resetDescTooltips = function() { AddTooltip(prefix + "Comp.Desc.Height", ""); @@ -223,14 +185,19 @@ function ApplyCompRace(newRace) { } var undoCreaturePresistents = function(prefix, objCrea) { - // remove strings - resetCompTypes(prefix); + // remove special companion type + ApplyCompanionType(false, prefix); // also empties Companion.Remember field // undo calcChanges (just calcChanges.hp) if (objCrea.calcChanges) addCompEvals(objCrea.calcChanges, prefix, objCrea.name + hpCalcTxt, false); // undo modifiers if (objCrea.addMod) processMods(false, objCrea.name, objCrea.addMod, prefix); // reset AC explanation AddTooltip(prefix + "Comp.Use.AC", undefined, ""); + // reset HP automation to default + var sHPfld = prefix + "Comp.Use.HP.Max"; + var aHPsets = How(sHPfld).split(","); + aHPsets[3] = "nothing"; + AddTooltip(sHPfld, undefined, aHPsets.join()); // execute the function for level-dependent features and doing the removeeval UpdateCompLevelFeatures(prefix, objCrea, strRaceEntry, 0); // run callbacks @@ -259,8 +226,16 @@ function ApplyCompRace(newRace) { return; //don't do the rest of the function } + // If this is not called by using the drop-down field, change the field to show the right name + if (!bIsRaceFld && newRace !== What(prefix + "Comp.Race")) { + IsSetDropDowns = true; + Value(prefix + "Comp.Race", newRace); + IsSetDropDowns = false; + } + var fndObj = FindCompRace(newRace, prefix); - if (!fndObj.new) { // No (new) race was found, so stop here + if (!fndObj.new && sCurrentCompType === sCompType) { + // No (new) race was found and no change was made to the compantion type, so stop here thermoM(thermoTxt, true); // Stop progress bar return; //don't do the rest of the function } @@ -268,9 +243,16 @@ function ApplyCompRace(newRace) { // Undo things from a previous race, if any undoCreaturePresistents(prefix, oldCrea); - // fill the global variable with the newly found race + // save the companion type + if (sCompType && CompanionList[sCompType]) { + Value(prefix + "Companion.Remember", sCompType); + } else if (sCompType !== "reset" && fndObj.type === "creature" && CreatureList[fndObj.found].companionApply) { + Value(prefix + "Companion.Remember", CreatureList[fndObj.found].companionApply); + } + // fill the global variable with the newly found race (and companion type) setCurrentCompRace(prefix, fndObj.type, fndObj.found); var aCrea = CurrentCompRace[prefix]; + var oComp = CompanionList[aCrea.typeCompanion]; if (aCrea.typeFound === "race") {// do the following if a race was found tDoc.resetForm(compFields); //reset all the fields @@ -286,7 +268,7 @@ function ApplyCompRace(newRace) { thermoM(1/11); //increment the progress dialog's progress //set race's size - SetCreatureSize(prefix, strRaceEntry.capitalize(), aCrea.size); + SetCreatureSize(prefix, strRaceEntryCap, aCrea.size); //set race's type Value(prefix + "Comp.Desc.MonsterType", "Humanoid"); @@ -299,7 +281,9 @@ function ApplyCompRace(newRace) { //set speed var raceSpeed = aCrea.speed; - if (isArray(raceSpeed)) { //legacy + if (typeof raceSpeed === "string") { //CompanionList change + var theSpeed = raceSpeed; + } else if (isArray(raceSpeed)) { //legacy var theSpeed = isNaN(raceSpeed[0]) ? raceSpeed[0] : raceSpeed[0] + " ft"; } else { var theSpeed = raceSpeed.walk && raceSpeed.walk.spd ? raceSpeed.walk.spd + " ft" : ""; @@ -457,7 +441,7 @@ function ApplyCompRace(newRace) { var doSkill = aSk; var doExp = false; } - var skillName = AddSkillProf(doSkill, true, doExp, true); + var skillName = AddSkillProf(doSkill, true, doExp, true, undefined, prefix); if (skillName) skillsNameArr.push(skillName); } skillsTxt = formatLineList("\u25C6 Skill Proficiencies:", skillsNameArr); @@ -476,7 +460,7 @@ function ApplyCompRace(newRace) { var weaponAdd = aCrea.weaponsAdd ? aCrea.weaponsAdd : aCrea.weapons ? aCrea.weapons : []; if (!isArray(weaponAdd)) weaponAdd = [weaponAdd]; for (i = 0; i < weaponAdd.length; i++) { - AddWeapon(weaponAdd[i]); + AddWeapon(weaponAdd[i], undefined, prefix); } //add armour @@ -501,11 +485,17 @@ function ApplyCompRace(newRace) { if (aCrea.header) Value(prefix + "Comp.Type", aCrea.header); //add the size - SetCreatureSize(prefix, strRaceEntry.capitalize(), aCrea.size); + SetCreatureSize(prefix, strRaceEntryCap, aCrea.size); //set race's type - var typeString = aCrea.subtype ? aCrea.type + " (" + aCrea.subtype + ")" : aCrea.type; - Value(prefix + "Comp.Desc.MonsterType", typeString); + var sCompExplStr = oComp ? ", a special companion (" + oComp.name.toLowerCase() + "), " : ""; + var aCreaTypeDialogTxt = [ + "Select creature type for the " + strRaceEntry + " on page " + iPageNo, // title + "The " + strRaceEntry + sCompExplStr + " on page " + iPageNo + " can be one of multiple creature types. It is up to you to choose which type will now be input in the dropdown on the companion page." // description + ] + var sCreaType = isArray(aCrea.type) ? AskUserOptions(aCreaTypeDialogTxt[0], aCreaTypeDialogTxt[1], aCrea.type, "radio", true) : aCrea.type; + var sCreaSubtype = aCrea.subtype ? " (" + (isArray(aCrea.subtype) ? AskUserOptions(aCreaTypeDialogTxt[0].replace("type", "subtype"), aCreaTypeDialogTxt[1].replace(/type/ig, "subtype"), aCrea.subtype, "radio", true) : aCrea.subtype) + ")" : ""; + Value(prefix + "Comp.Desc.MonsterType", sCreaType + sCreaSubtype); //set senses var theSenses = What("Unit System") === "imperial" ? aCrea.senses : ConvertToMetric(aCrea.senses, 0.5); @@ -549,7 +539,7 @@ function ApplyCompRace(newRace) { //add any weapons the creature possesses for (var a = 0; a < aCrea.attacks.length; a++) { - AddWeapon(aCrea.attacks[a].name); + AddWeapon(aCrea.attacks[a].name, undefined, prefix); } thermoM(4/10); //increment the progress dialog's progress @@ -566,7 +556,7 @@ function ApplyCompRace(newRace) { if (aCrea.skills) { for (var aSkill in aCrea.skills) { var profSkill = CompSkillRefer(aSkill, aCrea.skills[aSkill], aCrea.scores, aCrea.proficiencyBonus); - AddSkillProf(profSkill[0], profSkill[1] !== "nothing", profSkill[1] === "expertise", false, profSkill[2]); //set the proficiency + AddSkillProf(profSkill[0], profSkill[1] !== "nothing", profSkill[1] === "expertise", false, profSkill[2], prefix); //set the proficiency } } @@ -629,19 +619,306 @@ function ApplyCompRace(newRace) { // Run callback functions, if any are present RunCreatureCallback(prefix, "creature", true); - - // make it into a special type of companion, if set to do so - if (IsNotSetCompType && aCrea.companionApply) changeCompType(aCrea.companionApply, prefix); } + // Apply the special companion type (if any) + ApplyCompanionType(true, prefix); + SetHPTooltip(false, true); thermoM(thermoTxt, true); // Stop progress bar } +// Make menu for the button on the companion page and do something with the result +function MakeCompMenu_CompOptions(prefix, MenuSelection, force) { + if (!prefix) prefix = getTemplPre(event.target.name, "AScomp", true); + var aVisLayers = eval_ish(What(prefix + "Companion.Layers.Remember")); + var creaCalcStr = StringEvals("creaStr"); + if (!MenuSelection || MenuSelection === "justMenu" || MenuSelection === "justCompanions") { + // function for adding a menu; array = [cName, cReturn, bMarked, bEnabled] + var menuLVL1 = function (aMenu, aMain) { + for (var i = 0; i < aMain.length; i++) { + aMenu.push({ + cName : aMain[i][0], + cReturn : "companion#" + aMain[i][1], + bMarked : aMain[i][2] !== undefined ? aMain[i][2] : false, + bEnabled : aMain[i][3] !== undefined ? aMain[i][3] : true + }); + } + }; + var menuLVL2 = function (aMenu, aMain, aSub) { + var aSubmenu = []; + for (var i = 0; i < aSub.length; i++) { + aSubmenu.push({ + cName : aSub[i][0], + cReturn : "companion#" + aMain[1] + "#" + aSub[i][1], + bMarked : aSub[i][2] !== undefined ? aSub[i][2] : false, + bEnabled : aSub[i][3] !== undefined ? aSub[i][3] : true + }) + } + aMenu.push({ + cName : aMain[0], + oSubMenu : aSubmenu, + bMarked : aMain[2] !== undefined ? aMain[2] : false, + bEnabled : aMain[3] !== undefined ? aMain[3] : true + }) + } + + var aCompMenu = [], bAddMenuDivider = false; + if (CurrentSources.globalExcl.indexOf("SRD") !== -1 && (!SourceList.M || (SourceList.M && CurrentSources.globalExcl.indexOf("M") !== -1))) { + // If the SRD is excluded (and the MM if it exists), add a warning here + aCompMenu = [{ + cName : "Be aware: the SRD " + (SourceList.M ? "and Monster Manual are" : "is") + " excluded from the sources!", + cReturn : "-", + bEnabled : false + }, { + cName : "-", + cReturn : "-" + }]; + } + var sCurrentCompType = What(prefix + "Companion.Remember"); + var sCurrentCompRaceLC = What(prefix + "Comp.Race").toLowerCase(); + // Menu options for creating special companions + // First get a list of all the companion options that should be available + var oCompanions = {}; + for (var sComp in CompanionList) { + if (testSource(sComp, CompanionList[sComp], "compExcl")) continue; + oCompanions[sComp] = []; + } + // Loop through the creatures and see which one are applicable for which companion options + for (var sCrea in CreatureList) { + var oCrea = CreatureList[sCrea]; + var iCreaCR = eval_ish(oCrea.challengeRating); + // test if the creature or its source isn't excluded or if it was added as part of a feature + if (testSource(sCrea, oCrea, "creaExcl") || (CurrentVars.extraCreatures && CurrentVars.extraCreatures[sCrea])) continue; + var objToAdd = {}; // the list of all companion types this creature should be added to (if any) + if (oCrea.companion) { + // Add any matching companions + var arrCreaComps = isArray(oCrea.companion) ? oCrea.companion : [oCrea.companion]; + for (var i = 0; i < arrCreaComps.length; i++) { + if (oCompanions[arrCreaComps[i]]) objToAdd[arrCreaComps[i]] = ""; + } + } + // Now test for each companion type with the includeCheck attribute if this creature should be added + for (var sComp in oCompanions) { + if (CompanionList[sComp].includeCheck && typeof CompanionList[sComp].includeCheck === "function") { + try { + var returnStr = CompanionList[sComp].includeCheck(sCrea, oCrea, iCreaCR); + if (returnStr !== false && returnStr !== undefined) { + objToAdd[sComp] = typeof returnStr === "string" ? returnStr : ""; + } + } catch (e) { + delete CompanionList[sComp].includeCheck; + var eText = "The `includeCheck` attribute from the '" + sComp + "' companion produced an error! Please contact the author of the feature to correct this issue and please include this error message:\n " + error; + for (var e in error) eText += "\n " + e + ": " + error[e]; + console.println(eText); + console.show(); + } + } + } + // Now add the creature to any of the companion types found to be applicable, if any + if (!ObjLength(objToAdd)) continue; + // First get an array of all possible names for the creature + var aCreaNames = [oCrea.name]; + var sCreaSrc = stringSource(oCrea, "first,abbr", "\t [", "]"); + if (oCrea.nameAlt) aCreaNames = aCreaNames.concat(isArray(oCrea.nameAlt) ? oCrea.nameAlt : [oCrea.nameAlt]); + // Then loop through all the found companion options, and add the full array of names (with extension string, if any) + for (var sComp in objToAdd) { + for (var i = 0; i < aCreaNames.length; i++) { + oCompanions[sComp].push([aCreaNames[i], sCrea, objToAdd[sComp] + sCreaSrc]); + } + } + } + // If we only want the object with the companions sorted + if (MenuSelection === "justCompanions") return oCompanions; + // Now loop through the companions and add a menu item for each + for (var sComp in oCompanions) { + if (!oCompanions[sComp].length) continue; // no familiar options + var oComp = CompanionList[sComp]; + var sCompSrc = stringSource(oComp, "first,abbr", "\t [", "]"); + var bIsCompType = sCurrentCompType === sComp; + var aSubInstructions = []; + oCompanions[sComp].sort(); + for (var i = 0; i < oCompanions[sComp].length; i++) { + var aCompOption = oCompanions[sComp][i]; + aSubInstructions.push([ + aCompOption[0] + (aCompOption[2] ? aCompOption[2] : ""), // cName + aCompOption[0] + "#" + aCompOption[1], // cReturn + bIsCompType && CurrentCompRace[prefix].known === aCompOption[1] && sCurrentCompRaceLC.indexOf(aCompOption[0].toLowerCase()) !== -1 // bMarked + ]); + } + menuLVL2(aCompMenu, [ + "Create " + oComp.nameMenu + sCompSrc, // cName + "add_comp#" + sComp, // cReturn + bIsCompType // bMarked + ], aSubInstructions); + bAddMenuDivider = true; + } + // Menu options for changing the current creature to a special companion option + if (bAddMenuDivider) menuLVL1(aCompMenu, [["-", "-"]]); + if (CurrentCompRace[prefix].typeFound === "creature") { + var aSubInstructions = []; + for (var sComp in oCompanions) { + var oComp = CompanionList[sComp]; + var sCompSrc = stringSource(oComp, "first,abbr", "\t [", "]"); + aSubInstructions.push([ + oComp.nameMenu + sCompSrc, // cName + sComp, // cReturn + sCurrentCompType === sComp // bMarked + ]); + } + aSubInstructions.push(["-", "-"], [ + "Reset to normal", // cName + "reset", // cReturn + undefined, // bMarked + sCurrentCompType ? true : false // bEnabled + ]); + menuLVL2(aCompMenu, [ + "Change current creature into a ... (resets creature)", + "change", + false, + CurrentCompRace[prefix].typeFound === "creature" + ], aSubInstructions); + } else if (sCurrentCompType) { + // Not a creature but has a companion type, so only allow reset + menuLVL1(aCompMenu, [["Reset current creature to normal (remove " + CompanionList[sCurrentCompType].name + ")", "change#reset"]]); + } else { + // Not a creature and no companion type, so disable this menu option + menuLVL1(aCompMenu, [["Change current creature into a ... (resets creature)", "-", false, false]]); + } + // Change visible sections + menuLVL1(aCompMenu, [["-", "-"]]); // add a divider + menuLVL2( + aCompMenu, + ["Change visible sections", "visible"], + [ + ["Show box for Companion's Appearance", "comp.img", aVisLayers[0]], + ["Show Equipment section", "comp.eqp", aVisLayers[1]] + ] + ); + // Reset companion page, add/remove page, show calculations + menuLVL1(aCompMenu, [ + ["-", "-"], + ["Reset this companion page", "reset_page"], + ["-", "-"], + ["Add extra companion page", "add_page"], + ["Remove this companion page", "remove_page"], + ["-", "-"], + ["Show things changing the companion automations", "showcalcs", undefined, creaCalcStr ? true : false] + ]); + // Save this menu in the global variable + Menus.companion = aCompMenu; + } + var MenuSelection = MenuSelection ? MenuSelection : getMenu("companion"); + if (!MenuSelection || MenuSelection[0] == "nothing" || MenuSelection[0] != "companion") return; + + // Start progress bar and stop calculations + var thermoTxt = thermoM("Applying companion menu option..."); + calcStop(); + + switch (MenuSelection[1]) { + case "add_comp" : + var sRaceName = MenuSelection[3] ? MenuSelection[3].capitalize() : CreatureList[MenuSelection[4]].name; + case "change" : + if (MenuSelection[1] === "change") var sRaceName = What(prefix + "Comp.Race"); + ApplyCompRace(sRaceName, prefix, MenuSelection[2]); + break; + case "reset_page": + thermoTxt = thermoM("Resetting the companion page...", false); // Change the progress bar text + + tDoc.resetForm([prefix + "Comp", prefix + "Text.Comp", prefix + "BlueText.Comp", prefix + "Cnote", prefix + "Companion"]); //reset all the fields + + thermoM(0.5); // Increment the progress bar + + ApplyAttackColor("", "", "Comp.", prefix); //reset the colour of the attack boxes + SetHPTooltip("reset", true); + ShowCompanionLayer(prefix); + ClearIcons(prefix + "Comp.img.Portrait", true); //reset the appearance image + + thermoTxt = thermoM("Applying...", false); // Change the progress bar text + break; + case "add_page": + DoTemplate("AScomp", "Add"); + break; + case "remove_page": + DoTemplate("AScomp", "Remove", prefix); + break; + case "visible": + if (MenuSelection[2].indexOf("comp.img") !== -1) { + aVisLayers[0] = force !== undefined ? force : !aVisLayers[0]; + } + if (MenuSelection[2].indexOf("comp.eqp") !== -1) { + aVisLayers[1] = force !== undefined ? force : !aVisLayers[1]; + } + Value(prefix + "Companion.Layers.Remember", aVisLayers.toSource()); + ShowCompanionLayer(prefix); + break; + case "showcalcs" : + ShowDialog("Things Affecting the Companion Automation", creaCalcStr); + break; + } + thermoM(thermoTxt, true); // Stop progress bar +} + +// Apply the CompanionList selection set in the Companion.Remember field +function ApplyCompanionType(bAddRemove, prefix) { + var sCompType = What(prefix + "Companion.Remember"); + if (!sCompType || !CompanionList[sCompType]) return; + var oComp = CompanionList[sCompType]; + if (CurrentCompRace[prefix].typeCompanion !== sCompType) CurrentCompRace[prefix].typeCompanion = sCompType; + + // Start progress bar and stop calculations + var thermoTxt = thermoM("Changing the companion type..."); + calcStop(); + + // First process the calcChanges + if (oComp.calcChanges) { + addCompEvals(oComp.calcChanges, prefix, oComp.name + " hit points calculation", bAddRemove); + } + + if (bAddRemove) { // adding + thermoTxt = thermoM("Applying the companion type...", false); //change the progress dialog text + + // Add companion related actions to the first page + if (oComp.action) processActions(true, oComp.nameTooltip ? oComp.nameTooltip : oComp.name, oComp.action, oComp.name); + + // Run companion callbacks from other features (last when adding) + RunCreatureCallback(prefix, "companion", true); + + } else { // removing + thermoTxt = thermoM("Resetting the companion type...", false); //change the progress dialog text + + // Remove actions from the first page, if this is the last companion of its type being removed + var iCompTypeCnt = 0; + for (var sCompPrefix in CurrentCompRace) { + if (sCompPrefix !== prefix && CurrentCompRace[sCompPrefix].typeCompanion === sCompType) iCompTypeCnt++; + } + if (oComp.action && !iCompTypeCnt) processActions(true, oComp.nameTooltip ? oComp.nameTooltip : oComp.name, oComp.action, oComp.name); + + // Run companion callbacks from other features (first when removing) + RunCreatureCallback(prefix, "companion", false); + + // Clear the remember field so that no automation picks it up + Value(prefix + "Companion.Remember", "", "", ""); + } + + thermoM(thermoTxt, true); // Stop progress bar +} + +// Create and add or remove the heading for a CompanionList entry +function SetCompanionListHeading(bAddRemove, prefix, sCompType, sFld) { + var oComp = CompanionList[sCompType]; + if (!oComp) return ""; + if (!sFld) sFld = prefix + "Cnote.Left"; + var sHeading = oComp.name; + var sOrigin = oComp.nameOrigin ? oComp.nameOrigin : ""; + var sSource = stringSource(oComp, "first,abbr", sOrigin ? ", " : ""); + if (sSource || sOrigin) sHeading += " (" + sOrigin + sSource + ")"; + tDoc[(bAddRemove ? "Add" : "Remove") + "String"](sFld, sHeading + ":", true); +} + // do the eval for a creature -function ApplyCreatureEval(prefix, objCrea, type, arrLvl, objEval) { - var theEval = objEval ? objEval[type] : objCrea[type]; - if (objCrea.typeFound !== "creature" || !theEval || typeof theEval != 'function') return; +function ApplyCreatureEval(prefix, objEval, arrLvl, sType, sName) { + if (!objEval[sType] || typeof objEval[sType] != 'function') return; if (arrLvl === undefined) { arrLvl = [ Number(How(prefix + "Comp.Desc.Age")), @@ -649,15 +926,14 @@ function ApplyCreatureEval(prefix, objCrea, type, arrLvl, objEval) { ]; } try { - return theEval(prefix, arrLvl); + return objEval[sType](prefix, arrLvl); } catch (error) { var iPageNo = tDoc.getField(prefix + 'Comp.Race').page + 1; - var sName = objCrea.name + (objEval ? '" feature/trait/action: "' + objEval.name : ''); - var eText = "The " + type + ' from "' + sName + '" on page ' + iPageNo + " produced an error! Please contact the author of the feature to correct this issue:\n " + error; + var eText = "The " + sType + ' for "' + sName + '" on page ' + iPageNo + " produced an error! Please contact the author of the feature to correct this issue and please include this error message:\n " + error; for (var e in error) eText += "\n " + e + ": " + error[e]; console.println(eText); console.show(); - delete objCrea[type]; + delete objEval[sType]; } } @@ -668,6 +944,9 @@ function UpdateCompLevelFeatures(prefix, objCrea, useName, newLvl) { if (objCrea.typeFound !== "creature") return; // only do this for CreatureList entries var isMetric = What("Unit System") === "metric", arrToEval = []; if (!useName) useName = What(prefix + "Comp.Race").toLowerCase(); + var sCompType = CurrentCompRace[prefix].typeCompanion; + var objComp = sCompType ? CompanionList[sCompType] : false; + if (!objComp && sCompType) delete CurrentCompRace[prefix].typeCompanion; // function to get the highest class level of an array of ClassList object names var highestClassLevel = function(input, isHD) { @@ -693,7 +972,7 @@ function UpdateCompLevelFeatures(prefix, objCrea, useName, newLvl) { /* Determine the old/new levels */ var oldLvl = Number(How(prefix + "Comp.Desc.Age")); - if (newLvl === undefined) { + if (newLvl === undefined || newLvl === false) { newLvl = objCrea.minlevelLinked ? highestClassLevel(objCrea.minlevelLinked) : classes.totallevel ? classes.totallevel : What("Character Level") ? Number(What("Character Level")) : 1; @@ -703,22 +982,31 @@ function UpdateCompLevelFeatures(prefix, objCrea, useName, newLvl) { AddTooltip(prefix + "Comp.Desc.Age", undefined, newLvl); var minLvl = Math.min(oldLvl, newLvl), maxLvl = Math.max(oldLvl, newLvl); - // Enqueue the main object's eval if adding the creature for the first time - if (oldLvl === 0 && newLvl > 0 && objCrea.eval) arrToEval.push(["eval", false]); + // Enqueue the main objects' eval if adding the creature for the first time + if (oldLvl === 0 && newLvl > 0) { + if (objCrea.eval) arrToEval.push([objCrea, "eval", objCrea.name]); + if (objComp && objComp.eval) arrToEval.push([objComp, "eval", objComp.menuName]); + } /* The string for the Features and Traits fields */ var arrProps = [ ["features", prefix + "Comp.Use.Features"], ["actions", prefix + "Comp.Use.Traits"], - ["traits", prefix + "Comp.Use.Traits"] + ["traits", prefix + "Comp.Use.Traits"], + ["notes", prefix + "Cnote.Left", objComp] ]; var arrCompAltStrLocs = [prefix + "Cnote.Left"]; if (!typePF) arrCompAltStrLocs.push(prefix + "Cnote.Right"); // Now loop through all the features/actions/traits for (var a = 0; a < arrProps.length; a++) { - if (!objCrea[arrProps[a][0]] || !objCrea[arrProps[a][0]].length) continue; - var feaA = [].concat(objCrea[arrProps[a][0]]); + var objUse = arrProps[a][2] ? arrProps[a][2] : objCrea; + if (!objUse || !objUse[arrProps[a][0]] || !objUse[arrProps[a][0]].length) continue; + var feaA = [].concat(objUse[arrProps[a][0]]); if (newLvl < oldLvl) feaA.reverse(); // when removing, loop through them backwards + // if this is the notes for a companion add/remove a heading if needed + if (arrProps[a][0] === "notes" && minLvl === 0) { + SetCompanionListHeading(oldLvl === 0, prefix, sCompType); + } var fldNm = arrProps[a][1]; var arrFlds = [fldNm].concat(arrCompAltStrLocs); var lastProp = a === 0 || fldNm !== arrProps[a-1][1] ? What(fldNm) : lastProp; @@ -729,8 +1017,9 @@ function UpdateCompLevelFeatures(prefix, objCrea, useName, newLvl) { var addIt = newLvl >= propMinLvl; if (doPropTxt) { // Create the strings for the property - var propFirstLine = ("\u25C6 " + (isMetric ? ConvertToMetric(prop.name, 0.5) : prop.name) + ": "); - var propFullLine = propFirstLine + (isMetric ? ConvertToMetric(prop.description, 0.5) : prop.description); + var sNameDescrCoupler = prop.joinString !== undefined ? prop.joinString : ": "; + var propFirstLine = "\u25C6 " + (isMetric ? ConvertToMetric(prop.name, 0.5) : prop.name); + var propFullLine = propFirstLine + sNameDescrCoupler + (isMetric ? ConvertToMetric(prop.description, 0.5) : prop.description); // Apply the name of the creature if [THIS] is present in the strings if (/\[THIS\]/.test(propFullLine)) { if (addIt) { @@ -755,9 +1044,9 @@ function UpdateCompLevelFeatures(prefix, objCrea, useName, newLvl) { } // Queue the (remove)eval var evalType = addIt ? "eval" : "removeeval"; - if (prop[evalType]) arrToEval.push([evalType, prop]); + if (prop[evalType]) arrToEval.push([prop, evalType, objUse.name + '" ' + fldNm + ' "' + prop.name]); // Process modifiers, if any - if (prop.addMod) processMods(addIt, objCrea.name + ": " + prop.name, prop.addMod, prefix); + if (prop.addMod) processMods(addIt, objUse.name + ": " + prop.name, prop.addMod, prefix); } if (doPropTxt) lastProp = propFullLine; } @@ -773,18 +1062,21 @@ function UpdateCompLevelFeatures(prefix, objCrea, useName, newLvl) { Value(prefix + 'Comp.Use.HD.Level', highestClassLevel(objCrea.hdLinked, true)); } - /* The Evals */ - if (oldLvl > 0 && newLvl === 0 && objCrea.removeeval) { - // Enqueue the main object's removeeval if removing the creature - arrToEval.push(["removeeval", false]); - } else if (newLvl > 0 && objCrea.changeeval) { - // Enqueue the main object's changeeeval if changing level - arrToEval.push(["changeeval", false]); + /* The main evals */ + if (oldLvl > 0 && newLvl === 0) { + // Enqueue the main objects' removeeval if removing the creature + if (objCrea.removeeval) arrToEval.push([objCrea, "removeeval", objCrea.name]); + if (objComp && objComp.removeeval) arrToEval.push([objComp, "removeeval", objComp.nameMenu]); + } else if (newLvl > 0) { + // Enqueue the main objects' changeeeval if changing level + if (objCrea.changeeval) arrToEval.push([objCrea, "changeeval", objCrea.name]); + if (objComp && objComp.changeeval) arrToEval.push([objComp, "changeeval", objComp.nameMenu]); } + // Process all the queued evals, in the order they were added - // This way, the main `eval` is process first, but after all the strings are in the right location + // This way, the main `eval` is processed first, but after all the strings are in the right location for (var i = 0; i < arrToEval.length; i++) { - ApplyCreatureEval(prefix, objCrea, arrToEval[i][0], [oldLvl, newLvl], arrToEval[i][1]); + ApplyCreatureEval(prefix, arrToEval[i][0], [oldLvl, newLvl], arrToEval[i][1], arrToEval[i][2]); } } @@ -853,6 +1145,7 @@ function processAddCompanions(bAddRemove, srcNm, aCreaAdds) { var sRaceLow = sRace.toString().toLowerCase(); var bRemoveWholePage = aCreaAdd[1]; var aCallBack = aCreaAdd[2]; + var sCompanionType = aCreaAdd[3] && CompanionList[aCreaAdd[3]] ? aCreaAdd[3] : false; var AScompA = isTemplVis('AScomp') ? What('Template.extras.AScomp').split(',') : false; if (bAddRemove) { // add var prefix = false, stopMatch = false; @@ -860,7 +1153,7 @@ function processAddCompanions(bAddRemove, srcNm, aCreaAdds) { for (var a = 1; a < AScompA.length; a++) { var sFndRace = What(AScompA[a] + 'Comp.Race'); if (!prefix && !sFndRace) prefix = AScompA[a]; - if (sFndRace.toLowerCase() === sRaceLow && CurrentCompRace[AScompA[a]]) { + if (sFndRace.toLowerCase() === sRaceLow && CurrentCompRace[AScompA[a]] && (!sCompanionType || CurrentCompRace[AScompA[a]].typeCompanion === sCompanionType)) { prefix = AScompA[a]; stopMatch = true; break; @@ -869,9 +1162,11 @@ function processAddCompanions(bAddRemove, srcNm, aCreaAdds) { } if (!prefix) prefix = DoTemplate('AScomp', 'Add'); if (!stopMatch) { - Value(prefix + 'Comp.Race', sRace); + ApplyCompRace(sRace, prefix, sCompanionType); doCallBack(aCallBack, prefix); - aChangeMsg.push('A "' + sRace + '" has been added to the companion page at page number ' + (tDoc.getField(prefix + 'Comp.Race').page + 1)); + var sChangeMsgName = '"' + What(prefix + 'Comp.Race') + '"'; // Get it from the page in case the callback changed it. + if (sCompanionType) sChangeMsgName = CompanionList[sCompanionType].nameMenu + " " + sChangeMsgName; + aChangeMsg.push('A ' + sChangeMsgName + ' has been added to the companion page at page number ' + (tDoc.getField(prefix + 'Comp.Race').page + 1) + '.'); } } else { // remove for (var a = 1; a < AScompA.length; a++) { @@ -1026,7 +1321,7 @@ function FindCompWeapons(ArrayNmbr, aPrefix) { CurrentWeapons.compKnown[prefix][j] = tempArray[j]; } } -}; +} function addCompEvals(evalObj, prefix, NameEntity, Add) { if (!evalObj) return; @@ -1418,13 +1713,13 @@ function ApplyWildshape() { //add actions if (theCrea.actions) { for (var t = 0; t < theCrea.actions.length; t++) { - strTraits += "\n\u25C6 " + theCrea.actions[t].name + ": " + theCrea.actions[t].description; + strTraits += "\n\u25C6 " + theCrea.actions[t].name + (theCrea.actions[t].joinString !== undefined ? theCrea.actions[t].joinString : ": ") + theCrea.actions[t].description; } } //add traits if (theCrea.traits) { for (var t = 0; t < theCrea.traits.length; t++) { - strTraits += "\n\u25C6 " + theCrea.traits[t].name + ": " + theCrea.traits[t].description; + strTraits += "\n\u25C6 " + theCrea.traits[t].name + (theCrea.traits[t].joinString !== undefined ? theCrea.traits[t].joinString : ": ") + theCrea.traits[t].description; } } } @@ -1880,442 +2175,6 @@ function SetCompDropdown(forceTooltips) { } } }; - -//Make menu for the button on the companion page and parse it to Menus.companion -function MakeCompMenu(prefix) { - if(!prefix) prefix = getTemplPre(event.target.name, "AScomp", true); - var usingRevisedRanger = ClassList.rangerua && !testSource("rangerua", ClassList.rangerua, "classExcl"); - var usingArtificer = SourceList["UA:A"] && CurrentSources.globalExcl.indexOf("UA:A") === -1; - var menuLVL2 = function (menu, name, array, specialArray, addName) { - var temp = {}; - var enabled = name[1] === "change" ? What(prefix + "Comp.Race") : true; - temp.cName = name[0]; - if (!enabled) { - temp.bEnabled = enabled; - } else { - temp.oSubMenu = []; - for (var i = 0; i < array.length; i++) { - if (name[1] === "visible") { - var toShow = eval_ish(What(prefix + "Companion.Layers.Remember")); - var subMarked = array[i][1] === "comp.img" ? toShow[0] : toShow[1]; - } else if (name[1] === "change") { - var subMarked = What(prefix + "Companion.Remember") === array[i][1]; - } else { - var subMarked = What(prefix + "Companion.Remember") === name[1] && CurrentCompRace[prefix].known === array[i][1]; - } - var aName = array[i][0]; - if (specialArray && addName && specialArray.indexOf(array[i][1]) !== -1) aName += addName; - temp.oSubMenu.push({ - cName : aName, - cReturn : name[1] + "#" + array[i][1] + "#" + array[i][0], - bMarked : subMarked, - bEnabled : array[i][1] === "no-mm" ? false : name[1] === "change" && (array[i][1] === "companion" || array[i][1] === "companionrr") && CurrentCompRace[prefix] && CurrentCompRace[prefix].typeFound !== "creature" ? false : true - }) - } - } - menu.push(temp); - }; - var menuLVL1 = function (item, array) { - for (var i = 0; i < array.length; i++) { - item.push({ - cName : array[i][0], - cReturn : array[i][1] + "#" + "nothing", - bEnabled : array[i][2] !== undefined ? array[i][2] : true - }); - } - }; - - var CompMenu = []; - var cObj = { - familiars : [], - familiars_not_al : [], - chainPact : [], - mounts : [], - steeds : [], - companions : [], - companionRR : [], - mechanicalServs : [] - }; - var change = [ - ["Into a familiar (Find Familiar spell)", "familiar"], - ["Into a Pact of the Chain familiar (Warlock feature)", "pact_of_the_chain"], - ["Into a mount (Find Steed spell)", "mount"] - ].concat(!SpellsList["find greater steed"] ? [] : [ - ["Into a greater mount (Find Greater Steed spell)", "steed"] - ]).concat(!usingArtificer ? [] : [ - ["Into a Mechanical Servant (Artificer feature)", "mechanicalserv"] - ]).concat([ - ["Into a Ranger's Companion", usingRevisedRanger ? "companionrr" : "companion"], - ["-", "-"], - ["Reset to normal", "reset"] - ]); - - var visOptions = [ - ["Show box for Companion's Appearance", "comp.img"], - ["Show Equipment section", "comp.eqp"] - ]; - - //make a list of all the creatures - for (var aCrea in CreatureList) { - var theCrea = CreatureList[aCrea]; - // test if the creature or its source isn't excluded or if it was added as part of a feature - if (testSource(aCrea, theCrea, "creaExcl") || (CurrentVars.extraCreatures && CurrentVars.extraCreatures[aCrea])) continue; - var arrAlts = []; - if (theCrea.nameAlt) { - var arrNames = isArray(theCrea.nameAlt) ? theCrea.nameAlt : [theCrea.nameAlt]; - for (var i = 0; i < arrNames.length; i++) { - arrAlts.push([arrNames[i], aCrea]); - } - } - if (theCrea.type === "Beast" && theCrea.size >= 3 && eval_ish(theCrea.challengeRating) <= 1/4) { - cObj.companions = cObj.companions.concat([[theCrea.name, aCrea]]).concat(arrAlts); - } else if (theCrea.type === "Beast" && theCrea.size === 2 && eval_ish(theCrea.challengeRating) <= 2) { - cObj.mechanicalServs = cObj.mechanicalServs.concat([[theCrea.name, aCrea]]).concat(arrAlts); - }; - switch (theCrea.companion) { - case "familiar_not_al" : - if (!isDisplay("DCI.Text")) break; - cObj.familiars_not_al.push(aCrea); - case "familiar" : - cObj.familiars = cObj.familiars.concat([[theCrea.name, aCrea]]).concat(arrAlts); - case "pact_of_the_chain" : - cObj.chainPact = cObj.chainPact.concat([[theCrea.name, aCrea]]).concat(arrAlts); - break; - case "mount" : - cObj.mounts = cObj.mounts.concat([[theCrea.name, aCrea]]).concat(arrAlts); - break; - case "steed" : - cObj.steeds = cObj.steeds.concat([[theCrea.name, aCrea]]).concat(arrAlts); - break; - case "companion" : - case "companionrr" : - cObj.companionRR = cObj.companionRR.concat([[theCrea.name, aCrea]]).concat(arrAlts); - break; - } - } - - // Add a reminder if the monster manual & SRD have been excluded from the sources - var noSrd = CurrentSources.globalExcl.indexOf("SRD") !== -1; - var existMm = SourceList.M; - var reminder = (existMm && CurrentSources.globalExcl.indexOf("M") && noSrd) || (!existMm && noSrd) ? ["Be aware: the SRD " + (existMm ? "and Monster Manual are" : "is") + " excluded from the sources!", "no-mm"] : false; - - // Sort the arrays and add the reminder (if any) - for (var cType in cObj) { - cObj[cType].sort(); - if (reminder && cType !== "familiars_not_al") cObj[cType].unshift(reminder); - } - - menuLVL2(CompMenu, ["Create familiar (Find Familiar spell)", "familiar"], cObj.familiars, cObj.familiars_not_al, " (if DM approves)"); - menuLVL2(CompMenu, ["Create familiar (Warlock Pact of the Chain)", "pact_of_the_chain"], cObj.chainPact, cObj.familiars_not_al, " (if DM approves)"); - menuLVL2(CompMenu, ["Create mount (Find Steed spell)", "mount"], cObj.mounts); - if (SpellsList["find greater steed"]) menuLVL2(CompMenu, ["Create greater mount (Find Greater Steed spell)", "steed"], cObj.steeds); - if (usingArtificer) menuLVL2(CompMenu, ["Create Mechanical Servant (Artificer feature)", "mechanicalserv"], cObj.mechanicalServs); - if (usingRevisedRanger) { - menuLVL2(CompMenu, ["Create Revised Ranger's Companion", "companionrr"], cObj.companionRR); - } else { - menuLVL2(CompMenu, ["Create Ranger's Companion", "companion"], cObj.companions); - }; - - CompMenu.push({cName : "-"}); //add a divider - menuLVL2(CompMenu, ["Change current creature", "change"], change); - CompMenu.push({cName : "-"}); //add a divider - menuLVL2(CompMenu, ["Change visible sections", "visible"], visOptions); - menuLVL1(CompMenu, [ - ["-", "-"], - ["Reset this Companion page", "reset"], - ["-", "-"], - ["Add extra 'Companion' page", "add page"], - [(prefix ? "Remove" : "Hide") + " this 'Companion' page", "remove page"], - ["-", "-"], - ["Show things changing the companion automations", "showcalcs", ObjLength(CurrentEvals.creaStr) && (CurrentEvals.creatureCallback || CurrentEvals.companionCallback) ? true : false] - ]); - - Menus.companion = CompMenu; - return cObj; -}; - -//call the companion menu and do something with the results -function CompOptions(prefix, input, force) { - var MenuSelection = input ? input : getMenu("companion"); - if (!MenuSelection || MenuSelection[0] == "nothing") return - if (!prefix) prefix = getTemplPre(event.target.name, "AScomp", true); - - switch (MenuSelection[0]) { - case "reset": - // Start progress bar and stop calculations - var thermoTxt = thermoM("Resetting the companion page..."); - calcStop(); - - tDoc.resetForm([prefix + "Comp", prefix + "Text.Comp", prefix + "BlueText.Comp", prefix + "Cnote", prefix + "Companion"]); //reset all the fields - - thermoM(0.5); // Increment the progress bar - - ApplyAttackColor("", "", "Comp.", prefix); //reset the colour of the attack boxes - SetHPTooltip("reset", true); - ShowCompanionLayer(prefix); - ClearIcons(prefix + "Comp.img.Portrait", true); //reset the appearance image - - thermoTxt = thermoM("Applying...", false); // Change the progress bar text - break; - case "add page": - DoTemplate("AScomp", "Add"); - break; - case "remove page": - DoTemplate("AScomp", "Remove", prefix); - break; - case "visible": - var toShow = eval_ish(What(prefix + "Companion.Layers.Remember")); - if (MenuSelection[1].indexOf("comp.img") !== -1) { - toShow[0] = force !== undefined ? force : !toShow[0]; - } - if (MenuSelection[1].indexOf("comp.eqp") !== -1) { - toShow[1] = force !== undefined ? force : !toShow[1]; - } - Value(prefix + "Companion.Layers.Remember", toShow.toSource()); - ShowCompanionLayer(prefix); - break; - case "showcalcs" : - var creaCalcStr = StringEvals("creaStr"); - if (creaCalcStr) ShowDialog("Things Affecting the Companion Automation", creaCalcStr); - break; - default: - if (MenuSelection[0] === "change" && MenuSelection[1] === "reset") { - resetCompTypes(prefix); - } else { - if (MenuSelection[0] !== "change") { - IsNotSetCompType = false; - Value(prefix + "Comp.Race", MenuSelection[2] ? MenuSelection[2].capitalize() : CreatureList[MenuSelection[1]].name); - IsNotSetCompType = true; - } - var type = MenuSelection[0] !== "change" ? MenuSelection[0] : MenuSelection[1]; - changeCompType(type, prefix); - } - } - thermoM(thermoTxt, true); // Stop progress bar -} - -//change the creature on the companion page into the chosen form (familiar, mount, or pact of the chain familiar) -function changeCompType(inputType, prefix) { - if (!compString[inputType]) return; - inputType = inputType.toLowerCase(); - var oldType = What(prefix + "Companion.Remember"); - if (oldType) resetCompTypes(prefix); - Value(prefix + "Companion.Remember", inputType); //set this so it can be called upon later - // Start progress bar and stop calculations - var thermoTxt = thermoM("Changing the companion to a predefined type..."); - calcStop(); - - // a function to add the languages - var addCharLangArr = function() { - var creaLangs = What(prefix + "Comp.Use.Features").match(/\u25C6 languages:.*/i); - if (creaLangs) creaLangs = creaLangs[0].replace(/\.$/, ""); - var charLanguages = []; - for (var i = 1; i <= FieldNumbers.langstools; i++) { - var charFld = What("Language " + i); - if (charFld && (!creaLangs || creaLangs.toLowerCase().indexOf(charFld.toLowerCase()) === -1)) { - charLanguages.push(charFld); - }; - }; - if ((/mount|steed/i).test(inputType) && charLanguages.length > 1) { - charLanguages = [AskUserOptions("Character's language the steed knows", "Find Greater Steed companion", charLanguages, "radio")]; - }; - var charLangs = charLanguages.length === 0 ? "" : (creaLangs ? "; and understands, but doesn't speak," : "\u25C6 Languages: Understands, but doesn't speak,"); - for (var i = 0; i < charLanguages.length; i++) { - charLangs += i !== 0 && charLanguages.length > 2 ? ", " : " "; - charLangs += i !== 0 && i === charLanguages.length - 1 ? "and " : ""; - charLangs += charLanguages[i]; - }; - if (creaLangs && charLangs) { - ReplaceString(prefix + "Comp.Use.Features", creaLangs + charLangs, true, creaLangs, true); - } else if (charLangs) { - AddString(prefix + "Comp.Use.Features", charLangs + ".", true); - }; - }; - - switch (inputType) { - case "familiar" : - tDoc.resetForm([prefix + "Comp.Use.Attack"]); // familiars can't make attacks - case "pact_of_the_chain" : - Value(prefix + "Comp.Type", "Familiar"); - if (CurrentCompRace[prefix].type === "Beast") changeCompDialog(prefix); //change the type, but only if just a beast - break; - case "companionrr" : - case "companion" : - Value(prefix + "Comp.Type", "Companion"); - break; - case "mount" : - case "steed" : - Value(prefix + "Comp.Type", "Mount"); - changeCompDialog(prefix); // change the type - - //add the new language options to the mount's features - addCharLangArr(); - - //set the Intelligence to 6 if less than 6 - var IntFld = prefix + "Comp.Use.Ability.Int.Score"; - if (What(IntFld) < 6) Value(IntFld, 6); - break; - case "mechanicalserv" : - Value(prefix + "Comp.Type", "Servant"); - Value(prefix + "Comp.Desc.MonsterType", "Construct"); - - //add the new language options - addCharLangArr(); - - //add the new poison damage immunity - var creaDamI = What(prefix + "Comp.Use.Features").match(/\u25C6 damage immunities:.*/i); - if (!creaDamI || !(/poison/i).test(creaDamI)) { - var newDamI = (creaDamI ? creaDamI[0].replace(/\.$/, ", ") : "\u25C6 Damage Immunities: ") + "poison."; - if (creaDamI) { - ReplaceString(prefix + "Comp.Use.Features", newDamI, true, creaDamI[0], true); - } else { - AddString(prefix + "Comp.Use.Features", newDamI, true); - }; - }; - - //add the new poisoned and charmed condition immunity - var creaConI = What(prefix + "Comp.Use.Features").match(/\u25C6 condition immunities:.*/i); - if (!creaConI) { - var newConI = "\u25C6 Condition Immunities: charmed, poisoned."; - AddString(prefix + "Comp.Use.Features", newConI, true); - } else if (!(/poisoned/i).test(creaConI) || !(/charmed/i).test(creaConI)) { - newConI = creaConI[0].replace(/\.$/, ", "); - if (!(/charmed/i).test(creaConI)) { - newConI += "charmed"; - var goCo = true; - } - if (!(/poisoned/i).test(creaConI)) newConI += (goCo ? ", " : "") + "poisoned"; - newConI += "."; - ReplaceString(prefix + "Comp.Use.Features", newConI, true, creaConI[0], true); - }; - - //add the 60 ft darkvision, if not already there, or upgrade it to 60 ft - var creaSens = What(prefix + "Comp.Use.Senses"); - var newDarkv = What("Unit System") === "metric" ? "Darkvision 18 m" : "Darkvision 60 ft"; - if (!(/darkvision \d+.?\d*.?(ft|m)/i).test(creaSens)) { - AddString(prefix + "Comp.Use.Senses", newDarkv, "; "); - } else if (!(/darkvision (60.?ft|18.?m)/i).test(creaSens)) { - var darkvis = creaSens.match(/darkvision \d+.?\d*.?(ft|m)/i)[0]; - if (parseFloat(darkvis.match(/\d+/)[0]) < (What("Unit System") === "metric" ? 18 : 60)) { - ReplaceString(prefix + "Comp.Use.Senses", newDarkv, true, darkvis, true); - } - }; - break; - default : - return; //don't do the rest of this function if inputType doesn't match one of the above - }; - - //add a string in the creature's feature section - AddString(prefix + "Comp.Use.Features", compString[inputType].featurestring, true); - - //make the string for the spell/ability explanation - AddString(prefix + "Cnote.Left", compString[inputType].string, true); - - //add any actions this spell/companion gives the character - for (var i = 0; i < compString[inputType].actions.length; i++) { - AddAction(compString[inputType].actions[i][0], compString[inputType].actions[i][1], compString[inputType].actionTooltip); - }; - thermoM(0.7); - //add level-dependent things if this is a ranger's companion - if (inputType === "companion") { - UpdateRangerCompanions(undefined, prefix); - } else if (inputType === "companionrr") { - UpdateRevisedRangerCompanions(undefined, prefix); - if (IsNotImport) { - app.alert({ - cMsg : toUni("Pick Two Skills") + "\nThe Ranger's Animal Companion that you have just added, gains proficiency with two additional skills as those already selected. Because there is no automation for selecting these proficiencies, please do so manually.\n\n" + toUni("Ability Score Improvements") + "\nThe Ranger's Animal Companion gains Ability Score Improvements whenever your character gains them. There is no automation for adding these either, so please don't forget to increase the ability scores for the animal companion when you get the reminder pop-up. Also, remember that any DCs for abilities that the beast possesses are based on ability scores and that they might need to be manually changed when changing the ability scores.\nThe 'Notes' section on the companion page automatically keeps track of how many points you can increase the ability scores and what the base value of those scores are according to the Monster Manual.", - nIcon : 3, - cTitle : "Don't forget the Skills and Ability Score Improvements!" - }); - } - } - - // Run callback functions, if any are present - RunCreatureCallback(prefix, "companion", true); - - thermoM(thermoTxt, true); // Stop progress bar -}; - -//change the type of the creature on the companion page to one of either Celestial, Fey, or Fiend -function changeCompDialog(prefix) { - if (!IsNotImport) return; - //The dialog for setting the pages to print - var theTxt = "A familiar or mount's type changes from beast to either celestial, fey, or fiend. Please select one."; - var theDialog = { - //variables to be set by the calling function - bType : "Celestial", - - //when starting the dialog - initialize : function (dialog) { - }, - - //when pressing the ok button - commit : function (dialog) { - var oResult = dialog.store(); - if (oResult["rCel"]) { - this.bType = "Celestial"; - } else if (oResult["rFey"]) { - this.bType = "Fey"; - } else if (oResult["rFie"]) { - this.bType = "Fiend"; - } - }, - - description : { - name : "COMPANION TYPE DIALOG", - elements : [{ - type : "view", - elements : [{ - type : "static_text", - item_id : "head", - alignment : "align_fill", - font : "heading", - bold : true, - height : 21, - char_width : 30, - name : "Choose the type of your familiar/mount" - }, { - type : "static_text", - item_id : "txt0", - wrap_name : true, - alignment : "align_fill", - font : "dialog", - char_width : 30, - name : theTxt - }, { - type : "cluster", - align_children : "align_distribute", - elements : [{ - type : "radio", - item_id : "rCel", - group_id : "Type", - name : "Celestial" - }, { - type : "radio", - item_id : "rFey", - group_id : "Type", - name : "Fey" - }, { - type : "radio", - item_id : "rFie", - group_id : "Type", - name : "Fiend" - }, ] - }, { - type : "gap", - height : 8 - }, { - type : "ok" - }, ] - }, ] - } - }; - - app.execDialog(theDialog); - - Value(prefix + "Comp.Desc.MonsterType", theDialog.bType); -} - //update the wild shape header and all the different shapes on the all the wildshape pages function WildshapeUpdate(inputArray) { var prefixA = What("Template.extras.WSfront").split(","); @@ -4162,166 +4021,6 @@ function setLifeStyle(input) { if (isSelection !== -1) Value("Lifestyle daily cost", Lifestyles.expenses[isSelection]); } -// update all the level-dependent features for the ranger companions on the companion pages -function UpdateRangerCompanions(newLvl, aPrefix) { - if (ClassList.rangerua && !testSource("rangerua", ClassList.rangerua, "classExcl")) { - UpdateRevisedRangerCompanions(newLvl, aPrefix); - return; - } - var thermoTxt; - - var textArray = [ - "\u2022 " + "If the beast takes the Attack action, I can use my Extra Attack feature to attack once myself", //add at level 5 - "\u2022 " + "The beast's attacks count as magical for the purpose of overcoming resistances and immunities" + "\n\u2022 " + "As a bonus action, I can command the beast to take the Dash/Disengage/Help action on its turn", //add at level 7 - "\u2022 " + "The beast can make two attacks (or multiattack) when I command it to take an Attack action", //add at level 11 - "\u2022 " + "When I cast a spell on myself, I can have it also affect the beast if it is within 30 ft of me", //add at level 15 - ] - - var theText = function (input) { - var toReturn = "If I don't command it to take an action, it takes the Dodge action instead"; - if (input >= 5) { - toReturn += "\n" + textArray[0]; - } - if (input >= 7) { - toReturn += "\n" + textArray[1]; - } - if (input >= 11) { - toReturn += "\n" + textArray[2]; - } - if (input >= 15) { - toReturn += "\n" + textArray[3]; - } - return toReturn; - } - - newLvl = newLvl !== undefined ? newLvl : classes.totallevel; - var deleteIt = newLvl === 0; - var newComp = !deleteIt && aPrefix; - - var newLvlProfB = newLvl ? ProficiencyBonusList[Math.min(newLvl, ProficiencyBonusList.length) - 1] : 0; - var RangerLvl = deleteIt || (!classes.known.ranger && !classes.known["spell-less ranger"]) ? newLvl : (classes.known.ranger ? classes.known.ranger.level : 0) + (classes.known["spell-less ranger"] ? classes.known["spell-less ranger"].level : 0); - var newLvlText = theText(RangerLvl); - var AScompA = aPrefix ? [aPrefix] : What("Template.extras.AScomp").split(",").splice(1); - - for (var i = 0; i < AScompA.length; i++) { - var prefix = AScompA[i]; - if (What(prefix + "Companion.Remember") === "companion") { //only do something if the creature is set to "companion" - - if (!thermoTxt) { // Start progress bar and stop calculations - thermoTxt = thermoM("Updating Ranger's Companion(s)..."); - calcStop(); - } - - thermoM((i+2)/(AScompA.length+2)); //increment the progress dialog's progress - - var thisCrea = CurrentCompRace[prefix] && CurrentCompRace[prefix].typeFound === "creature" ? CurrentCompRace[prefix] : false; - //first look into adding the proficiency bonus to AC, attacks, proficiencies - var remLvl = Who(prefix + "Companion.Remember").split(","); - var oldLvl = Number(remLvl[0]); - var RangerLvlOld = remLvl[1] !== undefined ? Number(remLvl[1]) : 0; - var oldLvlProfB = oldLvl ? ProficiencyBonusList[Math.min(oldLvl, ProficiencyBonusList.length) - 1] : 0; - var BlueTextArrayAdd = []; - var BlueTextArrayRemove = []; - - //add saving throw proficiencies - for (var a = 0; a < AbilityScores.abbreviations.length; a++) { - var theSave = prefix + "Comp.Use.Ability." + AbilityScores.abbreviations[a] + ".ST.Prof"; - var theSaveBT = prefix + "BlueText.Comp.Use.Ability." + AbilityScores.abbreviations[a] + ".ST.Bonus"; - var hasProfAdded = What(theSaveBT).indexOf("oProf") !== -1; - if (!deleteIt && !hasProfAdded && tDoc.getField(theSave).isBoxChecked(0) === 1) { - BlueTextArrayAdd.push(theSaveBT); - } else if ((deleteIt || tDoc.getField(theSave).isBoxChecked(0) === 0) && hasProfAdded) { - BlueTextArrayRemove.push(theSaveBT); - }; - }; - - //add skill proficiencies - for (var s = 0; s < (SkillsList.abbreviations.length - 2); s++) { - var theSkill = prefix + (typePF ? "" : "Text.") + "Comp.Use.Skills." + SkillsList.abbreviations[s] + ".Prof"; - var isProf = typePF ? tDoc.getField(theSkill).isBoxChecked(0) : What(theSkill) !== "nothing"; - var theSkillBT = prefix + "BlueText.Comp.Use.Skills." + SkillsList.abbreviations[s] + ".Bonus"; - hasProfAdded = What(theSkillBT).indexOf("oProf") !== -1; - if (!deleteIt && !hasProfAdded && isProf) { - BlueTextArrayAdd.push(theSkillBT); - } else if ((deleteIt || !isProf) && hasProfAdded) { - BlueTextArrayRemove.push(theSkillBT); - }; - }; - - //add attacks damage and to hit bonus fields, as well as count as magical to description - for (var A = 1; A <= 3; A++) { - if (What(prefix + "Comp.Use.Attack." + A + ".Weapon Selection")) { - var weaHit = prefix + "BlueText.Comp.Use.Attack." + A + ".To Hit Bonus"; - hasProfAdded = What(weaHit).indexOf("oProf") !== -1; - if (!deleteIt && !hasProfAdded) { - BlueTextArrayAdd.push(weaHit); - } else if (deleteIt && hasProfAdded) { - BlueTextArrayRemove.push(weaHit); - }; - var weaDmg = prefix + "BlueText.Comp.Use.Attack." + A + ".Damage Bonus"; - hasProfAdded = What(weaDmg).indexOf("oProf") !== -1; - if (!deleteIt && !hasProfAdded) { - BlueTextArrayAdd.push(weaDmg); - } else if (deleteIt && hasProfAdded) { - BlueTextArrayRemove.push(weaDmg); - }; - var weaDescr = prefix + "Comp.Use.Attack." + A + ".Description"; - var weaDescrStr = What(weaDescr); - var countMagic = (/(,|;)? ?counts as magical/i).test(weaDescrStr); - if (newLvl >= 7 && oldLvl < 7 && !countMagic) { - AddString(weaDescr, "Counts as magical", "; "); - } else if (newLvl < 7 && oldLvl >= 7 && countMagic) { - Value(weaDescr, weaDescrStr.replace(/(,|;)? ?counts as magical/i, '')); - } - }; - }; - - //add AC (only when adding/removing) - if (newComp) { - BlueTextArrayAdd.push(prefix + "Comp.Use.AC"); - } else if (deleteIt) { - BlueTextArrayRemove.push(prefix + "Comp.Use.AC"); - } - - var NameEntity = "Ranger's Companion"; - var Explanation = "A ranger's companion adds its master's proficiency bonus (oProf) to its AC, all skills and saving throws it is proficient with, and the to hit and damage of its attacks."; - for (var f = 0; f < BlueTextArrayAdd.length; f++) { - AddToModFld(BlueTextArrayAdd[f], "oProf", false, NameEntity, Explanation); - }; - for (var f = 0; f < BlueTextArrayRemove.length; f++) { - AddToModFld(BlueTextArrayRemove[f], "oProf", true, NameEntity, Explanation); - }; - - // Change HP calculation only when creating for the first time or when removing - if (deleteIt || newComp) { - addCompEvals(compString.companion.calcChanges, prefix, NameEntity + " hit points calculation", newComp); - if (thisCrea && deleteIt) Value(prefix + "Comp.Use.HP.Max", thisCrea.hp); - } - - //then look into the attacks per action - if (thisCrea && deleteIt) { - Value(prefix + "Comp.Use.Attack.perAction", thisCrea.attacksAction); - } else { - Value(prefix + "Comp.Use.Attack.perAction", newLvl >= 11 ? 2 : 1); - } - - //then look into the string in the notes field - if (deleteIt) { - for (var t = 0; t < textArray.length; t++) { - RemoveString(prefix + "Cnote.Left", textArray[t]); - }; - } else { - var oldLvlText = theText(RangerLvlOld); - ReplaceString(prefix + "Cnote.Left", newLvlText, false, oldLvlText); - }; - - //set the new level to the tooltip text of the remember field for later use - if (!deleteIt) AddTooltip(prefix + "Companion.Remember", newLvl + "," + RangerLvl); - } - } - if (thermoTxt) thermoM(thermoTxt, true); // Stop progress bar -} - // Give the total HP (average, fixed, max) for the main character (prefix == "") or a companion page function calcHPtotals(prefix) { var conFld = prefix ? prefix + "Comp.Use.Ability.Con.Score" : "Con"; @@ -4952,10 +4651,10 @@ function ChangeToCompleteAdvLogSheet(FAQpath) { //move the pages that we want to extract to a new instance, by running code from a console var forConsole = [ - "Execute the following:\nFirst:", + "Execute the following:\n\tFirst:", "tDoc.extractPages({nStart: 0, nEnd: 3});", - "\nAnd in the newly created document:", - "var toDelScripts = ['AbilityScores', 'ClassSelection', 'ListsBackgrounds', 'ListsClasses', 'ListsCreatures', 'ListsFeats', 'ListsGear', 'ListsMagicItems', 'ListsPsionics', 'ListsRaces', 'ListsSources', 'ListsSpells'];", + "\n\tAnd in the newly created document:", + "var toDelScripts = ['AbilityScores', 'ClassSelection', 'ListsBackgrounds', 'ListsClasses', 'ListsCompanions', 'ListsCreatures', 'ListsFeats', 'ListsGear', 'ListsMagicItems', 'ListsPsionics', 'ListsRaces', 'ListsSources', 'ListsSpells'];", "for (var s = 0; s < toDelScripts.length; s++) {this.removeScript(toDelScripts[s]);};", "this.createTemplate({cName:'ALlog', nPage:1 });", "this.createTemplate({cName:'remember', nPage:2 });", @@ -5032,218 +4731,6 @@ function CreateBkmrksCompleteAdvLogSheet() { createBookmarks(tDoc.bookmarkRoot, bkmrks); } -// update all the level-dependent features for the UA's revised ranger companions on the companion pages -function UpdateRevisedRangerCompanions(newLvl, aPrefix) { - var thermoTxt, isMetric = What("Unit System") === "metric"; - - var notesArray = [ - "\u2022 " + "When I take the Attack action, my companion can use its reaction to make one melee attack", //add at level 5 - "\u2022 " + "While my companion can see me, it has advantage on all saving throws", //add at level 7 - "\u2022 " + "My companion can, as an action, make a melee attack vs. all creatures within 5 ft of it", //add at level 11 - "\u2022 " + "My companion can, as a reaction, halve the damage of an attack from an attacker that it sees", //add at level 15 - ]; - - var theText = function (input) { - var toReturn = "My companion gains a bonus on damage rolls against my favored enemies just like me"; - if (input >= 5) { - toReturn += "\n" + notesArray[0]; - } - if (input >= 7) { - toReturn += "\n" + notesArray[1]; - } - if (input >= 11) { - toReturn += "\n" + notesArray[2]; - } - if (input >= 15) { - toReturn += "\n" + notesArray[3]; - } - return isMetric ? ConvertToMetric(toReturn, 0.5) : toReturn; - } - - var featuresArray = [ - "\u25C6 " + "Coordinated Attack: " + "As a reaction when the ranger owner takes the attack action, the companion can make one melee attack.", //add at level 5 - "\u25C6 " + "Beast's Defense: " + "While the ranger owner is within eyeshot, the companion has advantage on all saving throws.", //add at level 7 - "\u25C6 " + "Storm of Claws and Fangs: " + "As an action, the companion can make a melee attack against each creature that is within 5 ft.", //add at level 11 - "\u25C6 " + "Superior Beast's Defense: " + "As a reaction, the companion can halve the damage of an attack from an attacker that it can see.", //add at level 15 - ]; - - var theFeature = function (input) { - var toReturn = ""; - if (input >= 5) { - toReturn += featuresArray[0]; - } - if (input >= 7) { - toReturn += "\n" + featuresArray[1]; - } - if (input >= 11) { - toReturn += "\n" + featuresArray[2]; - } - if (input >= 15) { - toReturn += "\n" + featuresArray[3]; - } - return isMetric ? ConvertToMetric(toReturn, 0.5) : toReturn; - } - - var ASIs = 0; - for (var aClass in classes.known) { - var classLvL = Math.min(CurrentClasses[aClass].improvements.length, classes.known[aClass].level); - ASIs += 2 * CurrentClasses[aClass].improvements[classLvL - 1]; - } - var ASIstring = function (aCreat) { - var toReturn = "whenever I gain an ASI\r Currently, there are " + ASIs + " points "; - toReturn += aCreat && aCreat.scores ? "(default: " + aCreat.scores[0] + " Str, " + aCreat.scores[1] + " Dex, " + aCreat.scores[2] + " Con, " + aCreat.scores[3] + " Int, " + aCreat.scores[4] + " Wis, " + aCreat.scores[5] + " Cha)" : "to divide among the ability scores"; - return toReturn; - } - - newLvl = newLvl !== undefined ? newLvl : classes.totallevel; - var deleteIt = newLvl === 0; - var newComp = !deleteIt && aPrefix; - - var newLvlProfB = newLvl ? ProficiencyBonusList[Math.min(newLvl, ProficiencyBonusList.length) - 1] : 0; - var RangerLvl = deleteIt || !classes.known.rangerua ? newLvl : classes.known.rangerua.level; - var newLvlText = theText(RangerLvl); - var newLvlFea = theFeature(RangerLvl); - var AScompA = aPrefix ? [aPrefix] : What("Template.extras.AScomp").split(",").splice(1); - - for (var i = 0; i < AScompA.length; i++) { - var prefix = AScompA[i]; - if (What(prefix + "Companion.Remember") === "companionrr") { //only do something if the creature is set to "companionrr" - - if (!thermoTxt) { // Start progress bar and stop calculations - thermoTxt = thermoM("Updating Revised Ranger's Companion(s)..."); - calcStop(); - } - - thermoM((i+2)/(AScompA.length+2)); //increment the progress dialog's progress - - var thisCrea = CurrentCompRace[prefix] && CurrentCompRace[prefix].typeFound === "creature" ? CurrentCompRace[prefix] : false; - - //first update the proficiency bonus - Value(prefix + "Comp.Use.Proficiency Bonus", !deleteIt || (thisCrea && thisCrea.proficiencyBonusLinked) ? newLvlProfB : thisCrea ? thisCrea.proficiencyBonus : ""); - - //now look into adding the proficiency bonus to attack damage and removing multiattacks - var remLvl = Who(prefix + "Companion.Remember").split(","); - var oldLvl = Number(remLvl[0]); - var RangerLvlOld = remLvl[1] !== undefined ? Number(remLvl[1]) : 0; - var oldLvlProfB = oldLvl ? ProficiencyBonusList[Math.min(oldLvl, ProficiencyBonusList.length) - 1] : 0; - var diff = newLvlProfB - oldLvlProfB; - - //add ranger's prof to attacks damage fields - var NameEntity = "Revised Ranger's Companion"; - var Explanation = "The Revised Ranger's Companion adds the ranger's proficiency bonus (oProf) to the damage of its attacks."; - for (var A = 1; A <= 3; A++) { - if (What(prefix + "Comp.Use.Attack." + A + ".Weapon Selection")) { - var weaFldDmg = prefix + "BlueText.Comp.Use.Attack." + A + ".Damage Bonus"; - var hasProfAdded = What(weaFldDmg).indexOf("oProf") !== -1; - if (!deleteIt) { - ReplaceString(prefix + "Comp.Use.Attack." + A + ".Description", "", false, "(((One|Two).+as an Attack action)|(2 per Attack));? ?", true); - if (!hasProfAdded) AddToModFld(weaFldDmg, "oProf", false, NameEntity, Explanation); - } else if (deleteIt && hasProfAdded) { - AddToModFld(weaFldDmg, "oProf", true, NameEntity, Explanation); - }; - }; - }; - - //add the HD - var theCompSetting = How(prefix + "Comp.Use.HP.Max").split(","); - theCompSetting[3] = deleteIt ? "nothing" : "fixed"; - if (thisCrea && deleteIt) { - Value(prefix + "Comp.Use.HD.Level", thisCrea.hd[0]); - Value(prefix + "Comp.Use.HP.Max", thisCrea.hp, undefined, theCompSetting.join()); - } else if (thisCrea) { - Value(prefix + "Comp.Use.HD.Level", thisCrea.hd[0] + RangerLvl - 3, undefined, theCompSetting.join()); - AddTooltip(prefix + "Comp.Use.HP.Max", undefined, theCompSetting.join()); - } else if (What(prefix + "Comp.Use.HD.Level")) { - var HDincr = oldLvl === 0 ? RangerLvl - 3 : RangerLvl - oldLvl; - Value(prefix + "Comp.Use.HD.Level", What(prefix + "Comp.Use.HD.Level") + HDincr, undefined, theCompSetting.join()); - AddTooltip(prefix + "Comp.Use.HP.Max", undefined, theCompSetting.join()); - } - - //add the alignment - if (thisCrea && deleteIt) { - Value(prefix + "Comp.Desc.Alignment", thisCrea.alignment); - } else { - var theAL = tDoc.getField("Alignment").currentValueIndices; - if (theAL !== -1) { - PickDropdown(prefix + "Comp.Desc.Alignment", theAL); - } else { - Value(prefix + "Comp.Desc.Alignment", What("Alignment")); - } - } - - //add saving throw proficiencies - for (var s = 0; s < 6; s++) { - var saveFld = prefix + "Comp.Use.Ability." + AbilityScores.abbreviations[s] + ".ST"; - var creaSave = thisCrea && thisCrea.saves && isArray(thisCrea.saves) && thisCrea.saves[s] !== undefined ? thisCrea.saves[s] : ""; - if (deleteIt && thisCrea && creaSave !== "") { - Checkbox(saveFld + ".Prof"); //set the save as proficient - } else if (deleteIt) { - Checkbox(saveFld + ".Prof", false); //set the save as not proficient - } else { - Checkbox(saveFld + ".Prof"); //set the save as proficient - } - } - - //then set/remove the AC bonus (only at first time) - if (deleteIt || newComp) { - AddToModFld(prefix + "Comp.Use.AC", "oProf", newComp, "Ranger's Companion", "A ranger's companion adds its master's proficiency bonus to its AC."); - } - - //then look into the attacks per action - if (thisCrea && deleteIt) { - Value(prefix + "Comp.Use.Attack.perAction", thisCrea.attacksAction); - } else { - Value(prefix + "Comp.Use.Attack.perAction", 1); - } - - //remove the old ASI line (if any) - var ASIregex = /whenever I gain an ASI\r.*Currently.+(scores|Cha\))/; - if ((ASIregex).test(What(prefix + "Cnote.Left"))) { - ReplaceString(prefix + "Cnote.Left", "whenever I gain an ASI", false, "whenever I gain an ASI\\r.*Currently.+(scores|Cha\\))", true); - } - - //then look into the string in the notes and feature fields - if (deleteIt) { - for (var t = 0; t < notesArray.length; t++) { - RemoveString(prefix + "Cnote.Left", isMetric ? ConvertToMetric(notesArray[t], 0.5) : notesArray[t]); - } - for (var t = 0; t < featuresArray.length; t++) { - RemoveString(prefix + "Comp.Use.Features", isMetric ? ConvertToMetric(featuresArray[t], 0.5) : featuresArray[t]); - } - RemoveString(prefix + "Cnote.Left", compString.companionrr.string); - } else { - var oldLvlText = theText(RangerLvlOld); - ReplaceString(prefix + "Cnote.Left", newLvlText, false, oldLvlText); - var oldLvlFea = theFeature(RangerLvlOld); - ReplaceString(prefix + "Comp.Use.Features", newLvlFea, false, oldLvlFea); - var creaASI = ASIstring(thisCrea); - ReplaceString(prefix + "Cnote.Left", creaASI, false, "whenever I gain an ASI"); - - //remove any multiattack trait - ReplaceString(prefix + "Comp.Use.Traits", "", false, "\u25C6 Multiattack: .+(\r|$)", true); - } - - if (!deleteIt) { - //set the new level to the tooltip text of the remember field for later use - AddTooltip(prefix + "Companion.Remember", newLvl + "," + RangerLvl + ","); - } else if (thisCrea && thisCrea.traits) { - //bring back the multiattack trait, if applicable - for (var t = 0; t < thisCrea.traits.length; t++) { - var tName = thisCrea.traits[t].name; - if ((/multiattack/i).test(tName)) { - var traitString = "\u25C6 " + tName + ": " + thisCrea.traits[t].description; - AddString(prefix + "Comp.Use.Traits", traitString, true); - } - } - } - } - } - if (thermoTxt) { - SetHPTooltip(false, true); - thermoM(thermoTxt, true); // Stop progress bar - } -} - //a function to change the sorting of the skills function MakeSkillsMenu_SkillsOptions(input, onlyTooltips) { var sWho = Who("Text.SkillsNames"); @@ -5460,11 +4947,6 @@ function setListsUnitSystem(isMetric, onStart) { isMetric = isMetric ? isMetric === "metric" : What("Unit System") === "metric"; if (onStart && !isMetric) return; //nothing to do on startup and the unit system is not metric var conStr = !onStart && wasMetric === isMetric ? "UpdateDecimals" : (isMetric ? "ConvertToMetric" : "ConvertToImperial"); - - for (var cType in compString) { - var cString = compString[cType].string - if (compString[cType].string) compString[cType].string = tDoc[conStr](compString[cType].string, 0.5); - }; } // automatically add a new entry on the Adventurers Logsheet with the sheets current values @@ -6424,7 +5906,7 @@ function CalcAttackDmgHit(fldName) { var evalThing = CurrentEvals.spellCalc[evalName]; if (!evalThing || typeof evalThing !== 'function') continue; try { - var addSpellNo = evalThing(spTypeFull, spCasters, abiScoreNo); + var addSpellNo = evalThing(spTypeFull, spCasters, abiScoreNo, thisWeapon[3]); if (!isNaN(addSpellNo)) output.extraHit += Number(addSpellNo); } catch (error) { var eText = "The custom spell attack/DC (spellCalc) script '" + evalName + "' produced an error! It will be removed from the sheet for now, but please contact the author of the feature to have this issue corrected:\n " + error; @@ -7669,12 +7151,12 @@ function SetProf(ProfType, AddRemove, ProfObj, ProfSrc, Extra) { }; break; case "savetxt" : { // text to be put in the "Saving Throw advantages / disadvantages" field var fld = "Saving Throw advantages / disadvantages"; + var rxCond = /.*?(\s?\((.*?)\)).*/i; //create the set object if it doesn't exist already - var setKeys = function() { - for (var e in set) {return true;}; + if ( !ObjLength(set) ) { CurrentProfs.savetxt = { text : {}, immune : {}, adv_vs : {} }; set = CurrentProfs.savetxt; - }(); + }; //put the input into a form we can use if (typeof ProfObj == "string") ProfObj = { text : [ProfObj] }; for (var st in ProfObj) { @@ -7687,22 +7169,23 @@ function SetProf(ProfType, AddRemove, ProfObj, ProfSrc, Extra) { //a functino to parse the 'immune' and 'adv_vs' parts into a usable string var preTxt = {adv_vs : "Adv. on saves vs.", immune : "Immune to"}; var parseSvTxt = function() { - var adv_vsArr = [], immuneArr = []; - for (var svAdv in set.adv_vs) { - if (!set.immune[svAdv]) adv_vsArr.push(set.adv_vs[svAdv].name); - }; - for (var svImm in set.immune) { - immuneArr.push(set.immune[svImm].name); - }; - adv_vsArr.sort(); - immuneArr.sort(); - var theRe = { - adv_vs : formatLineList(preTxt.adv_vs, adv_vsArr), - adv_vsA : adv_vsArr, - immune : formatLineList(preTxt.immune, immuneArr), - immuneA : immuneArr + var sUseName = metric ? "nameMetric" : "name"; + var oTypes = { adv_vs : [], immune : [] }; + for (var sType in oTypes) { + for (var sThing in set[sType]) { + var obj = set[sType][sThing]; + var sBase = obj.condition ? obj.conditionBase : sThing; + if ( (sType === "adv_vs" && set.immune[sBase]) || (obj.condition && set[sType][sBase])) continue; + oTypes[sType].push(obj[sUseName]); + } + oTypes[sType].sort(); + } + return { + adv_vs : formatLineList(preTxt.adv_vs, oTypes.adv_vs), + adv_vsA : oTypes.adv_vs, + immune : formatLineList(preTxt.immune, oTypes.immune), + immuneA : oTypes.immune }; - return theRe; }; //create an object of the current state var oldSvTxt = parseSvTxt(); @@ -7721,6 +7204,11 @@ function SetProf(ProfType, AddRemove, ProfObj, ProfSrc, Extra) { nameMetric : iAddM, src : [ProfSrc] }; + var aMatchCond = iAddLC.match(rxCond); + if (aMatchCond) { + setT[iAddLC].condition = aMatchCond[2]; + setT[iAddLC].conditionBase = iAddLC.replace(aMatchCond[1], ""); + } if (attr === "text") { AddString(fld, metric ? iAdd : iAddM, "; "); } else if (attr === "immune" && CurrentProfs.resistance[iAddLC]) { diff --git a/_functions/Functions3.js b/_functions/Functions3.js index 33bf3a3d..dca776bc 100644 --- a/_functions/Functions3.js +++ b/_functions/Functions3.js @@ -57,6 +57,10 @@ function GetFeatureType(type, bNoDefaultReturn, bSingularReturn) { case "background": theReturn = "background"; break; + case "background features": + case "background feature": + theReturn = "background feature"; + break; case "races": case "race": theReturn = "race"; @@ -136,7 +140,7 @@ function ApplyFeatureAttributes(type, fObjName, lvlA, choiceA, forceNonCurrent) runEval(evalThing, attributeName, true); return; } - var eText = "The " + attributeName + " from '" + fObjName + (aParent ? "' of the '" + aParent : "") + "' " + type + " produced an error! Please contact the author of the feature to correct this issue:\n " + error; + var eText = "The " + attributeName + " from '" + fObjName + (aParent ? "' of the '" + aParent : "") + "' " + type + " produced an error! Please contact the author of the feature to correct this issue and please include this error message:\n " + error; for (var e in error) eText += "\n " + e + ": " + error[e]; console.println(eText); console.show(); @@ -309,6 +313,13 @@ function ApplyFeatureAttributes(type, fObjName, lvlA, choiceA, forceNonCurrent) type = "background"; var fObj = forceNonCurrent && BackgroundList[fObjName] ? BackgroundList[fObjName] : CurrentBackground; var displName = fObj.name; + tipNmExtra = "(background)"; + break; + case "background feature": + type = "background feature"; + var fObj = BackgroundFeatureList[fObjName]; + var displName = fObjName.capitalize(); + tipNmExtra = "(background feature)"; break; case "feats": type = "feat"; @@ -1355,9 +1366,15 @@ function applyClassFeatureText(act, fldA, oldTxtA, newTxtA, prevTxt) { if (!oldTxtA || !oldTxtA[0]) return false; // no oldTxt, so we can't do anything // make some regex objects - var oldFrstLnEsc = oldTxtA[0].replace(/^(\r|\n)*/, '').RegEscape(); - var oldRxHead = RegExp(oldFrstLnEsc + ".*", "i"); - var oldRx = RegExp("\\r?" + oldFrstLnEsc + "(.|\\r\\s\\s|\\r\\w)*", "i"); // everything until the first line that doesn't start with two spaces or a letter/number (e.g. an empty line or a new bullet point) + var fReplaceLinebreaks = function(str) { + var sEscaped = str.replace(/\n/g, '\r').replace(/^\r+/, '').RegEscape(); + var sJustLine = RegExp(sEscaped + ".*", "i"); + var sFullSection = RegExp("\\r?" + sEscaped + "(.|\\r\\s\\s|\\r\\w)*", "i"); // everything until the first line that doesn't start with two spaces or a letter/number (e.g. an empty line or a new bullet point) + return [sJustLine, sFullSection]; + } + var oldFrstLnRx = fReplaceLinebreaks(oldTxtA[0]); + var oldRxHead = oldFrstLnRx[0]; + var oldRx = oldFrstLnRx[1]; // find the field we are supposed to update var fld = fldA[0]; @@ -1382,9 +1399,8 @@ function applyClassFeatureText(act, fldA, oldTxtA, newTxtA, prevTxt) { break; case "insert" : // add the newTxt after the prevTxt if (!prevTxt) return false; // no prevTxt, so we can't do anything - var prevFrstLnEsc = prevTxt.replace(/^(\r|\n)*/, '').RegEscape(); - var prevRx = RegExp(prevFrstLnEsc + "(.|\\r\\s\\s|\\r\\w)*", "i"); - var prevTxtFound = fldTxt.match(prevRx); + var prevFrstLnRx = fReplaceLinebreaks(prevTxt); + var prevTxtFound = fldTxt.match(prevFrstLnRx[1]); var changeTxt = prevTxtFound ? fldTxt.replace(prevTxtFound[0], prevTxtFound[0] + newTxtA[1]) : fldTxt; break; case "remove" : // remove the oldTxt @@ -1435,9 +1451,9 @@ function UpdateSheetDisplay() { return; } - if (ChangesDialogSkip.chXP === undefined) { + if (!ChangesDialogSkip || ChangesDialogSkip.chXP === undefined) { var cDialogFld = What("ChangesDialogSkip.Stringified"); - ChangesDialogSkip = cDialogFld !== "({})" ? eval(cDialogFld) : { + ChangesDialogSkip = cDialogFld && cDialogFld !== "({})" ? eval_ish(cDialogFld) : { chXP : false, // experience points chAS : false, // ability scores chHP : false, // hit points @@ -3279,6 +3295,16 @@ function MakeMagicItemMenu_MagicItemOptions(MenuSelection, itemNmbr) { // Add a magic item to the third page or overflow page function AddMagicItem(item, attuned, itemDescr, itemWeight, overflow, forceAttunedVisible) { + // Check if the item is recognized and if that is already known to be present + var aParsedItem = ParseMagicItem(item); + if (aParsedItem[0] && !MagicItemsList[aParsedItem[0]].allowDuplicates && CurrentMagicItems.known.indexOf(MagicItemsList[0]) !== -1) { + return; + } else if (aParsedItem[0]) { + for (var i = 0; i < CurrentMagicItems.known.length; i++) { + if (CurrentMagicItems.known[i] === aParsedItem[0] && CurrentMagicItems.choices[i] === aParsedItem[1]) return; + } + } + // Item is not recognized and/or not present exactly, so do a manual check item = item.substring(0, 2) === "- " ? item.substring(2) : item; var itemLower = item.toLowerCase(); var RegExItem = "\\b" + item.RegEscape() + "\\b"; @@ -3323,13 +3349,22 @@ function AddMagicItem(item, attuned, itemDescr, itemWeight, overflow, forceAttun // Remove a magic item from the third page or overflow page function RemoveMagicItem(item) { - // First try if this is a recognizable magic item and remove that - var parseItem = ParseMagicItem(item); - var fndItem = CurrentMagicItems.known.indexOf(parseItem[0]); - if (parseItem[0] && fndItem !== -1) { - MagicItemClear(fndItem + 1, true); - return; - } + // First try if this is a recognizable feat and remove that + var aParsedItem = ParseMagicItem(item); + var iItemKnwn = CurrentMagicItems.known.indexOf(aParsedItem[0]); + if (aParsedItem[0] && iItemKnwn !== -1) { + if (!MagicItemsList[aParsedItem[0]].allowDuplicates) { + MagicItemClear(iItemKnwn + 1, true); + return; + } else { + for (var i = 0; i < CurrentMagicItems.known.length; i++) { + if (CurrentMagicItems.known[i] === aParsedItem[0] && CurrentMagicItems.choices[i] === aParsedItem[1]) { + MagicItemClear(i + 1, true); + return; + } + } + } + } // Not recognized, so try it the hard way item = item.substring(0, 2) === "- " ? item.substring(2) : item; var itemLower = item.toLowerCase(); diff --git a/_functions/FunctionsImport.js b/_functions/FunctionsImport.js index bab640f3..a72594d7 100644 --- a/_functions/FunctionsImport.js +++ b/_functions/FunctionsImport.js @@ -1186,21 +1186,23 @@ function DirectImport(consoleTrigger) { if (ImportField(prefixTo + "Companion.Layers.Remember", {notTooltip: true, notSubmitName: true}, prefixFrom + "Companion.Layers.Remember")) ShowCompanionLayer(prefixTo); doChildren("Whiteout.Cnote", prefixFrom, prefixTo, false, true); - //set the race - ImportField(prefixTo + "Comp.Race", {notTooltip: true, notSubmitName: true}, prefixFrom + "Comp.Race"); + //get and apply the race and companion type + var compRaceFldFrom = global.docFrom.getField(prefixFrom + "Comp.Race"); + var compRaceFldTo = global.docFrom.getField(prefixFrom + "Comp.Race"); + if (compRaceFldFrom.value) { + if (compRaceFldFrom.submitName) compRaceFldTo.submitName = compRaceFldFrom.submitName; + var compTypeFldFrom = global.docFrom.getField(prefixFrom + "Companion.Remember"); + global.docTo.ApplyCompRace(compRaceFldFrom.value, prefixTo, compTypeFldFrom ? compTypeFldFrom.value : ""); + } - //set companion ability scores and modifiers now, for coming automation might require it + //set companion ability scores and modifiers for (var a = 0; a < AbilityScores.abbreviations.length; a++) { var abiS = AbilityScores.abbreviations[a]; ImportField(prefixTo+"Comp.Use.Ability."+abiS+".Score", {notTooltip: true, notSubmitName: true}, prefixFrom+"Comp.Use.Ability."+abiS+".Score"); Value(prefixTo+"Comp.Use.Ability."+abiS+".Mod", Math.round((What(prefixTo+"Comp.Use.Ability."+abiS+".Score") - 10.5) * 0.5)); } - //set the type, if any - var compTypeFrom = global.docFrom.getField(prefixFrom + "Companion.Remember"); - if (compTypeFrom && compTypeFrom.value) changeCompType(compTypeFrom.value, prefixTo); - - //Set some one-off fields + //set some one-off fields ImportField(prefixTo + "Comp.Type", {notTooltip: true, notSubmitName: true}, prefixFrom + "Comp.Type"); //do the description fields @@ -2855,12 +2857,18 @@ function ImportUserScriptFile(filePath) { } }; var iFileName = iFileCont.match(/iFileName ?= ?("|')(.*?[^\\])\1/); - var useFileName = iFileName ? util.printd("yyyy/mm/dd", new Date()) + " - " + iFileName[2].replace(/\\/g, "") : util.printd("yyyy/mm/dd HH:mm", new Date()) + " - " + "no iFileName"; + if (iFileName) { + iFileName = iFileName[2].replace(/\\/g, ""); + var iFileNameLC = iFileName.toLowerCase(); + var useFileName = util.printd("yyyy/mm/dd", new Date()) + " - " + iFileName + } else { + var useFileName = util.printd("yyyy/mm/dd HH:mm", new Date()) + " - " + "no iFileName"; + } var iFileNameMatch = false; if (iFileName) { for (var aFileName in CurrentScriptFiles) { var endFileName = aFileName.replace(/\d+\/\d+\/\d+ - /, ""); - if (endFileName.toLowerCase() === iFileName.toLowerCase()) { + if (endFileName.toLowerCase() === iFileNameLC) { iFileNameMatch = aFileName; break; }; diff --git a/_functions/FunctionsResources.js b/_functions/FunctionsResources.js index c70cda62..60503d38 100644 --- a/_functions/FunctionsResources.js +++ b/_functions/FunctionsResources.js @@ -282,6 +282,10 @@ function resourceExclusionSetting(spellSources, noChanges, oldResults) { exclObj : "creaExcl", name : "Creatures", listObj : "CreatureList" + }, { + exclObj : "compExcl", + name : "Companion Options", + listObj : "CompanionList" }]; if (tDoc.info.SpellsOnly) { resourceOptions = [{ @@ -339,7 +343,7 @@ function resourceExclusionSetting(spellSources, noChanges, oldResults) { var subKey = subObjs[c].toLowerCase(); if (opt.exclObj == "racesExcl") subKey = key + "-" + subKey; var subObj = optSubObj[subKey]; - if (subObj.defaultExcluded) { + if (subObj && subObj.defaultExcluded) { var subID = subKey; var subName = mainObjName + ": " + ( opt.subListObjName && subObj[opt.subListObjName] ? subObj[opt.subListObjName] : @@ -371,20 +375,27 @@ function resourceDecisionDialog(atOpening, atReset, forceDDupdate) { var isFirstTime = atReset ? atReset : CurrentSources.firstTime; var spellSources = []; if (tDoc.info.SpellsOnly) { - // If this is a spell sheet, only use sources that have spells or spellcasting classes associated with them - for (var u in SpellsList) { - var sSource = parseSource(SpellsList[u].source); - if (!sSource) continue; - for (var i = 0; i < sSource.length; i++) { - if (spellSources.indexOf(sSource[i][0]) === -1) spellSources.push(sSource[i][0]); - }; - }; - for (var aClass in ClassList) { - var sSource = parseSource(ClassList[aClass].source); - if (!sSource || !ClassList[aClass].spellcastingFactor || aClass === "rangerua") continue; - for (var i = 0; i < sSource.length; i++) { - if (spellSources.indexOf(sSource[i][0]) === -1) spellSources.push(sSource[i][0]); + var fAddToSpellSources = function(aRawSource) { + var aSources = parseSource(aRawSource); + if (!aSources) return; + for (var i = 0; i < aSources.length; i++) { + if (spellSources.indexOf(aSources[i][0]) === -1) spellSources.push(aSources[i][0]); }; + } + // If this is a spell sheet, only use sources that have spells or spellcasting classes associated with them + for (var u in SpellsList) fAddToSpellSources(SpellsList[u].source); + for (var sClass in ClassList) { + var objClass = ClassList[sClass]; + if (objClass.spellcastingFactor && sClass !== "rangerua") { + fAddToSpellSources(objClass.source); + } + if (objClass.subclasses && isArray(objClass.subclasses[1])) { + for (var i = 0; i < objClass.subclasses[1].length; i++) { + var objSubClass = ClassSubList[objClass.subclasses[1][i]] + if (objSubClass && objSubClass.spellcastingFactor) + fAddToSpellSources(objSubClass.source); + }; + } }; }; @@ -605,6 +616,7 @@ function resourceDecisionDialog(atOpening, atReset, forceDDupdate) { bBac : function (dialog) {resourceSelectionDialog("background"); this.updateDefExcl(dialog);}, bBaF : function (dialog) {resourceSelectionDialog("background feature"); this.updateDefExcl(dialog);}, bCre : function (dialog) {resourceSelectionDialog("creature"); this.updateDefExcl(dialog);}, + bCom : function (dialog) {resourceSelectionDialog("companion"); this.updateDefExcl(dialog);}, bAtk : function (dialog) {resourceSelectionDialog("weapon"); this.updateDefExcl(dialog);}, bArm : function (dialog) {resourceSelectionDialog("armor"); this.updateDefExcl(dialog);}, bAmm : function (dialog) {resourceSelectionDialog("ammo"); this.updateDefExcl(dialog);}, @@ -809,6 +821,12 @@ function resourceDecisionDialog(atOpening, atReset, forceDDupdate) { bold : true, item_id : "bSpe", name : "Spells/Psionics" + }, { + type : "button", + font : "dialog", + bold : true, + item_id : "bCom", + name : "Companion Options" }] }, { type : "view", @@ -1172,6 +1190,22 @@ function resourceSelectionDialog(type) { } }; break; + case "companion" : + var theName = "Special Companion Options"; + var CSatt = "compExcl"; + for (var u in CompanionList) { + var oEntry = CompanionList[u]; + var uName = amendSource(oEntry.nameMenu ? oEntry.nameMenu : oEntry.name, oEntry); + var uTest = testSource(u, oEntry, CSatt, true); + if (uTest === "source") continue; + refObj[uName] = u; + if (uTest) { + exclObj[uName] = -1; + } else { + inclObj[uName] = -1; + } + }; + break; case "weapon" : var theName = "Weapons/Attacks"; var CSatt = "weapExcl"; diff --git a/_functions/FunctionsSpells.js b/_functions/FunctionsSpells.js index abdfc10d..dbb04411 100644 --- a/_functions/FunctionsSpells.js +++ b/_functions/FunctionsSpells.js @@ -31,14 +31,14 @@ function ReturnSpellFieldsContentArray(underscores, psionic) { "", underscores ? Array(21 + (typePF ? 6 : 0)).join("_") : psionic ? "PSIONIC POWER" : "SPELL", underscores ? Array(84 + (typePF ? 25: 0)).join("_") : "DESCRIPTION", - underscores ? Array( 4 + (typePF ? 1 : 0)).join("_") : "SAVE", + underscores ? Array( 5).join("_") : "SAVE", underscores ? Array( 7 + (typePF ? 1 : 0)).join("_") : psionic ? " ORDER" : "SCHOOL", underscores ? Array( 7 + (typePF ? 1 : 0)).join("_") : "TIME", underscores ? Array(10 + (typePF ? 2 : 0)).join("_") : "RANGE", - underscores ? Array(7).join("_") : "COMP", + underscores ? Array( 7).join("_") : "COMP", underscores ? Array(12 + (typePF ? 3 : 0)).join("_") : "DURATION", - underscores ? Array( 2 + (typePF ? 1 : 0)).join("_") : "B", - underscores ? Array(4).join("_") : "PG." + underscores ? Array( 3).join("_") : "B", + underscores ? Array( 4).join("_") : "PG." ]; }; @@ -228,7 +228,11 @@ function GetSpellObject(theSpl, theCast, firstCol, noOverrides, tipShortDescr) { if (ttSpellObj.ritual) spTooltip += " (ritual)"; } - if (ttSpellObj.time) spTooltip += "\n Casting Time: " + ttSpellObj.time.replace(/1 a\b/i, '1 action').replace(/1 bns\b/i, '1 bonus action').replace(/1 rea\b/i, '1 reaction').replace(/\b1 min\b/i, '1 minute').replace(/\b1 h\b/i, '1 hour').replace(/\bmin\b/i, 'minutes').replace(/\bh\b/i, 'hours'); + if (ttSpellObj.timeFull) { + spTooltip += "\n Casting Time: " + ttSpellObj.timeFull; + } else if (ttSpellObj.time) { + spTooltip += "\n Casting Time: " + ttSpellObj.time.replace(/1 a\b/i, '1 action').replace(/1 bns\b/i, '1 bonus action').replace(/1 rea\b/i, '1 reaction').replace(/\b1 min\b/i, '1 minute').replace(/\b1 h\b/i, '1 hour').replace(/\bmin\b/i, 'minutes').replace(/\bh\b/i, 'hours'); + } if (ttSpellObj.range) spTooltip += "\n Range: " + ttSpellObj.range.replace(/s: *(.*)/i, "Self ($1)").replace(/rad\b/i, "radius").replace(/(\d+)(ft|m)/i, "$1-$2"); @@ -464,11 +468,13 @@ function ApplySpell(FldValue, rememberFldName) { //set the spell book name and page var parseSrc = parseSource(aSpell.source); - // Only use the first letter of the source, as there is no more space + // Only use the first source, as there is no more space var spBook = parseSrc ? parseSrc[0][0] : ""; + var spBookAbbr = spBook && SourceList[spBook].abbreviationSpellsheet ? SourceList[spBook].abbreviationSpellsheet : spBook.substr(0,1); // Get the page number, unless it is Unearthed Arcana, then get the abbreviation (the first three characters after the colon) var spPage = spBook && spBook !== "UA:TMC" && SourceList[spBook].group === "Unearthed Arcana" ? spBook.replace("UA:", "").substr(0,3) : parseSrc && parseSrc[0][1] ? parseSrc[0][1] : ""; - Value(base.replace("remember", "book"), spBook.substr(0,1), aSpell.tooltipSource); + // Add them to the sheet + Value(base.replace("remember", "book"), spBookAbbr, aSpell.tooltipSource); Value(base.replace("remember", "page"), spPage, aSpell.tooltipSource); input[1] = aSpell.firstCol; // use the firstCol, as the CurrentEval could have changed it @@ -3410,7 +3416,12 @@ function GenerateSpellSheet(GoOn) { //now add the general list, if chosen to do the full class list or if this is a 'list' spellcaster that didn't chose to only do the prepared spells if (spCast.typeList === 4 || (spCast.typeSp === "list" && spCast.typeList !== 3)) { var spListLevel = spCast.list.level; //put the level of the list here for safe keeping - spCast.list.level = [spCast.typeList === 4 ? 0 : 1, spCast.factor && spCast.factor[1] == "warlock" ? 9 : spListLevel ? spListLevel[1] : 9]; //set the list level to generate + // Set the list level to generate + var iLowestLevel = spListLevel ? spListLevel[0] : 0; + spCast.list.level = [ + spCast.typeList === 4 ? iLowestLevel : Math.max(iLowestLevel, 1), + spCast.factor && spCast.factor[1] == "warlock" ? 9 : spListLevel ? spListLevel[1] : 9 + ]; //add the full spell list of the class var fullClassSpellList = CreateSpellList(spCast.list, false, false, false, CurrentCasters.incl[i], spCast.typeSp); @@ -5131,10 +5142,10 @@ function ChangeToCompleteSpellSheet(thisClass, FAQpath) { //move the pages that we want to extract to a new instance, by running code from a console var forConsole = [ - "Execute the following:\nFirst:", + "Execute the following:\n\tFirst:", "tDoc.extractPages({nStart: 0, nEnd: 4});", - "\nAnd in the newly created document:", - "var toDelScripts = ['AbilityScores', 'ClassSelection', 'ListsBackgrounds', 'ListsCreatures', 'ListsFeats', 'ListsGear', 'ListsMagicItems', 'ListsRaces'];", + "\n\tAnd in the newly created document:", + "var toDelScripts = ['AbilityScores', 'ClassSelection', 'ListsBackgrounds', 'ListsCompanions', 'ListsCreatures', 'ListsFeats', 'ListsGear', 'ListsMagicItems', 'ListsRaces'];", "for (var s = 0; s < toDelScripts.length; s++) {this.removeScript(toDelScripts[s]);};", "this.createTemplate({cName:'SSfront', nPage:1 });", "this.createTemplate({cName:'SSmore', nPage:2 });", diff --git a/_variables/Lists.js b/_variables/Lists.js index 6346fd52..998d5008 100644 --- a/_variables/Lists.js +++ b/_variables/Lists.js @@ -24,6 +24,7 @@ function InitiateLists() { "BackgroundFeatureList", "ClassList", "ClassSubList", + "CompanionList", "CreatureList", "FeatsList", "MagicItemsList", @@ -367,7 +368,6 @@ var IsNotMagicItemMenu = true; var IsNotWeaponMenu = true; var IsNotConditionSet = true; var IsNotUserScript = true; -var IsNotSetCompType = true; var IsSetDropDowns = false; var IsCharLvlVal = false; @@ -902,6 +902,7 @@ var SetToManual_Dialog = { //variables to be set by the calling function mAtt : false, mBac : false, + mBFe : false, mCla : false, mFea : false, mRac : false, @@ -913,6 +914,7 @@ var SetToManual_Dialog = { "img1" : allIcons.automanual, "Atta" : this.mAtt, "Back" : this.mBac, + "BaFe" : this.mBFe, "Clas" : this.mCla, "Feat" : this.mFea, "Item" : this.mMag, @@ -926,6 +928,7 @@ var SetToManual_Dialog = { this.mAtt = oResult["Atta"]; this.mRac = oResult["Race"]; this.mBac = oResult["Back"]; + this.mBFe = oResult["BaFe"]; this.mCla = oResult["Clas"]; this.mFea = oResult["Feat"]; this.mMag = oResult["Item"]; @@ -977,7 +980,7 @@ var SetToManual_Dialog = { type : "check_box", item_id : "Atta", name : "Attacks", - char_width : 12 + char_width : 15 }, { type : "static_text", item_id : "tAtt", @@ -992,12 +995,27 @@ var SetToManual_Dialog = { type : "check_box", item_id : "Back", name : "Background", - char_width : 12 + char_width : 15 }, { type : "static_text", item_id : "tBac", name : "Do nothing when changing the background" }] + }, { + type : "view", + align_children : "align_row", + char_height : 2, + char_width : 38, + elements : [{ + type : "check_box", + item_id : "BaFe", + name : "Background Feature", + char_width : 15 + }, { + type : "static_text", + item_id : "tBaF", + name : "Do nothing when changing the background feature" + }] }, { type : "view", align_children : "align_row", @@ -1007,7 +1025,7 @@ var SetToManual_Dialog = { type : "check_box", item_id : "Clas", name : "Class", - char_width : 12 + char_width : 15 }, { type : "static_text", item_id : "tCla", @@ -1022,7 +1040,7 @@ var SetToManual_Dialog = { type : "check_box", item_id : "Feat", name : "Feats", - char_width : 12 + char_width : 15 }, { type : "static_text", item_id : "tFea", @@ -1037,7 +1055,7 @@ var SetToManual_Dialog = { type : "check_box", item_id : "Item", name : "Magic Items", - char_width : 12 + char_width : 15 }, { type : "static_text", item_id : "tFea", @@ -1052,7 +1070,7 @@ var SetToManual_Dialog = { type : "check_box", item_id : "Race", name : "Race", - char_width : 12 + char_width : 15 }, { type : "static_text", item_id : "tRac", @@ -1718,166 +1736,6 @@ var warlockSpellTable = [ var SpellPointsTable = [0, 4, 6, 14, 17, 27, 32, 38, 44, 57, 64, 73, 73, 83, 83, 94 ,94, 107, 114, 123, 133]; -var compString = { - mount : { - featurestring : "\u25C6 Find Steed: If dropped to 0 HP, the steed disappears, leaving behind no physical form.", - string : "Find Steed (2nd-level conjuration spell, PHB 240):" + - "\n\u2022 " + "Summon a spirit in the form of a steed, appearing in an unoccupied space within 30 ft" + - "\n " + "It assumes a chosen form: warhorse, pony, camel, elk, or mastiff (DM can allow more forms)" + - "\n " + "The steed has the statistics of the chosen form, though its type is celestial, fey, or fiend" + - "\n " + "If it has an Intelligence of 5 or less, its Intelligence becomes 6 " + - "\n " + "It gains the ability to understand one language that I, the caster, can speak" + - "\n " + "When the steed drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n\u2022 " + "The steed serves me as a mount. I have a bond with it that allows us to fight as a seamless unit" + - "\n\u2022 " + "While mounted on my steed, I can make any spell I cast that targets only me also target it" + - "\n\u2022 " + "While my steed is within 1 mile of me, we can communicate with each other telepathically" + - "\n\u2022 " + "I can dismiss my steed at any time as an action, causing it to disappear" + - "\n\u2022 " + "Casting this spell again summons the same steed, restored to its max HP, without conditions" + - "\n\u2022 " + "I can't have more than one steed bonded at a time; as an action, I can release it from its bond", - actions : [["action", "Find Steed (dismiss)"]], - actionTooltip : "the Find Steed spell" - }, - steed : { - featurestring : "\u25C6 Find Greater Steed: If dropped to 0 HP, the steed disappears, leaving behind no physical form.", - string : "Find Greater Steed (4th-level conjuration spell, XGtE 156):" + - "\n\u2022 " + "Summon a spirit in the form of a steed, appearing in an unoccupied space within 30 ft" + - "\n " + "It has the chosen form: griffon, pegasus, peryton, dire wolf, rhinoceros, or saber-toothed tiger" + - "\n " + "The steed has the statistics of the chosen form, though its type is celestial, fey, or fiend" + - "\n " + "If it has an Intelligence of 5 or less, its Intelligence becomes 6 " + - "\n " + "It gains the ability to understand one language that I, the caster, can speak" + - "\n " + "When the steed drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n\u2022 " + "The steed serves me as a mount. I have a bond with it that allows us to fight as a seamless unit" + - "\n\u2022 " + "While mounted on my steed, I can make any spell I cast that targets only me also target it" + - "\n\u2022 " + "While my steed is within 1 mile of me, I can communicate with it telepathically" + - "\n\u2022 " + "I can dismiss my steed at any time as an action, causing it to disappear" + - "\n\u2022 " + "Casting this spell again summons the same steed, restored to its max HP, without conditions" + - "\n\u2022 " + "I can't have more than one steed bonded at a time; as an action, I can release it from its bond", - actions : [["action", "Find Greater Steed (dismiss)"]], - actionTooltip : "the Find Greater Steed spell" - }, - familiar : { - featurestring : "\u25C6 Find Familiar: If dropped to 0 HP, the familiar disappears, leaving behind no physical form. The familiar must obey all commands of the master.", - string : "Find Familiar (1st-level conjuration [ritual] spell, PHB 240):" + - "\n\u2022 " + "Summon a spirit that serves as a familiar, appearing in an unoccupied space within 10 ft" + - "\n " + "It assumes a chosen form (can change at every casting): bat, cat, crab, frog (toad), hawk," + - "\n " + "lizard, octopus, owl, poisonous snake, fish (quipper), rat, raven, sea horse, spider, or weasel." + - "\n " + "It has the chosen form's statistics, but its type changes from beast to celestial, fey, or fiend" + - "\n " + "When the familiar drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n " + "It reappears when I cast this spell again (in a new form if so desired)" + - "\n\u2022 " + "The familiar acts independently of me, but it always obeys my commands" + - "\n " + "In combat, it rolls its own initiative and acts on its own turn, but it can't attack" + - "\n\u2022 " + "While it is within 100 ft of me, I can communicate with it telepathically" + - "\n\u2022 " + "As an action, I see/hear what it does (but not with my senses) until the start of my next turn" + - "\n\u2022 " + "As an action, I can temporarily dismiss it, having it disappears into a pocket dimension" + - "\n\u2022 " + "As an action, while it is temporarily dismissed, I can cause it to reappear within 30 ft" + - "\n\u2022 " + "I can't have more than one familiar bonded at a time; as an action, I can dismiss it forever" + - "\n\u2022 " + "When I cast a spell with a range of touch, my familiar can deliver the spell" + - "\n " + "It must be within 100 ft of me and it must use its reaction to deliver the spell when I cast it" + - "\n " + "It acts as if it cast the spell, but it can use my modifiers for any attack rolls the spell requires", - actions : [["action", "Find Familiar (dismiss/reappear)"], ["action", "Use familiar's senses"]], - actionTooltip : "the Find Familiar spell" - }, - pact_of_the_chain : { - featurestring : "\u25C6 Pact of the Chain: If dropped to 0 HP, the familiar disappears, leaving behind no physical form. It must obey all commands of the master.", - string : "Pact of the Chain (variant of the Find Familiar 1st-level conjuration [ritual] spell, PHB 240):" + - "\n\u2022 " + "Summon a spirit that serves as a familiar, appearing in an unoccupied space within 10 ft" + - "\n " + "It assumes a chosen form (can change at every casting): bat, cat, crab, frog (toad), hawk," + - "\n " + "lizard, octopus, owl, poisonous snake, fish (quipper), rat, raven, sea horse, spider, weasel," + - "\n " + "or one of the special forms: imp, pseudodragon, quasit, or sprite." + - "\n " + "It has the chosen form's statistics, but its type changes from beast to celestial, fey, or fiend" + - "\n " + "When the familiar drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n " + "It reappears when I cast this spell again (in a new form if so desired)" + - "\n\u2022 " + "The familiar acts independently of me, but it always obeys my commands" + - "\n " + "In combat, it rolls its own initiative and acts on its own turn, but it can't attack on its turn" + - "\n\u2022 " + "While it is within 100 ft of me, I can communicate with it telepathically" + - "\n\u2022 " + "With my Attack action, I can forgo one attacks to have the familiar make one with its reaction" + - "\n\u2022 " + "As an action, I see/hear what it does (but not with my senses) until the start of my next turn" + - "\n\u2022 " + "As an action, I can temporarily dismiss it, having it disappears into a pocket dimension" + - "\n\u2022 " + "As an action, while it is temporarily dismissed, I can cause it to reappear within 30 ft" + - "\n\u2022 " + "I can't have more than one familiar bonded at a time; as an action, I can dismiss it forever" + - "\n\u2022 " + "When I cast a spell with a range of touch, my familiar can deliver the spell" + - "\n " + "It must be within 100 ft of me and it must use its reaction to deliver the spell when I cast it" + - "\n " + "It acts as if it cast the spells, but it can use my modifiers for any attack rolls the spell requires", - actions : [["action", "Have familiar attack (part of my Attack action)"], ["action", "Familiar (dismiss/reappear)"], ["action", "Use familiar's senses"]], - actionTooltip : "Warlock (Pact of the Chain)" - }, - companion : { - featurestring : "", - string : "Ranger's Companion (PHB 93):" + - "\n\u2022 " + "A beast no larger than medium of challenge rating 1/4 or lower" + - "\n\u2022 " + "If the beast dies, I can spend 8 hours magically bonding with another that isn't hostile to me" + - "\n\u2022 " + "When moving in favored terrain with only the beast, I can move stealthily at a normal pace" + - "\n\u2022 " + "The beast adds my proficiency bonus to its AC, attack rolls, damage rolls," + - "\n " + "as well as to any saving throws and skills it is proficient with." + - "\n\u2022 " + "The beast's Hit Point maximum equals four times my ranger level if higher than its normal HP" + - "\n\u2022 " + "The beast takes its turn on my initiative" + - "\n\u2022 " + "I can verbally command the beast where to move (no action)" + - "\n\u2022 " + "As an action, I can have the beast do an Attack, Dash, Disengage, or Help action on its turn" + - "\n\u2022 " + "If I don't command it to take an action, it takes the Dodge action instead", - actions : [["action", "Command Companion"]], - calcChanges : { - hp : function (totalHD, HDobj, prefix) { - var classTxt = "", useLvl, totHP, strHp; - if (!classes.known.ranger && !classes.known["spell-less ranger"]) { - useLvl = classes.totallevel; - } else { - classTxt = "ranger "; - useLvl = (classes.known.ranger ? classes.known.ranger.level : 0) + (classes.known["spell-less ranger"] ? classes.known["spell-less ranger"].level : 0); - } - var rngrCompHp = 4 * useLvl; - var rngrCompHpStr = " 4 \xD7 " + useLvl + " from four times my " + classTxt + "level (" + rngrCompHp + ")"; - if (!CurrentCompRace[prefix] || CurrentCompRace[prefix].typeFound !== "creature") { - totHP = rngrCompHp; - strHp = " =" + rngrCompHpStr; - } else { - var creaHp = CurrentCompRace[prefix] && CurrentCompRace[prefix].hp ? CurrentCompRace[prefix].hp : 0; - var creaName = CurrentCompRace[prefix] && CurrentCompRace[prefix].name ? CurrentCompRace[prefix].name.toLowerCase() : "creature"; - totHP = Math.max(rngrCompHp, creaHp); - strHp = " = the highest of either:\n I. " + creaHp + " from a " + creaName + "'s normal maximum HP\n II." + rngrCompHpStr; - if (HDobj.alt.length) { - // there are already other alternate HP calculations, so use them if higher - totHP = Math.max.apply(Math, HDobj.alt.concat(totHP)); - strHp += "\n III. Other alternative hit point calculation(s) (" + totHP + ")"; - } - } - HDobj.alt.push(totHP); - HDobj.altStr.push(strHp); - }, - setAltHp : true - } - }, - companionrr : { - featurestring : "", - string : "Ranger's Animal Companion (UA:RR 5):" + - "\n\u2022 " + "Call forth and bond with an animal from the wilderness by spending 8 hours and 50 gp" + - "\n\u2022 " + "The animal can be an ape, black bear, boar, giant badger, giant weasel, mule, panther, or wolf" + - "\n\u2022 " + "I can have one companion at a time; If it dies, I can spend 8 hours and 25 gp to bring it back" + - "\n\u2022 " + "My companion uses my Proficiency Bonus instead of its own and also adds it to AC & damage" + - "\n\u2022 " + "My companion gains a Hit Dice for every ranger level I gain after 3rd" + - "\n\u2022 " + "My companion can divide 2 points among its ability scores (to max 20) whenever I gain an ASI" + - "\n\u2022 " + "My companion is proficient in two skills of my choice, as well as all saving throws" + - "\n\u2022 " + "My companion obeys my commands as best it can, or act on its own if I can't command it" + - "\n\u2022 " + "My companion rolls for initiative and takes actions as normal, but can't use Multiattack" + - "\n\u2022 " + "When moving stealthily together with only my companion, we can move at a normal pace" + - "\n\u2022 " + "My companion gains a bonus on damage rolls against my favored enemies just like me", - actions : [] - }, - mechanicalserv : { - featurestring : "", - string : "Artificer's Mechanical Servant (UA:A 4):" + - "\n\u2022 " + "The mechanical servant has the statistics of a chosen large beast of challenge rating 2 or lower" + - "\n " + "It has the Construct type, understands any language that I know, and has 60 ft Darkvision" + - "\n " + "In addition, it is immune to poison damage, being poisoned, and being charmed" + - "\n\u2022 " + "I can have one servant at a time; If it dies, I can repair it or create a new one" + - "\n " + "I can repair the servant over the course of a long rest, which restores it to 1 HP" + - "\n " + "I can build a new servant by spending 8 hours a day for 7 days and 1000 gp of materials" + - "\n\u2022 " + "The servant rolls initiative and takes actions as normal, obeying my commands as best it can" + - "\n\u2022 " + "As a reaction when I am attacked in melee and my mechanical servant is within 5 ft of me," + - "\n I can command the servant to use its reaction to make a melee attack against the attacker", - actions : [["reaction", "Mechanical Servant (if attacked)"]] - } -}; - //list of recommended fonts and there size to use var FontList = { "SegoePrint" : !typePF ? 5.74 : 6.3, diff --git a/_variables/ListsClasses.js b/_variables/ListsClasses.js index a44ad350..3ed44b9f 100644 --- a/_variables/ListsClasses.js +++ b/_variables/ListsClasses.js @@ -2736,7 +2736,7 @@ var Base_ClassSubList = { name : "Remarkable Athlete", source : [["SRD", 25], ["P", 72]], minlevel : 7, - description : "\n " + "I add half my proficiency bonus to Str/Dex/Con checks if I would otherwise add none" + "\n " + "When making running jumps, I add my Strength modifier to the distance in feet", + description : "\n " + "I add half my proficiency bonus to Str/Dex/Con checks if I would otherwise add none" + "\n " + "When making running long jumps, I add my Strength modifier to the distance in feet", eval : function() { Checkbox('Remarkable Athlete', true); }, removeeval : function() { Checkbox('Remarkable Athlete', false); } }, diff --git a/_variables/ListsCompanions.js b/_variables/ListsCompanions.js index 5b993668..52ad91f2 100644 --- a/_variables/ListsCompanions.js +++ b/_variables/ListsCompanions.js @@ -1,136 +1,358 @@ var Base_CompanionList = { "familiar" : { - name : "familiar (Find Familiar spell)" - } -}; - - -var compString = { - mount : { - featurestring : "\u25C6 Find Steed: If dropped to 0 HP, the steed disappears, leaving behind no physical form", - string : "Find Steed (2nd-level conjuration spell, PHB 240):" + - "\n\u2022 " + "Summon a spirit in the form of a steed, appearing in an unoccupied space within 30 ft" + - "\n " + "It assumes a chosen form: warhorse, pony, camel, elk, or mastiff (DM can allow more forms)" + - "\n " + "The steed has the statistics of the chosen form, though its type is celestial, fey, or fiend" + - "\n " + "If it has an Intelligence of 5 or less, its Intelligence becomes 6 " + - "\n " + "It gains the ability to understand one language that I, the caster, can speak" + - "\n " + "When the steed drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n\u2022 " + "The steed serves me as a mount. I have a bond with it that allows us to fight as a seamless unit" + - "\n\u2022 " + "While mounted on my steed, I can make any spell I cast that targets only me also target it" + - "\n\u2022 " + "While my steed is within 1 mile of me, we can communicate with each other telepathically" + - "\n\u2022 " + "I can dismiss my steed at any time as an action, causing it to disappear" + - "\n\u2022 " + "Casting this spell again summons the same steed, restored to its max HP, without conditions" + - "\n\u2022 " + "I can't have more than one steed bonded at a time; as an action, I can release it from its bond", - actions : [["action", "Find Steed (dismiss)"]], - actionTooltip : "the Find Steed spell" - }, - steed : { - featurestring : "\u25C6 Find Greater Steed: If dropped to 0 HP, the steed disappears, leaving behind no physical form", - string : "Find Greater Steed (4th-level conjuration spell, XGtE 156):" + - "\n\u2022 " + "Summon a spirit in the form of a steed, appearing in an unoccupied space within 30 ft" + - "\n " + "It has the chosen form: griffon, pegasus, peryton, dire wolf, rhinoceros, or saber-toothed tiger" + - "\n " + "The steed has the statistics of the chosen form, though its type is celestial, fey, or fiend" + - "\n " + "If it has an Intelligence of 5 or less, its Intelligence becomes 6 " + - "\n " + "It gains the ability to understand one language that I, the caster, can speak" + - "\n " + "When the steed drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n\u2022 " + "The steed serves me as a mount. I have a bond with it that allows us to fight as a seamless unit" + - "\n\u2022 " + "While mounted on my steed, I can make any spell I cast that targets only me also target it" + - "\n\u2022 " + "While my steed is within 1 mile of me, I can communicate with it telepathically" + - "\n\u2022 " + "I can dismiss my steed at any time as an action, causing it to disappear" + - "\n\u2022 " + "Casting this spell again summons the same steed, restored to its max HP, without conditions" + - "\n\u2022 " + "I can't have more than one steed bonded at a time; as an action, I can release it from its bond", - actions : [["action", "Find Greater Steed (dismiss)"]], - actionTooltip : "the Find Greater Steed spell" + name : "Find Familiar", + nameTooltip : "the Find Familiar spell", + nameOrigin : "1st-level conjuration [ritual] spell", + nameMenu : "Familiar (Find Familiar spell)", // required + source : [["SRD", 143], ["P", 240]], // required + includeCheck : function(sCrea, objCrea, iCreaCR) { + // return true if to be included, or a string to add a note to the menu option + return !!isDisplay("DCI.Text") && objCrea.companion === "familiar_not_al" ? " (if DM approves)" : false; + }, + action : [ + ["action", "Familiar (dismiss/reappear)"], + ["action", "Use familiar's senses"] + ], + notes : [{ + name : "Summon a spirit that serves as a familiar", + description : [ + "appearing in an unoccupied space within 10 ft", + "It assumes a chosen form (can change at every casting): bat, cat, crab, frog (toad), hawk,", + "lizard, octopus, owl, poisonous snake, fish (quipper), rat, raven, sea horse, spider, or weasel.", + "It has the chosen form's statistics, but its type changes from beast to celestial, fey, or fiend", + "When the familiar drops to 0 hit points, it disappears, leaving behind no physical form", + "It reappears when I cast this spell again (in a new form if so desired)" + ].join("\n "), + joinString : ", " + }, { + name : "The familiar acts independently of me", + description : [ + "but it always obeys my commands", + "In combat, it rolls its own initiative and acts on its own turn, but it can't attack" + ].join("\n "), + joinString : ", " + }, { + name : "While it is within 100 ft of me", + description : "I can communicate with it telepathically", + joinString : ", " + }, { + name : "As an action, I see/hear what it does", + description : " (but not with my senses) until the start of my next turn", + joinString : "" + }, { + name : "As an action, I can temporarily dismiss it", + description : "having it disappear into a pocket dimension", + joinString : ", " + }, { + name : "As an action, while it is temporarily dismissed", + description : "I can cause it to reappear within 30 ft", + joinString : ", " + }, { + name : "I can't have more than one familiar bonded at a time", + description : "As an action, I can dismiss it forever", + joinString : "; " + }, { + name : "When I cast a spell with a range of touch", + description : [ + "my familiar can deliver the spell", + "It must be within 100 ft of me and it must use its reaction to deliver the spell when I cast it", + "It acts as if it cast the spell, but it can use my modifiers for any attack rolls the spell requires" + ].join("\n "), + joinString : ", " + }], + attributesAdd : { + header : "Familiar", + features : [{ + name : "Find Familiar", + description : "If dropped to 0 HP, the familiar disappears, leaving behind no physical form. The familiar must obey all commands of its master." + }] + }, + attributesChange : function(sCrea, objCrea) { + // can't do any attacks + objCrea.attacks = []; + if (objCrea.type.toLowerCase() === "beast") { + objCrea.type = ["Celestial", "Fey", "Fiend"]; + objCrea.subtype = ""; + } + }, }, - familiar : { - featurestring : "\u25C6 Find Familiar: If dropped to 0 HP, the familiar disappears, leaving behind no physical form. The familiar must obey all commands of the master", - string : "Find Familiar (1st-level conjuration [ritual] spell, PHB 240):" + - "\n\u2022 " + "Summon a spirit that serves as a familiar, appearing in an unoccupied space within 10 ft" + - "\n " + "It assumes a chosen form (can change at every casting): bat, cat, crab, frog (toad), hawk," + - "\n " + "lizard, octopus, owl, poisonous snake, fish (quipper), rat, raven, sea horse, spider, or weasel." + - "\n " + "It has the chosen form's statistics, but its type changes from beast to celestial, fey, or fiend" + - "\n " + "When the familiar drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n " + "It reappears when I cast this spell again (in a new form if so desired)" + - "\n\u2022 " + "The familiar acts independently of me, but it always obeys my commands" + - "\n " + "In combat, it rolls its own initiative and acts on its own turn, but it can't attack" + - "\n\u2022 " + "While it is within 100 ft of me, I can communicate with it telepathically" + - "\n\u2022 " + "As an action, I see/hear what it does (but not with my senses) until the start of my next turn" + - "\n\u2022 " + "As an action, I can temporarily dismiss it, having it disappears into a pocket dimension" + - "\n\u2022 " + "As an action, while it is temporarily dismissed, I can cause it to reappear within 30 ft" + - "\n\u2022 " + "I can't have more than one familiar bonded at a time; as an action, I can dismiss it forever" + - "\n\u2022 " + "When I cast a spell with a range of touch, my familiar can deliver the spell" + - "\n " + "It must be within 100 ft of me and it must use its reaction to deliver the spell when I cast it" + - "\n " + "It acts as if it cast the spell, but it can use my modifiers for any attack rolls the spell requires", - actions : [["action", "Find Familiar (dismiss/reappear)"], ["action", "Use familiar's senses"]], - actionTooltip : "the Find Familiar spell" + "pact_of_the_chain" : { + name : "Pact of the Chain", + nameTooltip : "Warlock (Pact of the Chain)", + nameOrigin : "variant of the Find Familiar 1st-level conjuration [ritual] spell", + nameMenu : "Pact of the Chain familiar (Warlock feature)", + source : [["SRD", 47], ["P", 107]], + includeCheck : function(sCrea, objCrea, iCreaCR) { + // return true if to be included, or a string to add a note to the menu option + return objCrea.companion === "familiar" ? true : !!isDisplay("DCI.Text") && objCrea.companion === "familiar_not_al" ? " (if DM approves)" : false; + }, + action : [ + ["action", "Familiar (dismiss/reappear)"], + ["action", "Use familiar's senses"] + ], + attributesAdd : { + header : "Familiar", + features : [{ + name : "Pact of the Chain", + description : "If dropped to 0 HP, the familiar disappears, leaving behind no physical form. The familiar must obey all commands of its master." + }] + }, + attributesChange : function(sCrea, objCrea) { + if (objCrea.type.toLowerCase() === "beast") { + objCrea.type = ["Celestial", "Fey", "Fiend"]; + objCrea.subtype = ""; + } + } }, - pact_of_the_chain : { - featurestring : "\u25C6 Pact of the Chain: If dropped to 0 HP, the familiar disappears, leaving behind no physical form. It must obey all commands of the master", - string : "Pact of the Chain (variant of the Find Familiar 1st-level conjuration [ritual] spell, PHB 240):" + - "\n\u2022 " + "Summon a spirit that serves as a familiar, appearing in an unoccupied space within 10 ft" + - "\n " + "It assumes a chosen form (can change at every casting): bat, cat, crab, frog (toad), hawk," + - "\n " + "lizard, octopus, owl, poisonous snake, fish (quipper), rat, raven, sea horse, spider, weasel," + - "\n " + "or one of the special forms: imp, pseudodragon, quasit, or sprite." + - "\n " + "It has the chosen form's statistics, but its type changes from beast to celestial, fey, or fiend" + - "\n " + "When the familiar drops to 0 hit points, it disappears, leaving behind no physical form" + - "\n " + "It reappears when I cast this spell again (in a new form if so desired)" + - "\n\u2022 " + "The familiar acts independently of me, but it always obeys my commands" + - "\n " + "In combat, it rolls its own initiative and acts on its own turn, but it can't attack on its turn" + - "\n\u2022 " + "While it is within 100 ft of me, I can communicate with it telepathically" + - "\n\u2022 " + "With my Attack action, I can forgo one attacks to have the familiar make one with its reaction" + - "\n\u2022 " + "As an action, I see/hear what it does (but not with my senses) until the start of my next turn" + - "\n\u2022 " + "As an action, I can temporarily dismiss it, having it disappears into a pocket dimension" + - "\n\u2022 " + "As an action, while it is temporarily dismissed, I can cause it to reappear within 30 ft" + - "\n\u2022 " + "I can't have more than one familiar bonded at a time; as an action, I can dismiss it forever" + - "\n\u2022 " + "When I cast a spell with a range of touch, my familiar can deliver the spell" + - "\n " + "It must be within 100 ft of me and it must use its reaction to deliver the spell when I cast it" + - "\n " + "It acts as if it cast the spells, but it can use my modifiers for any attack rolls the spell requires", - actions : [["action", "Have familiar attack (part of my Attack action)"], ["action", "Familiar (dismiss/reappear)"], ["action", "Use familiar's senses"]], - actionTooltip : "Warlock (Pact of the Chain)" + "mount" : { + name : "Find Steed", + nameTooltip : "the Find Steed spell", + nameOrigin : "2nd-level conjuration spell", + nameMenu : "Steed (Find Steed spell)", + source : [["SRD", 143], ["P", 240]], + action : [["action", "Find Steed (dismiss)"]], + notes : [{ + name : "Summon a spirit in the form of a steed", + description : [ + "appearing in an unoccupied space within 30 ft", + "It assumes a chosen form: warhorse, pony, camel, elk, or mastiff (DM can allow more forms)", + "The steed has the statistics of the chosen form, though its type is celestial, fey, or fiend", + "If it has an Intelligence of 5 or less, its Intelligence becomes 6 ", + "It gains the ability to understand one language that I, the caster, can speak", + "When the steed drops to 0 hit points, it disappears, leaving behind no physical form" + ].join("\n "), + joinString : ", " + }, { + name : "The steed serves me as a mount", + description : "I have a bond with it that allows us to fight as a seamless unit", + joinString : ". " + }, { + name : "While mounted on my steed", + description : "I can make any spell I cast that targets only me also target it", + joinString : ", " + }, { + name : "While my steed is within 1 mile of me", + description : "I can communicate with it telepathically", + joinString : ", " + }, { + name : "I can dismiss my steed at any time as an action", + description : "causing it to disappear", + joinString : ", " + }, { + name : "Casting this spell again", + description : "summons the same steed, restored to its max HP, without conditions", + joinString : " " + }, { + name : "I can't have more than one steed bonded at a time", + description : "As an action, I can release it from its bond", + joinString : "; " + }], + attributesAdd : { + header : "Mount", + type : ["Celestial", "Fey", "Fiend"], + subtype : "", + features : [{ + name : "Find Steed", + description : "If dropped to 0 HP, the steed disappears, leaving behind no physical form." + }], + languages : "understands one language its master speaks (master's choice)" + }, + attributesChange : function(sCrea, objCrea) { + if (objCrea.scores[3] < 6) objCrea.scores[3] = 6; + } }, - companion : { - featurestring : "", - string : "Ranger's Companion (PHB 93):" + - "\n\u2022 " + "A beast no larger than medium of challenge rating 1/4 or lower" + - "\n\u2022 " + "If the beast dies, I can spend 8 hours magically bonding with another that isn't hostile to me" + - "\n\u2022 " + "When moving in favored terrain with only the beast, I can move stealthily at a normal pace" + - "\n\u2022 " + "The beast adds my proficiency bonus to its AC, attack rolls, damage rolls," + - "\n " + "as well as to any saving throws and skills it is proficient with." + - "\n\u2022 " + "The beast's Hit Point maximum equals four times my ranger level if higher than its normal HP" + - "\n\u2022 " + "The beast takes its turn on my initiative" + - "\n\u2022 " + "I can verbally command the beast where to move (no action)" + - "\n\u2022 " + "As an action, I can have the beast do an Attack, Dash, Disengage, or Help action on its turn" + - "\n\u2022 " + "If I don't command it to take an action, it takes the Dodge action instead", - actions : [] - }, - companionrr : { - featurestring : "", - string : "Ranger's Animal Companion (UA:RR 5):" + - "\n\u2022 " + "Call forth and bond with an animal from the wilderness by spending 8 hours and 50 gp" + - "\n\u2022 " + "The animal can be an ape, black bear, boar, giant badger, giant weasel, mule, panther, or wolf" + - "\n\u2022 " + "I can have one companion at a time; If it dies, I can spend 8 hours and 25 gp to bring it back" + - "\n\u2022 " + "My companion uses my Proficiency Bonus instead of its own and also adds it to AC & damage" + - "\n\u2022 " + "My companion gains a Hit Dice for every ranger level I gain after 3rd" + - "\n\u2022 " + "My companion can divide 2 points among its ability scores (to max 20) whenever I gain an ASI" + - "\n\u2022 " + "My companion is proficient in two skills of my choice, as well as all saving throws" + - "\n\u2022 " + "My companion obeys my commands as best it can, or act on its own if I can't command it" + - "\n\u2022 " + "My companion rolls for initiative and takes actions as normal, but can't use Multiattack" + - "\n\u2022 " + "When moving stealthily together with only my companion, we can move at a normal pace" + - "\n\u2022 " + "My companion gains a bonus on damage rolls against my favored enemies just like me", - actions : [] - }, - mechanicalserv : { - featurestring : "", - string : "Artificer's Mechanical Servant (UA:A 4):" + - "\n\u2022 " + "The mechanical servant has the statistics of a chosen large beast of challenge rating 2 or lower" + - "\n " + "It has the Construct type, understands any language that I know, and has 60 ft Darkvision" + - "\n " + "In addition, it is immune to poison damage, being poisoned, and being charmed" + - "\n\u2022 " + "I can have one servant at a time; If it dies, I can repair it or create a new one" + - "\n " + "I can repair the servant over the course of a long rest, which restores it to 1 HP" + - "\n " + "I can build a new servant by spending 8 hours a day for 7 days and 1000 gp of materials" + - "\n\u2022 " + "The servant rolls initiative and takes actions as normal, obeying my commands as best it can" + - "\n\u2022 " + "As a reaction when I am attacked in melee and my mechanical servant is within 5 ft of me," + - "\n I can command the servant to use its reaction to make a melee attack against the attacker", - actions : [["reaction", "Mechanical Servant (if attacked)"]] + "companion" : { + name : "Ranger's Companion", + nameTooltip : "Beast Master: Ranger's Companion", + nameOrigin : "Beast Master 3", + nameMenu : "Ranger's Companion (Beast Master feature)", + source : [["P", 93]], + includeCheck : function(sCrea, objCrea, iCreaCR) { + return objCrea.type.toLowerCase() === "beast" && objCrea.size >= 3 && iCreaCR <= 1/4 ? true : false; + }, + action : [["action", "Ranger's Companion (command)"]], + notes : [{ + name : "A beast no larger than Medium", + description : "of challenge rating 1/4 or lower", + joinString : " " + }, { + name : "If the beast dies", + description : "I can spend 8 hours magically bonding with another that isn't hostile to me", + joinString : ", " + }, { + name : "When moving in favored terrain with only the beast", + description : "we can move stealthily at a normal pace", + joinString : ", " + }, { + name : "The beast adds my proficiency bonus", + description : typePF ? "to its AC, attack rolls, damage rolls, and saves/skills it is proficient with" : [ + "to its AC, attack rolls, damage rolls,", + "as well as to any saving throws and skills it is proficient with." + ].join("\n "), + joinString : " " + }, { + name : "The beast's hit point maximum equals", + description : "four times my ranger level if higher than its normal HP", + joinString : " " + }, { + name : "In Combat", + description : [ + "The beast takes its turn on my initiative", + "I can verbally command the beast where to move (no action)", + "As an action, I can have the beast do an Attack, Dash, Disengage, or Help action on its turn", + "If I don't command it to take an action, it takes the Dodge action instead" + ].join("\n "), + joinString : typePF ? ": " : ":\n " + }, { + name : "Extra Attack (Ranger 5, PHB 89)", + description : "If the beast takes the Attack action, I can use my Extra Attack feature to attack once myself", + joinString : "\n ", + minlevel : 5 + }, { + name : "Exceptional Training (Beast Master 7, PHB 93)", + description : [ + "The beast's attacks count as magical for the purpose of overcoming resistances and immunities", + "As a bonus action, I can command it to take the Dash, Disengage, or Help action on its turn" + ].join("\n "), + joinString : "\n ", + minlevel : 7, + eval : function(prefix, lvl) { + for (var i = 1; i <= 3; i++) { + if (!What(prefix + "Comp.Use.Attack." + i + ".Weapon Selection")) continue; + var sDescrFld = prefix + "Comp.Use.Attack." + i + ".Description"; + if (!/(,|;)? ?counts as magical/i.test(What(sDescrFld))) { + AddString(sDescrFld, "Counts as magical", "; "); + }; + } + processActions(true, "Beast Master: Ranger's Companion", [["bonus action", "Exceptional Training (Dash/Disengage/Help)"]], "Ranger's Companion"); + }, + removeeval : function(prefix, lvl) { + for (var i = 1; i <= 3; i++) { + var sDescrFld = prefix + "Comp.Use.Attack." + i + ".Description"; + var sDescr = What(sDescrFld); + var rCaM = /(,|;)? ?counts as magical/i; + if (rCaM.test(sDescr)) Value(sDescrFld, sDescr.replace(rCaM, '')); + } + processActions(false, "Beast Master: Ranger's Companion", [["bonus action", "Exceptional Training (Dash/Disengage/Help)"]], "Ranger's Companion"); + } + }, { + name : "Bestial Fury (Beast Master 11, PHB 93)", + description : "The beast can make two attacks (or multiattack) when I command it to take an Attack action", + joinString : "\n ", + minlevel : 11, + eval : function(prefix, lvl) { + Value(prefix + "Comp.Use.Attack.perAction", 2); + }, + removeeval : function(prefix, lvl) { + Value(prefix + "Comp.Use.Attack.perAction", 1); + } + }, { + name : "Share Spells (Beast Master 15, PHB 93)", + description : "When I cast a spell on myself, I can have it also affect the beast if it is within 30 ft of me", + joinString : "\n ", + minlevel : 15 + }], + attributesAdd : { + header : "Companion", + minlevelLinked : ["ranger", "rangerua", "spell-less ranger"], + attacksAction : 1 + }, + attributesChange : function(sCrea, objCrea) { + // Add oProf to attacks to hit and damage + for (var i = 0; i < objCrea.attacks.length; i++) { + var oAtk = objCrea.attacks[i]; + if (!oAtk.modifiers) { + oAtk.modifiers = ["oProf", "oProf"]; + } else { + oAtk.modifiers[0] += "+oProf"; + oAtk.modifiers[1] += "+oProf"; + } + if (oAtk.description) { // Remove multiattack + oAtk.description = oAtk.description.replace(/(((One|Two|2).+as an Attack action)|(2 per Attack));? ?/i, ''); + } + }; + // Change multiattack trait/feature/action to level 11 feature + ["traits", "features", "actions"].forEach(function (n) { + if (!objCrea[n]) return; + for (var i = 0; i < objCrea[n].length; i++) { + var oN = objCrea[n][i]; + if (oN.name && /multiattack/i.test(oN.name)) { + objCrea[n][i].minlevel = 11; + } + } + }) + }, + changeeval : function (prefix, lvl) { + var sNameEntity = "Ranger's Companion"; + var sExplanation = "A ranger's companion adds its master's proficiency bonus (oProf) to its AC, all skills and saving throws it is proficient with, and the to hit and damage of its attacks."; + // Add oProf to the AC, if not already present + var sACfld = prefix + "Comp.Use.AC"; + if (lvl[0] === 0 && What(sACfld).indexOf("oProf") === -1) { + AddToModFld(sACfld, "oProf", false, sNameEntity, sExplanation); + } + // Add oProf to proficient Saving Throws / Skills and remove where no longer proficient + var processFld = function(sType, sFld, sModFld) { + var bIsProf = sType === "skill" && !typePF ? What(sFld) !== "nothing" : tDoc.getField(sFld).isBoxChecked(0); + var boProfMod = What(sModFld).indexOf("oProf") !== -1; + if ((!bIsProf && boProfMod) || (bIsProf && !boProfMod)) { + AddToModFld(sModFld, "oProf", !bIsProf, sNameEntity, sExplanation); + } + } + // Loop through the saves and process them + for (var i = 0; i < AbilityScores.abbreviations.length; i++) { + var sFld = prefix + "Comp.Use.Ability." + AbilityScores.abbreviations[i] + ".ST.Prof"; + if (!tDoc.getField(sFld)) continue; + var sModFld = prefix + "BlueText.Comp.Use.Ability." + AbilityScores.abbreviations[i] + ".ST.Bonus"; + processFld("save", sFld, sModFld); + } + // Loop through the skills and process them + for (var i = 0; i < SkillsList.abbreviations.length; i++) { + var sFld = prefix + (typePF ? "" : "Text.") + "Comp.Use.Skills." + SkillsList.abbreviations[i] + ".Prof"; + if (!tDoc.getField(sFld)) continue; + var sModFld = prefix + "BlueText.Comp.Use.Skills." + SkillsList.abbreviations[i] + ".Bonus"; + processFld("skill", sFld, sModFld); + } + }, + calcChanges : { + hp : function (totalHD, HDobj, prefix) { + var classTxt = "", useLvl, totHP, strHp; + if (!classes.known.ranger && !classes.known["spell-less ranger"] && !classes.known["rangerua"]) { + useLvl = classes.totallevel; + } else { + classTxt = "ranger "; + useLvl = (classes.known.ranger ? classes.known.ranger.level : 0) + (classes.known["spell-less ranger"] ? classes.known["spell-less ranger"].level : (classes.known.rangerua ? classes.known.rangerua.level : 0)); + } + var rngrCompHp = 4 * useLvl; + var rngrCompHpStr = " 4 \xD7 " + useLvl + " from four times my " + classTxt + "level (" + rngrCompHp + ")"; + if (!CurrentCompRace[prefix] || CurrentCompRace[prefix].typeFound !== "creature") { + totHP = rngrCompHp; + strHp = " =" + rngrCompHpStr; + } else { + var creaHp = CurrentCompRace[prefix] && CurrentCompRace[prefix].hp ? CurrentCompRace[prefix].hp : 0; + var creaName = CurrentCompRace[prefix] && CurrentCompRace[prefix].name ? CurrentCompRace[prefix].name.toLowerCase() : "creature"; + totHP = Math.max(rngrCompHp, creaHp); + strHp = " = the highest of either:\n I. " + creaHp + " from a " + creaName + "'s normal maximum HP\n II." + rngrCompHpStr; + if (HDobj.alt.length) { + // there are already other alternate HP calculations, so use them if higher + totHP = Math.max.apply(Math, HDobj.alt.concat(totHP)); + strHp += "\n III. Other alternative hit point calculation(s) (" + totHP + ")"; + } + } + HDobj.alt.push(totHP); + HDobj.altStr.push(strHp); + }, + setAltHp : true + } } -}; \ No newline at end of file +}; +Base_CompanionList.pact_of_the_chain.notes = function() { + var a = newObj(Base_CompanionList.familiar.notes); + a[0].description = a[0].description.replace("or weasel.", "weasel,\n or one of the special forms: imp, pseudodragon, quasit, or sprite."); + a[1].description = a[1].description.replace("but it can't attack", "but it can't attack on its turn"); + a.splice(3, 0, { + name : "With my Attack action", + description : "I can forgo one attack to have the familiar make one with its reaction", + joinString : ", " + }); + return a; +}(); diff --git a/_variables/ListsCreatures.js b/_variables/ListsCreatures.js index ba13f277..8d758587 100644 --- a/_variables/ListsCreatures.js +++ b/_variables/ListsCreatures.js @@ -313,6 +313,10 @@ var Base_CreatureList = { range : "Melee (5 ft)", description : "One claws and one beak attack as an Attack action" }], + actions : [{ + name : "Multiattack", + description : "The griffon makes two attacks: one with its beak and one with its claws." + }], traits : [{ name : "Keen Sight", description : "The griffon has advantage on Wisdom (Perception) checks that rely on sight." @@ -350,6 +354,10 @@ var Base_CreatureList = { range : "Melee (5 ft)", description : "One claws and one beak attack as an Attack action" }], + actions : [{ + name : "Multiattack", + description : "The hippogriff makes two attacks: one with its beak and one with its claws." + }], traits : [{ name : "Keen Sight", description : "The hippogriff has advantage on Wisdom (Perception) checks that rely on sight." @@ -424,6 +432,10 @@ var Base_CreatureList = { range : "Melee (5 ft)", description : "One claws and one beak attack as an Attack action" }], + actions : [{ + name : "Multiattack", + description : "The owlbear makes two attacks: one with its beak and one with its claws." + }], traits : [{ name : "Keen Sight and Smell", description : "The owlbear has advantage on Wisdom (Perception) checks that rely on sight or smell." @@ -1575,7 +1587,6 @@ var Base_CreatureList = { source : [["SRD", 366], ["M", 317]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 12, hp : 19, @@ -1603,6 +1614,10 @@ var Base_CreatureList = { damage : [1, 6, "bludgeoning"], range : "25/50 ft", description : "One rock attack as an Attack action" + }], + actions : [{ + name : "Multiattack", + description : "The ape makes two fist attacks." }] }, "axe beak" : { @@ -1725,7 +1740,6 @@ var Base_CreatureList = { source : [["SRD", 367], ["M", 318]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 11, hp : 19, @@ -1745,13 +1759,17 @@ var Base_CreatureList = { ability : 1, damage : [1, 6, "piercing"], range : "Melee (5 ft)", - description : "One bite and one claw attack as an Attack action" + description : "One bite and one claws attack as an Attack action" }, { - name : "Claw", + name : "Claws", ability : 1, damage : [2, 4, "slashing"], range : "Melee (5 ft)", - description : "One claw and one bite attack as an Attack action" + description : "One claws and one bite attack as an Attack action" + }], + actions : [{ + name : "Multiattack", + description : "The bear makes two attacks: one with its bite and one with its claws." }], traits : [{ name : "Keen Smell", @@ -1834,7 +1852,6 @@ var Base_CreatureList = { source : [["SRD", 368], ["M", 319]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 11, hp : 11, @@ -1886,13 +1903,17 @@ var Base_CreatureList = { ability : 1, damage : [1, 8, "piercing"], range : "Melee (5 ft)", - description : "One bite and one claw attack as an Attack action" + description : "One bite and one claws attack as an Attack action" }, { - name : "Claw", + name : "Claws", ability : 1, damage : [2, 6, "slashing"], range : "Melee (5 ft)", - description : "One claw and one bite attack as an Attack action" + description : "One claws and one bite attack as an Attack action" + }], + actions : [{ + name : "Multiattack", + description : "The bear makes two attacks: one with its bite and one with its claws." }], traits : [{ name : "Keen Smell", @@ -1985,13 +2006,17 @@ var Base_CreatureList = { ability : 1, damage : [1, 8, "piercing"], range : "Melee (5 ft)", - description : "One bite and one claw attack as an Attack action" + description : "One bite and one claws attack as an Attack action" }, { - name : "Claw", + name : "Claws", ability : 1, damage : [2, 6, "slashing"], range : "Melee (5 ft)", - description : "One claw and one bite attack as an Attack action" + description : "One claws and one bite attack as an Attack action" + }], + actions : [{ + name : "Multiattack", + description : "The bear makes two attacks: one with its bite and one with its claws." }], traits : [{ name : "Keen Smell", @@ -2121,6 +2146,10 @@ var Base_CreatureList = { description : "Two bite attacks as an Attack action; Target DC 12 Con save or diseased and poisoned", tooltip : "If the target of the death dog's bite attack is a creature, it must succeed on a DC 12 Constitution saving throw against disease or become poisoned until the disease is cured. Every 24 hours that elapse, the creature must repeat the saving throw, reducing its hit point maximum by 5 (1d10) on a failure. This reduction lasts until the disease is cured. The creature dies if the disease reduces its hit point maximum to 0." }], + actions : [{ + name : "Multiattack", + description : "The dog makes two bite attacks." + }], traits : [{ name : "Two-Headed", description : "The death dog has advantage on Wisdom (Perception) checks and on saving throws against being blinded, charmed, deafened, frightened, stunned, or knocked unconscious." @@ -2406,6 +2435,10 @@ var Base_CreatureList = { damage : [7, 6, "bludgeoning"], range : "50/100 ft", description : "One rock attack as an Attack action" + }], + actions : [{ + name : "Multiattack", + description : "The ape makes two fist attacks." }] }, "giant badger" : { @@ -2413,7 +2446,6 @@ var Base_CreatureList = { source : [["SRD", 373], ["M", 323]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 10, hp : 13, @@ -2438,6 +2470,10 @@ var Base_CreatureList = { range : "Melee (5 ft)", description : "One claws and one bite attack as an Attack action" }], + actions : [{ + name : "Multiattack", + description : "The badger makes two attacks: one with its bite and one with its claws." + }], traits : [{ name : "Keen Smell", description : "The badger has advantage on Wisdom (Perception) checks that rely on smell." @@ -2670,6 +2706,10 @@ var Base_CreatureList = { range : "Melee (5 ft)", description : "One talons and one beak attack as an Attack action" }], + actions : [{ + name : "Multiattack", + description : "The eagle makes two attacks: one with its beak and one with its talons." + }], traits : [{ name : "Keen Sight", description : "The eagle has advantage on Wisdom (Perception) checks that rely on sight." @@ -3279,6 +3319,10 @@ var Base_CreatureList = { description : "One talons and one beak attack as an Attack action", modifiers : [-1, "", ""] }], + actions : [{ + name : "Multiattack", + description : "The eagle makes two attacks: one with its beak and one with its talons." + }], traits : [{ name : "Keen Sight and Smell", description : "The vulture has advantage on Wisdom (Perception) checks that rely on sight or smell." @@ -3320,7 +3364,6 @@ var Base_CreatureList = { source : [["SRD", 381], ["M", 329]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 13, hp : 9, @@ -3737,7 +3780,6 @@ var Base_CreatureList = { source : [["SRD", 384], ["M", 333]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 10, hp : 11, @@ -3847,7 +3889,6 @@ var Base_CreatureList = { source : [["SRD", 385], ["M", 333]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 12, hp : 13, @@ -3935,13 +3976,17 @@ var Base_CreatureList = { ability : 1, damage : [1, 8, "piercing"], range : "Melee (5 ft)", - description : "One bite and one claw attack as an Attack action" + description : "One bite and one claws attack as an Attack action" }, { name : "Claw", ability : 1, damage : [2, 6, "slashing"], range : "Melee (5 ft)", - description : "One claw and one bite attack as an Attack action" + description : "One claws and one bite attack as an Attack action" + }], + actions : [{ + name : "Multiattack", + description : "The bear makes two attacks: one with its bite and one with its claws." }], traits : [{ name : "Keen Smell", @@ -4462,7 +4507,6 @@ var Base_CreatureList = { source : [["SRD", 393], ["M", 341]], size : 3, //Medium type : "Beast", - companion : "companion", alignment : "Unaligned", ac : 13, hp : 11, diff --git a/_variables/ListsMagicItems.js b/_variables/ListsMagicItems.js index 4ac63666..6e79b468 100644 --- a/_variables/ListsMagicItems.js +++ b/_variables/ListsMagicItems.js @@ -1715,9 +1715,8 @@ var Base_MagicItemsList = { magicItemTable : "B", description : "As an action, I can sprinkle a pinch of dust over water, turning a 15-ft cube into a floating, marble-sized pellet. As an action, someone can smash the pellet against a hard surface, destroying it and releasing the absorbed water. A pinch of dust does 10d6 necrotic damage to a water elemental, Con save DC 13 halves.", descriptionFull : "This small packet contains 1d6+4 pinches of dust. You can use an action to sprinkle a pinch of it over water. The dust turns a cube of water 15 feet on a side into one marble-sized pellet, which floats or rests near where the dust was sprinkled. The pellet's weight is negligible.\n Someone can use an action to smash the pellet against a hard surface, causing the pellet to shatter and release the water the dust absorbed. Doing so ends that pellet's magic.\n An elemental composed mostly of water that is exposed to a pinch of the dust must make a DC 13 Constitution saving throw, taking 10d6 necrotic damage on a failed save, or half as much damage on a successful one.", - usages : 10, - recovery : "Never", - additional : "1d6+4 pinches" + usages : "1d6+4", + recovery : "Never" }, "dust of sneezing and choking" : { name : "Dust of Sneezing and Choking", @@ -2636,7 +2635,7 @@ var Base_MagicItemsList = { type : "wondrous item", rarity : "very rare", magicItemTable : "D", - description : "While all four shoes are affixed to the hooves of a creature, they allow it to move normally while floating 4 inches above the floor. the creature can cross or stand above liquid or unstable surfaces, leaves no tracks, ignores difficult terrain, and doesn't suffer exhaustion from moving at normal speed for 12 hours a day.", + description : "While all four shoes are affixed to the hooves of a creature, they allow it to move normally while floating 4 inches above the floor. The creature leaves no tracks, can cross or stand above liquid or unstable surfaces, ignores difficult terrain, and doesn't suffer exhaustion from moving at normal speed for 12 hours a day.", descriptionFull : "These iron horseshoes come in a set of four. While all four shoes are affixed to the hooves of a horse or similar creature, they allow the creature to move normally while floating 4 inches above the ground. This effect means the creature can cross or stand above nonsolid or unstable surfaces, such as water or lava. The creature leaves no tracks and ignores difficult terrain. In addition, the creature can move at normal speed for up to 12 hours a day without suffering exhaustion from a forced march." }, "horseshoes of speed" : { @@ -4148,7 +4147,7 @@ var Base_MagicItemsList = { description : "While I wear this ring, difficult terrain doesn't cost me extra movement. In addition, magic can neither reduce my speed nor cause me to be paralyzed or restrained.", descriptionFull : "While you wear this ring, difficult terrain doesn't cost you extra movement. In addition, magic can neither reduce your speed nor cause you to be paralyzed or restrained.", attunement : true, - savetxt : { immune : ["paralyzed", "restrained"] } + savetxt : { immune : ["paralyzed (by magic)", "restrained (by magic)"] } }, "ring of invisibility" : { name : "Ring of Invisibility", @@ -4406,7 +4405,7 @@ var Base_MagicItemsList = { description : "While wearing this ring, I have advantage on saves against any spell that targets only me (not in an area of effect). In addition, if I roll a 20 for the save and the spell is 7th level or lower, the spell has no effect on me and instead targets the caster as if the caster had effectively targeted itself.", descriptionFull : "While wearing this ring, you have advantage on saving throws against any spell that targets only you (not in an area of effect). In addition, if you roll a 20 for the save and the spell is 7th level or lower, the spell has no effect on you and instead targets the caster, using the slot level, spell save DC, attack bonus, and spellcasting ability of the caster.", attunement : true, - savetxt : { adv_vs : ["spells targeting only me"] } + savetxt : { adv_vs : ["spells (targeting only me)"] } }, "ring of swimming" : { name : "Ring of Swimming", @@ -6332,7 +6331,7 @@ var Base_MagicItemsList = { }, "wand of the war mage, +1, +2, or +3" : { name : "Wand of the War Mage, +1, +2, or +3", - nameTest : /^(?=.*(enhanced|war mage))(?=.*(arcane focus|rod|wand|staff)).*$/i, + nameTest : /^(?=.*war mage)(?=.*(arcane focus|rod|wand|staff)).*$/i, source : [["SRD", 249], ["D", 212]], type : "wand", description : "While I am holding this wand, I gain a bonus to spell attack rolls determined by the wand's rarity: uncommon (+1), rare (+2), or very rare (+3). In addition, I ignore half cover when making a spell attack.", @@ -6344,7 +6343,7 @@ var Base_MagicItemsList = { choices : ["+1 to spell attacks (uncommon)", "+2 to spell attacks (rare)", "+3 to spell attacks (very rare)"], "+1 to spell attacks (uncommon)" : { name : "Wand of the War Mage +1", - nameTest : /^(?=.*(enhanced|war mage))(?=.*(arcane focus|rod|wand|staff))(?=.*\+1)(?!.*\+2)(?!.*\+3).*$/i, + nameTest : /^(?=.*war mage)(?=.*(arcane focus|rod|wand|staff))(?=.*\+1)(?!.*\+2)(?!.*\+3).*$/i, rarity : "uncommon", magicItemTable : "F", description : "While I am holding this arcane focus, I gain a +1 bonus to spell attack rolls and I ignore half cover when making a spell attack.", @@ -6359,7 +6358,7 @@ var Base_MagicItemsList = { }, "+2 to spell attacks (rare)" : { name : "Wand of the War Mage +2", - nameTest : /^(?=.*(enhanced|war mage))(?=.*(arcane focus|rod|wand|staff))(?!.*\+1)(?=.*\+2)(?!.*\+3).*$/i, + nameTest : /^(?=.*war mage)(?=.*(arcane focus|rod|wand|staff))(?!.*\+1)(?=.*\+2)(?!.*\+3).*$/i, rarity : "rare", magicItemTable : "G", description : "While I am holding this arcane focus, I gain a +2 bonus to spell attack rolls and I ignore half cover when making a spell attack.", @@ -6374,7 +6373,7 @@ var Base_MagicItemsList = { }, "+3 to spell attacks (very rare)" : { name : "Wand of the War Mage +3", - nameTest : /^(?=.*(enhanced|war mage))(?=.*(arcane focus|rod|wand|staff))(?!.*\+1)(?!.*\+2)(?=.*\+3).*$/i, + nameTest : /^(?=.*war mage)(?=.*(arcane focus|rod|wand|staff))(?!.*\+1)(?!.*\+2)(?=.*\+3).*$/i, rarity : "very rare", magicItemTable : "H", description : "While I am holding this arcane focus, I gain a +3 bonus to spell attack rolls and I ignore half cover when making a spell attack.", diff --git a/_variables/ListsSources.js b/_variables/ListsSources.js index 34330605..073ee496 100644 --- a/_variables/ListsSources.js +++ b/_variables/ListsSources.js @@ -3,6 +3,7 @@ var Base_SourceList = { "SRD" : { name : "System Reference Document 5.1", abbreviation : "SRD", + abbreviationSpellsheet : "SR", group : "Primary Sources", url : "https://media.wizards.com/2016/downloads/DND/SRD-OGL_V5.1.pdf", date : "2016/05/04" @@ -17,6 +18,7 @@ var Base_SourceList = { "HB" : { name : "Homebrew", abbreviation : "Homebrew", + abbreviationSpellsheet : "HB", group : "default" }, "ALbackground" : { // For backwards-compatibility, but this source has been superseded by the "All AL-legal backgrounds/races" source below diff --git a/_variables/ListsSpells.js b/_variables/ListsSpells.js index 861e8477..dadacd54 100644 --- a/_variables/ListsSpells.js +++ b/_variables/ListsSpells.js @@ -889,6 +889,7 @@ var Base_SpellsList = { level : 3, school : "Abjur", time : "1 rea", + timeFull : "1 reaction, which you take when you see a creature within 60 feet of you casting a spell", range : "60 ft", components : "S", duration : "Instantaneous", @@ -1522,6 +1523,7 @@ var Base_SpellsList = { level : 1, school : "Trans", time : "1 rea", + timeFull : "1 reaction, which you take when you or a creature within 60 feet of you falls", range : "60 ft", components : "V,M", compMaterial : "A small feather or piece of down", @@ -2179,6 +2181,7 @@ var Base_SpellsList = { level : 1, school : "Evoc", time : "1 rea", + timeFull : "1 reaction, which you take in response to being damaged by a creature within 60 feet of you that you can see", range : "60 ft", components : "V,S", duration : "Instantaneous", @@ -3216,6 +3219,7 @@ var Base_SpellsList = { level : 3, school : "Trans", time : "1 a/8h", + timeFull : "1 action or 8 hours", range : "150 ft", components : "V,S", duration : "Instantaneous", @@ -3783,6 +3787,7 @@ var Base_SpellsList = { level : 1, school : "Abjur", time : "1 rea", + timeFull : "1 reaction, which you take when you are hit by an attack or targeted by the magic missile spell", range : "Self", components : "V,S", duration : "1 rnd", diff --git a/additional content syntax/_common attributes.js b/additional content syntax/_common attributes.js index 4d796755..161f8e3a 100644 --- a/additional content syntax/_common attributes.js +++ b/additional content syntax/_common attributes.js @@ -43,12 +43,13 @@ Race main attributes Race features Background main attributes + Background Features main attributes Feat main attributes Feat choices Magic Item main attributes Magic Item choices - Sheet: v13.0.9 and newer + Sheet: v13.1.0 and newer */ "example feature name" = { // you can ignore this, it is just here to make this file valid JavaScript @@ -578,26 +579,42 @@ savetxt : { Each string in the array is added to the 1st page, exactly as given here. */ - immune : ["poison", "disease"], + immune : ["poison", "disease", "paralyzed (by magic)"], /* immune // OPTIONAL // TYPE: array of strings USE: add strings to the "Immune to" text on the 1st page + CHANGE: v13.1.0 (conditional added) Each string in the array is added to the list of "Immune to" things in the 1st page "Saving Throws" section. Immunities from all sources are combined and listed alphabetically. - In this example it would result in "Immune to disease and poison". + In this example it would result in "Immune to disease, paralyzed (by magic), and poison". If a damage resistance is present while immunity for the same is also present, then the damage resistance will be hidden as long as the immunity is present. + + CONDITIONAL (since v13.1.0) + If the string contains something between brackets, then whatever is between those brackets is seen + as "conditional". This means that if another feature adds the same thing without brackets, only the + version without brackets will be shown. + Thus, using the example above, if something else adds "paralyzed", the entry "paralyzed (by magic)" + will be ignored. */ - adv_vs : ["traps", "charmed"] + adv_vs : ["traps", "charmed", "sleep (by magic)"] /* adv_vs // OPTIONAL // TYPE: array of strings USE: add strings to the "Adv. on saves vs." text on the 1st page + CHANGE: v13.1.0 (conditional added) Each string in the array is added to the list of "Adv. on saves vs." things in the 1st page "Saving Throws" section. Saving throw advantages from all sources are combined and listed alphabetically. - In this example it would result in "Adv. on saves vs. charmed and traps". + In this example it would result in "Adv. on saves vs. charmed, sleep (by magic), and traps". + + CONDITIONAL (since v13.1.0) + If the string contains something between brackets, then whatever is between those brackets is seen + as "conditional". This means that if another feature adds the same thing without brackets, only the + version without brackets will be shown. + Thus, using the example above, if something else adds "sleep", the entry "sleep (by magic)" + will be ignored. */ }, @@ -1337,14 +1354,16 @@ creaturesAdd : [ if (AddRemove) PickDropdown(prefix + "Comp.Desc.Size", 4); } ], - ["Purple Crawler"] + ["Purple Crawler"], + ["Cat", false, false, "familiar"] ], /* creatureOptions // OPTIONAL // TYPE: array of arrays (variable length) USE: adds a creature to a companion page (adds companion page if none empty) ADDED: v13.0.6 + CHANGE: v13.0.10 (added 4th array entry) - Each array must contain 1, 2, or 3 entries, which are the following: + Each array must contain 1, 2, 3, or 4 entries, which are the following: 1) String with the name of the race to add to the companion page // REQUIRED The sheet will search for the first empty companion page (or add an empty page) and add this entry in the "Race" drop-down box on that page. @@ -1380,7 +1399,29 @@ creaturesAdd : [ If you are adding a creature that is itself added using the `creatureOptions` attribute, consider not using this callback function, but adding an `eval` and `removeeval` to its CreatureList object instead. - + + 4) String of the special companion type that should be applied // OPTIONAL + This string has to be a key in the CompanionList object. + This companion type will be applied before the callback function above + (3rd array entry) is called. + + Import scripts can add things to the CompanionList object, but generally these + options should be available (if the applicable scripts are imported if they're not SRD): + + SRD OBJECT KEY EXPLANATION + V "familiar" Find Familiar spell + V "pact_of_the_chain" Pact of the Chain familiar (Warlock 3rd-level boon) + V "mount" Find Steed spell + - "steed" Find Greater Steed spell + - "companion" Ranger's Companion (Ranger: Beast Master feature) + - "strixhaven_mascot" Strixhaven Mascot familiar (Strixhaven Mascot feat) + - "companionrr" Animal Companion (2016/09/12 Unearthed Arcana: + Revised Ranger's Beast Conclave feature) + - "mechanicalserv" Mechanical Servant (2017/01/09 Unearthed Arcana: + Artificer's Mechanical Servant feature) + + If the string doesn't match a CompanionList object key, nothing will happen. + The changes dialog will list on which companion page something was added or removed. If a feature with this attribute is removed, these creatures will be removed as well. @@ -1975,6 +2016,7 @@ calcChanges : { This has to be a string and will be shown in the "Changes" dialog when this feature is added/removed. This explanation is also available under the Companion Options button on any companion page. + // 3rd array entry // OPTIONAL // This has to be a positive number that will be used to prioritise the order in which the functions are processed. The lowest number gets processed first. @@ -2019,17 +2061,22 @@ calcChanges : { 3) bAdd, a boolean to indicate whether adding (true) or removing (false) the special companion type. Make sure that the function removes whatever changes it does when this variable is false. - 4) sCompType, a string that indicates the special companion type. - This string is one of the predefined entries below, to help you filter on the companion type. - - STRING TYPE - "familiar" Find Familiar spell - "pact_of_the_chain" Pact of the Chain warlock boon - "mount" Find Steed spell - "steed" Find Greater Steed spell - "companion" Ranger: Beast Master's Ranger's Companion feature - "companionrr" 2016/09/12 Unearthed Arcana: Revised Ranger's Beast Conclave feature - "mechanicalserv" 2017/01/09 Unearthed Arcana: Artificer's Mechanical Servant feature + 4) sCompType, a string to indicate the special companion type (key in the CompanionList object). + + Import scripts can add things to the CompanionList object, but generally these + options should be available (if the applicable scripts are imported if they're not SRD): + + SRD OBJECT KEY EXPLANATION + V "familiar" Find Familiar spell + V "pact_of_the_chain" Pact of the Chain familiar (Warlock 3rd-level boon) + V "mount" Find Steed spell + - "steed" Find Greater Steed spell + - "companion" Ranger's Companion (Ranger: Beast Master feature) + - "strixhaven_mascot" Strixhaven Mascot familiar (Strixhaven Mascot feat) + - "companionrr" Animal Companion (2016/09/12 Unearthed Arcana: + Revised Ranger's Beast Conclave feature) + - "mechanicalserv" Mechanical Servant (2017/01/09 Unearthed Arcana: + Artificer's Mechanical Servant feature) When adding a special companion type, this function is processed last, after all changes for the special companion type have completed. diff --git a/additional content syntax/companion template option (CompanionList).js b/additional content syntax/companion template option (CompanionList).js new file mode 100644 index 00000000..8a4aecc9 --- /dev/null +++ b/additional content syntax/companion template option (CompanionList).js @@ -0,0 +1,552 @@ +/* -WHAT IS THIS?- + The script featured here is an explanation of how to make your own custom addition to MPMB's D&D 5e Character Tools. + To add your own content to the Character Sheet, use the syntax below and save it in a file. + You can then import this file directly to the sheet using the "Import" button and "Import/Export" bookmark. + There you can either import the file as a whole or just copy the text into a dialogue. + + -KEEP IN MIND- + Note that you can add as many custom codes as you want, either by importing consecutive files or pasting the scripts into the dialogue. + It is recommended to enter the code in a freshly downloaded sheet or to first reset sheet. + Thus you don't run the risk of things that have already been filled out causing conflicts. + + -HOW TO READ- + Every line comes with a comment immediately after it to show whether it is // Optional // or // Required //, + followed by a more explanatory comment + + -THIS IS JAVASCRIPT- + The imports scripts work by creating a new entry inside an existing object or by calling functions. + You can create new or overwrite existing global variables by omitting 'var'. + You will need to understand the basics of JavaScript variables: strings, arrays, and JSON objects. + Note that every opening symbol must have its closing counterpart: (), {}, [], "", ''. + If these are not present, the code will give an error when imported. + Use proper editing software for code (like Notepad++). Text processors like Microsoft Word will screw up your code. + To help finding syntax errors, use (online) code checking software like https://jshint.com + + -COMMENTS IN THE EXAMPLE- + Anything on a line after two forward slashes is a comment and will be ignored when running the code. + Multiline comments are possible. Open them using the forward slash followed by an asterisk and close them with the opposite. + The below contains a lot of these comments. The comments are not necessary for the script to work, so feel free to remove them. +*/ + +/* -INFORMATION- + + Subject: Companion template option + + Effect: This is the syntax for adding a new companion template option to the sheet, + for use on the companion pages in the Companion Options button on each page. + + Remarks: You will also need the syntax for adding a creature if you want to change certain + attributes of a creature, as they are only described there. + You will also need the syntax for common attributes for certain attributes, + as they are identical as described there and refer to that file. + + Sheet: v13.1.0 and newer + +*/ + +var iFileName = "Homebrew Syntax - CompanionList.js"; +/* iFileName // OPTIONAL // + TYPE: string + USE: how the file will be named in the sheet if you import it as a file + + Note that this is a variable called 'iFileName'. + Variables invoked inside an import script will not be available after importing. + However, if you invoke the variable without the 'var', it will be available after importing. + + This doesn't have to be the same as the actual name of the file. + This doesn't need to have the .js file extension. + Only the first occurrence of this variable will be used. +*/ + +RequiredSheetVersion("13.1.0"); +/* RequiredSheetVersion // OPTIONAL // + TYPE: function call with one variable, a string or number + USE: the minimum version of the sheet required for the import script to work + + If this script is imported into a sheet with an earlier version than given here, the player will be given a warning. + + The variable you input can be a the full semantic version of the sheet as a string (e.g. "13.0.6" or "13.1.0-beta1+201209"). + Alternatively, you can input a number, which the sheet will translate to a semantic version. + For example: + FUNCTION CALL REQUIRED MINIMUM VERSION + `RequiredSheetVersion(13);` 13.0.0 + `RequiredSheetVersion(13.1);` 13.1.0 + + You can find the full semantic version of the sheet at the bottom of every page, + or look at the "Get Latest Version" bookmark, which lists the version number, + or go to File >> Properties >> Description, where the version is part of the document title. +*/ + +CompanionList["purple familiar"] = { +/* CreatureList object name // REQUIRED // + TYPE: string + USE: object name of the companion as it will be used by the sheet + + By adding a new object to the existing CompanionList object, we create a new companion option. + The object name here is 'purple familiar'. You can use any object name as long as it is not already used. + If you do use an object name that is already in use, you will be overwriting that object. + + Note the use of only lower case! Also note the absence of the word "var" and the use of brackets []. +*/ + name : "Purple Familiar", +/* name // REQUIRED // + TYPE: string + USE: name of the companion option as it will be displayed on the sheet + + This name will be used to populate the Notes field on the companion page, as the header of its attributes. + This name will not be used in the Companion Options menu, the `nameMenu` will be used instead. +*/ + nameMenu : "Familiar (Purple Familiar class feature)", +/* nameMenu // REQUIRED // + TYPE: string + USE: name of the companion option as it will appear in the Companion Options menu + + This name will also be used on several other places, like the changes dialog pop-up or in error messages. +*/ + nameTooltip : "the Purple Familiar class feature", +/* nameTooltip // OPTIONAL // + TYPE: string + USE: name of the companion options as it will appear in tooltips for actions on the 1st page + + This attribute is only used if the `action` attribute is present as well. + If `nameTooltip` is not defined, the `name` attribute will be used instead. +*/ + nameOrigin : "variant of the Find Familiar 1st-level conjuration [ritual] spell", +/* nameOrigin // OPTIONAL // + TYPE: string + USE: additional information displayed in the + + This attribute is added in brackets after the `name` attribute and together with the `source` + to form the heading in the Notes section of the companion page. + + For example, using these attributes: + name : "Purple Familiar", + nameOrigin : "variant of the Find Familiar 1st-level conjuration [ritual] spell", + source : [["HB", 105], ["Purple", 12]] + The resulting heading would be: + "Purple Familiar (variant of the Find Familiar 1st-level conjuration [ritual] spell, HB 105)" + Note that only the first source option is used that is not set to excluded in the Source Material dialog. +*/ + source : ["SRD", 204], + source : [["E", 7], ["S", 115]], +/* source // REQUIRED // + TYPE: array with two entries (or array of these arrays) + USE: define where the companion option is found + + This attribute is used by the sheet to determine if the companion option should be available + depending on the sources included and excluded. + + This array has two entries, a string followed by a number + 1. string + The first entry has to be the object name of a SourceList object. + 2. number + The second entry is the page number to find the creature at. + This can be any number and is ignored if it is a 0. + + See the "source (SourceList).js" file for learning how to add a custom source. + + Alternatively, this can be an array of arrays to indicate it appears in multiple sources. + The example above says something appears on both page 7 of the Elemental Evil Player's Companion and + on page 115 of the Sword Coast Adventure Guide. + + If a creature is completely homebrew, or you don't want to make a custom source, just put the following: + source : ["HB", 0], + "HB" refers to the 'homebrew' source. +*/ + defaultExcluded : true, +/* defaultExcluded // OPTIONAL // + TYPE: boolean + USE: whether this companion option should be excluded by default (true) or included by default (false) + + Include this attribute and set it to true if the companion option should appear in the Excluded list + of the Source Selection Dialog when the script is added for the first time. + It will have to be manually set to be included before it is used by the sheet's automation. + The user will be made aware of this exclusion. + + This is useful for optional companion options that you wouldn't normally want to use (e.g. playtest or campaign-specific). + + Setting this attribute to false is the same as not including this attribute. +*/ +action : [ + ["reaction", " (start)"], + ["bonus action", "Shove"] +], +/* action // OPTIONAL // + TYPE: array (variable length) + USE: add entry to the "Actions", "Bonus Actions", or "Reactions" section on the 1st page + + The entries in this array must always be arrays with 2 strings each: + 1. The first string in each sub-array is the type of action, written in lowercase. + The options are "action", "bonus action", or "reaction". + 2. The second string can be one of two things: + 2.1 When the first character of the string is non-alphabetic (e.g. a space or a hyphen), it is amended to the name of the companion option. + This amended total is then added as an action. + 2.2 When the first character of the string is an alphabetic character (e.g. everything from a-Z), it is not amended to the name of the feature. + The string is taken as-is and added as an action. + + For the tooltip of the origin of these action(s), the `nameTooltip` will be used if + defined, see above. If `nameTooltip` isn't defined, the `name` will be used instead. +*/ + includeCheck : function(sCrea, objCrea, iCreaCR) { + return objCrea.type.toLowerCase() === "beast" && objCrea.size >= 3 && iCreaCR <= 1/4 ? true : false; + }, +/* includeCheck // OPTIONAL // + TYPE: function + USE: filter things from the list of creatures to create the menu options + + This function is called when the Companion Options button is pressed, to generate + the menu entries for this companion template option. + By default, the only creatures that will be displayed in the menu, + will be those that have the `companion` attribute set to this CompanionList object's name. + If options should be more dynamic, or if you don't want to alter a bunch of CreatureList entries, + it is recommended to add this attribute. + + If what the function returns == `true` for an entry, that entry will be added + to the menu options. + If the function returns a string, that entry will be added to the menu options and + that string will be amended to it (e.g. return " (if DM approves)" for a "Cat" to have it "Cat (if DM approves)" in the menu). + + This function is passed three variables: + 1) sCrea + A string of the name of the entry in the CreatureList object + 2) objCrea + The object of the entry (e.g. CreatureList[sCrea]) + 3) iCreaCR + The numerical value of the challenge rating of the CreatureList object + (e.g. 0.25 if the challengeRating attribute is "1/4") + + Only creatures in the CreatureList will be processed, not those added through + the `creaturesAdd` attribute. + + Note that it is always possible to apply a companion template option to any creature + on the Companion Page, using the Companion Options button and selecting to change + the current creature into a template option. +*/ + +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // +// >>> Change CreatureList object >>> // +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // +/* + When a companion template option is selected, the sheet will create a temporary CreatureList object. + This object is based on the CreatureList object of the base selected creature (the "Race" dropdown) + but will be amended with the settings of the CompanionList object. + Together the two create a new object that will be used to populate the Companion Page. + You can use the CompanionList object to add, change, or remove attributes of a CreatureList object. +*/ + attributesAdd : { + header : "Familiar", + type : ["Celestial", "Fey", "Fiend"], + attacks : [{ + name : "Acid Spit", + ability : 2, + damage : [1, 6, "acid"], + range : "60 ft", + description : "" + }], + }, +/* attributesAdd // OPTIONAL // + TYPE: object containing any attributes of the CreatureList object + USE: add or overwrite attributes in the base CreatureList object + + This object can contain almost all attributes of a CreatureList object. + See the "creature, wild shape option (CreatureList).js" file for all possible attributes. + + When a companion template option is selected, the sheet will create a temporary CreatureList object + that has all the same attributes of the race's CreatureList object, but will amend it with the selected + companion template options, if any. + The sheet will add any attribute of this `attributesAdd` object to the temporary CreatureList object. + + Using the above example, any creature that has this companion template option selected, + will get the header in the top right set to "Familiar", will have its type changed to a choice of + "Celestial", "Fey", or "Fiend", and will have the "Acid Spit" attack added to its attack options. + + If the attribute already exists in the base CreatureList object, the attributes will be added together, + if they are a string or array. See below how that works: + + TYPE EFFECT + string the string will be added to the string, with a comma + array the `attributesAdd` array will be added to the end of the CreatureList array + boolean, function, + number, or object the `attributesAdd` version will overwrite the CreatureList version + + EXCEPTIONS + `attributesAdd` can't contain the following attributes, they will simply not be processed: + `name`, `nameAlt`, `companionApply`, `saves`, `scores`, `skills`, `source`, + `eval`, `removeeval`, or `changeeval` + If `attributesAdd` contains any of the following attributes, they will always overwrite the base: + `challengeRating`, `header`, `subtype`, `type`, and `wildshapeString` + + It is recommended to not include a `calcChanges` this way, but to add it to the main CompanionList object + instead, see below at `calcChanges`. +*/ + attributesChange : function(sCrea, objCrea) { + // can't do any attacks + objCrea.attacks = []; + if (objCrea.type.toLowerCase() === "beast") { + objCrea.type = ["Celestial", "Fey", "Fiend"]; + objCrea.subtype = ""; + } + }, +/* attributesChange // OPTIONAL // + TYPE: function + USE: change attributes of the temporary CreatureList object + + After the temporary CreatureList object is generated from the base creature and the `attributesAdd` + attribute (see above), that object is run through this function so attributes can be changed dynamically + + Use this to change things that you can't set using `attributesAdd` (see above. Things like the ones that + are dependent on other factors (e.g. change subtype only if the base is a certain type), + or if you need to change a string or array that you don't want to merge. + + The function is passed two variables: + 1) sCrea + A string of the name of the entry in the CreatureList object + You can use this to reference the original base creature, i.e. CreatureList[sCrea] + Don't change attributes of this base creature, but instead change the temporary object, objCrea + 2) objCrea + The object that you can change the attributes off so they will be applied on the Companion page +*/ + +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // +// >>> Add text to Notes section >>> // +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // + notes : [{ + name : "False Appearance", + description : "While the purple familiar remains motionless, it is indistinguishable from an ordinary purple flower.", + joinString : "\n " + }, { + name : "Invisibility", + minlevel : 5, + description : "As an action, the purple familiar magically turns invisible until it attacks or casts a spell, or until its concentration ends (as if concentrating on a spell).", + addMod : [{ type : "skill", field : "all", mod : "max(oCha|1)", text : "The purple familiar adds its master's Charisma modifier (min 1) to all its skill checks." }] + }, { + name : "Keen Sight", + minlevel : 8, + description : "The purple familiar has advantage on Wisdom (Perception) checks that rely on sight. It size increases to Large.", + eval : function(prefix, lvl) { + // Increase size to Large + PickDropdown(prefix + "Comp.Desc.Size", 2); + }, + removeeval : function(prefix, lvl) { + // Change size back to Medium + PickDropdown(prefix + "Comp.Desc.Size", 3); + } + }], +/* notes // OPTIONAL // + TYPE: array (variable length) with objects + USE: add text to the leftmost Notes sections on the Companion page + + This attribute works identical to the `actions` , `features`, and `traits` from the CreatureList object, + but the entries are added to the left Notes section instead. + + The attribute an array with objects that have at least two attributes, `name` and `description`, that each contain a string. + + Each object can also have the following optional attributes: + ATTRIBUTE EXPLANATION + minlevel determines at which level the feature is added + addMod add custom modifiers to calculated values + eval run a function when added (useful combined with minlevel) + removeeval run a function when removed (useful combined with minlevel) + For a more detailed explanation of these attributes, see below. + + Each name is preceded by a bullet point and, by default, followed by a colon and the description, + for example: + { + name : "Invisibility", + description : "As an action, the purple familiar magically turns invisible until it attacks or casts a spell, or until its concentration ends (as if concentrating on a spell)." + } + Will result in: + ◆ Invisibility: As an action, the purple familiar magically turns invisible until it attacks or casts a spell, or until its concentration ends (as if concentrating on a spell). + + If you want something else than a colon, you can change it to anything you like by adding the + `joinString` attribute. For example: + { + name : "False Appearance (HB 105)", + description : "While the purple familiar remains motionless, it is indistinguishable from an ordinary purple flower.", + joinString : "\n " + } + Will result in: + ◆ False Appearance (HB 105) + While the purple familiar remains motionless, it is indistinguishable from an ordinary purple flower. + + If the `description` attribute is not present, no string will be added to the field. + Any description will do, even an empty string (e.g. description : ""). + + As these are added to the Notes section, it shouldn't interfere with any of the + `traits`, `features`, or `actions` defined by a CreatureList object. + + The array is processed in the order it is in the code, no sorting will take place. +*/ + +/* minlevel // OPTIONAL // + (Part of `notes` object, see above) + TYPE: number + USE: the level at which to add the note + + This attribute is part of an object in the `notes` arrays, see above. + Use this if an entry in that array is only supposed to be displayed + once the main character (the character on the 1st page) reaches a certain level. + If the main character goes below this level, the entry is removed again. + The level checked against can be different than the main character level if the + attribute `minlevelLinked` exists, see the CreatureList syntax. + + Setting this attribute to 1 is the same as not including it. +*/ + +/* addMod // OPTIONAL // + (Part of `notes` object, see above) + TYPE: array of objects (variable length) + USE: add value to a modifier field + + This attribute will only be processed when the `notes` object is processed, + which can be influenced using the `minlevel` attribute, see above. + + This attribute works identical to the `addMod` attribute found in the + "_common attributes.js" file. + Please look there for a complete explanation. +*/ + +/* eval & removeeval // OPTIONAL // + (Part of `notes` object, see above) + TYPE: variable, see the entries for `eval` or `removeeval` + USE: variable, see the entries for `eval` or `removeeval` + + These attributes are part of an object in the `notes` arrays, see above. + These optional attributes function identical to those that share their name. + They function exactly as described for the main object, but they will only be called when the + `notes` object is processed, which can be influenced using the `minlevel` attribute, see above. +*/ + + +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // +// >>> Change Companion Page calculations >>> // +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // + calcChanges : { +/* calcChanges // OPTIONAL // + TYPE: object (optional attributes) + USE: change how the hit points automation works + + The attributes of this object can be `hp` and `setAltHp`. + + Note that `calcChanges` also appears in the list of common attributes, + but only its `hp` attribute is shared with the object here. + + This object works identical to that present in the CreatureList syntax. + You could also add a `calcChanges` object to `attributesAdd`, but if you do that + and the CreatureList object has a `calcChanges` as well, only this version in the CompanionList + object will be used. + By adding a `calcChanges` object to the main CompanionList object, both extra hp calculation options + will be added. +*/ + hp : function (totalHD, HDobj, prefix) { + if (!classes.known.ranger) return; + var creaHP = CurrentCompRace[prefix] && CurrentCompRace[prefix].hp ? CurrentCompRace[prefix].hp : 0; + var creaName = CurrentCompRace[prefix] && CurrentCompRace[prefix].name ? CurrentCompRace[prefix].name : "the creature"; + var rngrLvl = classes.known.ranger.level; + var rngrCompHp = 4 * rngrLvl; + HDobj.alt.push( Math.max(creaHP, rngrCompHp) ); + HDobj.altStr.push(" = the highest of either\n \u2022 " + creaHp + " from " + creaName + "'s normal maximum HP, or\n \u2022 4 \xD7 " + rngrLvl + " from four times my ranger level (" + rngrCompHp + ")"); + }, + /* hp // OPTIONAL // + TYPE: function + USE: change how Hit Points are calculated and what the Hit Points tooltip says + + This function works identical to the `calcChanges.hp` function found in the + "_common attributes.js" file. + Please look there for a complete explanation. + */ + setAltHp : true, + /* setAltHp // OPTIONAL // + TYPE: boolean + USE: set the maximum HP field to automatically assume the alternative calculation method added with the `hp` function + + This attribute will only work if you set the `hp` attribute (see above) in the same object. + Set this attribute to true if you push a value to the HDobj.alt array with the function in the `hp` attribute. + + Setting this attribute to false is the same as not including it. + */ + }, + + +// >>>>>>>>>>>>>>>>>>>>>>>>>>> // +// >>> Run custom function >>> // +// >>>>>>>>>>>>>>>>>>>>>>>>>>> // + eval : function(prefix, lvl) { + AddString(prefix + 'Cnote.Left', 'The purple familiar always serves a singular master. If that master gets killed, it will serve the one who killed its master, if any.', true); + }, +/* eval // OPTIONAL // + TYPE: function + USE: runs a piece of code when the companion option is selected + + The function is passed two variables: + 1) The first variable is a string: the prefix of the Companion page this companion option was selected on + You can use this variable to call on fields on that page. The example above uses it to set + a string to the leftmost Notes section on the Companion page. + 2) The second variable is an array with 2 numbers: the old level and the new level + e.g. lvl = [0,5] when the companion option gets added and the character is 5th level + The first entry, the old level, is the level that was passed as the second entry the last time + this function was called. + The first entry will be zero (0) as this is only called when the companion option is added for the + first time. + The second entry, the new level, is the current main character level. + The new level passed can be different than the main character level if the attribute + `minlevelLinked` exists, see the CreatureList syntax. + + This can be any JavaScript you want to have run whenever this companion option is selected. + This attribute is processed last, after all other attributes are processed, even after the attribute by + the same name from the CreatureList object has been processed. +*/ + + removeeval : function(prefix, lvl) { + RemoveString(prefix + 'Cnote.Left', 'The purple familiar always serves a singular master. If that master gets killed, it will serve the one who killed its master, if any.', true); + }, +/* removeeval // OPTIONAL // + TYPE: function + USE: runs a piece of code when the companion options is removed + + The function is passed two variables: + 1) The first variable is a string: the prefix of the Companion page this companion option was selected on + You can use this variable to call on fields on that page. The example above uses it to remove + a string to the leftmost Notes section on the Companion page. + 2) The second variable is an array with 2 numbers: the old level and the new level + e.g. lvl = [0,5] when the companion option gets added and the character is 5th level + The first entry, the old level, is the level that the companion option had before being removed. + The second entry, the new level, will be zero (0) as this is only called when the companion + option is being removed. + + This can be any JavaScript you want to have run whenever the companion option is removed. + This attribute is processed last, after all other attributes are processed, even after the attribute by + the same name from the CreatureList object has been processed. +*/ + + changeeval : function(prefix, lvl) { + Value( prefix + "Comp.Use.HD.Die", lvl[1] < 15 ? 8 : 10 ); + }, +/* changeeval // OPTIONAL // + TYPE: function + USE: runs a piece of code every time the main character's level changes + + "Main character" refers to the character on the first page. + A companion doesn't have its own 'level' that is used for the automation. + + The function is passed two variables: + 1) The first variable is a string: the prefix of the Companion page this companion option was selected on + You can use this variable to call on fields on that page. The example above uses it to set the + companion option's hit dice size depending on the character's level (d8 or d10, if level 15+). + 2) The second variable is an array with 2 numbers: the old level and the new level + e.g. lvl = [0,5] when the companion option gets added and the character is 5th level + The first entry, the old level, is the level that was passed as the second entry the last time + this function was called. + The first entry will be zero (0) if the companion option is added for the first time. + The second entry, the new level, is the current main character level. + The new level will be zero (0) if the companion option is being removed. + The new level passed can be different than the main character level if the attribute + `minlevelLinked` exists, see above. + + This can be any JavaScript you want to have run whenever the level changes. + This attribute is processed last, after all other attributes have been processed, even after + the attribute by the same name from the CreatureList object has been processed. + It is processed both when the companion options is first added to the companion page and + when the main character's level changes, but not when the companion option is removed. +*/ +} diff --git a/additional content syntax/creature, wild shape option (CreatureList).js b/additional content syntax/creature, wild shape option (CreatureList).js index f548b394..82ea75cb 100644 --- a/additional content syntax/creature, wild shape option (CreatureList).js +++ b/additional content syntax/creature, wild shape option (CreatureList).js @@ -42,7 +42,7 @@ You will also need the syntax for common attributes if you want to use a custom calculation for hit points (calcChanges.hp). - Sheet: v13.0.6 and newer + Sheet: v13.1.0 and newer */ @@ -176,11 +176,19 @@ CreatureList["purple crawler"] = { 5 Tiny */ type : "Fiend", + type : ["Celestial", "Fey", "Fiend"], /* type // REQUIRED // - TYPE: string + TYPE: string or array (since v13.1.0) USE: set the type drop-down box - This can be anything you like, but is usually one of the known creature types: + This can be either a: + 1) String + The name of the creature type + 2) Array (since v13.1.0) + If the creature can be several different creature types, using an array will prompt the player + to make a choice which one to use. + + You can put here anything you like, but is usually one of the known creature types: Aberration Beast Celestial @@ -200,68 +208,92 @@ CreatureList["purple crawler"] = { thus it is recommended to capitalize it for consistency. */ subtype : "devil", + subtype : ["demon", "devil"], /* subtype // OPTIONAL // - TYPE: string + TYPE: string or array (since v13.1.0) USE: add the subtype in the type drop-down box + This can be either a: + 1) String + The name of the creature subtype + 2) Array (since v13.1.0) + If the creature can be several different creature subtypes, using an array will prompt the player + to make a choice which one to use. + This can be anything you like. It will be added to the string of the `type` attribute in brackets, and together will be set in the type drop-down box. - In this example, it will end up reading as "Fiend (devil)". + In the first example, it will end up reading as "Fiend (devil)". This value is put in the type drop-down box of the sheet without any changes, - only with added brackets around it, - thus it is recommended to have it all lowercase for consistency. + only with added brackets around it, thus it is recommended to have it all lowercase for consistency. */ companion : "familiar", + companion : ["familiar_not_al", "pact_of_the_chain"], /* companion // OPTIONAL // - TYPE: string + TYPE: string or array of strings USE: list this creature as an option for a special type of companion + CHANGE: v13.1.0 - Setting this to one of the below pre-defined values makes this creature selectable - using the Companion Options button on the companion page for special options. + This attribute is either a key corresponding to a CompanionList object name, or an array + of those keys. + Doing so makes this creature selectable as that type of special companion, + using the Companion Options button on the companion page. - Note that you can change any creature into one of the special options using the - Companion Options button by first selecting a race from the drop-down and subsequently selecting the "Change this into" option in the Companion Options menu. + Some CompanionList objects have their own filter for determining which creatures are + applicable. + However, if you set this attribute to match the CompanionList object key, + they will always be available for that CompanionList entry, regardless of its filter. - Use this `companion` attribute for things that are obvious candidates for the special options. + Note that you can change any creature into one of the special options by first selecting + a race from the drop-down and selecting "Change current creature into a ..." with + the Companion Options button. - OPTION EXPLANATION - "familiar" Find Familiar spell and Pact of the Chain warlock boon - "pact_of_the_chain" Pact of the Chain warlock boon (but not Find Familiar spell) - "familiar_not_al" Same as "familiar", but with the added description "(if DM approves)" - However, this creature will not be shown for either Find Familiar - or Pact of the Chain when the DCI field is visible (i.e. Adventurers League) - "mount" Find Steed spell - "steed" Find Greater Steed spell - "companion" Unearthed Arcana: Revised Ranger's Beast Conclave feature + Use this `companion` attribute for things that are obvious candidates for the special options. - Some special companion options are not governed by this setting and list their options dynamically. - For example, the PHB ranger's companion includes anything that has `type` set as "Beast", - `size` as 3 or higher (medium or smaller), and `challengeRating` as 1/4 or lower. + OPTION EXPLANATION + "familiar" Find Familiar spell and Pact of the Chain warlock boon + "pact_of_the_chain" Pact of the Chain warlock boon (but not Find Familiar spell) + "familiar_not_al" Same as "familiar", but with the added description "(if DM approves)" + However, this creature will not be shown for either Find Familiar + or Pact of the Chain when the DCI field is visible (i.e. Adventurers League). + "mount" Find Steed spell + "steed" Find Greater Steed spell + "companion" Ranger's Companion (Beast Master feature) + Has its own filter, so normally you don't need this option. + Filter: any Beast, Medium or smaller, and CR of 1/4 or lower + "companionrr" 2016/09/12 Unearthed Arcana: Revised Ranger's Beast Conclave feature + "strixhaven_mascot" Strixhaven Mascot familiar (Strixhaven Mascot feat), but not Find Familiar spell */ companionApply : "companion", /* companionApply // OPTIONAL // TYPE: string USE: always set this creature to be this special type of companion - Setting this to one of the below pre-defined values will make the sheet - automatically apply the features of that special type of companion. + Setting this to a key in the CompanionList object will make the sheet + automatically apply the features of that special companion type. - Note that you can change any creature into one of the special options using the - Companion Options button by first selecting a race from the drop-down and subsequently selecting the "Change this into" option in the Companion Options menu. + Note that you can change any creature into one of the special options by first selecting + a race from the drop-down and selecting "Change current creature into a ..." with + the Companion Options button. Use this `companionApply` attribute only if the creature *always* is that kind of companion. - OPTION TYPE OF COMPANION - "familiar" Find Familiar spell - "pact_of_the_chain" Pact of the Chain warlock boon - "mount" Find Steed spell - "steed" Find Greater Steed spell - "companion" Ranger: Beast Master's Ranger's Companion feature - "companionrr" 2016/09/12 Unearthed Arcana: Revised Ranger's Beast Conclave feature - "mechanicalserv" 2017/01/09 Unearthed Arcana: Artificer's Mechanical Servant feature + Import scripts can add things to the CompanionList object, but generally these + options should be available (if the applicable scripts are imported if they're not SRD): + + SRD OBJECT KEY EXPLANATION + V "familiar" Find Familiar spell + V "pact_of_the_chain" Pact of the Chain familiar (Warlock 3rd-level boon) + V "mount" Find Steed spell + - "steed" Find Greater Steed spell + - "companion" Ranger's Companion (Ranger: Beast Master feature) + - "strixhaven_mascot" Strixhaven Mascot familiar (Strixhaven Mascot feat) + - "companionrr" Animal Companion (2016/09/12 Unearthed Arcana: + Revised Ranger's Beast Conclave feature) + - "mechanicalserv" Mechanical Servant (2017/01/09 Unearthed Arcana: + Artificer's Mechanical Servant feature) - Be aware that this list is different than the one for the `companion` attribute! + Be aware that this list is slightly different than the one for the `companion` attribute! */ alignment : "Unaligned", /* alignment // REQUIRED // @@ -528,6 +560,7 @@ CreatureList["purple crawler"] = { features : [{ name : "False Appearance", description : "While the purple crawler remains motionless, it is indistinguishable from an ordinary purple flower.", + joinString : "\n " }], actions : [{ name : "Invisibility", @@ -553,11 +586,21 @@ CreatureList["purple crawler"] = { traits // OPTIONAL // TYPE: array (variable length) with objects USE: add text to the Traits and Features sections on the Companion page + CHANGE: v13.1.0 (added `joinString` attribute) Each of these three attributes work in the same way. Each is an array with objects that have at least two attributes, `name` and `description`, that each contain a string. - Each name is preceded by a bullet point and followed by a colon and the description when + Each object can also have the following optional attributes: + ATTRIBUTE EXPLANATION + minlevel determines at which level the feature is added + addMod add custom modifiers to calculated values + eval run a function when added (useful combined with minlevel) + removeeval run a function when removed (useful combined with minlevel) + For a more detailed explanation of these attributes, see below in the + Companion Page Only section. + + Each name is preceded by a bullet point and, by default, followed by a colon and the description when added to the right section, for example: { name : "Invisibility", @@ -565,8 +608,20 @@ CreatureList["purple crawler"] = { } Will result in: ◆ Invisibility: As an action, the purple crawler magically turns invisible until it attacks or casts a spell, or until its concentration ends (as if concentrating on a spell). + + If you want something else than a colon, you can change it to anything you like by adding the + `joinString` attribute. For example: + { + name : "False Appearance", + description : "While the purple crawler remains motionless, it is indistinguishable from an ordinary purple flower.", + joinString : "\n " + } + Will result in: + ◆ False Appearance + While the purple crawler remains motionless, it is indistinguishable from an ordinary purple flower. - If the `description` attribute is not present, no string will be added to the field, but any + If the `description` attribute is not present, no string will be added to the field. + Any description will do, even an empty string (e.g. description : ""). The three different attributes, traits, features, and actions, are added to different parts of the companion page: @@ -585,8 +640,8 @@ CreatureList["purple crawler"] = { These text are also displayed on the wild shape page, but all together in the singular Traits & Features section, regardless of their `minlevel` attribute value. Also, `eval` and `changeeval` are not executed when this creature is selected on the Wild Shape page. - As the wild shape pages offer limited space, it is recommended to test if all of these and - the other attributes together will fit. + As the wild shape pages offer limited space, it is recommended to test if all of + these and the other attributes together will fit. If they don't fit (well), consider using the `wildshapeString` attribute, see below. */ @@ -608,8 +663,8 @@ CreatureList["purple crawler"] = { Use this if an entry in that array is only supposed to be displayed once the main character (the character on the 1st page) reaches a certain level. If the main character goes below this level, the entry is removed again. - The level checked against can be different than the main character level if the attribute - `minlevelLinked` exists, see below. + The level checked against can be different than the main character level if the + attribute `minlevelLinked` exists, see below. Setting this attribute to 1 is the same as not including it. */ @@ -655,7 +710,6 @@ CreatureList["purple crawler"] = { ADDED: v13.0.6 */ - header : "Summon", /* header // OPTIONAL // TYPE: string @@ -740,7 +794,7 @@ CreatureList["purple crawler"] = { You can use this variable to call on fields on that page. The example above uses it to set a string to the leftmost Notes section on the Companion page. 2) The second variable is an array with 2 numbers: the old level and the new level - e.g. lvl = [0,5] when the creature gets added an the character is 5th level + e.g. lvl = [0,5] when the creature gets added and the character is 5th level The first entry, the old level, is the level that was passed as the second entry the last time this function was called. The first entry will be zero (0) as this is only called when the creature is added for the first time. @@ -764,7 +818,7 @@ CreatureList["purple crawler"] = { You can use this variable to call on fields on that page. The example above uses it to remove a string to the leftmost Notes section on the Companion page. 2) The second variable is an array with 2 numbers: the old level and the new level - e.g. lvl = [0,5] when the creature gets added an the character is 5th level + e.g. lvl = [0,5] when the creature gets added and the character is 5th level The first entry, the old level, is the level that the creature had before being removed. The second entry, the new level, will be zero (0) as this is only called when the creature is being removed. @@ -788,7 +842,7 @@ CreatureList["purple crawler"] = { You can use this variable to call on fields on that page. The example above uses it to set the creature's hit dice size depending on the character's level (d8 or d10, if level 15 or higher). 2) The second variable is an array with 2 numbers: the old level and the new level - e.g. lvl = [0,5] when the creature gets added an the character is 5th level + e.g. lvl = [0,5] when the creature gets added and the character is 5th level The first entry, the old level, is the level that was passed as the second entry the last time this function was called. The first entry will be zero (0) if the creature is added for the first time. diff --git a/additional content syntax/source (SourceList).js b/additional content syntax/source (SourceList).js index 27055059..3ec043c8 100644 --- a/additional content syntax/source (SourceList).js +++ b/additional content syntax/source (SourceList).js @@ -37,7 +37,7 @@ Remarks: The object name of a source is used for almost everything that you can import into the sheet. A tip is to invoke a new source object at the start of an import script to make sure it is available for whatever you are adding. - Sheet: v13.0.6 and newer + Sheet: v13.1.0 and newer */ var iFileName = "Homebrew Syntax - SourceList.js"; @@ -100,7 +100,7 @@ SourceList["BoP"] = { The full name will only be used in tooltips and dialogs, so make it as long as you want. */ abbreviation : "BoP", -/* name // REQUIRED // +/* abbreviation // REQUIRED // TYPE: string USE: abbreviation of the source as it will be used by the sheet @@ -108,6 +108,24 @@ SourceList["BoP"] = { This is a greatly shortened name of the source. The abbreviation will be used extensively throughout the sheets, so be sure to make it easily recognizable. +*/ + abbreviationSpellsheet : "P", +/* abbreviationSpellsheet // OPTIONAL // + TYPE: string + USE: abbreviation of the source as it will be used on the spell sheet pages in the "B" column + ADDED: v13.0.1 + + The spell sheet pages have very limited space for the abbreviations so this is a separate attribute. + It is recommended to keep this abbreviation to just a single character. + + Two characters might fit, depending on the character. For example, the "M" and "W" are wider + in most fonts than other letter. + If you are using more than one character, be sure to try the abbreviation on the sheet, + or else the text might overflow the field, resulting in a [+] sign that will obscure the whole field. + + If this attribute is not present, the sheet will default to the first letter of the object name + (not the `abbreviation` attribute). + For example, for the above `SourceList["BoP"]`, the sheet will put a "B" on the spell sheet page. */ date : "2019/02/26", /* date // OPTIONAL // diff --git a/additional content syntax/spell (SpellsList).js b/additional content syntax/spell (SpellsList).js index f3023cee..1514322b 100644 --- a/additional content syntax/spell (SpellsList).js +++ b/additional content syntax/spell (SpellsList).js @@ -38,7 +38,7 @@ If you want attack cantrips or spells to be added to the attack section, use the syntax for adding a weapon (as well), see "weapon (WeaponsList).js". - Sheet: v13.0.7 and newer + Sheet: v13.1.0 and newer */ @@ -228,7 +228,7 @@ SpellsList["sindering purple"] = { time : "1 min", /* time // REQUIRED // TYPE: string - USE: the casting time of the spell + USE: the casting time of the spell as it should appear on the sheet The text you add here will be put in the appropriate field on the sheet literally, thus it is important to check that it fits in the field. @@ -241,6 +241,22 @@ SpellsList["sindering purple"] = { 1 rea 1 reaction min minute(s) h hours(s) +*/ + timeFull : "1 reaction, which you take when you see a creature within 60 feet of you casting a spell", +/* timeFull // OPTIONAL // + TYPE: string + USE: the casting time of the spell as should appear in the spell's full description + ADDED: v13.1.0 + + The text you add here will only be used in the tooltip and dialogs where the spell's + full description is shown. + + This entry is only needed if the casting time doesn't follow any of the standard rules. + The sheet can automatically extrapolate the casting time from the abbreviation used in the `time` + attribute. + + In the official spells, this `timeFull` attribute is only used for the spells with a casting time + of 1 reaction, as those have extra explanatory text. */ range : "60 ft", /* range // REQUIRED //