"use strict"; class DamageTagger { static _addDamageTypeToArray (arr, str, options) { str = str.toLowerCase().trim(); if (str === "all" || str === "one" || str === "a") arr.push(...Parser.DMG_TYPES); else if (Parser.DMG_TYPES.includes(str)) arr.push(str); else options.cbWarning(`Unknown damage type "${str}"`); } } class DamageInflictTagger extends DamageTagger { static tryRun (sp, options) { sp.damageInflict = []; JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(/(?:{@damage [^}]+}|\d+) (\w+)((?:, \w+)*)(,? or \w+)? damage/ig, (...m) => { if (m[1]) this._addDamageTypeToArray(sp.damageInflict, m[1], options); if (m[2]) m[2].split(",").map(it => it.trim()).filter(Boolean).forEach(str => this._addDamageTypeToArray(sp.damageInflict, str, options)); if (m[3]) this._addDamageTypeToArray(sp.damageInflict, m[3].split(" ").last(), options); }); if (!sp.damageInflict.length) delete sp.damageInflict; else sp.damageInflict = [...new Set(sp.damageInflict)].sort(SortUtil.ascSort); } } class DamageResVulnImmuneTagger extends DamageTagger { static tryRun (sp, prop, options) { sp[prop] = []; JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(/resistance to (\w+)((?:, \w+)*)(,? or \w+)? damage/ig, (...m) => { if (m[1]) this._addDamageTypeToArray(sp[prop], m[1], options); if (m[2]) m[2].split(",").map(it => it.trim()).filter(Boolean).forEach(str => this._addDamageTypeToArray(sp[prop], str, options)); if (m[3]) this._addDamageTypeToArray(sp[prop], m[3].split(" ").last(), options); }); if (!sp[prop].length) delete sp[prop]; else sp[prop] = [...new Set(sp[prop])].sort(SortUtil.ascSort); } } class ConditionInflictTagger { static tryRun (sp, options) { sp.conditionInflict = []; JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(/{@condition ([^}]+)}/ig, (...m) => sp.conditionInflict.push(m[1].toLowerCase())); if (!sp.conditionInflict.length) delete sp.conditionInflict; else sp.conditionInflict = [...new Set(sp.conditionInflict)].sort(SortUtil.ascSort); } } class SavingThrowTagger { static tryRun (sp, options) { sp.savingThrow = []; JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(/(Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma) saving throw/ig, (...m) => sp.savingThrow.push(m[1].toLowerCase())); if (!sp.savingThrow.length) delete sp.savingThrow; else sp.savingThrow = [...new Set(sp.savingThrow)].sort(SortUtil.ascSort); } } class AbilityCheckTagger { static tryRun (sp, options) { sp.abilityCheck = []; JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(/a (Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma) check/ig, (...m) => sp.abilityCheck.push(m[1].toLowerCase())); if (!sp.abilityCheck.length) delete sp.abilityCheck; else sp.abilityCheck = [...new Set(sp.abilityCheck)].sort(SortUtil.ascSort); } } class SpellAttackTagger { static tryRun (sp, options) { sp.spellAttack = []; JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(/make (?:a|up to [^ ]+) (ranged|melee) spell attack/ig, (...m) => sp.spellAttack.push(m[1][0].toUpperCase())); if (!sp.spellAttack.length) delete sp.spellAttack; else sp.spellAttack = [...new Set(sp.spellAttack)].sort(SortUtil.ascSort); } } // TODO areaTags class MiscTagsTagger { static _addTag ({tags, tag, options}) { if (options?.allowlistTags && !options?.allowlistTags.has(tag)) return; tags.add(tag); } static tryRun (sp, options) { const tags = new Set(sp.miscTags || []); MiscTagsTagger._WALKER = MiscTagsTagger._WALKER || MiscUtil.getWalker({isNoModification: true, keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST}); MiscTagsTagger._WALKER.walk( [sp.entries, sp.entriesHigherLevel], { string: (str) => { const stripped = Renderer.stripTags(str); if (/becomes permanent/ig.test(str)) this._addTag({tags, tag: "PRM", options}); if (/when you reach/ig.test(str)) this._addTag({tags, tag: "SCL", options}); if ((/regain|restore/ig.test(str) && /hit point/ig.test(str)) || /heal/ig.test(str)) this._addTag({tags, tag: "HL", options}); if (/temporary hit points/ig.test(str)) this._addTag({tags, tag: "THP", options}); if (/you summon/ig.test(str) || /creature shares your initiative count/ig.test(str)) this._addTag({tags, tag: "SMN", options}); if (/you can see/ig.test(str)) this._addTag({tags, tag: "SGT", options}); if (/you (?:can then )?teleport/i.test(str) || /instantly (?:transports you|teleport)/i.test(str) || /enters(?:[^.]+)portal instantly/i.test(str) || /entering the portal exits from the other portal/i.test(str)) this._addTag({tags, tag: "TP", options}); if ((str.includes("bonus") || str.includes("penalty")) && str.includes("AC")) this._addTag({tags, tag: "MAC", options}); if (/target's (?:base )?AC becomes/.exec(str)) this._addTag({tags, tag: "MAC", options}); if (/target's AC can't be less than/.exec(str)) this._addTag({tags, tag: "MAC", options}); if (/(?:^|\W)(?:pull(?:|ed|s)|push(?:|ed|s)) [^.!?:]*\d+\s+(?:ft|feet|foot|mile|square)/ig.test(str)) this._addTag({tags, tag: "FMV", options}); if (/rolls? (?:a )?{@dice [^}]+} and consults? the table/.test(str)) this._addTag({tags, tag: "RO", options}); if ((/\bbright light\b/i.test(str) || /\bdim light\b/i.test(str)) && /\b\d+[- ]foot[- ]radius\b/i.test(str)) { if (/\bsunlight\b/.test(str)) this._addTag({tags, tag: "LGTS", options}); else this._addTag({tags, tag: "LGT", options}); } if (/\bbonus action\b/i.test(str)) this._addTag({tags, tag: "UBA", options}); if (/\b(?:lightly|heavily) obscured\b/i.test(str)) this._addTag({tags, tag: "OBS", options}); if (/\b(?:is|creates an area of|becomes?) difficult terrain\b/i.test(Renderer.stripTags(str)) || /spends? \d+ (?:feet|foot) of movement for every 1 foot/.test(str)) this._addTag({tags, tag: "DFT", options}); if ( /\battacks? deals? an extra\b[^.!?]+\bdamage\b/.test(str) || /\bdeals? an extra\b[^.!?]+\bdamage\b[^.!?]+\b(?:weapon attack|when it hits)\b/.test(str) || /weapon attacks?\b[^.!?]+\b(?:takes an extra|deal an extra)\b[^.!?]+\bdamage/.test(str) ) this._addTag({tags, tag: "AAD", options}); if ( /\b(?:any|one|a) creatures? or objects?\b/i.test(str) || /\b(?:flammable|nonmagical|metal|unsecured) objects?\b/.test(str) || /\bobjects?\b[^.!?]+\b(?:created by magic|(?:that )?you touch|that is neither held nor carried)\b/.test(str) || /\bobject\b[^.!?]+\bthat isn't being worn or carried\b/.test(str) || /\bobjects? (?:of your choice|that is familiar to you|of (?:Tiny|Small|Medium|Large|Huge|Gargantuan) size)\b/.test(str) || /\b(?:Tiny|Small|Medium|Large|Huge|Gargantuan) or smaller object\b/.test(str) || /\baffected by this spell, the object is\b/.test(str) || /\ball creatures and objects\b/i.test(str) || /\ba(?:ny|n)? (?:(?:willing|visible|affected) )?(?:creature|place) or an object\b/i.test(str) || /\bone creature, object, or magical effect\b/i.test(str) || /\ba person, place, or object\b/i.test(str) || /\b(choose|touch|manipulate|soil) (an|one) object\b/i.test(str) ) this._addTag({tags, tag: "OBJ", options}); if ( /\b(?:and(?: it)?|each target|the( [a-z]+)+) (?:also )?(?:has|gains) advantage\b/i.test(stripped) || /\bcreature in the area (?:[^.!?]+ )?has advantage\b/i.test(stripped) || /\broll(?:made )? against (?:an affected creature|this target) (?:[^.!?]+ )?has advantage\b/i.test(stripped) || /\bother creatures? have advantage on(?:[^.!?]+ )? rolls\b/i.test(stripped) || /\byou (?:have|gain|(?:can )?give yourself) advantage\b/i.test(stripped) || /\b(?:has|have) advantage on (?:Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma|all)\b/i.test(stripped) || /\bmakes? (?:all )?(?:Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma) saving throws with advantage\b/i.test(stripped) ) this._addTag({tags, tag: "ADV", options}); }, object: (obj) => { if (obj.type !== "table") return; const rollMode = Renderer.table.getAutoConvertedRollMode(obj); if (rollMode !== RollerUtil.ROLL_COL_NONE) this._addTag({tags, tag: "RO", options}); }, }, ); sp.miscTags = [...tags].sort(SortUtil.ascSortLower); if (!sp.miscTags.length) delete sp.miscTags; } } MiscTagsTagger._WALKER = null; class ScalingLevelDiceTagger { static _WALKER_BOR = MiscUtil.getWalker({keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, isNoModification: true, isBreakOnReturn: true}); static _isParseFirstSecondLineRolls ({sp}) { // Two "flat" paragraphs; first is spell text, second is cantrip scaling text if (!sp.entriesHigherLevel) return sp.entries.length === 2 && sp.entries.filter(it => typeof it === "string").length === 2; // One paragraph of spell text; one e.g. "Cantrip Upgrade" header with one paragraph of cantrip scaling text return sp.entries.length === 1 && typeof sp.entries[0] === "string" && sp.entriesHigherLevel.length === 1 && sp.entriesHigherLevel[0].type === "entries" && sp.entriesHigherLevel[0].entries?.length === 1 && typeof sp.entriesHigherLevel[0].entries[0] === "string"; } static _getRollsFirstSecondLine ({firstLine, secondLine}) { const rollsFirstLine = []; const rollsSecondLine = []; firstLine.replace(/{@(?:damage|dice) ([^}]+)}/g, (...m) => { rollsFirstLine.push(m[1].split("|")[0]); }); secondLine.replace(/\({@(?:damage|dice) ([^}]+)}\)/g, (...m) => { rollsSecondLine.push(m[1].split("|")[0]); }); return {rollsFirstLine, rollsSecondLine}; } static _RE_DAMAGE_TYPE = new RegExp(`\\b${ConverterConst.STR_RE_DAMAGE_TYPE}\\b`, "i"); static _getLabel ({sp, options}) { let label; const handlers = { string: str => { const mDamageType = this._RE_DAMAGE_TYPE.exec(str); if (mDamageType) { label = `${mDamageType[1]} damage`; return true; } }, }; if (sp.entriesHigherLevel) { this._WALKER_BOR.walk(sp.entriesHigherLevel, handlers); if (label) return label; } this._WALKER_BOR.walk(sp.entries, handlers); if (label) return label; options.cbWarning(`${sp.name ? `(${sp.name}) ` : ""}Could not create scalingLevelDice label!`); return "NO_LABEL"; } static tryRun (sp, options) { if (sp.level !== 0) return; // Prefer `entriesHigherLevel`, as we may have e.g. a `"Cantrip Upgrade"` header const strEntries = JSON.stringify(sp.entriesHigherLevel || sp.entries); const rolls = []; strEntries.replace(/{@(?:damage|dice) ([^}]+)}/g, (...m) => { rolls.push(m[1].split("|")[0]); }); if ((rolls.length === 4 && strEntries.includes("one die")) || rolls.length === 5) { if (rolls.length === 5 && rolls[0] !== rolls[1]) options.cbWarning(`${sp.name ? `(${sp.name}) ` : ""}scalingLevelDice rolls may require manual checking--mismatched roll number of rolls!`); sp.scalingLevelDice = { label: this._getLabel({sp, options}), scaling: rolls.length === 4 ? { 1: rolls[0], 5: rolls[1], 11: rolls[2], 17: rolls[3], } : { 1: rolls[0], 5: rolls[2], 11: rolls[3], 17: rolls[4], }, }; return; } if (this._isParseFirstSecondLineRolls({sp})) { const {rollsFirstLine, rollsSecondLine} = this._getRollsFirstSecondLine({ firstLine: sp.entries[0], secondLine: sp.entriesHigherLevel ? sp.entriesHigherLevel[0].entries[0] : sp.entries[1], }); if (rollsFirstLine.length >= 1 && rollsSecondLine.length >= 3) { if (rollsFirstLine.length > 1 || rollsSecondLine.length > 3) { options.cbWarning(`${sp.name ? `(${sp.name}) ` : ""}scalingLevelDice rolls may require manual checking--too many dice parts!`); } const label = this._getLabel({sp, options}); sp.scalingLevelDice = { label: label, scaling: { 1: rollsFirstLine[0], 5: rollsSecondLine[0], 11: rollsSecondLine[1], 17: rollsSecondLine[2], }, }; } } } } class AffectedCreatureTypeTagger { static tryRun (sp, options) { const setAffected = new Set(); const setNotAffected = new Set(); const walker = MiscUtil.getWalker({isNoModification: true}); walker.walk( sp.entries, { string: (str) => { str = Renderer.stripTags(str); const sens = str.split(/[.!?]/g); sens.forEach(sen => { // region Not affected sen // Blight :: PHB .replace(/This spell has no effect on (.+)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Command :: PHB .replace(/The spell has no effect if the target is (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Raise Dead :: PHB .replace(/The spell can't return an (.*?) creature/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Shapechange :: PHB .replace(/The creature can't be (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Sleep :: PHB .replace(/(.*?) aren't affected by this spell/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Speak with Dead :: PHB .replace(/The corpse\b.*?\bcan't be (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Cause Fear :: XGE .replace(/A (.*?) is immune to this effect/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) // Healing Spirit :: XGE .replace(/can't heal (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setNotAffected, type: n[1]})); }) ; // endregion // region Affected sen // Awaken :: PHB .replace(/you touch a [^ ]+ or (?:smaller|larger) (.+)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Calm Emotions :: PHB .replace(/Each (.+) in a \d+-foot/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Charm Person :: PHB .replace(/One (.*?) of your choice/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Crown of Madness :: PHB .replace(/You attempt to .* a (.+) you can see/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Detect Evil and Good :: PHB .replace(/you know if there is an? (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Dispel Evil and Good :: PHB .replace(/For the duration, (.*?) have disadvantage/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Hold Person :: PHB .replace(/Choose (.+)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Locate Animals or Plants :: PHB .replace(/name a specific kind of (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Magic Jar :: PHB .replace(/You can attempt to possess any (.*?) that you can see/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Planar Binding :: PHB .replace(/you attempt to bind a (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Protection from Evil and Good :: PHB .replace(/types of creatures: (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Reincarnate :: PHB .replace(/You touch a dead (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Simulacrum :: PHB .replace(/You shape an illusory duplicate of one (.*)/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Speak with Animals :: PHB .replace(/communicate with (.*?) for the duration/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Fast Friends :: AI .replace(/choose one (.*?) within range/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Beast Bond :: XGE .replace(/telepathic link with one (.*?) you touch/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Ceremony :: XGE .replace(/You touch one (.*?) who/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) // Soul Cage :: XGE .replace(/\bsoul of (.*?) as it dies/g, (...m) => { m[1].replace(AffectedCreatureTypeTagger._RE_TYPES, (...n) => this._doAddType({set: setAffected, type: n[1]})); }) ; // endregion }); }, }, ); if (!setAffected.size && !setNotAffected.size) return; const setAffectedOut = new Set([ ...(sp.affectsCreatureType || []), ...setAffected, ]); if (!setAffectedOut.size) Parser.MON_TYPES.forEach(it => setAffectedOut.add(it)); sp.affectsCreatureType = [...CollectionUtil.setDiff(setAffectedOut, setNotAffected)].sort(SortUtil.ascSortLower); if (!sp.affectsCreatureType.length) delete sp.affectsCreatureType; } static _doAddType ({set, type}) { type = Parser._parse_bToA(Parser.MON_TYPE_TO_PLURAL, type, type); set.add(type); return ""; } } AffectedCreatureTypeTagger._RE_TYPES = new RegExp(`\\b(${[...Parser.MON_TYPES, ...Object.values(Parser.MON_TYPE_TO_PLURAL)].map(it => it.escapeRegexp()).join("|")})\\b`, "gi"); globalThis.DamageInflictTagger = DamageInflictTagger; globalThis.DamageResVulnImmuneTagger = DamageResVulnImmuneTagger; globalThis.ConditionInflictTagger = ConditionInflictTagger; globalThis.SavingThrowTagger = SavingThrowTagger; globalThis.AbilityCheckTagger = AbilityCheckTagger; globalThis.SpellAttackTagger = SpellAttackTagger; globalThis.MiscTagsTagger = MiscTagsTagger; globalThis.ScalingLevelDiceTagger = ScalingLevelDiceTagger; globalThis.AffectedCreatureTypeTagger = AffectedCreatureTypeTagger;