"use strict"; // PARSING ============================================================================================================= globalThis.Parser = {}; Parser._parse_aToB = function (abMap, a, fallback) { if (a === undefined || a === null) throw new TypeError("undefined or null object passed to parser"); if (typeof a === "string") a = a.trim(); if (abMap[a] !== undefined) return abMap[a]; return fallback !== undefined ? fallback : a; }; Parser._parse_bToA = function (abMap, b, fallback) { if (b === undefined || b === null) throw new TypeError("undefined or null object passed to parser"); if (typeof b === "string") b = b.trim(); for (const v in abMap) { if (!abMap.hasOwnProperty(v)) continue; if (abMap[v] === b) return v; } return fallback !== undefined ? fallback : b; }; Parser.attrChooseToFull = function (attList) { if (attList.length === 1) return `${Parser.attAbvToFull(attList[0])} modifier`; else { const attsTemp = []; for (let i = 0; i < attList.length; ++i) { attsTemp.push(Parser.attAbvToFull(attList[i])); } return `${attsTemp.join(" or ")} modifier (your choice)`; } }; Parser.numberToText = function (number) { if (number == null) throw new TypeError(`undefined or null object passed to parser`); if (Math.abs(number) >= 100) return `${number}`; return `${number < 0 ? "negative " : ""}${Parser.numberToText._getPositiveNumberAsText(Math.abs(number))}`; }; Parser.numberToText._getPositiveNumberAsText = num => { const [preDotRaw, postDotRaw] = `${num}`.split("."); if (!postDotRaw) return Parser.numberToText._getPositiveIntegerAsText(num); let preDot = preDotRaw === "0" ? "" : `${Parser.numberToText._getPositiveIntegerAsText(Math.trunc(num))} and `; // See also: `Parser.numberToVulgar` switch (postDotRaw) { case "125": return `${preDot}one-eighth`; case "2": return `${preDot}one-fifth`; case "25": return `${preDot}one-quarter`; case "375": return `${preDot}three-eighths`; case "4": return `${preDot}two-fifths`; case "5": return `${preDot}one-half`; case "6": return `${preDot}three-fifths`; case "625": return `${preDot}five-eighths`; case "75": return `${preDot}three-quarters`; case "8": return `${preDot}four-fifths`; case "875": return `${preDot}seven-eighths`; default: { // Handle recursive const asNum = Number(`0.${postDotRaw}`); if (asNum.toFixed(2) === (1 / 3).toFixed(2)) return `${preDot}one-third`; if (asNum.toFixed(2) === (2 / 3).toFixed(2)) return `${preDot}two-thirds`; if (asNum.toFixed(2) === (1 / 6).toFixed(2)) return `${preDot}one-sixth`; if (asNum.toFixed(2) === (5 / 6).toFixed(2)) return `${preDot}five-sixths`; } } }; Parser.numberToText._getPositiveIntegerAsText = num => { switch (num) { case 0: return "zero"; case 1: return "one"; case 2: return "two"; case 3: return "three"; case 4: return "four"; case 5: return "five"; case 6: return "six"; case 7: return "seven"; case 8: return "eight"; case 9: return "nine"; case 10: return "ten"; case 11: return "eleven"; case 12: return "twelve"; case 13: return "thirteen"; case 14: return "fourteen"; case 15: return "fifteen"; case 16: return "sixteen"; case 17: return "seventeen"; case 18: return "eighteen"; case 19: return "nineteen"; case 20: return "twenty"; case 30: return "thirty"; case 40: return "forty"; case 50: return "fifty"; case 60: return "sixty"; case 70: return "seventy"; case 80: return "eighty"; case 90: return "ninety"; default: { const str = String(num); return `${Parser.numberToText._getPositiveIntegerAsText(Number(`${str[0]}0`))}-${Parser.numberToText._getPositiveIntegerAsText(Number(str[1]))}`; } } }; Parser.textToNumber = function (str) { str = str.trim().toLowerCase(); if (!isNaN(str)) return Number(str); switch (str) { case "zero": return 0; case "one": case "a": case "an": return 1; case "two": case "double": return 2; case "three": case "triple": return 3; case "four": case "quadruple": return 4; case "five": return 5; case "six": return 6; case "seven": return 7; case "eight": return 8; case "nine": return 9; case "ten": return 10; case "eleven": return 11; case "twelve": return 12; case "thirteen": return 13; case "fourteen": return 14; case "fifteen": return 15; case "sixteen": return 16; case "seventeen": return 17; case "eighteen": return 18; case "nineteen": return 19; case "twenty": return 20; case "thirty": return 30; case "forty": return 40; case "fifty": return 50; case "sixty": return 60; case "seventy": return 70; case "eighty": return 80; case "ninety": return 90; } return NaN; }; Parser.numberToVulgar = function (number, {isFallbackOnFractional = true} = {}) { const isNeg = number < 0; const spl = `${number}`.replace(/^-/, "").split("."); if (spl.length === 1) return number; let preDot = spl[0] === "0" ? "" : spl[0]; if (isNeg) preDot = `-${preDot}`; // See also: `Parser.numberToText._getPositiveNumberAsText` switch (spl[1]) { case "125": return `${preDot}⅛`; case "2": return `${preDot}⅕`; case "25": return `${preDot}¼`; case "375": return `${preDot}⅜`; case "4": return `${preDot}⅖`; case "5": return `${preDot}½`; case "6": return `${preDot}⅗`; case "625": return `${preDot}⅝`; case "75": return `${preDot}¾`; case "8": return `${preDot}⅘`; case "875": return `${preDot}⅞`; default: { // Handle recursive const asNum = Number(`0.${spl[1]}`); if (asNum.toFixed(2) === (1 / 3).toFixed(2)) return `${preDot}⅓`; if (asNum.toFixed(2) === (2 / 3).toFixed(2)) return `${preDot}⅔`; if (asNum.toFixed(2) === (1 / 6).toFixed(2)) return `${preDot}⅙`; if (asNum.toFixed(2) === (5 / 6).toFixed(2)) return `${preDot}⅚`; } } return isFallbackOnFractional ? Parser.numberToFractional(number) : null; }; Parser.vulgarToNumber = function (str) { const [, leading = "0", vulgar = ""] = /^(\d+)?([⅛¼⅜½⅝¾⅞⅓⅔⅙⅚])?$/.exec(str) || []; let out = Number(leading); switch (vulgar) { case "⅛": out += 0.125; break; case "¼": out += 0.25; break; case "⅜": out += 0.375; break; case "½": out += 0.5; break; case "⅝": out += 0.625; break; case "¾": out += 0.75; break; case "⅞": out += 0.875; break; case "⅓": out += 1 / 3; break; case "⅔": out += 2 / 3; break; case "⅙": out += 1 / 6; break; case "⅚": out += 5 / 6; break; case "": break; default: throw new Error(`Unhandled vulgar part "${vulgar}"`); } return out; }; Parser.numberToSuperscript = function (number) { return `${number}`.split("").map(c => isNaN(c) ? c : Parser._NUMBERS_SUPERSCRIPT[Number(c)]).join(""); }; Parser._NUMBERS_SUPERSCRIPT = "⁰¹²³⁴⁵⁶⁷⁸⁹"; Parser.numberToSubscript = function (number) { return `${number}`.split("").map(c => isNaN(c) ? c : Parser._NUMBERS_SUBSCRIPT[Number(c)]).join(""); }; Parser._NUMBERS_SUBSCRIPT = "₀₁₂₃₄₅₆₇₈₉"; Parser._greatestCommonDivisor = function (a, b) { if (b < Number.EPSILON) return a; return Parser._greatestCommonDivisor(b, Math.floor(a % b)); }; Parser.numberToFractional = function (number) { const len = number.toString().length - 2; let denominator = 10 ** len; let numerator = number * denominator; const divisor = Parser._greatestCommonDivisor(numerator, denominator); numerator = Math.floor(numerator / divisor); denominator = Math.floor(denominator / divisor); return denominator === 1 ? String(numerator) : `${Math.floor(numerator)}/${Math.floor(denominator)}`; }; Parser.ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Parser.attAbvToFull = function (abv) { return Parser._parse_aToB(Parser.ATB_ABV_TO_FULL, abv); }; Parser.attFullToAbv = function (full) { return Parser._parse_bToA(Parser.ATB_ABV_TO_FULL, full); }; Parser.sizeAbvToFull = function (abv) { return Parser._parse_aToB(Parser.SIZE_ABV_TO_FULL, abv); }; Parser.getAbilityModNumber = function (abilityScore) { return Math.floor((abilityScore - 10) / 2); }; Parser.getAbilityModifier = function (abilityScore) { let modifier = Parser.getAbilityModNumber(abilityScore); if (modifier >= 0) modifier = `+${modifier}`; return `${modifier}`; }; Parser.getSpeedString = (ent, {isMetric = false, isSkipZeroWalk = false} = {}) => { if (ent.speed == null) return "\u2014"; const unit = isMetric ? Parser.metric.getMetricUnit({originalUnit: "ft.", isShortForm: true}) : "ft."; if (typeof ent.speed === "object") { const stack = []; let joiner = ", "; Parser.SPEED_MODES .filter(mode => !ent.speed.hidden?.includes(mode)) .forEach(mode => Parser._getSpeedString_addSpeedMode({ent, prop: mode, stack, isMetric, isSkipZeroWalk, unit})); if (ent.speed.choose && !ent.speed.hidden?.includes("choose")) { joiner = "; "; stack.push(`${ent.speed.choose.from.sort().joinConjunct(", ", " or ")} ${ent.speed.choose.amount} ${unit}${ent.speed.choose.note ? ` ${ent.speed.choose.note}` : ""}`); } return stack.join(joiner) + (ent.speed.note ? ` ${ent.speed.note}` : ""); } return (isMetric ? Parser.metric.getMetricNumber({originalValue: ent.speed, originalUnit: Parser.UNT_FEET}) : ent.speed) + (ent.speed === "Varies" ? "" : ` ${unit} `); }; Parser._getSpeedString_addSpeedMode = ({ent, prop, stack, isMetric, isSkipZeroWalk, unit}) => { if (ent.speed[prop] || (!isSkipZeroWalk && prop === "walk")) Parser._getSpeedString_addSpeed({prop, speed: ent.speed[prop] || 0, isMetric, unit, stack}); if (ent.speed.alternate && ent.speed.alternate[prop]) ent.speed.alternate[prop].forEach(speed => Parser._getSpeedString_addSpeed({prop, speed, isMetric, unit, stack})); }; Parser._getSpeedString_addSpeed = ({prop, speed, isMetric, unit, stack}) => { const ptName = prop === "walk" ? "" : `${prop} `; const ptValue = Parser._getSpeedString_getVal({prop, speed, isMetric}); const ptUnit = speed === true ? "" : ` ${unit}`; const ptCondition = Parser._getSpeedString_getCondition({speed}); stack.push([ptName, ptValue, ptUnit, ptCondition].join("")); }; Parser._getSpeedString_getVal = ({prop, speed, isMetric}) => { if (speed === true && prop !== "walk") return "equal to your walking speed"; const num = speed === true ? 0 : speed.number != null ? speed.number : speed; return isMetric ? Parser.metric.getMetricNumber({originalValue: num, originalUnit: Parser.UNT_FEET}) : num; }; Parser._getSpeedString_getCondition = ({speed}) => speed.condition ? ` ${Renderer.get().render(speed.condition)}` : ""; Parser.SPEED_MODES = ["walk", "burrow", "climb", "fly", "swim"]; Parser.SPEED_TO_PROGRESSIVE = { "walk": "walking", "burrow": "burrowing", "climb": "climbing", "fly": "flying", "swim": "swimming", }; Parser.speedToProgressive = function (prop) { return Parser._parse_aToB(Parser.SPEED_TO_PROGRESSIVE, prop); }; Parser._addCommas = function (intNum) { return `${intNum}`.replace(/(\d)(?=(\d{3})+$)/g, "$1,"); }; Parser.raceCreatureTypesToFull = function (creatureTypes) { const hasSubOptions = creatureTypes.some(it => it.choose); return creatureTypes .map(it => { if (!it.choose) return Parser.monTypeToFullObj(it).asText; return [...it.choose] .sort(SortUtil.ascSortLower) .map(sub => Parser.monTypeToFullObj(sub).asText) .joinConjunct(", ", " or "); }) .joinConjunct(hasSubOptions ? "; " : ", ", " and "); }; Parser.crToXp = function (cr, {isDouble = false} = {}) { if (cr != null && cr.xp) return Parser._addCommas(`${isDouble ? cr.xp * 2 : cr.xp}`); const toConvert = cr ? (cr.cr || cr) : null; if (toConvert === "Unknown" || toConvert == null || !Parser.XP_CHART_ALT[toConvert]) return "Unknown"; // CR 0 creatures can be 0 or 10 XP, but 10 XP is used in almost every case. // Exceptions, such as MM's Frog and Sea Horse, have their XP set to 0 on the creature if (toConvert === "0") return "10"; const xp = Parser.XP_CHART_ALT[toConvert]; return Parser._addCommas(`${isDouble ? 2 * xp : xp}`); }; Parser.crToXpNumber = function (cr) { if (cr != null && cr.xp) return cr.xp; const toConvert = cr ? (cr.cr || cr) : cr; if (toConvert === "Unknown" || toConvert == null) return null; return Parser.XP_CHART_ALT[toConvert] ?? null; }; Parser.LEVEL_TO_XP_EASY = [0, 25, 50, 75, 125, 250, 300, 350, 450, 550, 600, 800, 1000, 1100, 1250, 1400, 1600, 2000, 2100, 2400, 2800]; Parser.LEVEL_TO_XP_MEDIUM = [0, 50, 100, 150, 250, 500, 600, 750, 900, 1100, 1200, 1600, 2000, 2200, 2500, 2800, 3200, 3900, 4100, 4900, 5700]; Parser.LEVEL_TO_XP_HARD = [0, 75, 150, 225, 375, 750, 900, 1100, 1400, 1600, 1900, 2400, 3000, 3400, 3800, 4300, 4800, 5900, 6300, 7300, 8500]; Parser.LEVEL_TO_XP_DEADLY = [0, 100, 200, 400, 500, 1100, 1400, 1700, 2100, 2400, 2800, 3600, 4500, 5100, 5700, 6400, 7200, 8800, 9500, 10900, 12700]; Parser.LEVEL_TO_XP_DAILY = [0, 300, 600, 1200, 1700, 3500, 4000, 5000, 6000, 7500, 9000, 10500, 11500, 13500, 15000, 18000, 20000, 25000, 27000, 30000, 40000]; Parser.LEVEL_XP_REQUIRED = [0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]; Parser.CRS = ["0", "1/8", "1/4", "1/2", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]; Parser.levelToXpThreshold = function (level) { return [Parser.LEVEL_TO_XP_EASY[level], Parser.LEVEL_TO_XP_MEDIUM[level], Parser.LEVEL_TO_XP_HARD[level], Parser.LEVEL_TO_XP_DEADLY[level]]; }; Parser.isValidCr = function (cr) { return Parser.CRS.includes(cr); }; Parser.crToNumber = function (cr, opts = {}) { const {isDefaultNull = false} = opts; if (cr === "Unknown" || cr === "\u2014" || cr == null) return isDefaultNull ? null : VeCt.CR_UNKNOWN; if (cr.cr) return Parser.crToNumber(cr.cr, opts); const parts = cr.trim().split("/"); if (!parts.length || parts.length >= 3) return isDefaultNull ? null : VeCt.CR_CUSTOM; if (isNaN(parts[0])) return isDefaultNull ? null : VeCt.CR_CUSTOM; if (parts.length === 2) { if (isNaN(Number(parts[1]))) return isDefaultNull ? null : VeCt.CR_CUSTOM; return Number(parts[0]) / Number(parts[1]); } return Number(parts[0]); }; Parser.numberToCr = function (number, safe) { // avoid dying if already-converted number is passed in if (safe && typeof number === "string" && Parser.CRS.includes(number)) return number; if (number == null) return "Unknown"; return Parser.numberToFractional(number); }; Parser.crToPb = function (cr) { if (cr === "Unknown" || cr == null) return 0; cr = cr.cr || cr; if (Parser.crToNumber(cr) < 5) return 2; return Math.ceil(cr / 4) + 1; }; Parser.levelToPb = function (level) { if (!level) return 2; return Math.ceil(level / 4) + 1; }; Parser.SKILL_TO_ATB_ABV = { "athletics": "str", "acrobatics": "dex", "sleight of hand": "dex", "stealth": "dex", "arcana": "int", "history": "int", "investigation": "int", "nature": "int", "religion": "int", "animal handling": "wis", "insight": "wis", "medicine": "wis", "perception": "wis", "survival": "wis", "deception": "cha", "intimidation": "cha", "performance": "cha", "persuasion": "cha", }; Parser.skillToAbilityAbv = function (skill) { return Parser._parse_aToB(Parser.SKILL_TO_ATB_ABV, skill); }; Parser.SKILL_TO_SHORT = { "athletics": "ath", "acrobatics": "acro", "sleight of hand": "soh", "stealth": "slth", "arcana": "arc", "history": "hist", "investigation": "invn", "nature": "natr", "religion": "reli", "animal handling": "hndl", "insight": "ins", "medicine": "med", "perception": "perp", "survival": "surv", "deception": "decp", "intimidation": "intm", "performance": "perf", "persuasion": "pers", }; Parser.skillToShort = function (skill) { return Parser._parse_aToB(Parser.SKILL_TO_SHORT, skill); }; Parser.LANGUAGES_STANDARD = [ "Common", "Dwarvish", "Elvish", "Giant", "Gnomish", "Goblin", "Halfling", "Orc", ]; Parser.LANGUAGES_EXOTIC = [ "Abyssal", "Aquan", "Auran", "Celestial", "Draconic", "Deep Speech", "Ignan", "Infernal", "Primordial", "Sylvan", "Terran", "Undercommon", ]; Parser.LANGUAGES_SECRET = [ "Druidic", "Thieves' cant", ]; Parser.LANGUAGES_ALL = [ ...Parser.LANGUAGES_STANDARD, ...Parser.LANGUAGES_EXOTIC, ...Parser.LANGUAGES_SECRET, ].sort(); Parser.acToFull = function (ac, renderer) { if (typeof ac === "string") return ac; // handle classic format renderer = renderer || Renderer.get(); let stack = ""; let inBraces = false; for (let i = 0; i < ac.length; ++i) { const cur = ac[i]; const nxt = ac[i + 1]; if (cur.special != null) { if (inBraces) inBraces = false; stack += cur.special; } else if (cur.ac) { const isNxtBraces = nxt && nxt.braces; if (!inBraces && cur.braces) { stack += "("; inBraces = true; } stack += cur.ac; if (cur.from) { // always brace nested braces if (cur.braces) { stack += " ("; } else { stack += inBraces ? "; " : " ("; } inBraces = true; stack += cur.from.map(it => renderer.render(it)).join(", "); if (cur.braces) { stack += ")"; } else if (!isNxtBraces) { stack += ")"; inBraces = false; } } if (cur.condition) stack += ` ${renderer.render(cur.condition)}`; if (inBraces && !isNxtBraces) { stack += ")"; inBraces = false; } } else { stack += cur; } if (nxt) { if (nxt.braces) { stack += inBraces ? "; " : " ("; inBraces = true; } else stack += ", "; } } if (inBraces) stack += ")"; return stack.trim(); }; Parser.MONSTER_COUNT_TO_XP_MULTIPLIER = [1, 1.5, 2, 2, 2, 2, 2.5, 2.5, 2.5, 2.5, 3, 3, 3, 3, 4]; Parser.numMonstersToXpMult = function (num, playerCount = 3) { const baseVal = (() => { if (num >= Parser.MONSTER_COUNT_TO_XP_MULTIPLIER.length) return 4; return Parser.MONSTER_COUNT_TO_XP_MULTIPLIER[num - 1]; })(); if (playerCount < 3) return baseVal >= 3 ? baseVal + 1 : baseVal + 0.5; else if (playerCount > 5) { return baseVal === 4 ? 3 : baseVal - 0.5; } else return baseVal; }; Parser.armorFullToAbv = function (armor) { return Parser._parse_bToA(Parser.ARMOR_ABV_TO_FULL, armor); }; Parser.weaponFullToAbv = function (weapon) { return Parser._parse_bToA(Parser.WEAPON_ABV_TO_FULL, weapon); }; Parser._getSourceStringFromSource = function (source) { if (source && source.source) return source.source; return source; }; Parser._buildSourceCache = function (dict) { const out = {}; Object.entries(dict).forEach(([k, v]) => out[k.toLowerCase()] = v); return out; }; Parser._sourceJsonCache = null; Parser.hasSourceJson = function (source) { Parser._sourceJsonCache = Parser._sourceJsonCache || Parser._buildSourceCache(Object.keys(Parser.SOURCE_JSON_TO_FULL).mergeMap(k => ({[k]: k}))); return !!Parser._sourceJsonCache[source.toLowerCase()]; }; Parser._sourceFullCache = null; Parser.hasSourceFull = function (source) { Parser._sourceFullCache = Parser._sourceFullCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_FULL); return !!Parser._sourceFullCache[source.toLowerCase()]; }; Parser._sourceAbvCache = null; Parser.hasSourceAbv = function (source) { Parser._sourceAbvCache = Parser._sourceAbvCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_ABV); return !!Parser._sourceAbvCache[source.toLowerCase()]; }; Parser._sourceDateCache = null; Parser.hasSourceDate = function (source) { Parser._sourceDateCache = Parser._sourceDateCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_DATE); return !!Parser._sourceDateCache[source.toLowerCase()]; }; Parser.sourceJsonToJson = function (source) { source = Parser._getSourceStringFromSource(source); if (Parser.hasSourceJson(source)) return Parser._sourceJsonCache[source.toLowerCase()]; if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToSource(source).json; if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToSource(source).json; return source; }; Parser.sourceJsonToFull = function (source) { source = Parser._getSourceStringFromSource(source); if (Parser.hasSourceFull(source)) return Parser._sourceFullCache[source.toLowerCase()].replace(/'/g, "\u2019"); if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToFull(source).replace(/'/g, "\u2019"); if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToFull(source).replace(/'/g, "\u2019"); return Parser._parse_aToB(Parser.SOURCE_JSON_TO_FULL, source).replace(/'/g, "\u2019"); }; Parser.sourceJsonToFullCompactPrefix = function (source) { return Parser.sourceJsonToFull(source) .replace(Parser.UA_PREFIX, Parser.UA_PREFIX_SHORT) .replace(/^Unearthed Arcana (\d+): /, "UA$1: ") .replace(Parser.AL_PREFIX, Parser.AL_PREFIX_SHORT) .replace(Parser.PS_PREFIX, Parser.PS_PREFIX_SHORT); }; Parser.sourceJsonToAbv = function (source) { source = Parser._getSourceStringFromSource(source); if (Parser.hasSourceAbv(source)) return Parser._sourceAbvCache[source.toLowerCase()]; if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToAbv(source); if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToAbv(source); return Parser._parse_aToB(Parser.SOURCE_JSON_TO_ABV, source); }; Parser.sourceJsonToDate = function (source) { source = Parser._getSourceStringFromSource(source); if (Parser.hasSourceDate(source)) return Parser._sourceDateCache[source.toLowerCase()]; if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToDate(source); if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToDate(source); return Parser._parse_aToB(Parser.SOURCE_JSON_TO_DATE, source, null); }; Parser.sourceJsonToColor = function (source) { return `source${Parser.sourceJsonToAbv(source)}`; }; Parser.sourceJsonToStyle = function (source) { source = Parser._getSourceStringFromSource(source); if (Parser.hasSourceJson(source)) return ""; if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToStyle(source); if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToStyle(source); return ""; }; Parser.sourceJsonToStylePart = function (source) { source = Parser._getSourceStringFromSource(source); if (Parser.hasSourceJson(source)) return ""; if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToStylePart(source); if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToStylePart(source); return ""; }; Parser.stringToSlug = function (str) { return str.trim().toLowerCase().toAscii().replace(/[^\w ]+/g, "").replace(/ +/g, "-"); }; Parser.stringToCasedSlug = function (str) { return str.toAscii().replace(/[^\w ]+/g, "").replace(/ +/g, "-"); }; Parser.ITEM_SPELLCASTING_FOCUS_CLASSES = ["Artificer", "Bard", "Cleric", "Druid", "Paladin", "Ranger", "Sorcerer", "Warlock", "Wizard"]; Parser.itemValueToFull = function (item, opts = {isShortForm: false, isSmallUnits: false}) { return Parser._moneyToFull(item, "value", "valueMult", opts); }; Parser.itemValueToFullMultiCurrency = function (item, opts = {isShortForm: false, isSmallUnits: false}) { return Parser._moneyToFullMultiCurrency(item, "value", "valueMult", opts); }; Parser.itemVehicleCostsToFull = function (item, isShortForm) { return { travelCostFull: Parser._moneyToFull(item, "travelCost", "travelCostMult", {isShortForm}), shippingCostFull: Parser._moneyToFull(item, "shippingCost", "shippingCostMult", {isShortForm}), }; }; Parser.spellComponentCostToFull = function (item, isShortForm) { return Parser._moneyToFull(item, "cost", "costMult", {isShortForm}); }; Parser.vehicleCostToFull = function (item, isShortForm) { return Parser._moneyToFull(item, "cost", "costMult", {isShortForm}); }; Parser._moneyToFull = function (it, prop, propMult, opts = {isShortForm: false, isSmallUnits: false}) { if (it[prop] == null && it[propMult] == null) return ""; if (it[prop] != null) { const {coin, mult} = Parser.getCurrencyAndMultiplier(it[prop], it.currencyConversion); return `${(it[prop] * mult).toLocaleString(undefined, {maximumFractionDigits: 5})}${opts.isSmallUnits ? `${coin}` : ` ${coin}`}`; } else if (it[propMult] != null) return opts.isShortForm ? `×${it[propMult]}` : `base value ×${it[propMult]}`; return ""; }; Parser._moneyToFullMultiCurrency = function (it, prop, propMult, {isShortForm, multiplier} = {}) { if (it[prop]) { const conversionTable = Parser.getCurrencyConversionTable(it.currencyConversion); const simplified = it.currencyConversion ? CurrencyUtil.doSimplifyCoins( { // Assume the e.g. item's value is in the lowest available denomination [conversionTable[0]?.coin || "cp"]: it[prop] * (multiplier ?? conversionTable[0]?.mult ?? 1), }, { currencyConversionId: it.currencyConversion, }, ) : CurrencyUtil.doSimplifyCoins({ cp: it[prop] * (multiplier ?? 1), }); return [...conversionTable] .reverse() .filter(meta => simplified[meta.coin]) .map(meta => `${simplified[meta.coin].toLocaleString(undefined, {maximumFractionDigits: 5})} ${meta.coin}`) .join(", "); } if (it[propMult]) return isShortForm ? `×${it[propMult]}` : `base value ×${it[propMult]}`; return ""; }; Parser.DEFAULT_CURRENCY_CONVERSION_TABLE = [ { coin: "cp", mult: 1, }, { coin: "sp", mult: 0.1, }, { coin: "gp", mult: 0.01, isFallback: true, }, ]; Parser.FULL_CURRENCY_CONVERSION_TABLE = [ { coin: "cp", mult: 1, }, { coin: "sp", mult: 0.1, }, { coin: "ep", mult: 0.02, }, { coin: "gp", mult: 0.01, isFallback: true, }, { coin: "pp", mult: 0.001, }, ]; Parser.getCurrencyConversionTable = function (currencyConversionId) { const fromPrerelease = currencyConversionId ? PrereleaseUtil.getMetaLookup("currencyConversions")?.[currencyConversionId] : null; const fromBrew = currencyConversionId ? BrewUtil2.getMetaLookup("currencyConversions")?.[currencyConversionId] : null; const conversionTable = fromPrerelease?.length ? fromPrerelease : fromBrew?.length ? fromBrew : Parser.DEFAULT_CURRENCY_CONVERSION_TABLE; if (conversionTable !== Parser.DEFAULT_CURRENCY_CONVERSION_TABLE) conversionTable.sort((a, b) => SortUtil.ascSort(b.mult, a.mult)); return conversionTable; }; Parser.getCurrencyAndMultiplier = function (value, currencyConversionId) { const conversionTable = Parser.getCurrencyConversionTable(currencyConversionId); if (!value) return conversionTable.find(it => it.isFallback) || conversionTable[0]; if (conversionTable.length === 1) return conversionTable[0]; if (!Number.isInteger(value) && value < conversionTable[0].mult) return conversionTable[0]; for (let i = conversionTable.length - 1; i >= 0; --i) { if (Number.isInteger(value * conversionTable[i].mult)) return conversionTable[i]; } return conversionTable.last(); }; Parser.COIN_ABVS = ["cp", "sp", "ep", "gp", "pp"]; Parser.COIN_ABV_TO_FULL = { "cp": "copper pieces", "sp": "silver pieces", "ep": "electrum pieces", "gp": "gold pieces", "pp": "platinum pieces", }; Parser.COIN_CONVERSIONS = [1, 10, 50, 100, 1000]; Parser.coinAbvToFull = function (coin) { return Parser._parse_aToB(Parser.COIN_ABV_TO_FULL, coin); }; /** * @param currency Object of the form `{pp: , gp: , ... }`. * @param isDisplayEmpty If "empty" values (i.e., those which are 0) should be displayed. */ Parser.getDisplayCurrency = function (currency, {isDisplayEmpty = false} = {}) { return [...Parser.COIN_ABVS] .reverse() .filter(abv => isDisplayEmpty ? currency[abv] != null : currency[abv]) .map(abv => `${currency[abv].toLocaleString()} ${abv}`) .join(", "); }; Parser.itemWeightToFull = function (item, isShortForm) { if (item.weight) { // Handle pure integers if (Math.round(item.weight) === item.weight) return `${item.weight} lb.${(item.weightNote ? ` ${item.weightNote}` : "")}`; const integerPart = Math.floor(item.weight); // Attempt to render the amount as (a number +) a vulgar const vulgarGlyph = Parser.numberToVulgar(item.weight - integerPart, {isFallbackOnFractional: false}); if (vulgarGlyph) return `${integerPart || ""}${vulgarGlyph} lb.${(item.weightNote ? ` ${item.weightNote}` : "")}`; // Fall back on decimal pounds or ounces return `${(item.weight < 1 ? item.weight * 16 : item.weight).toLocaleString(undefined, {maximumFractionDigits: 5})} ${item.weight < 1 ? "oz" : "lb"}.${(item.weightNote ? ` ${item.weightNote}` : "")}`; } if (item.weightMult) return isShortForm ? `×${item.weightMult}` : `base weight ×${item.weightMult}`; return ""; }; Parser.ITEM_RECHARGE_TO_FULL = { round: "Every Round", restShort: "Short Rest", restLong: "Long Rest", dawn: "Dawn", dusk: "Dusk", midnight: "Midnight", week: "Week", month: "Month", year: "Year", decade: "Decade", century: "Century", special: "Special", }; Parser.itemRechargeToFull = function (recharge) { return Parser._parse_aToB(Parser.ITEM_RECHARGE_TO_FULL, recharge); }; Parser.ITEM_MISC_TAG_TO_FULL = { "CF/W": "Creates Food/Water", "TT": "Trinket Table", }; Parser.itemMiscTagToFull = function (type) { return Parser._parse_aToB(Parser.ITEM_MISC_TAG_TO_FULL, type); }; Parser._decimalSeparator = (0.1).toLocaleString().substring(1, 2); Parser._numberCleanRegexp = Parser._decimalSeparator === "." ? new RegExp(/[\s,]*/g, "g") : new RegExp(/[\s.]*/g, "g"); Parser._costSplitRegexp = Parser._decimalSeparator === "." ? new RegExp(/(\d+(\.\d+)?)([csegp]p)/) : new RegExp(/(\d+(,\d+)?)([csegp]p)/); /** input e.g. "25 gp", "1,000pp" */ Parser.coinValueToNumber = function (value) { if (!value) return 0; // handle oddities if (value === "Varies") return 0; value = value .replace(/\s*/, "") .replace(Parser._numberCleanRegexp, "") .toLowerCase(); const m = Parser._costSplitRegexp.exec(value); if (!m) throw new Error(`Badly formatted value "${value}"`); const ixCoin = Parser.COIN_ABVS.indexOf(m[3]); if (!~ixCoin) throw new Error(`Unknown coin type "${m[3]}"`); return Number(m[1]) * Parser.COIN_CONVERSIONS[ixCoin]; }; Parser.weightValueToNumber = function (value) { if (!value) return 0; if (Number(value)) return Number(value); else throw new Error(`Badly formatted value ${value}`); }; Parser.dmgTypeToFull = function (dmgType) { return Parser._parse_aToB(Parser.DMGTYPE_JSON_TO_FULL, dmgType); }; Parser.skillProficienciesToFull = function (skillProficiencies) { function renderSingle (skProf) { if (skProf.any) { skProf = MiscUtil.copyFast(skProf); skProf.choose = {"from": Object.keys(Parser.SKILL_TO_ATB_ABV), "count": skProf.any}; delete skProf.any; } const keys = Object.keys(skProf).sort(SortUtil.ascSortLower); const ixChoose = keys.indexOf("choose"); if (~ixChoose) keys.splice(ixChoose, 1); const baseStack = []; keys.filter(k => skProf[k]).forEach(k => baseStack.push(Renderer.get().render(`{@skill ${k.toTitleCase()}}`))); const chooseStack = []; if (~ixChoose) { const chObj = skProf.choose; if (chObj.from.length === 18) { chooseStack.push(`choose any ${!chObj.count || chObj.count === 1 ? "skill" : chObj.count}`); } else { chooseStack.push(`choose ${chObj.count || 1} from ${chObj.from.map(it => Renderer.get().render(`{@skill ${it.toTitleCase()}}`)).joinConjunct(", ", " and ")}`); } } const base = baseStack.joinConjunct(", ", " and "); const choose = chooseStack.join(""); // this should currently only ever be 1-length if (baseStack.length && chooseStack.length) return `${base}; and ${choose}`; else if (baseStack.length) return base; else if (chooseStack.length) return choose; } return skillProficiencies.map(renderSingle).join(" or "); }; // sp-prefix functions are for parsing spell data, and shared with the roll20 script Parser.spSchoolAndSubschoolsAbvsToFull = function (school, subschools) { if (!subschools || !subschools.length) return Parser.spSchoolAbvToFull(school); else return `${Parser.spSchoolAbvToFull(school)} (${subschools.map(sub => Parser.spSchoolAbvToFull(sub)).join(", ")})`; }; Parser.spSchoolAbvToFull = function (schoolOrSubschool) { const out = Parser._parse_aToB(Parser.SP_SCHOOL_ABV_TO_FULL, schoolOrSubschool); if (Parser.SP_SCHOOL_ABV_TO_FULL[schoolOrSubschool]) return out; if (PrereleaseUtil.getMetaLookup("spellSchools")?.[schoolOrSubschool]) return PrereleaseUtil.getMetaLookup("spellSchools")?.[schoolOrSubschool].full; if (BrewUtil2.getMetaLookup("spellSchools")?.[schoolOrSubschool]) return BrewUtil2.getMetaLookup("spellSchools")?.[schoolOrSubschool].full; return out; }; Parser.spSchoolAndSubschoolsAbvsShort = function (school, subschools) { if (!subschools || !subschools.length) return Parser.spSchoolAbvToShort(school); else return `${Parser.spSchoolAbvToShort(school)} (${subschools.map(sub => Parser.spSchoolAbvToShort(sub)).join(", ")})`; }; Parser.spSchoolAbvToShort = function (school) { const out = Parser._parse_aToB(Parser.SP_SCHOOL_ABV_TO_SHORT, school); if (Parser.SP_SCHOOL_ABV_TO_SHORT[school]) return out; if (PrereleaseUtil.getMetaLookup("spellSchools")?.[school]) return PrereleaseUtil.getMetaLookup("spellSchools")?.[school].short; if (BrewUtil2.getMetaLookup("spellSchools")?.[school]) return BrewUtil2.getMetaLookup("spellSchools")?.[school].short; if (out.length <= 4) return out; return `${out.slice(0, 3)}.`; }; Parser.spSchoolAbvToStyle = function (school) { // For prerelease/homebrew const stylePart = Parser.spSchoolAbvToStylePart(school); if (!stylePart) return stylePart; return `style="${stylePart}"`; }; Parser.spSchoolAbvToStylePart = function (school) { // For prerelease/homebrew return Parser._spSchoolAbvToStylePart_prereleaseBrew({school, brewUtil: PrereleaseUtil}) || Parser._spSchoolAbvToStylePart_prereleaseBrew({school, brewUtil: BrewUtil2}) || ""; }; Parser._spSchoolAbvToStylePart_prereleaseBrew = function ({school, brewUtil}) { const rawColor = brewUtil.getMetaLookup("spellSchools")?.[school]?.color; if (!rawColor || !rawColor.trim()) return ""; const validColor = BrewUtilShared.getValidColor(rawColor); if (validColor.length) return `color: #${validColor};`; }; Parser.getOrdinalForm = function (i) { i = Number(i); if (isNaN(i)) return ""; const j = i % 10; const k = i % 100; if (j === 1 && k !== 11) return `${i}st`; if (j === 2 && k !== 12) return `${i}nd`; if (j === 3 && k !== 13) return `${i}rd`; return `${i}th`; }; Parser.spLevelToFull = function (level) { if (level === 0) return "Cantrip"; else return Parser.getOrdinalForm(level); }; Parser.getArticle = function (str) { str = `${str}`; str = str.replace(/\d+/g, (...m) => Parser.numberToText(m[0])); return /^[aeiou]/i.test(str) ? "an" : "a"; }; Parser.spLevelToFullLevelText = function (level, {isDash = false, isPluralCantrips = true} = {}) { return `${Parser.spLevelToFull(level)}${(level === 0 ? (isPluralCantrips ? "s" : "") : `${isDash ? "-" : " "}level`)}`; }; Parser.spLevelToSpellPoints = function (lvl) { lvl = Number(lvl); if (isNaN(lvl) || lvl === 0) return 0; return Math.ceil(1.34 * lvl); }; Parser.spMetaToArr = function (meta) { if (!meta) return []; return Object.entries(meta) .filter(([_, v]) => v) .sort(SortUtil.ascSort) .map(([k]) => k); }; Parser.spMetaToFull = function (meta) { if (!meta) return ""; const metaTags = Parser.spMetaToArr(meta); if (metaTags.length) return ` (${metaTags.join(", ")})`; return ""; }; Parser.spLevelSchoolMetaToFull = function (level, school, meta, subschools) { const levelPart = level === 0 ? Parser.spLevelToFull(level).toLowerCase() : `${Parser.spLevelToFull(level)}-level`; const levelSchoolStr = level === 0 ? `${Parser.spSchoolAbvToFull(school)} ${levelPart}` : `${levelPart} ${Parser.spSchoolAbvToFull(school).toLowerCase()}`; const metaArr = Parser.spMetaToArr(meta); if (metaArr.length || (subschools && subschools.length)) { const metaAndSubschoolPart = [ (subschools || []).map(sub => Parser.spSchoolAbvToFull(sub)).join(", "), metaArr.join(", "), ].filter(Boolean).join("; ").toLowerCase(); return `${levelSchoolStr} (${metaAndSubschoolPart})`; } return levelSchoolStr; }; Parser.spTimeListToFull = function (times, isStripTags) { return times.map(t => `${Parser.getTimeToFull(t)}${t.condition ? `, ${isStripTags ? Renderer.stripTags(t.condition) : Renderer.get().render(t.condition)}` : ""}`).join(" or "); }; Parser.getTimeToFull = function (time) { return `${time.number ? `${time.number} ` : ""}${time.unit === "bonus" ? "bonus action" : time.unit}${time.number > 1 ? "s" : ""}`; }; Parser.getMinutesToFull = function (mins) { const days = Math.floor(mins / (24 * 60)); mins = mins % (24 * 60); const hours = Math.floor(mins / 60); mins = mins % 60; return [ days ? `${days} day${days > 1 ? "s" : ""}` : null, hours ? `${hours} hour${hours > 1 ? "s" : ""}` : null, mins ? `${mins} minute${mins > 1 ? "s" : ""}` : null, ].filter(Boolean) .join(" "); }; Parser.RNG_SPECIAL = "special"; Parser.RNG_POINT = "point"; Parser.RNG_LINE = "line"; Parser.RNG_CUBE = "cube"; Parser.RNG_CONE = "cone"; Parser.RNG_RADIUS = "radius"; Parser.RNG_SPHERE = "sphere"; Parser.RNG_HEMISPHERE = "hemisphere"; Parser.RNG_CYLINDER = "cylinder"; // homebrew only Parser.RNG_SELF = "self"; Parser.RNG_SIGHT = "sight"; Parser.RNG_UNLIMITED = "unlimited"; Parser.RNG_UNLIMITED_SAME_PLANE = "plane"; Parser.RNG_TOUCH = "touch"; Parser.SP_RANGE_TYPE_TO_FULL = { [Parser.RNG_SPECIAL]: "Special", [Parser.RNG_POINT]: "Point", [Parser.RNG_LINE]: "Line", [Parser.RNG_CUBE]: "Cube", [Parser.RNG_CONE]: "Cone", [Parser.RNG_RADIUS]: "Radius", [Parser.RNG_SPHERE]: "Sphere", [Parser.RNG_HEMISPHERE]: "Hemisphere", [Parser.RNG_CYLINDER]: "Cylinder", [Parser.RNG_SELF]: "Self", [Parser.RNG_SIGHT]: "Sight", [Parser.RNG_UNLIMITED]: "Unlimited", [Parser.RNG_UNLIMITED_SAME_PLANE]: "Unlimited on the same plane", [Parser.RNG_TOUCH]: "Touch", }; Parser.spRangeTypeToFull = function (range) { return Parser._parse_aToB(Parser.SP_RANGE_TYPE_TO_FULL, range); }; Parser.UNT_FEET = "feet"; Parser.UNT_YARDS = "yards"; Parser.UNT_MILES = "miles"; Parser.SP_DIST_TYPE_TO_FULL = { [Parser.UNT_FEET]: "Feet", [Parser.UNT_YARDS]: "Yards", [Parser.UNT_MILES]: "Miles", [Parser.RNG_SELF]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_SELF], [Parser.RNG_TOUCH]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_TOUCH], [Parser.RNG_SIGHT]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_SIGHT], [Parser.RNG_UNLIMITED]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_UNLIMITED], [Parser.RNG_UNLIMITED_SAME_PLANE]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_UNLIMITED_SAME_PLANE], }; Parser.spDistanceTypeToFull = function (range) { return Parser._parse_aToB(Parser.SP_DIST_TYPE_TO_FULL, range); }; Parser.SP_RANGE_TO_ICON = { [Parser.RNG_SPECIAL]: "fa-star", [Parser.RNG_POINT]: "", [Parser.RNG_LINE]: "fa-grip-lines-vertical", [Parser.RNG_CUBE]: "fa-cube", [Parser.RNG_CONE]: "fa-traffic-cone", [Parser.RNG_RADIUS]: "fa-hockey-puck", [Parser.RNG_SPHERE]: "fa-globe", [Parser.RNG_HEMISPHERE]: "fa-globe", [Parser.RNG_CYLINDER]: "fa-database", [Parser.RNG_SELF]: "fa-street-view", [Parser.RNG_SIGHT]: "fa-eye", [Parser.RNG_UNLIMITED_SAME_PLANE]: "fa-globe-americas", [Parser.RNG_UNLIMITED]: "fa-infinity", [Parser.RNG_TOUCH]: "fa-hand-paper", }; Parser.spRangeTypeToIcon = function (range) { return Parser._parse_aToB(Parser.SP_RANGE_TO_ICON, range); }; Parser.spRangeToShortHtml = function (range) { switch (range.type) { case Parser.RNG_SPECIAL: return ``; case Parser.RNG_POINT: return Parser.spRangeToShortHtml._renderPoint(range); case Parser.RNG_LINE: case Parser.RNG_CUBE: case Parser.RNG_CONE: case Parser.RNG_RADIUS: case Parser.RNG_SPHERE: case Parser.RNG_HEMISPHERE: case Parser.RNG_CYLINDER: return Parser.spRangeToShortHtml._renderArea(range); } }; Parser.spRangeToShortHtml._renderPoint = function (range) { const dist = range.distance; switch (dist.type) { case Parser.RNG_SELF: case Parser.RNG_SIGHT: case Parser.RNG_UNLIMITED: case Parser.RNG_UNLIMITED_SAME_PLANE: case Parser.RNG_SPECIAL: case Parser.RNG_TOUCH: return ``; case Parser.UNT_FEET: case Parser.UNT_YARDS: case Parser.UNT_MILES: default: return `${dist.amount} ${Parser.getSingletonUnit(dist.type, true)}`; } }; Parser.spRangeToShortHtml._renderArea = function (range) { const size = range.distance; return ` ${size.amount}-${Parser.getSingletonUnit(size.type, true)} ${Parser.spRangeToShortHtml._getAreaStyleString(range)}`; }; Parser.spRangeToShortHtml._getAreaStyleString = function (range) { return ``; }; Parser.spRangeToFull = function (range) { switch (range.type) { case Parser.RNG_SPECIAL: return Parser.spRangeTypeToFull(range.type); case Parser.RNG_POINT: return Parser.spRangeToFull._renderPoint(range); case Parser.RNG_LINE: case Parser.RNG_CUBE: case Parser.RNG_CONE: case Parser.RNG_RADIUS: case Parser.RNG_SPHERE: case Parser.RNG_HEMISPHERE: case Parser.RNG_CYLINDER: return Parser.spRangeToFull._renderArea(range); } }; Parser.spRangeToFull._renderPoint = function (range) { const dist = range.distance; switch (dist.type) { case Parser.RNG_SELF: case Parser.RNG_SIGHT: case Parser.RNG_UNLIMITED: case Parser.RNG_UNLIMITED_SAME_PLANE: case Parser.RNG_SPECIAL: case Parser.RNG_TOUCH: return Parser.spRangeTypeToFull(dist.type); case Parser.UNT_FEET: case Parser.UNT_YARDS: case Parser.UNT_MILES: default: return `${dist.amount} ${dist.amount === 1 ? Parser.getSingletonUnit(dist.type) : dist.type}`; } }; Parser.spRangeToFull._renderArea = function (range) { const size = range.distance; return `Self (${size.amount}-${Parser.getSingletonUnit(size.type)}${Parser.spRangeToFull._getAreaStyleString(range)}${range.type === Parser.RNG_CYLINDER ? `${size.amountSecondary != null && size.typeSecondary != null ? `, ${size.amountSecondary}-${Parser.getSingletonUnit(size.typeSecondary)}-high` : ""} cylinder` : ""})`; }; Parser.spRangeToFull._getAreaStyleString = function (range) { switch (range.type) { case Parser.RNG_SPHERE: return " radius"; case Parser.RNG_HEMISPHERE: return `-radius ${range.type}`; case Parser.RNG_CYLINDER: return "-radius"; default: return ` ${range.type}`; } }; Parser.getSingletonUnit = function (unit, isShort) { switch (unit) { case Parser.UNT_FEET: return isShort ? "ft." : "foot"; case Parser.UNT_YARDS: return isShort ? "yd." : "yard"; case Parser.UNT_MILES: return isShort ? "mi." : "mile"; default: { const fromPrerelease = Parser._getSingletonUnit_prereleaseBrew({unit, isShort, brewUtil: PrereleaseUtil}); if (fromPrerelease) return fromPrerelease; const fromBrew = Parser._getSingletonUnit_prereleaseBrew({unit, isShort, brewUtil: BrewUtil2}); if (fromBrew) return fromBrew; if (unit.charAt(unit.length - 1) === "s") return unit.slice(0, -1); return unit; } } }; Parser._getSingletonUnit_prereleaseBrew = function ({unit, isShort, brewUtil}) { const fromBrew = brewUtil.getMetaLookup("spellDistanceUnits")?.[unit]?.["singular"]; if (fromBrew) return fromBrew; }; Parser.RANGE_TYPES = [ {type: Parser.RNG_POINT, hasDistance: true, isRequireAmount: false}, {type: Parser.RNG_LINE, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_CUBE, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_CONE, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_RADIUS, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_SPHERE, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_HEMISPHERE, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_CYLINDER, hasDistance: true, isRequireAmount: true}, {type: Parser.RNG_SPECIAL, hasDistance: false, isRequireAmount: false}, ]; Parser.DIST_TYPES = [ {type: Parser.RNG_SELF, hasAmount: false}, {type: Parser.RNG_TOUCH, hasAmount: false}, {type: Parser.UNT_FEET, hasAmount: true}, {type: Parser.UNT_YARDS, hasAmount: true}, {type: Parser.UNT_MILES, hasAmount: true}, {type: Parser.RNG_SIGHT, hasAmount: false}, {type: Parser.RNG_UNLIMITED_SAME_PLANE, hasAmount: false}, {type: Parser.RNG_UNLIMITED, hasAmount: false}, ]; Parser.spComponentsToFull = function (comp, level, {isPlainText = false} = {}) { if (!comp) return "None"; const out = []; if (comp.v) out.push("V"); if (comp.s) out.push("S"); if (comp.m != null) { const fnRender = isPlainText ? Renderer.stripTags.bind(Renderer) : Renderer.get().render.bind(Renderer.get()); out.push(`M${comp.m !== true ? ` (${fnRender(comp.m.text != null ? comp.m.text : comp.m)})` : ""}`); } if (comp.r) out.push(`R (${level} gp)`); return out.join(", ") || "None"; }; Parser.SP_END_TYPE_TO_FULL = { "dispel": "dispelled", "trigger": "triggered", "discharge": "discharged", }; Parser.spEndTypeToFull = function (type) { return Parser._parse_aToB(Parser.SP_END_TYPE_TO_FULL, type); }; Parser.spDurationToFull = function (dur) { let hasSubOr = false; const outParts = dur.map(d => { switch (d.type) { case "special": return "Special"; case "instant": return `Instantaneous${d.condition ? ` (${d.condition})` : ""}`; case "timed": return `${d.concentration ? "Concentration, " : ""}${d.concentration ? "u" : d.duration.upTo ? "U" : ""}${d.concentration || d.duration.upTo ? "p to " : ""}${d.duration.amount} ${d.duration.amount === 1 ? d.duration.type : `${d.duration.type}s`}`; case "permanent": { if (d.ends) { const endsToJoin = d.ends.map(m => Parser.spEndTypeToFull(m)); hasSubOr = hasSubOr || endsToJoin.length > 1; return `Until ${endsToJoin.joinConjunct(", ", " or ")}`; } else { return "Permanent"; } } } }); return `${outParts.joinConjunct(hasSubOr ? "; " : ", ", " or ")}${dur.length > 1 ? " (see below)" : ""}`; }; Parser.DURATION_TYPES = [ {type: "instant", full: "Instantaneous"}, {type: "timed", hasAmount: true}, {type: "permanent", hasEnds: true}, {type: "special"}, ]; Parser.DURATION_AMOUNT_TYPES = [ "turn", "round", "minute", "hour", "day", "week", "year", ]; Parser.spClassesToFull = function (sp, {isTextOnly = false, subclassLookup = {}} = {}) { const fromSubclassList = Renderer.spell.getCombinedClasses(sp, "fromSubclass"); const fromSubclasses = Parser.spSubclassesToFull(fromSubclassList, {isTextOnly, subclassLookup}); const fromClassList = Renderer.spell.getCombinedClasses(sp, "fromClassList"); return `${Parser.spMainClassesToFull(fromClassList, {isTextOnly})}${fromSubclasses ? `, ${fromSubclasses}` : ""}`; }; Parser.spMainClassesToFull = function (fromClassList, {isTextOnly = false} = {}) { return fromClassList .map(c => ({hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](c), c})) .filter(it => !ExcludeUtil.isInitialised || !ExcludeUtil.isExcluded(it.hash, "class", it.c.source)) .sort((a, b) => SortUtil.ascSort(a.c.name, b.c.name)) .map(it => { if (isTextOnly) return it.c.name; return `${Renderer.get().render(`{@class ${it.c.name}|${it.c.source}}`)}`; }) .join(", ") || ""; }; Parser.spSubclassesToFull = function (fromSubclassList, {isTextOnly = false, subclassLookup = {}} = {}) { return fromSubclassList .filter(mt => { if (!ExcludeUtil.isInitialised) return true; const excludeClass = ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](mt.class), "class", mt.class.source); if (excludeClass) return false; return !ExcludeUtil.isExcluded( UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ shortName: mt.subclass.name, source: mt.subclass.source, className: mt.class.name, classSource: mt.class.source, }), "subclass", mt.subclass.source, {isNoCount: true}, ); }) .sort((a, b) => { const byName = SortUtil.ascSort(a.class.name, b.class.name); return byName || SortUtil.ascSort(a.subclass.name, b.subclass.name); }) .map(c => Parser._spSubclassItem({fromSubclass: c, isTextOnly})) .join(", ") || ""; }; Parser._spSubclassItem = function ({fromSubclass, isTextOnly}) { const c = fromSubclass.class; const sc = fromSubclass.subclass; const text = `${sc.shortName}${sc.subSubclass ? ` (${sc.subSubclass})` : ""}`; if (isTextOnly) return text; const classPart = `${Renderer.get().render(`{@class ${c.name}|${c.source}}`)}`; return `${Renderer.get().render(`{@class ${c.name}|${c.source}|${text}|${sc.shortName}|${sc.source}}`)} ${classPart}`; }; Parser.SPELL_ATTACK_TYPE_TO_FULL = {}; Parser.SPELL_ATTACK_TYPE_TO_FULL["M"] = "Melee"; Parser.SPELL_ATTACK_TYPE_TO_FULL["R"] = "Ranged"; Parser.SPELL_ATTACK_TYPE_TO_FULL["O"] = "Other/Unknown"; Parser.spAttackTypeToFull = function (type) { return Parser._parse_aToB(Parser.SPELL_ATTACK_TYPE_TO_FULL, type); }; Parser.SPELL_AREA_TYPE_TO_FULL = { ST: "Single Target", MT: "Multiple Targets", C: "Cube", N: "Cone", Y: "Cylinder", S: "Sphere", R: "Circle", Q: "Square", L: "Line", H: "Hemisphere", W: "Wall", }; Parser.spAreaTypeToFull = function (type) { return Parser._parse_aToB(Parser.SPELL_AREA_TYPE_TO_FULL, type); }; Parser.SP_MISC_TAG_TO_FULL = { HL: "Healing", THP: "Grants Temporary Hit Points", SGT: "Requires Sight", PRM: "Permanent Effects", SCL: "Scaling Effects", SMN: "Summons Creature", MAC: "Modifies AC", TP: "Teleportation", FMV: "Forced Movement", RO: "Rollable Effects", LGTS: "Creates Sunlight", LGT: "Creates Light", UBA: "Uses Bonus Action", PS: "Plane Shifting", OBS: "Obscures Vision", DFT: "Difficult Terrain", AAD: "Additional Attack Damage", OBJ: "Affects Objects", ADV: "Grants Advantage", }; Parser.spMiscTagToFull = function (type) { return Parser._parse_aToB(Parser.SP_MISC_TAG_TO_FULL, type); }; Parser.SP_CASTER_PROGRESSION_TO_FULL = { full: "Full", "1/2": "Half", "1/3": "One-Third", "pact": "Pact Magic", }; Parser.spCasterProgressionToFull = function (type) { return Parser._parse_aToB(Parser.SP_CASTER_PROGRESSION_TO_FULL, type); }; // mon-prefix functions are for parsing monster data, and shared with the roll20 script Parser.monTypeToFullObj = function (type) { const out = { types: [], tags: [], asText: "", asTextShort: "", typeSidekick: null, tagsSidekick: [], asTextSidekick: null, }; // handles e.g. "fey" if (typeof type === "string") { out.types = [type]; out.asText = type.toTitleCase(); out.asTextShort = out.asText; return out; } if (type.type?.choose) { out.types = type.type.choose; } else { out.types = [type.type]; } if (type.swarmSize) { out.tags.push("swarm"); out.asText = `swarm of ${Parser.sizeAbvToFull(type.swarmSize)} ${out.types.map(typ => Parser.monTypeToPlural(typ).toTitleCase()).joinConjunct(", ", " or ")}`; out.asTextShort = out.asText; out.swarmSize = type.swarmSize; } else { out.asText = out.types.map(typ => typ.toTitleCase()).joinConjunct(", ", " or "); out.asTextShort = out.asText; } const tagMetas = Parser.monTypeToFullObj._getTagMetas(type.tags); if (tagMetas.length) { out.tags.push(...tagMetas.map(({filterTag}) => filterTag)); const ptTags = ` (${tagMetas.map(({displayTag}) => displayTag).join(", ")})`; out.asText += ptTags; out.asTextShort += ptTags; } if (type.note) out.asText += ` ${type.note}`; // region Sidekick if (type.sidekickType) { out.typeSidekick = type.sidekickType; if (!type.sidekickHidden) out.asTextSidekick = `${type.sidekickType}`; const tagMetas = Parser.monTypeToFullObj._getTagMetas(type.sidekickTags); if (tagMetas.length) { out.tagsSidekick.push(...tagMetas.map(({filterTag}) => filterTag)); if (!type.sidekickHidden) out.asTextSidekick += ` (${tagMetas.map(({displayTag}) => displayTag).join(", ")})`; } } // endregion return out; }; Parser.monTypeToFullObj._getTagMetas = (tags) => { return tags ? tags.map(tag => { if (typeof tag === "string") { // handles e.g. "Fiend (Devil)" return { filterTag: tag.toLowerCase(), displayTag: tag.toTitleCase(), }; } else { // handles e.g. "Humanoid (Chondathan Human)" return { filterTag: tag.tag.toLowerCase(), displayTag: `${tag.prefix} ${tag.tag}`.toTitleCase(), }; } }) : []; }; Parser.monTypeToPlural = function (type) { return Parser._parse_aToB(Parser.MON_TYPE_TO_PLURAL, type); }; Parser.monTypeFromPlural = function (type) { return Parser._parse_bToA(Parser.MON_TYPE_TO_PLURAL, type); }; Parser.monCrToFull = function (cr, {xp = null, isMythic = false} = {}) { if (cr == null) return ""; if (typeof cr === "string") { if (Parser.crToNumber(cr) >= VeCt.CR_CUSTOM) return `${cr}${xp != null ? ` (${xp} XP)` : ""}`; xp = xp != null ? Parser._addCommas(xp) : Parser.crToXp(cr); return `${cr} (${xp} XP${isMythic ? `, or ${Parser.crToXp(cr, {isDouble: true})} XP as a mythic encounter` : ""})`; } else { const stack = [Parser.monCrToFull(cr.cr, {xp: cr.xp, isMythic})]; if (cr.lair) stack.push(`${Parser.monCrToFull(cr.lair)} when encountered in lair`); if (cr.coven) stack.push(`${Parser.monCrToFull(cr.coven)} when part of a coven`); return stack.joinConjunct(", ", " or "); } }; Parser.getFullImmRes = function (toParse) { if (!toParse?.length) return ""; let maxDepth = 0; function toString (it, depth = 0) { maxDepth = Math.max(maxDepth, depth); if (typeof it === "string") { return it; } else if (it.special) { return it.special; } else { const stack = []; if (it.preNote) stack.push(it.preNote); const prop = it.immune ? "immune" : it.resist ? "resist" : it.vulnerable ? "vulnerable" : null; if (prop) { const toJoin = it[prop].length === Parser.DMG_TYPES.length && CollectionUtil.deepEquals(Parser.DMG_TYPES, it[prop]) ? ["all damage"] : it[prop].map(nxt => toString(nxt, depth + 1)); stack.push(depth ? toJoin.join(maxDepth ? "; " : ", ") : toJoin.joinConjunct(", ", " and ")); } if (it.note) stack.push(it.note); return stack.join(" "); } } const arr = toParse.map(it => toString(it)); if (arr.length <= 1) return arr.join(""); let out = ""; for (let i = 0; i < arr.length - 1; ++i) { const it = arr[i]; const nxt = arr[i + 1]; const orig = toParse[i]; const origNxt = toParse[i + 1]; out += it; out += (it.includes(",") || nxt.includes(",") || (orig && orig.cond) || (origNxt && origNxt.cond)) ? "; " : ", "; } out += arr.last(); return out; }; Parser.getFullCondImm = function (condImm, {isPlainText = false, isEntry = false} = {}) { if (isPlainText && isEntry) throw new Error(`Options "isPlainText" and "isEntry" are mutually exclusive!`); if (!condImm?.length) return ""; const render = condition => { if (isPlainText) return condition; const ent = `{@condition ${condition}}`; if (isEntry) return ent; return Renderer.get().render(ent); }; return condImm .map(it => { if (it.special) return it.special; if (it.conditionImmune) return `${it.preNote ? `${it.preNote} ` : ""}${it.conditionImmune.map(render).join(", ")}${it.note ? ` ${it.note}` : ""}`; return render(it); }) .sort(SortUtil.ascSortLower).join(", "); }; Parser.MON_SENSE_TAG_TO_FULL = { "B": "blindsight", "D": "darkvision", "SD": "superior darkvision", "T": "tremorsense", "U": "truesight", }; Parser.monSenseTagToFull = function (tag) { return Parser._parse_aToB(Parser.MON_SENSE_TAG_TO_FULL, tag); }; Parser.MON_SPELLCASTING_TAG_TO_FULL = { "P": "Psionics", "I": "Innate", "F": "Form Only", "S": "Shared", "O": "Other", "CA": "Class, Artificer", "CB": "Class, Bard", "CC": "Class, Cleric", "CD": "Class, Druid", "CP": "Class, Paladin", "CR": "Class, Ranger", "CS": "Class, Sorcerer", "CL": "Class, Warlock", "CW": "Class, Wizard", }; Parser.monSpellcastingTagToFull = function (tag) { return Parser._parse_aToB(Parser.MON_SPELLCASTING_TAG_TO_FULL, tag); }; Parser.MON_MISC_TAG_TO_FULL = { "AOE": "Has Areas of Effect", "HPR": "Has HP Reduction", "MW": "Has Weapon Attacks, Melee", "RW": "Has Weapon Attacks, Ranged", "MLW": "Has Melee Weapons", "RNG": "Has Ranged Weapons", "RCH": "Has Reach Attacks", "THW": "Has Thrown Weapons", }; Parser.monMiscTagToFull = function (tag) { return Parser._parse_aToB(Parser.MON_MISC_TAG_TO_FULL, tag); }; Parser.MON_LANGUAGE_TAG_TO_FULL = { "AB": "Abyssal", "AQ": "Aquan", "AU": "Auran", "C": "Common", "CE": "Celestial", "CS": "Can't Speak Known Languages", "D": "Dwarvish", "DR": "Draconic", "DS": "Deep Speech", "DU": "Druidic", "E": "Elvish", "G": "Gnomish", "GI": "Giant", "GO": "Goblin", "GTH": "Gith", "H": "Halfling", "I": "Infernal", "IG": "Ignan", "LF": "Languages Known in Life", "O": "Orc", "OTH": "Other", "P": "Primordial", "S": "Sylvan", "T": "Terran", "TC": "Thieves' cant", "TP": "Telepathy", "U": "Undercommon", "X": "Any (Choose)", "XX": "All", }; Parser.monLanguageTagToFull = function (tag) { return Parser._parse_aToB(Parser.MON_LANGUAGE_TAG_TO_FULL, tag); }; Parser.ENVIRONMENTS = ["arctic", "coastal", "desert", "forest", "grassland", "hill", "mountain", "swamp", "underdark", "underwater", "urban"]; // psi-prefix functions are for parsing psionic data, and shared with the roll20 script Parser.PSI_ABV_TYPE_TALENT = "T"; Parser.PSI_ABV_TYPE_DISCIPLINE = "D"; Parser.PSI_ORDER_NONE = "None"; Parser.psiTypeToFull = type => Parser.psiTypeToMeta(type).full; Parser.psiTypeToMeta = type => { let out = {}; if (type === Parser.PSI_ABV_TYPE_TALENT) out = {hasOrder: false, full: "Talent"}; else if (type === Parser.PSI_ABV_TYPE_DISCIPLINE) out = {hasOrder: true, full: "Discipline"}; else if (PrereleaseUtil.getMetaLookup("psionicTypes")?.[type]) out = MiscUtil.copyFast(PrereleaseUtil.getMetaLookup("psionicTypes")[type]); else if (BrewUtil2.getMetaLookup("psionicTypes")?.[type]) out = MiscUtil.copyFast(BrewUtil2.getMetaLookup("psionicTypes")[type]); out.full = out.full || "Unknown"; out.short = out.short || out.full; return out; }; Parser.psiOrderToFull = (order) => { return order === undefined ? Parser.PSI_ORDER_NONE : order; }; Parser.prereqSpellToFull = function (spell, {isTextOnly = false} = {}) { if (spell) { const [text, suffix] = spell.split("#"); if (!suffix) return isTextOnly ? spell : Renderer.get().render(`{@spell ${spell}}`); else if (suffix === "c") return (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))(`{@spell ${text}} cantrip`); else if (suffix === "x") return (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))("{@spell hex} spell or a warlock feature that curses"); } else return VeCt.STR_NONE; }; Parser.prereqPactToFull = function (pact) { if (pact === "Chain") return "Pact of the Chain"; if (pact === "Tome") return "Pact of the Tome"; if (pact === "Blade") return "Pact of the Blade"; if (pact === "Talisman") return "Pact of the Talisman"; return pact; }; Parser.prereqPatronToShort = function (patron) { if (patron === "Any") return patron; const mThe = /^The (.*?)$/.exec(patron); if (mThe) return mThe[1]; return patron; }; // NOTE: These need to be reflected in omnidexer.js to be indexed Parser.OPT_FEATURE_TYPE_TO_FULL = { AI: "Artificer Infusion", ED: "Elemental Discipline", EI: "Eldritch Invocation", MM: "Metamagic", "MV": "Maneuver", "MV:B": "Maneuver, Battle Master", "MV:C2-UA": "Maneuver, Cavalier V2 (UA)", "AS:V1-UA": "Arcane Shot, V1 (UA)", "AS:V2-UA": "Arcane Shot, V2 (UA)", "AS": "Arcane Shot", OTH: "Other", "FS:F": "Fighting Style; Fighter", "FS:B": "Fighting Style; Bard", "FS:P": "Fighting Style; Paladin", "FS:R": "Fighting Style; Ranger", "PB": "Pact Boon", "OR": "Onomancy Resonant", "RN": "Rune Knight Rune", "AF": "Alchemical Formula", }; Parser.optFeatureTypeToFull = function (type) { if (Parser.OPT_FEATURE_TYPE_TO_FULL[type]) return Parser.OPT_FEATURE_TYPE_TO_FULL[type]; if (PrereleaseUtil.getMetaLookup("optionalFeatureTypes")?.[type]) return PrereleaseUtil.getMetaLookup("optionalFeatureTypes")[type]; if (BrewUtil2.getMetaLookup("optionalFeatureTypes")?.[type]) return BrewUtil2.getMetaLookup("optionalFeatureTypes")[type]; return type; }; Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL = { "SG": "Supernatural Gift", "OF": "Optional Feature", "DG": "Dark Gift", "RF:B": "Replacement Feature: Background", "CS": "Character Secret", // Specific to IDRotF (rules on page 14) }; Parser.charCreationOptionTypeToFull = function (type) { if (Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL[type]) return Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL[type]; if (PrereleaseUtil.getMetaLookup("charOption")?.[type]) return PrereleaseUtil.getMetaLookup("charOption")[type]; if (BrewUtil2.getMetaLookup("charOption")?.[type]) return BrewUtil2.getMetaLookup("charOption")[type]; return type; }; Parser.alignmentAbvToFull = function (alignment) { if (!alignment) return null; // used in sidekicks if (typeof alignment === "object") { if (alignment.special != null) { // use in MTF Sacred Statue return alignment.special; } else { // e.g. `{alignment: ["N", "G"], chance: 50}` or `{alignment: ["N", "G"]}` return `${alignment.alignment.map(a => Parser.alignmentAbvToFull(a)).join(" ")}${alignment.chance ? ` (${alignment.chance}%)` : ""}${alignment.note ? ` (${alignment.note})` : ""}`; } } else { alignment = alignment.toUpperCase(); switch (alignment) { case "L": return "lawful"; case "N": return "neutral"; case "NX": return "neutral (law/chaos axis)"; case "NY": return "neutral (good/evil axis)"; case "C": return "chaotic"; case "G": return "good"; case "E": return "evil"; // "special" values case "U": return "unaligned"; case "A": return "any alignment"; } return alignment; } }; Parser.alignmentListToFull = function (alignList) { if (!alignList) return ""; if (alignList.some(it => typeof it !== "string")) { if (alignList.some(it => typeof it === "string")) throw new Error(`Mixed alignment types: ${JSON.stringify(alignList)}`); // filter out any nonexistent alignments, as we don't care about "alignment does not exist" if there are other alignments alignList = alignList.filter(it => it.alignment === undefined || it.alignment != null); return alignList.map(it => it.special != null || it.chance != null || it.note != null ? Parser.alignmentAbvToFull(it) : Parser.alignmentListToFull(it.alignment)).join(" or "); } else { // assume all single-length arrays can be simply parsed if (alignList.length === 1) return Parser.alignmentAbvToFull(alignList[0]); // a pair of abv's, e.g. "L" "G" if (alignList.length === 2) { return alignList.map(a => Parser.alignmentAbvToFull(a)).join(" "); } if (alignList.length === 3) { if (alignList.includes("NX") && alignList.includes("NY") && alignList.includes("N")) return "any neutral alignment"; } // longer arrays should have a custom mapping if (alignList.length === 5) { if (!alignList.includes("G")) return "any non-good alignment"; if (!alignList.includes("E")) return "any non-evil alignment"; if (!alignList.includes("L")) return "any non-lawful alignment"; if (!alignList.includes("C")) return "any non-chaotic alignment"; } if (alignList.length === 4) { if (!alignList.includes("L") && !alignList.includes("NX")) return "any chaotic alignment"; if (!alignList.includes("G") && !alignList.includes("NY")) return "any evil alignment"; if (!alignList.includes("C") && !alignList.includes("NX")) return "any lawful alignment"; if (!alignList.includes("E") && !alignList.includes("NY")) return "any good alignment"; } throw new Error(`Unmapped alignment: ${JSON.stringify(alignList)}`); } }; Parser.weightToFull = function (lbs, isSmallUnit) { const tons = Math.floor(lbs / 2000); lbs = lbs - (2000 * tons); return [ tons ? `${tons}${isSmallUnit ? `` : " "}ton${tons === 1 ? "" : "s"}${isSmallUnit ? `` : ""}` : null, lbs ? `${lbs}${isSmallUnit ? `` : " "}lb.${isSmallUnit ? `` : ""}` : null, ].filter(Boolean).join(", "); }; Parser.RARITIES = ["common", "uncommon", "rare", "very rare", "legendary", "artifact"]; Parser.ITEM_RARITIES = ["none", ...Parser.RARITIES, "unknown", "unknown (magic)", "other"]; Parser.CAT_ID_CREATURE = 1; Parser.CAT_ID_SPELL = 2; Parser.CAT_ID_BACKGROUND = 3; Parser.CAT_ID_ITEM = 4; Parser.CAT_ID_CLASS = 5; Parser.CAT_ID_CONDITION = 6; Parser.CAT_ID_FEAT = 7; Parser.CAT_ID_ELDRITCH_INVOCATION = 8; Parser.CAT_ID_PSIONIC = 9; Parser.CAT_ID_RACE = 10; Parser.CAT_ID_OTHER_REWARD = 11; Parser.CAT_ID_VARIANT_OPTIONAL_RULE = 12; Parser.CAT_ID_ADVENTURE = 13; Parser.CAT_ID_DEITY = 14; Parser.CAT_ID_OBJECT = 15; Parser.CAT_ID_TRAP = 16; Parser.CAT_ID_HAZARD = 17; Parser.CAT_ID_QUICKREF = 18; Parser.CAT_ID_CULT = 19; Parser.CAT_ID_BOON = 20; Parser.CAT_ID_DISEASE = 21; Parser.CAT_ID_METAMAGIC = 22; Parser.CAT_ID_MANEUVER_BATTLEMASTER = 23; Parser.CAT_ID_TABLE = 24; Parser.CAT_ID_TABLE_GROUP = 25; Parser.CAT_ID_MANEUVER_CAVALIER = 26; Parser.CAT_ID_ARCANE_SHOT = 27; Parser.CAT_ID_OPTIONAL_FEATURE_OTHER = 28; Parser.CAT_ID_FIGHTING_STYLE = 29; Parser.CAT_ID_CLASS_FEATURE = 30; Parser.CAT_ID_VEHICLE = 31; Parser.CAT_ID_PACT_BOON = 32; Parser.CAT_ID_ELEMENTAL_DISCIPLINE = 33; Parser.CAT_ID_ARTIFICER_INFUSION = 34; Parser.CAT_ID_SHIP_UPGRADE = 35; Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE = 36; Parser.CAT_ID_ONOMANCY_RESONANT = 37; Parser.CAT_ID_RUNE_KNIGHT_RUNE = 37; Parser.CAT_ID_ALCHEMICAL_FORMULA = 38; Parser.CAT_ID_MANEUVER = 39; Parser.CAT_ID_SUBCLASS = 40; Parser.CAT_ID_SUBCLASS_FEATURE = 41; Parser.CAT_ID_ACTION = 42; Parser.CAT_ID_LANGUAGE = 43; Parser.CAT_ID_BOOK = 44; Parser.CAT_ID_PAGE = 45; Parser.CAT_ID_LEGENDARY_GROUP = 46; Parser.CAT_ID_CHAR_CREATION_OPTIONS = 47; Parser.CAT_ID_RECIPES = 48; Parser.CAT_ID_STATUS = 49; Parser.CAT_ID_SKILLS = 50; Parser.CAT_ID_SENSES = 51; Parser.CAT_ID_DECK = 52; Parser.CAT_ID_CARD = 53; Parser.CAT_ID_TO_FULL = {}; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CREATURE] = "Bestiary"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SPELL] = "Spell"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BACKGROUND] = "Background"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ITEM] = "Item"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CLASS] = "Class"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CONDITION] = "Condition"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_FEAT] = "Feat"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ELDRITCH_INVOCATION] = "Eldritch Invocation"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PSIONIC] = "Psionic"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RACE] = "Race"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OTHER_REWARD] = "Other Reward"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = "Variant/Optional Rule"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ADVENTURE] = "Adventure"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DEITY] = "Deity"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OBJECT] = "Object"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TRAP] = "Trap"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_HAZARD] = "Hazard"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_QUICKREF] = "Quick Reference"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CULT] = "Cult"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BOON] = "Boon"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DISEASE] = "Disease"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_METAMAGIC] = "Metamagic"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = "Maneuver; Battlemaster"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TABLE] = "Table"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TABLE_GROUP] = "Table"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER_CAVALIER] = "Maneuver; Cavalier"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ARCANE_SHOT] = "Arcane Shot"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = "Optional Feature"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_FIGHTING_STYLE] = "Fighting Style"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CLASS_FEATURE] = "Class Feature"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_VEHICLE] = "Vehicle"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PACT_BOON] = "Pact Boon"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = "Elemental Discipline"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ARTIFICER_INFUSION] = "Infusion"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SHIP_UPGRADE] = "Ship Upgrade"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = "Infernal War Machine Upgrade"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ONOMANCY_RESONANT] = "Onomancy Resonant"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = "Rune Knight Rune"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ALCHEMICAL_FORMULA] = "Alchemical Formula"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER] = "Maneuver"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SUBCLASS] = "Subclass"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SUBCLASS_FEATURE] = "Subclass Feature"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ACTION] = "Action"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_LANGUAGE] = "Language"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BOOK] = "Book"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PAGE] = "Page"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_LEGENDARY_GROUP] = "Legendary Group"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = "Character Creation Option"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RECIPES] = "Recipe"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_STATUS] = "Status"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DECK] = "Deck"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CARD] = "Card"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SKILLS] = "Skill"; Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SENSES] = "Sense"; Parser.pageCategoryToFull = function (catId) { return Parser._parse_aToB(Parser.CAT_ID_TO_FULL, catId); }; Parser.CAT_ID_TO_PROP = {}; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CREATURE] = "monster"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SPELL] = "spell"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BACKGROUND] = "background"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ITEM] = "item"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CLASS] = "class"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CONDITION] = "condition"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_FEAT] = "feat"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PSIONIC] = "psionic"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RACE] = "race"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OTHER_REWARD] = "reward"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = "variantrule"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ADVENTURE] = "adventure"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DEITY] = "deity"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OBJECT] = "object"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TRAP] = "trap"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_HAZARD] = "hazard"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CULT] = "cult"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BOON] = "boon"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DISEASE] = "condition"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TABLE] = "table"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TABLE_GROUP] = "tableGroup"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_VEHICLE] = "vehicle"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ELDRITCH_INVOCATION] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER_CAVALIER] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ARCANE_SHOT] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_FIGHTING_STYLE] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_METAMAGIC] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PACT_BOON] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ARTIFICER_INFUSION] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SHIP_UPGRADE] = "vehicleUpgrade"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = "vehicleUpgrade"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ONOMANCY_RESONANT] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ALCHEMICAL_FORMULA] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER] = "optionalfeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_QUICKREF] = null; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CLASS_FEATURE] = "classFeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SUBCLASS] = "subclass"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SUBCLASS_FEATURE] = "subclassFeature"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ACTION] = "action"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_LANGUAGE] = "language"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BOOK] = "book"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PAGE] = null; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_LEGENDARY_GROUP] = "legendaryGroup"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = "charoption"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RECIPES] = "recipe"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_STATUS] = "status"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DECK] = "deck"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CARD] = "card"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SKILLS] = "skill"; Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SENSES] = "sense"; Parser.pageCategoryToProp = function (catId) { return Parser._parse_aToB(Parser.CAT_ID_TO_PROP, catId); }; Parser.ABIL_ABVS = ["str", "dex", "con", "int", "wis", "cha"]; Parser.spClassesToCurrentAndLegacy = function (fromClassList) { const current = []; const legacy = []; fromClassList.forEach(cls => { if ((cls.name === "Artificer" && cls.source === "UAArtificer") || (cls.name === "Artificer (Revisited)" && cls.source === "UAArtificerRevisited")) legacy.push(cls); else current.push(cls); }); return [current, legacy]; }; /** * Build a pair of strings; one with all current subclasses, one with all legacy subclasses * * @param sp a spell * @param subclassLookup Data loaded from `generated/gendata-subclass-lookup.json`. Of the form: `{PHB: {Barbarian: {PHB: {Berserker: "Path of the Berserker"}}}}` * @returns {*[]} A two-element array. First item is a string of all the current subclasses, second item a string of * all the legacy/superseded subclasses */ Parser.spSubclassesToCurrentAndLegacyFull = function (sp, subclassLookup) { return Parser._spSubclassesToCurrentAndLegacyFull({sp, subclassLookup, prop: "fromSubclass"}); }; Parser.spVariantSubclassesToCurrentAndLegacyFull = function (sp, subclassLookup) { return Parser._spSubclassesToCurrentAndLegacyFull({sp, subclassLookup, prop: "fromSubclassVariant"}); }; Parser._spSubclassesToCurrentAndLegacyFull = ({sp, subclassLookup, prop}) => { const fromSubclass = Renderer.spell.getCombinedClasses(sp, prop); if (!fromSubclass.length) return ["", ""]; const current = []; const legacy = []; const curNames = new Set(); const toCheck = []; fromSubclass .filter(c => { const excludeClass = ExcludeUtil.isExcluded( UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: c.class.name, source: c.class.source}), "class", c.class.source, {isNoCount: true}, ); if (excludeClass) return false; const excludeSubclass = ExcludeUtil.isExcluded( UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ shortName: c.subclass.shortName, source: c.subclass.source, className: c.class.name, classSource: c.class.source, }), "subclass", c.subclass.source, {isNoCount: true}, ); if (excludeSubclass) return false; return !Renderer.spell.isExcludedSubclassVariantSource({classDefinedInSource: c.class.definedInSource}); }) .sort((a, b) => { const byName = SortUtil.ascSort(a.subclass.name, b.subclass.name); return byName || SortUtil.ascSort(a.class.name, b.class.name); }) .forEach(c => { const nm = c.subclass.name; const src = c.subclass.source; const toAdd = Parser._spSubclassItem({fromSubclass: c, isTextOnly: false}); const fromLookup = MiscUtil.get( subclassLookup, c.class.source, c.class.name, c.subclass.source, c.subclass.name, ); if (fromLookup && fromLookup.isReprinted) { legacy.push(toAdd); } else if (SourceUtil.isNonstandardSource(src)) { const cleanName = Parser._spSubclassesToCurrentAndLegacyFull.mapClassShortNameToMostRecent( nm.split("(")[0].trim().split(/v\d+/)[0].trim(), ); toCheck.push({"name": cleanName, "ele": toAdd}); } else { current.push(toAdd); curNames.add(nm); } }); toCheck.forEach(n => { if (curNames.has(n.name)) { legacy.push(n.ele); } else { current.push(n.ele); } }); return [current.join(", "), legacy.join(", ")]; }; /** * Get the most recent iteration of a subclass name. */ Parser._spSubclassesToCurrentAndLegacyFull.mapClassShortNameToMostRecent = (shortName) => { switch (shortName) { case "Favored Soul": return "Divine Soul"; case "Undying Light": return "Celestial"; case "Deep Stalker": return "Gloom Stalker"; } return shortName; }; Parser.spVariantClassesToCurrentAndLegacy = function (fromVariantClassList) { const current = []; const legacy = []; fromVariantClassList.forEach(cls => { if (SourceUtil.isPrereleaseSource(cls.definedInSource)) legacy.push(cls); else current.push(cls); }); return [current, legacy]; }; Parser.attackTypeToFull = function (attackType) { return Parser._parse_aToB(Parser.ATK_TYPE_TO_FULL, attackType); }; Parser.trapHazTypeToFull = function (type) { return Parser._parse_aToB(Parser.TRAP_HAZARD_TYPE_TO_FULL, type); }; Parser.TRAP_HAZARD_TYPE_TO_FULL = { MECH: "Mechanical Trap", MAG: "Magical Trap", SMPL: "Simple Trap", CMPX: "Complex Trap", HAZ: "Hazard", WTH: "Weather", ENV: "Environmental Hazard", WLD: "Wilderness Hazard", GEN: "Generic", EST: "Eldritch Storm", }; Parser.tierToFullLevel = function (tier) { return Parser._parse_aToB(Parser.TIER_TO_FULL_LEVEL, tier); }; Parser.TIER_TO_FULL_LEVEL = {}; Parser.TIER_TO_FULL_LEVEL[1] = "1st\u20134th Level"; Parser.TIER_TO_FULL_LEVEL[2] = "5th\u201310th Level"; Parser.TIER_TO_FULL_LEVEL[3] = "11th\u201316th Level"; Parser.TIER_TO_FULL_LEVEL[4] = "17th\u201320th Level"; Parser.trapInitToFull = function (init) { return Parser._parse_aToB(Parser.TRAP_INIT_TO_FULL, init); }; Parser.TRAP_INIT_TO_FULL = {}; Parser.TRAP_INIT_TO_FULL[1] = "initiative count 10"; Parser.TRAP_INIT_TO_FULL[2] = "initiative count 20"; Parser.TRAP_INIT_TO_FULL[3] = "initiative count 20 and initiative count 10"; Parser.ATK_TYPE_TO_FULL = {}; Parser.ATK_TYPE_TO_FULL["MW"] = "Melee Weapon Attack"; Parser.ATK_TYPE_TO_FULL["RW"] = "Ranged Weapon Attack"; Parser.bookOrdinalToAbv = (ordinal, preNoSuff) => { if (ordinal === undefined) return ""; switch (ordinal.type) { case "part": return `${preNoSuff ? " " : ""}Part ${ordinal.identifier}${preNoSuff ? "" : " \u2014 "}`; case "chapter": return `${preNoSuff ? " " : ""}Ch. ${ordinal.identifier}${preNoSuff ? "" : ": "}`; case "episode": return `${preNoSuff ? " " : ""}Ep. ${ordinal.identifier}${preNoSuff ? "" : ": "}`; case "appendix": return `${preNoSuff ? " " : ""}App.${ordinal.identifier != null ? ` ${ordinal.identifier}` : ""}${preNoSuff ? "" : ": "}`; case "level": return `${preNoSuff ? " " : ""}Level ${ordinal.identifier}${preNoSuff ? "" : ": "}`; default: throw new Error(`Unhandled ordinal type "${ordinal.type}"`); } }; Parser.IMAGE_TYPE_TO_FULL = { "map": "Map", "mapPlayer": "Map (Player)", }; Parser.imageTypeToFull = function (imageType) { return Parser._parse_aToB(Parser.IMAGE_TYPE_TO_FULL, imageType, "Other"); }; Parser.nameToTokenName = function (name) { return name .toAscii() .replace(/"/g, ""); }; Parser.bytesToHumanReadable = function (bytes, {fixedDigits = 2} = {}) { if (bytes == null) return ""; if (!bytes) return "0 B"; const e = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, e)).toFixed(fixedDigits)} ${`\u200bKMGTP`.charAt(e)}B`; }; Parser.SKL_ABV_ABJ = "A"; Parser.SKL_ABV_EVO = "V"; Parser.SKL_ABV_ENC = "E"; Parser.SKL_ABV_ILL = "I"; Parser.SKL_ABV_DIV = "D"; Parser.SKL_ABV_NEC = "N"; Parser.SKL_ABV_TRA = "T"; Parser.SKL_ABV_CON = "C"; Parser.SKL_ABV_PSI = "P"; Parser.SKL_ABVS = [ Parser.SKL_ABV_ABJ, Parser.SKL_ABV_CON, Parser.SKL_ABV_DIV, Parser.SKL_ABV_ENC, Parser.SKL_ABV_EVO, Parser.SKL_ABV_ILL, Parser.SKL_ABV_NEC, Parser.SKL_ABV_PSI, Parser.SKL_ABV_TRA, ]; Parser.SP_TM_ACTION = "action"; Parser.SP_TM_B_ACTION = "bonus"; Parser.SP_TM_REACTION = "reaction"; Parser.SP_TM_ROUND = "round"; Parser.SP_TM_MINS = "minute"; Parser.SP_TM_HRS = "hour"; Parser.SP_TIME_SINGLETONS = [Parser.SP_TM_ACTION, Parser.SP_TM_B_ACTION, Parser.SP_TM_REACTION, Parser.SP_TM_ROUND]; Parser.SP_TIME_TO_FULL = { [Parser.SP_TM_ACTION]: "Action", [Parser.SP_TM_B_ACTION]: "Bonus Action", [Parser.SP_TM_REACTION]: "Reaction", [Parser.SP_TM_ROUND]: "Rounds", [Parser.SP_TM_MINS]: "Minutes", [Parser.SP_TM_HRS]: "Hours", }; Parser.spTimeUnitToFull = function (timeUnit) { return Parser._parse_aToB(Parser.SP_TIME_TO_FULL, timeUnit); }; Parser.SP_TIME_TO_SHORT = { [Parser.SP_TM_ROUND]: "Rnd.", [Parser.SP_TM_MINS]: "Min.", [Parser.SP_TM_HRS]: "Hr.", }; Parser.spTimeUnitToShort = function (timeUnit) { return Parser._parse_aToB(Parser.SP_TIME_TO_SHORT, timeUnit); }; Parser.SP_TIME_TO_ABV = { [Parser.SP_TM_ACTION]: "A", [Parser.SP_TM_B_ACTION]: "BA", [Parser.SP_TM_REACTION]: "R", [Parser.SP_TM_ROUND]: "rnd", [Parser.SP_TM_MINS]: "min", [Parser.SP_TM_HRS]: "hr", }; Parser.spTimeUnitToAbv = function (timeUnit) { return Parser._parse_aToB(Parser.SP_TIME_TO_ABV, timeUnit); }; Parser.spTimeToShort = function (time, isHtml) { if (!time) return ""; return (time.number === 1 && Parser.SP_TIME_SINGLETONS.includes(time.unit)) ? `${Parser.spTimeUnitToAbv(time.unit).uppercaseFirst()}${time.condition ? "*" : ""}` : `${time.number} ${isHtml ? `` : ""}${Parser.spTimeUnitToAbv(time.unit)}${isHtml ? `` : ""}${time.condition ? "*" : ""}`; }; Parser.SKL_ABJ = "Abjuration"; Parser.SKL_EVO = "Evocation"; Parser.SKL_ENC = "Enchantment"; Parser.SKL_ILL = "Illusion"; Parser.SKL_DIV = "Divination"; Parser.SKL_NEC = "Necromancy"; Parser.SKL_TRA = "Transmutation"; Parser.SKL_CON = "Conjuration"; Parser.SKL_PSI = "Psionic"; Parser.SP_SCHOOL_ABV_TO_FULL = {}; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ABJ] = Parser.SKL_ABJ; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_EVO] = Parser.SKL_EVO; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ENC] = Parser.SKL_ENC; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ILL] = Parser.SKL_ILL; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_DIV] = Parser.SKL_DIV; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_NEC] = Parser.SKL_NEC; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_TRA] = Parser.SKL_TRA; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_CON] = Parser.SKL_CON; Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_PSI] = Parser.SKL_PSI; Parser.SP_SCHOOL_ABV_TO_SHORT = {}; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ABJ] = "Abj."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_EVO] = "Evoc."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ENC] = "Ench."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ILL] = "Illu."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_DIV] = "Divin."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_NEC] = "Necro."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_TRA] = "Trans."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_CON] = "Conj."; Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_PSI] = "Psi."; Parser.ATB_ABV_TO_FULL = { "str": "Strength", "dex": "Dexterity", "con": "Constitution", "int": "Intelligence", "wis": "Wisdom", "cha": "Charisma", }; Parser.TP_ABERRATION = "aberration"; Parser.TP_BEAST = "beast"; Parser.TP_CELESTIAL = "celestial"; Parser.TP_CONSTRUCT = "construct"; Parser.TP_DRAGON = "dragon"; Parser.TP_ELEMENTAL = "elemental"; Parser.TP_FEY = "fey"; Parser.TP_FIEND = "fiend"; Parser.TP_GIANT = "giant"; Parser.TP_HUMANOID = "humanoid"; Parser.TP_MONSTROSITY = "monstrosity"; Parser.TP_OOZE = "ooze"; Parser.TP_PLANT = "plant"; Parser.TP_UNDEAD = "undead"; Parser.MON_TYPES = [Parser.TP_ABERRATION, Parser.TP_BEAST, Parser.TP_CELESTIAL, Parser.TP_CONSTRUCT, Parser.TP_DRAGON, Parser.TP_ELEMENTAL, Parser.TP_FEY, Parser.TP_FIEND, Parser.TP_GIANT, Parser.TP_HUMANOID, Parser.TP_MONSTROSITY, Parser.TP_OOZE, Parser.TP_PLANT, Parser.TP_UNDEAD]; Parser.MON_TYPE_TO_PLURAL = {}; Parser.MON_TYPE_TO_PLURAL[Parser.TP_ABERRATION] = "aberrations"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_BEAST] = "beasts"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_CELESTIAL] = "celestials"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_CONSTRUCT] = "constructs"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_DRAGON] = "dragons"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_ELEMENTAL] = "elementals"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_FEY] = "fey"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_FIEND] = "fiends"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_GIANT] = "giants"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_HUMANOID] = "humanoids"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_MONSTROSITY] = "monstrosities"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_OOZE] = "oozes"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_PLANT] = "plants"; Parser.MON_TYPE_TO_PLURAL[Parser.TP_UNDEAD] = "undead"; Parser.SZ_FINE = "F"; Parser.SZ_DIMINUTIVE = "D"; Parser.SZ_TINY = "T"; Parser.SZ_SMALL = "S"; Parser.SZ_MEDIUM = "M"; Parser.SZ_LARGE = "L"; Parser.SZ_HUGE = "H"; Parser.SZ_GARGANTUAN = "G"; Parser.SZ_COLOSSAL = "C"; Parser.SZ_VARIES = "V"; Parser.SIZE_ABVS = [Parser.SZ_TINY, Parser.SZ_SMALL, Parser.SZ_MEDIUM, Parser.SZ_LARGE, Parser.SZ_HUGE, Parser.SZ_GARGANTUAN, Parser.SZ_VARIES]; Parser.SIZE_ABV_TO_FULL = {}; Parser.SIZE_ABV_TO_FULL[Parser.SZ_FINE] = "Fine"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_DIMINUTIVE] = "Diminutive"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_TINY] = "Tiny"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_SMALL] = "Small"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_MEDIUM] = "Medium"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_LARGE] = "Large"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_HUGE] = "Huge"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_GARGANTUAN] = "Gargantuan"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_COLOSSAL] = "Colossal"; Parser.SIZE_ABV_TO_FULL[Parser.SZ_VARIES] = "Varies"; Parser.XP_CHART_ALT = { "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, }; Parser.ARMOR_ABV_TO_FULL = { "l.": "light", "m.": "medium", "h.": "heavy", }; Parser.WEAPON_ABV_TO_FULL = { "s.": "simple", "m.": "martial", }; Parser.CONDITION_TO_COLOR = { "Blinded": "#525252", "Charmed": "#f01789", "Deafened": "#ababab", "Exhausted": "#947a47", "Frightened": "#c9ca18", "Grappled": "#8784a0", "Incapacitated": "#3165a0", "Invisible": "#7ad2d6", "Paralyzed": "#c00900", "Petrified": "#a0a0a0", "Poisoned": "#4dc200", "Prone": "#5e60a0", "Restrained": "#d98000", "Stunned": "#a23bcb", "Unconscious": "#3a40ad", "Concentration": "#009f7a", }; Parser.RULE_TYPE_TO_FULL = { "O": "Optional", "P": "Prerelease", "V": "Variant", "VO": "Variant Optional", "VV": "Variant Variant", "U": "Unknown", }; Parser.ruleTypeToFull = function (ruleType) { return Parser._parse_aToB(Parser.RULE_TYPE_TO_FULL, ruleType); }; Parser.VEHICLE_TYPE_TO_FULL = { "SHIP": "Ship", "SPELLJAMMER": "Spelljammer Ship", "INFWAR": "Infernal War Machine", "CREATURE": "Creature", "OBJECT": "Object", "SHP:H": "Ship Upgrade, Hull", "SHP:M": "Ship Upgrade, Movement", "SHP:W": "Ship Upgrade, Weapon", "SHP:F": "Ship Upgrade, Figurehead", "SHP:O": "Ship Upgrade, Miscellaneous", "IWM:W": "Infernal War Machine Variant, Weapon", "IWM:A": "Infernal War Machine Upgrade, Armor", "IWM:G": "Infernal War Machine Upgrade, Gadget", }; Parser.vehicleTypeToFull = function (vehicleType) { return Parser._parse_aToB(Parser.VEHICLE_TYPE_TO_FULL, vehicleType); }; // SOURCES ============================================================================================================= Parser.SRC_5ETOOLS_TMP = "Parser.SRC_5ETOOLS_TMP"; // Temp source, used as a placeholder value Parser.SRC_CoS = "CoS"; Parser.SRC_DMG = "DMG"; Parser.SRC_EEPC = "EEPC"; Parser.SRC_EET = "EET"; Parser.SRC_HotDQ = "HotDQ"; Parser.SRC_LMoP = "LMoP"; Parser.SRC_MM = "MM"; Parser.SRC_OotA = "OotA"; Parser.SRC_PHB = "PHB"; Parser.SRC_PotA = "PotA"; Parser.SRC_RoT = "RoT"; Parser.SRC_RoTOS = "RoTOS"; Parser.SRC_SCAG = "SCAG"; Parser.SRC_SKT = "SKT"; Parser.SRC_ToA = "ToA"; Parser.SRC_TLK = "TLK"; Parser.SRC_ToD = "ToD"; Parser.SRC_TTP = "TTP"; Parser.SRC_TYP = "TftYP"; Parser.SRC_TYP_AtG = "TftYP-AtG"; Parser.SRC_TYP_DiT = "TftYP-DiT"; Parser.SRC_TYP_TFoF = "TftYP-TFoF"; Parser.SRC_TYP_THSoT = "TftYP-THSoT"; Parser.SRC_TYP_TSC = "TftYP-TSC"; Parser.SRC_TYP_ToH = "TftYP-ToH"; Parser.SRC_TYP_WPM = "TftYP-WPM"; Parser.SRC_VGM = "VGM"; Parser.SRC_XGE = "XGE"; Parser.SRC_OGA = "OGA"; Parser.SRC_MTF = "MTF"; Parser.SRC_WDH = "WDH"; Parser.SRC_WDMM = "WDMM"; Parser.SRC_GGR = "GGR"; Parser.SRC_KKW = "KKW"; Parser.SRC_LLK = "LLK"; Parser.SRC_AZfyT = "AZfyT"; Parser.SRC_GoS = "GoS"; Parser.SRC_AI = "AI"; Parser.SRC_OoW = "OoW"; Parser.SRC_ESK = "ESK"; Parser.SRC_DIP = "DIP"; Parser.SRC_HftT = "HftT"; Parser.SRC_DC = "DC"; Parser.SRC_SLW = "SLW"; Parser.SRC_SDW = "SDW"; Parser.SRC_BGDIA = "BGDIA"; Parser.SRC_LR = "LR"; Parser.SRC_AL = "AL"; Parser.SRC_SAC = "SAC"; Parser.SRC_ERLW = "ERLW"; Parser.SRC_EFR = "EFR"; Parser.SRC_RMBRE = "RMBRE"; Parser.SRC_RMR = "RMR"; Parser.SRC_MFF = "MFF"; Parser.SRC_AWM = "AWM"; Parser.SRC_IMR = "IMR"; Parser.SRC_SADS = "SADS"; Parser.SRC_EGW = "EGW"; Parser.SRC_EGW_ToR = "ToR"; Parser.SRC_EGW_DD = "DD"; Parser.SRC_EGW_FS = "FS"; Parser.SRC_EGW_US = "US"; Parser.SRC_MOT = "MOT"; Parser.SRC_IDRotF = "IDRotF"; Parser.SRC_TCE = "TCE"; Parser.SRC_VRGR = "VRGR"; Parser.SRC_HoL = "HoL"; Parser.SRC_XMtS = "XMtS"; Parser.SRC_RtG = "RtG"; Parser.SRC_AitFR = "AitFR"; Parser.SRC_AitFR_ISF = "AitFR-ISF"; Parser.SRC_AitFR_THP = "AitFR-THP"; Parser.SRC_AitFR_AVT = "AitFR-AVT"; Parser.SRC_AitFR_DN = "AitFR-DN"; Parser.SRC_AitFR_FCD = "AitFR-FCD"; Parser.SRC_WBtW = "WBtW"; Parser.SRC_DoD = "DoD"; Parser.SRC_MaBJoV = "MaBJoV"; Parser.SRC_FTD = "FTD"; Parser.SRC_SCC = "SCC"; Parser.SRC_SCC_CK = "SCC-CK"; Parser.SRC_SCC_HfMT = "SCC-HfMT"; Parser.SRC_SCC_TMM = "SCC-TMM"; Parser.SRC_SCC_ARiR = "SCC-ARiR"; Parser.SRC_MPMM = "MPMM"; Parser.SRC_CRCotN = "CRCotN"; Parser.SRC_JttRC = "JttRC"; Parser.SRC_SAiS = "SAiS"; Parser.SRC_AAG = "AAG"; Parser.SRC_BAM = "BAM"; Parser.SRC_LoX = "LoX"; Parser.SRC_DoSI = "DoSI"; Parser.SRC_DSotDQ = "DSotDQ"; Parser.SRC_KftGV = "KftGV"; Parser.SRC_BGG = "BGG"; Parser.SRC_TDCSR = "TDCSR"; Parser.SRC_PaBTSO = "PaBTSO"; Parser.SRC_PAitM = "PAitM"; Parser.SRC_SatO = "SatO"; Parser.SRC_ToFW = "ToFW"; Parser.SRC_MPP = "MPP"; Parser.SRC_BMT = "BMT"; Parser.SRC_GHLoE = "GHLoE"; Parser.SRC_DoDk = "DoDk"; Parser.SRC_SCREEN = "Screen"; Parser.SRC_SCREEN_WILDERNESS_KIT = "ScreenWildernessKit"; Parser.SRC_SCREEN_DUNGEON_KIT = "ScreenDungeonKit"; Parser.SRC_SCREEN_SPELLJAMMER = "ScreenSpelljammer"; Parser.SRC_HF = "HF"; Parser.SRC_HFFotM = "HFFotM"; Parser.SRC_HFStCM = "HFStCM"; Parser.SRC_CM = "CM"; Parser.SRC_NRH = "NRH"; Parser.SRC_NRH_TCMC = "NRH-TCMC"; Parser.SRC_NRH_AVitW = "NRH-AVitW"; Parser.SRC_NRH_ASS = "NRH-ASS"; // lmao Parser.SRC_NRH_CoI = "NRH-CoI"; Parser.SRC_NRH_TLT = "NRH-TLT"; Parser.SRC_NRH_AWoL = "NRH-AWoL"; Parser.SRC_NRH_AT = "NRH-AT"; Parser.SRC_MGELFT = "MGELFT"; Parser.SRC_VD = "VD"; Parser.SRC_SjA = "SjA"; Parser.SRC_HAT_TG = "HAT-TG"; Parser.SRC_HAT_LMI = "HAT-LMI"; Parser.SRC_GotSF = "GotSF"; Parser.SRC_LK = "LK"; Parser.SRC_CoA = "CoA"; Parser.SRC_PiP = "PiP"; Parser.SRC_AL_PREFIX = "AL"; Parser.SRC_ALCoS = `${Parser.SRC_AL_PREFIX}CurseOfStrahd`; Parser.SRC_ALEE = `${Parser.SRC_AL_PREFIX}ElementalEvil`; Parser.SRC_ALRoD = `${Parser.SRC_AL_PREFIX}RageOfDemons`; Parser.SRC_PS_PREFIX = "PS"; Parser.SRC_PSA = `${Parser.SRC_PS_PREFIX}A`; Parser.SRC_PSI = `${Parser.SRC_PS_PREFIX}I`; Parser.SRC_PSK = `${Parser.SRC_PS_PREFIX}K`; Parser.SRC_PSZ = `${Parser.SRC_PS_PREFIX}Z`; Parser.SRC_PSX = `${Parser.SRC_PS_PREFIX}X`; Parser.SRC_PSD = `${Parser.SRC_PS_PREFIX}D`; Parser.SRC_UA_PREFIX = "UA"; Parser.SRC_UA_ONE_PREFIX = "XUA"; Parser.SRC_MCVX_PREFIX = "MCV"; Parser.SRC_MisMVX_PREFIX = "MisMV"; Parser.SRC_AA_PREFIX = "AA"; Parser.SRC_UATMC = `${Parser.SRC_UA_PREFIX}TheMysticClass`; Parser.SRC_MCV1SC = `${Parser.SRC_MCVX_PREFIX}1SC`; Parser.SRC_MCV2DC = `${Parser.SRC_MCVX_PREFIX}2DC`; Parser.SRC_MCV3MC = `${Parser.SRC_MCVX_PREFIX}3MC`; Parser.SRC_MCV4EC = `${Parser.SRC_MCVX_PREFIX}4EC`; Parser.SRC_MisMV1 = `${Parser.SRC_MisMVX_PREFIX}1`; Parser.SRC_AATM = `${Parser.SRC_AA_PREFIX}TM`; Parser.AL_PREFIX = "Adventurers League: "; Parser.AL_PREFIX_SHORT = "AL: "; Parser.PS_PREFIX = "Plane Shift: "; Parser.PS_PREFIX_SHORT = "PS: "; Parser.UA_PREFIX = "Unearthed Arcana: "; Parser.UA_PREFIX_SHORT = "UA: "; Parser.TftYP_NAME = "Tales from the Yawning Portal"; Parser.AitFR_NAME = "Adventures in the Forgotten Realms"; Parser.NRH_NAME = "NERDS Restoring Harmony"; Parser.MCVX_PREFIX = "Monstrous Compendium Volume "; Parser.MisMVX_PREFIX = "Misplaced Monsters: Volume "; Parser.AA_PREFIX = "Adventure Atlas: "; Parser.SOURCE_JSON_TO_FULL = {}; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CoS] = "Curse of Strahd"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DMG] = "Dungeon Master's Guide"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EEPC] = "Elemental Evil Player's Companion"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EET] = "Elemental Evil: Trinkets"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HotDQ] = "Hoard of the Dragon Queen"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LMoP] = "Lost Mine of Phandelver"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MM] = "Monster Manual"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OotA] = "Out of the Abyss"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PHB] = "Player's Handbook"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PotA] = "Princes of the Apocalypse"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RoT] = "The Rise of Tiamat"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RoTOS] = "The Rise of Tiamat Online Supplement"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCAG] = "Sword Coast Adventurer's Guide"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SKT] = "Storm King's Thunder"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToA] = "Tomb of Annihilation"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TLK] = "The Lost Kenku"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToD] = "Tyranny of Dragons"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TTP] = "The Tortle Package"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP] = Parser.TftYP_NAME; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_AtG] = `${Parser.TftYP_NAME}: Against the Giants`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_DiT] = `${Parser.TftYP_NAME}: Dead in Thay`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_TFoF] = `${Parser.TftYP_NAME}: The Forge of Fury`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_THSoT] = `${Parser.TftYP_NAME}: The Hidden Shrine of Tamoachan`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_TSC] = `${Parser.TftYP_NAME}: The Sunless Citadel`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_ToH] = `${Parser.TftYP_NAME}: Tomb of Horrors`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_WPM] = `${Parser.TftYP_NAME}: White Plume Mountain`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VGM] = "Volo's Guide to Monsters"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_XGE] = "Xanathar's Guide to Everything"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OGA] = "One Grung Above"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MTF] = "Mordenkainen's Tome of Foes"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WDH] = "Waterdeep: Dragon Heist"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WDMM] = "Waterdeep: Dungeon of the Mad Mage"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GGR] = "Guildmasters' Guide to Ravnica"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_KKW] = "Krenko's Way"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LLK] = "Lost Laboratory of Kwalish"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AZfyT] = "A Zib for your Thoughts"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GoS] = "Ghosts of Saltmarsh"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AI] = "Acquisitions Incorporated"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OoW] = "The Orrery of the Wanderer"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ESK] = "Essentials Kit"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DIP] = "Dragon of Icespire Peak"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HftT] = "Hunt for the Thessalhydra"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DC] = "Divine Contention"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SLW] = "Storm Lord's Wrath"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SDW] = "Sleeping Dragon's Wake"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BGDIA] = "Baldur's Gate: Descent Into Avernus"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LR] = "Locathah Rising"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AL] = "Adventurers' League"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SAC] = "Sage Advice Compendium"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ERLW] = "Eberron: Rising from the Last War"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EFR] = "Eberron: Forgotten Relics"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RMBRE] = "The Lost Dungeon of Rickedness: Big Rick Energy"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RMR] = "Dungeons & Dragons vs. Rick and Morty: Basic Rules"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MFF] = "Mordenkainen's Fiendish Folio"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AWM] = "Adventure with Muk"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_IMR] = "Infernal Machine Rebuild"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SADS] = "Sapphire Anniversary Dice Set"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW] = "Explorer's Guide to Wildemount"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_ToR] = "Tide of Retribution"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_DD] = "Dangerous Designs"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_FS] = "Frozen Sick"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_US] = "Unwelcome Spirits"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MOT] = "Mythic Odysseys of Theros"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_IDRotF] = "Icewind Dale: Rime of the Frostmaiden"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TCE] = "Tasha's Cauldron of Everything"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VRGR] = "Van Richten's Guide to Ravenloft"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HoL] = "The House of Lament"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RtG] = "Return to Glory"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR] = Parser.AitFR_NAME; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_ISF] = `${Parser.AitFR_NAME}: In Scarlet Flames`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_THP] = `${Parser.AitFR_NAME}: The Hidden Page`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_AVT] = `${Parser.AitFR_NAME}: A Verdant Tomb`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_DN] = `${Parser.AitFR_NAME}: Deepest Night`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_FCD] = `${Parser.AitFR_NAME}: From Cyan Depths`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WBtW] = "The Wild Beyond the Witchlight"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoD] = "Domains of Delight"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MaBJoV] = "Minsc and Boo's Journal of Villainy"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_FTD] = "Fizban's Treasury of Dragons"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC] = "Strixhaven: A Curriculum of Chaos"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_CK] = "Campus Kerfuffle"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_HfMT] = "Hunt for Mage Tower"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_TMM] = "The Magister's Masquerade"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_ARiR] = "A Reckoning in Ruins"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MPMM] = "Mordenkainen Presents: Monsters of the Multiverse"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CRCotN] = "Critical Role: Call of the Netherdeep"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_JttRC] = "Journeys through the Radiant Citadel"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SAiS] = "Spelljammer: Adventures in Space"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AAG] = "Astral Adventurer's Guide"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BAM] = "Boo's Astral Menagerie"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LoX] = "Light of Xaryxis"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoSI] = "Dragons of Stormwreck Isle"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DSotDQ] = "Dragonlance: Shadow of the Dragon Queen"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_KftGV] = "Keys from the Golden Vault"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BGG] = "Bigby Presents: Glory of the Giants"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TDCSR] = "Tal'Dorei Campaign Setting Reborn"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PaBTSO] = "Phandelver and Below: The Shattered Obelisk"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PAitM] = "Planescape: Adventures in the Multiverse"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SatO] = "Sigil and the Outlands"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToFW] = "Turn of Fortune's Wheel"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MPP] = "Morte's Planar Parade"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BMT] = "The Book of Many Things"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GHLoE] = "Grim Hollow: Lairs of Etharis"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoDk] = "Dungeons of Drakkenheim"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN] = "Dungeon Master's Screen"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_WILDERNESS_KIT] = "Dungeon Master's Screen: Wilderness Kit"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_DUNGEON_KIT] = "Dungeon Master's Screen: Dungeon Kit"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_SPELLJAMMER] = "Dungeon Master's Screen: Spelljammer"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HF] = "Heroes' Feast"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HFFotM] = "Heroes' Feast: Flavors of the Multiverse"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HFStCM] = "Heroes' Feast: Saving the Childrens Menu"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CM] = "Candlekeep Mysteries"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH] = Parser.NRH_NAME; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_TCMC] = `${Parser.NRH_NAME}: The Candy Mountain Caper`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AVitW] = `${Parser.NRH_NAME}: A Voice in the Wilderness`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_ASS] = `${Parser.NRH_NAME}: A Sticky Situation`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_CoI] = `${Parser.NRH_NAME}: Circus of Illusions`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_TLT] = `${Parser.NRH_NAME}: The Lost Tomb`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AWoL] = `${Parser.NRH_NAME}: A Web of Lies`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AT] = `${Parser.NRH_NAME}: Adventure Together`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MGELFT] = "Muk's Guide To Everything He Learned From Tasha"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VD] = "Vecna Dossier"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SjA] = "Spelljammer Academy"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HAT_TG] = "Honor Among Thieves: Thieves' Gallery"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HAT_LMI] = "Honor Among Thieves: Legendary Magic Items"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GotSF] = "Giants of the Star Forge"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LK] = "Lightning Keep"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CoA] = "Chains of Asmodeus"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PiP] = "Peril in Pinebrook"; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALCoS] = `${Parser.AL_PREFIX}Curse of Strahd`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALEE] = `${Parser.AL_PREFIX}Elemental Evil`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALRoD] = `${Parser.AL_PREFIX}Rage of Demons`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSA] = `${Parser.PS_PREFIX}Amonkhet`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSI] = `${Parser.PS_PREFIX}Innistrad`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSK] = `${Parser.PS_PREFIX}Kaladesh`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSZ] = `${Parser.PS_PREFIX}Zendikar`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSX] = `${Parser.PS_PREFIX}Ixalan`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSD] = `${Parser.PS_PREFIX}Dominaria`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_XMtS] = `X Marks the Spot`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_UATMC] = `${Parser.UA_PREFIX}The Mystic Class`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV1SC] = `${Parser.MCVX_PREFIX}1: Spelljammer Creatures`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV2DC] = `${Parser.MCVX_PREFIX}2: Dragonlance Creatures`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV3MC] = `${Parser.MCVX_PREFIX}3: Minecraft Creatures`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV4EC] = `${Parser.MCVX_PREFIX}4: Eldraine Creatures`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MisMV1] = `${Parser.MisMVX_PREFIX}1`; Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AATM] = `${Parser.AA_PREFIX}The Mortuary`; Parser.SOURCE_JSON_TO_ABV = {}; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CoS] = "CoS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DMG] = "DMG"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EEPC] = "EEPC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EET] = "EET"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HotDQ] = "HotDQ"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LMoP] = "LMoP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MM] = "MM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OotA] = "OotA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PHB] = "PHB"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PotA] = "PotA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RoT] = "RoT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RoTOS] = "RoTOS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCAG] = "SCAG"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SKT] = "SKT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToA] = "ToA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TLK] = "TLK"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToD] = "ToD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TTP] = "TTP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_AtG] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_DiT] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_TFoF] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_THSoT] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_TSC] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_ToH] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_WPM] = "TftYP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VGM] = "VGM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_XGE] = "XGE"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OGA] = "OGA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MTF] = "MTF"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WDH] = "WDH"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WDMM] = "WDMM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GGR] = "GGR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_KKW] = "KKW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LLK] = "LLK"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AZfyT] = "AZfyT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GoS] = "GoS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AI] = "AI"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OoW] = "OoW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ESK] = "ESK"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DIP] = "DIP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HftT] = "HftT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DC] = "DC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SLW] = "SLW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SDW] = "SDW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BGDIA] = "BGDIA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LR] = "LR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AL] = "AL"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SAC] = "SAC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ERLW] = "ERLW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EFR] = "EFR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RMBRE] = "RMBRE"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RMR] = "RMR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MFF] = "MFF"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AWM] = "AWM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_IMR] = "IMR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SADS] = "SADS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW] = "EGW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_ToR] = "ToR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_DD] = "DD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_FS] = "FS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_US] = "US"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MOT] = "MOT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_IDRotF] = "IDRotF"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TCE] = "TCE"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VRGR] = "VRGR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HoL] = "HoL"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RtG] = "RtG"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR] = "AitFR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_ISF] = "AitFR-ISF"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_THP] = "AitFR-THP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_AVT] = "AitFR-AVT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_DN] = "AitFR-DN"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_FCD] = "AitFR-FCD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WBtW] = "WBtW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoD] = "DoD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MaBJoV] = "MaBJoV"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_FTD] = "FTD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC] = "SCC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_CK] = "SCC-CK"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_HfMT] = "SCC-HfMT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_TMM] = "SCC-TMM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_ARiR] = "SCC-ARiR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MPMM] = "MPMM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CRCotN] = "CRCotN"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_JttRC] = "JttRC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SAiS] = "SAiS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AAG] = "AAG"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BAM] = "BAM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LoX] = "LoX"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoSI] = "DoSI"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DSotDQ] = "DSotDQ"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_KftGV] = "KftGV"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BGG] = "BGG"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TDCSR] = "TDCSR"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PaBTSO] = "PaBTSO"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PAitM] = "PAitM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SatO] = "SatO"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToFW] = "ToFW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MPP] = "MPP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BMT] = "BMT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GHLoE] = "GHLoE"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoDk] = "DoDk"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN] = "Screen"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_WILDERNESS_KIT] = "ScWild"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_DUNGEON_KIT] = "ScDun"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_SPELLJAMMER] = "ScSJ"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HF] = "HF"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HFFotM] = "HFFotM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HFStCM] = "HFStCM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CM] = "CM"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH] = "NRH"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_TCMC] = "NRH-TCMC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AVitW] = "NRH-AVitW"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_ASS] = "NRH-ASS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_CoI] = "NRH-CoI"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_TLT] = "NRH-TLT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AWoL] = "NRH-AWoL"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AT] = "NRH-AT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MGELFT] = "MGELFT"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VD] = "VD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SjA] = "SjA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HAT_TG] = "HAT-TG"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HAT_LMI] = "HAT-LMI"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GotSF] = "GotSF"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LK] = "LK"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CoA] = "CoA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PiP] = "PiP"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALCoS] = "ALCoS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALEE] = "ALEE"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALRoD] = "ALRoD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSA] = "PSA"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSI] = "PSI"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSK] = "PSK"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSZ] = "PSZ"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSX] = "PSX"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSD] = "PSD"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_XMtS] = "XMtS"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_UATMC] = "UAMy"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV1SC] = "MCV1SC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV2DC] = "MCV2DC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV3MC] = "MCV3MC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV4EC] = "MCV4EC"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MisMV1] = "MisMV1"; Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AATM] = "AATM"; Parser.SOURCE_JSON_TO_DATE = {}; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CoS] = "2016-03-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DMG] = "2014-12-09"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EEPC] = "2015-03-10"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EET] = "2015-03-10"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HotDQ] = "2014-08-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LMoP] = "2014-07-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MM] = "2014-09-30"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OotA] = "2015-09-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PHB] = "2014-08-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PotA] = "2015-04-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RoT] = "2014-11-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RoTOS] = "2014-11-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCAG] = "2015-11-03"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SKT] = "2016-09-06"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToA] = "2017-09-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TLK] = "2017-11-28"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToD] = "2019-10-22"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TTP] = "2017-09-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_AtG] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_DiT] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_TFoF] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_THSoT] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_TSC] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_ToH] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_WPM] = "2017-04-04"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VGM] = "2016-11-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_XGE] = "2017-11-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OGA] = "2017-10-11"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MTF] = "2018-05-29"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WDH] = "2018-09-18"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WDMM] = "2018-11-20"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GGR] = "2018-11-20"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_KKW] = "2018-11-20"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LLK] = "2018-11-10"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AZfyT] = "2019-03-05"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GoS] = "2019-05-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AI] = "2019-06-18"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OoW] = "2019-06-18"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ESK] = "2019-06-24"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DIP] = "2019-06-24"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HftT] = "2019-05-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DC] = "2019-06-24"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SLW] = "2019-06-24"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SDW] = "2019-06-24"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BGDIA] = "2019-09-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LR] = "2019-09-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SAC] = "2019-01-31"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ERLW] = "2019-11-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EFR] = "2019-11-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RMBRE] = "2019-11-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RMR] = "2019-11-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MFF] = "2019-11-12"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AWM] = "2019-11-12"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_IMR] = "2019-11-12"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SADS] = "2019-12-12"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW] = "2020-03-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_ToR] = "2020-03-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_DD] = "2020-03-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_FS] = "2020-03-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_US] = "2020-03-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MOT] = "2020-06-02"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_IDRotF] = "2020-09-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TCE] = "2020-11-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VRGR] = "2021-05-18"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HoL] = "2021-05-18"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RtG] = "2021-05-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR] = "2021-06-30"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_ISF] = "2021-06-30"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_THP] = "2021-07-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_AVT] = "2021-07-14"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_DN] = "2021-07-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_FCD] = "2021-07-28"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WBtW] = "2021-09-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoD] = "2021-09-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MaBJoV] = "2021-10-05"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_FTD] = "2021-11-26"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC] = "2021-12-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_CK] = "2021-12-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_HfMT] = "2021-12-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_TMM] = "2021-12-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_ARiR] = "2021-12-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MPMM] = "2022-01-25"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CRCotN] = "2022-03-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_JttRC] = "2022-07-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SAiS] = "2022-08-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AAG] = "2022-08-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BAM] = "2022-08-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LoX] = "2022-08-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoSI] = "2022-07-31"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DSotDQ] = "2022-11-22"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_KftGV] = "2023-02-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BGG] = "2023-08-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TDCSR] = "2022-01-18"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PaBTSO] = "2023-09-19"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PAitM] = "2023-10-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SatO] = "2023-10-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToFW] = "2023-10-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MPP] = "2023-10-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BMT] = "2023-11-14"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GHLoE] = "2023-11-30"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoDk] = "2023-12-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN] = "2015-01-20"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_WILDERNESS_KIT] = "2020-11-17"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_DUNGEON_KIT] = "2020-09-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_SPELLJAMMER] = "2022-08-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HF] = "2020-10-27"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HFFotM] = "2023-11-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HFStCM] = "2023-11-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CM] = "2021-03-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_TCMC] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AVitW] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_ASS] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_CoI] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_TLT] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AWoL] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AT] = "2021-09-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MGELFT] = "2020-12-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VD] = "2022-06-09"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SjA] = "2022-07-11"; // pt1; pt2 2022-07-18; pt3 2022-07-25; pt4 2022-08-01 Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HAT_TG] = "2023-03-06"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HAT_LMI] = "2023-03-31"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GotSF] = "2023-08-01"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LK] = "2023-09-26"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CoA] = "2023-10-30"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PiP] = "2023-11-20"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALCoS] = "2016-03-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALEE] = "2015-04-07"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALRoD] = "2015-09-15"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSA] = "2017-07-06"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSI] = "2016-07-12"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSK] = "2017-02-16"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSZ] = "2016-04-27"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSX] = "2018-01-09"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSD] = "2018-07-31"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_XMtS] = "2017-12-11"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_UATMC] = "2017-03-13"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV1SC] = "2022-04-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV2DC] = "2022-12-05"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV3MC] = "2023-03-28"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV4EC] = "2023-09-21"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MisMV1] = "2023-05-03"; Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AATM] = "2023-10-17"; Parser.SOURCES_ADVENTURES = new Set([ Parser.SRC_LMoP, Parser.SRC_HotDQ, Parser.SRC_RoT, Parser.SRC_RoTOS, Parser.SRC_PotA, Parser.SRC_OotA, Parser.SRC_CoS, Parser.SRC_SKT, Parser.SRC_TYP, Parser.SRC_TYP_AtG, Parser.SRC_TYP_DiT, Parser.SRC_TYP_TFoF, Parser.SRC_TYP_THSoT, Parser.SRC_TYP_TSC, Parser.SRC_TYP_ToH, Parser.SRC_TYP_WPM, Parser.SRC_ToA, Parser.SRC_TLK, Parser.SRC_TTP, Parser.SRC_WDH, Parser.SRC_LLK, Parser.SRC_WDMM, Parser.SRC_KKW, Parser.SRC_AZfyT, Parser.SRC_GoS, Parser.SRC_HftT, Parser.SRC_OoW, Parser.SRC_DIP, Parser.SRC_SLW, Parser.SRC_SDW, Parser.SRC_DC, Parser.SRC_BGDIA, Parser.SRC_LR, Parser.SRC_EFR, Parser.SRC_RMBRE, Parser.SRC_IMR, Parser.SRC_EGW_ToR, Parser.SRC_EGW_DD, Parser.SRC_EGW_FS, Parser.SRC_EGW_US, Parser.SRC_IDRotF, Parser.SRC_CM, Parser.SRC_HoL, Parser.SRC_XMtS, Parser.SRC_RtG, Parser.SRC_AitFR, Parser.SRC_AitFR_ISF, Parser.SRC_AitFR_THP, Parser.SRC_AitFR_AVT, Parser.SRC_AitFR_DN, Parser.SRC_AitFR_FCD, Parser.SRC_WBtW, Parser.SRC_NRH, Parser.SRC_NRH_TCMC, Parser.SRC_NRH_AVitW, Parser.SRC_NRH_ASS, Parser.SRC_NRH_CoI, Parser.SRC_NRH_TLT, Parser.SRC_NRH_AWoL, Parser.SRC_NRH_AT, Parser.SRC_SCC, Parser.SRC_SCC_CK, Parser.SRC_SCC_HfMT, Parser.SRC_SCC_TMM, Parser.SRC_SCC_ARiR, Parser.SRC_CRCotN, Parser.SRC_JttRC, Parser.SRC_SjA, Parser.SRC_LoX, Parser.SRC_DoSI, Parser.SRC_DSotDQ, Parser.SRC_KftGV, Parser.SRC_GotSF, Parser.SRC_PaBTSO, Parser.SRC_LK, Parser.SRC_CoA, Parser.SRC_PiP, Parser.SRC_HFStCM, Parser.SRC_GHLoE, Parser.SRC_DoDk, Parser.SRC_AWM, ]); Parser.SOURCES_CORE_SUPPLEMENTS = new Set(Object.keys(Parser.SOURCE_JSON_TO_FULL).filter(it => !Parser.SOURCES_ADVENTURES.has(it))); Parser.SOURCES_NON_STANDARD_WOTC = new Set([ Parser.SRC_OGA, Parser.SRC_LLK, Parser.SRC_AZfyT, Parser.SRC_LR, Parser.SRC_TLK, Parser.SRC_TTP, Parser.SRC_AWM, Parser.SRC_IMR, Parser.SRC_SADS, Parser.SRC_MFF, Parser.SRC_XMtS, Parser.SRC_RtG, Parser.SRC_AitFR, Parser.SRC_AitFR_ISF, Parser.SRC_AitFR_THP, Parser.SRC_AitFR_AVT, Parser.SRC_AitFR_DN, Parser.SRC_AitFR_FCD, Parser.SRC_DoD, Parser.SRC_MaBJoV, Parser.SRC_NRH, Parser.SRC_NRH_TCMC, Parser.SRC_NRH_AVitW, Parser.SRC_NRH_ASS, Parser.SRC_NRH_CoI, Parser.SRC_NRH_TLT, Parser.SRC_NRH_AWoL, Parser.SRC_NRH_AT, Parser.SRC_MGELFT, Parser.SRC_VD, Parser.SRC_SjA, Parser.SRC_HAT_TG, Parser.SRC_HAT_LMI, Parser.SRC_GotSF, Parser.SRC_MCV3MC, Parser.SRC_MCV4EC, Parser.SRC_MisMV1, Parser.SRC_LK, Parser.SRC_AATM, Parser.SRC_CoA, Parser.SRC_PiP, Parser.SRC_HFStCM, ]); Parser.SOURCES_PARTNERED_WOTC = new Set([ Parser.SRC_RMBRE, Parser.SRC_RMR, Parser.SRC_EGW, Parser.SRC_EGW_ToR, Parser.SRC_EGW_DD, Parser.SRC_EGW_FS, Parser.SRC_EGW_US, Parser.SRC_CRCotN, Parser.SRC_TDCSR, Parser.SRC_HftT, Parser.SRC_GHLoE, Parser.SRC_DoDk, ]); // region Source categories // An opinionated set of source that could be considered "core-core" Parser.SOURCES_VANILLA = new Set([ Parser.SRC_DMG, Parser.SRC_MM, Parser.SRC_PHB, Parser.SRC_SCAG, // Parser.SRC_TTP, // "Legacy" source, removed in favor of MPMM // Parser.SRC_VGM, // "Legacy" source, removed in favor of MPMM Parser.SRC_XGE, // Parser.SRC_MTF, // "Legacy" source, removed in favor of MPMM Parser.SRC_SAC, Parser.SRC_MFF, Parser.SRC_SADS, Parser.SRC_TCE, Parser.SRC_FTD, Parser.SRC_MPMM, Parser.SRC_SCREEN, Parser.SRC_SCREEN_WILDERNESS_KIT, Parser.SRC_SCREEN_DUNGEON_KIT, Parser.SRC_VD, Parser.SRC_GotSF, Parser.SRC_BGG, Parser.SRC_MaBJoV, Parser.SRC_CoA, Parser.SRC_BMT, ]); // Any opinionated set of sources that are """hilarious, dude""" Parser.SOURCES_COMEDY = new Set([ Parser.SRC_AI, Parser.SRC_OoW, Parser.SRC_RMR, Parser.SRC_RMBRE, Parser.SRC_HftT, Parser.SRC_AWM, Parser.SRC_MGELFT, Parser.SRC_HAT_TG, Parser.SRC_HAT_LMI, Parser.SRC_MCV3MC, Parser.SRC_MisMV1, Parser.SRC_LK, Parser.SRC_PiP, ]); // Any opinionated set of sources that are "other settings" Parser.SOURCES_NON_FR = new Set([ Parser.SRC_GGR, Parser.SRC_KKW, Parser.SRC_ERLW, Parser.SRC_EFR, Parser.SRC_EGW, Parser.SRC_EGW_ToR, Parser.SRC_EGW_DD, Parser.SRC_EGW_FS, Parser.SRC_EGW_US, Parser.SRC_MOT, Parser.SRC_XMtS, Parser.SRC_AZfyT, Parser.SRC_SCC, Parser.SRC_SCC_CK, Parser.SRC_SCC_HfMT, Parser.SRC_SCC_TMM, Parser.SRC_SCC_ARiR, Parser.SRC_CRCotN, Parser.SRC_SjA, Parser.SRC_SAiS, Parser.SRC_AAG, Parser.SRC_BAM, Parser.SRC_LoX, Parser.SRC_DSotDQ, Parser.SRC_TDCSR, Parser.SRC_PAitM, Parser.SRC_SatO, Parser.SRC_ToFW, Parser.SRC_MPP, Parser.SRC_MCV4EC, Parser.SRC_LK, Parser.SRC_GHLoE, Parser.SRC_DoDk, ]); // endregion Parser.SOURCES_AVAILABLE_DOCS_BOOK = {}; [ Parser.SRC_PHB, Parser.SRC_MM, Parser.SRC_DMG, Parser.SRC_SCAG, Parser.SRC_VGM, Parser.SRC_OGA, Parser.SRC_XGE, Parser.SRC_MTF, Parser.SRC_GGR, Parser.SRC_AI, Parser.SRC_ERLW, Parser.SRC_RMR, Parser.SRC_EGW, Parser.SRC_MOT, Parser.SRC_TCE, Parser.SRC_VRGR, Parser.SRC_DoD, Parser.SRC_MaBJoV, Parser.SRC_FTD, Parser.SRC_SCC, Parser.SRC_MPMM, Parser.SRC_AAG, Parser.SRC_BAM, Parser.SRC_HAT_TG, Parser.SRC_SCREEN, Parser.SRC_SCREEN_WILDERNESS_KIT, Parser.SRC_SCREEN_DUNGEON_KIT, Parser.SRC_SCREEN_SPELLJAMMER, Parser.SRC_BGG, Parser.SRC_TDCSR, Parser.SRC_SatO, Parser.SRC_MPP, Parser.SRC_HF, Parser.SRC_HFFotM, Parser.SRC_BMT, ].forEach(src => { Parser.SOURCES_AVAILABLE_DOCS_BOOK[src] = src; Parser.SOURCES_AVAILABLE_DOCS_BOOK[src.toLowerCase()] = src; }); [ {src: Parser.SRC_PSA, id: "PS-A"}, {src: Parser.SRC_PSI, id: "PS-I"}, {src: Parser.SRC_PSK, id: "PS-K"}, {src: Parser.SRC_PSZ, id: "PS-Z"}, {src: Parser.SRC_PSX, id: "PS-X"}, {src: Parser.SRC_PSD, id: "PS-D"}, ].forEach(({src, id}) => { Parser.SOURCES_AVAILABLE_DOCS_BOOK[src] = id; Parser.SOURCES_AVAILABLE_DOCS_BOOK[src.toLowerCase()] = id; }); Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE = {}; [ Parser.SRC_LMoP, Parser.SRC_HotDQ, Parser.SRC_RoT, Parser.SRC_PotA, Parser.SRC_OotA, Parser.SRC_CoS, Parser.SRC_SKT, Parser.SRC_TYP_AtG, Parser.SRC_TYP_DiT, Parser.SRC_TYP_TFoF, Parser.SRC_TYP_THSoT, Parser.SRC_TYP_TSC, Parser.SRC_TYP_ToH, Parser.SRC_TYP_WPM, Parser.SRC_ToA, Parser.SRC_TLK, Parser.SRC_TTP, Parser.SRC_WDH, Parser.SRC_LLK, Parser.SRC_WDMM, Parser.SRC_KKW, Parser.SRC_AZfyT, Parser.SRC_GoS, Parser.SRC_HftT, Parser.SRC_OoW, Parser.SRC_DIP, Parser.SRC_SLW, Parser.SRC_SDW, Parser.SRC_DC, Parser.SRC_BGDIA, Parser.SRC_LR, Parser.SRC_EFR, Parser.SRC_RMBRE, Parser.SRC_IMR, Parser.SRC_EGW_ToR, Parser.SRC_EGW_DD, Parser.SRC_EGW_FS, Parser.SRC_EGW_US, Parser.SRC_IDRotF, Parser.SRC_CM, Parser.SRC_HoL, Parser.SRC_XMtS, Parser.SRC_RtG, Parser.SRC_AitFR_ISF, Parser.SRC_AitFR_THP, Parser.SRC_AitFR_AVT, Parser.SRC_AitFR_DN, Parser.SRC_AitFR_FCD, Parser.SRC_WBtW, Parser.SRC_NRH, Parser.SRC_NRH_TCMC, Parser.SRC_NRH_AVitW, Parser.SRC_NRH_ASS, Parser.SRC_NRH_CoI, Parser.SRC_NRH_TLT, Parser.SRC_NRH_AWoL, Parser.SRC_NRH_AT, Parser.SRC_SCC_CK, Parser.SRC_SCC_HfMT, Parser.SRC_SCC_TMM, Parser.SRC_SCC_ARiR, Parser.SRC_CRCotN, Parser.SRC_JttRC, Parser.SRC_LoX, Parser.SRC_DoSI, Parser.SRC_DSotDQ, Parser.SRC_KftGV, Parser.SRC_GotSF, Parser.SRC_PaBTSO, Parser.SRC_ToFW, Parser.SRC_LK, Parser.SRC_CoA, Parser.SRC_PiP, Parser.SRC_HFStCM, Parser.SRC_GHLoE, Parser.SRC_DoDk, ].forEach(src => { Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[src] = src; Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[src.toLowerCase()] = src; }); Parser.getTagSource = function (tag, source) { if (source && source.trim()) return source; tag = tag.trim(); const tagMeta = Renderer.tag.TAG_LOOKUP[tag]; if (!tagMeta) throw new Error(`Unhandled tag "${tag}"`); return tagMeta.defaultSource; }; Parser.PROP_TO_TAG = { "monster": "creature", "optionalfeature": "optfeature", "tableGroup": "table", "vehicleUpgrade": "vehupgrade", "baseitem": "item", "itemGroup": "item", "magicvariant": "item", }; Parser.getPropTag = function (prop) { if (Parser.PROP_TO_TAG[prop]) return Parser.PROP_TO_TAG[prop]; return prop; }; Parser.PROP_TO_DISPLAY_NAME = { "variantrule": "Variant Rule", "optionalfeature": "Option/Feature", "magicvariant": "Magic Item Variant", "baseitem": "Item (Base)", "item": "Item", "adventure": "Adventure", "adventureData": "Adventure Text", "book": "Book", "bookData": "Book Text", "makebrewCreatureTrait": "Homebrew Builder Creature Trait", "charoption": "Other Character Creation Option", "bonus": "Bonus Action", "legendary": "Legendary Action", "mythic": "Mythic Action", "lairActions": "Lair Action", "regionalEffects": "Regional Effect", }; Parser.getPropDisplayName = function (prop, {suffix = ""} = {}) { if (Parser.PROP_TO_DISPLAY_NAME[prop]) return `${Parser.PROP_TO_DISPLAY_NAME[prop]}${suffix}`; const mFluff = /Fluff$/.exec(prop); if (mFluff) return Parser.getPropDisplayName(prop.slice(0, -mFluff[0].length), {suffix: " Fluff"}); const mFoundry = /^foundry(?[A-Z].*)$/.exec(prop); if (mFoundry) return Parser.getPropDisplayName(mFoundry.groups.prop.lowercaseFirst(), {suffix: " Foundry Data"}); return `${prop.split(/([A-Z][a-z]+)/g).filter(Boolean).join(" ").uppercaseFirst()}${suffix}`; }; Parser.ITEM_TYPE_JSON_TO_ABV = { "A": "ammunition", "AF": "ammunition", "AT": "artisan's tools", "EM": "eldritch machine", "EXP": "explosive", "FD": "food and drink", "G": "adventuring gear", "GS": "gaming set", "HA": "heavy armor", "IDG": "illegal drug", "INS": "instrument", "LA": "light armor", "M": "melee weapon", "MA": "medium armor", "MNT": "mount", "MR": "master rune", "GV": "generic variant", "P": "potion", "R": "ranged weapon", "RD": "rod", "RG": "ring", "S": "shield", "SC": "scroll", "SCF": "spellcasting focus", "OTH": "other", "T": "tools", "TAH": "tack and harness", "TG": "trade good", "$": "treasure", "VEH": "vehicle (land)", "SHP": "vehicle (water)", "AIR": "vehicle (air)", "SPC": "vehicle (space)", "WD": "wand", }; Parser.DMGTYPE_JSON_TO_FULL = { "A": "acid", "B": "bludgeoning", "C": "cold", "F": "fire", "O": "force", "L": "lightning", "N": "necrotic", "P": "piercing", "I": "poison", "Y": "psychic", "R": "radiant", "S": "slashing", "T": "thunder", }; Parser.DMG_TYPES = ["acid", "bludgeoning", "cold", "fire", "force", "lightning", "necrotic", "piercing", "poison", "psychic", "radiant", "slashing", "thunder"]; Parser.CONDITIONS = ["blinded", "charmed", "deafened", "exhaustion", "frightened", "grappled", "incapacitated", "invisible", "paralyzed", "petrified", "poisoned", "prone", "restrained", "stunned", "unconscious"]; Parser.SENSES = [ {"name": "blindsight", "source": Parser.SRC_PHB}, {"name": "darkvision", "source": Parser.SRC_PHB}, {"name": "tremorsense", "source": Parser.SRC_MM}, {"name": "truesight", "source": Parser.SRC_PHB}, ]; Parser.NUMBERS_ONES = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]; Parser.NUMBERS_TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]; Parser.NUMBERS_TEENS = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"]; // region Metric conversion Parser.metric = { // See MPMB's breakdown: https://old.reddit.com/r/dndnext/comments/6gkuec MILES_TO_KILOMETRES: 1.6, FEET_TO_METRES: 0.3, // 5 ft = 1.5 m YARDS_TO_METRES: 0.9, // (as above) POUNDS_TO_KILOGRAMS: 0.5, // 2 lb = 1 kg getMetricNumber ({originalValue, originalUnit, toFixed = null}) { if (originalValue == null || isNaN(originalValue)) return originalValue; originalValue = Number(originalValue); if (!originalValue) return originalValue; let out = null; switch (originalUnit) { case "ft.": case "ft": case Parser.UNT_FEET: out = originalValue * Parser.metric.FEET_TO_METRES; break; case "yd.": case "yd": case Parser.UNT_YARDS: out = originalValue * Parser.metric.YARDS_TO_METRES; break; case "mi.": case "mi": case Parser.UNT_MILES: out = originalValue * Parser.metric.MILES_TO_KILOMETRES; break; case "lb.": case "lb": case "lbs": out = originalValue * Parser.metric.POUNDS_TO_KILOGRAMS; break; default: return originalValue; } if (toFixed != null) return NumberUtil.toFixedNumber(out, toFixed); return out; }, getMetricUnit ({originalUnit, isShortForm = false, isPlural = true}) { switch (originalUnit) { case "ft.": case "ft": case Parser.UNT_FEET: return isShortForm ? "m" : `meter`[isPlural ? "toPlural" : "toString"](); case "yd.": case "yd": case Parser.UNT_YARDS: return isShortForm ? "m" : `meter`[isPlural ? "toPlural" : "toString"](); case "mi.": case "mi": case Parser.UNT_MILES: return isShortForm ? "km" : `kilometre`[isPlural ? "toPlural" : "toString"](); case "lb.": case "lb": case "lbs": return isShortForm ? "kg" : `kilogram`[isPlural ? "toPlural" : "toString"](); default: return originalUnit; } }, }; // endregion // region Map grids Parser.MAP_GRID_TYPE_TO_FULL = {}; Parser.MAP_GRID_TYPE_TO_FULL["none"] = "None"; Parser.MAP_GRID_TYPE_TO_FULL["square"] = "Square"; Parser.MAP_GRID_TYPE_TO_FULL["hexRowsOdd"] = "Hex Rows (Odd)"; Parser.MAP_GRID_TYPE_TO_FULL["hexRowsEven"] = "Hex Rows (Even)"; Parser.MAP_GRID_TYPE_TO_FULL["hexColsOdd"] = "Hex Columns (Odd)"; Parser.MAP_GRID_TYPE_TO_FULL["hexColsEven"] = "Hex Columns (Even)"; Parser.mapGridTypeToFull = function (gridType) { return Parser._parse_aToB(Parser.MAP_GRID_TYPE_TO_FULL, gridType); }; // endregion