"use strict";
const MONSTER_STATS_BY_CR_JSON_URL = "data/msbcr.json";
const MONSTER_FEATURES_JSON_URL = "data/monsterfeatures.json";
let msbcr;
let monsterFeatures;
window.addEventListener("load", async () => {
await Promise.all([
PrereleaseUtil.pInit(),
BrewUtil2.pInit(),
]);
ExcludeUtil.pInitialise().then(null); // don't await, as this is only used for search
msbcr = await DataUtil.loadJSON(MONSTER_STATS_BY_CR_JSON_URL);
const mfData = await DataUtil.loadJSON(MONSTER_FEATURES_JSON_URL);
addMonsterFeatures(mfData);
window.dispatchEvent(new Event("toolsLoaded"));
});
function addMonsterFeatures (mfData) {
monsterFeatures = mfData.monsterfeatures;
for (let i = 0; i < msbcr.cr.length; i++) {
const curCr = msbcr.cr[i];
$("#msbcr").append(`
| ${curCr._cr} | ${Parser.crToXp(curCr._cr)} | ${curCr.pb} | ${curCr.ac} | ${curCr.hpMin}-${curCr.hpMax} | ${curCr.attackBonus} | ${curCr.dprMin}-${curCr.dprMax} | ${curCr.saveDc} |
`);
}
$("#crcalc input").change(calculateCr);
$("#saveprofs, #resistances").change(calculateCr);
$("#saveinstead").change(function () {
const curVal = parseInt($("#attackbonus").val());
if (!$(this).is(":checked")) $("#attackbonus").val(curVal - 10);
if ($(this).is(":checked")) $("#attackbonus").val(curVal + 10);
calculateCr();
});
function changeSize ($selSize) {
const newSize = $selSize.val();
if (newSize === "Tiny") $("#hdval").html("d4");
if (newSize === "Small") $("#hdval").html("d6");
if (newSize === "Medium") $("#hdval").html("d8");
if (newSize === "Large") $("#hdval").html("d10");
if (newSize === "Huge") $("#hdval").html("d12");
if (newSize === "Gargantuan") $("#hdval").html("d20");
$("#hp").val(calculateHp());
}
$("select#size").change(function () {
changeSize($(this));
calculateCr();
});
$("#hd, #con").change(function () {
$("#hp").val(calculateHp());
calculateCr();
});
// when clicking a row in the "Monster Statistics by Challenge Rating" table
$("#msbcr tr").not(":has(th)").click(async function () {
if (!await InputUiUtil.pGetUserBoolean({title: "Reset", htmlDescription: "This will reset the calculator. Are you sure?", textYes: "Yes", textNo: "Cancel"})) return;
$("#expectedcr").val($(this).children("td:eq(0)").html());
const [minHp, maxHp] = $(this).children("td:eq(4)").html().split("-").map(it => parseInt(it));
$("#hp").val(minHp + (maxHp - minHp) / 2);
$("#hd").val(calculateHd());
$("#ac").val($(this).children("td:eq(3)").html());
$("#dpr").val($(this).children("td:eq(6)").html().split("-")[0]);
$("#attackbonus").val($(this).children("td:eq(5)").html());
if ($("#saveinstead").is(":checked")) $("#attackbonus").val($(this).children("td:eq(7)").html());
calculateCr();
});
$("#hp").change(function () {
$("#hd").val(calculateHd());
calculateCr();
});
// parse monsterfeatures
const $wrpMonFeatures = $(`#monsterfeatures .crc__wrp_mon_features`);
monsterFeatures.forEach(f => {
const effectOnCr = [];
if (f.hp) effectOnCr.push(`HP: ${f.hp}`);
if (f.ac) effectOnCr.push(`AC: ${f.ac}`);
if (f.dpr) effectOnCr.push(`DPR: ${f.dpr}`);
if (f.attackBonus) effectOnCr.push(`AB: ${f.attackBonus}`);
const numBox = f.hasNumberParam ? `` : "";
$wrpMonFeatures.append(`
`);
});
function parseUrl () {
if (window.location.hash) {
let curData = window.location.hash.split("#")[1].split(",");
$("#expectedcr").val(curData[0]);
$("#ac").val(curData[1]);
$("#dpr").val(curData[2]);
$("#attackbonus").val(curData[3]);
if (curData[4] === "true") $("#saveinstead").attr("checked", true);
changeSize($("#size").val(curData[5]));
$("#hd").val(curData[6]);
$("#con").val(curData[7]);
$("#hp").val(calculateHp());
if (curData[8] === "true") $("#vulnerabilities").attr("checked", true);
$("#resistances").val(curData[9]);
if (curData[10] === "true") $("#flying").attr("checked", true);
$("#saveprofs").val(curData[11]);
$(`.crc__mon_feature_cb`).each((i, e) => {
const $cb = $(e);
const idCb = $cb.attr("id");
const val = Hist.getSubHash(idCb);
if (val) {
$cb.prop("checked", true);
if (val !== "true") {
$cb.siblings("input[type=number]").val(val);
}
}
});
}
calculateCr();
}
function handleMonsterFeaturesChange ($cbFeature, $iptNum) {
const curFeature = $cbFeature.attr("id");
if ($cbFeature.prop("checked")) {
Hist.setSubhash(curFeature, $iptNum.length ? $iptNum.val() : true);
} else {
Hist.setSubhash(curFeature, null);
}
}
// Monster Features table
$(".crc__mon_feature_cb").change(function () {
const $cbFeature = $(this);
const $iptNum = $(this).siblings("input[type=number]");
handleMonsterFeaturesChange($cbFeature, $iptNum);
});
$(`.crc__mon_feature_num`).change(function () {
const $iptNum = $(this);
const $cbFeature = $(this).siblings("input[type=checkbox]");
handleMonsterFeaturesChange($cbFeature, $iptNum);
});
$("#monsterfeatures .crc__wrp_mon_features input").change(calculateCr);
$("#crcalc_reset").click(async () => {
if (!await InputUiUtil.pGetUserBoolean({title: "Reset", htmlDescription: "Are you sure?", textYes: "Yes", textNo: "Cancel"})) return;
window.location = "";
parseUrl();
});
parseUrl();
}
function calculateCr () {
const expectedCr = parseInt($("#expectedcr").val());
// Effective HP
let hp = parseInt($("#crcalc #hp").val());
// Used in e.g. "Damage Transfer"
const hpActual = hp;
if ($("#vulnerabilities").prop("checked")) hp *= 0.5;
if ($("#resistances").val() === "res") {
if (expectedCr >= 0 && expectedCr <= 4) hp *= 2;
if (expectedCr >= 5 && expectedCr <= 10) hp *= 1.5;
if (expectedCr >= 11 && expectedCr <= 16) hp *= 1.25;
}
if ($("#resistances").val() === "imm") {
if (expectedCr >= 0 && expectedCr <= 4) hp *= 2;
if (expectedCr >= 5 && expectedCr <= 10) hp *= 2;
if (expectedCr >= 11 && expectedCr <= 16) hp *= 1.5;
if (expectedCr >= 17) hp *= 1.25;
}
let ac = parseInt($("#crcalc #ac").val()) + parseInt($("#saveprofs").val()) + parseInt($("#flying").prop("checked") * 2);
let dpr = parseInt($("#crcalc #dpr").val());
let attackBonus = parseInt($("#crcalc #attackbonus").val());
const useSaveDc = $("#saveinstead").prop("checked");
let offensiveCR = -1;
let defensiveCR = -1;
// go through monster features
$("#monsterfeatures input:checked").each(function () {
// `trait` is used within the "eval"s below
let trait = 0;
if ($(this).siblings("input[type=number]").length) trait = $(this).siblings("input[type=number]").val();
/* eslint-disable */
if ($(this).attr("data-hp") !== "") hp += Number(eval($(this).attr("data-hp")));
if ($(this).attr("data-ac") !== "") ac += Number(eval($(this).attr("data-ac")));
if ($(this).attr("data-dpr") !== "") dpr += Number(eval($(this).attr("data-dpr")));
/* eslint-enable */
if (!useSaveDc && $(this).attr("data-attackbonus") !== "") attackBonus += Number($(this).attr("data-attackbonus"));
});
hp = Math.floor(hp);
dpr = Math.floor(dpr);
const effectiveHp = hp;
const effectiveDpr = dpr;
// make sure we don't break the CR
if (hp > 850) hp = 850;
if (dpr > 320) dpr = 320;
for (let i = 0; i < msbcr.cr.length; i++) {
const curCr = msbcr.cr[i];
if (hp >= parseInt(curCr.hpMin) && hp <= parseInt(curCr.hpMax)) {
let defenseDifference = parseInt(curCr.ac) - ac;
if (defenseDifference > 0) defenseDifference = Math.floor(defenseDifference / 2);
if (defenseDifference < 0) defenseDifference = Math.ceil(defenseDifference / 2);
defenseDifference = i - defenseDifference;
if (defenseDifference < 0) defenseDifference = 0;
if (defenseDifference >= msbcr.cr.length) defenseDifference = msbcr.cr.length - 1;
defensiveCR = msbcr.cr[defenseDifference]._cr;
}
if (dpr >= curCr.dprMin && dpr <= curCr.dprMax) {
let adjuster = parseInt(curCr.attackBonus);
if (useSaveDc) adjuster = parseInt(curCr.saveDc);
let attackDifference = adjuster - attackBonus;
if (attackDifference > 0) attackDifference = Math.floor(attackDifference / 2);
if (attackDifference < 0) attackDifference = Math.ceil(attackDifference / 2);
attackDifference = i - attackDifference;
if (attackDifference < 0) attackDifference = 0;
if (attackDifference >= msbcr.cr.length) attackDifference = msbcr.cr.length - 1;
offensiveCR = msbcr.cr[attackDifference]._cr;
}
}
if (offensiveCR === -1) offensiveCR = "0";
if (defensiveCR === -1) defensiveCR = "0";
let cr = ((fractionStrToDecimal(offensiveCR) + fractionStrToDecimal(defensiveCR)) / 2).toString();
if (cr === "0.5625") cr = "1/2";
if (cr === "0.5") cr = "1/2";
if (cr === "0.375") cr = "1/4";
if (cr === "0.3125") cr = "1/4";
if (cr === "0.25") cr = "1/4";
if (cr === "0.1875") cr = "1/8";
if (cr === "0.125") cr = "1/8";
if (cr === "0.0625") cr = "1/8";
if (cr.indexOf(".") !== -1) cr = Math.round(cr).toString();
let finalCr = 0;
for (let i = 0; i < msbcr.cr.length; i++) {
if (msbcr.cr[i]._cr === cr) {
finalCr = i;
break;
}
}
const hitDice = calculateHd();
const hitDiceSize = $("#hdval").html();
const conMod = Parser.getAbilityModNumber($("#con").val());
const hashParts = [
$("#expectedcr").val(), // 0
$("#ac").val(), // 1
$("#dpr").val(), // 2
$("#attackbonus").val(), // 3
useSaveDc, // 4
$("#size").val(), // 5
$("#hd").val(), // 6
$("#con").val(), // 7
$("#vulnerabilities").prop("checked"), // 8
$("#resistances").val(), // 9
$("#flying").prop("checked"), // 10
$("#saveprofs").val(), // 11
$(`.crc__mon_feature_cb`).map((i, e) => {
const $cb = $(e);
if ($cb.prop("checked")) {
const $iptNum = $cb.siblings("input[type=number]");
return `${$cb.attr("id")}:${$iptNum.length ? $iptNum.val() : true}`;
} else return false;
}).get().filter(Boolean).join(","),
];
window.location = `#${hashParts.join(",")}`;
$("#croutput").html(`
Challenge Rating: ${cr}
Offensive CR: ${offensiveCR}
Defensive CR: ${defensiveCR}
Proficiency Bonus: +${msbcr.cr[finalCr].pb}
Effective HP: ${effectiveHp} (${hitDice}${hitDiceSize}${conMod < 0 ? "" : "+"}${conMod * hitDice})
Effective AC: ${ac}
Average Damage Per Round: ${effectiveDpr}
${useSaveDc ? "Save DC: " : "Effective Attack Bonus: +"}${attackBonus}
Experience Points: ${Parser.crToXp(msbcr.cr[finalCr]._cr)}
`);
}
function calculateHd () {
const avgHp = $("#hdval").html().split("d")[1] / 2 + 0.5;
const conMod = Parser.getAbilityModNumber($("#con").val());
let curHd = Math.round(parseInt($("#hp").val()) / (avgHp + conMod));
if (!curHd) curHd = 1;
return curHd;
}
function calculateHp () {
const avgHp = $("#hdval").html().split("d")[1] / 2 + 0.5;
const conMod = Parser.getAbilityModNumber($("#con").val());
return Math.floor((avgHp + conMod) * $("#hd").val());
}
function fractionStrToDecimal (str) {
return str === "0" ? 0 : parseFloat(str.split("/").reduce((numerator, denominator) => numerator / denominator));
}