This commit is contained in:
TheGiddyLimit
2024-04-23 23:14:25 +01:00
parent adec95d4ab
commit 99cb60d804
97 changed files with 8206 additions and 5742 deletions

View File

@@ -52,7 +52,6 @@ class BackgroundPage extends ListPage {
const pageFilter = new PageFilterBackgrounds();
super({
dataSource: DataUtil.background.loadJSON.bind(DataUtil.background),
dataSourceFluff: DataUtil.backgroundFluff.loadJSON.bind(DataUtil.backgroundFluff),
pFnGetFluff: Renderer.background.pGetFluff.bind(Renderer.background),

View File

@@ -691,7 +691,7 @@ class BestiaryPage extends ListPageMultiSource {
) {
Renderer.get().setFirstSection(true);
const $btnScaleCr = !ScaleCreature.isCrInScaleRange(mon) ? null : $(`<button id="btn-scale-cr" title="Scale Creature By CR (Highly Experimental)" class="mon__btn-scale-cr btn btn-xs btn-default ve-popwindow__hidden"><span class="glyphicon glyphicon-signal"></span></button>`)
const $btnScaleCr = !ScaleCreature.isCrInScaleRange(mon) ? null : $(`<button id="btn-scale-cr" title="Scale Creature By CR (Highly Experimental)" class="mon__btn-scale-cr btn btn-xs btn-default ve-popwindow__hidden no-print lst-is-exporting-image__hidden"><span class="glyphicon glyphicon-signal"></span></button>`)
.click((evt) => {
evt.stopPropagation();
const win = (evt.view || {}).window;
@@ -708,7 +708,7 @@ class BestiaryPage extends ListPageMultiSource {
});
});
const $btnResetScaleCr = !ScaleCreature.isCrInScaleRange(mon) ? null : $(`<button id="btn-reset-cr" title="Reset CR Scaling" class="mon__btn-reset-cr btn btn-xs btn-default ve-popwindow__hidden"><span class="glyphicon glyphicon-refresh"></span></button>`)
const $btnResetScaleCr = !ScaleCreature.isCrInScaleRange(mon) ? null : $(`<button id="btn-reset-cr" title="Reset CR Scaling" class="mon__btn-reset-cr btn btn-xs btn-default ve-popwindow__hidden no-print lst-is-exporting-image__hidden"><span class="glyphicon glyphicon-refresh"></span></button>`)
.click(() => Hist.setSubhash(VeCt.HASH_SCALED, null))
.toggle(isScaledCr);

View File

@@ -266,10 +266,10 @@ class BookUtil {
BookUtil.curRender.controls.$btnsPrv = BookUtil.curRender.controls.$btnsPrv || [];
let $btnPrev;
if (BookUtil.referenceId) {
$btnPrev = $(`<button class="btn btn-xs btn-default bk__nav-head-foot-item"><span class="glyphicon glyphicon-chevron-left"/>Previous</button>`)
$btnPrev = $(`<button class="btn btn-xs btn-default bk__nav-head-foot-item no-print"><span class="glyphicon glyphicon-chevron-left"></span>Previous</button>`)
.click(() => this._showBookContent_goToPage({mod: -1, bookId, ixChapter}));
} else {
$btnPrev = $(`<a href="#${this._showBookContent_goToPage({mod: -1, isGetHref: true, bookId, ixChapter})}" class="btn btn-xs btn-default bk__nav-head-foot-item"><span class="glyphicon glyphicon-chevron-left"/>Previous</a>`)
$btnPrev = $(`<a href="#${this._showBookContent_goToPage({mod: -1, isGetHref: true, bookId, ixChapter})}" class="btn btn-xs btn-default bk__nav-head-foot-item no-print"><span class="glyphicon glyphicon-chevron-left"></span>Previous</a>`)
.click(() => MiscUtil.scrollPageTop());
}
$btnPrev
@@ -278,7 +278,7 @@ class BookUtil {
BookUtil.curRender.controls.$btnsPrv.push($btnPrev);
(BookUtil.curRender.controls.$divsPrv = BookUtil.curRender.controls.$divsPrv || [])
.push($(`<div class="bk__nav-head-foot-item"/>`)
.push($(`<div class="bk__nav-head-foot-item no-print"></div>`)
.toggle(!showPrev)
.appendTo($wrpControls));
@@ -289,10 +289,10 @@ class BookUtil {
BookUtil.curRender.controls.$btnsNxt = BookUtil.curRender.controls.$btnsNxt || [];
let $btnNext;
if (BookUtil.referenceId) {
$btnNext = $(`<button class="btn btn-xs btn-default bk__nav-head-foot-item">Next<span class="glyphicon glyphicon-chevron-right"/></button>`)
$btnNext = $(`<button class="btn btn-xs btn-default bk__nav-head-foot-item no-print">Next<span class="glyphicon glyphicon-chevron-right"></span></button>`)
.click(() => this._showBookContent_goToPage({mod: 1, bookId, ixChapter}));
} else {
$btnNext = $(`<a href="#${this._showBookContent_goToPage({mod: 1, isGetHref: true, bookId, ixChapter})}" class="btn btn-xs btn-default bk__nav-head-foot-item">Next<span class="glyphicon glyphicon-chevron-right"/></a>`)
$btnNext = $(`<a href="#${this._showBookContent_goToPage({mod: 1, isGetHref: true, bookId, ixChapter})}" class="btn btn-xs btn-default bk__nav-head-foot-item no-print">Next<span class="glyphicon glyphicon-chevron-right"></span></a>`)
.click(() => MiscUtil.scrollPageTop());
}
$btnNext
@@ -301,7 +301,7 @@ class BookUtil {
BookUtil.curRender.controls.$btnsNxt.push($btnNext);
(BookUtil.curRender.controls.$divsNxt = BookUtil.curRender.controls.$divsNxt || [])
.push($(`<div class="bk__nav-head-foot-item"/>`)
.push($(`<div class="bk__nav-head-foot-item no-print"></div>`)
.toggle(!showNxt)
.appendTo($wrpControls));

View File

@@ -50,7 +50,6 @@ class CharCreationOptionsPage extends ListPage {
const pageFilter = new PageFilterCharCreationOptions();
super({
dataSource: DataUtil.charoption.loadJSON.bind(DataUtil.charoption),
dataSourceFluff: DataUtil.charoptionFluff.loadJSON.bind(DataUtil.charoptionFluff),
pFnGetFluff: Renderer.charoption.pGetFluff.bind(Renderer.charoption),

View File

@@ -65,6 +65,135 @@ class UtilClassesPage {
}
return "fresh";
}
/* -------------------------------------------- */
static _getRenderedClassSubclassFluff (
{
ent,
entFluff,
depthArr = null,
isRemoveRootName = false,
isAddLeadingHr = false,
isAddTrailingHr = false,
isAddSourceNote = false,
},
) {
entFluff = MiscUtil.copyFast(entFluff);
const hasEntries = !!entFluff?.entries?.length;
const hasImages = !!entFluff?.images?.length;
let stack = "";
Renderer.get().setFirstSection(true);
if (hasEntries) {
const renderer = Renderer.get();
if (depthArr) renderer.setDepthTracker(depthArr, {additionalPropsInherited: ["_isStandardSource"]});
entFluff.entries.filter(f => f.source === ent.source).forEach(f => f._isStandardSource = true);
entFluff.entries.forEach((f, i) => {
const cpy = MiscUtil.copyFast(f);
// Remove the name from the first section if it is a copy of the class/subclass name
if (
isRemoveRootName
&& i === 0
&& cpy.name
&& (
cpy.name.toLowerCase() === ent.name.toLowerCase()
|| cpy.name.toLowerCase() === `the ${ent.name.toLowerCase()}`
)
) {
delete cpy.name;
}
if (
isAddSourceNote
&& typeof cpy !== "string"
&& cpy.source
&& cpy.source !== ent.source
&& cpy.entries
) {
cpy.entries.unshift(`{@note The following information is from ${Parser.sourceJsonToFull(cpy.source)}${Renderer.utils.isDisplayPage(cpy.page) ? `, page ${cpy.page}` : ""}.}`);
}
stack += renderer.render(cpy);
});
}
if (hasImages) {
if (hasEntries) {
stack += `<div class="py-2"></div>`;
}
this._getFluffLayoutImages(entFluff.images)
.forEach(ent => stack += Renderer.get().render(ent));
}
if (hasImages || hasEntries) {
if (isAddLeadingHr) stack = Renderer.get().render({type: "hr"}) + stack;
if (isAddTrailingHr) stack += Renderer.get().render({type: "hr"});
}
return {
hasEntries,
hasImages,
rendered: stack || null,
};
}
static getRenderedClassFluff (
{
cls,
clsFluff,
depthArr = null,
isRemoveRootName = false,
isAddTrailingHr = false,
},
) {
return this._getRenderedClassSubclassFluff({
ent: cls,
entFluff: clsFluff,
depthArr,
isRemoveRootName,
isAddTrailingHr,
isAddSourceNote: true,
});
}
static getRenderedSubclassFluff (
{
sc,
scFluff,
},
) {
return this._getRenderedClassSubclassFluff({
ent: sc,
entFluff: scFluff,
isAddLeadingHr: true,
isAddTrailingHr: true,
});
}
static _getFluffLayoutImages (images) {
if (images.length === 1) {
return [
{
maxWidth: "98",
maxWidthUnits: "%",
...images[0],
},
];
}
return {
type: "gallery",
images: [...images],
};
}
}
class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProxyBase(ListPage))) {
@@ -228,7 +357,6 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
// Force data on any classes with unusual sources to behave as though they have normal sources
if (SourceUtil.isNonstandardSource(cls.source) || PrereleaseUtil.hasSourceJson(cls.source) || BrewUtil2.hasSourceJson(cls.source)) {
if (cls.fluff) cls.fluff.filter(f => f.source === cls.source).forEach(f => f._isStandardSource = true);
cls.subclasses.filter(sc => sc.source === cls.source).forEach(sc => sc._isStandardSource = true);
}
@@ -668,7 +796,7 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
this._render_renderClassTable();
this._render_renderSidebar();
await this._render_pRenderSubclassTabs();
this._render_renderClassContent();
await this._render_pRenderClassContent();
this._render_renderOutline();
this._render_renderAltViews();
// endregion
@@ -694,7 +822,19 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
this._addHookBase("feature", hkScrollToFeature);
hkScrollToFeature();
const hkDisplayFluff = () => $(`.cls-main__cls-fluff`).toggleVe(!!this._state.isShowFluff);
const hkDisplayFluff = () => {
$(`.cls-main__cls-fluff`).toggleVe(!!this._state.isShowFluff);
if (!this._state.isShowFluff) {
$(`.cls-main__sc-fluff`).hideVe();
} else {
$(`.cls-main__sc-fluff`)
.each((i, e) => {
const $e = $(e);
$e.toggleVe(!!this._state[$e.attr("data-subclass-id")]);
});
}
};
this._addHookBase("isShowFluff", hkDisplayFluff);
MiscUtil.pDefer(hkDisplayFluff);
@@ -1863,7 +2003,7 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
// endregion
}
_render_renderClassContent () {
async _render_pRenderClassContent () {
const $content = $(document.getElementById("pagecontent")).empty();
const cls = this.activeClass;
this._outlineData = {};
@@ -1872,43 +2012,36 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
$content.append(Renderer.utils.getBorderTr());
if (cls.fluff) {
const clsFluff = await Renderer.class.pGetFluff(cls);
if (clsFluff) {
const depthArr = [];
let stack = "";
Renderer.get().setFirstSection(true);
cls.fluff.forEach((f, i) => {
const cpy = MiscUtil.copyFast(f);
const {hasEntries, rendered} = UtilClassesPage.getRenderedClassFluff({cls, clsFluff, depthArr, isAddTrailingHr: true});
if (typeof cpy !== "string") {
if (f.source && f.source !== cls.source && cpy.entries) cpy.entries.unshift(`{@note The following information is from ${Parser.sourceJsonToFull(f.source)}${Renderer.utils.isDisplayPage(f.page) ? `, page ${f.page}` : ""}.}`);
}
if (rendered) {
const $trFluff = $(`<tr class="cls-main__cls-fluff"><td colspan="6"></td></tr>`).fastSetHtml(rendered).appendTo($content);
}
stack += Renderer.get().setDepthTracker(depthArr, {additionalPropsInherited: ["_isStandardSource"]}).render(cpy);
});
// Add a trailing `<hr>`
stack += Renderer.get().render({type: "section"});
const $trFluff = $(`<tr class="cls-main__cls-fluff"><td colspan="6"/></tr>`).fastSetHtml(stack).appendTo($content);
this._trackOutlineFluffData(depthArr);
if (hasEntries) this._trackOutlineFluffData(depthArr);
}
const ptrIsFirstSubclassLevel = {_: true};
cls.classFeatures.forEach((lvlFeatures, ixLvl) => {
const ptrsHasRenderedSubclass = {};
await cls.classFeatures.pSerialAwaitMap(async (lvlFeatures, ixLvl) => {
const ptrHasHandledSubclassFeatures = {_: false};
lvlFeatures.forEach((feature, ixFeature) => {
await lvlFeatures.pSerialAwaitMap(async (feature, ixFeature) => {
if (feature.source === cls.source) {
feature = MiscUtil.copyFast(feature);
feature._isStandardSource = true;
}
this._render_renderClassContent_renderFeature({
await this._render_renderClassContent_pRenderFeature({
ixLvl,
feature,
ixFeature,
ptrHasHandledSubclassFeatures,
ptrsHasRenderedSubclass,
ptrIsFirstSubclassLevel,
$content,
cls,
@@ -1918,10 +2051,11 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
// If there are out-of-sync subclass features (e.g. Stryxhaven subclasses), add a "fake" feature to compensate
if (!ptrHasHandledSubclassFeatures._ && this.constructor._hasSubclassFeaturesAtLevel(cls, ixLvl + 1)) {
this.constructor._hasSubclassFeaturesAtLevel(cls, ixLvl + 1);
this._render_renderClassContent_renderFeature({
await this._render_renderClassContent_pRenderFeature({
ixLvl,
feature: this.constructor._getFauxGainSubclassFeatureFeature(cls, ixLvl + 1),
ixFeature: -1,
ptrsHasRenderedSubclass,
ptrIsFirstSubclassLevel,
$content,
cls,
@@ -1945,12 +2079,13 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
.removePlugins("entries_namePrefix");
}
_render_renderClassContent_renderFeature (
async _render_renderClassContent_pRenderFeature (
{
ixLvl,
feature,
ixFeature,
ptrHasHandledSubclassFeatures,
ptrsHasRenderedSubclass,
ptrIsFirstSubclassLevel,
$content,
cls,
@@ -1987,12 +2122,15 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
.fastSetHtml(Renderer.get().setDepthTracker([]).render({type: "entries", entries: [{name: `{@note No Subclass Selected}`, type: "entries", entries: [`{@note <span class="clickable roller" data-jump-select-a-subclass="true">Select a subclass</span> to view its feature(s) here.}`]}]}))
.appendTo($content);
cls.subclasses.forEach(sc => {
await cls.subclasses.pSerialAwaitMap(async sc => {
const stateKey = UrlUtil.getStateKeySubclass(sc);
const scLvlFeatures = sc.subclassFeatures.find(it => it[0]?.level === ixLvl + 1);
if (!scLvlFeatures) return;
const scFluff = ptrsHasRenderedSubclass[stateKey] ? null : await Renderer.subclass.pGetFluff(sc);
ptrsHasRenderedSubclass[stateKey] = true;
scLvlFeatures.forEach((scFeature, ixScFeature) => {
const depthArr = [];
@@ -2000,7 +2138,8 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
? Renderer.get().render(`{@note This subclass was published on ${DatetimeUtil.getDateStr({date: new Date(Parser.sourceJsonToDate(sc.source))})}.}`)
: "";
const ptSources = ptrIsFirstSubclassLevel._ === true && sc.otherSources ? `{@note {@b Subclass source:} ${Renderer.utils.getSourceAndPageHtml(sc)}}` : "";
const toRender = (ptDate || ptSources) && scFeature.entries ? MiscUtil.copyFast(scFeature) : scFeature;
const toRender = MiscUtil.copyFast(scFeature);
if (ptDate && toRender.entries) toRender.entries.unshift(ptDate);
if (ptSources && toRender.entries) toRender.entries.push(ptSources);
@@ -2030,7 +2169,7 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
if (source === sc.source) return {isSkip: true};
},
fn: () => {
const $trSubclassFeature = $(`<tr class="cls-main__sc-feature" data-subclass-id="${UrlUtil.getStateKeySubclass(sc)}"><td colspan="6"/></tr>`)
const $trSubclassFeature = $(`<tr class="cls-main__sc-feature" data-subclass-id="${UrlUtil.getStateKeySubclass(sc)}"><td colspan="6"></td></tr>`)
.fastSetHtml(Renderer.get().setDepthTracker(depthArr, {additionalProps: ["isReprinted"], additionalPropsInherited: ["_isStandardSource", "isClassFeatureVariant"]}).render(toRender))
.appendTo($content);
},
@@ -2039,6 +2178,14 @@ class ClassesPage extends MixinComponentGlobalState(MixinBaseComponent(MixinProx
Renderer.get().removePlugins("entries_namePrefix");
this._trackOutlineScData(stateKey, ixLvl + 1, ixScFeature, depthArr);
const {rendered: rdScFluff} = UtilClassesPage.getRenderedSubclassFluff({sc, scFluff});
if (!rdScFluff?.length) return;
$(`<tr class="cls-main__sc-fluff" data-subclass-id="${UrlUtil.getStateKeySubclass(sc)}"><td colspan="6"></td></tr>`)
.fastSetHtml(rdScFluff)
.appendTo($content);
});
});
@@ -2369,7 +2516,7 @@ ClassesPage.ClassBookView = class extends BookModeViewBase {
const cls = this._classPage.activeClass;
// Top bar
const $pnlMenu = $(`<div class="cls-bkmv__wrp-tabs ve-flex-h-center"/>`).appendTo($wrpContent);
const $pnlMenu = $(`<div class="cls-bkmv__wrp-tabs ve-flex-h-center no-print"></div>`).appendTo($wrpContent);
// Main panel
const $tblBook = $(`<table class="w-100 stats stats--book stats--book-large stats--bkmv"></div>`);
@@ -2381,23 +2528,18 @@ ClassesPage.ClassBookView = class extends BookModeViewBase {
Renderer.get().recursiveRender({type: "section", name: cls.name}, renderStack);
renderStack.push(`</td></tr>`);
renderStack.push(`<tr class="text" data-cls-book-fluff="true"><td colspan="6" class="py-3 px-5">`);
const clsFluff = await Renderer.class.pGetFluff(cls);
if (clsFluff) {
const {hasEntries, rendered} = UtilClassesPage.getRenderedClassFluff({cls, clsFluff, isRemoveRootName: true});
if (rendered) {
renderStack.push(`<tr class="text" data-cls-book-fluff="true"><td colspan="6" class="py-3 px-5">`);
renderStack.push(rendered);
renderStack.push(`</td></tr>`);
}
}
Renderer.get().setFirstSection(true);
(cls.fluff || []).forEach((f, i) => {
f = MiscUtil.copyFast(f);
// Remove the name from the first section if it is a copy of the class name
if (i === 0 && f.name && f.name.toLowerCase() === cls.name.toLowerCase()) {
delete f.name;
}
if (f.source && f.source !== cls.source && f.entries) {
f.entries.unshift(`{@note The following information is from ${Parser.sourceJsonToFull(f.source)}${Renderer.utils.isDisplayPage(f.page) ? `, page ${f.page}` : ""}.}`);
}
Renderer.get().recursiveRender(f, renderStack);
});
renderStack.push(`</td></tr>`);
renderStack.push(`<tr class="text" data-cls-book-cf="true"><td colspan="6" class="py-3 px-5">`);
cls.classFeatures.forEach(lvl => {
@@ -2405,14 +2547,26 @@ ClassesPage.ClassBookView = class extends BookModeViewBase {
});
renderStack.push(`</td></tr>`);
cls.subclasses
await cls.subclasses
.filter(sc => !ClassesPage.isSubclassExcluded_(cls, sc))
.forEach((sc, ixSubclass) => {
renderStack.push(`<tr data-cls-book-sc-ix="${ixSubclass}" class="cls-main__sc-feature"><td colspan="6" class="py-3 px-5">`);
sc.subclassFeatures.forEach(lvl => {
.pSerialAwaitMap(async (sc, ixSubclass) => {
const scFluff = await Renderer.subclass.pGetFluff(sc);
sc.subclassFeatures.forEach((lvl, ix) => {
renderStack.push(`<tr data-cls-book-sc-ix="${ixSubclass}" class="cls-main__sc-feature"><td colspan="6" class="py-3 px-5">`);
lvl.forEach(f => Renderer.get().recursiveRender(f, renderStack));
renderStack.push(`</td></tr>`);
if (ix !== 0) return;
const {rendered: rdScFluff} = UtilClassesPage.getRenderedSubclassFluff({sc, scFluff});
if (!rdScFluff?.length) return;
renderStack.push(`<tr data-cls-book-sc-fluff-ix="${ixSubclass}" class="cls-main__sc-fluff"><td colspan="6" class="py-3 px-5">`);
renderStack.push(rdScFluff);
renderStack.push(`</td></tr>`);
});
renderStack.push(`</td></tr>`);
});
renderStack.push(Renderer.utils.getBorderTr());
$tblBook.append(renderStack.join(""));
@@ -2454,6 +2608,16 @@ ClassesPage.ClassBookView = class extends BookModeViewBase {
this._parent.addHook(stateKey, hkShowHide);
hkShowHide();
const hkShowHideFluff = () => {
const isActive = !!this._parent.get(stateKey) && !!this._parent.get("isShowFluff");
$wrpContent.find(`[data-cls-book-sc-fluff-ix="${i}"]`).toggleVe(!!isActive);
};
(this._hooks[stateKey] ||= []).push(hkShowHideFluff);
this._parent.addHook(stateKey, hkShowHideFluff);
(this._hooks["isShowFluff"] ||= []).push(hkShowHideFluff);
this._parent.addHook("isShowFluff", hkShowHideFluff);
hkShowHideFluff();
$pnlMenu.append($btnToggleSc);
});
@@ -2463,17 +2627,17 @@ ClassesPage.ClassBookView = class extends BookModeViewBase {
$btnToggleCf.toggleClass("cls__btn-cf--active", isActive);
$dispFeatures.toggleVe(!!isActive);
};
(this._hooks["isHideFeatures"] = this._hooks["isHideFeatures"] || []).push(hkFeatures);
(this._hooks["isHideFeatures"] ||= []).push(hkFeatures);
this._parent.addHook("isHideFeatures", hkFeatures);
hkFeatures();
const hkFluff = () => {
const $dispFluff = $wrpContent.find(`[data-cls-book-fluff="true"]`);
const isHidden = !this._parent.get("isShowFluff");
$btnToggleInfo.toggleVe(!!isHidden);
$btnToggleInfo.toggleClass("active", !isHidden);
$dispFluff.toggleVe(!isHidden);
};
(this._hooks["isShowFluff"] = this._hooks["isShowFluff"] || []).push(hkFluff);
(this._hooks["isShowFluff"] ||= []).push(hkFluff);
this._parent.addHook("isShowFluff", hkFluff);
hkFluff();

View File

@@ -198,9 +198,9 @@ class SpellParser extends BaseParser {
static _addTags (stats, options) {
DamageInflictTagger.tryRun(stats, options);
DamageResVulnImmuneTagger.tryRun(stats, "damageResist", options);
DamageResVulnImmuneTagger.tryRun(stats, "damageImmune", options);
DamageResVulnImmuneTagger.tryRun(stats, "damageVulnerable", options);
DamageResTagger.tryRun(stats, options);
DamageVulnTagger.tryRun(stats, options);
DamageImmuneTagger.tryRun(stats, options);
ConditionInflictTagger.tryRun(stats, options);
SavingThrowTagger.tryRun(stats, options);
AbilityCheckTagger.tryRun(stats, options);
@@ -405,6 +405,19 @@ class SpellParser extends BaseParser {
;
}
static _getComponentCurrencyMult ({mCost}) {
const {currency, currencyLong} = mCost.groups;
if (currency) return Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf(currency.toLowerCase())];
switch (currencyLong.toLowerCase()) {
case "gold": {
return Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf("gp")];
}
default: throw new Error("Unimplemented!");
}
}
static _setCleanComponents (stats, line, options) {
const components = ConvertUtil.getStatblockLineHeaderText({reStartStr: this._RE_START_COMPONENTS, line});
const parts = components.split(StrUtil.COMMAS_NOT_IN_PARENTHESES_REGEX);
@@ -422,11 +435,11 @@ class SpellParser extends BaseParser {
default: {
if (lowerPt.startsWith("m ")) {
const materialText = pt.replace(/^m\s*\((.*)\)$/i, "$1").trim();
const mCost = /(\d*,?\d+)\s?(cp|sp|ep|gp|pp)/gi.exec(materialText);
const mCost = /(\d*,?\d+)\s?(?:(?<currency>cp|sp|ep|gp|pp)|(?:(?<currencyLong>gold)(?: pieces)?))/gi.exec(materialText);
const isConsumed = pt.toLowerCase().includes("consume");
if (mCost) {
const valueMult = Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf(mCost[2].toLowerCase())];
const valueMult = this._getComponentCurrencyMult({mCost});
const valueNum = Number(mCost[1].replace(/,/g, ""));
stats.components.m = {

View File

@@ -1,40 +1,67 @@
"use strict";
class DamageTagger {
static _addDamageTypeToArray (arr, str, options) {
static _addDamageTypeToSet (set, 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);
if (str === "all" || str === "one" || str === "a") Parser.DMG_TYPES.forEach(it => set.add(it));
else if (Parser.DMG_TYPES.includes(str)) set.add(str);
else options.cbWarning(`Unknown damage type "${str}"`);
}
}
class DamageInflictTagger extends DamageTagger {
static tryRun (sp, options) {
sp.damageInflict = [];
const tags = new Set();
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 (m[1]) this._addDamageTypeToSet(tags, m[1], options);
if (m[2]) m[2].split(",").map(it => it.trim()).filter(Boolean).forEach(str => this._addDamageTypeToSet(tags, str, options));
if (m[3]) this._addDamageTypeToSet(tags, m[3].split(" ").last(), options);
});
if (!sp.damageInflict.length) delete sp.damageInflict;
else sp.damageInflict = [...new Set(sp.damageInflict)].sort(SortUtil.ascSort);
if (!tags.size) return;
sp.damageInflict = [...tags].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);
static get _RE () {
return (this.__RE ||= new RegExp(`${this._TYPE} to (?<ptBase>\\w+)(?<ptList>(?:, \\w+)*)(?<ptConj>,? or \\w+)? damage`, "gi"));
}
static tryRun (sp, options) {
const tags = new Set();
JSON.stringify([sp.entries, sp.entriesHigherLevel]).replace(this._RE, (...m) => {
const {ptBase, ptList, ptConj} = m.last();
if (ptBase) this._addDamageTypeToSet(tags, ptBase, options);
if (ptList) ptList.split(",").map(it => it.trim()).filter(Boolean).forEach(str => this._addDamageTypeToSet(tags, str, options));
if (ptConj) this._addDamageTypeToSet(tags, ptConj.split(" ").last(), options);
});
if (!sp[prop].length) delete sp[prop];
else sp[prop] = [...new Set(sp[prop])].sort(SortUtil.ascSort);
if (!tags.size) return;
sp[this._PROP] = [...tags].sort(SortUtil.ascSort);
}
}
class DamageResTagger extends DamageResVulnImmuneTagger {
static _TYPE = "resistance";
static _PROP = "damageResist";
}
globalThis.DamageResTagger = DamageResTagger;
class DamageVulnTagger extends DamageResVulnImmuneTagger {
static _TYPE = "vulnerability";
static _PROP = "damageVulnerable";
}
globalThis.DamageVulnTagger = DamageVulnTagger;
class DamageImmuneTagger extends DamageResVulnImmuneTagger {
static _TYPE = "immunity";
static _PROP = "damageImmune";
}
globalThis.DamageImmuneTagger = DamageImmuneTagger;
class ConditionInflictTagger {
static tryRun (sp, options) {
sp.conditionInflict = [];
@@ -438,7 +465,6 @@ class AffectedCreatureTypeTagger {
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;

View File

@@ -55,7 +55,6 @@ class FeatsPage extends ListPage {
const pageFilter = new PageFilterFeats();
super({
dataSource: DataUtil.feat.loadJSON.bind(DataUtil.feat),
dataSourceFluff: DataUtil.featFluff.loadJSON.bind(DataUtil.featFluff),
pFnGetFluff: Renderer.feat.pGetFluff.bind(Renderer.feat),

View File

@@ -93,9 +93,11 @@ class PageFilterClassesBase extends PageFilter {
opts = opts || {};
const subclassExclusions = opts.subclassExclusions || {};
// region Sources
// Note that we assume that, for fluff from a given source, a class/subclass will exist from that source.
// This allows us to skip loading the class/subclass fluff in order to track the fluff's sources.
this._sourceFilter.addItem(cls.source);
if (cls.fluff) cls.fluff.forEach(it => this._addEntrySourcesToFilter(it));
cls.classFeatures.forEach(lvlFeatures => lvlFeatures.forEach(feature => this._addEntrySourcesToFilter(feature)));
cls.subclasses.forEach(sc => {
@@ -105,6 +107,7 @@ class PageFilterClassesBase extends PageFilter {
sc.subclassFeatures.forEach(lvlFeatures => lvlFeatures.forEach(feature => this._addEntrySourcesToFilter(feature)));
}
});
// endregion
}
async _pPopulateBoxOptions (opts) {

View File

@@ -978,11 +978,6 @@ class ListPage {
* @param [opts.prereleaseDataSource] Function to fetch prerelease data.
* @param [opts.brewDataSource] Function to fetch brew data.
* @param [opts.pFnGetFluff] Function to fetch fluff for a given entity.
* @param [opts.dataSourceFluff] Fluff JSON data url or function to fetch fluff data.
* @param [opts.filters] Array of filters to use in the filter box. (Either `filters` and `filterSource` or
* `pageFilter` must be specified.)
* @param [opts.filterSource] Source filter. (Either `filters` and `filterSource` or
* `pageFilter` must be specified.)
* @param [opts.pageFilter] PageFilter implementation for this page. (Either `filters` and `filterSource` or
* `pageFilter` must be specified.)
* @param opts.listOptions Other list options.
@@ -1010,9 +1005,6 @@ class ListPage {
this._prereleaseDataSource = opts.prereleaseDataSource;
this._brewDataSource = opts.brewDataSource;
this._pFnGetFluff = opts.pFnGetFluff;
this._dataSourcefluff = opts.dataSourceFluff;
this._filters = opts.filters;
this._filterSource = opts.filterSource;
this._pageFilter = opts.pageFilter;
this._listOptions = opts.listOptions || {};
this._dataProps = opts.dataProps;
@@ -1034,7 +1026,6 @@ class ListPage {
this._dataList = [];
this._ixData = 0;
this._bookView = null;
this._bookViewToShow = null;
this._sublistManager = null;
this._btnsTabs = {};
this._lastRender = {};
@@ -1764,6 +1755,11 @@ class ListPage {
evt => this._sublistManager.pHandleClick_save(evt),
),
null,
new ContextUtil.Action(
"Export as Image (SHIFT to Copy Image)",
evt => this._pHandleClick_exportAsImage({evt, isFast: evt.shiftKey, $eleCopyEffect: $btnOptions}),
),
null,
new ContextUtil.Action(
"Download Pinned List (SHIFT to Copy Link)",
evt => this._sublistManager.pHandleClick_download({isUrl: evt.shiftKey, $eleCopyEffect: $btnOptions}),
@@ -1817,8 +1813,15 @@ class ListPage {
const menu = ContextUtil.getMenu(contextOptions);
$btnOptions
.off("mousedown")
.on("mousedown", evt => {
evt.preventDefault();
})
.off("click")
.on("click", evt => ContextUtil.pOpenMenu(evt, menu));
.on("click", async evt => {
evt.preventDefault();
await ContextUtil.pOpenMenu(evt, menu);
});
}
async _handleGenericContextMenuClick_pDoMassPopout (evt, ele, selection) {
@@ -2011,6 +2014,93 @@ class ListPage {
/** @abstract */
_renderStats_doBuildStatsTab ({ent}) { throw new Error("Unimplemented!"); }
/* -------------------------------------------- */
static _OFFSET_WINDOW_EXPORT_AS_IMAGE = 17;
async _pHandleClick_exportAsImage ({evt, isFast, $eleCopyEffect}) {
if (typeof domtoimage === "undefined") await import("../lib/dom-to-image-more.min.js");
const ent = this._dataList[Hist.lastLoadedId];
const optsDomToImage = {
// FIXME(Future) doesn't seem to have the desired effect; `lst__is-exporting-image` bodge used instead
adjustClonedNode: (node, clone, isAfter) => {
if (node.classList && node.classList.contains("stats-source") && !isAfter) {
clone.style.paddingRight = "0px";
}
return clone;
},
};
if (isFast) {
let blob;
try {
this._$pgContent.addClass("lst__is-exporting-image");
blob = await domtoimage.toBlob(this._$pgContent[0], optsDomToImage);
} finally {
this._$pgContent.removeClass("lst__is-exporting-image");
}
const isCopy = await MiscUtil.pCopyBlobToClipboard(blob);
if (isCopy) JqueryUtil.showCopiedEffect($eleCopyEffect, "Copied!");
return;
}
const html = this._$pgContent[0].outerHTML;
const page = UrlUtil.getCurrentPage();
const $cpy = $(html)
.addClass("lst__is-exporting-image");
$cpy.find();
const $btnCpy = $(`<button class="btn btn-default btn-xs" title="SHIFT to Copy and Close">Copy</button>`)
.on("click", async evt => {
const blob = await domtoimage.toBlob($cpy[0], optsDomToImage);
const isCopy = await MiscUtil.pCopyBlobToClipboard(blob);
if (isCopy) JqueryUtil.showCopiedEffect($btnCpy, "Copied!");
if (isCopy && evt.shiftKey) hoverWindow.doClose();
});
const $btnSave = $(`<button class="btn btn-default btn-xs" title="SHIFT to Save and Close">Save</button>`)
.on("click", async evt => {
const dataUrl = await domtoimage.toPng($cpy[0], optsDomToImage);
DataUtil.userDownloadDataUrl(`${ent.name}.png`, dataUrl);
if (evt.shiftKey) hoverWindow.doClose();
});
const width = this._$pgContent[0].getBoundingClientRect().width;
const posBtn = $eleCopyEffect[0].getBoundingClientRect().toJSON();
const hoverWindow = Renderer.hover.getShowWindow(
$$`<div class="ve-flex-col">
<div class="split-v-center mb-2 px-2 mt-2">
<i class="mr-2">Optionally resize the width of the window, then Copy or Save.</i>
<div class="btn-group">
${$btnCpy}
${$btnSave}
</div>
</div>
${$cpy}
</div>`,
Renderer.hover.getWindowPositionExact(
posBtn.left - width + posBtn.width - this.constructor._OFFSET_WINDOW_EXPORT_AS_IMAGE,
posBtn.top + posBtn.height + this.constructor._OFFSET_WINDOW_EXPORT_AS_IMAGE,
evt,
),
{
title: `Image Export - ${ent.name}`,
isPermanent: true,
isBookContent: page === UrlUtil.PG_RECIPES,
isResizeOnlyWidth: true,
isHideBottomBorder: true,
width,
},
);
}
}
class ListPageBookView extends BookModeViewBase {

View File

@@ -52,7 +52,6 @@ class ObjectsPage extends ListPage {
super({
dataSource: DataUtil.object.loadJSON.bind(DataUtil.object),
dataSourceFluff: DataUtil.objectFluff.loadJSON.bind(DataUtil.objectFluff),
pFnGetFluff,

View File

@@ -647,7 +647,7 @@ class Omnisearch {
const $btnToTop = $(`<button class="btn btn-sm btn-default" title="To Top"><span class="glyphicon glyphicon-arrow-up"/></button>`)
.click(() => MiscUtil.scrollPageTop());
const $wrpTop = $$`<div class="bk__to-top">
const $wrpTop = $$`<div class="bk__to-top no-print">
${$btnToTop}
</div>`.appendTo(document.body);

View File

@@ -77,7 +77,6 @@ class OptionalFeaturesPage extends ListPage {
super({
dataSource: DataUtil.optionalfeature.loadJSON.bind(DataUtil.optionalfeature),
dataSourceFluff: DataUtil.featFluff.loadJSON.bind(DataUtil.featFluff),
pFnGetFluff: Renderer.optionalfeature.pGetFluff.bind(Renderer.optionalfeature),

View File

@@ -1453,6 +1453,7 @@ Parser.SP_MISC_TAG_TO_FULL = {
AAD: "Additional Attack Damage",
OBJ: "Affects Objects",
ADV: "Grants Advantage",
PIR: "Permanent If Repeated",
};
Parser.spMiscTagToFull = function (type) {
return Parser._parse_aToB(Parser.SP_MISC_TAG_TO_FULL, type);

View File

@@ -59,7 +59,6 @@ class RacesPage extends ListPage {
const pageFilter = new PageFilterRaces();
super({
dataSource: DataUtil.race.loadJSON.bind(DataUtil.race, {isAddBaseRaces: true}),
dataSourceFluff: DataUtil.raceFluff.loadJSON.bind(DataUtil.raceFluff),
prereleaseDataSource: DataUtil.race.loadPrerelease.bind(DataUtil.race),
brewDataSource: DataUtil.race.loadBrew.bind(DataUtil.race),

View File

@@ -934,11 +934,11 @@ globalThis.Renderer = function () {
};
this._getPtExpandCollapse = function () {
return `<span class="rd__h-toggle ml-2 clickable no-select" data-rd-h-toggle-button="true" title="Toggle Visibility (CTRL to Toggle All)">[\u2013]</span>`;
return `<span class="rd__h-toggle ml-2 clickable no-select no-print lst-is-exporting-image__hidden" data-rd-h-toggle-button="true" title="Toggle Visibility (CTRL to Toggle All)">[\u2013]</span>`;
};
this._getPtExpandCollapseSpecial = function () {
return `<span class="rd__h-toggle ml-2 clickable no-select" data-rd-h-special-toggle-button="true" title="Toggle Visibility (CTRL to Toggle All)">[\u2013]</span>`;
return `<span class="rd__h-toggle ml-2 clickable no-select no-print lst-is-exporting-image__hidden" data-rd-h-special-toggle-button="true" title="Toggle Visibility (CTRL to Toggle All)">[\u2013]</span>`;
};
this._renderInset = function (entry, textStack, meta, options) {
@@ -1887,6 +1887,20 @@ globalThis.Renderer = function () {
break;
}
case "@5etoolsImg": {
const [displayText, page] = Renderer.splitTagByPipe(text);
const fauxEntry = {
type: "link",
href: {
type: "external",
url: UrlUtil.link(this.getMediaUrl("img", page)),
},
text: displayText,
};
this._recursiveRender(fauxEntry, textStack, meta);
break;
}
// OTHER HOVERABLES ////////////////////////////////////////////////////////////////////////////////
case "@footnote": {
@@ -2817,7 +2831,7 @@ Renderer.utils = class {
let pageLinkPart;
if (opts.page) {
const hash = UrlUtil.URL_TO_HASH_BUILDER[opts.page](it);
dataPart = `data-page="${opts.page}" data-source="${it.source.escapeQuotes()}" data-hash="${hash.escapeQuotes()}" ${opts.extensionData != null ? `data-extension='${JSON.stringify(opts.extensionData).escapeQuotes()}` : ""}'`;
dataPart = `data-page="${opts.page}" data-source="${it.source.escapeQuotes()}" data-hash="${hash.escapeQuotes()}" ${opts.extensionData != null ? `data-extension='${JSON.stringify(opts.extensionData).escapeQuotes()}'` : ""}`;
pageLinkPart = SourceUtil.getAdventureBookSourceHref(it.source, it.page);
// Enable Rivet import for entities embedded in entries
@@ -3122,28 +3136,31 @@ Renderer.utils = class {
return fluff;
}
static async pGetFluff ({entity, pFnPostProcess, fnGetFluffData, fluffUrl, fluffBaseUrl, fluffProp} = {}) {
let predefinedFluff = await Renderer.utils.pGetPredefinedFluff(entity, fluffProp);
static async _pGetFluff ({entity, fluffProp} = {}) {
const fluffEntity = await DataLoader.pCacheAndGet(fluffProp, entity.source, UrlUtil.URL_TO_HASH_BUILDER[fluffProp](entity));
if (fluffEntity) return fluffEntity;
if (entity._versionBase_name && entity._versionBase_source) {
return DataLoader.pCacheAndGet(fluffProp, entity.source, UrlUtil.URL_TO_HASH_BUILDER[fluffProp]({
name: entity._versionBase_name,
source: entity._versionBase_source,
}));
}
return null;
}
static async pGetFluff ({entity, pFnPostProcess, fluffProp} = {}) {
const predefinedFluff = await Renderer.utils.pGetPredefinedFluff(entity, fluffProp);
if (predefinedFluff) {
if (pFnPostProcess) predefinedFluff = await pFnPostProcess(predefinedFluff);
if (pFnPostProcess) return pFnPostProcess(predefinedFluff);
return predefinedFluff;
}
if (!fnGetFluffData && !fluffBaseUrl && !fluffUrl) return null;
const fluffIndex = fluffBaseUrl ? await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffBaseUrl}fluff-index.json`) : null;
if (fluffIndex && !fluffIndex[entity.source]) return null;
const data = fnGetFluffData ? await fnGetFluffData() : fluffIndex && fluffIndex[entity.source]
? await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffBaseUrl}${fluffIndex[entity.source]}`)
: await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffUrl}`);
if (!data) return null;
let fluff = (data[fluffProp] || []).find(it => it.name === entity.name && it.source === entity.source);
if (!fluff && entity._versionBase_name && entity._versionBase_source) fluff = (data[fluffProp] || []).find(it => it.name === entity._versionBase_name && it.source === entity._versionBase_source);
const fluff = await Renderer.utils._pGetFluff({entity, fluffProp});
if (!fluff) return null;
// Avoid modifying the original object
if (pFnPostProcess) fluff = await pFnPostProcess(fluff);
if (pFnPostProcess) return pFnPostProcess(fluff);
return fluff;
}
@@ -4612,6 +4629,10 @@ Renderer.tag = class {
tagName = "5etools";
};
static Tag5etoolsImg = class extends this._TagPipedNoDisplayText {
tagName = "5etoolsImg";
};
static TagAdventure = class extends this._TagPipedNoDisplayText {
tagName = "adventure";
};
@@ -5010,6 +5031,7 @@ Renderer.tag = class {
new this.TagCoinflip(),
new this.Tag5etools(),
new this.Tag5etoolsImg(),
new this.TagAdventure(),
new this.TagBook(),
new this.TagFilter(),
@@ -5361,7 +5383,6 @@ Renderer.feat = class {
static pGetFluff (feat) {
return Renderer.utils.pGetFluff({
entity: feat,
fnGetFluffData: DataUtil.featFluff.loadJSON.bind(DataUtil.featFluff),
fluffProp: "featFluff",
});
}
@@ -5508,20 +5529,51 @@ Renderer.class = class {
);
});
}
static pGetFluff (cls) {
// Handle legacy/deprecated class fluff
// TODO(Future) remove this after ~July 2024
if (cls.fluff instanceof Array) {
cls = {...cls};
cls.fluff = {entries: cls.fluff};
}
return Renderer.utils.pGetFluff({
entity: cls,
fluffProp: "classFluff",
});
}
};
Renderer.subclass = class {
static getCompactRenderedString (sc) {
const entries = MiscUtil.copyFast((sc.subclassFeatures || []).flat());
if (entries[0]?.name === sc.name) delete entries[0].name;
const scEntry = {
type: "section",
name: sc.name,
source: sc.source,
page: sc.page,
entries: MiscUtil.copyFast((sc.subclassFeatures || []).flat()),
entries,
};
return Renderer.hover.getGenericCompactRenderedString(scEntry);
}
static pGetFluff (sc) {
return Renderer.utils.pGetFluff({
entity: sc,
fluffProp: "subclassFluff",
});
}
};
Renderer.classSubclass = class {
static pGetFluff (clsOrSc) {
if (clsOrSc.__prop === "subclass") return Renderer.subclass.pGetFluff(clsOrSc);
return Renderer.class.pGetFluff(clsOrSc);
}
};
Renderer.spell = class {
@@ -6083,7 +6135,6 @@ Renderer.spell = class {
static pGetFluff (sp) {
return Renderer.utils.pGetFluff({
entity: sp,
fluffBaseUrl: `data/spells/`,
fluffProp: "spellFluff",
});
}
@@ -6111,7 +6162,6 @@ Renderer.condition = class {
static pGetFluff (it) {
return Renderer.utils.pGetFluff({
entity: it,
fnGetFluffData: it.__prop === "condition" ? DataUtil.conditionFluff.loadJSON.bind(DataUtil.conditionFluff) : null,
fluffProp: it.__prop === "condition" ? "conditionFluff" : "diseaseFluff",
});
}
@@ -6131,7 +6181,6 @@ Renderer.background = class {
static pGetFluff (bg) {
return Renderer.utils.pGetFluff({
entity: bg,
fnGetFluffData: DataUtil.backgroundFluff.loadJSON.bind(DataUtil.backgroundFluff),
fluffProp: "backgroundFluff",
});
}
@@ -6219,7 +6268,6 @@ Renderer.optionalfeature = class {
static pGetFluff (ent) {
return Renderer.utils.pGetFluff({
entity: ent,
fnGetFluffData: DataUtil.optionalfeatureFluff.loadJSON.bind(DataUtil.optionalfeatureFluff),
fluffProp: "optionalfeatureFluff",
});
}
@@ -6259,7 +6307,6 @@ Renderer.reward = class {
static pGetFluff (ent) {
return Renderer.utils.pGetFluff({
entity: ent,
fnGetFluffData: DataUtil.rewardFluff.loadJSON.bind(DataUtil.rewardFluff),
fluffProp: "rewardFluff",
});
}
@@ -6766,7 +6813,6 @@ Renderer.race = class {
static pGetFluff (race) {
return Renderer.utils.pGetFluff({
entity: race,
fnGetFluffData: DataUtil.raceFluff.loadJSON.bind(DataUtil.raceFluff),
fluffProp: "raceFluff",
});
}
@@ -6952,7 +6998,6 @@ Renderer.object = class {
static pGetFluff (obj) {
return Renderer.utils.pGetFluff({
entity: obj,
fnGetFluffData: DataUtil.objectFluff.loadJSON.bind(DataUtil.objectFluff),
fluffProp: "objectFluff",
});
}
@@ -7087,7 +7132,6 @@ Renderer.traphazard = class {
static pGetFluff (ent) {
return Renderer.utils.pGetFluff({
entity: ent,
fnGetFluffData: ent.__prop === "trap" ? DataUtil.trapFluff.loadJSON.bind(DataUtil.trapFluff) : DataUtil.hazardFluff.loadJSON.bind(DataUtil.hazardFluff),
fluffProp: ent.__prop === "trap" ? "trapFluff" : "hazardFluff",
});
}
@@ -7529,7 +7573,7 @@ Renderer.monster = class {
return e_({
tag: "select",
clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden",
clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden no-print lst-is-exporting-image__hidden",
name: "mon__sel-summon-spell-level",
children: [
e_({tag: "option", val: "-1", text: "\u2014"}),
@@ -7547,7 +7591,7 @@ Renderer.monster = class {
return e_({
tag: "select",
clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden",
clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden no-print lst-is-exporting-image__hidden",
name: "mon__sel-summon-class-level",
children: [
e_({tag: "option", val: "-1", text: "\u2014"}),
@@ -7659,12 +7703,12 @@ Renderer.monster = class {
ptCrSpellLevel = `<td colspan="2">
${Parser.monCrToFull(mon.cr, {isMythic: !!mon.mythic})}
${opts.isShowScalers && !opts.isScaledCr && Parser.isValidCr(mon.cr ? (mon.cr.cr || mon.cr) : null) ? `
<button title="Scale Creature By CR (Highly Experimental)" class="mon__btn-scale-cr btn btn-xs btn-default">
<button title="Scale Creature By CR (Highly Experimental)" class="mon__btn-scale-cr btn btn-xs btn-default no-print">
<span class="glyphicon glyphicon-signal"></span>
</button>
` : ""}
${opts.isScaledCr ? `
<button title="Reset CR Scaling" class="mon__btn-reset-cr btn btn-xs btn-default">
<button title="Reset CR Scaling" class="mon__btn-reset-cr btn btn-xs btn-default no-print">
<span class="glyphicon glyphicon-refresh"></span>
</button>
` : ""}
@@ -7978,8 +8022,7 @@ Renderer.monster = class {
static pGetFluff (mon) {
return Renderer.utils.pGetFluff({
entity: mon,
pFnPostProcess: Renderer.monster.postProcessFluff.bind(null, mon),
fluffBaseUrl: `data/bestiary/`,
pFnPostProcess: Renderer.monster.postProcessFluff.bind(Renderer.monster, mon),
fluffProp: "monsterFluff",
});
}
@@ -9413,7 +9456,6 @@ Renderer.item = class {
static pGetFluff (item) {
return Renderer.utils.pGetFluff({
entity: item,
fnGetFluffData: DataUtil.itemFluff.loadJSON.bind(DataUtil.itemFluff),
fluffProp: "itemFluff",
});
}
@@ -10140,7 +10182,6 @@ Renderer.vehicle = class {
static pGetFluff (veh) {
return Renderer.utils.pGetFluff({
entity: veh,
fnGetFluffData: DataUtil.vehicleFluff.loadJSON.bind(DataUtil.vehicleFluff),
fluffProp: "vehicleFluff",
});
}
@@ -10226,7 +10267,6 @@ Renderer.language = class {
static pGetFluff (it) {
return Renderer.utils.pGetFluff({
entity: it,
fnGetFluffData: DataUtil.languageFluff.loadJSON.bind(DataUtil.languageFluff),
fluffProp: "languageFluff",
});
}
@@ -10370,7 +10410,6 @@ Renderer.charoption = class {
static pGetFluff (it) {
return Renderer.utils.pGetFluff({
entity: it,
fnGetFluffData: DataUtil.charoptionFluff.loadJSON.bind(DataUtil.charoptionFluff),
fluffProp: "charoptionFluff",
});
}
@@ -10490,7 +10529,6 @@ Renderer.recipe = class {
static pGetFluff (it) {
return Renderer.utils.pGetFluff({
entity: it,
fnGetFluffData: DataUtil.recipeFluff.loadJSON.bind(DataUtil.recipeFluff),
fluffProp: "recipeFluff",
});
}
@@ -11651,9 +11689,14 @@ Renderer.hover = class {
* @param [opts.isPopout] If the window should be immediately popped out.
* @param [opts.compactReferenceData] Reference (e.g. page/source/hash/others) which can be used to load the contents into the DM screen.
* @param [opts.sourceData] Source JSON (as raw as possible) used to construct this popout.
* @param [opts.isResizeOnlyWidth]
* @param [opts.isHideBottomBorder]
*/
static getShowWindow ($content, position, opts) {
opts = opts || {};
const {isHideBottomBorder, isResizeOnlyWidth} = opts;
if (isHideBottomBorder && !isResizeOnlyWidth) throw new Error(`"isHideBottomBorder" option requires "isResizeOnlyWidth"!`);
Renderer.hover._doInit();
@@ -11680,31 +11723,40 @@ Renderer.hover = class {
const drag = {};
const $brdrTopRightResize = $(`<div class="hoverborder__resize-ne"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 1}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 1, isResizeOnlyWidth}));
if (isResizeOnlyWidth) $brdrTopRightResize.hideVe();
const $brdrRightResize = $(`<div class="hoverborder__resize-e"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 2}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 2, isResizeOnlyWidth}));
const $brdrBottomRightResize = $(`<div class="hoverborder__resize-se"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 3}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 3, isResizeOnlyWidth}));
if (isResizeOnlyWidth) $brdrBottomRightResize.hideVe();
const $brdrBtm = $(`<div class="hoverborder hoverborder--btm ${opts.isBookContent ? "hoverborder-book" : ""}"><div class="hoverborder__resize-s"></div></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 4}));
const $brdrBtm = $(`<div class="hoverborder hoverborder--btm ${opts.isBookContent ? "hoverborder-book" : ""}"></div>`);
const $brdrBtmResize = $(`<div class="hoverborder__resize-s"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 4, isResizeOnlyWidth}))
.appendTo($brdrBtm);
if (isResizeOnlyWidth) $brdrBtmResize.hideVe();
if (isHideBottomBorder) $brdrBtm.hideVe();
const $brdrBtmLeftResize = $(`<div class="hoverborder__resize-sw"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 5}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 5, isResizeOnlyWidth}));
if (isResizeOnlyWidth) $brdrBtmLeftResize.hideVe();
const $brdrLeftResize = $(`<div class="hoverborder__resize-w"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 6}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 6, isResizeOnlyWidth}));
const $brdrTopLeftResize = $(`<div class="hoverborder__resize-nw"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 7}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 7, isResizeOnlyWidth}));
if (isResizeOnlyWidth) $brdrTopLeftResize.hideVe();
const $brdrTopResize = $(`<div class="hoverborder__resize-n"></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 8}));
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 8, isResizeOnlyWidth}));
if (isResizeOnlyWidth) $brdrTopResize.hideVe();
const $brdrTop = $(`<div class="hoverborder hoverborder--top ${opts.isBookContent ? "hoverborder-book" : ""}" ${opts.isPermanent ? `data-perm="true"` : ""}></div>`)
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 9}))
.on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 9, isResizeOnlyWidth}))
.on("contextmenu", (evt) => {
Renderer.hover._contextMenuLastClicked = {
hoverId,
@@ -11883,7 +11935,7 @@ Renderer.hover = class {
if (opts.cbClose) opts.cbClose(hoverWindow);
}
static _getShowWindow_handleDragMousedown ({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type}) {
static _getShowWindow_handleDragMousedown ({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type, isResizeOnlyWidth}) {
if (evt.which === 0 || evt.which === 1) evt.preventDefault();
hoverWindow.zIndex = Renderer.hover._getNextZIndex(hoverId);
$hov.css({
@@ -11895,11 +11947,11 @@ Renderer.hover = class {
drag.startY = EventUtil.getClientY(evt);
drag.baseTop = parseFloat($hov.css("top"));
drag.baseLeft = parseFloat($hov.css("left"));
drag.baseHeight = $wrpContent.height();
if (!isResizeOnlyWidth) drag.baseHeight = $wrpContent.height();
drag.baseWidth = parseFloat($hov.css("width"));
if (type < 9) {
$wrpContent.css({
"height": drag.baseHeight,
...(isResizeOnlyWidth ? {} : {"height": drag.baseHeight}),
"max-height": "initial",
});
$hov.css("max-width", "initial");
@@ -12372,6 +12424,8 @@ Renderer.hover = class {
case UrlUtil.PG_VEHICLES: return Renderer.vehicle.pGetFluff;
case UrlUtil.PG_CHAR_CREATION_OPTIONS: return Renderer.charoption.pGetFluff;
case UrlUtil.PG_RECIPES: return Renderer.recipe.pGetFluff;
case UrlUtil.PG_TRAPS_HAZARDS: return Renderer.traphazard.pGetFluff;
case UrlUtil.PG_CLASSES: return Renderer.classSubclass.pGetFluff;
default: return null;
}
}

View File

@@ -49,7 +49,6 @@ class RewardsPage extends ListPage {
const pageFilter = new PageFilterRewards();
super({
dataSource: DataUtil.reward.loadJSON.bind(DataUtil.reward),
dataSourceFluff: DataUtil.rewardFluff.loadJSON.bind(DataUtil.rewardFluff),
pFnGetFluff: Renderer.reward.pGetFluff.bind(Renderer.feat),

View File

@@ -1062,6 +1062,28 @@ class _DataTypeLoaderCustomSpellFluff extends _DataTypeLoaderMultiSource {
_prop = "spellFluff";
}
class _DataTypeLoaderClassSubclassFluff extends _DataTypeLoaderMultiSource {
static PROPS = ["classFluff", "subclassFluff"];
static PAGE = UrlUtil.PG_CLASSES;
static IS_FLUFF = true;
_getSiteIdent ({pageClean, sourceClean}) {
// use `.toString()` in case `sourceClean` is a `Symbol`
return `${this.constructor.PROPS.join("__")}__${sourceClean.toString()}`;
}
async _pGetSiteData ({pageClean, sourceClean}) {
return this._pGetSiteDataAll();
}
async _pGetSiteDataAll () {
const jsons = await this.constructor.PROPS.pMap(prop => DataUtil[prop].loadJSON());
const out = {};
jsons.forEach(json => Object.assign(out, {...json}));
return out;
}
}
/** @abstract */
class _DataTypeLoaderCustomRawable extends _DataTypeLoader {
static _PROPS_RAWABLE;
@@ -1670,6 +1692,7 @@ class DataLoader {
_DataTypeLoaderCustomMonsterFluff.register({fnRegister});
_DataTypeLoaderCustomSpell.register({fnRegister});
_DataTypeLoaderCustomSpellFluff.register({fnRegister});
_DataTypeLoaderClassSubclassFluff.register({fnRegister});
// endregion
// region Predefined

View File

@@ -722,6 +722,9 @@ PropOrder._CLASS = [
"subclassTitle",
"hasFluff",
"hasFluffImages",
"fluff",
"foundrySystem",
@@ -787,6 +790,11 @@ PropOrder._SUBCLASS = [
"subclassTableGroups",
"subclassFeatures",
"hasFluff",
"hasFluffImages",
"fluff",
"foundrySystem",
"foundryFlags",
"foundryAdvancement",
@@ -799,6 +807,7 @@ PropOrder._SUBCLASS__COPY_MOD = [
];
PropOrder._SUBCLASS_FLUFF = [
"name",
"shortName",
"source",
"className",
"classSource",

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.205.0"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.VERSION_NUMBER = /* 5ETOOLS_VERSION__OPEN */"1.206.0"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.DEPLOYED_IMG_ROOT = undefined;
// for the roll20 script to set
globalThis.IS_VTT = false;
@@ -1342,6 +1342,34 @@ globalThis.MiscUtil = class {
} else doCompatibilityCopy();
}
static async pCopyBlobToClipboard (blob) {
if (!navigator?.permissions) {
JqueryUtil.doToast({type: "danger", content: `Could not access clipboard!`});
return false;
}
const access = await navigator.permissions.query({name: "clipboard-write"});
if (!["granted", "prompt"].includes(access.state)) {
JqueryUtil.doToast({type: "danger", content: `Could not access clipboard!`});
return false;
}
try {
await navigator.clipboard.write([
new ClipboardItem({[blob.type]: blob}),
]);
return true;
} catch (e) {
if (e.message.includes("Document is not focused")) {
JqueryUtil.doToast({type: "danger", content: `Please focus the window first!`});
return false;
}
JqueryUtil.doToast({type: "danger", content: `Failed to copy! ${VeCt.STR_SEE_CONSOLE}`});
throw e;
}
}
static checkProperty (object, ...path) {
for (let i = 0; i < path.length; ++i) {
object = object[path[i]];
@@ -2428,6 +2456,9 @@ globalThis.ContextUtil = {
const result = await this.fnAction(evt, {userData: menu.userData});
if (menu.resolveResult_) menu.resolveResult_(result);
})
.on("mousedown", evt => {
evt.preventDefault();
})
.keydown(evt => {
if (evt.key !== "Enter") return;
$btnAction.click();
@@ -2451,6 +2482,9 @@ globalThis.ContextUtil = {
const result = await this.fnActionAlt(evt, {userData: menu.userData});
if (menu.resolveResult_) menu.resolveResult_(result);
})
.on("mousedown", evt => {
evt.preventDefault();
});
if (this.titleAlt) $btnActionAlt.title(this.titleAlt);
@@ -2585,6 +2619,9 @@ globalThis.ContextUtil = {
);
menu.close();
})
.on("mousedown", evt => {
evt.preventDefault();
});
return {
@@ -3414,6 +3451,26 @@ globalThis.SortUtil = {
},
};
globalThis.MultiSourceUtil = class {
static getIndexKey (prop, ent) {
switch (prop) {
case "class":
case "classFluff":
return (ent.name || "").toLowerCase().split(" ").at(-1);
case "subclass":
case "subclassFluff":
return (ent.className || "").toLowerCase().split(" ").at(-1);
default:
return ent.source;
}
}
static isEntityIndexKeyMatch (indexKey, prop, ent) {
if (indexKey == null) return true;
return indexKey === MultiSourceUtil.getIndexKey(prop, ent);
}
};
// JSON LOADING ========================================================================================================
class _DataUtilPropConfig {
static _MERGE_REQUIRES_PRESERVE = {};
@@ -3452,7 +3509,7 @@ class _DataUtilPropConfigMultiSource extends _DataUtilPropConfig {
static async pLoadAll () {
const json = await this.loadJSON();
return json[this._PROP];
return json[this._PROP] || [];
}
static async loadJSON () { return this._loadJSON({isUnmerged: false}); }
@@ -3462,7 +3519,7 @@ class _DataUtilPropConfigMultiSource extends _DataUtilPropConfig {
const index = await this.pLoadIndex();
const allData = await Object.entries(index)
.pMap(async ([source, file]) => this._pLoadSourceEntities({source, isUnmerged, file}));
.pMap(async ([indexKey, file]) => this._pLoadSourceEntities({indexKey, isUnmerged, file}));
return {[this._PROP]: allData.flat()};
}
@@ -3473,16 +3530,16 @@ class _DataUtilPropConfigMultiSource extends _DataUtilPropConfig {
const file = index[source];
if (!file) return null;
return {[this._PROP]: await this._pLoadSourceEntities({source, file})};
return {[this._PROP]: await this._pLoadSourceEntities({indexKey: source, file})};
}
static async _pLoadSourceEntities ({source, isUnmerged = false, file}) {
static async _pLoadSourceEntities ({indexKey = null, isUnmerged = false, file}) {
await this._pInitPreData();
const fnLoad = isUnmerged ? DataUtil.loadRawJSON.bind(DataUtil) : DataUtil.loadJSON.bind(DataUtil);
let data = await fnLoad(`${Renderer.get().baseUrl}data/${this._DIR}/${file}`);
data = data[this._PROP].filter(it => it.source === source);
data = (data[this._PROP] || []).filter(MultiSourceUtil.isEntityIndexKeyMatch.bind(this, indexKey, this._PROP));
if (!this._IS_MUT_ENTITIES) return data;
@@ -3783,12 +3840,17 @@ globalThis.DataUtil = {
},
_userDownload (filename, data, mimeType) {
const a = document.createElement("a");
const t = new Blob([data], {type: mimeType});
a.href = window.URL.createObjectURL(t);
const dataUrl = window.URL.createObjectURL(t);
DataUtil.userDownloadDataUrl(filename, dataUrl);
setTimeout(() => window.URL.revokeObjectURL(dataUrl), 100);
},
userDownloadDataUrl (filename, dataUrl) {
const a = document.createElement("a");
a.href = dataUrl;
a.download = filename;
a.dispatchEvent(new MouseEvent("click", {bubbles: true, cancelable: true, view: window}));
setTimeout(() => window.URL.revokeObjectURL(a.href), 100);
},
/** Always returns an array of files, even in "single" mode. */
@@ -3901,7 +3963,9 @@ globalThis.DataUtil = {
"spell": "spells",
"spellFluff": "spells",
"class": "class",
"classFluff": "class",
"subclass": "class",
"subclassFluff": "class",
"classFeature": "class",
"subclassFeature": "class",
},
@@ -3929,7 +3993,9 @@ globalThis.DataUtil = {
// region Multi-source
case "class":
case "classFluff":
case "subclass":
case "subclassFluff":
case "classFeature":
case "subclassFeature": {
const baseUrlPart = `${Renderer.get().baseUrl}data/${DataUtil._MULTI_SOURCE_PROP_TO_DIR[prop]}`;
@@ -5623,7 +5689,6 @@ globalThis.DataUtil = {
for (const r of recipes) {
const fluff = await Renderer.utils.pGetFluff({
entity: r,
fnGetFluffData: DataUtil.recipeFluff.loadJSON.bind(DataUtil.recipeFluff),
fluffProp: "recipeFluff",
});
@@ -5839,8 +5904,25 @@ globalThis.DataUtil = {
// endregion
},
subclass: class extends _DataUtilPropConfig {
classFluff: class extends _DataUtilPropConfigMultiSource {
static _PAGE = UrlUtil.PG_CLASSES;
static _DIR = "class";
static _PROP = "classFluff";
},
subclass: class extends _DataUtilPropConfigCustom {
static _PAGE = "subclass";
static _PROP = "subclassFluff";
static async loadJSON () {
return DataUtil.class.loadJSON();
}
},
subclassFluff: class extends _DataUtilPropConfigMultiSource {
static _PAGE = "subclassFluff";
static _DIR = "class";
static _PROP = "subclassFluff";
},
deity: class extends _DataUtilPropConfigSingleSource {

View File

@@ -54,7 +54,6 @@ class VehiclesPage extends ListPage {
super({
dataSource: "data/vehicles.json",
dataSourceFluff: "data/fluff-vehicles.json",
pFnGetFluff,