mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2025-10-28 20:45:35 -05:00
v1.198.1
This commit is contained in:
435
js/converterutils-spell-sources.js
Normal file
435
js/converterutils-spell-sources.js
Normal file
@@ -0,0 +1,435 @@
|
||||
// region Based on `Charactermancer_AdditionalSpellsUtil`
|
||||
|
||||
class _SpellSourceUtil {
|
||||
static _getCleanUid (uid) {
|
||||
return DataUtil.proxy.getUid(
|
||||
"spell",
|
||||
DataUtil.proxy.unpackUid("spell", uid.split("#")[0], "spell"),
|
||||
);
|
||||
}
|
||||
|
||||
// region Data flattening
|
||||
static getSpellUids (additionalSpellBlock, modalFilterSpells) {
|
||||
additionalSpellBlock = MiscUtil.copyFast(additionalSpellBlock);
|
||||
|
||||
const outSpells = [];
|
||||
|
||||
Object.entries(additionalSpellBlock)
|
||||
.forEach(([additionType, additionMeta]) => {
|
||||
switch (additionType) {
|
||||
case "innate":
|
||||
case "known":
|
||||
case "prepared":
|
||||
case "expanded": {
|
||||
this._getSpellUids_doProcessAdditionMeta({additionType, additionMeta, outSpells, modalFilterSpells});
|
||||
break;
|
||||
}
|
||||
|
||||
// Ignored
|
||||
case "name":
|
||||
case "ability":
|
||||
case "resourceName": break;
|
||||
|
||||
default: throw new Error(`Unhandled spell addition type "${additionType}"`);
|
||||
}
|
||||
});
|
||||
|
||||
return outSpells.unique();
|
||||
}
|
||||
|
||||
static _getSpellUids_doProcessAdditionMeta (opts) {
|
||||
const {additionMeta, modalFilterSpells} = opts;
|
||||
|
||||
Object.values(additionMeta).forEach((levelMeta) => {
|
||||
if (levelMeta instanceof Array) {
|
||||
return levelMeta.forEach(spellItem => this._getSpellUids_doProcessSpellItem({...opts, spellItem, modalFilterSpells}));
|
||||
}
|
||||
|
||||
Object.entries(levelMeta).forEach(([rechargeType, levelMetaInner]) => {
|
||||
this._getSpellUids_doProcessSpellRechargeBlock({...opts, rechargeType, levelMetaInner, modalFilterSpells});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static _getSpellUids_doProcessSpellItem (opts) {
|
||||
const {outSpells, spellItem, modalFilterSpells} = opts;
|
||||
|
||||
if (typeof spellItem === "string") {
|
||||
return outSpells.push(this._getCleanUid(spellItem));
|
||||
}
|
||||
|
||||
if (spellItem.all != null) { // A filter expression
|
||||
return modalFilterSpells.getEntitiesMatchingFilterExpression({filterExpression: spellItem.all})
|
||||
.forEach(ent => outSpells.push(DataUtil.generic.getUid({name: ent.name, source: ent.source})));
|
||||
}
|
||||
|
||||
if (spellItem.choose != null) {
|
||||
if (typeof spellItem.choose === "string") { // A filter expression
|
||||
return modalFilterSpells.getEntitiesMatchingFilterExpression({filterExpression: spellItem.choose})
|
||||
.forEach(ent => outSpells.push(DataUtil.generic.getUid({name: ent.name, source: ent.source})));
|
||||
}
|
||||
|
||||
if (spellItem.choose.from) { // An array of choices
|
||||
return spellItem.choose.from
|
||||
.forEach(uid => outSpells.push(this._getCleanUid(uid)));
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled additional spell format: "${JSON.stringify(spellItem)}"`);
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled additional spell format: "${JSON.stringify(spellItem)}"`);
|
||||
}
|
||||
|
||||
static _getSpellUids_doProcessSpellRechargeBlock (opts) {
|
||||
const {rechargeType, levelMetaInner} = opts;
|
||||
|
||||
switch (rechargeType) {
|
||||
case "rest":
|
||||
case "daily": {
|
||||
Object.values(levelMetaInner)
|
||||
.forEach(spellList => {
|
||||
spellList.forEach(spellItem => this._getSpellUids_doProcessSpellItem({...opts, spellItem}));
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "resource": {
|
||||
Object.values(levelMetaInner)
|
||||
.forEach(spellList => {
|
||||
spellList.forEach(spellItem => this._getSpellUids_doProcessSpellItem({...opts, spellItem}));
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "will":
|
||||
case "ritual":
|
||||
case "_": {
|
||||
levelMetaInner.forEach(spellItem => this._getSpellUids_doProcessSpellItem({...opts, spellItem}));
|
||||
break;
|
||||
}
|
||||
|
||||
default: throw new Error(`Unhandled spell recharge type "${rechargeType}"`);
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
class _SpellSource {
|
||||
constructor () {
|
||||
this._lookup = {};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
mutLookup (otherLookup) {
|
||||
this._mutLookup_recurse(otherLookup, this._lookup, []);
|
||||
}
|
||||
|
||||
_mutLookup_recurse (otherLookup, obj, path) {
|
||||
Object.entries(obj)
|
||||
.forEach(([k, v]) => {
|
||||
if (typeof v !== "object" || v instanceof Array) {
|
||||
return MiscUtil.set(otherLookup, ...path, k, v);
|
||||
}
|
||||
|
||||
this._mutLookup_recurse(otherLookup, v, [...path, k]);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pInit () { throw new Error("Unimplemented!"); }
|
||||
}
|
||||
|
||||
class _SpellSourceClasses extends _SpellSource {
|
||||
constructor ({spellSourceLookupAdditional = null}) {
|
||||
super();
|
||||
this._spellSourceLookupAdditional = spellSourceLookupAdditional;
|
||||
}
|
||||
|
||||
async pInit () {
|
||||
this._mutAddSpellSourceLookup({spellSourceLookup: await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/spells/sources.json`)});
|
||||
this._mutAddSpellSourceLookup({spellSourceLookup: this._spellSourceLookupAdditional});
|
||||
}
|
||||
|
||||
_mutAddSpellSourceLookup ({spellSourceLookup}) {
|
||||
if (!spellSourceLookup) return;
|
||||
|
||||
Object.entries(spellSourceLookup)
|
||||
.forEach(([spellSource, spellNameTo]) => {
|
||||
Object.entries(spellNameTo)
|
||||
.forEach(([spellName, propTo]) => {
|
||||
Object.entries(propTo)
|
||||
.forEach(([prop, arr]) => {
|
||||
const grouped = {};
|
||||
|
||||
arr
|
||||
.forEach(({name: className, source: classSource, definedInSource}) => {
|
||||
const k = definedInSource || null;
|
||||
|
||||
const tgt = MiscUtil.getOrSet(grouped, classSource, className, {definedInSources: []});
|
||||
|
||||
if (!tgt.definedInSources.includes(k)) tgt.definedInSources.push(k);
|
||||
});
|
||||
|
||||
Object.entries(grouped)
|
||||
.forEach(([classSource, byClassSource]) => {
|
||||
Object.entries(byClassSource)
|
||||
.forEach(([className, byClassName]) => {
|
||||
MiscUtil.set(
|
||||
this._lookup,
|
||||
spellSource.toLowerCase(),
|
||||
spellName.toLowerCase(),
|
||||
prop,
|
||||
classSource,
|
||||
className,
|
||||
byClassName.definedInSources.some(it => it != null)
|
||||
? {definedInSources: byClassName.definedInSources}
|
||||
: true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSource extends _SpellSource {
|
||||
constructor ({props, modalFilterSpells} = {}) {
|
||||
super();
|
||||
this._props = props;
|
||||
this._modalFilterSpells = modalFilterSpells;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pInit () {
|
||||
const data = await this._pLoadData();
|
||||
this._props
|
||||
.forEach(prop => {
|
||||
data[prop]
|
||||
.filter(ent => ent.additionalSpells)
|
||||
.filter(ent => !this._isSkipEntity(ent))
|
||||
.forEach(ent => {
|
||||
const propPath = this._getPropPath(ent);
|
||||
|
||||
const uidToSummary = {};
|
||||
|
||||
ent.additionalSpells
|
||||
.forEach(additionalSpellBlock => {
|
||||
_SpellSourceUtil.getSpellUids(additionalSpellBlock, this._modalFilterSpells)
|
||||
.forEach(uid => {
|
||||
uidToSummary[uid] = uidToSummary[uid] || {
|
||||
cntAdditionalSpellBlocks: ent.additionalSpells.length,
|
||||
};
|
||||
|
||||
if (additionalSpellBlock.name) {
|
||||
(uidToSummary[uid].names = uidToSummary[uid].names || []).push(additionalSpellBlock.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(uidToSummary)
|
||||
.forEach(([uid, additionalSpellsSummary]) => {
|
||||
const val = this._getLookupValue(ent, additionalSpellsSummary);
|
||||
const {name, source} = DataUtil.proxy.unpackUid("spell", uid, "spell", {isLower: true});
|
||||
MiscUtil.set(this._lookup, source, name, ...propPath, val);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _pLoadData () { throw new Error("Unimplemented!"); }
|
||||
|
||||
_isSkipEntity (ent) { return false; }
|
||||
|
||||
_getPropPath (ent) { throw new Error("Unimplemented!"); }
|
||||
|
||||
_getPropPath_nameSource (ent) { return [ent.source, ent.name]; }
|
||||
|
||||
_getLookupValue (ent, additionalSpellsSummary) { return true; }
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceClassesSubclasses extends _AdditionalSpellSource {
|
||||
static _HASHES_SKIPPED = new Set([
|
||||
UrlUtil.URL_TO_HASH_BUILDER["subclass"]({
|
||||
name: "College of Lore",
|
||||
shortName: "Lore",
|
||||
source: "PHB",
|
||||
className: "Bard",
|
||||
classSource: "PHB",
|
||||
}),
|
||||
]);
|
||||
|
||||
constructor (opts) {
|
||||
super({
|
||||
...opts,
|
||||
props: [
|
||||
// Only include subclass additionalSpells, otherwise we add e.g. Bard Magical Secrets, which isn't helpful
|
||||
// "class",
|
||||
"subclass",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async _pLoadData () {
|
||||
return DataUtil.class.loadJSON();
|
||||
}
|
||||
|
||||
_isSkipEntity (ent) {
|
||||
if (ent.className === VeCt.STR_GENERIC || ent.classSource === VeCt.STR_GENERIC) return true;
|
||||
const hash = UrlUtil.URL_TO_HASH_BUILDER["subclass"](ent);
|
||||
return this.constructor._HASHES_SKIPPED.has(hash);
|
||||
}
|
||||
|
||||
_getPropPath (ent) {
|
||||
switch (ent.__prop) {
|
||||
case "class": return [ent.__prop, ...this._getPropPath_nameSource(ent)];
|
||||
case "subclass": return [ent.__prop, ent.classSource, ent.className, ent.source, ent.shortName];
|
||||
default: throw new Error(`Unhandled __prop "${ent.__prop}"`);
|
||||
}
|
||||
}
|
||||
|
||||
_getLookupValue (ent, additionalSpellsSummary) {
|
||||
switch (ent.__prop) {
|
||||
case "subclass": {
|
||||
const out = {name: ent.name};
|
||||
|
||||
// Only add `subSubclasses` if a spell is not shared between every sub-subclass
|
||||
if (additionalSpellsSummary.names && additionalSpellsSummary.cntAdditionalSpellBlocks !== additionalSpellsSummary.names.length) out.subSubclasses = additionalSpellsSummary.names;
|
||||
|
||||
return out;
|
||||
}
|
||||
default: return super._getLookupValue(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceRaces extends _AdditionalSpellSource {
|
||||
constructor (opts) {
|
||||
super({
|
||||
...opts,
|
||||
props: ["race"],
|
||||
});
|
||||
}
|
||||
|
||||
async _pLoadData () {
|
||||
return DataUtil.race.loadJSON();
|
||||
}
|
||||
|
||||
_getPropPath (ent) { return ["race", ...this._getPropPath_nameSource(ent)]; }
|
||||
|
||||
_getLookupValue (ent) {
|
||||
if (!ent._isSubRace) return super._getLookupValue(ent);
|
||||
return {baseName: ent._baseName, baseSource: ent._baseSource};
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceFile extends _AdditionalSpellSource {
|
||||
constructor ({file, ...rest} = {}) {
|
||||
super({...rest});
|
||||
this._file = file;
|
||||
}
|
||||
|
||||
async _pLoadData () {
|
||||
return DataUtil.loadJSON(`./data/${this._file}`);
|
||||
}
|
||||
|
||||
_getPropPath (ent) { return [ent.__prop, ...this._getPropPath_nameSource(ent)]; }
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceBackgrounds extends _AdditionalSpellSourceFile {
|
||||
constructor ({...rest}) {
|
||||
super({
|
||||
...rest,
|
||||
props: ["background"],
|
||||
file: "backgrounds.json",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceCharCreationOptions extends _AdditionalSpellSourceFile {
|
||||
constructor ({...rest}) {
|
||||
super({
|
||||
...rest,
|
||||
props: ["charoption"],
|
||||
file: "charcreationoptions.json",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceFeats extends _AdditionalSpellSourceFile {
|
||||
constructor ({...rest}) {
|
||||
super({
|
||||
...rest,
|
||||
props: ["feat"],
|
||||
file: "feats.json",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceOptionalFeatures extends _AdditionalSpellSourceFile {
|
||||
constructor ({...rest}) {
|
||||
super({
|
||||
...rest,
|
||||
props: ["optionalfeature"],
|
||||
file: "optionalfeatures.json",
|
||||
});
|
||||
}
|
||||
|
||||
_getLookupValue (ent) {
|
||||
return {featureType: [...ent.featureType]};
|
||||
}
|
||||
}
|
||||
|
||||
class _AdditionalSpellSourceRewards extends _AdditionalSpellSourceFile {
|
||||
constructor ({...rest}) {
|
||||
super({
|
||||
...rest,
|
||||
props: ["reward"],
|
||||
file: "rewards.json",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SpellSourceLookupBuilder {
|
||||
static async pGetLookup ({spells, spellSourceLookupAdditional = null}) {
|
||||
const cpySpells = MiscUtil.copyFast(spells);
|
||||
|
||||
const lookup = {};
|
||||
|
||||
for (
|
||||
const Clazz of [
|
||||
_SpellSourceClasses,
|
||||
_AdditionalSpellSourceClassesSubclasses,
|
||||
_AdditionalSpellSourceBackgrounds,
|
||||
_AdditionalSpellSourceCharCreationOptions,
|
||||
_AdditionalSpellSourceFeats,
|
||||
_AdditionalSpellSourceOptionalFeatures,
|
||||
_AdditionalSpellSourceRaces,
|
||||
_AdditionalSpellSourceRewards,
|
||||
]
|
||||
) {
|
||||
cpySpells.forEach(sp => PageFilterSpells.unmutateForFilters(sp));
|
||||
const modalFilterSpells = new ModalFilterSpells({allData: cpySpells});
|
||||
await modalFilterSpells.pPopulateHiddenWrapper();
|
||||
|
||||
const adder = new Clazz({modalFilterSpells, spellSourceLookupAdditional});
|
||||
await adder.pInit();
|
||||
adder.mutLookup(lookup);
|
||||
|
||||
DataUtil.spell.setSpellSourceLookup(lookup, {isExternalApplication: true});
|
||||
cpySpells.forEach(sp => {
|
||||
DataUtil.spell.unmutEntity(sp, {isExternalApplication: true});
|
||||
DataUtil.spell.mutEntity(sp, {isExternalApplication: true});
|
||||
});
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user