This commit is contained in:
TheGiddyLimit
2024-01-10 17:30:40 +00:00
parent e3bf75f42a
commit 5ffd4acdb4
26 changed files with 473 additions and 322 deletions

View File

@@ -136,7 +136,15 @@ class BaseParserFeature extends BaseParser {
}
handleStack();
if (pres.length) state.entity.prerequisite = pres;
if (!pres.length) return;
const presDeduped = [];
pres.forEach(pre => {
if (presDeduped.some(it => CollectionUtil.deepEquals(pre, it))) return;
presDeduped.push(pre);
});
state.entity.prerequisite = presDeduped;
}
}

View File

@@ -337,6 +337,8 @@ class SpellParser extends BaseParser {
unit = unit.toLowerCase().trim();
switch (unit) {
case "days":
case "weeks":
case "months":
case "years":
case "hours":
case "minutes":
@@ -344,6 +346,8 @@ class SpellParser extends BaseParser {
case "rounds": return unit.slice(0, -1);
case "day":
case "week":
case "month":
case "year":
case "hour":
case "minute":
@@ -441,7 +445,7 @@ class SpellParser extends BaseParser {
if (dur.toLowerCase() === "special") return stats.duration = [{type: "special"}];
if (dur.toLowerCase() === "permanent") return stats.duration = [{type: "permanent"}];
const mConcOrUpTo = /^(concentration, )?up to (\d+|an?) (hour|minute|turn|round|week|day|year)(?:s)?$/i.exec(dur);
const mConcOrUpTo = /^(concentration, )?up to (\d+|an?) (hour|minute|turn|round|week|month|day|year)(?:s)?$/i.exec(dur);
if (mConcOrUpTo) {
const amount = mConcOrUpTo[2].toLowerCase().startsWith("a") ? 1 : Number(mConcOrUpTo[2]);
const out = {type: "timed", duration: {type: this._getCleanTimeUnit(mConcOrUpTo[3], true, options), amount}, concentration: true};
@@ -450,7 +454,7 @@ class SpellParser extends BaseParser {
return stats.duration = [out];
}
const mTimed = /^(\d+) (hour|minute|turn|round|week|day|year)(?:s)?$/i.exec(dur);
const mTimed = /^(\d+) (hour|minute|turn|round|week|month|day|year)(?:s)?$/i.exec(dur);
if (mTimed) return stats.duration = [{type: "timed", duration: {type: this._getCleanTimeUnit(mTimed[2], true, options), amount: Number(mTimed[1])}}];
const mDispelledTriggered = /^until dispelled( or triggered)?$/i.exec(dur);

View File

@@ -801,7 +801,7 @@ class BackgroundConverter extends BaseConverter {
}
}
// region sample
BackgroundConverter._SAMPLE_TEXT = `Giant Foundling
BackgroundConverter._SAMPLE_TEXT = `Giant Foundling
Skill Proficiencies: Intimidation, Survival
Languages: Giant and one other language of your choice
Equipment: A backpack, a set of travelers clothes, a small stone or sprig that reminds you of home, and a pouch containing 10 gp
@@ -996,117 +996,139 @@ class ConverterUi extends BaseComponent {
JqueryUtil.doToast({type: "warning", content: "Enabled editing. Note that edits will be overwritten as you parse new stat blocks."});
});
const $btnSaveLocal = $(`#save_local`).click(async () => {
const output = this._outText;
$(`#preview`)
.on("click", async evt => {
const metaCurr = this._getCurrentEntities();
if (!(output || "").trim()) {
if (!metaCurr?.entities?.length) return JqueryUtil.doToast({content: "Nothing to preview!", type: "warning"});
if (metaCurr.error) return JqueryUtil.doToast({content: `Current output was not valid JSON!`, type: "danger"});
const entries = !this.activeConverter.prop
? metaCurr.entities.flat()
: metaCurr.entities
.map(ent => {
// Handle nameless/sourceless entities (e.g. tables)
if (!ent.name) ent.name = "(Unnamed)";
if (!ent.source) ent.source = VeCt.STR_GENERIC;
return {
type: "statblockInline",
dataType: this.activeConverter.prop,
data: ent,
};
});
Renderer.hover.getShowWindow(
Renderer.hover.$getHoverContent_generic({
type: "entries",
entries,
}),
Renderer.hover.getWindowPositionFromEvent(evt),
{
title: "Preview",
isPermanent: true,
},
);
});
const $btnSaveLocal = $(`#save_local`).click(async () => {
const metaCurr = this._getCurrentEntities();
if (!metaCurr?.entities?.length) return JqueryUtil.doToast({content: "Nothing to save!", type: "warning"});
if (metaCurr.error) return JqueryUtil.doToast({content: `Current output was not valid JSON!`, type: "danger"});
const prop = this.activeConverter.prop;
const invalidSources = metaCurr.entities.map(it => !it.source || !BrewUtil2.hasSourceJson(it.source) ? (it.name || it.caption || "(Unnamed)").trim() : false).filter(Boolean);
if (invalidSources.length) {
JqueryUtil.doToast({
content: `One or more entries have missing or unknown sources: ${invalidSources.join(", ")}`,
type: "danger",
});
return;
}
const brewDocEditable = await BrewUtil2.pGetEditableBrewDoc();
const uneditableSources = metaCurr.entities
.filter(ent => !(brewDocEditable?.body?._meta?.sources || []).some(src => src.json === ent.source))
.map(ent => ent.source);
if (uneditableSources.length) {
JqueryUtil.doToast({
content: `One or more entries have sources which belong to non-editable homebrew: ${uneditableSources.join(", ")}`,
type: "danger",
});
return;
}
// ignore duplicates
const _dupes = {};
const dupes = [];
const dedupedEntries = metaCurr.entities
.map(it => {
const lSource = it.source.toLowerCase();
const lName = it.name.toLowerCase();
_dupes[lSource] = _dupes[lSource] || {};
if (_dupes[lSource][lName]) {
dupes.push(it.name);
return null;
} else {
_dupes[lSource][lName] = true;
return it;
}
})
.filter(Boolean);
if (dupes.length) {
JqueryUtil.doToast({
type: "warning",
content: `Ignored ${dupes.length} duplicate entr${dupes.length === 1 ? "y" : "ies"}`,
});
}
if (!dedupedEntries.length) {
return JqueryUtil.doToast({
content: "Nothing to save!",
type: "warning",
});
}
try {
const prop = this.activeConverter.prop;
const entries = JSON.parse(`[${output}]`);
// handle overwrites
const brewDoc = await BrewUtil2.pGetOrCreateEditableBrewDoc();
const overwriteMeta = dedupedEntries
.map(it => {
if (!brewDoc?.body?.[prop]) return {entry: it, isOverwrite: false};
const invalidSources = entries.map(it => !it.source || !BrewUtil2.hasSourceJson(it.source) ? (it.name || it.caption || "(Unnamed)").trim() : false).filter(Boolean);
if (invalidSources.length) {
JqueryUtil.doToast({
content: `One or more entries have missing or unknown sources: ${invalidSources.join(", ")}`,
type: "danger",
});
return;
}
const ix = brewDoc.body[prop].findIndex(bru => bru.name.toLowerCase() === it.name.toLowerCase() && bru.source.toLowerCase() === it.source.toLowerCase());
if (!~ix) return {entry: it, isOverwrite: false};
const brewDocEditable = await BrewUtil2.pGetEditableBrewDoc();
const uneditableSources = entries
.filter(ent => !(brewDocEditable?.body?._meta?.sources || []).some(src => src.json === ent.source))
.map(ent => ent.source);
if (uneditableSources.length) {
JqueryUtil.doToast({
content: `One or more entries have sources which belong to non-editable homebrew: ${uneditableSources.join(", ")}`,
type: "danger",
});
return;
}
return {
isOverwrite: true,
ix,
entry: it,
};
})
.filter(Boolean);
// ignore duplicates
const _dupes = {};
const dupes = [];
const dedupedEntries = entries
.map(it => {
const lSource = it.source.toLowerCase();
const lName = it.name.toLowerCase();
_dupes[lSource] = _dupes[lSource] || {};
if (_dupes[lSource][lName]) {
dupes.push(it.name);
return null;
} else {
_dupes[lSource][lName] = true;
return it;
}
})
.filter(Boolean);
if (dupes.length) {
JqueryUtil.doToast({
type: "warning",
content: `Ignored ${dupes.length} duplicate entr${dupes.length === 1 ? "y" : "ies"}`,
});
}
if (!dedupedEntries.length) {
return JqueryUtil.doToast({
content: "Nothing to save!",
type: "warning",
});
}
// handle overwrites
const brewDoc = await BrewUtil2.pGetOrCreateEditableBrewDoc();
const overwriteMeta = dedupedEntries
.map(it => {
if (!brewDoc?.body?.[prop]) return {entry: it, isOverwrite: false};
const ix = brewDoc.body[prop].findIndex(bru => bru.name.toLowerCase() === it.name.toLowerCase() && bru.source.toLowerCase() === it.source.toLowerCase());
if (!~ix) return {entry: it, isOverwrite: false};
return {
isOverwrite: true,
ix,
entry: it,
};
})
.filter(Boolean);
const willOverwrite = overwriteMeta.map(it => it.isOverwrite).filter(Boolean);
if (
willOverwrite.length
&& !await InputUiUtil.pGetUserBoolean({title: "Overwrite Entries", htmlDescription: `This will overwrite ${willOverwrite.length} entr${willOverwrite.length === 1 ? "y" : "ies"}. Are you sure?`, textYes: "Yes", textNo: "Cancel"})
) {
return;
}
const cpyBrewDoc = MiscUtil.copy(brewDoc);
overwriteMeta.forEach(meta => {
if (meta.isOverwrite) return cpyBrewDoc.body[prop][meta.ix] = MiscUtil.copy(meta.entry);
(cpyBrewDoc.body[prop] = cpyBrewDoc.body[prop] || []).push(MiscUtil.copy(meta.entry));
});
await BrewUtil2.pSetEditableBrewDoc(cpyBrewDoc);
JqueryUtil.doToast({
type: "success",
content: `Saved!`,
});
} catch (e) {
JqueryUtil.doToast({
content: `Current output was not valid JSON!`,
type: "danger",
});
setTimeout(() => { throw e; });
const willOverwrite = overwriteMeta.map(it => it.isOverwrite).filter(Boolean);
if (
willOverwrite.length
&& !await InputUiUtil.pGetUserBoolean({title: "Overwrite Entries", htmlDescription: `This will overwrite ${willOverwrite.length} entr${willOverwrite.length === 1 ? "y" : "ies"}. Are you sure?`, textYes: "Yes", textNo: "Cancel"})
) {
return;
}
const cpyBrewDoc = MiscUtil.copy(brewDoc);
overwriteMeta.forEach(meta => {
if (meta.isOverwrite) return cpyBrewDoc.body[prop][meta.ix] = MiscUtil.copy(meta.entry);
(cpyBrewDoc.body[prop] = cpyBrewDoc.body[prop] || []).push(MiscUtil.copy(meta.entry));
});
await BrewUtil2.pSetEditableBrewDoc(cpyBrewDoc);
JqueryUtil.doToast({
type: "success",
content: `Saved!`,
});
});
const hkConverter = () => {
$btnSaveLocal.toggleClass("hidden", !this.activeConverter.canSaveLocal);
@@ -1115,26 +1137,20 @@ class ConverterUi extends BaseComponent {
hkConverter();
$(`#btn-output-download`).click(() => {
const output = this._outText;
if (!output || !output.trim()) {
return JqueryUtil.doToast({
content: "Nothing to download!",
type: "danger",
});
}
const metaCurr = this._getCurrentEntities();
try {
const prop = this.activeConverter.prop;
const out = {[prop]: JSON.parse(`[${output}]`)};
DataUtil.userDownload(`converter-output`, out);
} catch (e) {
if (!metaCurr?.entities?.length) return JqueryUtil.doToast({content: "Nothing to download!", type: "warning"});
if (metaCurr.error) {
JqueryUtil.doToast({
content: `Current output was not valid JSON. Downloading as <span class="code">.txt</span> instead.`,
type: "warning",
});
DataUtil.userDownloadText(`converter-output.txt`, output);
setTimeout(() => { throw e; });
DataUtil.userDownloadText(`converter-output.txt`, metaCurr.text);
return;
}
const out = {[this.activeConverter.prop]: metaCurr.entities};
DataUtil.userDownload(`converter-output`, out);
});
$(`#btn-output-copy`).click(async evt => {
@@ -1204,6 +1220,18 @@ class ConverterUi extends BaseComponent {
window.dispatchEvent(new Event("toolsLoaded"));
}
_getCurrentEntities () {
const output = this._outText;
if (!(output || "").trim()) return null;
try {
return {entities: JSON.parse(`[${output}]`)};
} catch (e) {
return {error: e.message, text: output.trim()};
}
}
initSideMenu () {
const $mnu = $(`.sidemenu`);
@@ -1267,7 +1295,7 @@ class ConverterUi extends BaseComponent {
if (append) {
const strs = [asCleanString, this._outText];
if (this._state.appendPrependMode === "prepend") strs.reverse();
this._outText = strs.join(",\n");
this._outText = strs.map(it => it.trimEnd()).join(",\n");
this._state.hasAppended = true;
} else {
this._outText = asCleanString;

View File

@@ -1368,6 +1368,7 @@ class SpellcastingTraitConvert {
{re: /\/rest/i, prop: "rest"},
{re: /\/day/i, prop: "daily"},
{re: /\/week/i, prop: "weekly"},
{re: /\/month/i, prop: "monthly"},
{re: /\/yeark/i, prop: "yearly"},
];

View File

@@ -191,8 +191,9 @@ class PageFilterSpells extends PageFilter {
return "24+ Hours";
}
case "week":
case "day":
case "week":
case "month":
case "year": return "24+ Hours";
default: return "Special";
}

View File

@@ -2352,6 +2352,11 @@ class CreatureBuilder extends Builder {
type: "weekly",
mode: "frequency",
},
{
display: "\uD835\uDC65/month (/each) spells",
type: "monthly",
mode: "frequency",
},
{
display: "\uD835\uDC65/year (/each) spells",
type: "yearly",
@@ -2439,6 +2444,7 @@ class CreatureBuilder extends Builder {
if (trait.daily) handleFrequency("daily");
if (trait.rest) handleFrequency("rest");
if (trait.weekly) handleFrequency("weekly");
if (trait.monthly) handleFrequency("monthly");
if (trait.yearly) handleFrequency("yearly");
if (trait.spells) {
Object.entries(trait.spells).forEach(([k, v]) => {
@@ -2539,6 +2545,7 @@ class CreatureBuilder extends Builder {
case "daily": return "/Day";
case "rest": return "/Rest";
case "weekly": return "/Week";
case "monthly": return "/Month";
case "yearly": return "/Year";
}
})();

View File

@@ -667,8 +667,8 @@ Parser.sourceJsonToStylePart = function (source) {
Parser.sourceJsonToMarkerHtml = function (source, {isList = true, additionalStyles = ""} = {}) {
source = Parser._getSourceStringFromSource(source);
// TODO(Future) consider enabling this
// if (SourceUtil.isPartneredSourceWotc(source)) return `<span class="help-subtle ve-source-marker ${isList ? `ve-source-marker--list` : ""} ve-source-marker--partnered ${additionalStyles}" title="D&amp;D Partnered Source"></span>`;
if (SourceUtil.isLegacySourceWotc(source)) return `<span class="help-subtle ve-source-marker ${isList ? `ve-source-marker--list` : ""} ve-source-marker--legacy ${additionalStyles}" title="Legacy Source">ʟ</span>`;
// if (SourceUtil.isPartneredSourceWotc(source)) return `<span class="help-subtle ve-source-marker ${isList ? `ve-source-marker--list` : ""} ve-source-marker--partnered ${additionalStyles}" title="D&amp;D Partnered Source">${isList ? "" : "["}✦${isList ? "" : "]"}</span>`;
if (SourceUtil.isLegacySourceWotc(source)) return `<span class="help-subtle ve-source-marker ${isList ? `ve-source-marker--list` : ""} ve-source-marker--legacy ${additionalStyles}" title="Legacy Source">${isList ? "" : "["}ʟ${isList ? "" : "]"}</span>`;
return "";
};
@@ -1345,6 +1345,7 @@ Parser.DURATION_AMOUNT_TYPES = [
"hour",
"day",
"week",
"month",
"year",
];

View File

@@ -1035,7 +1035,7 @@ globalThis.Renderer = function () {
const hidden = new Set(entry.hidden || []);
const toRender = [{type: "entries", name: entry.name, entries: entry.headerEntries ? MiscUtil.copyFast(entry.headerEntries) : []}];
if (entry.constant || entry.will || entry.recharge || entry.charges || entry.rest || entry.daily || entry.weekly || entry.yearly || entry.ritual) {
if (entry.constant || entry.will || entry.recharge || entry.charges || entry.rest || entry.daily || entry.weekly || entry.monthly || entry.yearly || entry.ritual) {
const tempList = {type: "list", style: "list-hang-notitle", items: [], data: {isSpellList: true}};
if (entry.constant && !hidden.has("constant")) tempList.items.push({type: "itemSpell", name: `Constant:`, entry: this._renderSpellcasting_getRenderableList(entry.constant).join(", ")});
if (entry.will && !hidden.has("will")) tempList.items.push({type: "itemSpell", name: `At will:`, entry: this._renderSpellcasting_getRenderableList(entry.will).join(", ")});
@@ -1045,6 +1045,7 @@ globalThis.Renderer = function () {
this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "rest", durationText: "/rest"});
this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "daily", durationText: "/day"});
this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "weekly", durationText: "/week"});
this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "monthly", durationText: "/month"});
this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "yearly", durationText: "/year"});
if (entry.ritual && !hidden.has("ritual")) tempList.items.push({type: "itemSpell", name: `Rituals:`, entry: this._renderSpellcasting_getRenderableList(entry.ritual).join(", ")});
@@ -1465,8 +1466,7 @@ globalThis.Renderer = function () {
};
this._renderHomebrew = function (entry, textStack, meta, options) {
this._renderPrefix(entry, textStack, meta, options);
textStack[0] += `<div class="homebrew-section"><div class="homebrew-float"><span class="homebrew-notice"></span>`;
textStack[0] += `<div class="rd-homebrew__b"><div class="rd-homebrew__wrp-notice"><span class="rd-homebrew__disp-notice"></span>`;
if (entry.oldEntries) {
const hoverMeta = Renderer.hover.getInlineHover({type: "entries", name: "Homebrew", entries: entry.oldEntries});
@@ -1478,7 +1478,7 @@ globalThis.Renderer = function () {
} else {
markerText = "(See removed content)";
}
textStack[0] += `<span class="homebrew-old-content" href="#${window.location.hash}" ${hoverMeta.html}>${markerText}</span>`;
textStack[0] += `<span class="rd-homebrew__disp-old-content" href="#${window.location.hash}" ${hoverMeta.html}>${markerText}</span>`;
}
textStack[0] += `</div>`;
@@ -1493,7 +1493,6 @@ globalThis.Renderer = function () {
}
textStack[0] += `</div>`;
this._renderSuffix(entry, textStack, meta, options);
};
this._renderCode = function (entry, textStack, meta, options) {
@@ -1849,7 +1848,7 @@ globalThis.Renderer = function () {
name: "Homebrew Modifications",
entries: tooltipEntries,
});
textStack[0] += `<span class="homebrew-inline" ${hoverMeta.html}>`;
textStack[0] += `<span class="rd-homebrew__disp-inline" ${hoverMeta.html}>`;
this._recursiveRender(newText || "[...]", textStack, meta);
textStack[0] += `</span>`;

View File

@@ -274,6 +274,7 @@ class _BrewUtil2Base {
DISPLAY_NAME_PLURAL;
DEFAULT_AUTHOR;
STYLE_BTN;
IS_PREFER_DATE_ADDED;
_LOCK = new VeLock({name: this.constructor.name});
@@ -290,6 +291,15 @@ class _BrewUtil2Base {
_storage = StorageUtil;
_parent = null;
/**
* @param {?_BrewUtil2Base} parent
*/
constructor ({parent = null} = {}) {
this._parent = parent;
}
/* -------------------------------------------- */
_pActiveInit = null;
@@ -572,8 +582,12 @@ class _BrewUtil2Base {
};
}
/* -------------------------------------------- */
getCacheIteration () { return this._cache_iteration; }
/* -------------------------------------------- */
async pSetBrew (val, {lockToken} = {}) {
try {
await this._LOCK.pLock({token: lockToken});
@@ -602,6 +616,8 @@ class _BrewUtil2Base {
this._setBrewMetas(val.map(brew => this._getBrewDocReduced(brew)));
}
/* -------------------------------------------- */
_getBrewId (brew) {
if (brew.head.url) return brew.head.url;
if (brew.body._meta?.sources?.length) return brew.body._meta.sources.map(src => (src.json || "").toLowerCase()).sort(SortUtil.ascSortLower).join(" :: ");
@@ -618,6 +634,24 @@ class _BrewUtil2Base {
return [...brews, ...brewsToAdd];
}
/* -------------------------------------------- */
async _pLoadParentDependencies ({unavailableSources}) {
if (!unavailableSources?.length) return false;
if (!this._parent) return false;
await Promise.allSettled(unavailableSources.map(async source => {
const url = await this._parent.pGetSourceUrl(source);
if (!url) return;
await this._parent.pAddBrewFromUrl(url, {isLazy: true});
}));
await this._parent.pAddBrewsLazyFinalize();
return false;
}
/* -------------------------------------------- */
async _pGetBrewDependencies ({brewDocs, brewsRaw = null, brewsRawLocal = null, lockToken}) {
try {
lockToken = await this._LOCK.pLock({token: lockToken});
@@ -629,11 +663,13 @@ class _BrewUtil2Base {
async _pGetBrewDependencies_ ({brewDocs, brewsRaw = null, brewsRawLocal = null, lockToken}) {
const urlRoot = await this.pGetCustomUrl();
const brewIndex = await this._pGetSourceIndex(urlRoot);
const brewIndex = await this.pGetSourceIndex(urlRoot);
const toLoadSources = [];
const loadedSources = new Set();
const out = [];
const unavailableSources = new Set();
const brewDocsDependencies = [];
brewsRaw = brewsRaw || await this._pGetBrewRaw({lockToken});
brewsRawLocal = brewsRawLocal || await this._pGetBrew_pGetLocalBrew({lockToken});
@@ -644,7 +680,11 @@ class _BrewUtil2Base {
brewsRaw.forEach(brew => trackLoaded(brew));
brewsRawLocal.forEach(brew => trackLoaded(brew));
brewDocs.forEach(brewDoc => toLoadSources.push(...this._getBrewDependencySources({brewDoc, brewIndex})));
brewDocs.forEach(brewDoc => {
const {available, unavailable} = this._getBrewDependencySources({brewDoc, brewIndex});
toLoadSources.push(...available);
unavailable.forEach(src => unavailableSources.add(src));
});
while (toLoadSources.length) {
const src = toLoadSources.pop();
@@ -653,18 +693,23 @@ class _BrewUtil2Base {
const url = this.getFileUrl(brewIndex[src], urlRoot);
const brewDocDep = await this._pGetBrewDocFromUrl({url});
out.push(brewDocDep);
brewDocsDependencies.push(brewDocDep);
trackLoaded(brewDocDep);
toLoadSources.push(...this._getBrewDependencySources({brewDoc: brewDocDep, brewIndex}));
const {available, unavailable} = this._getBrewDependencySources({brewDoc: brewDocDep, brewIndex});
toLoadSources.push(...available);
unavailable.forEach(src => unavailableSources.add(src));
}
return out;
return {
brewDocsDependencies,
unavailableSources: [...unavailableSources].sort(SortUtil.ascSortLower),
};
}
async pGetSourceUrl (source) {
const urlRoot = await this.pGetCustomUrl();
const brewIndex = await this._pGetSourceIndex(urlRoot);
const brewIndex = await this.pGetSourceIndex(urlRoot);
if (brewIndex[source]) return this.getFileUrl(brewIndex[source], urlRoot);
@@ -677,7 +722,7 @@ class _BrewUtil2Base {
}
/** @abstract */
async _pGetSourceIndex (urlRoot) { throw new Error("Unimplemented!"); }
async pGetSourceIndex (urlRoot) { throw new Error("Unimplemented!"); }
/** @abstract */
getFileUrl (path, urlRoot) { throw new Error("Unimplemented!"); }
/** @abstract */
@@ -691,15 +736,14 @@ class _BrewUtil2Base {
_PROPS_DEPS_DEEP = ["otherSources"];
_getBrewDependencySources ({brewDoc, brewIndex}) {
const out = new Set();
const sources = new Set();
this._PROPS_DEPS.forEach(prop => {
const obj = brewDoc.body._meta?.[prop];
if (!obj || !Object.keys(obj).length) return;
Object.values(obj)
.flat()
.filter(src => brewIndex[src])
.forEach(src => out.add(src));
.forEach(src => sources.add(src));
});
this._PROPS_DEPS_DEEP.forEach(prop => {
@@ -708,21 +752,28 @@ class _BrewUtil2Base {
return Object.values(obj)
.map(objSub => Object.keys(objSub))
.flat()
.filter(src => brewIndex[src])
.forEach(src => out.add(src));
.forEach(src => sources.add(src));
});
return out;
const [available, unavailable] = [...sources]
.segregate(src => brewIndex[src]);
return {available, unavailable};
}
async pAddBrewFromUrl (url, {lockToken, isLazy} = {}) {
async pAddBrewFromUrl (url, {isLazy} = {}) {
let brewDocs = []; let unavailableSources = [];
try {
return (await this._pAddBrewFromUrl({url, lockToken, isLazy}));
({brewDocs, unavailableSources} = await this._pAddBrewFromUrl({url, isLazy}));
} catch (e) {
JqueryUtil.doToast({type: "danger", content: `Failed to load ${this.DISPLAY_NAME} from URL "${url}"! ${VeCt.STR_SEE_CONSOLE}`});
setTimeout(() => { throw e; });
return [];
}
return [];
await this._pLoadParentDependencies({unavailableSources});
return brewDocs;
}
async _pGetBrewDocFromUrl ({url}) {
@@ -741,16 +792,17 @@ class _BrewUtil2Base {
this._LOCK.unlock();
}
return [brewDoc];
return {brewDocs: [brewDoc], unavailableSources: []};
}
const brewDocs = [brewDoc];
const brewDocs = [brewDoc]; const unavailableSources = [];
try {
lockToken = await this._LOCK.pLock({token: lockToken});
const brews = MiscUtil.copyFast(await this._pGetBrewRaw({lockToken}));
const brewDocsDependencies = await this._pGetBrewDependencies({brewDocs, brewsRaw: brews, lockToken});
const {brewDocsDependencies, unavailableSources: unavailableSources_} = await this._pGetBrewDependencies({brewDocs, brewsRaw: brews, lockToken});
brewDocs.push(...brewDocsDependencies);
unavailableSources.push(...unavailableSources_);
const brewsNxt = this._getNextBrews(brews, brewDocs);
await this.pSetBrew(brewsNxt, {lockToken});
@@ -758,20 +810,25 @@ class _BrewUtil2Base {
this._LOCK.unlock();
}
return brewDocs;
return {brewDocs, unavailableSources};
}
async pAddBrewsFromFiles (files) {
let brewDocs = []; let unavailableSources = [];
try {
const lockToken = await this._LOCK.pLock();
return (await this._pAddBrewsFromFiles({files, lockToken}));
({brewDocs, unavailableSources} = await this._pAddBrewsFromFiles({files, lockToken}));
} catch (e) {
JqueryUtil.doToast({type: "danger", content: `Failed to load ${this.DISPLAY_NAME} from file(s)! ${VeCt.STR_SEE_CONSOLE}`});
setTimeout(() => { throw e; });
return [];
} finally {
this._LOCK.unlock();
}
return [];
await this._pLoadParentDependencies({unavailableSources});
return brewDocs;
}
async _pAddBrewsFromFiles ({files, lockToken}) {
@@ -779,32 +836,41 @@ class _BrewUtil2Base {
const brews = MiscUtil.copyFast(await this._pGetBrewRaw({lockToken}));
const brewDocsDependencies = await this._pGetBrewDependencies({brewDocs, brewsRaw: brews, lockToken});
const {brewDocsDependencies, unavailableSources} = await this._pGetBrewDependencies({brewDocs, brewsRaw: brews, lockToken});
brewDocs.push(...brewDocsDependencies);
const brewsNxt = this._getNextBrews(brews, brewDocs);
await this.pSetBrew(brewsNxt, {lockToken});
return brewDocs;
return {brewDocs, unavailableSources};
}
async pAddBrewsLazyFinalize ({lockToken} = {}) {
async pAddBrewsLazyFinalize () {
let brewDocs = []; let unavailableSources = [];
try {
lockToken = await this._LOCK.pLock({token: lockToken});
return (await this._pAddBrewsLazyFinalize_({lockToken}));
const lockToken = await this._LOCK.pLock();
({brewDocs, unavailableSources} = await this._pAddBrewsLazyFinalize_({lockToken}));
} catch (e) {
JqueryUtil.doToast({type: "danger", content: `Failed to finalize ${this.DISPLAY_NAME_PLURAL}! ${VeCt.STR_SEE_CONSOLE}`});
setTimeout(() => { throw e; });
return [];
} finally {
this._LOCK.unlock();
}
await this._pLoadParentDependencies({unavailableSources});
return brewDocs;
}
async _pAddBrewsLazyFinalize_ ({lockToken}) {
const brewsRaw = await this._pGetBrewRaw({lockToken});
const brewDeps = await this._pGetBrewDependencies({brewDocs: this._addLazy_brewsTemp, brewsRaw, lockToken});
const out = MiscUtil.copyFast(brewDeps);
const brewsNxt = this._getNextBrews(MiscUtil.copyFast(brewsRaw), [...this._addLazy_brewsTemp, ...brewDeps]);
const {brewDocsDependencies, unavailableSources} = await this._pGetBrewDependencies({brewDocs: this._addLazy_brewsTemp, brewsRaw, lockToken});
const brewDocs = MiscUtil.copyFast(brewDocsDependencies);
const brewsNxt = this._getNextBrews(MiscUtil.copyFast(brewsRaw), [...this._addLazy_brewsTemp, ...brewDocsDependencies]);
await this.pSetBrew(brewsNxt, {lockToken});
this._addLazy_brewsTemp = [];
return out;
return {brewDocs, unavailableSources};
}
async pPullAllBrews ({brews} = {}) {
@@ -1268,6 +1334,66 @@ class _BrewUtil2Base {
// endregion
}
class _PrereleaseUtil extends _BrewUtil2Base {
_STORAGE_KEY_LEGACY = null;
_STORAGE_KEY_LEGACY_META = null;
_STORAGE_KEY = "PRERELEASE_STORAGE";
_STORAGE_KEY_META = "PRERELEASE_META_STORAGE";
_STORAGE_KEY_CUSTOM_URL = "PRERELEASE_CUSTOM_REPO_URL";
_STORAGE_KEY_MIGRATION_VERSION = "PRERELEASE_STORAGE_MIGRATION";
_PATH_LOCAL_DIR = "prerelease";
_PATH_LOCAL_INDEX = VeCt.JSON_PRERELEASE_INDEX;
_VERSION = 1;
IS_EDITABLE = false;
PAGE_MANAGE = UrlUtil.PG_MANAGE_PRERELEASE;
URL_REPO_DEFAULT = VeCt.URL_PRERELEASE;
DISPLAY_NAME = "prerelease content";
DISPLAY_NAME_PLURAL = "prereleases";
DEFAULT_AUTHOR = "Wizards of the Coast";
STYLE_BTN = "btn-primary";
IS_PREFER_DATE_ADDED = false;
/* -------------------------------------------- */
_pInit_doBindDragDrop () { /* No-op */ }
/* -------------------------------------------- */
async pGetSourceIndex (urlRoot) { return DataUtil.prerelease.pLoadSourceIndex(urlRoot); }
getFileUrl (path, urlRoot) { return DataUtil.prerelease.getFileUrl(path, urlRoot); }
pLoadTimestamps (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadTimestamps(urlRoot); }
pLoadPropIndex (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadPropIndex(urlRoot); }
pLoadMetaIndex (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadMetaIndex(urlRoot); }
/* -------------------------------------------- */
// region Editable
pGetEditableBrewDoc (brew) { return super.pGetEditableBrewDoc(brew); }
pGetOrCreateEditableBrewDoc () { return super.pGetOrCreateEditableBrewDoc(); }
pSetEditableBrewDoc () { return super.pSetEditableBrewDoc(); }
pGetEditableBrewEntity (prop, uniqueId, {isDuplicate = false} = {}) { return super.pGetEditableBrewEntity(prop, uniqueId, {isDuplicate}); }
pPersistEditableBrewEntity (prop, ent) { return super.pPersistEditableBrewEntity(prop, ent); }
pRemoveEditableBrewEntity (prop, uniqueId) { return super.pRemoveEditableBrewEntity(prop, uniqueId); }
pAddSource (sourceObj) { return super.pAddSource(sourceObj); }
pEditSource (sourceObj) { return super.pEditSource(sourceObj); }
pIsEditableSourceJson (sourceJson) { return super.pIsEditableSourceJson(sourceJson); }
pMoveOrCopyToEditableBySourceJson (sourceJson) { return super.pMoveOrCopyToEditableBySourceJson(sourceJson); }
pMoveToEditable ({brews}) { return super.pMoveToEditable({brews}); }
pCopyToEditable ({brews}) { return super.pCopyToEditable({brews}); }
// endregion
}
class _BrewUtil2 extends _BrewUtil2Base {
_STORAGE_KEY_LEGACY = "HOMEBREW_STORAGE";
_STORAGE_KEY_LEGACY_META = "HOMEBREW_META_STORAGE";
@@ -1291,6 +1417,7 @@ class _BrewUtil2 extends _BrewUtil2Base {
DISPLAY_NAME_PLURAL = "homebrews";
DEFAULT_AUTHOR = "";
STYLE_BTN = "btn-info";
IS_PREFER_DATE_ADDED = true;
/* -------------------------------------------- */
@@ -1344,7 +1471,7 @@ class _BrewUtil2 extends _BrewUtil2Base {
/* -------------------------------------------- */
async _pGetSourceIndex (urlRoot) { return DataUtil.brew.pLoadSourceIndex(urlRoot); }
async pGetSourceIndex (urlRoot) { return DataUtil.brew.pLoadSourceIndex(urlRoot); }
getFileUrl (path, urlRoot) { return DataUtil.brew.getFileUrl(path, urlRoot); }
@@ -1518,67 +1645,8 @@ class _BrewUtil2 extends _BrewUtil2Base {
// endregion
}
class _PrereleaseUtil extends _BrewUtil2Base {
_STORAGE_KEY_LEGACY = null;
_STORAGE_KEY_LEGACY_META = null;
_STORAGE_KEY = "PRERELEASE_STORAGE";
_STORAGE_KEY_META = "PRERELEASE_META_STORAGE";
_STORAGE_KEY_CUSTOM_URL = "PRERELEASE_CUSTOM_REPO_URL";
_STORAGE_KEY_MIGRATION_VERSION = "PRERELEASE_STORAGE_MIGRATION";
_PATH_LOCAL_DIR = "prerelease";
_PATH_LOCAL_INDEX = VeCt.JSON_PRERELEASE_INDEX;
_VERSION = 1;
IS_EDITABLE = false;
PAGE_MANAGE = UrlUtil.PG_MANAGE_PRERELEASE;
URL_REPO_DEFAULT = VeCt.URL_PRERELEASE;
DISPLAY_NAME = "prerelease content";
DISPLAY_NAME_PLURAL = "prereleases";
DEFAULT_AUTHOR = "Wizards of the Coast";
STYLE_BTN = "btn-primary";
/* -------------------------------------------- */
_pInit_doBindDragDrop () { /* No-op */ }
/* -------------------------------------------- */
async _pGetSourceIndex (urlRoot) { return DataUtil.prerelease.pLoadSourceIndex(urlRoot); }
getFileUrl (path, urlRoot) { return DataUtil.prerelease.getFileUrl(path, urlRoot); }
pLoadTimestamps (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadTimestamps(urlRoot); }
pLoadPropIndex (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadPropIndex(urlRoot); }
pLoadMetaIndex (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadMetaIndex(urlRoot); }
/* -------------------------------------------- */
// region Editable
pGetEditableBrewDoc (brew) { return super.pGetEditableBrewDoc(brew); }
pGetOrCreateEditableBrewDoc () { return super.pGetOrCreateEditableBrewDoc(); }
pSetEditableBrewDoc () { return super.pSetEditableBrewDoc(); }
pGetEditableBrewEntity (prop, uniqueId, {isDuplicate = false} = {}) { return super.pGetEditableBrewEntity(prop, uniqueId, {isDuplicate}); }
pPersistEditableBrewEntity (prop, ent) { return super.pPersistEditableBrewEntity(prop, ent); }
pRemoveEditableBrewEntity (prop, uniqueId) { return super.pRemoveEditableBrewEntity(prop, uniqueId); }
pAddSource (sourceObj) { return super.pAddSource(sourceObj); }
pEditSource (sourceObj) { return super.pEditSource(sourceObj); }
pIsEditableSourceJson (sourceJson) { return super.pIsEditableSourceJson(sourceJson); }
pMoveOrCopyToEditableBySourceJson (sourceJson) { return super.pMoveOrCopyToEditableBySourceJson(sourceJson); }
pMoveToEditable ({brews}) { return super.pMoveToEditable({brews}); }
pCopyToEditable ({brews}) { return super.pCopyToEditable({brews}); }
// endregion
}
globalThis.BrewUtil2 = new _BrewUtil2();
globalThis.PrereleaseUtil = new _PrereleaseUtil();
globalThis.BrewUtil2 = new _BrewUtil2({parent: globalThis.PrereleaseUtil}); // Homebrew can depend on prerelease, but not the other way around
class ManageBrewUi {
static _RenderState = class {
@@ -2496,13 +2564,16 @@ class GetBrewUi {
}
static mutateForFilters (brewInfo) {
if (brewInfo._brewAuthor && brewInfo._brewAuthor.toLowerCase().startsWith("sample -")) brewInfo._fMisc = ["Sample"];
brewInfo._fMisc = [];
if (brewInfo._brewAuthor && brewInfo._brewAuthor.toLowerCase().startsWith("sample -")) brewInfo._fMisc.push("Sample");
if (brewInfo.sources?.some(ab => ab.startsWith(Parser.SRC_UA_ONE_PREFIX))) brewInfo._fMisc.push("One D&D");
}
addToFilters (it, isExcluded) {
if (isExcluded) return;
this._typeFilter.addItem(it.props);
this._miscFilter.addItem(it._fMisc);
}
async _pPopulateBoxOptions (opts) {
@@ -2555,6 +2626,7 @@ class GetBrewUi {
case "category": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSortLower, "_brewPropDisplayName");
case "added": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSort, "_brewAdded");
case "modified": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSort, "_brewModified");
case "published": return this.constructor._sortUrlList_orFallback(a, b, SortUtil.ascSort, "_brewPublished");
default: throw new Error(`No sort order defined for property "${o.sortBy}"`);
}
}
@@ -2573,10 +2645,11 @@ class GetBrewUi {
async pInit () {
const urlRoot = await this._brewUtil.pGetCustomUrl();
const [timestamps, propIndex, metaIndex] = await Promise.all([
const [timestamps, propIndex, metaIndex, sourceIndex] = await Promise.all([
this._brewUtil.pLoadTimestamps(urlRoot),
this._brewUtil.pLoadPropIndex(urlRoot),
this._brewUtil.pLoadMetaIndex(urlRoot),
this._brewUtil.pGetSourceIndex(urlRoot),
]);
const pathToMeta = {};
@@ -2589,6 +2662,12 @@ class GetBrewUi {
});
});
Object.entries(sourceIndex)
.forEach(([src, path]) => {
if (!pathToMeta[path]) return;
(pathToMeta[path].sources ||= []).push(src);
});
this._dataList = Object.entries(pathToMeta)
.map(([path, meta]) => {
const out = {
@@ -2597,6 +2676,7 @@ class GetBrewUi {
name: UrlUtil.getFilename(path),
dirProp: this._brewUtil.getDirProp(meta.dir),
props: meta.props,
sources: meta.sources,
};
const spl = out.name.trim().replace(/\.json$/, "").split(";").map(it => it.trim());
@@ -2610,6 +2690,7 @@ class GetBrewUi {
out._brewAdded = timestamps[out.path]?.a ?? 0;
out._brewModified = timestamps[out.path]?.m ?? 0;
out._brewPublished = timestamps[out.path]?.p ?? 0;
out._brewInternalSources = metaIndex[out.name]?.n || [];
out._brewStatus = metaIndex[out.name]?.s || "ready";
out._brewPropDisplayName = this._brewUtil.getPropDisplayName(out.dirProp);
@@ -2662,13 +2743,17 @@ class GetBrewUi {
const $wrpMiniPills = $(`<div class="fltr__mini-view btn-group"></div>`);
const btnSortAddedPublished = this._brewUtil.IS_PREFER_DATE_ADDED
? `<button class="col-1-4 sort btn btn-default btn-xs" data-sort="added">Added</button>`
: `<button class="col-1-4 sort btn btn-default btn-xs" data-sort="published">Published</button>`;
const $wrpSort = $$`<div class="filtertools manbrew__filtertools btn-group input-group input-group--bottom ve-flex no-shrink">
<label class="col-0-5 pr-0 btn btn-default btn-xs ve-flex-vh-center">${rdState.cbAll}</label>
<button class="col-3-5 sort btn btn-default btn-xs" data-sort="name">Name</button>
<button class="col-3 sort btn btn-default btn-xs" data-sort="author">Author</button>
<button class="col-1-2 sort btn btn-default btn-xs" data-sort="category">Category</button>
<button class="col-1-4 sort btn btn-default btn-xs" data-sort="modified">Modified</button>
<button class="col-1-4 sort btn btn-default btn-xs" data-sort="added">Added</button>
${btnSortAddedPublished}
<button class="sort btn btn-default btn-xs ve-grow" disabled>Source</button>
</div>`;
@@ -2747,8 +2832,9 @@ class GetBrewUi {
}
_pRender_getUrlRowMeta (rdState, brewInfo, ix) {
const timestampAdded = brewInfo._brewAdded
? DatetimeUtil.getDateStr({date: new Date(brewInfo._brewAdded * 1000), isShort: true, isPad: true})
const epochAddedPublished = this._brewUtil.IS_PREFER_DATE_ADDED ? brewInfo._brewAdded : brewInfo._brewPublished;
const timestampAddedPublished = epochAddedPublished
? DatetimeUtil.getDateStr({date: new Date(epochAddedPublished * 1000), isShort: true, isPad: true})
: "";
const timestampModified = brewInfo._brewModified
? DatetimeUtil.getDateStr({date: new Date(brewInfo._brewModified * 1000), isShort: true, isPad: true})
@@ -2784,7 +2870,7 @@ class GetBrewUi {
e_({tag: "span", clazz: "col-3", text: brewInfo._brewAuthor}),
e_({tag: "span", clazz: "col-1-2 ve-text-center mobile__text-clip-ellipsis", text: brewInfo._brewPropDisplayName, title: brewInfo._brewPropDisplayName}),
e_({tag: "span", clazz: "col-1-4 ve-text-center code", text: timestampModified}),
e_({tag: "span", clazz: "col-1-4 ve-text-center code", text: timestampAdded}),
e_({tag: "span", clazz: "col-1-4 ve-text-center code", text: timestampAddedPublished}),
e_({
tag: "span",
clazz: "col-1 manbrew__source ve-text-center pr-0",

View File

@@ -177,6 +177,7 @@ PropOrder._MONSTER = [
"rest",
"daily",
"weekly",
"monthly",
"yearly",
"recharge",
"charges",

View File

@@ -2,7 +2,7 @@
// in deployment, `IS_DEPLOYED = "<version number>";` should be set below.
globalThis.IS_DEPLOYED = undefined;
globalThis.VERSION_NUMBER = /* 5ETOOLS_VERSION__OPEN */"1.197.3"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.VERSION_NUMBER = /* 5ETOOLS_VERSION__OPEN */"1.197.4"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.DEPLOYED_IMG_ROOT = undefined;
// for the roll20 script to set
globalThis.IS_VTT = false;
@@ -3108,17 +3108,6 @@ if (!IS_DEPLOYED && !IS_VTT && typeof window !== "undefined") {
}
}
});
// TODO(img) remove this in future
window.addEventListener("load", () => {
if (window.location?.host === "5etools-mirror-1.github.io") {
JqueryUtil.doToast({
type: "warning",
isAutoHide: false,
content: $(`<div>This mirror is no longer being updated/maintained, and will be shut down on March 1st 2024.<br>Please use <a href="https://5etools-mirror-2.github.io/" rel="noopener noreferrer">5etools-mirror-2.github.io</a> instead, and <a href="https://gist.github.com/5etools-mirror-2/40d6d80f40205882d3fa5006fae963a4" rel="noopener noreferrer">migrate your data</a>.</div>`),
});
}
});
}
// SORTING =============================================================================================================
@@ -4359,7 +4348,7 @@ globalThis.DataUtil = {
modInfo[prop].forEach(sp => (spellcasting[prop] = spellcasting[prop] || []).push(sp));
});
["recharge", "charges", "rest", "daily", "weekly", "yearly"].forEach(prop => {
["recharge", "charges", "rest", "daily", "weekly", "monthly", "yearly"].forEach(prop => {
if (!modInfo[prop]) return;
for (let i = 1; i <= 9; ++i) {
@@ -4442,7 +4431,7 @@ globalThis.DataUtil = {
spellcasting[prop].filter(it => !modInfo[prop].includes(it));
});
["recharge", "charges", "rest", "daily", "weekly", "yearly"].forEach(prop => {
["recharge", "charges", "rest", "daily", "weekly", "monthly", "yearly"].forEach(prop => {
if (!modInfo[prop]) return;
for (let i = 1; i <= 9; ++i) {
@@ -7567,4 +7556,15 @@ if (!IS_VTT && typeof window !== "undefined") {
// $(`.cancer__sidebar-rhs-inner--top`).append(`<div class="TEST_RHS_TOP"></div>`)
// $(`.cancer__sidebar-rhs-inner--bottom`).append(`<div class="TEST_RHS_BOTTOM"></div>`)
// });
// TODO(img) remove this in future
window.addEventListener("load", () => {
if (window.location?.host !== "5etools-mirror-1.github.io") return;
JqueryUtil.doToast({
type: "warning",
isAutoHide: false,
content: $(`<div>This mirror is no longer being updated/maintained, and will be shut down on March 1st 2024.<br>Please use <a href="https://5etools-mirror-2.github.io/" rel="noopener noreferrer">5etools-mirror-2.github.io</a> instead, and <a href="https://gist.github.com/5etools-mirror-2/40d6d80f40205882d3fa5006fae963a4" rel="noopener noreferrer">migrate your data</a>.</div>`),
});
});
}