Files
2026-06-06 23:05:22 -04:00

4208 lines
219 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Create a collective list of attributs to reduce redundancy
var abilityList = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"];
var abilityListEnabledOptionals = [];
const globalAttributesByCategory = {
abilities: ['strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma'],
abilitiesWithAllOptionals: ['strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma', 'honor', 'sanity'],
//classes: ['artificer','barbarian','bard','cleric','druid','fighter','monk','paladin','ranger','rogue','sorcerer','warlock','wizard'],
classes: ['adept', 'cybermancer', 'engineer', 'hunter', 'melder', 'sentinel', 'specialist', 'warrior'],
hitDieTypes: ['d4', 'd6', 'd8', 'd10', 'd12'],
traitsTypes: ['Background', 'Feats', 'Racial', 'Class', 'Other'],
proficiencyTypes: ['LANGUAGE', 'ARMOR', 'WEAPON', 'OTHER'],
skills: {
strength: [
'athletics'
],
constitution: [],
dexterity: [
'acrobatics',
'sleight_of_hand',
'stealth',
],
intelligence: [
'arcana',
'history',
'investigation',
'nature',
'religion',
],
wisdom: [
'animal_handling',
'insight',
'medicine',
'perception',
'survival'
],
charisma: [
'deception',
'intimidation',
'performance',
'persuasion',
],
honor: [],
sanity: [],
all: () => { return [...globalAttributesByCategory.skills.strength, ...globalAttributesByCategory.skills.dexterity, ...globalAttributesByCategory.skills.intelligence, ...globalAttributesByCategory.skills.wisdom, ...globalAttributesByCategory.skills.charisma]; },
getParentAbility: skill => {
let parent = '';
globalAttributesByCategory['abilities'].forEach(ability => {
if(globalAttributesByCategory.skills[ability].indexOf(skill) > -1) {
parent = ability;
return;
}
});
return parent;
}
}
};
on("sheet:opened", function(eventinfo) {
lastDropID = "";
// Refresh the PC initiative bonus + its display label on open so existing characters
// correct any stale initiative_bonus_label (NPC init is managed separately).
getAttrs(["npc"], function(initv) {
if (!(initv["npc"] && initv["npc"] == "1")) { update_initiative(); }
});
versioning(function() {
var getInfo = function(sections, callback, results) {
results = results || {};
if(sections.length > 0) {
var section = sections.pop();
getSectionIDs(section, function(ids) {
results[section] = ids;
getInfo(sections, callback, results);
});
} else {
callback(results);
}
}
var spellSections = ["spell-prime"];
for(var x=1; x<=9; x++) {
spellSections.push("spell-" + x);
}
getInfo(spellSections, function(results) {
var getList = ["character_id"];
_.each(results, function(sectionids, sectionname) {
_.each(sectionids, function(spellid) {
getList.push("repeating_" + sectionname + "_" + spellid + "_rollcontent");
});
});
getAttrs(getList, function(attrs) {
var set = {};
_.each(attrs, function(data, name) {
if(data.length === 68 && data.split("|")[0].substr(2) !== attrs["character_id"]) {
set[name] = "%{" + attrs["character_id"] + data.substr(22);
}
});
setAttrs(set, function() {
if(eventinfo.sourceType === "sheetworker") {
setAttrs({l1mancer_status: "completed"})
}
else {
check_l1_mancer();
check_lp_mancer();
};
});
});
});
});
});
//Start of Compendium Drops
on("sheet:compendium-drop", function() {
getAttrs(["hp_max","npc_senses","ship_senses","token_size","cd_bar1_v","cd_bar1_m","cd_bar1_l","cd_bar2_v","cd_bar2_m","cd_bar2_l","cd_bar3_v","cd_bar3_m","cd_bar3_l"], function(v) {
var default_attr = {};
default_attr["width"] = 70;
default_attr["height"] = 70;
if(v["npc_senses"].toLowerCase().match(/(darkvision|blindsight|tremorsense|truesight)/)) {
default_attr["light_radius"] = Math.max.apply(Math, v["npc_senses"].match(/\d+/g));
}
if(v["token_size"]) {
var squarelength = 70;
if(v["token_size"].toString().indexOf(",") > -1) {
var setwidth = !isNaN(v["token_size"].split(",")[0]) ? v["token_size"].split(",")[0] : 1;
var setheight = !isNaN(v["token_size"].split(",")[1]) ? v["token_size"].split(",")[1] : 1;
default_attr["width"] = setwidth * squarelength;
default_attr["height"] = setheight * squarelength;
}
else {
default_attr["width"] = squarelength * v["token_size"]
default_attr["height"] = squarelength * v["token_size"]
};
}
var getList = {};
for(x = 1; x<=3; x++) {
_.each(["v", "m"], function(letter) {
var keyname = "cd_bar" + x + "_" + letter;
if(v[keyname] != undefined && isNaN(v[keyname])) {
getList[keyname] = v[keyname];
}
});
}
getAttrs(_.values(getList), function(values) {
_.each(_.keys(getList), function(keyname) {
v[keyname] = values[getList[keyname]] == undefined ? "" : values[getList[keyname]];
});
if(v["cd_bar1_l"]) {
default_attr["bar1_link"] = v["cd_bar1_l"];
}
else if(v["cd_bar1_v"] || v["cd_bar1_m"]) {
if(v["cd_bar1_v"]) {
default_attr["bar1_value"] = v["cd_bar1_v"];
}
if(v["cd_bar1_m"]) {
default_attr["bar1_max"] = v["cd_bar1_m"];
}
}
else {
default_attr["bar1_value"] = v["hp_max"];
default_attr["bar1_max"] = v["hp_max"];
}
if(v["cd_bar2_l"]) {
default_attr["bar2_link"] = v["cd_bar2_l"];
}
else if(v["cd_bar2_v"] || v["cd_bar2_m"]) {
if(v["cd_bar2_v"]) {
default_attr["bar2_value"] = v["cd_bar2_v"];
}
if(v["cd_bar2_m"]) {
default_attr["bar2_max"] = v["cd_bar2_m"];
}
}
else {
default_attr["bar2_link"] = "npc_ac";
}
if(v["cd_bar3_l"]) {
default_attr["bar3_link"] = v["cd_bar3_l"];
}
else if(v["cd_bar3_v"] || v["cd_bar3_m"]) {
if(v["cd_bar3_v"]) {
default_attr["bar3_value"] = v["cd_bar3_v"];
}
if(v["cd_bar3_m"]) {
default_attr["bar3_max"] = v["cd_bar3_m"];
}
}
setDefaultToken(default_attr);
});
});
});
['strength','dexterity','constitution','intelligence','wisdom','charisma'].forEach(attr => {
on(`change:${attr}_base change:${attr}_bonus`, function() {
update_attr(`${attr}`);
});
});
['strength','dexterity','constitution','intelligence','wisdom','charisma'].forEach(attr => {
on(`change:${attr}`, function() {
update_mod(`${attr}`);
const cap = attr.charAt(0).toUpperCase() + attr.slice(1);
check_customac(cap);
(attr === "strength") ? update_weight() : false;
(attr === "dexterity") ? update_initiative() : false;
});
});
['strength','dexterity','constitution','intelligence','wisdom','charisma'].forEach(attr => {
on(`change:${attr}_mod`, function() {
update_save(`${attr}`);
update_attacks(`${attr}`);
update_tool(`${attr}`);
update_spell_info(`${attr}`);
switch(`${attr}`) {
case "strength":
update_skills(["athletics"]);
break;
case "dexterity":
update_skills(["acrobatics", "sleight_of_hand", "stealth", "piloting"]);
update_ac();
update_initiative();
break;
case "intelligence":
update_skills(["astrophysics", "lore", "investigation", "xenobiology", "computers", "arcana", "history", "nature", "religion"]);
break;
case "wisdom":
update_skills(["mechanics", "insight", "medicine", "perception", "survival", "animal_handling"]);
break;
case "charisma":
update_skills(["deception", "intimidation", "performance", "persuasion"]);
break;
default:
false;
}
});
});
['strength','dexterity','constitution','intelligence','wisdom','charisma'].forEach(attr => {
on(`change:${attr}_save_prof change:${attr}_save_mod`, function(eventinfo) {
if(eventinfo.sourceType === "sheetworker") {return;};
update_save(`${attr}`);
});
});
on("change:globalsavemod", function(eventinfo) {
if(eventinfo.sourceType === "sheetworker") {return;};
update_all_saves();
});
on("change:death_save_mod", function(eventinfo) {
if(eventinfo.sourceType === "sheetworker") {return;};
update_save("death");
});
['acrobatics','animal_handling','arcana','astrophysics','athletics','computers','deception','history','insight',
'intimidation','investigation','lore','mechanics','medicine','nature','perception','performance','persuasion',
'piloting','religion','sleight_of_hand','stealth','survival','xenobiology'].forEach(attr => {
on(`change:${attr}_prof change:${attr}_type change:${attr}_flat`, function(eventinfo) {
if(eventinfo.sourceType === "sheetworker") {return;};
update_skills([`${attr}`]);
});
});
on("change:repeating_tool:toolname change:repeating_tool:toolbonus_base change:repeating_tool:toolattr_base change:repeating_tool:tool_mod", function(eventinfo) {
if(eventinfo.sourceType === "sheetworker") {
return;
}
var tool_id = eventinfo.sourceAttribute.substring(15, 35);
update_tool(tool_id);
});
on("change:repeating_attack:atkname change:repeating_attack:atkflag change:repeating_attack:atkattr_base change:repeating_attack:atkmod change:repeating_attack:atkmagic change:repeating_attack:atkprofflag change:repeating_attack:dmgflag change:repeating_attack:dmgbase change:repeating_attack:dmgattr change:repeating_attack:dmgmod change:repeating_attack:dmgtype change:repeating_attack:dmg2flag change:repeating_attack:dmg2base change:repeating_attack:dmg2attr change:repeating_attack:dmg2mod change:repeating_attack:dmg2type change:repeating_attack:saveflag change:repeating_attack:savedc change:repeating_attack:saveflat change:repeating_attack:dmgcustcrit change:repeating_attack:dmg2custcrit change:repeating_attack:ammo change:repeating_attack:saveattr change:repeating_attack:atkrange", function(eventinfo) {
if(eventinfo.sourceType === "sheetworker") {
return;
}
var source = eventinfo.sourceAttribute.substr(38);
var attackid = eventinfo.sourceAttribute.substring(17, 37);
if(source == "atkattr_base" || source == "savedc") {
getAttrs(["repeating_attack_spellid", "repeating_attack_spelllevel"], function(v) {
set = {};
if(v.repeating_attack_spellid && v.repeating_attack_spellid != "" && v.repeating_attack_spelllevel && v.repeating_attack_spelllevel != "") {
var newVal = eventinfo.newValue == "spell" ? "spell" : _.last(eventinfo.newValue.split("_")[0].split("{"));
set["repeating_attack_atkattr_base"] = newVal == "spell" ? "spell" : "@{" + newVal + "_mod}";
set["repeating_attack_savedc"] = newVal == "spell" ? "spell" : "(@{" + newVal + "_mod}+8+@{pb})";
set["repeating_spell-" + v.repeating_attack_spelllevel + "_" + v.repeating_attack_spellid + "_spell_ability"] = newVal == "spell" ? "spell" : "@{" + newVal + "_mod}+";
}
setAttrs(set, function() {
update_attacks(attackid);
});
});
} else {
update_attacks(attackid);
}
});
on("change:repeating_damagemod remove:repeating_damagemod", function(eventinfo) {
update_globaldamage();
});
on("change:global_damage_mod_flag", function(eventinfo) {
getSectionIDs("damagemod", function(ids) {
var update = {};
if(eventinfo.newValue === "1") {
if(!ids || ids.length === 0) {
var rowid = generateRowID();
update[`repeating_damagemod_${rowid}_global_damage_active_flag`] = "1";
}
} else {
_.each(ids, function(rowid) {
update[`repeating_damagemod_${rowid}_global_damage_active_flag`] = "0";
});
}
setAttrs(update);
});
});
on("change:exhaustion_toggle", function(eventinfo) {
if(eventinfo.newValue !== "0") {
getAttrs(["exhaustion_level"], function(attrs) {
if(!attrs.exhaustion_level || attrs.exhaustion_level === "") {
var update = {};
update.exhaustion_level = "0";
setAttrs(update);
}
});
}
});
on("change:exhaustion_level", function(eventinfo) {
const newValue = parseInt(eventinfo.newValue) || 0, previousValue = parseInt(eventinfo.previousValue) || 0;
let update = {};
if (newValue === 0) {
//If exhaustion is 0 the reset exhaustion_1 to "No Effect" and blank the other spans
for(let i = 2; i <= 6; i++) { update[`exhaustion_${i}`] = ""}
update[`exhaustion_1`] = "• " + getTranslationByKey(`exhaustion-0`)
} else if (newValue > previousValue) {
//If exhaustion increase then add text to the spans
for(let i = previousValue; i <= newValue; i++)
{update[`exhaustion_${i}`] = "• " + getTranslationByKey(`exhaustion-${i}`)}
} else {
//If exhaustion decrease remove text from spans
for(let i = newValue + 1; i <= previousValue; i++) {update[`exhaustion_${i}`] = ""}
};
setAttrs(update, {silent: true});
});
on("change:race change:subrace", function(eventinfo) {
update_race_display();
});
on(`change:repeating_inventory:hasattack`, function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {return;};
const itemid = eventinfo.sourceAttribute.substring(20, 40);
getAttrs([`repeating_inventory_${itemid}_itemattackid`], function(v) {
const hasattack = eventinfo.newValue, itemattackid = v[`repeating_inventory_${itemid}_itemattackid`];
(hasattack == 1) ? create_attack_from_item(itemid) : remove_attack(itemattackid);
});
});
on(`change:repeating_inventory:useasresource`, function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {return;};
const itemid = eventinfo.sourceAttribute.substring(20, 40);
getAttrs([`repeating_inventory_${itemid}_itemresourceid`], function(v) {
const useasresource = eventinfo.newValue, itemresourceid = v[`repeating_inventory_${itemid}_itemresourceid`];
(useasresource == 1) ? create_resource_from_item(itemid) : remove_resource(itemresourceid);
});
});
on("change:repeating_inventory:itemname change:repeating_inventory:itemproperties change:repeating_inventory:itemmodifiers change:repeating_inventory:itemcount", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
var itemid = eventinfo.sourceAttribute.substring(20, 40);
getAttrs(["repeating_inventory_" + itemid + "_itemattackid", "repeating_inventory_" + itemid + "_itemresourceid"], function(v) {
var attackid = v["repeating_inventory_" + itemid + "_itemattackid"];
var resourceid = v["repeating_inventory_" + itemid + "_itemresourceid"];
if(attackid) {
update_attack_from_item(itemid, attackid);
}
if(resourceid) {
update_resource_from_item(itemid, resourceid);
}
});
});
['other_resource','repeating_resource:resource_left','repeating_resource:resource_right'].forEach(attr => {
on(`change:${attr} change:${attr}_name`, (eventinfo) => {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {return;}
const resourceid = (eventinfo.sourceAttribute.includes("_name")) ? eventinfo.sourceAttribute.slice(0, -5) : eventinfo.sourceAttribute;
getAttrs([`${resourceid}_itemid`], (v) => {
const value = eventinfo.newValue;
//Update repeating_inventory if an itemid is associated with a resource
if (v[`${resourceid}_itemid`] && v[`${resourceid}_itemid`] != "") {
const itemid = v[`${resourceid}_itemid`];
let update = {};
if (eventinfo.sourceAttribute.includes("_name")) {
update[`repeating_inventory_${itemid}_itemname`] = eventinfo.newValue;
} else {
update[`repeating_inventory_${itemid}_itemcount`] = eventinfo.newValue;
};
setAttrs(update, {silent: true}, () => {update_weight()});
};
});
});
});
on("change:repeating_inventory:itemweight change:repeating_inventory:itemcount change:cp change:sp change:ep change:cu change:cn change:encumberance_setting change:strength change:size change:carrying_capacity_mod change:powerful_build", function() {
update_weight();
});
on("change:repeating_inventory:itemmodifiers change:repeating_inventory:equipped", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
var itemid = eventinfo.sourceAttribute.substring(20, 40);
getAttrs(["repeating_inventory_" + itemid + "_itemmodifiers"], function(v) {
if(v["repeating_inventory_" + itemid + "_itemmodifiers"]) {
check_itemmodifiers(v["repeating_inventory_" + itemid + "_itemmodifiers"], eventinfo.previousValue);
};
});
});
on("change:custom_ac_flag change:custom_ac_base change:custom_ac_part1 change:custom_ac_part2 change:custom_ac_shield change:ac_misc_mod", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_ac();
});
['spell-prime','spell-1','spell-2','spell-3','spell-4','spell-5','spell-6','spell-7','spell-8','spell-9'].forEach(attr => {
on(`change:repeating_${attr}:includedesc change:repeating_${attr}:innate change:repeating_${attr}:spell_ability change:repeating_${attr}:spell_updateflag change:repeating_${attr}:spellathigherlevels change:repeating_${attr}:spellattack change:repeating_${attr}:spelldamage change:repeating_${attr}:spelldamage2 change:repeating_${attr}:spelldamagetype change:repeating_${attr}:spelldamagetype2 change:repeating_${attr}:spelldescription change:repeating_${attr}:spelldmgmod change:repeating_${attr}:spellhealing change:repeating_${attr}:spellhlbonus change:repeating_${attr}:spellhldie change:repeating_${attr}:spellhldietype change:repeating_${attr}:spellname change:repeating_${attr}:spellprepared change:repeating_${attr}:spellrange change:repeating_${attr}:spellsave change:repeating_${attr}:spellsavesuccess change:repeating_${attr}:spelltarget change:repeating_${attr}:spell_damage_progression`, (eventinfo) => {
if (eventinfo.sourceType && eventinfo.sourceType === "sheetworker") { return; }
const spellid = eventinfo.sourceAttribute.split("_")[2], repeating_source = `repeating_${attr}_${spellid}`;
getAttrs([`${repeating_source}_spellattackid`], (v) => {
var attackid = v[`${repeating_source}_spellattackid`];
var lvl = attr.split("spell-")[1];
if(attackid && lvl && spellid) {
update_attack_from_spell(lvl, spellid, attackid)
}
});
});
});
['spell-prime','spell-1','spell-2','spell-3','spell-4','spell-5','spell-6','spell-7','spell-8','spell-9'].forEach(attr => {
on(`change:repeating_${attr}:spelloutput`, (eventinfo) => {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {return;}
const spellid = eventinfo.sourceAttribute.split("_")[2], repeating_source = `repeating_${attr}_${spellid}`;
getAttrs([`${repeating_source}_spellattackid`, `${repeating_source}_spelllevel`, `${repeating_source}_spellathigherlevels`, "character_id"], function(v) {
const attackid = v[repeating_source + "_spellattackid"];
const higherlevels = v[repeating_source + "_spellathigherlevels"];
const spelloutput = eventinfo.newValue;
let lvl = v[repeating_source + "_spelllevel"];
if (spelloutput && spelloutput === "ATTACK") {
create_attack_from_spell(lvl, spellid, v["character_id"]);
} else if (spelloutput && spelloutput === "POWERCARD") {
// Always regenerate the powercard macro when switching to POWERCARD. Do NOT
// gate this on an existing attackid: if it's empty, a spell whose rollcontent
// is a stale %{...attack} call (or broken/empty) never gets reset to the spell
// template, so the button keeps firing the old ATTACK roll. Remove the attack
// only if one actually exists.
const lvl2 = parseInt(v[repeating_source + "_spelllevel"], 10);
if (attackid && attackid != "") { remove_attack(attackid); }
update_spelloutput(higherlevels, lvl2, repeating_source, spelloutput, licensedsheet);
}
});
});
});
['spell-prime','spell-1','spell-2','spell-3','spell-4','spell-5','spell-6','spell-7','spell-8','spell-9'].forEach(attr => {
on(`change:repeating_${attr}:spellathigherlevels`, (eventinfo) => {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {return;}
const spellid = eventinfo.sourceAttribute.split("_")[2], repeating_source = `repeating_${attr}_${spellid}`;
getAttrs([`${repeating_source}_spelllevel`, `${repeating_source}_spelloutput`], function(v) {
const higherlevels = eventinfo.newValue;
const lvl = parseInt(v[repeating_source + "_spelllevel"], 10);
const spelloutput = v[repeating_source + "_spelloutput"];
if (spelloutput && spelloutput === "POWERCARD") {
update_spelloutput(higherlevels, lvl, repeating_source, spelloutput, licensedsheet);
}
});
});
});
const update_spelloutput = (higherlevels, lvl, repeating_source, spelloutput, licensedsheet) => {
let update = {};
// POWERCARD is a descriptive card with no damage roll, so it displays the base rank
// statically. A ?{Cast at what level?} query inside the &{template:spell} level field
// breaks the macro (Roll20 silently drops the whole message) for EVERY power with "At
// Higher Ranks" text; the higher-rank info is already shown via {{athigherlevels=...}}
// and the ATTACK output handles real cast-level scaling. (higherlevels/lvl params kept
// for signature compatibility with existing callers.)
// Must stay byte-identical to dropFunctions.buildSpellQuery() and the sheet:opened
// POWERCARD_MACRO self-heal, or the self-heal rewrites this on the next open (thrash).
update[repeating_source + "_rollcontent"] = `@{wtype}&{template:spell} {{level=Rank @{spelllevel} @{spellschool}}} {{name=@{spellname}}} {{castingtime=@{spellcastingtime}}} {{range=@{spellrange}}} {{target=@{spelltarget}}} {{pointcost=@{pointcost}}} @{spellcomp_v} @{spellcomp_s} @{spellcomp_m} {{material=@{spellcomp_materials}}} {{duration=@{spellduration}}} {{description=@{spelldescription}}} {{athigherlevels=@{spellathigherlevels}}} @{spellritual} {{innate=@{innate}}} @{spellconcentration} @{charname_output}`;
setAttrs(update, {silent: true});
};
// Self-heal for the POWERCARD macro. Earlier sheet versions baked a ?{Cast at what level?}
// query into each power's rollcontent (via buildSpellQuery on compendium drop, and the old
// update_spelloutput). That query inside the &{template:spell} {{level=...}} field silently
// breaks the macro for any power with higher-rank text. On open, normalize every non-ATTACK
// spell's rollcontent to the current canonical macro: repairs broken macros (old query,
// stale %{...attack} calls, empty) AND upgrades older working macros to the latest template
// (e.g. to add the Point Cost line). ATTACK spells keep their %{...attack} call. Already-
// canonical rows are skipped, so there's no thrash — all generators emit this exact string.
on("sheet:opened", function() {
const POWERCARD_MACRO = `@{wtype}&{template:spell} {{level=Rank @{spelllevel} @{spellschool}}} {{name=@{spellname}}} {{castingtime=@{spellcastingtime}}} {{range=@{spellrange}}} {{target=@{spelltarget}}} {{pointcost=@{pointcost}}} @{spellcomp_v} @{spellcomp_s} @{spellcomp_m} {{material=@{spellcomp_materials}}} {{duration=@{spellduration}}} {{description=@{spelldescription}}} {{athigherlevels=@{spellathigherlevels}}} @{spellritual} {{innate=@{innate}}} @{spellconcentration} @{charname_output}`;
['spell-prime','spell-1','spell-2','spell-3','spell-4','spell-5','spell-6','spell-7','spell-8','spell-9'].forEach(function(attr) {
getSectionIDs(`repeating_${attr}`, function(ids) {
if(!ids || !ids.length) { return; }
const fields = [];
ids.forEach(function(id) {
fields.push(`repeating_${attr}_${id}_rollcontent`, `repeating_${attr}_${id}_spelloutput`);
});
getAttrs(fields, function(v) {
const update = {};
ids.forEach(function(id) {
const rcKey = `repeating_${attr}_${id}_rollcontent`;
const output = v[`repeating_${attr}_${id}_spelloutput`];
const rc = v[rcKey] || "";
if(output !== "ATTACK" && rc !== POWERCARD_MACRO) {
update[rcKey] = POWERCARD_MACRO;
}
});
if(Object.keys(update).length) { setAttrs(update, {silent: true}); }
});
});
});
});
on("change:class change:custom_class change:cust_classname change:cust_hitdietype change:cust_spellcasting_ability change:cust_spellslots change:cust_strength_save_prof change:cust_dexterity_save_prof change:cust_constitution_save_prof change:cust_intelligence_save_prof change:cust_wisdom_save_prof change:cust_charisma_save_prof change:subclass change:multiclass1 change:multiclass1_subclass change:multiclass2 change:multiclass2_subclass change:multiclass3 change:multiclass3_subclass" , function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_class();
});
on("change:base_level change:multiclass1_flag change:multiclass1 change:multiclass1_lvl change:multiclass2_flag change:multiclass2 change:multiclass2_lvl change:multiclass3_flag change:multiclass3 change:multiclass3_lvl change:arcane_fighter change:arcane_rogue", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
set_level();
});
// Cybermancer Talent Slot auto-calculation.
// Fires when class or level changes; sets slots_max, slot_rank_max, and slots_used
// from the Cybermancer level progression table (analogous to Warlock pact slots).
var cybermancerSlotTable = [
// [slots, rank] index = level - 1
[1,1],[2,1],[2,2],[2,2],[2,3],[2,3],[2,4],[2,4],[2,5],[2,5],
[3,5],[3,5],[3,5],[3,5],[3,5],[3,5],[4,5],[4,5],[4,5],[4,5]
];
var update_cybermancer_slots = function() {
getAttrs(["class", "base_level"], function(values) {
var className = (values["class"] || "").toLowerCase().trim();
if(className !== "cybermancer") return;
var level = parseInt(values["base_level"], 10) || 1;
level = Math.min(Math.max(level, 1), 20);
var entry = cybermancerSlotTable[level - 1];
setAttrs({ slots_max: entry[0], slot_rank_max: entry[1], slots_used: entry[0] });
});
};
on("change:class change:base_level", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") return;
update_cybermancer_slots();
});
on("change:level_calculations change:caster_level change:lvl1_slots_mod change:lvl2_slots_mod change:lvl3_slots_mod change:lvl4_slots_mod change:lvl5_slots_mod change:lvl6_slots_mod change:lvl7_slots_mod change:lvl8_slots_mod change:lvl9_slots_mod", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
getAttrs(["level_calculations"], function(v) {
if(!v["level_calculations"] || v["level_calculations"] == "on") {
update_spell_slots();
};
});
});
on("change:caster_level", function(eventinfo) {
getAttrs(["caster_level","npc"], function(v) {
var casterlvl = v["caster_level"] && !isNaN(parseInt(v["caster_level"], 10)) ? parseInt(v["caster_level"], 10) : 0;
if(v["npc"] && v["npc"] == 1 && casterlvl > 0) {
setAttrs({level: casterlvl})
};
});
});
on("change:pb_type change:pb_custom", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_pb();
});
on("change:dtype", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_attacks("all");
update_npc_action("all");
});
on("change:default_critical_range", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_attacks("all");
});
on("change:jack_of_all_trades", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_jack_attr();
update_all_ability_checks();
});
on("change:reliable_talent", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_skills(["athletics","acrobatics","sleight_of_hand","stealth","astrophysics","lore","investigation","xenobiology","computers","mechanics","insight","medicine","perception","survival","deception","intimidation","performance","persuasion","animal_handling","arcana","history","nature","religion"]);
});
on("change:show_5e_skills", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {return;}
update_skills(["animal_handling","arcana","history","nature","religion"]);
});
// ---- Feature: Hero Points ----
var updateHeroPoints = function(level) {
getAttrs(["use_hero_points", "level"], values => {
if (!values["use_hero_points"] || values["use_hero_points"] != "1") return;
const lvl = level !== undefined ? parseInt(level) : (parseInt(values["level"]) || 0);
setAttrs({ hero_points: Math.floor(lvl / 2) + 5 }, { silent: true });
});
};
on("change:consumed_hero_points", () => {
getAttrs(["hero_points"], values => {
const hp = values["hero_points"] ? parseInt(values["hero_points"]) : 0;
setAttrs({ hero_points: Math.max(hp - 1, 0) }, { silent: true });
});
});
on("change:hero_points", eventInfo => {
const hp = eventInfo.newValue ? Math.max(parseInt(eventInfo.newValue), 0) : 0;
setAttrs({ hero_points: hp }, { silent: true });
});
on("change:use_hero_points", eventInfo => {
if (eventInfo.newValue && eventInfo.newValue == "1") updateHeroPoints();
});
on("change:initmod change:init_tiebreaker", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_initiative();
});
on("change:spellcasting_ability change:spell_dc_mod change:globalmagicmod", function(eventinfo) {
if(eventinfo.sourceType && eventinfo.sourceType === "sheetworker") {
return;
}
update_spell_info();
});
on("change:npc_challenge", function() {
update_challenge();
});
on("change:npc_str_save_base change:npc_dex_save_base change:npc_con_save_base change:npc_int_save_base change:npc_wis_save_base change:npc_cha_save_base", function(eventinfo) {
update_npc_saves();
});
on("change:npc_acrobatics_base change:npc_astrophysics_base change:npc_athletics_base change:npc_computers_base change:npc_deception_base change:npc_insight_base change:npc_intimidation_base change:npc_investigation_base change:npc_lore_base change:npc_mechanics_base change:npc_medicine_base change:npc_perception_base change:npc_performance_base change:npc_persuasion_base change:npc_piloting_base change:npc_sleight_of_hand_base change:npc_stealth_base change:npc_survival_base change:npc_xenobiology_base", function(eventinfo) {
update_npc_skills();
});
on("change:repeating_npcaction:attack_flag change:repeating_npcaction:attack_type change:repeating_npcaction:attack_range change:repeating_npcaction:attack_target change:repeating_npcaction:attack_tohit change:repeating_npcaction:attack_damage change:repeating_npcaction:attack_damagetype change:repeating_npcaction:attack_damage2 change:repeating_npcaction:attack_damagetype2 change:repeating_npcaction-l:attack_flag change:repeating_npcaction-l:attack_type change:repeating_npcaction-l:attack_range change:repeating_npcaction-l:attack_target change:repeating_npcaction-l:attack_tohit change:repeating_npcaction-l:attack_damage change:repeating_npcaction-l:attack_damagetype change:repeating_npcaction-l:attack_damage2 change:repeating_npcaction-l:attack_damagetype2 change:repeating_npcaction:show_desc change:repeating_npcaction-l:show_desc change:repeating_npcaction:description change:repeating_npcaction-l:description", function(eventinfo) {
const actionid = eventinfo.sourceAttribute.split("_")[2];
const legendary = eventinfo.sourceAttribute.includes("npcaction-l") ? true : false;
update_npc_action(actionid, legendary);
});
on("change:core_die change:halflingluck_flag", function() {
getAttrs(["core_die","halflingluck_flag"], function(v) {
core = v.core_die && v.core_die != "" ? v.core_die : "1d20";
luck = v.halflingluck_flag && v.halflingluck_flag === "1" ? "ro<1" : "";
update = {};
update["d20"] = core + luck;
if(!v.core_die || v.core_die === "") {
update["core_die"] = "1d20";
}
setAttrs(update);
});
});
[`ac`,`attack`,'save','skill',].forEach(attr => {
on(`change:global_${attr}_mod_flag`, (eventinfo) => {
const mod = (attr === "attack") ? "tohitmod" : `${attr}mod`;
if(eventinfo.newValue === "1") {
const firstAttr = (attr === "ac") ? "val" : "roll";
const firstAttrValue = (attr === "ac") ? 1 : "1d4";
const secondAttrValue = (attr === "ac") ? "Defense" : (attr === "skill") ? "GUIDANCE" : "BLESS";
getSectionIDs(mod, (ids) => {
if(!ids || ids.length === 0) {
var update = {};
var rowid = generateRowID();
update[`repeating_${mod}_${rowid}_global_${attr}_${firstAttr}`]= `${firstAttrValue}`;
update[`repeating_${mod}_${rowid}_global_${attr}_name`] = `${secondAttrValue}`;
update[`repeating_${mod}_${rowid}_global_${attr}_active_flag`] = "1";
setAttrs(update);
}
});
} else {
getSectionIDs(mod, function(ids) {
var update = {};
var rowid = generateRowID();
_.each(ids, function(rowid) {
update[`repeating_${mod}_${rowid}_global_${attr}_active_flag`] = "0";
});
setAttrs(update);
});
}
});
});
on("change:repeating_skillmod remove:repeating_skillmod", function(eventinfo) {
update_globalskills();
});
on("change:repeating_savemod remove:repeating_savemod", function(eventinfo) {
update_globalsaves();
});
on("change:repeating_tohitmod remove:repeating_tohitmod", function(eventinfo) {
update_globalattack();
});
on("change:repeating_acmod remove:repeating_acmod", function(eventinfo) {
update_ac();
});
on("change:confirm", function(eventinfo) {
setAttrs({monster_confirm_flag: ""});
getAttrs(["drop_category"], function(v) {
if(v["drop_category"]) {
handle_drop(v["drop_category"]);
}
});
});
on("change:cancel", function(eventinfo) {
setAttrs({monster_confirm_flag: ""});
var update = {};
update["drop_category"] = "";
update["drop_name"] = "";
update["drop_data"] = "";
update["drop_content"] = "";
setAttrs(update, {silent: true});
});
on("change:mancer_confirm", function(eventinfo) {
setAttrs({mancer_confirm_flag: "", charactermancer_step: "l1-welcome"}, function() {
check_l1_mancer();
});
});
on("change:mancer_cancel", function(eventinfo) {
setAttrs({mancer_confirm_flag: "", l1mancer_status: "completed"}, function() {
check_l1_mancer();
});
});
on("change:mancer_npc", function(eventinfo) {
setAttrs({mancer_confirm_flag: "", l1mancer_status: "completed", npc: "1"}, function() {
check_l1_mancer();
});
});
on("change:passiveperceptionmod", function(eventinfo) {
update_passive_perception();
});
on("remove:repeating_inventory", function(eventinfo) {
var itemid = eventinfo.sourceAttribute.substring(20, 40);
var attackids = eventinfo.removedInfo && eventinfo.removedInfo["repeating_inventory_" + itemid + "_itemattackid"] ? eventinfo.removedInfo["repeating_inventory_" + itemid + "_itemattackid"] : undefined;
var resourceid = eventinfo.removedInfo && eventinfo.removedInfo["repeating_inventory_" + itemid + "_itemresourceid"] ? eventinfo.removedInfo["repeating_inventory_" + itemid + "_itemresourceid"] : undefined;
if(attackids) {
_.each(attackids.split(","), function(value) { remove_attack(value); });
}
if(resourceid) {
remove_resource(resourceid);
}
if(eventinfo.removedInfo && eventinfo.removedInfo["repeating_inventory_" + itemid + "_itemmodifiers"]) {
check_itemmodifiers(eventinfo.removedInfo["repeating_inventory_" + itemid + "_itemmodifiers"]);
}
update_weight();
});
on("remove:repeating_attack", function(eventinfo) {
var attackid = eventinfo.sourceAttribute.substring(17, 37);
var itemid = eventinfo.removedInfo["repeating_attack_" + attackid + "_itemid"];
var spellid = eventinfo.removedInfo["repeating_attack_" + attackid + "_spellid"];
var spelllvl = eventinfo.removedInfo["repeating_attack_" + attackid + "_spelllevel"];
if(itemid) {
getAttrs(["repeating_inventory_" + itemid + "_hasattack"], function(v) {
if(v["repeating_inventory_" + itemid + "_hasattack"] && v["repeating_inventory_" + itemid + "_hasattack"] == 1) {
var update = {};
update["repeating_inventory_" + itemid + "_itemattackid"] = "";
update["repeating_inventory_" + itemid + "_hasattack"] = 0;
setAttrs(update, {silent: true});
}
});
};
if(spellid && spelllvl) {
getAttrs(["repeating_spell-" + spelllvl + "_" + spellid + "_spelloutput"], function(v) {
if(v["repeating_spell-" + spelllvl + "_" + spellid + "_spelloutput"] && v["repeating_spell-" + spelllvl + "_" + spellid + "_spelloutput"] == "ATTACK") {
var update = {};
update["repeating_spell-" + spelllvl + "_" + spellid + "_spellattackid"] = "";
update["repeating_spell-" + spelllvl + "_" + spellid + "_spelloutput"] = "POWERCARD";
setAttrs(update, {silent: true});
}
});
};
});
on("remove:repeating_resource", function(eventinfo) {
const resourceid = eventinfo.sourceAttribute.substring(19, 39);
let update = {};
_.each(['left','right'], (side) => {
const itemid = eventinfo.removedInfo[`repeating_resource_${resourceid}_resource_${side}_itemid`];
if (itemid) {
update[`repeating_inventory_${itemid}_useasresource`] = 0;
update[`repeating_inventory_${itemid}_itemresourceid`] = "";
};
});
setAttrs(update, {silent: true});
});
on("remove:repeating_spell-prime remove:repeating_spell-1 remove:repeating_spell-2 remove:repeating_spell-3 remove:repeating_spell-4 remove:repeating_spell-5 remove:repeating_spell-6 remove:repeating_spell-7 remove:repeating_spell-8 remove:repeating_spell-9", function(eventinfo) {
var attackid = eventinfo.removedInfo[eventinfo.sourceAttribute + "_spellattackid"];
if(attackid) {
remove_attack(attackid);
}
});
on("clicked:relaunch_lvl1mancer", function(eventinfo) {
getAttrs(["l1mancer_status"], function(v) {
if(v["l1mancer_status"] === "completed") {
setAttrs({l1mancer_status: "relaunch"});
}
check_l1_mancer();
});
});
on("clicked:launch_lvl+mancer", function(eventinfo) {
getAttrs(["class", "level", "hp_max", "custom_class","multiclass1_flag", "multiclass2_flag", "multiclass3_flag", "multiclass1", "multiclass2", "multiclass3"], function(v) {
var throw_warning = false;
var class_array = [v["class"]];
if(!v["class"] || !v["hp_max"] || isNaN(parseInt(v["hp_max"], 10)) || !v["level"] || isNaN(parseInt(v["level"], 10)) || parseInt(v["level"], 10) < 1 || (v["multiclass2_flag"] == 1 && v["multiclass1_flag"] == 0) || (v["multiclass3_flag"] == 1 && v["multiclass2_flag"] == 0) ) {
throw_warning = true;
};
if(v["multiclass1_flag"] == 1) {class_array.push(v["multiclass1"])};
if(v["multiclass2_flag"] == 1) {class_array.push(v["multiclass2"])};
if(v["multiclass3_flag"] == 1) {class_array.push(v["multiclass3"])};
// Check to see if there are any duplicate multiclasses
if((new Set(class_array)).size !== class_array.length) {
throw_warning = true;
};
if(throw_warning === true) {
setAttrs({leveler_warningflag: "show"});
return;
};
setAttrs({lpmancer_status: "active"}, function() {
startCharactermancer("lp-welcome");
});
});
});
on("change:experience", function(eventinfo) {
update_leveler_display();
});
var update_attr = function(attr) {
var update = {};
var attr_fields = [attr + "_base",attr + "_bonus"];
getSectionIDs("repeating_inventory", function(idarray) {
_.each(idarray, function(currentID, i) {
attr_fields.push("repeating_inventory_" + currentID + "_equipped");
attr_fields.push("repeating_inventory_" + currentID + "_itemmodifiers");
});
getAttrs(attr_fields, function(v) {
var base = v[attr + "_base"] && !isNaN(parseInt(v[attr + "_base"], 10)) ? parseInt(v[attr + "_base"], 10) : 10;
var bonus = v[attr + "_bonus"] && !isNaN(parseInt(v[attr + "_bonus"], 10)) ? parseInt(v[attr + "_bonus"], 10) : 0;
var item_base = 0;
var item_bonus = 0;
_.each(idarray, function(currentID) {
if((!v["repeating_inventory_" + currentID + "_equipped"] || v["repeating_inventory_" + currentID + "_equipped"] === "1") && v["repeating_inventory_" + currentID + "_itemmodifiers"] && v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf(attr > -1)) {
var mods = v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().split(",");
_.each(mods, function(mod) {
if(mod.indexOf(attr) > -1 && mod.indexOf("save") === -1) {
if(mod.indexOf(":") > -1) {
var new_base = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
item_base = new_base && new_base > item_base ? new_base : item_base;
}
else if(mod.indexOf("-") > -1) {
var new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
item_bonus = new_mod ? item_bonus - new_mod : item_bonus;
}
else {
var new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
item_bonus = new_mod ? item_bonus + new_mod : item_bonus;
}
};
});
}
});
update[attr + "_flag"] = bonus > 0 || item_bonus > 0 || item_base > base ? 1 : 0;
base = base > item_base ? base : item_base;
update[attr] = base + bonus + item_bonus;
setAttrs(update);
});
});
};
var update_mod = function (attr) {
getAttrs([attr], function(v) {
var attr_abr = attr.substring(0,3);
var finalattr = v[attr] && isNaN(v[attr]) === false ? Math.floor((parseInt(v[attr], 10) - 10) / 2) : 0;
var update = {};
update[attr + "_mod"] = finalattr;
update["npc_" + attr_abr + "_negative"] = v[attr] && !isNaN(v[attr]) && parseInt(v[attr], 10) < 10 ? 1 : 0;
setAttrs(update);
});
};
var update_save = function (attr) {
var save_attrs = [attr + "_mod", attr + "_save_prof", attr + "_save_mod","pb","globalsavemod","pb_type"];
getSectionIDs("repeating_inventory", function(idarray) {
_.each(idarray, function(currentID, i) {
save_attrs.push("repeating_inventory_" + currentID + "_equipped");
save_attrs.push("repeating_inventory_" + currentID + "_itemmodifiers");
});
getAttrs(save_attrs, function(v) {
var attr_mod = v[attr + "_mod"] ? parseInt(v[attr + "_mod"], 10) : 0;
var prof = v[attr + "_save_prof"] && v[attr + "_save_prof"] != 0 && !isNaN(v["pb"]) ? parseInt(v["pb"], 10) : 0;
var save_mod = v[attr + "_save_mod"] && !isNaN(parseInt(v[attr + "_save_mod"], 10)) ? parseInt(v[attr + "_save_mod"], 10) : 0;
var global = v["globalsavemod"] && !isNaN(v["globalsavemod"]) ? parseInt(v["globalsavemod"], 10) : 0;
var items = 0;
_.each(idarray, function(currentID) {
if(v["repeating_inventory_" + currentID + "_equipped"] && v["repeating_inventory_" + currentID + "_equipped"] === "1" && v["repeating_inventory_" + currentID + "_itemmodifiers"] && (v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf("saving throws") > -1 || v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf(attr + " save") > -1)) {
var mods = v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().split(",");
_.each(mods, function(mod) {
if(mod.indexOf(attr + " save") > -1) {
var substr = mod.slice(mod.lastIndexOf(attr + " save") + attr.length + " save".length);
var bonus = substr && substr.length > 0 && !isNaN(parseInt(substr,10)) ? parseInt(substr,10) : 0;
}
else if(mod.indexOf("saving throws") > -1) {
var substr = mod.slice(mod.lastIndexOf("saving throws") + "saving throws".length);
var bonus = substr && substr.length > 0 && !isNaN(parseInt(substr,10)) ? parseInt(substr,10) : 0;
};
if(bonus && bonus != 0) {
items = items + bonus;
};
});
}
});
var total = attr_mod + prof + save_mod + global + items;
if(v["pb_type"] && v["pb_type"] === "die" && v[attr + "_save_prof"] != 0 && attr != "death") {
total = total + "+" + v["pb"];
};
var update = {};
update[attr + "_save_bonus"] = total;
setAttrs(update, {silent: true});
});
});
};
var update_all_saves = function() {
update_save("strength");
update_save("dexterity");
update_save("constitution");
update_save("intelligence");
update_save("wisdom");
update_save("charisma");
update_save("death");
};
var update_all_ability_checks = function(){
update_initiative();
update_skills(["athletics", "acrobatics", "sleight_of_hand", "stealth", "astrophysics", "lore", "investigation", "xenobiology", "computers", "mechanics", "insight", "medicine", "perception", "survival", "deception", "intimidation", "performance", "persuasion", "piloting", "animal_handling", "arcana", "history", "nature", "religion"]);
};
var update_skills = function (skills_array) {
var skill_parent = {athletics: "strength", acrobatics: "dexterity", sleight_of_hand: "dexterity", stealth: "dexterity", astrophysics: "intelligence", lore: "intelligence", investigation: "intelligence", xenobiology: "intelligence", computers: "intelligence", mechanics: "wisdom", insight: "wisdom", medicine: "wisdom", perception: "wisdom", survival: "wisdom", deception: "charisma", intimidation: "charisma", performance: "charisma", persuasion: "charisma", piloting: "dexterity",
animal_handling: "wisdom", arcana: "intelligence", history: "intelligence", nature: "intelligence", religion: "intelligence"};
var attrs_to_get = ["pb","pb_type","jack_of_all_trades","jack","reliable_talent"];
var update = {};
var callbacks = [];
if(skills_array.indexOf("perception") > -1) {
callbacks.push( function() {update_passive_perception();} )
};
_.each(skills_array, function(s) {
if(skill_parent[s] && attrs_to_get.indexOf(skill_parent[s]) === -1) {attrs_to_get.push(skill_parent[s] + "_mod")};
attrs_to_get.push(s + "_prof");
attrs_to_get.push(s + "_type");
attrs_to_get.push(s + "_flat");
});
getSectionIDs("repeating_inventory", function(idarray) {
_.each(idarray, function(currentID, i) {
attrs_to_get.push("repeating_inventory_" + currentID + "_equipped");
attrs_to_get.push("repeating_inventory_" + currentID + "_itemmodifiers");
});
getAttrs(attrs_to_get, function(v) {
console.log("UPDATING SKILLS");
_.each(skills_array, function(s) {
var attr_mod = v[skill_parent[s] + "_mod"] ? parseInt(v[skill_parent[s] + "_mod"], 10) : 0;
var prof = v[s + "_prof"] != 0 && !isNaN(v["pb"]) ? parseInt(v["pb"], 10) : 0;
var flat = v[s + "_flat"] && !isNaN(parseInt(v[s + "_flat"], 10)) ? parseInt(v[s + "_flat"], 10) : 0;
var type = v[s + "_type"] && !isNaN(parseInt(v[s + "_type"], 10)) ? parseInt(v[s + "_type"], 10) : 1;
var jack = v["jack_of_all_trades"] && v["jack_of_all_trades"] != 0 && v["jack"] ? v["jack"] : 0;
var item_bonus = 0;
_.each(idarray, function(currentID) {
if(v["repeating_inventory_" + currentID + "_equipped"] && v["repeating_inventory_" + currentID + "_equipped"] === "1" && v["repeating_inventory_" + currentID + "_itemmodifiers"] && (v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().replace(/ /g,"_").indexOf(s) > -1 || v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf("ability checks") > -1)) {
var mods = v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().split(",");
_.each(mods, function(mod) {
if(mod.replace(/ /g,"_").indexOf(s) > -1 || mod.indexOf("ability checks") > -1) {
if(mod.indexOf("-") > -1) {
var new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
item_bonus = new_mod ? item_bonus - new_mod : item_bonus;
}
else {
var new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
item_bonus = new_mod ? item_bonus + new_mod : item_bonus;
}
};
});
};
});
var total = attr_mod + flat + item_bonus;
if(v["pb_type"] && v["pb_type"] === "die") {
if(v[s + "_prof"] != 0) {
type === 1 ? "" : "2"
total = total + "+" + type + v["pb"];
}
else if(v[s + "_prof"] == 0 && jack != 0) {
total = total + "+" + jack;
};
}
else {
if(v[s + "_prof"] != 0) {
total = total + (prof * type);
}
else if(v[s + "_prof"] == 0 && jack != 0) {
total = total + parseInt(jack, 10);
};
};
update[s + "_bonus"] = total;
// Reliable Talent: floor skill die at 10 for proficient skills (Specialist class feature)
const isProficient = v[s + "_prof"] && v[s + "_prof"] != 0;
const hasReliableTalent = v["reliable_talent"] && parseInt(v["reliable_talent"]) === 10;
update[s + "_d20"] = (isProficient && hasReliableTalent) ? "{@{d20},0d20+10}k1" : "@{d20}";
});
setAttrs(update, {silent: true}, function() {callbacks.forEach(function(callback) {callback(); })} );
});
});
};
var update_tool = function(tool_id) {
//D&D 5e Mancer: Land Vehicles proficiency does not drop with Marine background (UC748)
//Added a test to check for an undefined tool_id so to prevent similar errors.
//By Miguel Peres
if(typeof tool_id == "undefined") return;
if(tool_id.substring(0,1) === "-" && tool_id.length === 20) {
do_update_tool([tool_id]);
}
else if(tool_id === "all") {
getSectionIDs("repeating_tool", function(idarray) {
do_update_tool(idarray);
});
}
else {
getSectionIDs("repeating_tool", function(idarray) {
var tool_attribs = [];
_.each(idarray, function(id) {
tool_attribs.push("repeating_tool_" + id + "_toolattr_base");
});
getAttrs(tool_attribs, function(v) {
var attr_tool_ids = [];
_.each(idarray, function(id) {
if(v["repeating_tool_" + id + "_toolattr_base"] && v["repeating_tool_" + id + "_toolattr_base"].indexOf(tool_id) > -1) {
attr_tool_ids.push(id);
}
});
if(attr_tool_ids.length > 0) {
do_update_tool(attr_tool_ids);
}
});
});
};
};
var do_update_tool = function(tool_array) {
var tool_attribs = ["pb","pb_type","jack","strength_mod","dexterity_mod","constitution_mod","intelligence_mod","wisdom_mod","charisma_mod"];
var update = {};
_.each(tool_array, function(tool_id) {
tool_attribs.push("repeating_tool_" + tool_id + "_toolbonus_base");
tool_attribs.push("repeating_tool_" + tool_id + "_tool_mod");
tool_attribs.push("repeating_tool_" + tool_id + "_toolattr_base");
});
getAttrs(tool_attribs, function(v) {
_.each(tool_array, function(tool_id) {
console.log("UPDATING TOOL: " + tool_id);
var query = false;
if(v["repeating_tool_" + tool_id + "_toolattr_base"] && v["repeating_tool_" + tool_id + "_toolattr_base"].substring(0,2) === "?{") {
update["repeating_tool_" + tool_id + "_toolattr"] = "QUERY";
var mod = "?{Attribute?|Strength,@{strength_mod}|Dexterity,@{dexterity_mod}|Constitution,@{constitution_mod}|Intelligence,@{intelligence_mod}|Wisdom,@{wisdom_mod}|Charisma,@{charisma_mod}}";
if(v["repeating_tool_" + tool_id + "_tool_mod"]) {
mod = mod + "+" + v["repeating_tool_" + tool_id + "_tool_mod"];
}
query = true;
} else {
var attr = v["repeating_tool_" + tool_id + "_toolattr_base"].substring(0, v["repeating_tool_" + tool_id + "_toolattr_base"].length - 5).substr(2);
var attr_mod = v[attr + "_mod"] ? parseInt(v[attr + "_mod"], 10) : 0;
var tool_mod = v["repeating_tool_" + tool_id + "_tool_mod"] && !isNaN(parseInt(v["repeating_tool_" + tool_id + "_tool_mod"], 10)) ? parseInt(v["repeating_tool_" + tool_id + "_tool_mod"], 10) : 0;
var mod = attr_mod + tool_mod;
update["repeating_tool_" + tool_id + "_toolattr"] = attr.toUpperCase();
if(v["repeating_tool_" + tool_id + "_tool_mod"] && v["repeating_tool_" + tool_id + "_tool_mod"].indexOf("@{") > -1) {
update["repeating_tool_" + tool_id + "_toolbonus"] = update["repeating_tool_" + tool_id + "_toolbonus"] + "+" + v["repeating_tool_" + tool_id + "_tool_mod"];
}
if(!v["repeating_tool_" + tool_id + "_tool_mod"]) {
update["repeating_tool_" + tool_id + "_tool_mod"] = 0;
}
};
if(v["pb_type"] && v["pb_type"] === "die" ) {
if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(@{pb})") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + "+" + v.pb}
else if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(@{pb}*2)") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + "+2" + v.pb}
else if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(floor(@{pb}/2))") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + "+" + v.jack};
}
else if(v["repeating_tool_" + tool_id + "_toolattr_base"] && v["repeating_tool_" + tool_id + "_toolattr_base"].substring(0,2) === "?{") {
if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(@{pb})") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + "+" + parseInt(v.pb, 10)}
else if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(@{pb}*2)") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + "+" + (parseInt(v.pb, 10)*2)}
else if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(floor(@{pb}/2))") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + "+" + parseInt(v.jack, 10)};
}
else {
if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(@{pb})") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + parseInt(v.pb, 10)}
else if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(@{pb}*2)") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + (parseInt(v.pb, 10)*2)}
else if(v["repeating_tool_" + tool_id + "_toolbonus_base"] === "(floor(@{pb}/2))") {update["repeating_tool_" + tool_id + "_toolbonus"] = mod + parseInt(v.jack, 10)};
};
if(query) {
update["repeating_tool_" + tool_id + "_toolbonus_display"] = "?";
}
else {
update["repeating_tool_" + tool_id + "_toolbonus_display"] = update["repeating_tool_" + tool_id + "_toolbonus"];
};
});
setAttrs(update, {silent: true});
});
};
var get_repeating_data = function(callback) {
var getallrepeating = function(getobj, thiscallback, attrlist) {
attrlist = attrlist || [];
var thisget = getobj.pop();
getSectionIDs(thisget.name, function(ids) {
_.each(ids, function(sectionId) {
_.each(thisget.list, function(attr) {
attrlist.push("repeating_" + thisget.name + "_" + sectionId + "_" + attr);
});
});
if(getobj.length > 0) {
getallrepeating(getobj, thiscallback, attrlist);
} else {
getAttrs(attrlist, function(vals) {
thiscallback(vals);
});
};
});
};
var getList = [
{name: "proficiencies", list: ["name"]},
{name: "tool", list: ["toolname", "toolattr_base"]},
{name: "traits", list: ["name", "source", "source_type"]},
{name: "resource", list: ["resource_left_name", "resource_right_name"]},
{name: "spell-prime", list: ["spellname", "spellattackid", "spellsource", "spellattackid"]},
{name: "savemod", list: ["global_save_name"]},
{name: "tohitmod", list: ["global_attack_name"]},
{name: "damagemod", list: ["global_damage_name"]},
{name: "acmod", list: ["global_ac_name"]},
{name: "skillmod", list: ["global_skill_name"]},
{name: "attack", list: ["atkname", "spellid"]},
{name: "hpmod", list: ["levels", "source", "mod"]}
];
for(var x=1; x<=9; x++) {
getList.push({ name: "spell-" + x, list: ["spellname", "spellattackid", "spellsource", "spellattackid"] });
}
var repeating = {prof_names: [], traits: []};
_.each(getList, function(section) {
if(!["proficiencies", "traits"].includes(section.name)) {
repeating[section.name] = {};
}
});
getallrepeating(getList, function(vals) {
var traitstemp = {};
_.each(vals, function(value, name) {
if(name.split("_")[1] == "proficiencies") {
repeating.prof_names.push(value.toLowerCase());
} else if(name.split("_")[1] == "tool") {
repeating.tool[name.split("_")[2]] = repeating.tool[name.split("_")[2]] || {};
let attr = _.last(name.split("_"));
repeating.tool[name.split("_")[2]][attr] = value.toLowerCase();
if(attr == "toolname") repeating.prof_names.push(value.toLowerCase());
} else if (name.split("_")[1] == "traits") {
traitstemp[name.split("_")[2]] = traitstemp[name.split("_")[2]] ? traitstemp[name.split("_")[2]] : {};
traitstemp[name.split("_")[2]][_.last(name.split("_"))] = value;
} else if (name.split("_")[1] == "resource") {
repeating.resource[name.split("_")[2]] = repeating.resource[name.split("_")[2]] || {};
repeating.resource[name.split("_")[2]][name.split("_")[4]] = value;
} else if (name.split("_")[1] == "hpmod") {
repeating.hpmod[name.split("_")[2]] = repeating.hpmod[name.split("_")[2]] || {};
repeating.hpmod[name.split("_")[2]][name.split("_")[3]] = value;
} else if (name.split("_")[1].split("-")[0] == "spell") {
repeating[name.split("_")[1]][name.split("_")[2]] = repeating[name.split("_")[1]][name.split("_")[2]] || {};
repeating[name.split("_")[1]][name.split("_")[2]][name.split("_")[3]] = value;
} else if (name.split("_")[1] == "attack") {
repeating[name.split("_")[1]][name.split("_")[2]] = repeating[name.split("_")[1]][name.split("_")[2]] || {};
repeating[name.split("_")[1]][name.split("_")[2]][name.split("_")[3]] = value;
} else {
repeating[name.split("_")[1]][name.split("_")[2]] = value;
}
});
_.each(traitstemp, function(v, k) {
var trait = _.clone(v);
trait.id = k;
repeating.traits.push(trait);
});
callback(repeating);
});
};
var check_itemmodifiers = function(modifiers, previousValue) {
var mods = modifiers.toLowerCase().split(",");
if(previousValue) {
prevmods = previousValue.toLowerCase().split(",");
mods = _.union(mods, prevmods);
};
_.each(mods, function(mod) {
if(mod.indexOf("ac:") > -1 || mod.indexOf("ac +") > -1 || mod.indexOf("ac -") > -1) {update_ac();};
if(mod.indexOf("spell") > -1) {update_spell_info();};
if(mod.indexOf("saving throws") > -1) {update_all_saves();};
if(mod.indexOf("strength save") > -1) {update_save("strength");} else if(mod.indexOf("strength") > -1) {update_attr("strength");};
if(mod.indexOf("dexterity save") > -1) {update_save("dexterity");} else if(mod.indexOf("dexterity") > -1) {update_attr("dexterity");};
if(mod.indexOf("constitution save") > -1) {update_save("constitution");} else if(mod.indexOf("constitution") > -1) {update_attr("constitution");};
if(mod.indexOf("intelligence save") > -1) {update_save("intelligence");} else if(mod.indexOf("intelligence") > -1) {update_attr("intelligence");};
if(mod.indexOf("wisdom save") > -1) {update_save("wisdom");} else if(mod.indexOf("wisdom") > -1) {update_attr("wisdom");};
if(mod.indexOf("charisma save") > -1) {update_save("charisma");} else if(mod.indexOf("charisma") > -1) {update_attr("charisma");};
if(mod.indexOf("ability checks") > -1) {update_all_ability_checks();};
if(mod.indexOf("acrobatics") > -1) {update_skills(["acrobatics"]);};
if(mod.indexOf("astrophysics") > -1) {update_skills(["astrophysics"]);};
if(mod.indexOf("athletics") > -1) {update_skills(["athletics"]);};
if(mod.indexOf("computers") > -1) {update_skills(["computers"]);};
if(mod.indexOf("deception") > -1) {update_skills(["deception"]);};
if(mod.indexOf("insight") > -1) {update_skills(["insight"]);};
if(mod.indexOf("intimidation") > -1) {update_skills(["intimidation"]);};
if(mod.indexOf("investigation") > -1) {update_skills(["investigation"]);};
if(mod.indexOf("lore") > -1) {update_skills(["lore"]);};
if(mod.indexOf("mechanics") > -1) {update_skills(["mechanics"]);};
if(mod.indexOf("medicine") > -1) {update_skills(["medicine"]);};
if(mod.indexOf("perception") > -1) {update_skills(["perception"]);};
if(mod.indexOf("performance") > -1) {update_skills(["performance"]);};
if(mod.indexOf("persuasion") > -1) {update_skills(["persuasion"]);};
if(mod.indexOf("piloting") > -1) {update_skills(["piloting"]);};
if(mod.indexOf("sleight of hand") > -1) {update_skills(["sleight_of_hand"]);};
if(mod.indexOf("stealth") > -1) {update_skills(["stealth"]);};
if(mod.indexOf("survival") > -1) {update_skills(["survival"]);};
if(mod.indexOf("xenobiology") > -1) {update_skills(["xenobiology"]);};
});
};
var create_attack_from_item = function(itemid, options) {
var update = {};
var newrowid = generateRowID();
update["repeating_inventory_" + itemid + "_itemattackid"] = newrowid;
if(options && options.versatile) {
var newrowid2 = generateRowID();
update["repeating_inventory_" + itemid + "_itemattackid"] += "," + newrowid2;
setAttrs(update, {}, function() {
update_attack_from_item(itemid, newrowid, {newattack: true, versatile: "primary"});
update_attack_from_item(itemid, newrowid2, {newattack: true, versatile: "secondary"});
});
}
else {
setAttrs(update, {}, update_attack_from_item(itemid, newrowid, {newattack: true}));
}
};
var update_attack_from_item = function(itemid, attackid, options) {
getAttrs(["repeating_inventory_" + itemid + "_itemname","repeating_inventory_" + itemid + "_itemproperties","repeating_inventory_" + itemid + "_itemmodifiers", "strength", "dexterity"], function(v) {
var update = {}; var itemtype; var damage; var damagetype; var damage2; var damagetype2; var attackmod; var damagemod; var range;
var alt = {};
if(options && options.newattack) {
update["repeating_attack_" + attackid + "_options-flag"] = "0";
update["repeating_attack_" + attackid + "_itemid"] = itemid;
}
if(v["repeating_inventory_" + itemid + "_itemmodifiers"] && v["repeating_inventory_" + itemid + "_itemmodifiers"] != "") {
var mods = v["repeating_inventory_" + itemid + "_itemmodifiers"].split(",");
_.each(mods, function(mod) {
if(mod.indexOf("Item Type:") > -1) {itemtype = mod.split(":")[1].trim()}
else if(mod.indexOf("Alternate Secondary Damage Type:") > -1) {alt.damagetype2 = mod.split(":")[1].trim();}
else if(mod.indexOf("Alternate Secondary Damage:") > -1) {alt.damage2 = mod.split(":")[1].trim();}
else if(mod.indexOf("Alternate Damage Type:") > -1) {alt.damagetype = mod.split(":")[1].trim();}
else if(mod.indexOf("Alternate Damage:") > -1) {alt.damage = mod.split(":")[1].trim();}
else if(mod.indexOf("Secondary Damage Type:") > -1) {damagetype2 = mod.split(":")[1].trim()}
else if(mod.indexOf("Secondary Damage:") > -1) {damage2 = mod.split(":")[1].trim()}
else if(mod.indexOf("Damage Type:") > -1) {damagetype = mod.split(":")[1].trim()}
else if(mod.indexOf("Damage:") > -1) {damage = mod.split(":")[1].trim()}
else if(mod.indexOf("Range:") > -1) {range = mod.split(":")[1].trim()}
else if(mod.indexOf(" Attacks ") > -1) {attackmod = mod.split(" Attacks ")[1].trim()}
else if(mod.indexOf(" Damage ") > -1) {damagemod = mod.split(" Damage ")[1].trim()}
});
}
if(v["repeating_inventory_" + itemid + "_itemname"] && v["repeating_inventory_" + itemid + "_itemname"] != "") {
update["repeating_attack_" + attackid + "_atkname"] = v["repeating_inventory_" + itemid + "_itemname"];
if(options && options.versatile === "primary") {
update["repeating_attack_" + attackid + "_atkname"] += " (One-Handed)";
} else if(options && options.versatile === "secondary") {
update["repeating_attack_" + attackid + "_atkname"] += " (Two-Handed)";
}
}
if(options && options.versatile === "secondary") {
if(alt.damage) {
update["repeating_attack_" + attackid + "_dmgbase"] = alt.damage;
}
if(alt.damagetype) {
update["repeating_attack_" + attackid + "_dmgtype"] = alt.damagetype;
}
if(alt.damage2) {
update["repeating_attack_" + attackid + "_dmg2base"] = alt.damage2;
update["repeating_attack_" + attackid + "_dmg2attr"] = 0;
update["repeating_attack_" + attackid + "_dmg2flag"] = "{{damage=1}} {{dmg2flag=1}}";
}
if(alt.damagetype2) {
update["repeating_attack_" + attackid + "_dmg2type"] = alt.damagetype2;
}
update["repeating_attack_" + attackid + "_versatile_alt"] = "1";
}
else {
if(damage) {
update["repeating_attack_" + attackid + "_dmgbase"] = damage;
}
if(damagetype) {
update["repeating_attack_" + attackid + "_dmgtype"] = damagetype;
}
if(damage2) {
update["repeating_attack_" + attackid + "_dmg2base"] = damage2;
update["repeating_attack_" + attackid + "_dmg2attr"] = 0;
update["repeating_attack_" + attackid + "_dmg2flag"] = "{{damage=1}} {{dmg2flag=1}}";
}
if(damagetype2) {
update["repeating_attack_" + attackid + "_dmg2type"] = damagetype2;
}
}
if(range) {
update["repeating_attack_" + attackid + "_atkrange"] = range;
}
var finesse = v["repeating_inventory_" + itemid + "_itemproperties"] && /finesse/i.test(v["repeating_inventory_" + itemid + "_itemproperties"]);
if( (itemtype && itemtype.indexOf("Ranged") > -1) || (finesse && +v.dexterity > +v.strength)) {
update["repeating_attack_" + attackid + "_atkattr_base"] = "@{dexterity_mod}";
update["repeating_attack_" + attackid + "_dmgattr"] = "@{dexterity_mod}";
}
else {
update["repeating_attack_" + attackid + "_atkattr_base"] = "@{strength_mod}";
update["repeating_attack_" + attackid + "_dmgattr"] = "@{strength_mod}";
}
if(attackmod && damagemod && attackmod == damagemod) {
update["repeating_attack_" + attackid + "_atkmagic"] = parseInt(attackmod, 10);
update["repeating_attack_" + attackid + "_atkmod"] = "";
update["repeating_attack_" + attackid + "_dmgmod"] = "";
}
else {
if(attackmod) {
update["repeating_attack_" + attackid + "_atkmod"] = parseInt(attackmod, 10);
}
if(damagemod) {
update["repeating_attack_" + attackid + "_dmgmod"] = parseInt(damagemod, 10);
}
update["repeating_attack_" + attackid + "_atkmagic"] = "";
}
var callback = function() {update_attacks(attackid, "item")};
setAttrs(update, {silent: true}, callback);
});
};
const create_resource_from_item = (itemid) => {
const newrowid = generateRowID();
let update = {};
getAttrs(["other_resource_name"], (v) => {
//Use other_resource if it is empty
if (!v.other_resource_name || v.other_resource_name == "") {
update[`repeating_inventory_${itemid}_itemresourceid`] = "other_resource";
setAttrs(update, {}, update_resource_from_item(itemid, "other_resource", true));
//If other_resource is populated look through the repeating sections for an empty spot
} else {
getSectionIDs(`repeating_resource`, (idarray) => {
if (idarray.length == 0) {
update[`repeating_inventory_${itemid}_itemresourceid`] = `${newrowid}_resource_left`;
setAttrs(update, {}, update_resource_from_item(itemid, `${newrowid}_resource_left`, true));
} else {
let array = [];
_.each(idarray, (currentID, i) => {
["left", "right"].forEach(side => {
array.push(`repeating_resource_${currentID}_resource_${side}`);
array.push(`repeating_resource_${currentID}_resource_${side}_name`);
array.push(`repeating_resource_${currentID}_resource_${side}_max`);
});
});
getAttrs(array, (y) => {
let existing = false;
_.each(idarray, (currentID, i) => {
["left", "right"].forEach(side => {
const Name = y[`repeating_resource_${currentID}_resource_${side}_name`] || false;
const Value = y[`repeating_resource_${currentID}_resource_${side}`] || false;
const Max = y[`repeating_resource_${currentID}_resource_${side}_max`] || false;
//If Name, Value, & Max are empty and existing === false then populate a resource there
if ((!Name && !Value && !Max) && existing === false) {
update[`repeating_inventory_${itemid}_itemresourceid`] = `${currentID}_resource_${side}`;
setAttrs(update, {}, update_resource_from_item(itemid, `${currentID}_resource_${side}`, true));
existing = true;
};
});
});
//If nothing is empty then generatae a new row
if(!existing) {
update[`repeating_inventory_${itemid}_itemresourceid`] = `${newrowid}_resource_left`;
setAttrs(update, {}, update_resource_from_item(itemid, `${newrowid}_resource_left`, true));
};
});
};
});
};
});
};
var update_resource_from_item = function(itemid, resourceid, newresource) {
getAttrs(["repeating_inventory_" + itemid + "_itemname","repeating_inventory_" + itemid + "_itemcount"], function(v) {
var update = {}; var id;
if(resourceid && resourceid == "other_resource") {
id = resourceid;
}
else {
id = "repeating_resource_" + resourceid;
};
if(newresource) {
update[id + "_itemid"] = itemid;
} ;
if(!v["repeating_inventory_" + itemid + "_itemname"]) {
update["repeating_inventory_" + itemid + "_useasresource"] = 0;
update["repeating_inventory_" + itemid + "_itemresourceid"] = "";
remove_resource(resourceid);
};
if(v["repeating_inventory_" + itemid + "_itemname"] && v["repeating_inventory_" + itemid + "_itemname"] != "") {
update[id + "_name"] = v["repeating_inventory_" + itemid + "_itemname"];
};
if(v["repeating_inventory_" + itemid + "_itemcount"] && v["repeating_inventory_" + itemid + "_itemcount"] != "") {
update[id] = v["repeating_inventory_" + itemid + "_itemcount"];
};
setAttrs(update, {silent: true});
});
};
const create_attack_from_spell = function(lvl, spellid, character_id) {
let update = {};
const newrowid = generateRowID();
update[`repeating_spell-${lvl}_${spellid}_spellattackid`] = newrowid;
update[`repeating_spell-${lvl}_${spellid}_rollcontent`] = `%{${character_id}|repeating_attack_${newrowid}_attack}`;
setAttrs(update, {}, update_attack_from_spell(lvl, spellid, newrowid, true));
}
const update_attack_from_spell = (lvl, spellid, attackid, newattack) => {
const repeating_spell = `repeating_spell-${lvl}_${spellid}`;
const repeating_attack = `repeating_attack_${attackid}`;
getAttrs([`${repeating_spell}_spellname`, `${repeating_spell}_spellrange`, `${repeating_spell}_spelltarget`, `${repeating_spell}_spellattack`, `${repeating_spell}_spelldamage`, `${repeating_spell}_spelldamage2`,`${repeating_spell}_spelldamagetype`,`${repeating_spell}_spelldamagetype2`,`${repeating_spell}_spellhealing`,`${repeating_spell}_spelldmgmod`,`${repeating_spell}_spellsave`,`${repeating_spell}_spellsavesuccess`,`${repeating_spell}_spellhldie`,`${repeating_spell}_spellhldietype`,`${repeating_spell}_spellhlbonus`,`${repeating_spell}_spelllevel`,`${repeating_spell}_includedesc`,`${repeating_spell}_spelldescription`,`${repeating_spell}_spellathigherlevels`,`${repeating_spell}_spell_damage_progression`,`${repeating_spell}_innate`,`${repeating_spell}_spell_ability`,"spellcasting_ability"], (v) => {
let update = {};
let description = "";
const spellAbility = (v[`${repeating_spell}_spell_ability`] != "spell") ? v[`${repeating_spell}_spell_ability`].slice(0, -1) : "spell";
update[`${repeating_attack}_atkattr_base`] = spellAbility;
if (newattack) {
update[`${repeating_attack}_options-flag`] = "0";
update[`${repeating_attack}_spellid`] = spellid;
update[`${repeating_attack}_spelllevel`] = lvl;
}
if (v[`${repeating_spell}_spell_ability`] == "spell") {
update[`${repeating_attack}_savedc`] = "(@{spell_save_dc})";
} else if (v[`${repeating_spell}_spell_ability`]) {
update[`${repeating_attack}_savedc`] = `(${spellAbility}+8+@{spell_dc_mod}+@{pb})`;
}
if (v[`${repeating_spell}_spellname`] && v[`${repeating_spell}_spellname`] != "") {
update[`${repeating_attack}_atkname`] = v[`${repeating_spell}_spellname`];
}
update[`${repeating_attack}_atkflag`] = (!v[`${repeating_spell}_spellattack`] || v[`${repeating_spell}_spellattack`] === "None") ? "0" : "{{attack=1}}";
if (v[`${repeating_spell}_spellattack`] || !v[`${repeating_spell}_spellattack`] === "None") {
description = description + v[`${repeating_spell}_spellattack`] + " Power Attack. ";
};
if (v[`${repeating_spell}_spelldamage`] && v[`${repeating_spell}_spelldamage`] != "") {
update[`${repeating_attack}_dmgbase`] =
(v[`${repeating_spell}_spell_damage_progression`] && v[`${repeating_spell}_spell_damage_progression`] === "Prime Dice") ? "[[round((@{level} + 1) / 6 + 0.5)]]" + v[`${repeating_spell}_spelldamage`].substring(1) :
v[`${repeating_spell}_spelldamage`];
};
update[`${repeating_attack}_dmgflag`] = (v[`${repeating_spell}_spelldamage`] && v[`${repeating_spell}_spelldamage`] != "") ? "{{damage=1}} {{dmg1flag=1}}" : "0";
update[`${repeating_attack}_dmgattr`] = (v[`${repeating_spell}_spelldmgmod`] && v[`${repeating_spell}_spelldmgmod`] === "Yes") ? spellAbility : "0";
update[`${repeating_attack}_dmgtype`] = (v[`${repeating_spell}_spelldamagetype`]) ? v[`${repeating_spell}_spelldamagetype`] : "";
update[`${repeating_attack}_dmg2base`] = (v[`${repeating_spell}_spelldamage2`]) ? v[`${repeating_spell}_spelldamage2`] : "";
update[`${repeating_attack}_dmg2attr`] = "0";
update[`${repeating_attack}_dmg2flag`] = (v[`${repeating_spell}_spelldamage2`]) ? "{{damage=1}} {{dmg2flag=1}}" : 0;
update[`${repeating_attack}_dmg2type`] = (v[`${repeating_spell}_spelldamagetype2`]) ? v[`${repeating_spell}_spelldamagetype2`] : "";
update[`${repeating_attack}_atkrange`] = (v[`${repeating_spell}_spellrange`]) ? v[`${repeating_spell}_spellrange`] : "";
update[`${repeating_attack}_saveflag`] = (v[`${repeating_spell}_spellsave`]) ? "{{save=1}} {{saveattr=@{saveattr}}} {{savedesc=@{saveeffect}}} {{savedc=[[[[@{savedc}]][SAVE]]]}}" : "0";
update[`${repeating_attack}_saveeffect`] = (v[`${repeating_spell}_spellsavesuccess`]) ? v[`${repeating_spell}_spellsavesuccess`] : "";
if(v[`${repeating_spell}_spellsave`]) {
update[`${repeating_attack}_saveattr`] = v[`${repeating_spell}_spellsave`];
};
if(v[`${repeating_spell}_spellhldie`] && v[`${repeating_spell}_spellhldie`] != "" && v[`${repeating_spell}_spellhldietype`] && v[`${repeating_spell}_spellhldietype`] != "") {
let bonus = "";
const spelllevel = v[`${repeating_spell}_spelllevel`];
let query = "?{Cast at what level?";
for(i = 0; i < 10-spelllevel; i++) {
query = query + "|Level " + (parseInt(i, 10) + parseInt(spelllevel, 10)) + "," + i;
}
query = query + "}";
if(v[`${repeating_spell}_spellhlbonus`] && v[`${repeating_spell}_spellhlbonus`] != "") {
bonus = "+(" + v[`${repeating_spell}_spellhlbonus`] + "*" + query + ")";
}
update[`${repeating_attack}_hldmg`] = `{{hldmg=[[(${v[`${repeating_spell}_spellhldie`]}*${query})${v[`${repeating_spell}_spellhldietype`]}${bonus}]]}}`;
} else {
update[`${repeating_attack}_hldmg`] = "";
}
if (v[`${repeating_spell}_spellhealing`] && v[`${repeating_spell}_spellhealing`] != "") {
if(!v[`${repeating_spell}_spelldamage`] || v[`${repeating_spell}_spelldamage`] === "") {
update[`${repeating_attack}_dmgbase`] = v[`${repeating_spell}_spellhealing`];
update[`${repeating_attack}_dmgflag`] = "{{damage=1}} {{dmg1flag=1}}";
update[`${repeating_attack}_dmgtype`] = "Healing";
} else if (!v[`${repeating_spell}_spelldamage2`] || v[`${repeating_spell}_spelldamage2`] === "") {
update[`${repeating_attack}_dmg2base`] = v[`${repeating_spell}_spellhealing`];
update[`${repeating_attack}_dmg2flag`] = "{{damage=1}} {{dmg2flag=1}}";
update[`${repeating_attack}_dmg2type`] = "Healing";
}
}
update[`${repeating_attack}_spell_innate`] = (v[`${repeating_spell}_innate`]) ? v[`${repeating_spell}_innate`] : "";
if(v[`${repeating_spell}_spelltarget`]) {
description = description + v[`${repeating_spell}_spelltarget`] + ". ";
}
if(v[`${repeating_spell}_includedesc`] && v[`${repeating_spell}_includedesc`] === "on") {
description = v[`${repeating_spell}_spelldescription`];
if(v[`${repeating_spell}_spellathigherlevels`] && v[`${repeating_spell}_spellathigherlevels`] != "") {
description = description + "\n\nAt Higher Ranks: " + v[`${repeating_spell}_spellathigherlevels`];
}
} else if(v[`${repeating_spell}_includedesc`] && v[`${repeating_spell}_includedesc`] === "off") {
description = "";
};
update[`${repeating_attack}_atk_desc`] = description;
var callback = () => {update_attacks(attackid, "spell")};
setAttrs(update, {silent: true}, callback);
});
};
const update_attacks = (update_id, source) => {
if (update_id.substring(0,1) === "-" && update_id.length === 20) {
do_update_attack([update_id], source);
} else if (["strength","dexterity","constitution","intelligence","wisdom","charisma","spells","all"].indexOf(update_id) > -1) {
getSectionIDs("repeating_attack", (idarray) => {
if (update_id === "all") {
do_update_attack(idarray);
} else if (update_id === "spells") {
let attack_attribs = [];
_.each(idarray, (id) => {
attack_attribs.push(`repeating_attack_${id}_spellid`, `repeating_attack_${id}_spelllevel`);
});
getAttrs(attack_attribs, (v) => {
const filteredIds = _.filter(idarray, (id) => {
return v[`repeating_attack_${id}_spellid`] && v[`repeating_attack_${id}_spellid`] != "";
});
let spell_attacks = {};
_.each(filteredIds, (attack_id) => {
spell_attacks[attack_id] = {
spell_id: v[`repeating_attack_${attack_id}_spellid`],
spell_lvl: v[`repeating_attack_${attack_id}_spelllevel`]
};
});
_.each(spell_attacks, (data, attack_id) => { update_attack_from_spell(data.spell_lvl, data.spell_id, attack_id); });
});
} else {
let attack_attribs = ["spellcasting_ability"];
_.each(idarray, (id) => {
attack_attribs.push(`repeating_attack_${id}_atkattr_base`);
attack_attribs.push(`repeating_attack_${id}_dmgattr`);
attack_attribs.push(`repeating_attack_${id}_dmg2attr`);
attack_attribs.push(`repeating_attack_${id}_savedc`);
});
getAttrs(attack_attribs, (v) => {
let attr_attack_ids = [];
_.each(idarray, (id) => {
if ((v[`repeating_attack_${id}_atkattr_base`] && v[`repeating_attack_${id}_atkattr_base`].indexOf(update_id) > -1) || (v[`repeating_attack_${id}_dmgattr`] && v[`repeating_attack_${id}_dmgattr`].indexOf(update_id) > -1) || (v[`repeating_attack_${id}_dmg2attr`] && v[`repeating_attack_${id}_dmg2attr`].indexOf(update_id) > -1) || (v[`repeating_attack_${id}_savedc`] && v[`repeating_attack_${id}_savedc`].indexOf(update_id) > -1) || (v[`repeating_attack_${id}_savedc`] && v[`repeating_attack_${id}_savedc`] === "(@{spell_save_dc})" && v["spellcasting_ability"] && v["spellcasting_ability"].indexOf(update_id) > -1)) {
attr_attack_ids.push(id);
}
});
if (attr_attack_ids.length > 0) {
do_update_attack(attr_attack_ids);
}
});
};
});
};
};
const do_update_attack = (attack_array, source) => {
let attack_attribs = ["level","d20","pb","pb_type","pbd_safe","dtype","globalmagicmod","strength_mod","dexterity_mod","constitution_mod","intelligence_mod","wisdom_mod","charisma_mod","spellcasting_ability","spell_save_dc","spell_attack_mod", "spell_dc_mod", "global_damage_mod_roll", "global_damage_mod_crit", "default_critical_range"];
_.each(attack_array, (attackid) => {
["atkflag","atkname","atkattr_base","atkmod","atkprofflag","atkmagic","dmgflag","dmgbase","dmgattr","dmgmod","dmgtype","dmg2flag","dmg2base","dmg2attr","dmg2mod","dmg2type","dmgcustcrit","dmg2custcrit","saveflag","savedc","saveeffect","saveflat","hldmg","spellid","spelllevel","atkrange","itemid","ammo",].forEach((attr) => {
attack_attribs.push(`repeating_attack_${attackid}_${attr}`)
});
});
getAttrs(attack_attribs, (v) => {
_.each(attack_array, (attackid) => {
const repeating_attack = `repeating_attack_${attackid}`;
let callbacks = [];
let update = {};
let hbonus = "";
let hdmg1 = "";
let hdmg2 = "";
let dmg = "";
let dmg2 = "";
let rollbase = "";
let spellattack = false;
let magicattackmod = 0;
let magicsavemod = 0;
let atkattr_abrev = "";
let dmgattr_abrev = "";
let dmg2attr_abrev = "";
//PROFICIENCY BONUS select in Settings.
const pbd_safe = v["pbd_safe"] || "";
//HIGHER LVL CAST DMG found in Powers for spells like Fireball
const hldmgcrit = (v[`${repeating_attack}_hldmg`] && v[`${repeating_attack}_hldmg`] != "") ? v[`${repeating_attack}_hldmg`].slice(0, 7) + "crit" + v[`${repeating_attack}_hldmg`].slice(7) : "";
if (v[`${repeating_attack}_spellid`] && v[`${repeating_attack}_spellid`] != "") {
spellattack = true;
//GLOBAL POWER ATTACK MODIFIER in Settings
magicattackmod = parseInt(v["spell_attack_mod"],10) || 0;
//POWER SAVE DC MOD in Settings
magicsavemod = parseInt(v["spell_dc_mod"],10) || 0;
};
//ATTACK select inside settings for the repeating attack
if (!v[`${repeating_attack}_atkattr_base`] || v[`${repeating_attack}_atkattr_base`] === "0") {
atkattr_base = 0
} else if (v[`${repeating_attack}_atkattr_base`] && v[`${repeating_attack}_atkattr_base`] === "spell") {
atkattr_base = parseInt(v[v["spellcasting_ability"].substring(2, v["spellcasting_ability"].length - 2)], 10);
atkattr_abrev = v["spellcasting_ability"].substring(2, 5).toUpperCase();
} else {
atkattr_base = parseInt(v[v[`${repeating_attack}_atkattr_base`].substring(2, v[`${repeating_attack}_atkattr_base`].length - 1)], 10);
atkattr_abrev = v[`${repeating_attack}_atkattr_base`].substring(2, 5).toUpperCase();
};
//DAMAGE 1 ability select inside settings for the repeating attack
if (!v[`${repeating_attack}_dmgattr`] || v[`${repeating_attack}_dmgattr`] === "0") {
dmgattr = 0;
} else if (v[`${repeating_attack}_dmgattr`] && v[`${repeating_attack}_dmgattr`] === "spell") {
dmgattr = parseInt(v[v["spellcasting_ability"].substring(2, v["spellcasting_ability"].length - 2)], 10);
dmgattr_abrev = v["spellcasting_ability"].substring(2, 5).toUpperCase();
} else {
dmgattr = parseInt(v[v[`${repeating_attack}_dmgattr`].substring(2, v[`${repeating_attack}_dmgattr`].length - 1)], 10);
dmgattr_abrev = v[`${repeating_attack}_dmgattr`].substring(2, 5).toUpperCase();
};
//DAMAGE 2 ability select inside settings for the repeating attack
if (!v[`${repeating_attack}_dmg2attr`] || v[`${repeating_attack}_dmg2attr`] === "0") {
dmg2attr = 0;
} else if (v[`${repeating_attack}_dmg2attr`] && v[`${repeating_attack}_dmg2attr`] === "spell") {
dmg2attr = parseInt(v[v["spellcasting_ability"].substring(2, v["spellcasting_ability"].length - 2)], 10);
dmg2attr_abrev = v["spellcasting_ability"].substring(2, 5).toUpperCase();
} else {
dmg2attr = parseInt(v[v[`${repeating_attack}_dmg2attr`].substring(2, v[`${repeating_attack}_dmg2attr`].length - 1)], 10);
dmg2attr_abrev =v[`${repeating_attack}_dmg2attr`].substring(2, 5).toUpperCase();
};
//DAMAGE first input inside settings for the repeating attack
const dmgbase = v[`${repeating_attack}_dmgbase`] || 0;
const dmg2base = v[`${repeating_attack}_dmg2base`] || 0;
//DAMAGE 1 second input inside settings for the repeating attack
const dmgmod = parseInt(v[`${repeating_attack}_dmgmod`],10) || 0;
//DAMAGE 2 second input inside settings for the repeating attack
const dmg2mod = parseInt(v[`${repeating_attack}_dmg2mod`],10) || 0;
//DAMAGE 1 TYPE input inside settings for the repeating attack
const dmgtype = v[`${repeating_attack}_dmgtype`] || "";
//DAMAGE 2 TYPE input inside settings for the repeating attack
const dmg2type = v[`${repeating_attack}_dmg2type`] || "";
//PROFICIENT flag inside settings for the repeating attack && PROFICIENCY BONUS from Settings
const atkprofflag = v[`${repeating_attack}_atkprofflag`] || 0;
const pb = (atkprofflag != 0 && v.pb) ? v.pb : 0;
//ATTACK input inside settings for the repeating attack
const atkmod = parseInt(v[`${repeating_attack}_atkmod`],10) || 0;
//MAGIC BONUS input inside settings for the repeating attack
const atkmag = parseInt(v[`${repeating_attack}_atkmagic`],10) || 0;
//used in _atkdmgtype display
const dmgmag = (isNaN(atkmag) === false && atkmag != 0 && ((v[`${repeating_attack}_dmgflag`] && v[`${repeating_attack}_dmgflag`] != 0) || (v[`${repeating_attack}_dmg2flag`] && v[`${repeating_attack}_dmg2flag`] != 0))) ? `+ ${atkmag} Magic Bonus` : "";
//ATTACK checkbox inside settings for the repeating attack
const atkflag = v[`${repeating_attack}_atkflag`] || 0;
if (atkflag != 0) {
bonus_mod = atkattr_base + atkmod + atkmag + magicattackmod;
plus_minus = (bonus_mod > -1) ? "+" : "";
//pb_type is PROFICIENCY BONUS select in Settings
if (v["pb_type"] && v["pb_type"] === "die") {
bonus = `${bonus_mod}+${pb}`;
} else {
bonus_mod = bonus_mod + parseInt(pb, 10);
bonus = plus_minus + bonus_mod;
};
//SAVING THROW checkbox inside settings for the repeating attack
} else if (v[`${repeating_attack}_saveflag`] && v[`${repeating_attack}_saveflag`] != 0) {
const saveDC = v[`${repeating_attack}_savedc`] || "";
//SAVING THROW VS DC select "Spell" inside settings for the repeating attack
if (!saveDC || saveDC === "(@{spell_save_dc})") {
const tempdc = v["spell_save_dc"];
bonus = "DC" + tempdc;
//SAVING THROW VS DC select "FLAT" inside settings for the repeating attack
} else if (saveDC === "(@{saveflat})") {
const tempdc = parseInt(v[`${repeating_attack}_saveflat`]) || 0;
bonus = "DC" + tempdc;
//SAVING THROW VS DC select ability score inside settings for the repeating attack
} else {
const savedcattr = v[`${repeating_attack}_savedc`].split("_mod")[0].slice(3);
const safe_pb = v["pb_type"] && v["pb_type"] === "die" ? parseInt(pb.substring(1), 10) / 2 : parseInt(pb,10);
const safe_attr = v[`${savedcattr}_mod`] ? parseInt(v[`${savedcattr}_mod`],10) : 0;
const tempdc = 8 + safe_attr + safe_pb + magicsavemod;
bonus = "DC" + tempdc;
};
} else {
bonus = "-";
}
//DAMAGE checkbox inside settings for the repeating attack
const dmgflag1 = v[`${repeating_attack}_dmgflag`] || 0;
const dmgflag2 = v[`${repeating_attack}_dmg2flag`] || 0;
if (dmgflag1 != 0) {
if (spellattack === true && dmgbase.indexOf("[[round((@{level} + 1) / 6 + 0.5)]]") > -1) {
// SPECIAL CANTRIP DAMAGE
dmgdiestring = Math.round(((parseInt(v["level"], 10) + 1) / 6) + 0.5).toString();
dmg = dmgdiestring + dmgbase.substring(dmgbase.lastIndexOf("d"));
//select & input to the right of damage
if (dmgattr + dmgmod != 0) {
dmg += `+${(dmgattr + dmgmod)}`;
}
dmg += ` ${dmgtype}`;
} else {
if (dmgbase === 0 && (dmgattr + dmgmod === 0)) {
dmg = 0;
}
if (dmgbase != 0) {
dmg = dmgbase;
}
if (dmgbase != 0 && (dmgattr + dmgmod != 0)) {
dmg = (dmgattr + dmgmod > 0) ? dmg + "+" : dmg;
}
if (dmgattr + dmgmod != 0) {
dmg = dmg + (dmgattr + dmgmod);
}
dmg = dmg + " " + dmgtype;
}
};
if (dmgflag2 != 0) {
if (dmg2base === 0 && (dmg2attr + dmg2mod === 0)) {
dmg2 = 0;
}
if (dmg2base != 0) {
dmg2 = dmg2base;
}
if (dmg2base != 0 && (dmg2attr + dmg2mod != 0)) {
dmg2 = (dmg2attr + dmg2mod > 0) ? dmg2 + "+" : dmg2;
}
if (dmg2attr + dmg2mod != 0) {
dmg2 = dmg2 + (dmg2attr + dmg2mod);
}
dmg2 = dmg2 + " " + dmg2type;
};
update[`${repeating_attack}_atkdmgtype`] = (dmgflag1 != 0 && dmgflag2 != 0) ? `${dmg} + ${dmg2}${dmgmag} `:`${dmg}${dmg2}${dmgmag} `;
//Build ROLL TEMPLATE with below variables
//ATTACK checkbox inside settings for the repeating attack
if (atkflag != 0) {
if (atkattr_base != 0) {hbonus += ` + ${atkattr_base}[${atkattr_abrev}]`};
if (atkmod != 0) {hbonus += ` + ${atkmod}[MOD]`};
if (pb != 0) {hbonus += ` + ${pb}${pbd_safe}[PROF]`};
if (atkmag != 0) {hbonus += ` + ${atkmag}[MAGIC]`};
if (magicattackmod != 0) {hbonus += ` + ${magicattackmod}[SPELLATK]`};
}
//DAMAGE 1 checkbox inside settings for the repeating attack
if (dmgflag1 != 0) {
hdmg1 += dmgbase;
if (dmgattr != 0) {hdmg1 += ` + ${dmgattr}[${dmgattr_abrev}]`};
if (dmgmod != 0) {hdmg1 += ` + ${dmgmod}[MOD]`};
if (atkmag != 0) {hdmg1 += ` + ${atkmag}[MAGIC]`};
} else {
hdmg1 += "0";
}
//DAMAGE 2 checkbox inside settings for the repeating attack
if (dmgflag2 != 0) {
hdmg2 += dmg2base
if (dmg2attr != 0) {hdmg2 += ` + ${dmg2attr}[${dmg2attr_abrev}]`};
if (dmg2mod != 0) {hdmg2 += ` + ${dmg2mod}[MOD]`};
} else {
hdmg2 += "0";
}
//CRIT input inside settings for the repeating attack
let crit1 = v[`${repeating_attack}_dmgcustcrit`] || 0;
let crit2 = v[`${repeating_attack}_dmg2custcrit`] || 0;
crit1 = (crit1 != 0) ? v[`${repeating_attack}_dmgcustcrit`] : dmgbase;
crit2 = (crit2 != 0) ? v[`${repeating_attack}_dmg2custcrit`] : dmg2base;
r1 = (atkflag != 0) ? "@{d20}" : "0d20";
r2 = (atkflag != 0) ? "@{rtype}" : "{{r2=[[0d20";
//set in the GLOBAL DAMAGE MODIFIER section
let globaldamage = `[[${v.global_damage_mod_roll || 0}]]`;
let globaldamagecrit = `[[${v.global_damage_mod_crit || 0}]]`;
//Assamble Roll Templates
// Use default_critical_range if set and lower than the per-attack range (e.g. Warrior subclass expanded crit)
const criticalrange =
v[`${repeating_attack}_atkcritrange`] &&
v["default_critical_range"] &&
parseInt(v["default_critical_range"]) < parseInt(v[`${repeating_attack}_atkcritrange`])
? "@{default_critical_range}"
: "@{atkcritrange}";
if (v.dtype === "full") {
rollbase = `@{wtype}&{template:atkdmg} {{mod=@{atkbonus}}} {{rname=@{atkname}}} {{r1=[[${r1}cs>${criticalrange}${hbonus}]]}} ${r2}cs>${criticalrange}${hbonus}]]}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[[${hdmg1}]]}} {{dmg1type=${dmgtype}}} @{dmg2flag} {{dmg2=[[${hdmg2}]]}} {{dmg2type=${dmg2type}}} {{crit1=[[${crit1}[CRIT]]]}} {{crit2=[[${crit2}[CRIT]]]}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} ${hldmgcrit} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globalattack=@{global_attack_mod}}} {{globaldamage=${globaldamage}}} {{globaldamagecrit=${globaldamagecrit}}} {{globaldamagetype=@{global_damage_mod_type}}} ammo=@{ammo} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
} else if (atkflag != 0) {
rollbase = `@{wtype}&{template:atk} {{mod=@{atkbonus}}} {{rname=[@{atkname}](~repeating_attack_attack_dmg)}} {{rnamec=[@{atkname}](~repeating_attack_attack_crit)}} {{r1=[[${r1}cs>${criticalrange}${hbonus}]]}} ${r2}cs>${criticalrange}${hbonus}]]}} {{range=@{atkrange}}} {{desc=@{atk_desc}}} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globalattack=@{global_attack_mod}}} ammo=@{ammo} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
} else if (dmgflag1 != 0) {
rollbase = `@{wtype}&{template:dmg} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[[${hdmg1}]]}} {{dmg1type=${dmgtype}}} @{dmg2flag} {{dmg2=[[${hdmg2}]]}} {{dmg2type=${dmg2type}}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=${globaldamage}}} {{globaldamagetype=@{global_damage_mod_type}}} ammo=@{ammo} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
} else {
rollbase = `@{wtype}&{template:dmg} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{saveflag} {{desc=@{atk_desc}}} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} ammo=@{ammo} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
}
update[`${repeating_attack}_rollbase_dmg`] = `@{wtype}&{template:dmg} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[[${hdmg1}]]}} {{dmg1type=${dmgtype}}} @{dmg2flag} {{dmg2=[[${hdmg2}]]}} {{dmg2type=${dmg2type}}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=${globaldamage}}} {{globaldamagetype=@{global_damage_mod_type}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
update[`${repeating_attack}_rollbase_crit`] = `@{wtype}&{template:dmg} {{crit=1}} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[[${hdmg1}]]}} {{dmg1type=${dmgtype}}} @{dmg2flag} {{dmg2=[[${hdmg2}]]}} {{dmg2type=${dmg2type}}} {{crit1=[[${crit1}]]}} {{crit2=[[${crit2}]]}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} ${hldmgcrit} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=${globaldamage}}} {{globaldamagecrit=${globaldamagecrit}}} {{globaldamagetype=@{global_damage_mod_type}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
update[`${repeating_attack}_atkbonus`] = bonus;
update[`${repeating_attack}_rollbase`] = rollbase;
if (v[`${repeating_attack}_spellid`] && v[`${repeating_attack}_spellid`] != "" && (!source || source && source != "spell") && v[`${repeating_attack}_spellid`].length == 20) {
const spellid = v[`${repeating_attack}_spellid`];
const lvl = v[`${repeating_attack}_spelllevel`];
callbacks.push(() => {
update_spell_from_attack(lvl, spellid, attackid);
});
}
if (v[`${repeating_attack}_itemid`] && v[`${repeating_attack}_itemid`] != "" && (!source || source && source != "item")) {
const itemid = v[`${repeating_attack}_itemid`];
callbacks.push(() => {
update_item_from_attack(itemid, attackid);
});
}
setAttrs(update, {silent: true}, () => {
callbacks.forEach((callback) => {
callback();
});
});
});
});
};
//Update spells in the attack section
const update_spell_from_attack = (lvl, spellid, attackid) => {
const repeating_attack = `repeating_attack_${attackid}`;
const repeating_spell = `repeating_spell-${lvl}_${spellid}`;
let update = {};
getAttrs([`${repeating_attack}_atkname`, `${repeating_attack}_atkrange`, `${repeating_attack}_atkflag`, `${repeating_attack}_atkattr_base`, `${repeating_attack}_dmgbase`, `${repeating_attack}_dmgtype`, `${repeating_attack}_dmg2base`, `${repeating_attack}_dmg2type`, `${repeating_attack}_saveflag`, `${repeating_attack}_saveattr`, `${repeating_attack}_saveeffect`], (v) => {
update[`${repeating_spell}_spellname`] = v[`${repeating_attack}_atkname`];
update[`${repeating_spell}_spellrange`] = v[`${repeating_attack}_atkrange`] || "";
update[`${repeating_spell}_spellsavesuccess`] = v[`${repeating_attack}_saveeffect`] || "";
if (v[`${repeating_attack}_dmgtype`] && v[`${repeating_attack}_dmgtype`].toLowerCase() == "healing") {
if (v[`${repeating_attack}_dmgbase`] && v[`${repeating_attack}_dmgbase`] != "") {
update[`${repeating_spell}_spellhealing`] = v[`${repeating_attack}_dmgbase`];
}
} else {
if (v[`${repeating_attack}_dmgbase`] && v[`${repeating_attack}_dmgbase`] != "" && v[`${repeating_attack}_dmgbase`].indexOf("[[round((@{level} + 1) / 6 + 0.5)]]") === -1) {
update[`${repeating_spell}_spelldamage`] = v[`${repeating_attack}_dmgbase`];
} else if (!v[`${repeating_attack}_dmgbase`] || v[`${repeating_attack}_dmgbase`] === "") {
update[`${repeating_spell}_spelldamage`] = "";
}
update[`${repeating_spell}_spelldamagetype`] = v[`${repeating_attack}_dmgtype`] || "";
};
if (v[`${repeating_attack}_dmg2type`] && v[`${repeating_attack}_dmg2type`].toLowerCase() === "healing") {
if (v[`${repeating_attack}_dmgbase`] && v[`${repeating_attack}_dmgbase`] != "") {
update[`${repeating_spell}_spellhealing`] = v[`${repeating_attack}_dmgbase`];
}
} else {
update[`${repeating_spell}_spelldamage2`] = v[`${repeating_attack}_dmg2base`] || "";
update[`${repeating_spell}_spelldamagetype2`] = v[`${repeating_attack}_dmg2type`] || "";
};
//SAVING THROW checkbox inside settings for the repeating attack
update[`${repeating_spell}_spellsave`] = (v[`${repeating_attack}_saveflag`] && v[`${repeating_attack}_saveflag`] != "0") ? v[`${repeating_attack}_saveattr`] : "";
setAttrs(update, {silent: true});
});
};
var update_item_from_attack = function(itemid, attackid) {
getAttrs(["repeating_attack_" + attackid + "_atkname", "repeating_attack_" + attackid + "_dmgbase", "repeating_attack_" + attackid + "_dmg2base", "repeating_attack_" + attackid + "_dmgtype", "repeating_attack_" + attackid + "_dmg2type", "repeating_attack_" + attackid + "_atkrange", "repeating_attack_" + attackid + "_atkmod", "repeating_attack_" + attackid + "_dmgmod", "repeating_inventory_" + itemid + "_itemmodifiers", "repeating_attack_" + attackid + "_versatile_alt", "repeating_inventory_" + itemid + "_itemproperties", "repeating_attack_" + attackid + "_atkmagic"], function(v) {
var update = {};
var mods = v["repeating_inventory_" + itemid + "_itemmodifiers"];
var damage = v["repeating_attack_" + attackid + "_dmgbase"] ? v["repeating_attack_" + attackid + "_dmgbase"] : 0;
var damage2 = v["repeating_attack_" + attackid + "_dmg2base"] ? v["repeating_attack_" + attackid + "_dmg2base"] : 0;
var damagetype = v["repeating_attack_" + attackid + "_dmgtype"] ? v["repeating_attack_" + attackid + "_dmgtype"] : 0;
var damagetype2 = v["repeating_attack_" + attackid + "_dmg2type"] ? v["repeating_attack_" + attackid + "_dmg2type"] : 0;
var range = v["repeating_attack_" + attackid + "_atkrange"] ? v["repeating_attack_" + attackid + "_atkrange"] : 0;
var attackmod = v["repeating_attack_" + attackid + "_atkmod"] ? v["repeating_attack_" + attackid + "_atkmod"] : 0;
var damagemod = v["repeating_attack_" + attackid + "_dmgmod"] ? v["repeating_attack_" + attackid + "_dmgmod"] : 0;
var magicmod = v["repeating_attack_" + attackid + "_atkmagic"] ? v["repeating_attack_" + attackid + "_atkmagic"] : 0;
var atktype = "";
var altprefix = v["repeating_attack_" + attackid + "_versatile_alt"] === "1" ? "Alternate " : "";
if(/Alternate Damage:/i.test(v["repeating_inventory_" + itemid + "_itemmodifiers"])) {
update["repeating_inventory_" + itemid + "_itemname"] = v["repeating_attack_" + attackid + "_atkname"].replace(/\s*(?:\(One-Handed\)|\(Two-Handed\))/, "");
} else {
update["repeating_inventory_" + itemid + "_itemname"] = v["repeating_attack_" + attackid + "_atkname"];
}
var attack_type_regex = /(?:^|,)\s*Item Type: (Melee|Ranged) Weapon(?:,|$)/i;
var attack_type_results = attack_type_regex.exec(v["repeating_inventory_" + itemid + "_itemmodifiers"]);
atktype = attack_type_results ? attack_type_results[1] : "";
if(mods) {
mods = mods.split(",");
} else {
mods = [];
}
var damage_regex = new RegExp("^\\s*" + altprefix + "Damage:\\s*(.+)$", "i");
var damage_found = false;
for(var i = 0; i < mods.length; i++) {
if(damage_found = damage_regex.exec(mods[i])) {
if(damage !== 0) {
mods[i] = mods[i].replace(damage_found[1], damage);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!damage_found && damage !== 0) {
mods.push(altprefix + "Damage: " + damage);
}
var damage2_regex = new RegExp("^\\s*" + altprefix + "Secondary Damage:\\s*(.+)$", "i");
var damage2_found = false;
for(var i = 0; i < mods.length; i++) {
if(damage2_found = damage2_regex.exec(mods[i])) {
if(damage2 !== 0) {
mods[i] = mods[i].replace(damage2_found[1], damage2);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!damage2_found && damage2 !== 0) {
mods.push(altprefix + "Secondary Damage: " + damage2);
}
var dmgtype_regex = new RegExp("^\\s*" + altprefix + "Damage Type:\\s*(.+)$", "i");
var dmgtype_found = false;
for(var i = 0; i < mods.length; i++) {
if(dmgtype_found = dmgtype_regex.exec(mods[i])) {
if(damagetype !== 0) {
mods[i] = mods[i].replace(dmgtype_found[1], damagetype);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!dmgtype_found && damagetype !== 0) {
mods.push(altprefix + "Damage Type: " + damagetype);
}
var dmgtype2_regex = new RegExp("^\\s*" + altprefix + "Secondary Damage Type:\\s*(.+)$", "i");
var dmgtype2_found = false;
for(var i = 0; i < mods.length; i++) {
if(dmgtype2_found = dmgtype2_regex.exec(mods[i])) {
if(damagetype2 !== 0) {
mods[i] = mods[i].replace(dmgtype_found[1], damagetype);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!dmgtype2_found && damagetype2 !== 0) {
mods.push(altprefix + "Secondary Damage Type: " + damagetype2);
}
var range_found = false;
for(var i = 0; i < mods.length; i++) {
if(range_found = /^\s*Range:\s*(.+)$/i.exec(mods[i])) {
if(range !== 0) {
mods[i] = mods[i].replace(range_found[1], range);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!range_found && range !== 0) {
mods.push("Range: " + range);
}
var attackmod_regex = new RegExp("^\\s*(?:" + (atktype !== "" ? atktype + "|" : "") + "Weapon) Attacks \\+?(.+)$", "i");
var attackmod_found = false;
for(var i = 0; i < mods.length; i++) {
if(attackmod_found = attackmod_regex.exec(mods[i])) {
if(magicmod !== 0 || attackmod !== 0) {
mods[i] = mods[i].replace(attackmod_found[1], magicmod !== 0 ? magicmod : attackmod);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!attackmod_found && (magicmod !== 0 || attackmod !== 0)) {
var properties = v["repeating_inventory_" + itemid + "_itemproperties"];
if(properties && /Thrown/i.test(properties)) {
mods.push("Weapon Attacks: " + (magicmod !== 0 ? magicmod : attackmod));
}
else {
mods.push(atktype + " Attacks: " + (magicmod !== 0 ? magicmod : attackmod));
}
}
var damagemod_regex = new RegExp("^\\s*(?:" + (atktype !== "" ? atktype + "|" : "") + "Weapon) Damage \\+?(.+)$", "i");
var damagemod_found = false;
for(var i = 0; i < mods.length; i++) {
if(damagemod_found = damagemod_regex.exec(mods[i])) {
if(magicmod !== 0 || damagemod !== 0) {
mods[i] = mods[i].replace(damagemod_found[1], magicmod !== 0 ? magicmod : attackmod);
} else {
mods.splice(i, 1);
}
break;
}
}
if(!damagemod_found && (magicmod !== 0 || damagemod !== 0)) {
var properties = v["repeating_inventory_" + itemid + "_itemproperties"];
if(properties && /Thrown/i.test(properties)) {
mods.push("Weapon Damage: " + (magicmod !== 0 ? magicmod : damagemod));
}
else {
mods.push(atktype + " Damage: " + (magicmod !== 0 ? magicmod : damagemod));
}
}
update["repeating_inventory_" + itemid + "_itemmodifiers"] = mods.join(",");
setAttrs(update, {silent: true});
});
};
var remove_attack = function(attackid) {
removeRepeatingRow("repeating_attack_" + attackid);
};
const remove_resource = (id) => {
const attr = (id === "other_resource") ? id : `repeating_resource_${id}_itemid`;
let update = {};
getAttrs([`${attr}`], (v) => {
const itemid = v[`${attr}`];
//Uncheck Use As A Resource in inventory for an item if its resource row is deleted
if (itemid) {
update[`repeating_inventory_${itemid}_useasresource`] = 0;
update[`repeating_inventory_${itemid}_itemresourceid`] = "";
};
if (id == "other_resource") {
update["other_resource"] = "";
update["other_resource_name"] = "";
update["other_resource_itemid"] = "";
setAttrs(update, {silent: true});
} else {
const currentID = id.replace("repeating_resource_", "").substring(0,20);
const side = (id.includes("left")) ? `right` : `left`;
//Get the inputs for the opposite side to see if they are empty.
//If they are empty delete the entire row. Otherwise set the side that was the source of this event to empty strings
getAttrs([`repeating_resource_${currentID}_resource_${side}`, `repeating_resource_${currentID}_resource_${side}_name`, `repeating_resource_${currentID}_resource_${side}_max`], (v) => {
const Name = v[`repeating_resource_${currentID}_resource_${side}_name`] || false;
const Value = v[`repeating_resource_${currentID}_resource_${side}`] || false;
const Max = v[`repeating_resource_${currentID}_resource_${side}_max`] || false;
if (!Name && !Value && !Max) {
removeRepeatingRow(`repeating_resource_${currentID}`);
} else {
update[`repeating_resource_${id}`] = "";
update[`repeating_resource_${id}_name`] = "";
update[`repeating_resource_${id}_max`] = "";
update[`repeating_resource_${id}_itemid`] = "";
};
setAttrs(update, {silent: true});
});
};
});
};
const update_weight = function() {
let update = {};
let wtotal = 0;
let weight_attrs = ["cp","sp","ep","cu","cn","encumberance_setting","strength","size","carrying_capacity_mod","powerful_build"];
getSectionIDs("repeating_inventory", (idarray) => {
_.each(idarray, (currentID, i) => {
weight_attrs.push(`repeating_inventory_${currentID}_itemweight`);
weight_attrs.push(`repeating_inventory_${currentID}_itemcount`);
});
getAttrs(weight_attrs, (v) => {
let coinWeight = 0;
["cp", "sp", "ep", "cu", "cn"].forEach((type)=> {
coinWeight += (isNaN(parseInt(v[`${type}`], 10)) === false) ? parseInt(v[`${type}`], 10) : 0;
});
wtotal = wtotal + ((coinWeight) / 50);
_.each(idarray, (currentID, i) => {
if (v[`repeating_inventory_${currentID}_itemweight`] && isNaN(parseInt(v[`repeating_inventory_${currentID}_itemweight`], 10)) === false) {
count = v[`repeating_inventory_${currentID}_itemcount`] && isNaN(parseFloat(v[`repeating_inventory_${currentID}_itemcount`])) === false ? parseFloat(v[`repeating_inventory_${currentID}_itemcount`]) : 1;
wtotal = wtotal + (parseFloat(v[`repeating_inventory_${currentID}_itemweight`]) * count);
}
});
update["weighttotal"] = wtotal;
const str_base = parseInt(v.strength, 10);
let size_multiplier = 1;
if (v["size"] && v["size"] != "") {
const size = v["size"].toLowerCase().trim();
size_multiplier =
(size === "tiny") ? .5 : (size === "large") ? 2 : (size === "huge") ? 4 : (size === "gargantuan") ? 8 : size_multiplier;
};
if (v["powerful_build"] && v["powerful_build"] == 1) { size_multiplier *= 2; }
let str = str_base*size_multiplier;
// Parse the carrying capacitiy modificator if any
if (v.carrying_capacity_mod) {
const operator = v.carrying_capacity_mod.substring(0,1);
const value = v.carrying_capacity_mod.substring(1);
if(["*","x","+","-"].indexOf(operator) > -1 && isNaN(parseFloat(value,10)) === false) {
str =
(operator === "*" || operator == "x") ? str*parseFloat(value,10) :
(operator === "+") ? str+parseFloat(value,10) :
(operator === "-") ? str-parseFloat(value,10) :
str;
}
}
update["carrying_capacity"] = isNaN(str) ? 0 : Math.round(str * 15);
if (!v.encumberance_setting || v.encumberance_setting === "off") {
update["encumberance"] = (wtotal > str*15) ? "OVER CARRYING CAPACITY" : " ";
} else if (v.encumberance_setting === "on") {
update["encumberance"] =
(wtotal > str*15) ? "IMMOBILE" :
(wtotal > str*10) ? "HEAVILY ENCUMBERED" :
(wtotal > str*5) ? "ENCUMBERED" :
" ";
} else {
update["encumberance"] = " ";
}
setAttrs(update, {silent: true});
});
});
};
var update_ac = function() {
getAttrs(["custom_ac_flag"], function(v) {
if(v.custom_ac_flag === "2") {
return;
}
else {
var update = {};
var ac_attrs = ["simpleinventory","custom_ac_base","custom_ac_part1","custom_ac_part2","strength_mod","dexterity_mod","constitution_mod","intelligence_mod","wisdom_mod","charisma_mod", "custom_ac_shield", "ac_misc_mod"];
getSectionIDs("repeating_acmod", function(acidarray) {
_.each(acidarray, function(currentID, i) {
ac_attrs.push("repeating_acmod_" + currentID + "_global_ac_val");
ac_attrs.push("repeating_acmod_" + currentID + "_global_ac_active_flag");
});
getSectionIDs("repeating_inventory", function(idarray) {
_.each(idarray, function(currentID, i) {
ac_attrs.push("repeating_inventory_" + currentID + "_equipped");
ac_attrs.push("repeating_inventory_" + currentID + "_itemmodifiers");
});
getAttrs(ac_attrs, function(b) {
var custom_total = 0;
if(v.custom_ac_flag === "1") {
var base = isNaN(parseInt(b.custom_ac_base, 10)) === false ? parseInt(b.custom_ac_base, 10) : 10;
var part1attr = b.custom_ac_part1.toLowerCase();
var part2attr = b.custom_ac_part2.toLowerCase();
var part1 = part1attr === "none" ? 0 : parseInt(b[part1attr + "_mod"], 10);
var part2 = part2attr === "none" ? 0 : parseInt(b[part2attr + "_mod"], 10);
custom_total = base + part1 + part2;
}
var globalacmod = 0;
_.each(acidarray, function(currentID, i) {
if(b["repeating_acmod_" + currentID + "_global_ac_active_flag"] == "1") {
globalacmod += parseInt(b["repeating_acmod_" + currentID + "_global_ac_val"], 10);
}
});
var dexmod = +b["dexterity_mod"];
var total = 10 + dexmod;
var armorcount = 0;
var shieldcount = 0;
var armoritems = [];
var armorDisplay = 10;
if(b.simpleinventory === "complex") {
_.each(idarray, function(currentID, i) {
if(b["repeating_inventory_" + currentID + "_equipped"] && b["repeating_inventory_" + currentID + "_equipped"] === "1" && b["repeating_inventory_" + currentID + "_itemmodifiers"] && b["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf("ac") > -1) {
var mods = b["repeating_inventory_" + currentID + "_itemmodifiers"].split(",");
var ac = 0;
var type = "mod";
_.each(mods, function(mod) {
if(mod.substring(0,10) === "Item Type:") {
type = mod.substring(11, mod.length).trim().toLowerCase();
}
if(mod.toLowerCase().indexOf("ac:") > -1 || mod.toLowerCase().indexOf("ac +") > -1 || mod.toLowerCase().indexOf("ac+") > -1) {
var regex = mod.replace(/[^0-9]/g, "");
var bonus = regex && regex.length > 0 && isNaN(parseInt(regex,10)) === false ? parseInt(regex,10) : 0;
ac = ac + bonus;
}
if(mod.toLowerCase().indexOf("ac -") > -1 || mod.toLowerCase().indexOf("ac-") > -1) {
var regex = mod.replace(/[^0-9]/g, "");
var bonus = regex && regex.length > 0 && isNaN(parseInt(regex,10)) === false ? parseInt(regex,10) : 0;
ac = ac - bonus;
}
});
armoritems.push({type: type, ac: ac});
}
});
armorcount = armoritems.filter(function(item){ return item["type"].indexOf("armor") > -1 }).length;
shieldcount = armoritems.filter(function(item){ return item["type"].indexOf("shield") > -1 }).length;
var base = dexmod;
var armorac = 10;
var shieldac = 0;
var modac = 0;
_.each(armoritems, function(item) {
if(item["type"].indexOf("light armor") > -1) {
armorac = item["ac"];
base = dexmod;
}
else if(item["type"].indexOf("medium armor") > -1) {
armorac = item["ac"];
base = Math.min(dexmod, 2);
}
else if(item["type"].indexOf("heavy armor") > -1) {
armorac = item["ac"];
base = 0;
}
else if(item["type"].indexOf("shield") > -1) {
shieldac = item["ac"];
}
else {
modac = modac + item["ac"]
}
});
total = base + armorac + shieldac + modac;
armorDisplay = armorac;
};
update["armorwarningflag"] = "hide";
update["customacwarningflag"] = "hide";
if(armorcount > 1 || shieldcount > 1) {
update["armorwarningflag"] = "show";
}
update["ac"] = total + globalacmod;
if(custom_total > 0) {
if(armorcount > 0 || (shieldcount > 0 && b.custom_ac_shield != "yes")) {
update["customacwarningflag"] = "show";
}
else {
update["ac"] = b.custom_ac_shield == "yes" ? custom_total + shieldac + globalacmod + modac : custom_total + globalacmod + modac;
armorDisplay = isNaN(parseInt(b.custom_ac_base, 10)) ? 10 : parseInt(b.custom_ac_base, 10);
}
}
var miscmod = isNaN(parseInt(b.ac_misc_mod, 10)) ? 0 : parseInt(b.ac_misc_mod, 10);
update["ac"] = update["ac"] + miscmod;
update["ac_armor"] = armorDisplay;
setAttrs(update, {silent: true});
});
});
});
};
});
};
var check_customac = function(attr) {
getAttrs(["custom_ac_flag","custom_ac_part1","custom_ac_part2"], function(v) {
if(v["custom_ac_flag"] && v["custom_ac_flag"] === "1" && ((v["custom_ac_part1"] && v["custom_ac_part1"] === attr) || (v["custom_ac_part2"] && v["custom_ac_part2"] === attr))) {
update_ac();
}
});
};
const update_initiative = () => {
const attrs_to_get = ["dexterity","dexterity_mod","initmod","jack_of_all_trades","jack","init_tiebreaker","pb_type"];
getSectionIDs("repeating_inventory", (idarray) => {
_.each(idarray, (currentID) => {
attrs_to_get.push(`repeating_inventory_${currentID}_equipped`);
attrs_to_get.push(`repeating_inventory_${currentID}_itemmodifiers`);
});
getAttrs(attrs_to_get, (v) => {
let update = {};
let final_init = parseInt(v["dexterity_mod"], 10);
if (v["initmod"] && !isNaN(parseInt(v["initmod"], 10))) {
final_init = final_init + parseInt(v["initmod"], 10);
}
if (v["init_tiebreaker"] && v["init_tiebreaker"] != 0) {
final_init = final_init + (parseInt(v["dexterity"], 10)/100);
}
if (v["jack_of_all_trades"] && v["jack_of_all_trades"] != 0) {
if (v["pb_type"] && v["pb_type"] === "die" && v["jack"]) {
final_init = final_init + "+" + v["jack"];
} else if (v["jack"] && !isNaN(parseInt(v["jack"], 10))) {
final_init = final_init + parseInt(v["jack"], 10);
}
}
_.each(idarray, (currentID) => {
if (v[`repeating_inventory_${currentID}_equipped`] && v[`repeating_inventory_${currentID}_equipped`] === "1" && v[`repeating_inventory_${currentID}_itemmodifiers`] && v[`repeating_inventory_${currentID}_itemmodifiers`].toLowerCase().indexOf("ability checks") > -1) {
const mods = v[`repeating_inventory_${currentID}_itemmodifiers`].toLowerCase().split(",");
_.each(mods, (mod) => {
if (mod.indexOf("ability checks") > -1) {
const new_mod = !isNaN(parseInt(mod.replace(/[^0-9]/g, ""), 10)) ? parseInt(mod.replace(/[^0-9]/g, ""), 10) : false;
final_init = (mod.indexOf("-") > -1 && new_mod) ? final_init - new_mod : (new_mod) ? final_init + new_mod : final_init;
}
});
}
});
if (final_init % 1 != 0) {
final_init = parseFloat(final_init.toPrecision(12)); // ROUNDING ERROR BUGFIX
}
update["initiative_bonus"] = final_init;
// Display label shown on the Initiative button (span name="attr_initiative_bonus_label").
// The roll uses initiative_bonus; this formatted string is what the user sees.
// Prefix non-negative values with "+".
let init_label = String(final_init);
if (init_label.charAt(0) !== "-" && init_label.charAt(0) !== "+") {
init_label = "+" + init_label;
}
update["initiative_bonus_label"] = init_label;
setAttrs(update, {silent: true});
});
});
};
const update_class = () => {
getAttrs(["class", "base_level","custom_class","cust_hitdietype","cust_spellcasting_ability","cust_strength_save_prof","cust_dexterity_save_prof","cust_constitution_save_prof","cust_intelligence_save_prof","cust_wisdom_save_prof","cust_charisma_save_prof", "npc"], (v) => {
if (v.npc && v.npc == "1") { return; }
const abilites = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"];
//Custom Classes
if (v.custom_class && v.custom_class != "0") {
update = {};
update["hitdietype"] = v.cust_hitdietype;
update["spellcasting_ability"] = v.cust_spellcasting_ability;
abilites.forEach((save) => {
const ability = save.toLowerCase();
update[`${ability}_save_prof`] = v[`cust_${ability}_save_prof`];
});
setAttrs(update, {silent: true});
//If "Choose" has been selected in the select
} else if (v.class === "") {
update = {};
update["hitdietype"] = 6;
update["spellcasting_ability"] = "0*";
abilites.forEach((save) => {
const ability = save.toLowerCase();
update[`${ability}_save_prof`] = 0;
});
setAttrs(update, {silent: true});
//Standard classes can pull their data from the Comependium
} else {
getCompendiumPage(v.class, (pages) => {
pages = removeDuplicatedPageData(pages);
update = {};
const classPages = (Array.isArray(pages)) ? pages : [pages];
classPages.forEach((classData) => {
if (classData.data && 'Category' in classData.data && classData.data[`Category`] === "Classes") {
update["hitdietype"] = classData.data["Hit Die"].slice(1);
update["spellcasting_ability"] = (classData.data["Spellcasting Ability"]) ? `@{${classData.data["Spellcasting Ability"].toLowerCase()}_mod}+` : "0*";
abilites.forEach((save) => {
const ability = save.toLowerCase();
update[`${ability}_save_prof`] = (classData.data["data-Saving Throws"].indexOf(`${save}`) > -1) ? "(@{pb})" : 0;
update_save(ability);;
});
};
setAttrs(update, {silent: true});
//Update Spell info to change or remove casting attributes in Powers
update_spell_info();
});
});
};
});
set_level();
};
const set_level = function() {
getAttrs(["base_level","multiclass1_flag","multiclass2_flag","multiclass3_flag","multiclass1_lvl","multiclass2_lvl","multiclass3_lvl","class","multiclass1","multiclass2","multiclass3", "arcane_fighter", "arcane_rogue", "custom_class", "cust_spellslots", "cust_classname", "level_calculations", "class", "subclass", "multiclass1_subclass","multiclass2_subclass","multiclass3_subclass"], (v) => {
let update = {};
let callbacks = [];
const multiclass = (v[`multiclass1_flag`] && v[`multiclass1_flag`] === "1") || (v.multiclass2_flag && v.multiclass2_flag === "1") || (v.multiclass3_flag && v.multiclass3_flag === "1") ? true : false;
const finalclass = (v[`custom_class`] && v[`custom_class`] != "0") ? v[`cust_spellslots`] : v[`class`];
let finallevel = (v[`base_level`] && v[`base_level`] > 0) ? parseInt(v.base_level,10) : 1;
const charclass = (v[`custom_class`] && v[`custom_class`] != "0") ? v[`cust_classname`] : v[`class`];
let hitdie_final = (multiclass) ? `?{Hit Die Class|${charclass},@{hitdietype}` : "@{hitdietype}";
const subclass = (v[`subclass`]) ? v[`subclass`] + " " : "";
let class_display = `${subclass}${charclass} ${finallevel}`;
const baselevel = finallevel;
// This nested array is used to determine the overall spellcasting level for the character.
var classes = [ [finalclass.toLowerCase(), v[`base_level`]] ];
// Reset all multiclass HD slots; active ones will be populated in the forEach below.
update["multiclass1_hitdietype"] = ""; update["multiclass1_hit_dice_max"] = "0";
update["multiclass2_hitdietype"] = ""; update["multiclass2_hit_dice_max"] = "0";
update["multiclass3_hitdietype"] = ""; update["multiclass3_hit_dice_max"] = "0";
["multiclass1", "multiclass2", "multiclass3"].forEach((multiclass) => {
if (v[`${multiclass}_flag`] && v[`${multiclass}_flag`] === "1") {
const multiclasslevel = (v[`${multiclass}_lvl`] && v[`${multiclass}_lvl`] > 0) ? parseInt(v[`${multiclass}_lvl`], 10) : 1;
const subclass = (v[`${multiclass}_subclass`]) ? v[`${multiclass}_subclass`] + " " : "";
finallevel = finallevel + multiclasslevel;
hitdie_final = `${hitdie_final}|` + v[`${multiclass}`].charAt(0).toUpperCase() + v[`${multiclass}`].slice(1) + "," + checkHitDie(v[`${multiclass}`]);
classes.push([ v[`${multiclass}`], multiclasslevel ]);
class_display = `${class_display}, ${subclass}` + v[`${multiclass}`] + ` ${multiclasslevel}`;
update[`${multiclass}_hitdietype`] = checkHitDie(v[`${multiclass}`]);
update[`${multiclass}_hit_dice_max`] = multiclasslevel;
};
});
const casterlevel = checkCasterLevel(classes, v[`arcane_fighter`], v[`arcane_rogue`]);
update["hitdie_final"] = (multiclass) ? `${hitdie_final}}` : hitdie_final;
update["level"] = finallevel;
update["caster_level"] = casterlevel;
// Set flag: does this class use power slots (not TP-only or Talent Slot system)?
var slotClasses = ["engineer","full","hunter","sentinel","half","third"];
var usesSlots = classes.some(function(c) {
var cn = c[0].toLowerCase();
return slotClasses.indexOf(cn) !== -1
|| (cn === "fighter" && v["arcane_fighter"] === "1")
|| (cn === "rogue" && v["arcane_rogue"] === "1")
|| (cn === "warrior" && v["arcane_fighter"] === "1")
|| (cn === "specialist" && v["arcane_rogue"] === "1");
});
update["uses_power_slots"] = usesSlots ? "1" : "0";
update["class_display"] = class_display;
if(!v["level_calculations"] || v["level_calculations"] === "on") {
update["hit_dice_max"] = baselevel;
callbacks.push(() => {update_spell_slots();} );
}
callbacks.push(() => {update_pb();} );
callbacks.push(() => {update_leveler_display();} );
callbacks.push(() => {updateHeroPoints(finallevel);} );
callbacks.push(() => {
getAttrs(["use_spell_points"], v => {
if (v["use_spell_points"] && v["use_spell_points"] == "1") setSpellPoints();
});
});
setAttrs(update, {silent: true},() => {callbacks.forEach((callback) => {callback(); })} );
});
};
var isMultiCaster = function(classes, arcane_fighter, arcane_rogue) {
var singlecaster = false;
var multicaster = false;
_.each(classes, function(multiclass) {
var caster = getCasterType(multiclass[0], multiclass[1], arcane_fighter, arcane_rogue) > 0;
if(caster && singlecaster) {
multicaster = true;
} else if(caster) {
singlecaster = true;
}
});
return multicaster;
};
var getCasterType = function(class_string, levels, arcane_fighter, arcane_rogue) {
// Full slot casters: Engineer only (Melder/Adept use TP; Cybermancer uses Talent Slots)
var full = ["engineer","full"];
var half = ["hunter","sentinel","half"];
class_string = class_string.toLowerCase();
if(full.indexOf(class_string) != -1) {
return 1;
} else if(half.indexOf(class_string) != -1) {
if(class_string === "artificer" && levels == 1) return 1;
return (levels == 1) ? 0 : (1/2);
} else if(class_string === "third"
|| (class_string === "fighter" && arcane_fighter === "1")
|| (class_string === "rogue" && arcane_rogue === "1")
|| (class_string === "warrior" && arcane_fighter === "1")
|| (class_string === "specialist" && arcane_rogue === "1")
) {
return (levels == 1 || levels == 2) ? 0 : (1/3);
} else {
return 0;
}
};
var checkCasterLevel = function(classes, arcane_fighter, arcane_rogue) {
console.log("CHECKING CASTER LEVEL");
var multicaster = isMultiCaster(classes, arcane_fighter, arcane_rogue);
var totalcasterlevel = 0;
_.each(classes, function(multiclass) {
var casterlevel = parseInt(multiclass[1], 10) * getCasterType(multiclass[0], multiclass[1], arcane_fighter, arcane_rogue);
// Characters with multiple spellcasting classes round down the casting level for that class
// Character with a single spellcasting class round up the casting level
totalcasterlevel = totalcasterlevel + (multicaster ? Math.floor(casterlevel) : Math.ceil(casterlevel));
});
return totalcasterlevel;
};
var checkHitDie = function(class_string) {
var d10class = ["hunter","sentinel","warrior"];
var d8class = ["adept","cybermancer","engineer","specialist"];
var d6class = ["melder"];
class_string = class_string.toLowerCase();
if(class_string === "barbarian") {return "12"}
else if (d10class.indexOf(class_string) != -1) {return "10"}
else if (d8class.indexOf(class_string) != -1) {return "8"}
else if (d6class.indexOf(class_string) != -1) {return "6"}
else {return "0"};
};
var update_leveler_display = function () {
getAttrs(["experience","level"], function(v) {
let lvl = 0;
let exp = 0;
let update = {};
update["showleveler"] = 0;
if(v["level"] && !isNaN(parseInt(v["level"], 10)) && parseInt(v["level"], 10) > 0) {
lvl = parseInt(v["level"], 10);
}
if(v["experience"] && !isNaN(parseInt(v["experience"], 10)) && parseInt(v["experience"], 10) > 0) {
exp = parseInt(v["experience"], 10);
}
if(lvl > 0 && exp > 0) {
if((lvl === 1 && exp >= 300) || (lvl === 2 && exp >= 900) || (lvl === 3 && exp >= 2700) || (lvl === 4 && exp >= 6500) || (lvl === 5 && exp >= 14000) || (lvl === 6 && exp >= 23000) || (lvl === 7 && exp >= 34000) || (lvl === 8 && exp >= 48000) || (lvl === 9 && exp >= 64000) || (lvl === 10 && exp >= 85000) || (lvl === 11 && exp >= 100000) || (lvl === 12 && exp >= 120000) || (lvl === 13 && exp >= 140000) || (lvl === 14 && exp >= 165000) || (lvl === 15 && exp >= 195000) || (lvl === 16 && exp >= 225000) || (lvl === 17 && exp >= 265000) || (lvl === 18 && exp >= 305000) || (lvl === 19 && exp >= 355000)) {
update["showleveler"] = 1;
};
};
setAttrs(update, {silent: true});
});
};
var update_spell_slots = function() {
getAttrs(["use_spell_points","lvl1_slots_mod","lvl2_slots_mod","lvl3_slots_mod","lvl4_slots_mod","lvl5_slots_mod","lvl6_slots_mod","lvl7_slots_mod","lvl8_slots_mod","lvl9_slots_mod","caster_level"], function(v) {
var update = {};
if (v["use_spell_points"] && v["use_spell_points"] == "1") {
for (var i = 1; i <= 9; i++) update[`lvl${i}_slots_total`] = 0;
setAttrs(update, {silent: true});
return;
}
var lvl = v["caster_level"] && !isNaN(parseInt(v["caster_level"], 10)) ? parseInt(v["caster_level"], 10) : 0;
var l1 = v["lvl1_slots_mod"] && !isNaN(parseInt(v["lvl1_slots_mod"], 10)) ? parseInt(v["lvl1_slots_mod"], 10) : 0;
var l2 = v["lvl2_slots_mod"] && !isNaN(parseInt(v["lvl2_slots_mod"], 10)) ? parseInt(v["lvl2_slots_mod"], 10) : 0;
var l3 = v["lvl3_slots_mod"] && !isNaN(parseInt(v["lvl3_slots_mod"], 10)) ? parseInt(v["lvl3_slots_mod"], 10) : 0;
var l4 = v["lvl4_slots_mod"] && !isNaN(parseInt(v["lvl4_slots_mod"], 10)) ? parseInt(v["lvl4_slots_mod"], 10) : 0;
var l5 = v["lvl5_slots_mod"] && !isNaN(parseInt(v["lvl5_slots_mod"], 10)) ? parseInt(v["lvl5_slots_mod"], 10) : 0;
var l6 = v["lvl6_slots_mod"] && !isNaN(parseInt(v["lvl6_slots_mod"], 10)) ? parseInt(v["lvl6_slots_mod"], 10) : 0;
var l7 = v["lvl7_slots_mod"] && !isNaN(parseInt(v["lvl7_slots_mod"], 10)) ? parseInt(v["lvl7_slots_mod"], 10) : 0;
var l8 = v["lvl8_slots_mod"] && !isNaN(parseInt(v["lvl8_slots_mod"], 10)) ? parseInt(v["lvl8_slots_mod"], 10) : 0;
var l9 = v["lvl9_slots_mod"] && !isNaN(parseInt(v["lvl9_slots_mod"], 10)) ? parseInt(v["lvl9_slots_mod"], 10) : 0;
if(lvl > 0) {
l1 = l1 + Math.min((lvl + 1),4);
if(lvl < 3) {l2 = l2 + 0;} else if(lvl === 3) {l2 = l2 + 2;} else {l2 = l2 + 3;};
if(lvl < 5) {l3 = l3 + 0;} else if(lvl === 5) {l3 = l3 + 2;} else {l3 = l3 + 3;};
if(lvl < 7) {l4 = l4 + 0;} else if(lvl === 7) {l4 = l4 + 1;} else if(lvl === 8) {l4 = l4 + 2;} else {l4 = l4 + 3;};
if(lvl < 9) {l5 = l5 + 0;} else if(lvl === 9) {l5 = l5 + 1;} else if(lvl < 18) {l5 = l5 + 2;} else {l5 = l5 + 3;};
if(lvl < 11) {l6 = l6 + 0;} else if(lvl < 19) {l6 = l6 + 1;} else {l6 = l6 + 2;};
if(lvl < 13) {l7 = l7 + 0;} else if(lvl < 20) {l7 = l7 + 1;} else {l7 = l7 + 2;};
if(lvl < 15) {l8 = l8 + 0;} else {l8 = l8 + 1;};
if(lvl < 17) {l9 = l9 + 0;} else {l9 = l9 + 1;};
};
update["lvl1_slots_total"] = l1;
update["lvl2_slots_total"] = l2;
update["lvl3_slots_total"] = l3;
update["lvl4_slots_total"] = l4;
update["lvl5_slots_total"] = l5;
update["lvl6_slots_total"] = l6;
update["lvl7_slots_total"] = l7;
update["lvl8_slots_total"] = l8;
update["lvl9_slots_total"] = l9;
setAttrs(update, {silent: true});
});
};
var update_pb = function() {
callbacks = [];
getAttrs(["level","pb_type","pb_custom"], function(v) {
var update = {};
var pb = 2;
var lvl = parseInt(v["level"],10);
if(lvl < 5) {pb = "2"} else if(lvl < 9) {pb = "3"} else if(lvl < 13) {pb = "4"} else if(lvl < 17) {pb = "5"} else {pb = "6"}
var jack = Math.floor(pb/2);
if(v["pb_type"] === "die") {
update["jack"] = "d" + pb;
update["pb"] = "d" + pb*2;
update["pbd_safe"] = "cs0cf0";
}
else if(v["pb_type"] === "custom" && v["pb_custom"] && v["pb_custom"] != "") {
update["pb"] = v["pb_custom"]
update["jack"] = !isNaN(parseInt(v["pb_custom"],10)) ? Math.floor(parseInt(v["pb_custom"],10)/2) : jack;
update["pbd_safe"] = "";
}
else {
update["pb"] = pb;
update["jack"] = jack;
update["pbd_safe"] = "";
};
callbacks.push( function() {update_attacks("all");} );
callbacks.push( function() {update_spell_info();} );
callbacks.push( function() {update_jack_attr();} );
callbacks.push( function() {update_initiative();} );
callbacks.push( function() {update_tool("all");} );
callbacks.push( function() {update_all_saves();} );
callbacks.push( function() {update_skills(["athletics", "acrobatics", "sleight_of_hand", "stealth", "astrophysics", "lore", "investigation", "xenobiology", "computers", "mechanics", "insight", "medicine", "perception", "survival", "deception", "intimidation", "performance", "persuasion", "piloting", "animal_handling", "arcana", "history", "nature", "religion"]);} );
setAttrs(update, {silent: true}, function() {callbacks.forEach(function(callback) {callback(); })} );
});
};
var update_jack_attr = function() {
var update = {};
getAttrs(["jack_of_all_trades","jack"], function(v) {
if(v["jack_of_all_trades"] && v["jack_of_all_trades"] != 0) {
update["jack_bonus"] = "+" + v["jack"];
update["jack_attr"] = "+" + v["jack"] + "@{pbd_safe}";
}
else {
update["jack_bonus"] = "";
update["jack_attr"] = "";
}
setAttrs(update, {silent: true});
});
};
var update_spell_info = function(attr) {
var update = {};
getAttrs(["spellcasting_ability","spell_dc_mod","globalmagicmod","strength_mod","dexterity_mod","constitution_mod","intelligence_mod","wisdom_mod","charisma_mod"], function(v) {
if(attr && v["spellcasting_ability"] && v["spellcasting_ability"].indexOf(attr) === -1) {
return
};
if(!v["spellcasting_ability"] || (v["spellcasting_ability"] && v["spellcasting_ability"] === "0*")) {
update["spell_attack_bonus"] = "0";
update["spell_save_dc"] = "0";
var callback = function() {update_attacks("spells")};
setAttrs(update, {silent: true}, callback);
return
};
var attr = attr ? attr : "";
console.log("UPDATING SPELL INFO: " + attr);
var ability = parseInt(v[v["spellcasting_ability"].substring(2,v["spellcasting_ability"].length-2)],10);
var spell_mod = v["globalmagicmod"] && !isNaN(parseInt(v["globalmagicmod"], 10)) ? parseInt(v["globalmagicmod"], 10) : 0;
var atk = v["globalmagicmod"] && !isNaN(parseInt(v["globalmagicmod"], 10)) ? ability + parseInt(v["globalmagicmod"], 10) : ability;
var dc = v["spell_dc_mod"] && !isNaN(parseInt(v["spell_dc_mod"], 10)) ? 8 + ability + parseInt(v["spell_dc_mod"], 10) : 8 + ability;
var itemfields = ["pb_type","pb"];
getSectionIDs("repeating_inventory", function(idarray) {
_.each(idarray, function(currentID, i) {
itemfields.push("repeating_inventory_" + currentID + "_equipped");
itemfields.push("repeating_inventory_" + currentID + "_itemmodifiers");
});
getAttrs(itemfields, function(v) {
_.each(idarray, function(currentID) {
if((!v["repeating_inventory_" + currentID + "_equipped"] || v["repeating_inventory_" + currentID + "_equipped"] === "1") && v["repeating_inventory_" + currentID + "_itemmodifiers"] && v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().indexOf("spell" > -1)) {
var mods = v["repeating_inventory_" + currentID + "_itemmodifiers"].toLowerCase().split(",");
_.each(mods, function(mod) {
if(mod.indexOf("spell attack") > -1) {
var substr = mod.slice(mod.lastIndexOf("spell attack") + "spell attack".length);
atk = substr && substr.length > 0 && !isNaN(parseInt(substr,10)) ? atk + parseInt(substr,10) : atk;
spell_mod = substr && substr.length > 0 && !isNaN(parseInt(substr,10)) ? spell_mod + parseInt(substr,10) : spell_mod;
};
if(mod.indexOf("spell dc") > -1) {
var substr = mod.slice(mod.lastIndexOf("spell dc") + "spell dc".length);
dc = substr && substr.length > 0 && !isNaN(parseInt(substr,10)) ? dc + parseInt(substr,10) : dc;
};
});
};
});
if(v["pb_type"] && v["pb_type"] === "die") {
atk = atk + "+" + v["pb"];
//Fixing Spell DC when using type Die
//By Miguel
//Used to be equals to: dc + parseInt(v["pb"].substring(1), 10) / 2
dc = dc + "+" + v["pb"];
}
else {
atk = parseInt(atk, 10) + parseInt(v["pb"], 10);
dc = parseInt(dc, 10) + parseInt(v["pb"], 10);
};
update["spell_attack_mod"] = spell_mod;
update["spell_attack_bonus"] = atk;
update["spell_save_dc"] = dc;
var callback = function() {update_attacks("spells")};
setAttrs(update, {silent: true}, callback);
});
});
});
};
var update_passive_perception = function() {
getAttrs(["pb_type","passiveperceptionmod","perception_bonus"], function(v) {
var passive_perception = 10;
var mod = !isNaN(parseInt(v["passiveperceptionmod"],10)) ? parseInt(v["passiveperceptionmod"],10) : 0;
var bonus = !isNaN(parseInt(v["perception_bonus"],10)) ? parseInt(v["perception_bonus"],10) : 0;
if(v["pb_type"] && v["pb_type"] === "die" && v["perception_bonus"] && isNaN(v["perception_bonus"]) && v["perception_bonus"].indexOf("+") > -1) {
var pieces = v["perception_bonus"].split(/\+|d/);
var base = !isNaN(parseInt(pieces[0],10)) ? parseInt(pieces[0],10) : 0;
var num_dice = !isNaN(parseInt(pieces[1],10)) ? parseInt(pieces[1],10) : 1;
var half_pb_die = !isNaN(parseInt(pieces[2],10)) ? parseInt(pieces[2],10)/2 : 2;
bonus = base + (num_dice * half_pb_die);
}
passive_perception = passive_perception + bonus + mod;
setAttrs({passive_wisdom: passive_perception})
});
};
var update_race_display = function() {
getAttrs(["race", "subrace"], function(v) {
var final_race = "";
final_race = v.subrace ? v.subrace : v.race;
if(v.race.toLowerCase() === "dragonborn") { final_race = v.race; };
setAttrs({race_display: final_race});
});
};
var organize_section_proficiencies = function() {
getSectionIDs("proficiencies", function(ids) {
var attribs = ["_reporder_repeating_proficiencies"];
_.each(ids, function(id) {
attribs.push("repeating_proficiencies_" + id + "_prof_type");
attribs.push("repeating_proficiencies_" + id + "_name");
});
getAttrs(attribs, function(v) {
var final_array = _(ids).chain().sortBy(function(id) {
return v["repeating_proficiencies_" + id + "_name"];
}).sortBy(function(id) {
return v["repeating_proficiencies_" + id + "_prof_type"];
}).value();
_.each(final_array, function(id) {
});
if(final_array && final_array.length > 0) {
setSectionOrder("proficiencies", final_array);
};
});
});
};
const update_challenge = () => {
getAttrs(["npc_challenge"], (v) => {
let update = {};
const challengeRatingsXP = { '0':'10', '1/8':'25', '1/4':'50', '1/2':'100', '1':'200', '2':'450', '3':'700', '4':'1100', '5':'1800', '6':'2300', '7':'2900', '8':'3900', '9':'5000', '10':'5900', '11':'7200', '12':'8400', '13':'10000', '14':'11500', '15':'13000', '16':'15000', '17':'18000', '18':'20000', '19':'22000', '20':'25000', '21':'33000', '22':'41000', '23':'50000', '24':'62000', '25':'75000', '26':'90000', '27':'105000', '28':'120000', '29':'135000', '30':'155000'
};
const xp = parseInt(challengeRatingsXP[v.npc_challenge]) || 0;
const pb = xp <= 1100 ? 2 : xp <= 3900 ? 3 : xp <= 8400 ? 4 : xp <= 15000 ? 5 : xp <= 25000 ? 6 : xp <= 62000 ? 7 : xp <= 120000 ? 8 : xp <= 155000 ? 9 : 0;
update["npc_xp"] = xp;
update["pb_custom"] = pb;
update["pb_type"] = "custom";
setAttrs(update, {silent: true}, function() {update_pb()});
});
};
const update_npc_saves = () => {
const list = ["npc_str_save_base","npc_dex_save_base","npc_con_save_base","npc_int_save_base","npc_wis_save_base","npc_cha_save_base"];
const type = "save";
update_npc_lists(list, type);
};
const update_npc_skills = () => {
const list = ["npc_acrobatics_base","npc_astrophysics_base","npc_athletics_base","npc_computers_base","npc_deception_base","npc_insight_base","npc_intimidation_base","npc_investigation_base","npc_lore_base","npc_mechanics_base","npc_medicine_base","npc_perception_base","npc_performance_base","npc_persuasion_base","npc_sleight_of_hand_base","npc_stealth_base","npc_survival_base","npc_xenobiology_base", "npc_piloting_base"];
const type = "skills";
update_npc_lists(list, type);
};
const update_npc_lists = (list, type) => {
getAttrs(list, function(v) {
let update = {};
let last_save = 0;
let npc_flag = 0;
_.each(list.reverse(), (base) => {
const attr = base.slice(4, -5); //Remove npc_ and _base
let item = parseInt(v[`${base}`], 10);
// CSS will add comma :after 2 & 4 and +/- :before
// 1 = Positive Number, 2 = Last Number, 3 = Negative Number, 4 = Last Negative Number
if (v[`${base}`] && !isNaN(item) || v[`${base}`] === 0) {
if (last_save === 0) {
last_save = 1;
item_flag = item < 0 ? 4 : 2;
} else {
item_flag = item < 0 ? 3 : 1;
}
} else {
item_flag = 0;
item = "";
};
update[`npc_${attr}_flag`] = item_flag;
update[`npc_${attr}`] = item;
npc_flag += item_flag;
});
const flagAttr = (type === "save") ? "saving" : type;
update[`npc_${flagAttr}_flag`] = (npc_flag === 0) ? "" : npc_flag;
setAttrs(update, {silent: true});
});
};
var update_npc_action = function(update_id, legendary) {
if(update_id.substring(0,1) === "-" && update_id.length === 20) {
do_update_npc_action([update_id], legendary);
}
else if(update_id === "all") {
var legendary_array = [];
var actions_array = [];
getSectionIDs("repeating_npcaction-l", function(idarray) {
legendary_array = idarray;
if(legendary_array.length > 0) {
do_update_npc_action(legendary_array, true);
}
getSectionIDs("repeating_npcaction", function(idarray) {
actions_array = idarray.filter(function(i) {return legendary_array.indexOf(i) < 0;});
if(actions_array.length > 0) {
do_update_npc_action(actions_array, false);
};
});
});
};
};
var do_update_npc_action = function(action_array, legendary) {
var repvar = legendary ? "repeating_npcaction-l_" : "repeating_npcaction_";
var action_attribs = ["dtype"];
_.each(action_array, function(actionid) {
action_attribs.push(repvar + actionid + "_attack_flag");
action_attribs.push(repvar + actionid + "_attack_type");
action_attribs.push(repvar + actionid + "_attack_range");
action_attribs.push(repvar + actionid + "_attack_target");
action_attribs.push(repvar + actionid + "_attack_tohit");
action_attribs.push(repvar + actionid + "_attack_damage");
action_attribs.push(repvar + actionid + "_attack_damagetype");
action_attribs.push(repvar + actionid + "_attack_damage2");
action_attribs.push(repvar + actionid + "_attack_damagetype2");
});
getAttrs(action_attribs, function(v) {
_.each(action_array, function(actionid) {
console.log("UPDATING NPC ACTION: " + actionid);
var callbacks = [];
var update = {};
var onhit = "";
var damage_flag = "";
var range = "";
var attack_flag = v[repvar + actionid + "_attack_flag"] && v[repvar + actionid + "_attack_flag"] != "0" ? "{{attack=1}}" : "";
var tohit = v[repvar + actionid + "_attack_tohit"] && isNaN(parseInt(v[repvar + actionid + "_attack_tohit"], 10)) === false ? parseInt(v[repvar + actionid + "_attack_tohit"], 10) : 0;
if(v[repvar + actionid + "_attack_type"] && v[repvar + actionid + "_attack_range"]) {
if(v[repvar + actionid + "_attack_type"] === "Melee") {var rangetype = "Reach";} else {var rangetype = "Range";};
range = ", " + rangetype + " " + v[repvar + actionid + "_attack_range"];
}
var target = v[repvar + actionid + "_attack_target"] && v[repvar + actionid + "_attack_target"] != "" ? ", " + v[repvar + actionid + "_attack_target"] : ""
var attack_tohitrange = "+" + tohit + range + target;
var dmg1 = v[repvar + actionid + "_attack_damage"] && v[repvar + actionid + "_attack_damage"] != "" ? v[repvar + actionid + "_attack_damage"] : "";
var dmg1type = v[repvar + actionid + "_attack_damagetype"] && v[repvar + actionid + "_attack_damagetype"] != "" ? " " + v[repvar + actionid + "_attack_damagetype"] : "";
var dmg2 = v[repvar + actionid + "_attack_damage2"] && v[repvar + actionid + "_attack_damage2"] != "" ? v[repvar + actionid + "_attack_damage2"] : "";
var dmg2type = v[repvar + actionid + "_attack_damagetype2"] && v[repvar + actionid + "_attack_damagetype2"] != "" ? " " + v[repvar + actionid + "_attack_damagetype2"] : "";
var dmgspacer = dmg1 != "" && dmg2 != "" ? " plus " : "";
var parsed_dmg1 = parse_roll_string(dmg1);
var parsed_dmg2 = parse_roll_string(dmg2);
if(dmg1 != "") {
onhit = onhit + parsed_dmg1.avg + " (" + dmg1 + ")" + dmg1type + " damage";
};
dmgspacer = dmg1 != "" && dmg2 != "" ? " plus " : "";
onhit = onhit + dmgspacer;
if(dmg2 != "") {
onhit = onhit + parsed_dmg2.avg + " (" + dmg2 + ")" + dmg2type + " damage";
};
if(dmg1 != "" || dmg2 != "") {damage_flag = damage_flag + "{{damage=1}} "};
if(dmg1 != "") {damage_flag = damage_flag + "{{dmg1flag=1}} "};
if(dmg2 != "") {damage_flag = damage_flag + "{{dmg2flag=1}} "};
var crit1 = "";
if(parsed_dmg1.rolls.length > 0){
parsed_dmg1.rolls.forEach(function(value) {
crit1 += parsed_dmg1.array[value] + "+";
});
crit1 = crit1.substring(0, crit1.length - 1);
}
var crit2 = "";
if(parsed_dmg2.rolls.length > 0){
parsed_dmg2.rolls.forEach(function(value) {
crit2 += parsed_dmg2.array[value] + "+";
});
crit2 = crit2.substring(0, crit2.length - 1);
}
var rollbase = "";
if(v.dtype === "full") {
rollbase = `@{wtype}&{template:npcaction} ${attack_flag} @{damage_flag} @{npc_name_flag} {{rname=@{name}}} {{r1=[[@{d20}+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{dmg1=[[@{attack_damage}+0]]}} {{dmg1type=@{attack_damagetype}}} {{dmg2=[[@{attack_damage2}+0]]}} {{dmg2type=@{attack_damagetype2}}} {{crit1=[[@{attack_crit}+0]]}} {{crit2=[[@{attack_crit2}+0]]}} {{description=@{show_desc}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
}
else if(v[repvar + actionid + "_attack_flag"] && v[repvar + actionid + "_attack_flag"] != "0") {
if(legendary) {
rollbase = `@{wtype}&{template:npcatk} ${attack_flag} @{damage_flag} @{npc_name_flag} {{rname=[@{name}](~repeating_npcaction-l_npc_dmg)}} {{rnamec=[@{name}](~repeating_npcaction-l_npc_crit)}} {{type=[Attack](~repeating_npcaction-l_npc_dmg)}} {{typec=[Attack](~repeating_npcaction-l_npc_crit)}} {{r1=[[@{d20}+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{description=@{show_desc}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`
}
else {
rollbase = `@{wtype}&{template:npcatk} ${attack_flag} @{damage_flag} @{npc_name_flag} {{rname=[@{name}](~repeating_npcaction_npc_dmg)}} {{rnamec=[@{name}](~repeating_npcaction_npc_crit)}} {{type=[Attack](~repeating_npcaction_npc_dmg)}} {{typec=[Attack](~repeating_npcaction_npc_crit)}} {{r1=[[@{d20}+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{description=@{show_desc}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`;
}
}
else if(dmg1 || dmg2) {
rollbase = `@{wtype}&{template:npcdmg} @{damage_flag} {{dmg1=[[@{attack_damage}+0]]}} {{dmg1type=@{attack_damagetype}}} {{dmg2=[[@{attack_damage2}+0]]}} {{dmg2type=@{attack_damagetype2}}} {{crit1=[[@{attack_crit}+0]]}} {{crit2=[[@{attack_crit2}+0]]}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`
}
else {
rollbase = `@{wtype}&{template:npcaction} @{npc_name_flag} {{rname=@{name}}} {{description=@{show_desc}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}`
}
update[repvar + actionid + "_attack_tohitrange"] = attack_tohitrange;
update[repvar + actionid + "_attack_onhit"] = onhit;
update[repvar + actionid + "_damage_flag"] = damage_flag;
update[repvar + actionid + "_attack_crit"] = crit1;
update[repvar + actionid + "_attack_crit2"] = crit2;
update[repvar + actionid + "_rollbase"] = rollbase;
setAttrs(update, {silent: true});
});
});
};
var parse_roll_string = function(rollstring) {
var out = {array: [], avg: 0, rolls: []};
if(!rollstring || rollstring === "") {
return out;
}
var rs_regex_initial = /(\-?\d+(?:d\d+)?)/ig;
var rs_regex_repeating = /([\+\-])(\-?\d+(?:d\d+)?)/ig;
var rs_nospace = rollstring.replace(/\s/g, '');
var rs_initial = rs_regex_initial.exec(rs_nospace);
if(rs_initial) {
out.array.push(rs_initial[1]);
rs_regex_repeating.lastIndex = rs_regex_initial.lastIndex;
var rs_repeating;
while(rs_repeating = rs_regex_repeating.exec(rs_nospace)) {
out.array.push(rs_repeating[1], rs_repeating[2]);
}
}
var add = true;
var dice_regex = /(\d+)d(\d+)/i;
var dice;
out.array.forEach(function(value, index, array) {
if(value === "+") {
add = true;
} else if(value === "-") {
add = false;
} else if(dice = dice_regex.exec(value)){
// this value is a die roll
var dice_avg = Math.floor(+dice[1] * (+dice[2] / 2 + 0.5));
out.avg += add ? dice_avg : -dice_avg;
out.rolls.push(index);
} else {
// this value is a number
out.avg += add ? +value : -+value;
}
})
return out;
};
var get_class_level = function(class_name, callback) {
getAttrs(["class", "base_level", "multiclass1_flag", "multiclass1", "multiclass1_lvl", "multiclass2_flag", "multiclass2", "multiclass2_lvl", "multiclass3_flag", "multiclass3", "multiclass3_lvl"], function(attrs) {
var regex = new RegExp(class_name, "i");
if(regex.test(attrs["class"])) {
callback(attrs.base_level);
} else if(attrs.multiclass1_flag && attrs.multiclass1_flag !== "0" && regex.test(attrs.multiclass1)) {
callback(attrs.multiclass1_lvl);
} else if(attrs.multiclass2_flag && attrs.multiclass2_flag !== "0" && regex.test(attrs.multiclass2)) {
callback(attrs.multiclass2_lvl);
} else if(attrs.multiclass3_flag && attrs.multiclass3_flag !== "0" && regex.test(attrs.multiclass3)) {
callback(attrs.multiclass3_lvl);
} else {
callback("0");
}
});
};
var update_globaldamage = function(callback) {
getSectionIDs("damagemod", function(ids) {
if(ids) {
var fields = {};
var attr_name_list = [];
ids.forEach(function(id) {
fields[id] = {};
attr_name_list.push(`repeating_damagemod_${id}_global_damage_active_flag`, `repeating_damagemod_${id}_global_damage_name`, `repeating_damagemod_${id}_global_damage_damage`, `repeating_damagemod_${id}_global_damage_type`);
});
getAttrs(attr_name_list, function(attrs) {
var regex = /^repeating_damagemod_(.+)_global_damage_(active_flag|name|damage|type)$/;
_.each(attrs, function(obj, name) {
var r = regex.exec(name);
if(r) {
fields[r[1]][r[2]] = obj;
};
});
var update = {global_damage_mod_roll: "", global_damage_mod_crit: "", global_damage_mod_type: ""};
console.log("GLOBALDAMAGE");
_.each(fields, function(element) {
if(element.active_flag != "0") {
if(element.name && element.name !== "") { update["global_damage_mod_roll"] += element.damage + '[' + element.name + ']' + "+"; }
if(element.type && element.type !== "") { update["global_damage_mod_type"] += element.type + "/"; }
}
});
update["global_damage_mod_roll"] = update["global_damage_mod_roll"].replace(/\+(?=$)/, '');
update["global_damage_mod_type"] = update["global_damage_mod_type"].replace(/\/(?=$)/, '');
// Remove any non-roll damage modifiers from the global damage modifier for the crit rolls
// Will also remove any labels attached to these non-roll damage modifiers
update["global_damage_mod_crit"] = update["global_damage_mod_roll"].replace(/(?:[+\-*\/%]|\*\*|^)\s*\d+(?:\[.*?])?(?!d\d+)/gi, '')
// If what was just replace removed the first damage modifier, remove any now precending plus signs
.replace(/(?:^\+)/i, '');
setAttrs(update, {silent:true}, function() {
update_attacks("all");
if(typeof callback == "function") callback();
});
});
}
});
};
var update_globalattack = function(callback) {
getSectionIDs("tohitmod", function(ids) {
if(ids) {
var fields = {};
var attr_name_list = [];
ids.forEach(function(id) {
fields[id] = {};
attr_name_list.push(`repeating_tohitmod_${id}_global_attack_active_flag`, `repeating_tohitmod_${id}_global_attack_roll`, `repeating_tohitmod_${id}_global_attack_name`);
});
getAttrs(attr_name_list, function(attrs) {
var regex = /^repeating_tohitmod_(.+)_global_attack_(active_flag|roll|name)$/;
_.each(attrs, function(obj, name) {
var r = regex.exec(name);
if(r) {
fields[r[1]][r[2]] = obj;
}
});
var update = {global_attack_mod: ""};
console.log("GLOBALATTACK");
_.each(fields, function(element) {
if(element.active_flag != "0") {
if(element.roll && element.roll !== "") { update["global_attack_mod"] += element.roll + "[" + element.name + "]" + "+"; }
}
});
if(update["global_attack_mod"] !== "") {
update["global_attack_mod"] = "[[" + update["global_attack_mod"].replace(/\+(?=$)/, '') + "]]";
}
setAttrs(update, {silent:true}, function() {
if(typeof callback == "function") callback();
});
});
}
});
};
var update_globalsaves = function(callback) {
getSectionIDs("savemod", function(ids) {
if(ids) {
var fields = {};
var attr_name_list = [];
ids.forEach(function(id) {
fields[id] = {};
attr_name_list.push(`repeating_savemod_${id}_global_save_active_flag`, `repeating_savemod_${id}_global_save_roll`, `repeating_savemod_${id}_global_save_name`);
});
getAttrs(attr_name_list, function(attrs) {
var regex = /^repeating_savemod_(.+)_global_save_(active_flag|roll|name)$/;
_.each(attrs, function(obj, name) {
var r = regex.exec(name);
if(r) {
fields[r[1]][r[2]] = obj;
}
});
var update = {global_save_mod: ""};
console.log("GLOBAL SAVES");
_.each(fields, function(element) {
if(element.active_flag != "0") {
if(element.roll && element.roll !== "") { update["global_save_mod"] += element.roll + "[" + element.name + "]" + "+"; }
}
});
if(update["global_save_mod"] !== "") {
update["global_save_mod"] = "[[" + update["global_save_mod"].replace(/\+(?=$)/, '') + "]]";
}
setAttrs(update, {silent:true}, function() {
if(typeof callback == "function") callback();
});
});
}
});
};
var update_globalskills = function(callback) {
getSectionIDs("skillmod", function(ids) {
if(ids) {
var fields = {};
var attr_name_list = [];
ids.forEach(function(id) {
fields[id] = {};
attr_name_list.push(`repeating_skillmod_${id}_global_skill_active_flag`, `repeating_skillmod_${id}_global_skill_roll`, `repeating_skillmod_${id}_global_skill_name`);
});
getAttrs(attr_name_list, function(attrs) {
var regex = /^repeating_skillmod_(.+)_global_skill_(active_flag|roll|name)$/;
_.each(attrs, function(obj, name) {
var r = regex.exec(name);
if(r) {
fields[r[1]][r[2]] = obj;
}
});
var update = {global_skill_mod: ""};
console.log("GLOBAL SKILLS");
_.each(fields, function(element) {
if(element.active_flag != "0") {
if(element.roll && element.roll !== "") { update["global_skill_mod"] += element.roll + "[" + element.name + "]" + "+"; }
}
});
if(update["global_skill_mod"] !== "") {
update["global_skill_mod"] = "[[" + update["global_skill_mod"].replace(/\+(?=$)/, '') + "]]";
}
setAttrs(update, {silent:true}, function() {
if(typeof callback == "function") callback();
});
});
}
});
};
var check_l1_mancer = function() {
getAttrs(["class", "base_level", "strength_base","dexterity_base","constitution_base","intelligence_base","wisdom_base","charisma_base","l1mancer_status","version","charactermancer_step"], function(v) {
if(!v["version"] || parseFloat(v["version"]) < 2.2) {
return;
}
if(v["l1mancer_status"] && v["l1mancer_status"] === "completed") {
return;
}
if(v["charactermancer_step"] && v["charactermancer_step"].split("-")[0] == "l1") {
startCharactermancer(v["charactermancer_step"]);
}
else {
if(v["l1mancer_status"] && v["l1mancer_status"] === "relaunch") {
startCharactermancer("l1-welcome");
} else {
setAttrs({mancer_confirm_flag: 1});
}
}
});
};
var check_lp_mancer = function() {
getAttrs(["class", "base_level", "strength_base","dexterity_base","constitution_base","intelligence_base","wisdom_base","charisma_base","l1mancer_status", "lpmancer_status","version","charactermancer_step"], function(v) {
if(v["lpmancer_status"] === "active" && v["charactermancer_step"] && v["charactermancer_step"].split("-")[0] == "lp") {
startCharactermancer(v["charactermancer_step"]);
}
});
};
on("mancer:cancel", function(eventinfo) {
if(!eventinfo["value"]) {return;};
var update = {};
if(eventinfo["value"] === "l1-welcome" || eventinfo["value"] === "l1-cancel") {
update["l1mancer_status"] = "completed";
update["charactermancer_step"] = "l1-welcome";
deleteCharmancerData(["l1-welcome","l1-race","l1-class","l1-abilities","l1-background","l1-equipment","l1-spells","l1-summary"]);
}
else if(eventinfo["value"].substring(0,3) === "l1-") {
update["l1mancer_status"] = eventinfo["value"];
}
else if(eventinfo["value"] === "lp-welcome" || eventinfo["value"] === "lp-cancel") {
update["lpmancer_status"] = "";
update["charactermancer_step"] = "";
deleteCharmancerData(["lp-welcome", "lp-levels", "lp-choices", "lp-asi", "lp-spells","lp-summary"]);
}
else if(eventinfo["value"].substring(0,3) === "lp-") {
update["charactermancer_step"] = "";
update["lpmancer_status"] = "";
};
setAttrs(update);
});
var v2_old_values_check = function() {
// update_attacks("all");
var update = {};
var attrs = ["simpletraits","features_and_traits","initiative_bonus","npc","character_id"];
getSectionIDs("repeating_spell-npc", function(idarray) {
_.each(idarray, function(id) {
attrs.push("repeating_spell-npc_" + id + "_rollcontent");
});
getAttrs(attrs, function(v) {
if(v["npc"] && v["npc"] == 1 && (!v["initiative_bonus"] || v["initiative_bonus"] == 0)) {
update_initiative();
}
var spellflag = idarray && idarray.length > 0 ? 1 : 0;
var missing = v["features_and_traits"] && v["simpletraits"] === "complex" ? 1 : 0;
update["npcspell_flag"] = spellflag;
update["missing_info"] = missing;
_.each(idarray, function(id) {
var content = v["repeating_spell-npc_" + id + "_rollcontent"];
if(content.substring(0,3) === "%{-" && content.substring(22,41) === "|repeating_attack_-" && content.substring(60,68) === "_attack}") {
var thisid = content.substring(2,21);
if(thisid != v["character_id"]) {
update["repeating_spell-npc_" + id + "_rollcontent"] = content.substring(0,2) + v["character_id"] + content.substring(22,68);
}
}
});
setAttrs(update);
});
});
};
var clear_npc_spell_attacks = function(complete) {
getSectionIDs("repeating_attack", function(attack_ids) {
var getList = [];
var done = false;
_.each(attack_ids, function(id) {
getList.push(`repeating_attack_${id}_spellid`);
});
getAttrs(getList, function(v) {
_.each(attack_ids, function(id) {
if (v[`repeating_attack_${id}_spellid`] && v[`repeating_attack_${id}_spellid`].indexOf("npc_") != -1) {
removeRepeatingRow(`repeating_attack_${id}`);
}
});
complete();
});
});
}
var upgrade_to_2_0 = function(doneupdating) {
getAttrs(["npc","strength","dexterity","constitution","intelligence","wisdom","charisma","strength_base","dexterity_base","constitution_base","intelligence_base","wisdom_base","charisma_base","deathsavemod","death_save_mod","npc_str_save","npc_dex_save","npc_con_save","npc_int_save","npc_wis_save","npc_cha_save","npc_str_save_base","npc_dex_save_base","npc_con_save_base","npc_int_save_base","npc_wis_save_base","npc_cha_save_base","npc_acrobatics_base", "npc_astrophysics_base", "npc_athletics_base", "npc_computers_base", "npc_deception_base", "npc_insight_base", "npc_intimidation_base", "npc_investigation_base", "npc_lore_base", "npc_mechanics_base", "npc_medicine_base", "npc_perception_base", "npc_performance_base", "npc_persuasion_base", "npc_sleight_of_hand_base", "npc_stealth_base", "npc_survival_base", "npc_xenobiology_base", "npc_piloting_base", "npc_acrobatics", "npc_astrophysics", "npc_athletics", "npc_computers", "npc_deception", "npc_insight", "npc_intimidation", "npc_investigation", "npc_lore", "npc_mechanics", "npc_medicine", "npc_perception", "npc_performance", "npc_persuasion", "npc_sleight_of_hand", "npc_stealth", "npc_survival", "npc_xenobiology", "npc_piloting"], function(v) {
var update = {};
var stats = ["strength","dexterity","constitution","intelligence","wisdom","charisma"];
var npc_stats = ["npc_str_save","npc_dex_save","npc_con_save","npc_int_save","npc_wis_save","npc_cha_save","npc_acrobatics", "npc_astrophysics", "npc_athletics", "npc_computers", "npc_deception", "npc_insight", "npc_intimidation", "npc_investigation", "npc_lore", "npc_mechanics", "npc_medicine", "npc_perception", "npc_performance", "npc_persuasion", "npc_piloting", "npc_sleight_of_hand", "npc_stealth", "npc_survival", "npc_xenobiology"];
_.each(stats, function(attr) {
if(v[attr] && v[attr] != "10" && v[attr + "_base"] == "10") {
update[attr + "_base"] = v[attr];
}
});
_.each(npc_stats, function(attr) {
if(v[attr] && !isNaN(v[attr]) && v[attr + "_base"] == "") {
update[attr + "_base"] = v[attr];
}
});
if(v["deathsavemod"] && v["deathsavemod"] != "0" && v["death_save_mod"] === "0") {v["death_save_mod"] = v["deathsavemod"];};
if(v["npc"] && v["npc"] == "1") {
var callback = function() {
update_attr("all");
update_mod("strength");
update_mod("dexterity");
update_mod("constitution");
update_mod("intelligence");
update_mod("wisdom");
update_mod("charisma");
update_npc_action("all");
update_npc_saves();
update_npc_skills();
update_initiative();
}
}
else {
var callback = function() {
update_attr("all");
update_mod("strength");
update_mod("dexterity");
update_mod("constitution");
update_mod("intelligence");
update_mod("wisdom");
update_mod("charisma");
update_all_saves();
update_skills(["athletics", "acrobatics", "sleight_of_hand", "stealth", "astrophysics", "lore", "investigation", "xenobiology", "computers", "mechanics", "insight", "medicine", "perception", "survival", "deception", "intimidation", "performance", "persuasion", "piloting", "animal_handling", "arcana", "history", "nature", "religion"]);
update_tool("all")
update_attacks("all");
update_pb();
update_jack_attr();
update_initiative();
update_weight();
update_spell_info();
update_ac();
}
}
setAttrs(update, {silent: true}, callback);
doneupdating();
});
};
var upgrade_to_2_1 = function(doneupdating) {
v2_old_values_check();
doneupdating();
};
var upgrade_to_2_2 = function(doneupdating) {
setAttrs({l1mancer_status: "completed"}, function(eventinfo) {
setAttrs({"options-class-selection":"0"});
console.log("Preprocessed v2.2 upgrade");
update_tool("all");
update_attacks("all");
update_class();
update_race_display();
doneupdating();
});
};
var upgrade_to_2_3 = function(doneupdating) {
getSectionIDs("damagemod", function(ids) {
var update = {};
_.each(ids, function(rowid) {
update[`repeating_damagemod_${rowid}_options-flag`] = "0";
});
getSectionIDs("tohitmod", function(ids) {
_.each(ids, function(rowid) {
update[`repeating_tohitmod_${rowid}_options-flag`] = "0";
});
setAttrs(update);
doneupdating();
});
});
};
var upgrade_to_2_4 = function(doneupdating) {
clear_npc_spell_attacks(function() {
update_globalskills(function() {
update_globalsaves(function() {
update_globalattack(function() {
update_globaldamage(function() {
getAttrs(["npc", "npcspellcastingflag", "spellcasting_ability", "caster_level", "level_calculations"], function(v) {
if(v.npc == "1" && v.npcspellcastingflag == "1") {
getSectionIDs("npctrait", function(secIds) {
var getList = [];
_.each(secIds, function(x) {
getList.push("repeating_npctrait_" + x + "_name");
getList.push("repeating_npctrait_" + x + "_desc");
});
getAttrs(getList, function(traits) {
var update = {};
if(v.spellcasting_ability == "0*" || v.caster_level == "0") {
var spellSec = "";
if (v.spellcasting_ability == "0*") {
update.spellcasting_ability = "@{intelligence_mod}+";
}
_.each(secIds, function(traitId) {
if(traits["repeating_npctrait_" + traitId + "_name"].toLowerCase().includes("spellcasting.")) spellSec = traitId;
});
if(spellSec != "") {
var spellcasting = traits["repeating_npctrait_" + spellSec + "_desc"].toLowerCase();
if (v.spellcasting_ability == "0*") {
var lastIndex = 9999;
_.each(["intelligence", "wisdom", "charisma"], function(ability) {
var found = spellcasting.indexOf(ability);
if(found > -1 && found < lastIndex) {
lastIndex = found;
update.spellcasting_ability = "@{" + ability + "_mod}+";
}
});
}
if (v.caster_level == "0") {
var foundLevelidx = spellcasting.search(/(\d|\d\d)(st|nd|rd|th)/);
if (foundLevelidx) {
var level = parseInt(spellcasting.substring(foundLevelidx, foundLevelidx+4));
console.log(`Found spellcasting level ${level} in trait, setting caster_level...`);
update.caster_level = level;
}
}
}
}
setAttrs(update, function() {
// Recalculate spell slots in case NPC level was restored
if(!v["level_calculations"] || v["level_calculations"] == "on") {
update_spell_slots();
};
// Set all spells without a given modifier to 'spell'
var spgetList = [];
getSectionIDs("spell-prime", function(secIds0) {
_.each(secIds0, function(x) {
spgetList.push("repeating_spell-prime_" + x + "_spell_ability");
});
getSectionIDs("spell-1", function(secIds1) {
_.each(secIds1, function(x) {
spgetList.push("repeating_spell-1_" + x + "_spell_ability");
});
getSectionIDs("spell-2", function(secIds2) {
_.each(secIds2, function(x) {
spgetList.push("repeating_spell-2_" + x + "_spell_ability");
});
getSectionIDs("spell-3", function(secIds3) {
_.each(secIds3, function(x) {
spgetList.push("repeating_spell-3_" + x + "_spell_ability");
});
getSectionIDs("spell-4", function(secIds4) {
_.each(secIds4, function(x) {
spgetList.push("repeating_spell-4_" + x + "_spell_ability");
});
getSectionIDs("spell-5", function(secIds5) {
_.each(secIds5, function(x) {
spgetList.push("repeating_spell-5_" + x + "_spell_ability");
});
getSectionIDs("spell-6", function(secIds6) {
_.each(secIds6, function(x) {
spgetList.push("repeating_spell-6_" + x + "_spell_ability");
});
getSectionIDs("spell-7", function(secIds7) {
_.each(secIds7, function(x) {
spgetList.push("repeating_spell-7_" + x + "_spell_ability");
});
getSectionIDs("spell-8", function(secIds8) {
_.each(secIds8, function(x) {
spgetList.push("repeating_spell-8_" + x + "_spell_ability");
});
getSectionIDs("spell-9", function(secIds9) {
_.each(secIds9, function(x) {
spgetList.push("repeating_spell-9_" + x + "_spell_ability");
});
getAttrs(spgetList, function(spellAbilities) {
spupdate = {};
_.each(spellAbilities, function(ability, attributeName) {
if (ability == "0*") {
console.log("UPDATING SPELL: " + attributeName);
spupdate[attributeName] = "spell";
}
});
setAttrs(spupdate, function() {
update_attacks("spells");
update_challenge();
doneupdating();
});
});
});
});
});
});
});
});
});
});
});
});
});
});
});
} else if(v.npc != "1") {
doneupdating();
} else {
doneupdating();
}
});
});
});
});
});
});
};
var upgrade_to_2_5 = function(doneupdating) {
getAttrs(["globalacmod"], function(v) {
var update = {};
if(v.globalacmod && v.globalacmod != "0") {
var rowid = generateRowID();
update[`repeating_acmod_${rowid}_global_ac_val`] = parseInt(v.globalacmod);
update[`repeating_acmod_${rowid}_global_ac_name`] = "GLOBAL ARMOR CLASS MODIFIER";
update[`repeating_acmod_${rowid}_global_ac_active_flag`] = "1";
update[`repeating_acmod_${rowid}_options-flag`] = "0";
update["global_ac_mod_flag"] = "1";
}
setAttrs(update, {silent: true}, function() {
update_ac();
doneupdating();
});
});
};
var upgrade_to_2_6 = function(doneupdating) {
getSectionIDs("hpmod", function(ids) {
var getArray = [];
_.each(ids, function(sectionid) {
getArray.push("repeating_hpmod_" + sectionid + "_source");
});
getAttrs(getArray, function(v) {
_.each(v, function(val, key) {
if(val === "CON") removeRepeatingRow("repeating_hpmod_" + key.split("_")[2]);
});
doneupdating();
});
});
};
var upgrade_to_2_7 = function(doneupdating) {
getSectionIDs("damagemod", function(ids) {
console.log("Version 2.7 UPGRADE");
let getArray = [];
ids.forEach(sectionid => {
getArray.push(`repeating_damagemod_${sectionid}_global_damage_rollstring`);
getArray.push(`repeating_damagemod_${sectionid}_global_damage_type`);
});
getSectionIDs("savemod", function(ids) {
_.each(ids, (sectionid) => {
getArray.push(`repeating_savemod_${sectionid}_global_save_rollstring`);
});
getSectionIDs("tohitmod", function(ids) {
_.each(ids, (sectionid) => {
getArray.push(`repeating_tohitmod_${sectionid}_global_attack_rollstring`);
});
getSectionIDs("skillmod", function(ids) {
_.each(ids, (sectionid) => {
getArray.push(`repeating_skillmod_${sectionid}_global_skill_rollstring`);
});
getAttrs(getArray, function(v) {
let set = {};
_.each(v, (value, attr) => {
if(_.last(attr.split("_")) === "rollstring") {
const section = attr.slice(0, -10);
const brackets = value.split('['), roll = brackets[0];
let name = brackets[1] ? brackets[1].slice(0, -1) : "";
if(attr.split("_")[1] == "damagemod") {
name = name ? name : v[`${section}type`];
set[`${section}damage`] = roll;
} else {
set[`${section}roll`] = roll;
}
set[`${section}name`] = name;
}
});
setAttrs(set);
doneupdating();
});
});
});
});
});
};
var no_version_bugfix = function(doneupdating) {
getAttrs(["npc","class"], function(v) {
if(v["npc"] && v["npc"] != "0" || v["class"] && v["class"] != "") {
setAttrs({version: "2.1"});
}
else {
setAttrs({version: "2.3"}, {silent: true});
}
});
doneupdating();
};
var versioning = function(finished) {
getAttrs(["version"], function(v) {
const version = parseFloat(v["version"]) || 0.0;
if(version >= "2.7") {
setAttrs({version: "2.7"}, function(){
finished();
});
console.log(`Esper Genesis by Roll20 v${v["version"]}`);
return;
}
else if(!version || version === "") {
console.log("NO VERSION FOUND");
no_version_bugfix(function() {
versioning(finished);
});
}
else if(version >= "2.6") {
console.log("UPGRADING TO v2.7");
upgrade_to_2_7(function() {
setAttrs({version: "2.7"}, function() {
versioning(finished);
});
});
}
else if(version >= "2.5") {
console.log("UPGRADING TO v2.6");
upgrade_to_2_6(function() {
setAttrs({version: "2.6"}, function() {
versioning(finished);
});
});
}
else if(version >= "2.4") {
console.log("UPGRADING TO v2.5");
upgrade_to_2_5(function() {
setAttrs({version: "2.5"}, function() {
versioning(finished);
});
});
}
else if(version >= "2.3") {
console.log("UPGRADING TO v2.4");
upgrade_to_2_4(function() {
setAttrs({version: "2.4"}, function() {
versioning(finished);
});
});
}
else if(version >= "2.2") {
console.log("UPGRADING TO v2.3");
upgrade_to_2_3(function() {
setAttrs({version: "2.3"}, function() {
versioning(finished);
});
});
}
else if(version >= "2.1") {
console.log("UPGRADING TO v2.2");
upgrade_to_2_2(function() {
setAttrs({version: "2.2"}, function() {
versioning(finished);
});
});
}
else if(version >= "2.0") {
console.log("UPGRADING TO v2.1");
upgrade_to_2_1(function() {
setAttrs({version: "2.1"}, function() {
versioning(finished);
});
});
}
else {
console.log("UPGRADING TO v2.0");
upgrade_to_2_0(function() {
setAttrs({version: "2.0"}, function() {
versioning(finished);
});
});
};
});
};
const classRelatedAttrs = ['class', 'subclass', 'base_level', 'multiclass1_flag', 'multiclass1', 'multiclass1_subclass', 'multiclass1_lvl', 'multiclass2_flag','multiclass2', 'multiclass2_subclass', 'multiclass2_lvl', 'multiclass3_flag', 'multiclass3', 'multiclass3_subclass', 'multiclass3_lvl'];
const getTalentPointsAndMastery = callback => {
getAttrs(classRelatedAttrs, values => {
let level = 0;
let mastery = 0;
const talentPointPerClass = {
'adept': 1,
'cybermancer': 1,
'melder': 1,
'artifice': 1/3,
'justicar': 1/3
};
const hasMastery = Object.keys(talentPointPerClass).filter(c => { return talentPointPerClass[c] === 1; });
let characterClassAttributes = {
mainclass: {
class: 'class',
subclass: 'subclass',
level: 'base_level'
}
};
for(let i=1; i<=3; i++) {
characterClassAttributes[`multiclass_${i}`] = {
enabled: `multiclass${i}_flag`,
class: `multiclass${i}`,
subclass: `multiclass${i}_subclass`,
level: `multiclass${i}_lvl`
}
}
const pointsPerLevel = ['',4,6,14,17,27,32,38,44,57,64,73,78,83,88,94,100,107,114,123,133];
const getLevelInfo = (charClass, values) => {
let charLevel = {
level: 0,
mastery: 0
};
if(!characterClassAttributes[charClass]) return charLevel;
if(!characterClassAttributes[charClass]['enabled'] || (values[characterClassAttributes[charClass]['enabled']] && values[characterClassAttributes[charClass]['enabled']] == 1)) {
const subclass = values[characterClassAttributes[charClass]['subclass']] || '';
const level = (values[characterClassAttributes[charClass]['level']]) ? parseInt(values[characterClassAttributes[charClass]['level']]) : 0;
charClass = (subclass.toLowerCase() == 'artifice' || subclass.toLowerCase() == 'justicar') ? subclass : values[characterClassAttributes[charClass]['class']];
const classIndex = Object.keys(talentPointPerClass).indexOf(charClass.toLowerCase());
if(classIndex > -1) {
charLevel.level = Math.floor(level * talentPointPerClass[Object.keys(talentPointPerClass)[classIndex]]);
charLevel.mastery = (hasMastery.indexOf(charClass.toLowerCase()) > -1) ? Math.ceil(Math.min(Math.max(level-10, 0), 7)/2) : 0;
}
}
return charLevel;
}
Object.keys(characterClassAttributes).forEach(charClass => {
const levelInfo = getLevelInfo(charClass, values);
level += levelInfo.level;
mastery = Math.max(mastery, levelInfo.mastery);
});
callback(pointsPerLevel[level], mastery);
});
};
const setTalentPointsAndMastery = () => {
getTalentPointsAndMastery((talentPoints, mastery) => {
console.log('CALCULATING TALENT POINTS AND ESPER MASTERY');
let update = {
tp_max:talentPoints,
mastery_6th: 0,
mastery_7th: 0,
mastery_8th: 0,
mastery_9th: 0
}
for(let i=0; i<mastery; i++) {
const masteryLevel = `mastery_${i+6}th`;
update[masteryLevel] = 1;
}
setAttrs(update);
});
}
classRelatedAttrs.forEach(attr => {
on(`change:${attr}`, evenInfo => {
setTalentPointsAndMastery();
});
});
/*
@{wtype}&{template:dmg} {{rname=@{atkname}}} @{atkflag} {{range=@{atkrange}}} @{dmgflag} {{dmg1=[[1d10]]}} {{dmg1type=}} @{dmg2flag} {{dmg2=[[0]]}} {{dmg2type=}} @{saveflag} {{desc=@{atk_desc}}} @{hldmg} {{spelllevel=@{spelllevel}}} {{innate=@{spell_innate}}} {{globaldamage=[[0]]}} {{globaldamagetype=@{global_damage_mod_type}}} @{charname_output} {{licensedsheet=@{licensedsheet}}}
on("change:repeating_tool:toolname change:repeating_tool:toolbonus_base change:repeating_tool:toolattr_base
*/
var calcShipAtkdmgc = function(damage) {
let damagec = damage.toLowerCase();
for(let i=20; i>=1; i--) {
damagec = damagec.replace(`${i}d`, `${i*2}d`);
}
damagec = damagec.replace(/\bd(?=[0-9])/g, '2d');
return damagec;
};
var addShipDamageType = function(damage, type) {
if(!damage || !type) return damage;
return `${damage} [${type}]`;
}
on("change:repeating_weapon-systems", eventinfo => {
if(!eventinfo.sourceType || eventinfo.sourceType != 'player' || eventinfo.sourceAttribute.includes('atkdmg')) return;
const id = SheetUtils.getUID(eventinfo.triggerName);
const pre = `repeating_weapon-systems_${id}`;
const weaponAttributes = ['gunner', 'weapon', 'range', 'tohit', 'damage', 'dmgtype', 'special', 'ammo', 'recharge'].map(attr => { return `${pre}_${attr}`; });
getAttrs(weaponAttributes, values => {
if(values[`${pre}_damage`]) {
const damage = addShipDamageType(values[`${pre}_damage`], values[`${pre}_dmgtype`]);
const damagec = addShipDamageType(calcShipAtkdmgc(values[`${pre}_damage`]), values[`${pre}_dmgtype`]);
const atkdmg = `@{wtype}&{template:dmg} {{rname=@{weapon}}} {{attack=1}} {{range=@{range}}} {{damage=1}} {{dmg1flag=1}} {{dmg1=[[${damage}]]}} {{dmg1type=${values[`${pre}_dmgtype`]}}} {{desc=@{special}}} @{character_name} {{licensedsheet=@{licensedsheet}}}`;
const atkdmgc = `@{wtype}&{template:dmg} {{rname=@{weapon}}} {{attack=1}} {{range=@{range}}} {{damage=1}} {{dmg1flag=1}} {{dmg1=[[${damagec}]]}} {{dmg1type=${values[`${pre}_dmgtype`]}}} {{desc=@{special}}} @{character_name} {{licensedsheet=@{licensedsheet}}}`;
const update = {
[`${pre}_atkdmg`]: atkdmg,
[`${pre}_atkdmgc`]: atkdmgc,
}
setAttrs(update, {silent: true});
}
});
});
// ---- Feature: Spell Points Variant ----
const createResource = function(name, max, current) {
getAttrs(["other_resource_name"], v => {
if (!v.other_resource_name || v.other_resource_name === "" || v.other_resource_name === name) {
setAttrs({ other_resource_name: name, other_resource_max: max, other_resource: current });
} else {
getSectionIDs("repeating_resource", ids => {
if (ids.length === 0) {
const newid = generateRowID();
setAttrs({
[`repeating_resource_${newid}_resource_left_name`]: name,
[`repeating_resource_${newid}_resource_left_max`]: max,
[`repeating_resource_${newid}_resource_left`]: current
});
return;
}
const nameAttrs = ids.map(id => `repeating_resource_${id}_resource_left_name`)
.concat(ids.map(id => `repeating_resource_${id}_resource_right_name`));
getAttrs(nameAttrs, nv => {
const existingKey = Object.keys(nv).find(k => nv[k] === name);
const side = existingKey && existingKey.includes("_right_") ? "right" : "left";
const existingID = existingKey
? existingKey.replace("repeating_resource_", "").split("_resource_")[0]
: generateRowID();
setAttrs({
[`repeating_resource_${existingID}_resource_${side}_name`]: name,
[`repeating_resource_${existingID}_resource_${side}_max`]: max,
[`repeating_resource_${existingID}_resource_${side}`]: current
});
});
});
}
});
};
const formatPrintedTools = () => {
getSectionIDs("tool", (ids) => {
const attrs = [
...ids.map((id) => `repeating_tool_${id}_toolbonus_base`),
...ids.map((id) => `repeating_tool_${id}_toolname`),
];
getAttrs(attrs, (values) => {
const tools = [];
ids.forEach((id) => {
const expert = values[`repeating_tool_${id}_toolbonus_base`] && values[`repeating_tool_${id}_toolbonus_base`] == "(@{pb}*2)" ? "⁺" : "";
const name = values[`repeating_tool_${id}_toolname`];
if (name) tools.push(name + expert);
});
setAttrs({ other_tool: tools.sort().join(", ") });
});
});
};
const formatPrintedSkills = () => {
getSectionIDs("proficiencies", (ids) => {
const attrs = [
...ids.map((id) => `repeating_proficiencies_${id}_prof_type`),
...ids.map((id) => `repeating_proficiencies_${id}_name`),
];
getAttrs(attrs, (values) => {
const otherSkills = { language: [], weapon: [], armor: [], other: [] };
ids.forEach((id) => {
const type = values[`repeating_proficiencies_${id}_prof_type`];
const name = values[`repeating_proficiencies_${id}_name`];
if (type && name) otherSkills[type.toLowerCase()].push(name);
});
setAttrs({
other_languages: otherSkills.language.sort().join(", "),
other_armor: otherSkills.armor.sort().join(", "),
other_weapon: otherSkills.weapon.sort().join(", "),
other_other: otherSkills.other.sort().join(", "),
});
});
});
};
const deleteResource = function(name) {
getAttrs(["other_resource_name"], v => {
if (v.other_resource_name === name) {
setAttrs({ other_resource_name: "", other_resource_max: "", other_resource: "" });
} else {
getSectionIDs("repeating_resource", ids => {
if (ids.length === 0) return;
const nameAttrs = ids.map(id => `repeating_resource_${id}_resource_left_name`)
.concat(ids.map(id => `repeating_resource_${id}_resource_right_name`));
getAttrs(nameAttrs, nv => {
const existingKey = Object.keys(nv).find(k => nv[k] === name);
if (!existingKey) return;
const existingID = existingKey.replace("repeating_resource_", "").split("_resource_")[0];
const side = existingKey.includes("_right_") ? "right" : "left";
const otherSide = side === "left" ? "right" : "left";
if (!nv[`repeating_resource_${existingID}_resource_${otherSide}_name`]) {
removeRepeatingRow(`repeating_resource_${existingID}`);
} else {
setAttrs({
[`repeating_resource_${existingID}_resource_${side}_name`]: "",
[`repeating_resource_${existingID}_resource_${side}_max`]: "",
[`repeating_resource_${existingID}_resource_${side}`]: ""
});
}
});
});
}
});
};
const getSpellPoints = function(callback) {
const classAttrs = ["base_level","class","multiclass1_flag","multiclass1","multiclass1_lvl",
"multiclass2_flag","multiclass2","multiclass2_lvl","multiclass3_flag","multiclass3","multiclass3_lvl",
"arcane_fighter","arcane_rogue"];
const pointsPerLevel = [0,4,6,14,17,27,32,38,44,57,64,73,78,83,88,94,100,107,114,123,133];
getAttrs(classAttrs, v => {
const classes = [[v["class"] || "", v["base_level"] || 0]].concat(
[1,2,3].filter(i => v[`multiclass${i}_flag`] === "1")
.map(i => [v[`multiclass${i}`] || "", v[`multiclass${i}_lvl`] || 0])
);
const multicaster = isMultiCaster(classes, v["arcane_fighter"], v["arcane_rogue"]);
let totalCasterLevel = 0;
classes.forEach(function([cls, lvl]) {
const lvlInt = parseInt(lvl) || 0;
const fraction = getCasterType(cls, lvlInt, v["arcane_fighter"], v["arcane_rogue"]);
const contribution = lvlInt * fraction;
totalCasterLevel += multicaster ? Math.floor(contribution) : Math.ceil(contribution);
});
const level = Math.min(Math.max(Math.round(totalCasterLevel), 0), 20);
callback(pointsPerLevel[level] || 0);
});
};
const setSpellPoints = function() {
getSpellPoints(points => {
if (points > 0) createResource("Spell Points", points, points);
});
};
on("change:use_spell_points", eventInfo => {
if (eventInfo.newValue && eventInfo.newValue == "1") {
setSpellPoints();
} else {
deleteResource("Spell Points");
}
update_spell_slots();
});
// ---- Feature: Short/Long Rest Buttons ----
class Rest {
static resetResources(types) {
if (!types.includes("long")) return;
getSectionIDs("resource", ids => {
const attributes = [
"class_resource_max", "class_resource_use_pb",
"other_resource_max", "other_resource_use_pb",
"pb"
];
ids.forEach(id => {
attributes.push(`repeating_resource_${id}_resource_left_max`);
attributes.push(`repeating_resource_${id}_resource_left_use_pb`);
attributes.push(`repeating_resource_${id}_resource_right_max`);
attributes.push(`repeating_resource_${id}_resource_right_use_pb`);
});
getAttrs(attributes, values => {
const update = {};
const pb = parseInt(values["pb"]) || 0;
const resetOne = (currentKey, maxKey, pbKey) => {
const usePB = values[pbKey] === "1";
const max = usePB ? pb : parseInt(values[maxKey]);
if (!isNaN(max) && max > 0) update[currentKey] = max;
};
resetOne("class_resource", "class_resource_max", "class_resource_use_pb");
resetOne("other_resource", "other_resource_max", "other_resource_use_pb");
ids.forEach(id => {
resetOne(`repeating_resource_${id}_resource_left`, `repeating_resource_${id}_resource_left_max`, `repeating_resource_${id}_resource_left_use_pb`);
resetOne(`repeating_resource_${id}_resource_right`, `repeating_resource_${id}_resource_right_max`, `repeating_resource_${id}_resource_right_use_pb`);
});
setAttrs(update);
});
});
}
static resetHP() {
getAttrs(["hp_max"], values => {
if(!isNaN(values["hp_max"])) setAttrs({hp: values["hp_max"]});
});
}
static resetHitDice() {
getAttrs(["level", "hit_dice", "hit_dice_max"], values => {
const update = {};
const level = parseInt(values["level"]) || 0;
const maxHitDice = Math.max(Math.floor(level / 2), 1);
if(!isNaN(values["hit_dice_max"])) {
if(!isNaN(values["hit_dice"])) {
update["hit_dice"] = String(Math.min(parseInt(values["hit_dice"]) + maxHitDice, parseInt(values["hit_dice_max"])));
} else {
update["hit_dice"] = String(maxHitDice);
}
}
setAttrs(update);
});
}
// Restores the Talent Points pool (tp). Used by Adept, Cybermancer, Melder on long rest.
static resetTalentPoints() {
getAttrs(["tp_max"], values => {
if(!isNaN(values["tp_max"])) setAttrs({tp: values["tp_max"]});
});
}
// Restores Cybermancer Talent Slots (slots_used = remaining slots) to max.
// Cybermancer is EG's Warlock equivalent: talent slots recover on both short and long rest.
static resetCybermancerSlots() {
get_class_level("cybermancer", cybermancerLvl => {
if(cybermancerLvl === 0 || cybermancerLvl === "0") return;
getAttrs(["slots_max"], values => {
if(!isNaN(values["slots_max"])) setAttrs({slots_used: values["slots_max"]});
});
});
}
// Restores power slots (lvl19) to their calculated totals on long rest.
static resetPowerSlots() {
const attributes = [
"lvl1_slots_total", "lvl2_slots_total", "lvl3_slots_total",
"lvl4_slots_total", "lvl5_slots_total", "lvl6_slots_total",
"lvl7_slots_total", "lvl8_slots_total", "lvl9_slots_total"
];
getAttrs(attributes, values => {
const update = {};
Object.keys(values).filter(key => !isNaN(values[key])).forEach(key => {
update[key.replace("_total", "_expended")] = values[key];
});
setAttrs(update);
});
}
// Clears Esper Mastery used-flags on long rest.
static resetEsperMastery() {
setAttrs({
mastery_6th_used: "",
mastery_7th_used: "",
mastery_8th_used: "",
mastery_9th_used: ""
});
}
static resetDeathSave() {
const update = {};
for(let i = 1; i <= 3; i++) {
update[`deathsave_succ${i}`] = "";
update[`deathsave_fail${i}`] = "";
}
setAttrs(update);
}
static decreaseExhaustionLevels() {
getAttrs(["exhaustion_toggle", "exhaustion_level"], values => {
if(!values["exhaustion_toggle"] || values["exhaustion_toggle"] !== "1") return;
const level = isNaN(values["exhaustion_level"])
? 0
: Math.max(parseInt(values["exhaustion_level"]) - 1, 0);
setAttrs({exhaustion_level: level});
});
}
static long() {
Rest.resetResources(["short", "long"]);
Rest.resetHP();
Rest.resetHitDice();
Rest.resetTalentPoints();
Rest.resetCybermancerSlots();
Rest.resetPowerSlots();
Rest.resetEsperMastery();
Rest.decreaseExhaustionLevels();
Rest.resetDeathSave();
}
static short() {
Rest.resetResources(["short"]);
Rest.resetCybermancerSlots();
Rest.resetDeathSave();
}
}
on("clicked:short_rest", () => {
console.log("Taking a short rest...");
Rest.short();
});
on("clicked:long_rest", () => {
console.log("Taking a long rest...");
Rest.long();
});
const sanitizeShipNum = (val) => {
const raw = (val || "").trim().replace(/^\+/, "");
const n = parseInt(raw, 10);
return isNaN(n) ? 0 : n;
};
on("change:pc_ship_piloting_maneuver_check", () => {
getAttrs(["pc_ship_piloting_maneuver_check"], function(v) {
setAttrs({ pc_ship_piloting_num: sanitizeShipNum(v["pc_ship_piloting_maneuver_check"]) }, { silent: true });
});
});
const update_pc_ship_ac = () => {
getAttrs(["pc_ship_base_armor_class", "pc_ship_maneuver_bonus", "pc_ship_pilot_dex_modifier", "pc_ship_misc_mod_bonus"], function(v) {
const ac = sanitizeShipNum(v["pc_ship_base_armor_class"])
+ sanitizeShipNum(v["pc_ship_maneuver_bonus"])
+ sanitizeShipNum(v["pc_ship_pilot_dex_modifier"])
+ sanitizeShipNum(v["pc_ship_misc_mod_bonus"]);
setAttrs({ pc_ship_armor_class: ac }, { silent: true });
});
};
const update_pc_ship_md = () => {
getAttrs(["pc_ship_proficiency_bonus", "pc_ship_pilot_wis_modifier", "pc_ship_captain_cha_modifier"], function(v) {
const md = 8
+ sanitizeShipNum(v["pc_ship_proficiency_bonus"])
+ sanitizeShipNum(v["pc_ship_pilot_wis_modifier"])
+ sanitizeShipNum(v["pc_ship_captain_cha_modifier"]);
setAttrs({ pc_ship_base_maneuver_defense: 8, pc_ship_maneuver_defense_bonus: md }, { silent: true });
});
};
on("change:pc_ship_base_armor_class change:pc_ship_maneuver_bonus change:pc_ship_pilot_dex_modifier change:pc_ship_misc_mod_bonus", update_pc_ship_ac);
on("change:pc_ship_proficiency_bonus change:pc_ship_pilot_wis_modifier change:pc_ship_captain_cha_modifier", update_pc_ship_md);
on("sheet:opened", () => {
update_pc_ship_ac();
update_pc_ship_md();
});
// ── Damage / Heal buttons ──────────────────────────────────────────────────
// Minus (): subtract hp_change from current HP, floor at 0
on("clicked:damage", function() {
getAttrs(["hp", "hp_change"], function(v) {
const current = parseInt(v.hp) || 0;
const change = parseInt(v.hp_change) || 0;
const newHp = Math.max(0, current - change);
setAttrs({ hp: newHp });
});
});
// Plus (+): add hp_change to current HP, ceiling at hp_max
on("clicked:heal", function() {
getAttrs(["hp", "hp_max", "hp_change"], function(v) {
const current = parseInt(v.hp) || 0;
const max = parseInt(v.hp_max) || 0;
const change = parseInt(v.hp_change) || 0;
const newHp = Math.min(max, current + change);
setAttrs({ hp: newHp });
});
});
//# sourceURL=espergenesis.js