Files
5etools-mirror-2.github.io/js/converterutils-entries.js
TheGiddyLimit 8117ebddc5 v1.198.1
2024-01-01 19:34:49 +00:00

638 lines
16 KiB
JavaScript

"use strict";
const LAST_KEY_ALLOWLIST = new Set([
"entries",
"entry",
"items",
"entriesHigherLevel",
"rows",
"row",
"fluff",
]);
class TagJsons {
static async pInit ({spells}) {
TagCondition.init();
SpellTag.init(spells);
await ItemTag.pInit();
await FeatTag.pInit();
await AdventureBookTag.pInit();
}
static mutTagObject (json, {keySet, isOptimistic = true, creaturesToTag = null} = {}) {
TagJsons.OPTIMISTIC = isOptimistic;
const fnCreatureTagSpecific = CreatureTag.getFnTryRunSpecific(creaturesToTag);
Object.keys(json)
.forEach(k => {
if (keySet != null && !keySet.has(k)) return;
json[k] = TagJsons.WALKER.walk(
{_: json[k]},
{
object: (obj, lastKey) => {
if (lastKey != null && !LAST_KEY_ALLOWLIST.has(lastKey)) return obj;
obj = TagCondition.tryRunBasic(obj);
obj = SkillTag.tryRun(obj);
obj = ActionTag.tryRun(obj);
obj = SenseTag.tryRun(obj);
obj = SpellTag.tryRun(obj);
obj = ItemTag.tryRun(obj);
obj = TableTag.tryRun(obj);
obj = TrapTag.tryRun(obj);
obj = HazardTag.tryRun(obj);
obj = ChanceTag.tryRun(obj);
obj = DiceConvert.getTaggedEntry(obj);
obj = QuickrefTag.tryRun(obj);
obj = FeatTag.tryRun(obj);
obj = AdventureBookTag.tryRun(obj);
if (fnCreatureTagSpecific) obj = fnCreatureTagSpecific(obj);
return obj;
},
},
)._;
});
}
}
TagJsons.OPTIMISTIC = true;
TagJsons._BLOCKLIST_FILE_PREFIXES = null;
TagJsons.WALKER_KEY_BLOCKLIST = new Set([
...MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST,
]);
TagJsons.WALKER = MiscUtil.getWalker({
keyBlocklist: TagJsons.WALKER_KEY_BLOCKLIST,
});
globalThis.TagJsons = TagJsons;
class SpellTag {
static _NON_STANDARD = new Set([
// Skip "Divination" to avoid tagging occurrences of the school
"Divination",
// Skip spells we specifically handle
"Antimagic Field",
"Dispel Magic",
].map(it => it.toLowerCase()));
static init (spells) {
spells
.forEach(sp => SpellTag._SPELL_NAMES[sp.name.toLowerCase()] = {name: sp.name, source: sp.source});
const spellNamesFiltered = Object.keys(SpellTag._SPELL_NAMES)
.filter(n => !SpellTag._NON_STANDARD.has(n));
SpellTag._SPELL_NAME_REGEX = new RegExp(`\\b(${spellNamesFiltered.map(it => it.escapeRegexp()).join("|")})\\b`, "gi");
SpellTag._SPELL_NAME_REGEX_SPELL = new RegExp(`\\b(${spellNamesFiltered.map(it => it.escapeRegexp()).join("|")}) (spell|cantrip)`, "gi");
SpellTag._SPELL_NAME_REGEX_AND = new RegExp(`\\b(${spellNamesFiltered.map(it => it.escapeRegexp()).join("|")}) (and {@spell)`, "gi");
SpellTag._SPELL_NAME_REGEX_CAST = new RegExp(`(?<prefix>casts?(?: the(?: spell)?)? )(?<spell>${spellNamesFiltered.map(it => it.escapeRegexp()).join("|")})\\b`, "gi");
}
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@spell"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag,
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
if (TagJsons.OPTIMISTIC) {
strMod = strMod
.replace(SpellTag._SPELL_NAME_REGEX_SPELL, (...m) => {
const spellMeta = SpellTag._SPELL_NAMES[m[1].toLowerCase()];
return `{@spell ${m[1]}${spellMeta.source !== Parser.SRC_PHB ? `|${spellMeta.source}` : ""}} ${m[2]}`;
});
}
// Tag common spells which often don't have e.g. the word "spell" nearby
strMod = strMod
.replace(/\b(antimagic field|dispel magic)\b/gi, (...m) => {
const spellMeta = SpellTag._SPELL_NAMES[m[1].toLowerCase()];
return `{@spell ${m[1]}${spellMeta.source !== Parser.SRC_PHB ? `|${spellMeta.source}` : ""}}`;
});
strMod
.replace(SpellTag._SPELL_NAME_REGEX_CAST, (...m) => {
const spellMeta = SpellTag._SPELL_NAMES[m.last().spell.toLowerCase()];
return `${m.last().prefix}{@spell ${m.last().spell}${spellMeta.source !== Parser.SRC_PHB ? `|${spellMeta.source}` : ""}}`;
});
return strMod
.replace(SpellTag._SPELL_NAME_REGEX_AND, (...m) => {
const spellMeta = SpellTag._SPELL_NAMES[m[1].toLowerCase()];
return `{@spell ${m[1]}${spellMeta.source !== Parser.SRC_PHB ? `|${spellMeta.source}` : ""}} ${m[2]}`;
})
.replace(/(spells(?:|[^.!?:{]*): )([^.!?]+)/gi, (...m) => {
const spellPart = m[2].replace(SpellTag._SPELL_NAME_REGEX, (...n) => {
const spellMeta = SpellTag._SPELL_NAMES[n[1].toLowerCase()];
return `{@spell ${n[1]}${spellMeta.source !== Parser.SRC_PHB ? `|${spellMeta.source}` : ""}}`;
});
return `${m[1]}${spellPart}`;
})
.replace(SpellTag._SPELL_NAME_REGEX_CAST, (...m) => {
const spellMeta = SpellTag._SPELL_NAMES[m.last().spell.toLowerCase()];
return `${m.last().prefix}{@spell ${m.last().spell}${spellMeta.source !== Parser.SRC_PHB ? `|${spellMeta.source}` : ""}}`;
})
;
}
}
SpellTag._SPELL_NAMES = {};
SpellTag._SPELL_NAME_REGEX = null;
SpellTag._SPELL_NAME_REGEX_SPELL = null;
SpellTag._SPELL_NAME_REGEX_AND = null;
SpellTag._SPELL_NAME_REGEX_CAST = null;
globalThis.SpellTag = SpellTag;
class ItemTag {
static _ITEM_NAMES = {};
static _ITEM_NAMES_REGEX_TOOLS = null;
static _ITEM_NAMES_REGEX_OTHER = null;
static _ITEM_NAMES_REGEX_EQUIPMENT = null;
static _WALKER = MiscUtil.getWalker({
keyBlocklist: new Set([
...TagJsons.WALKER_KEY_BLOCKLIST,
"packContents", // Avoid tagging item pack contents
"items", // Avoid tagging item group item lists
]),
});
static async pInit () {
const itemArr = await Renderer.item.pBuildList();
const standardItems = itemArr.filter(it => !SourceUtil.isNonstandardSource(it.source));
// region Tools
const toolTypes = new Set(["AT", "GS", "INS", "T"]);
const tools = standardItems.filter(it => toolTypes.has(it.type) && it.name !== "Horn");
tools.forEach(tool => {
this._ITEM_NAMES[tool.name.toLowerCase()] = {name: tool.name, source: tool.source};
});
this._ITEM_NAMES_REGEX_TOOLS = new RegExp(`\\b(${tools.map(it => it.name.escapeRegexp()).join("|")})\\b`, "gi");
// endregion
// region Other items
const otherItems = standardItems.filter(it => {
if (toolTypes.has(it.type)) return false;
// Disallow specific items
if (it.name === "Wave" && it.source === Parser.SRC_DMG) return false;
// Allow all non-specific-variant DMG items
if (it.source === Parser.SRC_DMG && !Renderer.item.isMundane(it) && it._category !== "Specific Variant") return true;
// Allow "sufficiently complex name" items
return it.name.split(" ").length > 2;
});
otherItems.forEach(it => {
this._ITEM_NAMES[it.name.toLowerCase()] = {name: it.name, source: it.source};
});
this._ITEM_NAMES_REGEX_OTHER = new RegExp(`\\b(${otherItems.map(it => it.name.escapeRegexp()).join("|")})\\b`, "gi");
// endregion
// region Basic equipment
// (Has some overlap with others)
const itemsEquipment = itemArr
.filter(itm => itm.source === "PHB" && !["M", "R", "LA", "MA", "HA", "S"].includes(itm.type));
this._ITEM_NAMES_REGEX_EQUIPMENT = new RegExp(`\\b(${itemsEquipment.map(it => it.name.escapeRegexp()).join("|")})\\b`, "gi");
itemsEquipment.forEach(itm => this._ITEM_NAMES[itm.name.toLowerCase()] = {name: itm.name, source: itm.source});
// endregion
}
/* -------------------------------------------- */
static tryRun (it) {
return this._WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@item"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag,
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(this._ITEM_NAMES_REGEX_TOOLS, (...m) => {
const itemMeta = this._ITEM_NAMES[m[1].toLowerCase()];
return `{@item ${m[1]}${itemMeta.source !== Parser.SRC_DMG ? `|${itemMeta.source}` : ""}}`;
})
.replace(this._ITEM_NAMES_REGEX_OTHER, (...m) => {
const itemMeta = this._ITEM_NAMES[m[1].toLowerCase()];
return `{@item ${m[1]}${itemMeta.source !== Parser.SRC_DMG ? `|${itemMeta.source}` : ""}}`;
})
;
}
/* -------------------------------------------- */
static tryRunBasicEquipment (it) {
return this._WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@item"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTagBasicEquipment,
},
);
return ptrStack._;
},
},
);
}
static _fnTagBasicEquipment (strMod) {
return strMod
.replace(ItemTag._ITEM_NAMES_REGEX_EQUIPMENT, (...m) => {
const itemMeta = ItemTag._ITEM_NAMES[m[1].toLowerCase()];
return `{@item ${m[1]}${itemMeta.source !== Parser.SRC_DMG ? `|${itemMeta.source}` : ""}}`;
})
;
}
}
globalThis.ItemTag = ItemTag;
class TableTag {
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@table"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag,
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(/Wild Magic Surge table/g, `{@table Wild Magic Surge|PHB} table`)
;
}
}
class TrapTag {
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@trap"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag,
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(TrapTag._RE_TRAP_SEE, (...m) => `{@trap ${m[1]}}${m[2]}`)
;
}
}
TrapTag._RE_TRAP_SEE = /\b(Fire-Breathing Statue|Sphere of Annihilation|Collapsing Roof|Falling Net|Pits|Poison Darts|Poison Needle|Rolling Sphere)( \(see)/gi;
class HazardTag {
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@hazard"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag,
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(HazardTag._RE_HAZARD_SEE, (...m) => `{@hazard ${m[1]}}${m[2]}`)
;
}
}
HazardTag._RE_HAZARD_SEE = /\b(High Altitude|Brown Mold|Green Slime|Webs|Yellow Mold|Extreme Cold|Extreme Heat|Heavy Precipitation|Strong Wind|Desecrated Ground|Frigid Water|Quicksand|Razorvine|Slippery Ice|Thin Ice)( \(see)/gi;
class CreatureTag {
/**
* Dynamically create a walker which can be re-used.
*/
static getFnTryRunSpecific (creaturesToTag) {
if (!creaturesToTag?.length) return null;
// region Create a regular expression per source
const bySource = {};
creaturesToTag.forEach(({name, source}) => {
(bySource[source] = bySource[source] || []).push(name);
});
const res = Object.entries(bySource)
.mergeMap(([source, names]) => {
const re = new RegExp(`\\b(${names.map(it => it.escapeRegexp()).join("|")})\\b`, "gi");
return {[source]: re};
});
// endregion
const fnTag = strMod => {
Object.entries(res)
.forEach(([source, re]) => {
strMod = strMod.replace(re, (...m) => `{@creature ${m[0]}${source !== Parser.SRC_DMG ? `|${source}` : ""}}`);
});
return strMod;
};
return (it) => {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@creature"],
ptrStack,
0,
0,
str,
{
fnTag,
},
);
return ptrStack._;
},
},
);
};
}
}
class ChanceTag {
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@chance"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag,
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(/\b(\d+)( percent)( chance)/g, (...m) => `{@chance ${m[1]}|${m[1]}${m[2]}}${m[3]}`)
;
}
}
class QuickrefTag {
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@quickref"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag.bind(this),
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(QuickrefTag._RE_BASIC, (...m) => `{@quickref ${QuickrefTag._LOOKUP_BASIC[m[0].toLowerCase()]}}`)
.replace(QuickrefTag._RE_VISION, (...m) => `{@quickref ${QuickrefTag._LOOKUP_VISION[m[0].toLowerCase()]}||${m[0]}}`)
;
}
}
QuickrefTag._RE_BASIC = /\b([Dd]ifficult [Tt]errain|Vision and Light)\b/g;
QuickrefTag._RE_VISION = /\b(dim light|bright light|lightly obscured|heavily obscured)\b/gi;
QuickrefTag._LOOKUP_BASIC = {
"difficult terrain": "difficult terrain||3",
"vision and light": "Vision and Light||2",
};
QuickrefTag._LOOKUP_VISION = {
"bright light": "Vision and Light||2",
"dim light": "Vision and Light||2",
"lightly obscured": "Vision and Light||2",
"heavily obscured": "Vision and Light||2",
};
class FeatTag {
static _FEAT_LOOKUP = [];
static async pInit () {
const featData = await DataUtil.feat.loadJSON();
const [featsNonStandard, feats] = [...featData.feat]
.sort((a, b) => SortUtil.ascSortDateString(Parser.sourceJsonToDate(a.source), Parser.sourceJsonToDate(b.source)) || SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.source, b.source))
.segregate(feat => SourceUtil.isNonstandardSource(feat.source));
this._FEAT_LOOKUP = [
...feats,
...featsNonStandard,
]
.map(feat => ({searchName: feat.name.toLowerCase(), feat}));
}
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@feat"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag.bind(this),
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
return strMod
.replace(/(?<pre>\bgain the )(?<name>.*)(?<post> feat\b)/, (...m) => {
const {pre, post, name} = m.at(-1);
const feat = this._getFeat(name);
if (!feat) return m[0];
const uid = DataUtil.proxy.getUid("feat", feat, {isMaintainCase: true});
const [uidName, ...uidRest] = uid.split("|");
// Tag display name not expected
if (name.toLowerCase() !== uidName.toLowerCase()) throw new Error(`Unimplemented!`);
const uidFinal = [
name,
...uidRest,
]
.join("|");
return `${pre}{@feat ${uidFinal}}${post}`;
})
;
}
static _getFeat (name) {
const searchName = name.toLowerCase().trim();
const featMeta = this._FEAT_LOOKUP.find(it => it.searchName === searchName);
if (!featMeta) return null;
return featMeta.feat;
}
}
class AdventureBookTag {
static _ADVENTURE_RES = [];
static _BOOK_RES = [];
static async pInit () {
for (const meta of [
{
propRes: "_ADVENTURE_RES",
propData: "adventure",
tag: "adventure",
contentsUrl: `${Renderer.get().baseUrl}data/adventures.json`,
},
{
propRes: "_BOOK_RES",
propData: "book",
tag: "book",
contentsUrl: `${Renderer.get().baseUrl}data/books.json`,
},
]) {
const contents = await DataUtil.loadJSON(meta.contentsUrl);
this[meta.propRes] = contents[meta.propData]
.map(({name, id}) => {
const re = new RegExp(`\\b${name.escapeRegexp()}\\b`, "g");
return str => str.replace(re, (...m) => `{@${meta.tag} ${m[0]}|${id}}`);
});
}
}
static tryRun (it) {
return TagJsons.WALKER.walk(
it,
{
string: (str) => {
const ptrStack = {_: ""};
TaggerUtils.walkerStringHandler(
["@adventure", "@book"],
ptrStack,
0,
0,
str,
{
fnTag: this._fnTag.bind(this),
},
);
return ptrStack._;
},
},
);
}
static _fnTag (strMod) {
for (const arr of [this._ADVENTURE_RES, this._BOOK_RES]) {
strMod = arr.reduce((str, fn) => fn(str), strMod);
}
return strMod;
}
}