This commit is contained in:
TheGiddyLimit
2024-01-25 23:07:09 +00:00
parent 5ffd4acdb4
commit a4e391a3e7
94 changed files with 7263 additions and 939 deletions

View File

@@ -821,8 +821,7 @@ class BestiaryPage extends ListPageMultiSource {
const $floatToken = this._$dispToken.empty();
const hasToken = mon.tokenUrl || mon.hasToken;
if (!hasToken) return;
if (!Renderer.monster.hasToken(mon)) return;
const imgLink = Renderer.monster.getTokenUrl(mon);
const $img = $(`<img src="${imgLink}" class="mon__token" alt="Token Image: ${(mon.name || "").qq()}" ${mon.tokenCredit ? `title="Credit: ${mon.tokenCredit.qq()}"` : ""} loading="lazy">`);
@@ -844,8 +843,9 @@ class BestiaryPage extends ListPageMultiSource {
const buildEle = (meta) => {
if (!meta.$ele) {
const imgLink = Renderer.monster.getTokenUrl({name: meta.name, source: meta.source, tokenUrl: meta.tokenUrl});
const $img = $(`<img src="${imgLink}" class="mon__token" alt="Token Image: ${(meta.displayName || meta.name || "").qq()}" ${meta.tokenCredit ? `title="Credit: ${meta.tokenCredit.qq()}"` : ""} loading="lazy">`)
const imgLink = Renderer.monster.getTokenUrl(meta);
const displayName = Renderer.monster.getAltArtDisplayName(meta);
const $img = $(`<img src="${imgLink}" class="mon__token" alt="Token Image${displayName ? `: ${displayName.qq()}` : ""}}" ${meta.tokenCredit ? `title="Credit: ${meta.tokenCredit.qq()}"` : ""} loading="lazy">`)
.on("error", () => {
$img.attr(
"src",
@@ -897,8 +897,7 @@ class BestiaryPage extends ListPageMultiSource {
meta.$ele.show();
setTimeout(() => meta.$ele.css("max-width", ""), 10); // hack to clear the earlier 100% width
if (meta.name && meta.source) $footer.html(Renderer.monster.getRenderedAltArtEntry(meta));
else $footer.html("");
$footer.html(Renderer.monster.getRenderedAltArtEntry(meta));
$wrpFooter.detach().appendTo(meta.$ele);
$btnLeft.detach().appendTo(meta.$ele);

View File

@@ -205,8 +205,7 @@ export class EncounterBuilderUiBestiary extends EncounterBuilderUi {
}
static getTokenHoverMeta (mon) {
const hasToken = mon.tokenUrl || mon.hasToken;
if (!hasToken) return null;
if (!Renderer.monster.hasToken(mon)) return null;
return Renderer.hover.getMakePredefinedHover(
{

View File

@@ -1635,7 +1635,7 @@ class CreatureParser extends BaseParser {
spl.forEach(it => {
const m = /^(?<abil>\w+)\s*(?<sign>[-+])\s*(?<save>\d+)(?<plusPb>(?:\s+plus\s+|\s*\+\s*)PB)?$/i.exec(it);
if (m) {
out[m.groups.abil] = `${m.groups.sign}${m.groups.save}${m.groups.plusPb ? m.groups.plusPb.replace(/\bpb\b/gi, "PB") : ""}`;
out[m.groups.abil.slice(0, 3)] = `${m.groups.sign}${m.groups.save}${m.groups.plusPb ? m.groups.plusPb.replace(/\bpb\b/gi, "PB") : ""}`;
return;
}

View File

@@ -6,6 +6,15 @@ class SpellParser extends BaseParser {
static _RE_START_DURATION = "Duration";
static _RE_START_CLASS = "Class(?:es)?";
static _REQUIRED_PROPS = [
"level",
"school",
"time",
"range",
"duration",
"entries",
];
/**
* Parses spells from raw text pastes
* @param inText Input text.
@@ -117,6 +126,9 @@ class SpellParser extends BaseParser {
const statsOut = this._getFinalState(spell, options);
const missingProps = this._REQUIRED_PROPS.filter(prop => !statsOut[prop]);
if (missingProps.length) options.cbWarning(`${statsOut.name ? `(${statsOut.name}) ` : ""}Missing properties: ${missingProps.join(", ")}`);
options.cbOutput(statsOut, options.isAppend);
}
@@ -445,12 +457,12 @@ 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|month|day|year)(?:s)?$/i.exec(dur);
const mConcOrUpTo = /^(?<conc>concentration, )?up to (?<amount>\d+|an?) (?<unit>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};
if (mConcOrUpTo[1]) out.concentration = true;
else out.upTo = true;
const amount = mConcOrUpTo.groups.amount.toLowerCase().startsWith("a") ? 1 : Number(mConcOrUpTo.groups.amount);
const out = {type: "timed", duration: {type: this._getCleanTimeUnit(mConcOrUpTo.groups.unit, true, options), amount}};
if (mConcOrUpTo.groups.conc) out.concentration = true;
else out.duration.upTo = true;
return stats.duration = [out];
}

View File

@@ -938,6 +938,9 @@ class ConverterUi extends BaseComponent {
this.saveSettingsDebounced = MiscUtil.debounce(() => StorageUtil.pSetForPage(ConverterUi.STORAGE_STATE, this.getBaseSaveableState()), 50);
this._addHookAll("state", () => this.saveSettingsDebounced());
this.__meta = this._getDefaultMetaState();
this._meta = this._getProxy("meta", this.__meta);
}
set converters (converters) { this._converters = converters; }
@@ -996,6 +999,7 @@ class ConverterUi extends BaseComponent {
JqueryUtil.doToast({type: "warning", content: "Enabled editing. Note that edits will be overwritten as you parse new stat blocks."});
});
let hovWindowPreview = null;
$(`#preview`)
.on("click", async evt => {
const metaCurr = this._getCurrentEntities();
@@ -1018,15 +1022,25 @@ class ConverterUi extends BaseComponent {
};
});
Renderer.hover.getShowWindow(
Renderer.hover.$getHoverContent_generic({
type: "entries",
entries,
}),
const $content = Renderer.hover.$getHoverContent_generic({
type: "entries",
entries,
});
if (hovWindowPreview) {
hovWindowPreview.$setContent($content);
return;
}
hovWindowPreview = Renderer.hover.getShowWindow(
$content,
Renderer.hover.getWindowPositionFromEvent(evt),
{
title: "Preview",
isPermanent: true,
cbClose: () => {
hovWindowPreview = null;
},
},
);
});
@@ -1172,16 +1186,12 @@ class ConverterUi extends BaseComponent {
*/
const catchErrors = async (pToRun) => {
try {
$(`#lastWarnings`).hide().html("");
$(`#lastError`).hide().html("");
this._editorOut.resize();
this._proxyAssignSimple("meta", this._getDefaultMetaState());
await pToRun();
} catch (x) {
const splitStack = x.stack.split("\n");
const atPos = splitStack.length > 1 ? splitStack[1].trim() : "(Unknown location)";
const message = `[Error] ${x.message} ${atPos}`;
$(`#lastError`).show().html(message);
this._editorOut.resize();
this._meta.errors = [...this._meta.errors, `${x.message} ${atPos}`];
setTimeout(() => { throw x; });
}
};
@@ -1197,7 +1207,10 @@ class ConverterUi extends BaseComponent {
const chunks = (this._state.inputSeparator
? this.inText.split(this._state.inputSeparator)
: [this.inText]).map(it => it.trim()).filter(Boolean);
if (!chunks.length) return this.showWarning("No input!");
if (!chunks.length) {
this._meta.warnings = [...this._meta.warnings, "No input!"];
return;
}
chunks
.reverse() // reverse as the append is actually a prepend
@@ -1205,7 +1218,7 @@ class ConverterUi extends BaseComponent {
this.activeConverter.handleParse(
chunk,
this.doCleanAndOutput.bind(this),
this.showWarning.bind(this),
(warning) => this._meta.warnings = [...this._meta.warnings, warning],
isAppend || i !== 0, // always clear the output for the first non-append chunk, then append
);
});
@@ -1217,9 +1230,60 @@ class ConverterUi extends BaseComponent {
this.initSideMenu();
this._pInit_dispErrorsWarnings();
window.dispatchEvent(new Event("toolsLoaded"));
}
_pInit_dispErrorsWarnings () {
const $stgErrors = $(`#lastError`);
const $stgWarnings = $(`#lastWarnings`);
const getRow = ({prefix, text, prop}) => {
const $btnClose = $(`<button class="btn btn-danger btn-xs w-24p" title="Dismiss ${prefix} (SHIFT to Dismiss All)">×</button>`)
.on("click", evt => {
if (evt.shiftKey) {
this._meta[prop] = [];
return;
}
const ix = this._meta[prop].indexOf(text);
if (!~ix) return;
this._meta[prop].splice(ix, 1);
this._meta[prop] = [...this._meta[prop]];
});
return $$`<div class="split-v-center py-1">
<div>[${prefix}] ${text}</div>
${$btnClose}
</div>`;
};
this._addHook("meta", "errors", () => {
$stgErrors.toggleVe(this._meta.errors.length);
$stgErrors.empty();
this._meta.errors
.forEach(it => {
getRow({prefix: "Error", text: it, prop: "errors"})
.appendTo($stgErrors);
});
})();
this._addHook("meta", "warnings", () => {
$stgWarnings.toggleVe(this._meta.warnings.length);
$stgWarnings.empty();
this._meta.warnings
.forEach(it => {
getRow({prefix: "Warning", text: it, prop: "warnings"})
.appendTo($stgWarnings);
});
})();
const hkResize = () => this._editorOut.resize();
this._addHook("meta", "errors", hkResize);
this._addHook("meta", "warnings", hkResize);
}
_getCurrentEntities () {
const output = this._outText;
@@ -1285,11 +1349,6 @@ class ConverterUi extends BaseComponent {
hkMode();
}
showWarning (text) {
$(`#lastWarnings`).show().append(`<div>[Warning] ${text}</div>`);
this._editorOut.resize();
}
doCleanAndOutput (obj, append) {
const asCleanString = CleanUtil.getCleanJson(obj, {isFast: false});
if (append) {
@@ -1312,6 +1371,13 @@ class ConverterUi extends BaseComponent {
set inText (text) { this._editorIn.setValue(text, -1); }
_getDefaultState () { return MiscUtil.copy(ConverterUi._DEFAULT_STATE); }
_getDefaultMetaState () {
return {
errors: [],
warnings: [],
};
}
}
ConverterUi.STORAGE_INPUT = "converterInput";
ConverterUi.STORAGE_STATE = "converterState";

View File

@@ -1697,7 +1697,24 @@ globalThis.SpeedConvert = SpeedConvert;
class DetectNamedCreature {
static tryRun (mon) {
if (this._tryRun_nickname(mon)) return;
this._tryRun_heuristic(mon);
}
static _tryRun_nickname (mon) {
if (
/^[^"]+ "[^"]+" [^"]+/.test(mon.name)
|| /^[^']+ '[^']+' [^']+/.test(mon.name)
) {
mon.isNamedCreature = true;
return true;
}
return false;
}
static _tryRun_heuristic (mon) {
const totals = {yes: 0, no: 0};
this._doCheckProp(mon, totals, "trait");
this._doCheckProp(mon, totals, "spellcasting");
this._doCheckProp(mon, totals, "action");
@@ -1707,6 +1724,8 @@ class DetectNamedCreature {
this._doCheckProp(mon, totals, "mythic");
if (totals.yes && totals.yes > totals.no) mon.isNamedCreature = true;
return true;
}
static _doCheckProp (mon, totals, prop) {

View File

@@ -136,7 +136,15 @@ class DmMapperRoot extends BaseComponent {
$parent.append(`<div class="ve-flex-vh-center w-100 h-100"><i class="dnd-font ve-muted">Loading...</i></div>`);
RenderMap.$pGetRendered(this._state)
RenderMap.$pGetRendered(
this._state,
{
fnGetContainerDimensions: () => {
const bcr = $parent[0].getBoundingClientRect();
return {w: bcr.width, h: bcr.height};
},
},
)
.then($ele => $parent.empty().append($ele));
}
}

View File

@@ -2,6 +2,8 @@ import {InitiativeTrackerStatColumnFactory} from "./dmscreen-initiativetracker-s
class _ConvertedEncounter {
constructor () {
this.isOverwriteStatsCols = false;
this.isStatsAddColumns = false;
this.statsCols = [];
@@ -52,7 +54,10 @@ export class InitiativeTrackerEncounterConverter {
const colNameIndex = {};
encounterInfo.colsExtraAdvanced = encounterInfo.colsExtraAdvanced || [];
if (encounterInfo.colsExtraAdvanced.length) out.isStatsAddColumns = true;
if (encounterInfo.colsExtraAdvanced.length) {
out.isOverwriteStatsCols = true;
out.isStatsAddColumns = true;
}
encounterInfo.colsExtraAdvanced.forEach((col, i) => colNameIndex[i] = (col?.name || "").toLowerCase());

View File

@@ -56,7 +56,8 @@ export class InitiativeTracker extends BaseComponent {
this._render_bindSortDirHooks();
const $wrpTracker = $(`<div class="dm-init dm__panel-bg dm__data-anchor"></div>`);
const $wrpTracker = $(`<div class="dm-init dm__panel-bg dm__data-anchor"></div>`)
.on("drop", evt => this._pDoHandleImportDrop(evt.originalEvent));
const sendStateToClientsDebounced = MiscUtil.debounce(
() => {
@@ -450,12 +451,6 @@ export class InitiativeTracker extends BaseComponent {
.filter(({id}) => !idsDefaultParty.has(id));
const stateNxt = {
// region TODO(DMS) not ideal--merge columns instead? Note that this also clobbers default party info
isStatsAddColumns: nxtState.isStatsAddColumns,
statsCols: nxtState.statsCols
.map(it => it.getAsStateData()),
// endregion
rows: this._state.importIsAppend
? [
...rowsPrevNonDefaultParty,
@@ -468,6 +463,54 @@ export class InitiativeTracker extends BaseComponent {
],
};
if (nxtState.isOverwriteStatsCols) {
const userVal = await InputUiUtil.pGetUserGenericButton({
title: "Overwrite Additional Columns",
buttons: [
new InputUiUtil.GenericButtonInfo({
text: "Yes",
clazzIcon: "glyphicon glyphicon-ok",
value: "yes",
}),
new InputUiUtil.GenericButtonInfo({
text: "No",
clazzIcon: "glyphicon glyphicon-remove",
isPrimary: true,
value: "no",
}),
new InputUiUtil.GenericButtonInfo({
text: "Cancel",
clazzIcon: "glyphicon glyphicon-stop",
isSmall: true,
value: "cancel",
}),
],
htmlDescription: `<p>The encounter you are trying to load contains additional column data from the Encounter Builder's "Advanced" mode.<br>Do you want to overwrite your existing additional columns with columns from the encounter?</p>`,
});
switch (userVal) {
case null:
case "cancel": {
this._state.rows = rowsPrev;
return;
}
case "yes": {
stateNxt.isStatsAddColumns = nxtState.isStatsAddColumns;
stateNxt.statsCols = nxtState.statsCols
.map(it => it.getAsStateData());
break;
}
case "no": {
// No-op
break;
}
default: throw new Error(`Unexpected value "${userVal}"`);
}
}
if (!this._state.importIsAppend) {
const defaultState = this._getDefaultState();
["round", "sort", "dir"]
@@ -479,6 +522,34 @@ export class InitiativeTracker extends BaseComponent {
/* -------------------------------------------- */
async _pDoHandleImportDrop (evt) {
const data = EventUtil.getDropJson(evt);
if (!data) return;
if (data.type !== VeCt.DRAG_TYPE_IMPORT) return;
evt.stopPropagation();
evt.preventDefault();
const {page, source, hash} = data;
if (page !== UrlUtil.PG_BESTIARY) return;
const ent = await DataLoader.pCacheAndGet(page, source, hash, {isRequired: true});
const rowsNxt = [...this._state.rows];
const rowToAdd = await this._rowStateBuilderActive.pGetNewRowState({
name: ent.name,
source: ent.source,
initiative: null,
rows: rowsNxt,
});
if (!rowToAdd) return;
rowsNxt.push(rowToAdd);
this._state.rows = rowsNxt;
}
/* -------------------------------------------- */
doConnectCreatureViewer ({creatureViewer}) {
if (this._creatureViewers.includes(creatureViewer)) return this;
this._creatureViewers.push(creatureViewer);

View File

@@ -274,19 +274,11 @@ class PageFilterBestiary extends PageFilter {
static mutateForFilters (mon) {
Renderer.monster.initParsed(mon);
if (typeof mon.speed === "number" && mon.speed > 0) {
mon._fSpeedType = ["walk"];
mon._fSpeed = mon.speed;
} else {
mon._fSpeedType = Object.keys(mon.speed).filter(k => mon.speed[k]);
if (mon._fSpeedType.length) mon._fSpeed = mon._fSpeedType.map(k => mon.speed[k].number || mon.speed[k]).filter(it => !isNaN(it)).sort((a, b) => SortUtil.ascSort(b, a))[0];
else mon._fSpeed = 0;
if (mon.speed.canHover) mon._fSpeedType.push("hover");
}
this._mutateForFilters_speed(mon);
mon._fAc = mon.ac.map(it => it.special ? null : (it.ac || it)).filter(it => it !== null);
mon._fAc = (mon.ac || []).map(it => it.special ? null : (it.ac || it)).filter(it => it !== null);
if (!mon._fAc.length) mon._fAc = null;
mon._fHp = mon.hp.average;
mon._fHp = mon.hp?.average ?? null;
if (mon.alignment) {
const tempAlign = typeof mon.alignment[0] === "object"
? Array.prototype.concat.apply([], mon.alignment.map(a => a.alignment))
@@ -348,7 +340,7 @@ class PageFilterBestiary extends PageFilter {
if (mon.srd) mon._fMisc.push("SRD");
if (mon.basicRules) mon._fMisc.push("Basic Rules");
if (SourceUtil.isLegacySourceWotc(mon.source)) mon._fMisc.push("Legacy");
if (mon.tokenUrl || mon.hasToken) mon._fMisc.push("Has Token");
if (Renderer.monster.hasToken(mon)) mon._fMisc.push("Has Token");
if (mon.mythic) mon._fMisc.push("Mythic");
if (mon.hasFluff || mon.fluff?.entries) mon._fMisc.push("Has Info");
if (mon.hasFluffImages || mon.fluff?.images) mon._fMisc.push("Has Images");
@@ -370,6 +362,25 @@ class PageFilterBestiary extends PageFilter {
mon._fEquipment = this._getEquipmentList(mon);
}
static _mutateForFilters_speed (mon) {
if (mon.speed == null) {
mon._fSpeedType = [];
mon._fSpeed = null;
return;
}
if (typeof mon.speed === "number" && mon.speed > 0) {
mon._fSpeedType = ["walk"];
mon._fSpeed = mon.speed;
return;
}
mon._fSpeedType = Object.keys(mon.speed).filter(k => mon.speed[k]);
if (mon._fSpeedType.length) mon._fSpeed = mon._fSpeedType.map(k => mon.speed[k].number || mon.speed[k]).filter(it => !isNaN(it)).sort((a, b) => SortUtil.ascSort(b, a))[0];
else mon._fSpeed = 0;
if (mon.speed.canHover) mon._fSpeedType.push("hover");
}
/* -------------------------------------------- */
static _getInitWalker () {
@@ -473,8 +484,8 @@ class PageFilterBestiary extends PageFilter {
this._wisdomFilter.addItem(mon._fWis);
this._charismaFilter.addItem(mon._fCha);
this._speedFilter.addItem(mon._fSpeed);
mon.ac.forEach(it => this._acFilter.addItem(it.ac || it));
if (mon.hp.average) this._averageHpFilter.addItem(mon.hp.average);
(mon.ac || []).forEach(it => this._acFilter.addItem(it.ac || it));
if (mon.hp?.average) this._averageHpFilter.addItem(mon.hp.average);
this._tagFilter.addItem(mon._pTypes.tags);
this._sidekickTypeFilter.addItem(mon._pTypes.typeSidekick);
this._sidekickTagFilter.addItem(mon._pTypes.tagsSidekick);

View File

@@ -10,7 +10,7 @@ class PageFilterObjects extends PageFilter {
static mutateForFilters (obj) {
obj._fMisc = obj.srd ? ["SRD"] : [];
if (SourceUtil.isLegacySourceWotc(obj.source)) obj._fMisc.push("Legacy");
if (obj.tokenUrl || obj.hasToken) obj._fMisc.push("Has Token");
if (Renderer.object.hasToken(obj)) obj._fMisc.push("Has Token");
if (obj.hasFluff || obj.fluff?.entries) obj._fMisc.push("Has Info");
if (obj.hasFluffImages || obj.fluff?.images) obj._fMisc.push("Has Images");
}

View File

@@ -25,44 +25,44 @@ class PageFilterVehicles extends PageFilter {
this._miscFilter = new Filter({header: "Miscellaneous", items: ["SRD", "Legacy", "Has Images", "Has Info", "Has Token"], isMiscFilter: true});
}
static mutateForFilters (it) {
it._fSpeed = 0;
if (typeof it.speed === "number" && it.speed > 0) {
it._fSpeed = it.speed;
} else if (it.speed) {
const maxSpeed = Math.max(...Object.values(it.speed));
if (maxSpeed > 0) it._fSpeed = maxSpeed;
} else if (it.pace && typeof it.pace === "number") {
it._fSpeed = it.pace * 10; // Based on "Special Travel Pace," DMG p242
static mutateForFilters (ent) {
ent._fSpeed = 0;
if (typeof ent.speed === "number" && ent.speed > 0) {
ent._fSpeed = ent.speed;
} else if (ent.speed) {
const maxSpeed = Math.max(...Object.values(ent.speed));
if (maxSpeed > 0) ent._fSpeed = maxSpeed;
} else if (ent.pace && typeof ent.pace === "number") {
ent._fSpeed = ent.pace * 10; // Based on "Special Travel Pace," DMG p242
}
it._fHp = 0;
if (it.hp && it.hp.hp != null) {
it._fHp = it.hp.hp;
} else if (it.hull && it.hull.hp != null) {
it._fHp = it.hull.hp;
} else if (it.hp && it.hp.average != null) {
it._fHp = it.hp.average;
ent._fHp = 0;
if (ent.hp && ent.hp.hp != null) {
ent._fHp = ent.hp.hp;
} else if (ent.hull && ent.hull.hp != null) {
ent._fHp = ent.hull.hp;
} else if (ent.hp && ent.hp.average != null) {
ent._fHp = ent.hp.average;
}
it._fAc = 0;
if (it.hull && it.hull.ac != null) {
it._fAc = it.hull.ac;
} else if (it.vehicleType === "INFWAR") {
it._fAc = 19 + Parser.getAbilityModNumber(it.dex == null ? 10 : it.dex);
} else if (it.ac instanceof Array) {
it._fAc = it.ac.map(it => it.special ? null : (it.ac || it)).filter(it => it !== null);
} else if (it.ac) {
it._fAc = it.ac;
ent._fAc = 0;
if (ent.hull && ent.hull.ac != null) {
ent._fAc = ent.hull.ac;
} else if (ent.vehicleType === "INFWAR") {
ent._fAc = 19 + Parser.getAbilityModNumber(ent.dex == null ? 10 : ent.dex);
} else if (ent.ac instanceof Array) {
ent._fAc = ent.ac.map(it => it.special ? null : (it.ac || it)).filter(it => it !== null);
} else if (ent.ac) {
ent._fAc = ent.ac;
}
it._fCreatureCapacity = (it.capCrew || 0) + (it.capPassenger || 0) + (it.capCreature || 0);
ent._fCreatureCapacity = (ent.capCrew || 0) + (ent.capPassenger || 0) + (ent.capCreature || 0);
it._fMisc = it.srd ? ["SRD"] : [];
if (SourceUtil.isLegacySourceWotc(it.source)) it._fMisc.push("Legacy");
if (it.tokenUrl || it.hasToken) it._fMisc.push("Has Token");
if (it.hasFluff || it.fluff?.entries) it._fMisc.push("Has Info");
if (it.hasFluffImages || it.fluff?.images) it._fMisc.push("Has Images");
ent._fMisc = ent.srd ? ["SRD"] : [];
if (SourceUtil.isLegacySourceWotc(ent.source)) ent._fMisc.push("Legacy");
if (Renderer.vehicle.hasToken(ent)) ent._fMisc.push("Has Token");
if (ent.hasFluff || ent.fluff?.entries) ent._fMisc.push("Has Info");
if (ent.hasFluffImages || ent.fluff?.images) ent._fMisc.push("Has Images");
}
addToFilters (it, isExcluded) {

View File

@@ -1469,6 +1469,7 @@ class Filter extends FilterBase {
* Defaults to ascending alphabetical sort.
* @param [opts.itemSortFnMini] Function which should be used to sort the `items` array when rendering mini-pills.
* @param [opts.groupFn] Function which takes an item and assigns it to a group.
* @param [opts.groupNameFn] Function which takes a group and returns a group name;
* @param [opts.minimalUi] True if the filter should render with a reduced UI, false otherwise.
* @param [opts.umbrellaItems] Items which should, when set active, show everything in the filter. E.g. "All".
* @param [opts.umbrellaExcludes] Items which should ignore the state of any `umbrellaItems`
@@ -1489,6 +1490,7 @@ class Filter extends FilterBase {
this._itemSortFn = opts.itemSortFn === undefined ? SortUtil.ascSort : opts.itemSortFn;
this._itemSortFnMini = opts.itemSortFnMini;
this._groupFn = opts.groupFn;
this._groupNameFn = opts.groupNameFn;
this._minimalUi = opts.minimalUi;
this._umbrellaItems = Filter._getAsFilterItems(opts.umbrellaItems);
this._umbrellaExcludes = Filter._getAsFilterItems(opts.umbrellaExcludes);
@@ -2137,14 +2139,14 @@ class Filter extends FilterBase {
_doRenderPills_doRenderWrpGroup (group) {
const existingMeta = this._pillGroupsMeta[group];
if (existingMeta && !existingMeta.isAttached) {
existingMeta.hrDivider.appendTo(this.__wrpPills);
existingMeta.wrpDivider.appendTo(this.__wrpPills);
existingMeta.wrpPills.appendTo(this.__wrpPills);
existingMeta.isAttached = true;
}
if (existingMeta) return;
this._pillGroupsMeta[group] = {
hrDivider: this._doRenderPills_doRenderWrpGroup_getHrDivider(group).appendTo(this.__wrpPills),
wrpDivider: this._doRenderPills_doRenderWrpGroup_getDivider(group).appendTo(this.__wrpPills),
wrpPills: this._doRenderPills_doRenderWrpGroup_getWrpPillsSub(group).appendTo(this.__wrpPills),
isAttached: true,
};
@@ -2152,14 +2154,14 @@ class Filter extends FilterBase {
Object.entries(this._pillGroupsMeta)
.sort((a, b) => SortUtil.ascSortLower(a[0], b[0]))
.forEach(([groupKey, groupMeta], i) => {
groupMeta.hrDivider.appendTo(this.__wrpPills);
groupMeta.hrDivider.toggleVe(!this._isGroupDividerHidden(groupKey, i));
groupMeta.wrpDivider.appendTo(this.__wrpPills);
groupMeta.wrpDivider.toggleVe(!this._isGroupDividerHidden(groupKey, i));
groupMeta.wrpPills.appendTo(this.__wrpPills);
});
if (this._nests) {
this._pillGroupsMeta[group].toggleDividerFromNestVisibility = () => {
this._pillGroupsMeta[group].hrDivider.toggleVe(!this._isGroupDividerHidden(group));
this._pillGroupsMeta[group].wrpDivider.toggleVe(!this._isGroupDividerHidden(group));
};
// bind group dividers to show/hide depending on nest visibility state
@@ -2184,7 +2186,34 @@ class Filter extends FilterBase {
return groupItems.length === hiddenGroupItems.length;
}
_doRenderPills_doRenderWrpGroup_getHrDivider () { return e_({tag: "hr", clazz: `fltr__dropdown-divider--sub hr-2 mx-3`}); }
_doRenderPills_doRenderWrpGroup_getDivider (group) {
const eleHeader = this._doRenderPills_doRenderWrpGroup_getDividerHeader(group);
const eleHr = this._doRenderPills_doRenderWrpGroup_getDividerHr(group);
return e_({
tag: "div",
clazz: "ve-flex-col w-100",
children: [
eleHr,
eleHeader,
]
.filter(Boolean),
});
}
_doRenderPills_doRenderWrpGroup_getDividerHr (group) { return e_({tag: "hr", clazz: `fltr__dropdown-divider--sub hr-2 mx-3`}); }
_doRenderPills_doRenderWrpGroup_getDividerHeader (group) {
const groupName = this._groupNameFn?.(group);
if (!groupName) return null;
return e_({
tag: "div",
clazz: `fltr__divider-header ve-muted italic ve-small`,
text: groupName,
});
}
_doRenderPills_doRenderWrpGroup_getWrpPillsSub () { return e_({tag: "div", clazz: `fltr__wrp-pills--sub fltr__container-pills`}); }
_doRenderMiniPills () {
@@ -2461,7 +2490,7 @@ class Filter extends FilterBase {
Object.values(this._pillGroupsMeta || {})
.forEach(it => {
it.hrDivider.detach();
it.wrpDivider.detach();
it.wrpPills.detach();
it.isAttached = false;
});
@@ -2816,6 +2845,7 @@ class SourceFilter extends Filter {
opts.itemSortFnMini = opts.itemSortFnMini === undefined ? SourceFilter._SORT_ITEMS_MINI.bind(SourceFilter) : opts.itemSortFnMini;
opts.itemSortFn = opts.itemSortFn === undefined ? (a, b) => SortUtil.ascSortLower(Parser.sourceJsonToFull(a.item), Parser.sourceJsonToFull(b.item)) : opts.itemSortFn;
opts.groupFn = opts.groupFn === undefined ? SourceUtil.getFilterGroup : opts.groupFn;
opts.groupNameFn = opts.groupNameFn === undefined ? SourceUtil.getFilterGroupName : opts.groupNameFn;
opts.selFn = opts.selFn === undefined ? PageFilter.defaultSourceSelFn : opts.selFn;
super(opts);
@@ -3071,15 +3101,15 @@ class SourceFilter extends Filter {
return [ent.source].concat(otherSourcesFilt.map(src => new SourceFilterItem({item: src.source, isIgnoreRed: true, isOtherSource: true})));
}
_doRenderPills_doRenderWrpGroup_getHrDivider (group) {
_doRenderPills_doRenderWrpGroup_getDividerHeader (group) {
switch (group) {
case SourceUtil.FILTER_GROUP_NON_STANDARD: return this._doRenderPills_doRenderWrpGroup_getHrDivider_groupNonStandard(group);
case SourceUtil.FILTER_GROUP_HOMEBREW: return this._doRenderPills_doRenderWrpGroup_getHrDivider_groupBrew(group);
default: return super._doRenderPills_doRenderWrpGroup_getHrDivider(group);
case SourceUtil.FILTER_GROUP_NON_STANDARD: return this._doRenderPills_doRenderWrpGroup_getDividerHeader_groupNonStandard(group);
case SourceUtil.FILTER_GROUP_HOMEBREW: return this._doRenderPills_doRenderWrpGroup_getDividerHeader_groupBrew(group);
default: return super._doRenderPills_doRenderWrpGroup_getDividerHeader(group);
}
}
_doRenderPills_doRenderWrpGroup_getHrDivider_groupNonStandard (group) {
_doRenderPills_doRenderWrpGroup_getDividerHeader_groupNonStandard (group) {
let dates = [];
const comp = BaseComponent.fromObject({
min: 0,
@@ -3203,9 +3233,9 @@ class SourceFilter extends Filter {
return e_({
tag: "div",
clazz: `ve-flex-col w-100`,
clazz: `split-v-center w-100`,
children: [
super._doRenderPills_doRenderWrpGroup_getHrDivider(),
super._doRenderPills_doRenderWrpGroup_getDividerHeader(group) || e_({clazz: "div"}),
e_({
tag: "div",
clazz: `mb-1 ve-flex-h-right`,
@@ -3219,7 +3249,7 @@ class SourceFilter extends Filter {
});
}
_doRenderPills_doRenderWrpGroup_getHrDivider_groupBrew (group) {
_doRenderPills_doRenderWrpGroup_getDividerHeader_groupBrew (group) {
const btnClear = e_({
tag: "button",
clazz: `btn btn-xxs btn-default px-1`,
@@ -3235,9 +3265,9 @@ class SourceFilter extends Filter {
return e_({
tag: "div",
clazz: `ve-flex-col w-100`,
clazz: `split-v-center w-100`,
children: [
super._doRenderPills_doRenderWrpGroup_getHrDivider(),
super._doRenderPills_doRenderWrpGroup_getDividerHeader(group) || e_({clazz: "div"}),
e_({
tag: "div",
clazz: `mb-1 ve-flex-h-right`,

View File

@@ -58,24 +58,13 @@ class CreatureBuilder extends Builder {
async pHandleSidebarLoadExistingData (creature, opts) {
opts = opts || [];
function pFetchToken (mon) {
return new Promise(resolve => {
const img = new Image();
const url = Renderer.monster.getTokenUrl(mon);
img.onload = resolve(url);
img.onerror = resolve(null);
img.src = url;
});
}
const cleanOrigin = window.location.origin.replace(/\/+$/, "");
// Get the token based on the original source
if (creature.tokenUrl || creature.hasToken) {
const rawTokenUrl = await pFetchToken(creature);
if (rawTokenUrl) {
creature.tokenUrl = /^[a-zA-Z\d]+:\/\//.test(rawTokenUrl) ? rawTokenUrl : `${cleanOrigin}/${rawTokenUrl}`;
}
if (creature.hasToken) {
creature.token = {
name: creature.name,
source: creature.source,
};
}
// Get the fluff based on the original source
@@ -3224,23 +3213,51 @@ class CreatureBuilder extends Builder {
}
__$getTokenInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Token Image URL");
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Token Image");
const $iptUrl = $(`<input class="form-control form-control--minimal input-xs mr-2">`)
.change(() => doUpdateState())
.val(this._state.tokenUrl || "");
const doUpdateState = () => {
delete this._state.token;
delete this._state.tokenUrl;
delete this._state.tokenHref;
const $btnPreview = $(`<button class="btn btn-xs btn-default mr-2" title="Preview Token"><span class="glyphicon glyphicon-fullscreen"/></button>`)
switch ($selMode.val()) {
case "0": {
this._state.token = {name: $iptExistingName.val().trim(), source: $iptExistingSource.val().trim()};
break;
}
case "1": {
this._state.tokenHref = {
type: "external",
url: $iptExternalUrl.val(),
};
break;
}
case "2": {
this._state.tokenHref = {
type: "internal",
path: $iptInternalPath.val(),
};
break;
}
default: throw new Error("Unimplemented!");
}
cb();
};
const $btnPreview = $(`<button class="btn btn-xs btn-default" title="Preview Token"><span class="glyphicon glyphicon-fullscreen"></span></button>`)
.click((evt) => {
const val = $iptUrl.val().trim();
if (!val) return JqueryUtil.doToast({content: "Please enter an image URL", type: "warning"});
if (!Renderer.monster.hasToken(this._state)) return JqueryUtil.doToast({content: "Please set a token first!", type: "warning"});
const $content = Renderer.hover.$getHoverContent_generic(
{
type: "image",
href: {
type: "external",
url: val,
url: Renderer.monster.getTokenUrl(this._state),
},
},
{isBookContent: true},
@@ -3256,15 +3273,81 @@ class CreatureBuilder extends Builder {
);
});
const doUpdateState = () => {
const val = $iptUrl.val().trim();
if (val) this._state.tokenUrl = val;
else delete this._state.tokenUrl;
const initialMode = this._state.token ? "0" : this._state.tokenHref?.type === "internal" ? "2" : "1";
cb();
};
const $selMode = $(`<select class="form-control input-xs mr-2">
<option value="0">Existing Creature</option>
<option value="1">External URL</option>
<option value="2">Internal URL</option>
</select>`)
.val(initialMode)
.change(() => {
switch ($selMode.val()) {
case "0": {
$stgExistingCreature.showVe(); $stgExternalUrl.hideVe(); $stgInternalUrl.hideVe();
doUpdateState();
break;
}
case "1": {
$stgExistingCreature.hideVe(); $stgExternalUrl.showVe(); $stgInternalUrl.hideVe();
doUpdateState();
break;
}
case "2": {
$stgExistingCreature.hideVe(); $stgExternalUrl.hideVe(); $stgInternalUrl.showVe();
doUpdateState();
break;
}
}
});
$$`<div class="ve-flex">${$iptUrl}${$btnPreview}</div>`.appendTo($rowInner);
// region Existing creature
const $iptExistingName = $(`<input class="form-control input-xs form-control--minimal">`)
.val(this._state.token?.name || "")
.on("change", () => doUpdateState());
const $iptExistingSource = $(`<input class="form-control input-xs form-control--minimal">`)
.val(this._state.token?.source || "")
.on("change", () => doUpdateState());
const $stgExistingCreature = $$`<div class="ve-flex-col mb-2">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--25">Name</span>${$iptExistingName}</div>
<div class="ve-flex-v-center"><span class="mr-2 mkbru__sub-name--25">Source</span>${$iptExistingSource}</div>
</div>`
.toggleVe(initialMode === "0");
// endregion
// region External URL
const $iptExternalUrl = $(`<input class="form-control form-control--minimal input-xs code">`)
.change(() => doUpdateState())
.val(
this._state.tokenHref?.url
|| this._state.tokenUrl // TODO(Future) legacy; remove
|| "",
);
const $stgExternalUrl = $$`<div class="ve-flex-col mb-2">
<div class="ve-flex-v-center"><span class="mr-2 mkbru__sub-name--25">URL</span>${$iptExternalUrl}</div>
</div>`
.toggleVe(initialMode === "1");
// endregion
// region Internal URL
const $iptInternalPath = $(`<input class="form-control form-control--minimal input-xs code">`)
.change(() => doUpdateState())
.val(this._state.tokenHref?.path || "");
const $stgInternalUrl = $$`<div class="ve-flex-col mb-2">
<div class="ve-flex-v-center"><span class="mr-2 mkbru__sub-name--25">Path</span>${$iptInternalPath}</div>
</div>`
.toggleVe(initialMode === "2");
// endregion
$$`<div class="ve-flex-col">
<div class="ve-flex mb-2">${$selMode}${$btnPreview}</div>
${$stgExistingCreature}
${$stgExternalUrl}
${$stgInternalUrl}
</div>`.appendTo($rowInner);
return $row;
}

View File

@@ -124,11 +124,10 @@ class ObjectsPage extends ListPage {
(this._$dispToken = this._$dispToken || $(`#float-token`)).empty();
const hasToken = ent.tokenUrl || ent.hasToken;
if (hasToken) {
const imgLink = Renderer.object.getTokenUrl(ent);
this._$dispToken.append(`<a href="${imgLink}" target="_blank" rel="noopener noreferrer"><img src="${imgLink}" id="token_image" class="token" alt="Token Image: ${(ent.name || "").qq()}" ${ent.tokenCredit ? `title="Credit: ${ent.tokenCredit.qq()}"` : ""} loading="lazy"></a>`);
}
if (!Renderer.object.hasToken(ent)) return;
const imgLink = Renderer.object.getTokenUrl(ent);
this._$dispToken.append(`<a href="${imgLink}" target="_blank" rel="noopener noreferrer"><img src="${imgLink}" id="token_image" class="token" alt="Token Image: ${(ent.name || "").qq()}" ${ent.tokenCredit ? `title="Credit: ${ent.tokenCredit.qq()}"` : ""} loading="lazy"></a>`);
}
_renderStats_onTabChangeStats () {

View File

@@ -246,7 +246,7 @@ class IndexableDirectoryBestiary extends IndexableDirectory {
baseUrl: "bestiary.html",
isHover: true,
fnGetToken: (ent) => {
if (!ent.tokenUrl && !ent.hasToken) return null;
if (!Renderer.monster.hasToken(ent)) return null;
return Renderer.monster.getTokenUrl(ent);
},
});
@@ -977,7 +977,7 @@ class IndexableFileObjects extends IndexableFile {
baseUrl: "objects.html",
isHover: true,
fnGetToken: (ent) => {
if (!ent.tokenUrl && !ent.hasToken) return null;
if (!Renderer.object.hasToken(ent)) return null;
return Renderer.object.getTokenUrl(ent);
},
});
@@ -1080,7 +1080,7 @@ class IndexableFileVehicles extends IndexableFile {
baseUrl: "vehicles.html",
isHover: true,
fnGetToken: (ent) => {
if (!ent.tokenUrl && !ent.hasToken) return null;
if (!Renderer.vehicle.hasToken(ent)) return null;
return Renderer.vehicle.getTokenUrl(ent);
},
});

View File

@@ -317,7 +317,7 @@ class Omnisearch {
}
static _renderLink_getHoverString (category, url, src, {isFauxPage = false} = {}) {
return `onmouseover="Renderer.hover.pHandleLinkMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" data-vet-page="${UrlUtil.categoryToHoverPage(category).qq()}" data-vet-source="${src.qq()}" data-vet-hash="${url.qq()}" ${isFauxPage ? `data-vet-is-faux-page="true"` : ""} ${Renderer.hover.getPreventTouchString()}`;
return `onmouseover="Renderer.hover.pHandleLinkMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" ondragstart="Renderer.hover.handleLinkDragStart(event, this)" data-vet-page="${UrlUtil.categoryToHoverPage(category).qq()}" data-vet-source="${src.qq()}" data-vet-hash="${url.qq()}" ${isFauxPage ? `data-vet-is-faux-page="true"` : ""} ${Renderer.hover.getPreventTouchString()}`;
}
static $getResultLink (r) {

View File

@@ -21,10 +21,6 @@ class PlutoniumPage {
{path: "plutonium/compact-chat.webp", id: "wrp-img-compact-chat"},
];
static _VIDEOS = [
{path: "plutonium/artbrowser.webm", id: "video-artbrowser"},
];
static _pOnLoad_initElements () {
this._IMAGES
.forEach(({path, id}) => {
@@ -34,14 +30,6 @@ class PlutoniumPage {
.html(`<img class="big-help-gif" src="${url}" loading="lazy">`)
.attr("href", url);
});
this._VIDEOS
.forEach(({path, id}) => {
const url = Renderer.get().getMediaUrl("img", path);
$(`#${id}`)
.html(`<source src="${url}" type="video/webm">`);
});
}
static async pOnLoad () {

View File

@@ -34,7 +34,7 @@ class RenderBestiary {
const htmlSourceAndEnvironment = this._$getRenderedCreature_getHtmlSourceAndEnvironment(mon, legGroup);
const hasToken = mon.tokenUrl || mon.hasToken;
const hasToken = Renderer.monster.hasToken(mon);
const extraThClasses = hasToken ? ["mon__name--token"] : null;
const ptsResource = mon.resource?.length
@@ -51,8 +51,8 @@ class RenderBestiary {
</td></tr>
<tr><td class="divider" colspan="6"><div></div></td></tr>
<tr><td colspan="6"><div ${hasToken ? `class="mon__wrp-avoid-token"` : ""}><strong>Armor Class</strong> ${Parser.acToFull(mon.ac)}</div></td></tr>
<tr><td colspan="6"><div ${hasToken ? `class="mon__wrp-avoid-token"` : ""}><strong>Hit Points</strong> ${Renderer.monster.getRenderedHp(mon.hp)}</div></td></tr>
<tr><td colspan="6"><div ${hasToken ? `class="mon__wrp-avoid-token"` : ""}><strong>Armor Class</strong> ${mon.ac == null ? "\u2014" : Parser.acToFull(mon.ac)}</div></td></tr>
<tr><td colspan="6"><div ${hasToken ? `class="mon__wrp-avoid-token"` : ""}><strong>Hit Points</strong> ${mon.hp == null ? "\u2014" : Renderer.monster.getRenderedHp(mon.hp)}</div></td></tr>
${ptsResource.join("")}
<tr><td colspan="6"><strong>Speed</strong> ${Parser.getSpeedString(mon)}</td></tr>
<tr><td class="divider" colspan="6"><div></div></td></tr>

View File

@@ -1,10 +1,17 @@
"use strict";
class RenderMap {
static _getZoom (mapData) {
return this._ZOOM_LEVELS[mapData.ixZoom];
static _ZOOM_LEVELS = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0, 5.0];
static _AREA_CACHE = {};
/* -------------------------------------------- */
static _getZoom (mapData, {ixZoom} = {}) {
return this._ZOOM_LEVELS[ixZoom ?? mapData.ixZoom];
}
/* -------------------------------------------- */
static async pShowViewer (evt, ele) {
const mapData = JSON.parse(ele.dataset.rdPackedMap);
@@ -16,10 +23,10 @@ class RenderMap {
if (!mapData.loadedImage) return;
const $content = this._$getWindowContent(mapData);
const {$wrp, setZoom} = this._$getWindowContent(mapData);
Renderer.hover.getShowWindow(
$content,
const hoverWindow = Renderer.hover.getShowWindow(
$wrp,
// Open in the top-right corner of the screen
Renderer.hover.getWindowPositionExact(document.body.clientWidth, 7, evt),
{
@@ -28,7 +35,7 @@ class RenderMap {
isBookContent: true,
width: Math.min(Math.floor(document.body.clientWidth / 2), mapData.width),
height: mapData.height + 32,
$pFnGetPopoutContent: this._$getWindowContent.bind(this, mapData),
$pFnGetPopoutContent: () => this._$getWindowContent(mapData).$wrp,
fnGetPopoutSize: () => {
return {
width: Math.min(window.innerWidth, Math.round(this._getZoom(mapData) * mapData.width)),
@@ -38,23 +45,67 @@ class RenderMap {
isPopout: !!evt.shiftKey,
},
);
this._mutInitialZoom({
fnGetContainerDimensions: () => {
const {wWrpContent, hWrapContent} = hoverWindow.getPosition();
return {
w: wWrpContent,
h: hWrapContent,
};
},
mapData,
setZoom,
});
}
static async $pGetRendered (mapData) {
/* -------------------------------------------- */
static async $pGetRendered (mapData, {fnGetContainerDimensions = null} = {}) {
await RenderMap._pMutMapData(mapData);
if (!mapData.loadedImage) return;
return this._$getWindowContent(mapData);
const {$wrp, setZoom} = this._$getWindowContent(mapData);
this._mutInitialZoom({
fnGetContainerDimensions,
mapData,
setZoom,
});
return $wrp;
}
/* -------------------------------------------- */
/** Treat container as slightly larger than it actually is, as many maps have borders/"dead" regions around the edges */
static _INITIAL_ZOOM_PAD_MULTIPLIER = 1.1;
static _mutInitialZoom ({fnGetContainerDimensions, mapData, setZoom}) {
if (!fnGetContainerDimensions) return;
const {w, h} = fnGetContainerDimensions();
// Attempt to zoom out until one full edge of the image is in-view
let ixZoomDesired = this._ZOOM_LEVELS.length - 1;
while (ixZoomDesired > 0) {
const isInW = Math.round(this._getZoom(mapData, {ixZoom: ixZoomDesired}) * mapData.width) <= (w * this._INITIAL_ZOOM_PAD_MULTIPLIER);
const isInH = Math.round(this._getZoom(mapData, {ixZoom: ixZoomDesired}) * mapData.height) <= (h * this._INITIAL_ZOOM_PAD_MULTIPLIER);
if (isInW || isInH) break;
ixZoomDesired--;
}
if (ixZoomDesired !== mapData.ixZoom) setZoom(ixZoomDesired);
}
/* -------------------------------------------- */
static async _pMutMapData (mapData) {
// Store some additional data on this mapData state object
mapData.ixZoom = RenderMap._ZOOM_LEVELS.indexOf(1.0);
mapData.activeWindows = {};
mapData.loadedImage = await RenderMap._pLoadImage(mapData);
if (mapData.loadedImage) {
mapData.width = mapData.width || mapData.loadedImage.naturalWidth;
mapData.height = mapData.height || mapData.loadedImage.naturalHeight;
}
if (!mapData.loadedImage) return;
mapData.width = mapData.width || mapData.loadedImage.naturalWidth;
mapData.height = mapData.height || mapData.loadedImage.naturalHeight;
}
static async _pLoadImage (mapData) {
@@ -75,6 +126,8 @@ class RenderMap {
return out;
}
/* -------------------------------------------- */
static _$getWindowContent (mapData) {
const X = 0;
const Y = 1;
@@ -101,6 +154,10 @@ class RenderMap {
if (lastIxZoom === mapData.ixZoom) return;
}
onZoomChange();
};
const onZoomChange = () => {
const zoom = this._getZoom(mapData);
const nxtWidth = Math.round(mapData.width * zoom);
@@ -346,7 +403,17 @@ class RenderMap {
zoomChange();
return $out;
return {
$wrp: $out,
setZoom: ixZoom => {
if (mapData.ixZoom === ixZoom) return;
if (!Array.from({length: this._ZOOM_LEVELS.length}, (_, i) => i).includes(ixZoom)) throw new Error(`Unhandled zoom index "${ixZoom}"`);
mapData.ixZoom = ixZoom;
onZoomChange();
},
};
}
static async _pGetArea (areaId, mapData) {
@@ -403,5 +470,3 @@ class RenderMap {
}
}
}
RenderMap._ZOOM_LEVELS = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0, 5.0];
RenderMap._AREA_CACHE = {};

View File

@@ -866,7 +866,7 @@ RendererMarkdown.utils = class {
static getRenderedAbilityScores (ent, {prefix = ""} = "") {
return `${prefix}|${Parser.ABIL_ABVS.map(it => `${it.toUpperCase()}|`).join("")}
${prefix}|:---:|:---:|:---:|:---:|:---:|:---:|
${prefix}|${Parser.ABIL_ABVS.map(ab => `${ent[ab]} (${Parser.getAbilityModifier(ent[ab])})|`).join("")}`;
${prefix}|${Parser.ABIL_ABVS.map(ab => ent[ab] == null ? `\u2014|` : `${ent[ab]} (${Parser.getAbilityModifier(ent[ab])})|`).join("")}`;
}
};
@@ -899,7 +899,7 @@ RendererMarkdown.monster = class {
const monTypes = Parser.monTypeToFullObj(mon.type);
RendererMarkdown.get().isSkipStylingItemLinks = true;
const acPart = Parser.acToFull(mon.ac, RendererMarkdown.get());
const acPart = mon.ac == null ? "\u2014" : Parser.acToFull(mon.ac, RendererMarkdown.get());
RendererMarkdown.get().isSkipStylingItemLinks = false;
const resourcePart = mon.resource?.length
? mon.resource
@@ -947,7 +947,7 @@ RendererMarkdown.monster = class {
>*${mon.level ? `${Parser.getOrdinalForm(mon.level)}-level ` : ""}${Renderer.utils.getRenderedSize(mon.size)} ${monTypes.asText}${mon.alignment ? `, ${mon.alignmentPrefix ? RendererMarkdown.get().render(mon.alignmentPrefix) : ""}${Parser.alignmentListToFull(mon.alignment)}` : ""}*
>___
>- **Armor Class** ${acPart}
>- **Hit Points** ${Renderer.monster.getRenderedHp(mon.hp, true)}${resourcePart}
>- **Hit Points** ${mon.hp == null ? "\u2014" : Renderer.monster.getRenderedHp(mon.hp, true)}${resourcePart}
>- **Speed** ${Parser.getSpeedString(mon)}
>___
${abilityScorePart}

View File

@@ -495,9 +495,11 @@ globalThis.Renderer = function () {
textStack[0] += `<div class="rd__image-title">`;
if (entry.title && !entry.mapRegions) textStack[0] += `<div class="rd__image-title-inner">${this.render(entry.title)}</div>`;
const isDynamicViewer = entry.mapRegions && !IS_VTT;
if (entry.mapRegions && !IS_VTT) {
if (entry.title && !isDynamicViewer) textStack[0] += `<div class="rd__image-title-inner">${this.render(entry.title)}</div>`;
if (isDynamicViewer) {
textStack[0] += `<button class="btn btn-xs btn-default rd__image-btn-viewer" onclick="RenderMap.pShowViewer(event, this)" data-rd-packed-map="${this._renderImage_getMapRegionData(entry)}" ${ptAdventureBookMeta} title="Open Dynamic Viewer (SHIFT to Open in New Window)"><span class="glyphicon glyphicon-picture"></span> ${Renderer.stripTags(entry.title) || "Dynamic Viewer"}</button>`;
}
@@ -2039,7 +2041,7 @@ globalThis.Renderer = function () {
const replacementAttributes = pluginData.map(it => it.attributesHoverReplace).filter(Boolean);
if (replacementAttributes.length) return replacementAttributes.join(" ");
return `onmouseover="Renderer.hover.pHandleLinkMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" data-vet-page="${entry.href.hover.page.qq()}" data-vet-source="${entry.href.hover.source.qq()}" data-vet-hash="${procHash.qq()}" ${entry.href.hover.preloadId != null ? `data-vet-preload-id="${`${entry.href.hover.preloadId}`.qq()}"` : ""} ${entry.href.hover.isFauxPage ? `data-vet-is-faux-page="true"` : ""} ${Renderer.hover.getPreventTouchString()}`;
return `onmouseover="Renderer.hover.pHandleLinkMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" ondragstart="Renderer.hover.handleLinkDragStart(event, this)" data-vet-page="${entry.href.hover.page.qq()}" data-vet-source="${entry.href.hover.source.qq()}" data-vet-hash="${procHash.qq()}" ${entry.href.hover.preloadId != null ? `data-vet-preload-id="${`${entry.href.hover.preloadId}`.qq()}"` : ""} ${entry.href.hover.isFauxPage ? `data-vet-is-faux-page="true"` : ""} ${Renderer.hover.getPreventTouchString()}`;
};
/**
@@ -6785,7 +6787,7 @@ Renderer.object = class {
const renderer = Renderer.get().setFirstSection(true);
const hasToken = ent.tokenUrl || ent.hasToken;
const hasToken = Renderer.object.hasToken(ent);
const extraThClasses = !opts.isCompact && hasToken ? ["objs__name--token"] : null;
const entriesMeta = Renderer.object.getObjectRenderableEntriesMeta(ent);
@@ -6807,9 +6809,12 @@ Renderer.object = class {
`;
}
static hasToken (obj) {
return Renderer.generic.hasToken(obj);
}
static getTokenUrl (obj) {
if (obj.tokenUrl) return obj.tokenUrl;
return Renderer.get().getMediaUrl("img", `objects/tokens/${Parser.sourceJsonToAbv(obj.source)}/${Parser.nameToTokenName(obj.name)}.webp`);
return Renderer.generic.getTokenUrl(obj, "objects/tokens");
}
static pGetFluff (obj) {
@@ -7499,7 +7504,7 @@ Renderer.monster = class {
const renderStack = [];
const legGroup = DataUtil.monster.getMetaGroup(mon);
const hasToken = mon.tokenUrl || mon.hasToken;
const hasToken = Renderer.monster.hasToken(mon);
const extraThClasses = !opts.isCompact && hasToken ? ["mon__name--token"] : null;
const isShowCrScaler = ScaleCreature.isCrInScaleRange(mon);
@@ -7550,8 +7555,8 @@ Renderer.monster = class {
${hasToken && !opts.isCompact ? `<th colspan="1"></th>` : ""}
</tr>
<tr>
<td colspan="2">${Parser.acToFull(mon.ac)}</td>
<td colspan="2">${Renderer.monster.getRenderedHp(mon.hp)}</td>
<td colspan="2">${mon.ac == null ? "\u2014" : Parser.acToFull(mon.ac)}</td>
<td colspan="2">${mon.hp == null ? "\u2014" : Renderer.monster.getRenderedHp(mon.hp)}</td>
<td colspan="2">${Parser.getSpeedString(mon)}</td>
${ptCrSpellLevel}
${mon.pbNote || Parser.crToNumber(mon.cr) < VeCt.CR_CUSTOM ? `<td colspan="1">${mon.pbNote ?? UiUtil.intToBonus(Parser.crToPb(mon.cr), {isPretty: true})}</td>` : ""}
@@ -7747,9 +7752,12 @@ Renderer.monster = class {
return skills;
}
static hasToken (mon) {
return Renderer.generic.hasToken(mon);
}
static getTokenUrl (mon) {
if (mon.tokenUrl) return mon.tokenUrl;
return Renderer.get().getMediaUrl("img", `bestiary/tokens/${Parser.sourceJsonToAbv(mon.source)}/${Parser.nameToTokenName(mon.name)}.webp`);
return Renderer.generic.getTokenUrl(mon, "bestiary/tokens");
}
static postProcessFluff (mon, fluff) {
@@ -7823,8 +7831,16 @@ Renderer.monster = class {
static getRenderedEnvironment (envs) { return (envs || []).sort(SortUtil.ascSortLower).map(it => it.toTitleCase()).join(", "); }
static getAltArtDisplayName (meta) { return meta.displayName || meta.name || meta.token?.name; }
static getAltArtSource (meta) { return meta.source || meta.token?.source; }
static getRenderedAltArtEntry (meta, {isPlainText = false} = {}) {
return `${isPlainText ? "" : `<div>`}${meta.displayName || meta.name}; ${isPlainText ? "" : `<span title="${Parser.sourceJsonToFull(meta.source)}">`}${Parser.sourceJsonToAbv(meta.source)}${Renderer.utils.isDisplayPage(meta.page) ? ` p${meta.page}` : ""}${isPlainText ? "" : `</span></div>`}`;
const displayName = Renderer.monster.getAltArtDisplayName(meta);
const source = Renderer.monster.getAltArtSource(meta);
if (!displayName || !source) return "";
return `${isPlainText ? "" : `<div>`}${displayName}; ${isPlainText ? "" : `<span title="${Parser.sourceJsonToFull(source)}">`}${Parser.sourceJsonToAbv(source)}${Renderer.utils.isDisplayPage(meta.page) ? ` p${meta.page}` : ""}${isPlainText ? "" : `</span></div>`}`;
}
static pGetFluff (mon) {
@@ -8904,7 +8920,6 @@ Renderer.item = class {
static _INHERITED_PROPS_BLOCKLIST = new Set([
// region Specific merge strategy
"entries",
"rarity",
// endregion
// region Meaningless on merged item
@@ -8932,11 +8947,6 @@ Renderer.item = class {
genericVariant.entries = MiscUtil.copyFast(Renderer.applyAllProperties(genericVariant.inherits.entries, genericVariant.inherits));
}
if (genericVariant.inherits.rarity == null) delete genericVariant.rarity;
else if (genericVariant.inherits.rarity === "varies") {
/* No-op, i.e., use current rarity */
} else genericVariant.rarity = genericVariant.inherits.rarity;
if (genericVariant.requires.armor) genericVariant.armor = genericVariant.requires.armor;
}
@@ -9874,7 +9884,7 @@ Renderer.vehicle = class {
const entriesMeta = Renderer.vehicle.getVehicleRenderableEntriesMeta(ent);
const entriesMetaShip = Renderer.vehicle.ship.getVehicleShipRenderableEntriesMeta(ent);
const hasToken = ent.tokenUrl || ent.hasToken;
const hasToken = Renderer.vehicle.hasToken(ent);
const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null;
return `
@@ -9913,7 +9923,7 @@ Renderer.vehicle = class {
static _getRenderedString_spelljammer (veh, opts) {
const renderer = Renderer.get();
const hasToken = veh.tokenUrl || veh.hasToken;
const hasToken = Renderer.vehicle.hasToken(veh);
const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null;
return `
@@ -9954,7 +9964,7 @@ Renderer.vehicle = class {
const entriesMeta = Renderer.vehicle.getVehicleRenderableEntriesMeta(ent);
const entriesMetaInfwar = Renderer.vehicle.infwar.getVehicleInfwarRenderableEntriesMeta(ent);
const hasToken = ent.tokenUrl || ent.hasToken;
const hasToken = Renderer.vehicle.hasToken(ent);
const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null;
return `
@@ -9985,9 +9995,12 @@ Renderer.vehicle = class {
});
}
static hasToken (veh) {
return Renderer.generic.hasToken(veh);
}
static getTokenUrl (veh) {
if (veh.tokenUrl) return veh.tokenUrl;
return Renderer.get().getMediaUrl("img", `vehicles/tokens/${Parser.sourceJsonToAbv(veh.source)}/${Parser.nameToTokenName(veh.name)}.webp`);
return Renderer.generic.getTokenUrl(veh, "vehicles/tokens");
}
};
@@ -10150,8 +10163,12 @@ Renderer.adventureBook = class {
static _isAltMissingCoverUsed = false;
static getCoverUrl (contents) {
if (contents.cover) {
return UrlUtil.link(Renderer.utils.getEntryMediaUrl(contents, "cover", "img"));
}
// TODO(Future) remove as deprecated; remove from schema; remove from proporder
if (contents.coverUrl) {
// FIXME(future) migrate to e.g. a `coverImage` "image"-type entry to avoid this hack
if (/^https?:\/\//.test(contents.coverUrl)) return contents.coverUrl;
return UrlUtil.link(Renderer.get().getMediaUrl("img", contents.coverUrl.replace(/^img\//, "")));
}
@@ -10896,32 +10913,51 @@ Renderer.generic = class {
default: return null;
}
}
/* -------------------------------------------- */
static hasToken (ent) {
return ent.tokenUrl // TODO(Future) legacy; remove
|| ent.hasToken // An implicit token
|| ent.token // An explicit token
|| ent.tokenHref // An explicit token URL (local or external)
;
}
static getTokenUrl (ent, mediaDir) {
if (ent.tokenUrl) return ent.tokenUrl; // TODO(Future) legacy; remove
if (ent.token) return Renderer.get().getMediaUrl("img", `${mediaDir}/${Parser.sourceJsonToAbv(ent.token.source)}/${Parser.nameToTokenName(ent.token.name)}.webp`);
if (ent.tokenHref) return Renderer.utils.getEntryMediaUrl(ent, "tokenHref", "img");
return Renderer.get().getMediaUrl("img", `${mediaDir}/${Parser.sourceJsonToAbv(ent.source)}/${Parser.nameToTokenName(ent.name)}.webp`);
}
};
Renderer.hover = {
LinkMeta: function () {
this.isHovered = false;
this.isLoading = false;
this.isPermanent = false;
this.windowMeta = null;
},
Renderer.hover = class {
static LinkMeta = class {
constructor () {
this.isHovered = false;
this.isLoading = false;
this.isPermanent = false;
this.windowMeta = null;
}
};
_BAR_HEIGHT: 16,
static _BAR_HEIGHT = 16;
_linkCache: {},
_eleCache: new Map(),
_entryCache: {},
_isInit: false,
_dmScreen: null,
_lastId: 0,
_contextMenu: null,
_contextMenuLastClicked: null,
static _linkCache = {};
static _eleCache = new Map();
static _entryCache = {};
static _isInit = false;
static _dmScreen = null;
static _lastId = 0;
static _contextMenu = null;
static _contextMenuLastClicked = null;
bindDmScreen (screen) { this._dmScreen = screen; },
static bindDmScreen (screen) { this._dmScreen = screen; }
_getNextId () { return ++Renderer.hover._lastId; },
static _getNextId () { return ++Renderer.hover._lastId; }
_doInit () {
static _doInit () {
if (!Renderer.hover._isInit) {
Renderer.hover._isInit = true;
@@ -10956,9 +10992,9 @@ Renderer.hover = {
),
]);
}
},
}
cleanTempWindows () {
static cleanTempWindows () {
for (const [key, meta] of Renderer.hover._eleCache.entries()) {
// If this is an element-less "permanent" show which has been closed
if (!meta.isPermanent && meta.windowMeta && typeof key === "number") {
@@ -10983,20 +11019,20 @@ Renderer.hover = {
}
}
}
},
}
_doCloseAllWindows ({hoverIdBlocklist = null} = {}) {
static _doCloseAllWindows ({hoverIdBlocklist = null} = {}) {
Object.entries(Renderer.hover._WINDOW_METAS)
.filter(([hoverId, meta]) => hoverIdBlocklist == null || !hoverIdBlocklist.has(Number(hoverId)))
.forEach(([, meta]) => meta.doClose());
},
}
_getSetMeta (ele) {
static _getSetMeta (ele) {
if (!Renderer.hover._eleCache.has(ele)) Renderer.hover._eleCache.set(ele, new Renderer.hover.LinkMeta());
return Renderer.hover._eleCache.get(ele);
},
}
_handleGenericMouseOverStart ({evt, ele}) {
static _handleGenericMouseOverStart ({evt, ele}) {
// Don't open on small screens unless forced
if (Renderer.hover.isSmallScreen(evt) && !evt.shiftKey) return;
@@ -11013,9 +11049,9 @@ Renderer.hover = {
meta.isPermanent = evt.shiftKey;
return meta;
},
}
_doPredefinedShowStart ({entryId}) {
static _doPredefinedShowStart ({entryId}) {
Renderer.hover.cleanTempWindows();
const meta = Renderer.hover._getSetMeta(entryId);
@@ -11023,10 +11059,20 @@ Renderer.hover = {
meta.isPermanent = true;
return meta;
},
}
static getLinkElementData (ele) {
return {
page: ele.dataset.vetPage,
source: ele.dataset.vetSource,
hash: ele.dataset.vetHash,
preloadId: ele.dataset.vetPreloadId,
isFauxPage: ele.dataset.vetIsFauxPage,
};
}
// (Baked into render strings)
async pHandleLinkMouseOver (evt, ele, opts) {
static async pHandleLinkMouseOver (evt, ele, opts) {
Renderer.hover._doInit();
let page, source, hash, preloadId, customHashId, isFauxPage;
@@ -11038,11 +11084,13 @@ Renderer.hover = {
customHashId = opts.customHashId;
isFauxPage = !!opts.isFauxPage;
} else {
page = ele.dataset.vetPage;
source = ele.dataset.vetSource;
hash = ele.dataset.vetHash;
preloadId = ele.dataset.vetPreloadId;
isFauxPage = ele.dataset.vetIsFauxPage;
({
page,
source,
hash,
preloadId,
isFauxPage,
} = Renderer.hover.getLinkElementData(ele));
}
let meta = Renderer.hover._handleGenericMouseOverStart({evt, ele});
@@ -11130,10 +11178,10 @@ Renderer.hover = {
const fnBind = Renderer.hover.getFnBindListenersCompact(page);
if (fnBind) fnBind(toRender, $content);
}
},
}
// (Baked into render strings)
handleInlineMouseOver (evt, ele, entry, opts) {
static handleInlineMouseOver (evt, ele, entry, opts) {
Renderer.hover._doInit();
entry = entry || JSON.parse(ele.dataset.vetEntry);
@@ -11173,9 +11221,9 @@ Renderer.hover = {
sourceData: entry,
},
);
},
}
async pGetHoverableFluff (page, source, hash, opts) {
static async pGetHoverableFluff (page, source, hash, opts) {
// Try to fetch the fluff directly
let toRender = await DataLoader.pCacheAndGet(`${page}Fluff`, source, hash, opts);
@@ -11200,10 +11248,10 @@ Renderer.hover = {
}
return toRender;
},
}
// (Baked into render strings)
handleLinkMouseLeave (evt, ele) {
static handleLinkMouseLeave (evt, ele) {
const meta = Renderer.hover._eleCache.get(ele);
ele.style.cursor = "";
@@ -11220,10 +11268,10 @@ Renderer.hover = {
meta.windowMeta.doClose();
meta.windowMeta = null;
}
},
}
// (Baked into render strings)
handleLinkMouseMove (evt, ele) {
static handleLinkMouseMove (evt, ele) {
const meta = Renderer.hover._eleCache.get(ele);
if (!meta || meta.isPermanent) return;
@@ -11241,7 +11289,7 @@ Renderer.hover = {
meta.isPermanent = true;
meta.windowMeta.setIsPermanent(true);
}
},
}
/**
* (Baked into render strings)
@@ -11252,7 +11300,7 @@ Renderer.hover = {
* @param [opts.isBookContent]
* @param [opts.isLargeBookContent]
*/
handlePredefinedMouseOver (evt, ele, entryId, opts) {
static handlePredefinedMouseOver (evt, ele, entryId, opts) {
opts = opts || {};
const meta = Renderer.hover._handleGenericMouseOverStart({evt, ele});
@@ -11280,9 +11328,24 @@ Renderer.hover = {
// Reset cursor
ele.style.cursor = "";
},
}
doPredefinedShow (entryId, opts) {
// (Baked into render strings)
static handleLinkDragStart (evt, ele) {
// Close the window
Renderer.hover.handleLinkMouseLeave(evt, ele);
const {page, source, hash} = Renderer.hover.getLinkElementData(ele);
const meta = {
type: VeCt.DRAG_TYPE_IMPORT,
page,
source,
hash,
};
evt.dataTransfer.setData("application/json", JSON.stringify(meta));
}
static doPredefinedShow (entryId, opts) {
opts = opts || {};
const meta = Renderer.hover._doPredefinedShowStart({entryId});
@@ -11303,24 +11366,24 @@ Renderer.hover = {
sourceData: toRender,
},
);
},
}
// (Baked into render strings)
handlePredefinedMouseLeave (evt, ele) { return Renderer.hover.handleLinkMouseLeave(evt, ele); },
static handlePredefinedMouseLeave (evt, ele) { return Renderer.hover.handleLinkMouseLeave(evt, ele); }
// (Baked into render strings)
handlePredefinedMouseMove (evt, ele) { return Renderer.hover.handleLinkMouseMove(evt, ele); },
static handlePredefinedMouseMove (evt, ele) { return Renderer.hover.handleLinkMouseMove(evt, ele); }
_WINDOW_POSITION_PROPS_FROM_EVENT: [
static _WINDOW_POSITION_PROPS_FROM_EVENT = [
"isFromBottom",
"isFromRight",
"clientX",
"window",
"isPreventFlicker",
"bcr",
],
];
getWindowPositionFromEvent (evt, {isPreventFlicker = false} = {}) {
static getWindowPositionFromEvent (evt, {isPreventFlicker = false} = {}) {
const ele = evt.target;
const win = evt?.view?.window || window;
@@ -11338,37 +11401,37 @@ Renderer.hover = {
isPreventFlicker,
bcr,
};
},
}
getWindowPositionExact (x, y, evt = null) {
static getWindowPositionExact (x, y, evt = null) {
return {
window: evt?.view?.window || window,
mode: "exact",
x,
y,
};
},
}
getWindowPositionExactVisibleBottom (x, y, evt = null) {
static getWindowPositionExactVisibleBottom (x, y, evt = null) {
return {
...Renderer.hover.getWindowPositionExact(x, y, evt),
mode: "exactVisibleBottom",
};
},
}
_WINDOW_METAS: {},
MIN_Z_INDEX: 200,
_MAX_Z_INDEX: 300,
_DEFAULT_WIDTH_PX: 600,
_BODY_SCROLLER_WIDTH_PX: 15,
static _WINDOW_METAS = {};
static MIN_Z_INDEX = 200;
static _MAX_Z_INDEX = 300;
static _DEFAULT_WIDTH_PX = 600;
static _BODY_SCROLLER_WIDTH_PX = 15;
_getZIndex () {
static _getZIndex () {
const zIndices = Object.values(Renderer.hover._WINDOW_METAS).map(it => it.zIndex);
if (!zIndices.length) return Renderer.hover.MIN_Z_INDEX;
return Math.max(...zIndices);
},
}
_getNextZIndex (hoverId) {
static _getNextZIndex (hoverId) {
const cur = Renderer.hover._getZIndex();
// If we're already the highest index, continue to use this index
if (hoverId != null && Renderer.hover._WINDOW_METAS[hoverId].zIndex === cur) return cur;
@@ -11394,14 +11457,14 @@ Renderer.hover = {
return Renderer.hover._getNextZIndex(hoverId);
} else return out;
},
}
_isIntersectRect (r1, r2) {
static _isIntersectRect (r1, r2) {
return r1.left <= r2.right
&& r2.left <= r1.right
&& r1.top <= r2.bottom
&& r2.top <= r1.bottom;
},
}
/**
* @param $content Content to append to the window.
@@ -11421,7 +11484,7 @@ Renderer.hover = {
* @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.
*/
getShowWindow ($content, position, opts) {
static getShowWindow ($content, position, opts) {
opts = opts || {};
Renderer.hover._doInit();
@@ -11632,12 +11695,16 @@ Renderer.hover = {
hoverWindow.doMaximize = Renderer.hover._getShowWindow_doMaximize.bind(this, {$brdrTop, $hov});
hoverWindow.doZIndexToFront = Renderer.hover._getShowWindow_doZIndexToFront.bind(this, {$hov, hoverWindow, hoverId});
hoverWindow.getPosition = Renderer.hover._getShowWindow_getPosition.bind(this, {$hov, $wrpContent, position});
hoverWindow.$setContent = ($contentNxt) => $wrpContent.empty().append($contentNxt);
if (opts.isPopout) Renderer.hover._getShowWindow_pDoPopout({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content});
return hoverWindow;
},
}
_getShowWindow_doClose ({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}) {
static _getShowWindow_doClose ({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}) {
$hov.remove();
$(position.window.document).off(mouseUpId);
$(position.window.document).off(mouseMoveId);
@@ -11646,9 +11713,9 @@ Renderer.hover = {
delete Renderer.hover._WINDOW_METAS[hoverId];
if (opts.cbClose) opts.cbClose(hoverWindow);
},
}
_getShowWindow_handleDragMousedown ({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type}) {
static _getShowWindow_handleDragMousedown ({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type}) {
if (evt.which === 0 || evt.which === 1) evt.preventDefault();
hoverWindow.zIndex = Renderer.hover._getNextZIndex(hoverId);
$hov.css({
@@ -11669,61 +11736,61 @@ Renderer.hover = {
});
$hov.css("max-width", "initial");
}
},
}
_getShowWindow_isOverHoverTarget ({evt, target}) {
static _getShowWindow_isOverHoverTarget ({evt, target}) {
return EventUtil.getClientX(evt) >= target.left
&& EventUtil.getClientX(evt) <= target.left + target.width
&& EventUtil.getClientY(evt) >= target.top
&& EventUtil.getClientY(evt) <= target.top + target.height;
},
}
_getShowWindow_handleNorthDrag ({$wrpContent, $hov, drag, evt}) {
static _getShowWindow_handleNorthDrag ({$wrpContent, $hov, drag, evt}) {
const diffY = Math.max(drag.startY - EventUtil.getClientY(evt), 80 - drag.baseHeight); // prevent <80 height, as this will cause the box to move downwards
$wrpContent.css("height", drag.baseHeight + diffY);
$hov.css("top", drag.baseTop - diffY);
drag.startY = EventUtil.getClientY(evt);
drag.baseHeight = $wrpContent.height();
drag.baseTop = parseFloat($hov.css("top"));
},
}
_getShowWindow_handleEastDrag ({$wrpContent, $hov, drag, evt}) {
static _getShowWindow_handleEastDrag ({$wrpContent, $hov, drag, evt}) {
const diffX = drag.startX - EventUtil.getClientX(evt);
$hov.css("width", drag.baseWidth - diffX);
drag.startX = EventUtil.getClientX(evt);
drag.baseWidth = parseFloat($hov.css("width"));
},
}
_getShowWindow_handleSouthDrag ({$wrpContent, $hov, drag, evt}) {
static _getShowWindow_handleSouthDrag ({$wrpContent, $hov, drag, evt}) {
const diffY = drag.startY - EventUtil.getClientY(evt);
$wrpContent.css("height", drag.baseHeight - diffY);
drag.startY = EventUtil.getClientY(evt);
drag.baseHeight = $wrpContent.height();
},
}
_getShowWindow_handleWestDrag ({$wrpContent, $hov, drag, evt}) {
static _getShowWindow_handleWestDrag ({$wrpContent, $hov, drag, evt}) {
const diffX = Math.max(drag.startX - EventUtil.getClientX(evt), 150 - drag.baseWidth);
$hov.css("width", drag.baseWidth + diffX)
.css("left", drag.baseLeft - diffX);
drag.startX = EventUtil.getClientX(evt);
drag.baseWidth = parseFloat($hov.css("width"));
drag.baseLeft = parseFloat($hov.css("left"));
},
}
_getShowWindow_doToggleMinimizedMaximized ({$brdrTop, $hov}) {
static _getShowWindow_doToggleMinimizedMaximized ({$brdrTop, $hov}) {
const curState = $brdrTop.attr("data-display-title");
const isNextMinified = curState === "false";
$brdrTop.attr("data-display-title", isNextMinified);
$brdrTop.attr("data-perm", true);
$hov.toggleClass("hwin--minified", isNextMinified);
},
}
_getShowWindow_doMaximize ({$brdrTop, $hov}) {
static _getShowWindow_doMaximize ({$brdrTop, $hov}) {
$brdrTop.attr("data-display-title", false);
$hov.toggleClass("hwin--minified", false);
},
}
async _getShowWindow_pDoPopout ({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content}, {evt} = {}) {
static async _getShowWindow_pDoPopout ({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content}, {evt} = {}) {
const dimensions = opts.fnGetPopoutSize ? opts.fnGetPopoutSize() : {width: 600, height: $content.height()};
const win = window.open(
"",
@@ -11802,9 +11869,9 @@ Renderer.hover = {
$cpyContent.appendTo(win.$wrpHoverContent.empty());
Renderer.hover._getShowWindow_doClose({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow});
},
}
_getShowWindow_setPosition ({$hov, $wrpContent, position}, positionNxt) {
static _getShowWindow_setPosition ({$hov, $wrpContent, position}, positionNxt) {
switch (positionNxt.mode) {
case "autoFromElement": {
const bcr = $hov[0].getBoundingClientRect();
@@ -11858,9 +11925,9 @@ Renderer.hover = {
}
Renderer.hover._getShowWindow_adjustPosition({$hov, $wrpContent, position});
},
}
_getShowWindow_adjustPosition ({$hov, $wrpContent, position}) {
static _getShowWindow_adjustPosition ({$hov, $wrpContent, position}) {
const eleHov = $hov[0];
const wrpContent = $wrpContent[0];
@@ -11904,22 +11971,29 @@ Renderer.hover = {
wrpContent.style.height = `${bcr.height}px`;
}
}
},
}
_getShowWindow_setIsPermanent ({opts, $brdrTop}, isPermanent) {
static _getShowWindow_getPosition ({$hov, $wrpContent}) {
return {
wWrpContent: $wrpContent.width(),
hWrapContent: $wrpContent.height(),
};
}
static _getShowWindow_setIsPermanent ({opts, $brdrTop}, isPermanent) {
opts.isPermanent = isPermanent;
$brdrTop.attr("data-perm", isPermanent);
},
}
_getShowWindow_setZIndex ({$hov, hoverWindow}, zIndex) {
static _getShowWindow_setZIndex ({$hov, hoverWindow}, zIndex) {
$hov.css("z-index", zIndex);
hoverWindow.zIndex = zIndex;
},
}
_getShowWindow_doZIndexToFront ({$hov, hoverWindow, hoverId}) {
static _getShowWindow_doZIndexToFront ({$hov, hoverWindow, hoverId}) {
const nxtZIndex = Renderer.hover._getNextZIndex(hoverId);
Renderer.hover._getShowWindow_setZIndex({$hov, hoverWindow}, nxtZIndex);
},
}
/**
* @param entry
@@ -11929,7 +12003,7 @@ Renderer.hover = {
* @param [opts.depth]
* @param [opts.id]
*/
getMakePredefinedHover (entry, opts) {
static getMakePredefinedHover (entry, opts) {
opts = opts || {};
const id = opts.id ?? Renderer.hover._getNextId();
@@ -11943,24 +12017,24 @@ Renderer.hover = {
touchStart: (evt, ele) => Renderer.hover.handleTouchStart(evt, ele),
show: () => Renderer.hover.doPredefinedShow(id, opts),
};
},
}
updatePredefinedHover (id, entry) {
static updatePredefinedHover (id, entry) {
Renderer.hover._entryCache[id] = entry;
},
}
getInlineHover (entry, opts) {
static getInlineHover (entry, opts) {
return {
// Re-use link handlers, as the inline version is a simplified version
html: `onmouseover="Renderer.hover.handleInlineMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" data-vet-entry="${JSON.stringify(entry).qq()}" ${opts ? `data-vet-opts="${JSON.stringify(opts).qq()}"` : ""} ${Renderer.hover.getPreventTouchString()}`,
};
},
}
getPreventTouchString () {
static getPreventTouchString () {
return `ontouchstart="Renderer.hover.handleTouchStart(event, this)"`;
},
}
handleTouchStart (evt, ele) {
static handleTouchStart (evt, ele) {
// on large touchscreen devices only (e.g. iPads)
if (!Renderer.hover.isSmallScreen(evt)) {
// cache the link location and redirect it to void
@@ -11976,10 +12050,10 @@ Renderer.hover = {
}
}, 100);
}
},
}
// region entry fetching
getEntityLink (
static getEntityLink (
ent,
{
displayText = null,
@@ -12026,9 +12100,9 @@ Renderer.hover = {
while (parts.length && !parts.last()?.length) parts.pop();
return Renderer.get().render(`{@${Parser.getPropTag(prop || ent.__prop)} ${parts.join("|")}}`);
},
}
getRefMetaFromTag (str) {
static getRefMetaFromTag (str) {
// convert e.g. `"{#itemEntry Ring of Resistance|DMG}"`
// to `{type: "refItemEntry", "itemEntry": "Ring of Resistance|DMG"}`
str = str.slice(2, -1);
@@ -12036,11 +12110,11 @@ Renderer.hover = {
const ref = refParts.join(" ");
const type = `ref${tag.uppercaseFirst()}`;
return {type, [tag]: ref};
},
}
// endregion
// region Apply custom hash IDs
async pApplyCustomHashId (page, ent, customHashId) {
static async pApplyCustomHashId (page, ent, customHashId) {
switch (page) {
case UrlUtil.PG_BESTIARY: {
const out = await Renderer.monster.pGetModifiedCreature(ent, customHashId);
@@ -12052,18 +12126,18 @@ Renderer.hover = {
default: return ent;
}
},
}
// endregion
getGenericCompactRenderedString (entry, depth = 0) {
static getGenericCompactRenderedString (entry, depth = 0) {
return `
<tr class="text homebrew-hover"><td colspan="6">
${Renderer.get().setFirstSection(true).render(entry, depth)}
</td></tr>
`;
},
}
getFnRenderCompact (page, {isStatic = false} = {}) {
static getFnRenderCompact (page, {isStatic = false} = {}) {
switch (page) {
case "generic":
case "hover": return Renderer.hover.getGenericCompactRenderedString;
@@ -12106,17 +12180,17 @@ Renderer.hover = {
if (Renderer[page]?.getCompactRenderedString) return Renderer[page].getCompactRenderedString;
return null;
}
},
}
getFnBindListenersCompact (page) {
static getFnBindListenersCompact (page) {
switch (page) {
case UrlUtil.PG_BESTIARY: return Renderer.monster.bindListenersCompact;
case UrlUtil.PG_RACES: return Renderer.race.bindListenersCompact;
default: return null;
}
},
}
_pageToFluffFn (page) {
static _pageToFluffFn (page) {
switch (page) {
case UrlUtil.PG_BESTIARY: return Renderer.monster.pGetFluff;
case UrlUtil.PG_ITEMS: return Renderer.item.pGetFluff;
@@ -12131,15 +12205,15 @@ Renderer.hover = {
case UrlUtil.PG_RECIPES: return Renderer.recipe.pGetFluff;
default: return null;
}
},
}
isSmallScreen (evt) {
static isSmallScreen (evt) {
if (typeof window === "undefined") return false;
evt = evt || {};
const win = (evt.view || {}).window || window;
return win.innerWidth <= 768;
},
}
/**
* @param page
@@ -12150,7 +12224,7 @@ Renderer.hover = {
* @param [opts.fnRender]
* @param [renderFnOpts]
*/
$getHoverContent_stats (page, toRender, opts, renderFnOpts) {
static $getHoverContent_stats (page, toRender, opts, renderFnOpts) {
opts = opts || {};
if (page === UrlUtil.PG_RECIPES) opts = {...MiscUtil.copyFast(opts), isBookContent: true};
@@ -12163,7 +12237,7 @@ Renderer.hover = {
}
return $out;
},
}
/**
* @param page
@@ -12172,7 +12246,7 @@ Renderer.hover = {
* @param [opts.isBookContent]
* @param [renderFnOpts]
*/
$getHoverContent_fluff (page, toRender, opts, renderFnOpts) {
static $getHoverContent_fluff (page, toRender, opts, renderFnOpts) {
opts = opts || {};
if (page === UrlUtil.PG_RECIPES) opts = {...MiscUtil.copyFast(opts), isBookContent: true};
@@ -12202,24 +12276,24 @@ Renderer.hover = {
}
return $$`<table class="w-100 stats ${opts.isBookContent ? `stats--book` : ""}">${Renderer.generic.getCompactRenderedString(toRender, renderFnOpts)}</table>`;
},
}
$getHoverContent_statsCode (toRender, {isSkipClean = false, title = null} = {}) {
static $getHoverContent_statsCode (toRender, {isSkipClean = false, title = null} = {}) {
const cleanCopy = isSkipClean ? toRender : DataUtil.cleanJson(MiscUtil.copyFast(toRender));
return Renderer.hover.$getHoverContent_miscCode(
title || [cleanCopy.name, "Source Data"].filter(Boolean).join(" \u2014 "),
JSON.stringify(cleanCopy, null, "\t"),
);
},
}
$getHoverContent_miscCode (name, code) {
static $getHoverContent_miscCode (name, code) {
const toRenderCode = {
type: "code",
name,
preformatted: code,
};
return $$`<table class="w-100 stats stats--book">${Renderer.get().render(toRenderCode)}</table>`;
},
}
/**
* @param toRender
@@ -12228,17 +12302,17 @@ Renderer.hover = {
* @param [opts.isLargeBookContent]
* @param [opts.depth]
*/
$getHoverContent_generic (toRender, opts) {
static $getHoverContent_generic (toRender, opts) {
opts = opts || {};
return $$`<table class="w-100 stats ${opts.isBookContent || opts.isLargeBookContent ? "stats--book" : ""} ${opts.isLargeBookContent ? "stats--book-large" : ""}">${Renderer.hover.getGenericCompactRenderedString(toRender, opts.depth || 0)}</table>`;
},
}
/**
* @param evt
* @param entity
*/
doPopoutCurPage (evt, entity) {
static doPopoutCurPage (evt, entity) {
const page = UrlUtil.getCurrentPage();
const $content = Renderer.hover.$getHoverContent_stats(page, entity);
Renderer.hover.getShowWindow(
@@ -12252,7 +12326,7 @@ Renderer.hover = {
sourceData: entity,
},
);
},
}
};
/**

View File

@@ -243,18 +243,49 @@ class SearchPage {
// region Render tokens, where available
let isImagePopulated = false;
switch (category) {
case Parser.CAT_ID_CREATURE:
case Parser.CAT_ID_VEHICLE:
case Parser.CAT_ID_OBJECT: {
const hasToken = ent.tokenUrl || ent.hasToken;
if (hasToken) {
const fnGetTokenUrl = category === Parser.CAT_ID_CREATURE ? Renderer.monster.getTokenUrl : category === Parser.CAT_ID_VEHICLE ? Renderer.vehicle.getTokenUrl : Renderer.object.getTokenUrl;
const displayTokenImage = (
{
fnHasToken,
fnGetTokenUrl,
},
ent,
) => {
if (!fnHasToken(ent)) return;
isImagePopulated = true;
const tokenUrl = fnGetTokenUrl(ent);
$dispImage.html(`<img src="${tokenUrl}" class="w-100 h-100" alt="Token Image: ${(ent.name || "").qq()}" ${ent.tokenCredit ? `title="Credit: ${ent.tokenCredit.qq()}"` : ""} loading="lazy">`);
}
isImagePopulated = true;
const tokenUrl = fnGetTokenUrl(ent);
$dispImage.html(`<img src="${tokenUrl}" class="w-100 h-100" alt="Token Image: ${(ent.name || "").qq()}" ${ent.tokenCredit ? `title="Credit: ${ent.tokenCredit.qq()}"` : ""} loading="lazy">`);
};
switch (category) {
case Parser.CAT_ID_CREATURE: {
displayTokenImage(
{
fnHasToken: Renderer.monster.hasToken,
fnGetTokenUrl: Renderer.monster.getTokenUrl,
},
ent,
);
break;
}
case Parser.CAT_ID_VEHICLE: {
displayTokenImage(
{
fnHasToken: Renderer.vehicle.hasToken,
fnGetTokenUrl: Renderer.vehicle.getTokenUrl,
},
ent,
);
break;
}
case Parser.CAT_ID_OBJECT: {
displayTokenImage(
{
fnHasToken: Renderer.object.hasToken,
fnGetTokenUrl: Renderer.object.getTokenUrl,
},
ent,
);
break;
}

View File

@@ -270,6 +270,7 @@ class _BrewUtil2Base {
IS_EDITABLE;
PAGE_MANAGE;
URL_REPO_DEFAULT;
URL_REPO_ROOT_DEFAULT;
DISPLAY_NAME;
DISPLAY_NAME_PLURAL;
DEFAULT_AUTHOR;
@@ -351,9 +352,11 @@ class _BrewUtil2Base {
async pGetCustomUrl () { return this._storage.pGet(this._STORAGE_KEY_CUSTOM_URL); }
async pSetCustomUrl (val) {
return !val
await (!val
? this._storage.pRemove(this._STORAGE_KEY_CUSTOM_URL)
: this._storage.pSet(this._STORAGE_KEY_CUSTOM_URL, val);
: this._storage.pSet(this._STORAGE_KEY_CUSTOM_URL, val));
location.reload();
}
/* -------------------------------------------- */
@@ -1352,6 +1355,7 @@ class _PrereleaseUtil extends _BrewUtil2Base {
IS_EDITABLE = false;
PAGE_MANAGE = UrlUtil.PG_MANAGE_PRERELEASE;
URL_REPO_DEFAULT = VeCt.URL_PRERELEASE;
URL_REPO_ROOT_DEFAULT = VeCt.URL_ROOT_PRERELEASE;
DISPLAY_NAME = "prerelease content";
DISPLAY_NAME_PLURAL = "prereleases";
DEFAULT_AUTHOR = "Wizards of the Coast";
@@ -1368,11 +1372,11 @@ class _PrereleaseUtil extends _BrewUtil2Base {
getFileUrl (path, urlRoot) { return DataUtil.prerelease.getFileUrl(path, urlRoot); }
pLoadTimestamps (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadTimestamps(urlRoot); }
pLoadTimestamps (urlRoot) { return DataUtil.prerelease.pLoadTimestamps(urlRoot); }
pLoadPropIndex (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadPropIndex(urlRoot); }
pLoadPropIndex (urlRoot) { return DataUtil.prerelease.pLoadPropIndex(urlRoot); }
pLoadMetaIndex (brewIndex, src, urlRoot) { return DataUtil.prerelease.pLoadMetaIndex(urlRoot); }
pLoadMetaIndex (urlRoot) { return DataUtil.prerelease.pLoadMetaIndex(urlRoot); }
/* -------------------------------------------- */
@@ -1413,6 +1417,7 @@ class _BrewUtil2 extends _BrewUtil2Base {
IS_EDITABLE = true;
PAGE_MANAGE = UrlUtil.PG_MANAGE_BREW;
URL_REPO_DEFAULT = VeCt.URL_BREW;
URL_REPO_ROOT_DEFAULT = VeCt.URL_ROOT_BREW;
DISPLAY_NAME = "homebrew";
DISPLAY_NAME_PLURAL = "homebrews";
DEFAULT_AUTHOR = "";
@@ -1475,11 +1480,11 @@ class _BrewUtil2 extends _BrewUtil2Base {
getFileUrl (path, urlRoot) { return DataUtil.brew.getFileUrl(path, urlRoot); }
pLoadTimestamps (brewIndex, src, urlRoot) { return DataUtil.brew.pLoadTimestamps(urlRoot); }
pLoadTimestamps (urlRoot) { return DataUtil.brew.pLoadTimestamps(urlRoot); }
pLoadPropIndex (brewIndex, src, urlRoot) { return DataUtil.brew.pLoadPropIndex(urlRoot); }
pLoadPropIndex (urlRoot) { return DataUtil.brew.pLoadPropIndex(urlRoot); }
pLoadMetaIndex (brewIndex, src, urlRoot) { return DataUtil.brew.pLoadMetaIndex(urlRoot); }
pLoadMetaIndex (urlRoot) { return DataUtil.brew.pLoadMetaIndex(urlRoot); }
/* -------------------------------------------- */
@@ -1862,7 +1867,7 @@ class ManageBrewUi {
title: `${this._brewUtil.DISPLAY_NAME.toTitleCase()} Repository URL`,
$elePre: $(`<div>
<p>Leave blank to use the <a href="${this._brewUtil.URL_REPO_DEFAULT}" rel="noopener noreferrer" target="_blank">default ${this._brewUtil.DISPLAY_NAME} repo</a>.</p>
<div>Note that for GitHub URLs, the <code>raw.</code> URL must be used. For example, <code>${this._brewUtil.URL_REPO_DEFAULT.replace(/TheGiddyLimit/g, "YourUsernameHere")}</code></div>
<div>Note that for GitHub URLs, the <code>raw.</code> URL must be used. For example, <code>${this._brewUtil.URL_REPO_ROOT_DEFAULT.replace(/TheGiddyLimit/g, "YourUsernameHere")}</code></div>
<hr class="hr-3">
</div>`),
default: customBrewUtl,

View File

@@ -1998,7 +1998,7 @@ class DataLoader {
static _isPossibleSource ({parent, sourceClean}) { return parent._isPrereleaseSource({sourceClean}) && !Parser.SOURCE_JSON_TO_FULL[Parser.sourceJsonToJson(sourceClean)]; }
static _getBrewUtil () { return typeof PrereleaseUtil !== "undefined" ? PrereleaseUtil : null; }
static _pGetSourceIndex () { return DataUtil.prerelease.pLoadSourceIndex(); }
static _pGetSourceIndex () { return DataUtil.prerelease.pLoadSourceIndex(PrereleaseUtil.pGetCustomUrl()); }
};
static _BrewPreloader = class extends this._PrereleaseBrewPreloader {
@@ -2009,7 +2009,7 @@ class DataLoader {
static _isPossibleSource ({parent, sourceClean}) { return !parent._isSiteSource({sourceClean}) && !parent._isPrereleaseSource({sourceClean}); }
static _getBrewUtil () { return typeof BrewUtil2 !== "undefined" ? BrewUtil2 : null; }
static _pGetSourceIndex () { return DataUtil.brew.pLoadSourceIndex(); }
static _pGetSourceIndex () { return DataUtil.brew.pLoadSourceIndex(BrewUtil2.pGetCustomUrl()); }
};
static async _pCacheAndGet_getCacheMeta ({pageClean, sourceClean, dataLoader}) {

View File

@@ -220,6 +220,8 @@ PropOrder._MONSTER = [
"dragonAge",
"tokenUrl",
"token",
"tokenHref",
"tokenCredit",
"soundClip",
"foundryImg",
@@ -515,6 +517,7 @@ PropOrder._ADVENTURE = [
"group",
"cover",
"coverUrl",
"published",
"publishedOrder",
@@ -544,6 +547,7 @@ PropOrder._BOOK = [
"group",
"cover",
"coverUrl",
"published",
"author",
@@ -1197,6 +1201,8 @@ PropOrder._VEHICLE = [
"reaction",
"tokenUrl",
"token",
"tokenHref",
"tokenCredit",
"hasToken",
@@ -1484,6 +1490,8 @@ PropOrder._OBJECT = [
"actionEntries",
"tokenUrl",
"token",
"tokenHref",
"tokenCredit",
"hasToken",
"hasFluff",

View File

@@ -2384,6 +2384,191 @@ class InputUiUtil {
});
}
/* -------------------------------------------- */
static GenericButtonInfo = class {
constructor (
{
text,
clazzIcon,
isPrimary,
isSmall,
isRemember,
value,
},
) {
this._text = text;
this._clazzIcon = clazzIcon;
this._isPrimary = isPrimary;
this._isSmall = isSmall;
this._isRemember = isRemember;
this._value = value;
}
get isPrimary () { return !!this._isPrimary; }
$getBtn ({doClose, fnRemember, isGlobal, storageKey}) {
if (this._isRemember && !storageKey) throw new Error(`No "storageKey" provided for button with saveable value!`);
return $(`<button class="btn ${this._isPrimary ? "btn-primary" : "btn-default"} ${this._isSmall ? "btn-sm" : ""} ve-flex-v-center mr-3">
<span class="${this._clazzIcon} mr-2"></span><span>${this._text}</span>
</button>`)
.on("click", evt => {
evt.stopPropagation();
doClose(true, this._value);
if (!this._isRemember) return;
if (fnRemember) {
fnRemember(this._value);
} else {
isGlobal
? StorageUtil.pSet(storageKey, true)
: StorageUtil.pSetForPage(storageKey, true);
}
});
}
};
static async pGetUserGenericButton (
{
title,
buttons,
textSkip,
htmlDescription,
$eleDescription,
storageKey,
isGlobal,
fnRemember,
isSkippable,
isIgnoreRemembered,
},
) {
if (storageKey && !isIgnoreRemembered) {
const prev = await (isGlobal ? StorageUtil.pGet(storageKey) : StorageUtil.pGetForPage(storageKey));
if (prev != null) return prev;
}
const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({
title: title || "Choose",
isMinHeight0: true,
});
const $btns = buttons.map(btnInfo => btnInfo.$getBtn({doClose, fnRemember, isGlobal, storageKey}));
const $btnSkip = !isSkippable ? null : $(`<button class="btn btn-default btn-sm ml-3"><span class="glyphicon glyphicon-forward"></span><span>${textSkip || "Skip"}</span></button>`)
.click(evt => {
evt.stopPropagation();
doClose(VeCt.SYM_UI_SKIP);
});
if ($eleDescription?.length) $$`<div class="ve-flex w-100 mb-1">${$eleDescription}</div>`.appendTo($modalInner);
else if (htmlDescription && htmlDescription.trim()) $$`<div class="ve-flex w-100 mb-1">${htmlDescription}</div>`.appendTo($modalInner);
$$`<div class="ve-flex-v-center ve-flex-h-right py-1 px-1">${$btns}${$btnSkip}</div>`.appendTo($modalInner);
if (doAutoResizeModal) doAutoResizeModal();
const ixPrimary = buttons.findIndex(btn => btn.isPrimary);
if (~ixPrimary) {
$btns[ixPrimary].focus();
$btns[ixPrimary].select();
}
// region Output
const [isDataEntered, out] = await pGetResolved();
if (typeof isDataEntered === "symbol") return isDataEntered;
if (!isDataEntered) return null;
if (out == null) throw new Error(`Callback must receive a value!`); // sense check
return out;
// endregion
}
/**
* @param [title] Prompt title.
* @param [textYesRemember] Text for "yes, and remember" button.
* @param [textYes] Text for "yes" button.
* @param [textNo] Text for "no" button.
* @param [textSkip] Text for "skip" button.
* @param [htmlDescription] Description HTML for the modal.
* @param [$eleDescription] Description element for the modal.
* @param [storageKey] Storage key to use when "remember" options are passed.
* @param [isGlobal] If the stored setting is global when "remember" options are passed.
* @param [fnRemember] Custom function to run when saving the "yes and remember" option.
* @param [isSkippable] If the prompt is skippable.
* @param [isAlert] If this prompt is just a notification/alert.
* @param [isIgnoreRemembered] If the remembered value should be ignored, in favour of re-prompting the user.
* @return {Promise} A promise which resolves to true/false if the user chose, or null otherwise.
*/
static async pGetUserBoolean (
{
title,
textYesRemember,
textYes,
textNo,
textSkip,
htmlDescription,
$eleDescription,
storageKey,
isGlobal,
fnRemember,
isSkippable,
isAlert,
isIgnoreRemembered,
},
) {
const buttons = [];
if (textYesRemember) {
buttons.push(
new this.GenericButtonInfo({
text: textYesRemember,
clazzIcon: "glyphicon glyphicon-ok",
isRemember: true,
isPrimary: true,
value: true,
}),
);
}
buttons.push(
new this.GenericButtonInfo({
text: textYes || "OK",
clazzIcon: "glyphicon glyphicon-ok",
isPrimary: true,
value: true,
}),
);
// TODO(Future) migrate usages to `pGetUserGenericButton` (or helper method)
if (!isAlert) {
buttons.push(
new this.GenericButtonInfo({
text: textNo || "Cancel",
clazzIcon: "glyphicon glyphicon-remove",
isSmall: true,
value: false,
}),
);
}
return this.pGetUserGenericButton({
title,
buttons,
textSkip,
htmlDescription,
$eleDescription,
storageKey,
isGlobal,
fnRemember,
isSkippable,
isIgnoreRemembered,
});
}
/* -------------------------------------------- */
/**
* @param opts Options.
* @param opts.min Minimum value.
@@ -2462,85 +2647,6 @@ class InputUiUtil {
// endregion
}
/**
* @param [opts] Options.
* @param [opts.title] Prompt title.
* @param [opts.textYesRemember] Text for "yes, and remember" button.
* @param [opts.textYes] Text for "yes" button.
* @param [opts.textNo] Text for "no" button.
* @param [opts.textSkip] Text for "skip" button.
* @param [opts.htmlDescription] Description HTML for the modal.
* @param [opts.$eleDescription] Description element for the modal.
* @param [opts.storageKey] Storage key to use when "remember" options are passed.
* @param [opts.isGlobal] If the stored setting is global when "remember" options are passed.
* @param [opts.fnRemember] Custom function to run when saving the "yes and remember" option.
* @param [opts.isSkippable] If the prompt is skippable.
* @param [opts.isAlert] If this prompt is just a notification/alert.
* @return {Promise} A promise which resolves to true/false if the user chose, or null otherwise.
*/
static async pGetUserBoolean (opts) {
opts = opts || {};
if (opts.storageKey) {
const prev = await (opts.isGlobal ? StorageUtil.pGet(opts.storageKey) : StorageUtil.pGetForPage(opts.storageKey));
if (prev != null) return prev;
}
const $btnTrueRemember = opts.textYesRemember ? $(`<button class="btn btn-primary ve-flex-v-center mr-2"><span class="glyphicon glyphicon-ok mr-2"></span><span>${opts.textYesRemember}</span></button>`)
.click(() => {
doClose(true, true);
if (opts.fnRemember) {
opts.fnRemember(true);
} else {
opts.isGlobal
? StorageUtil.pSet(opts.storageKey, true)
: StorageUtil.pSetForPage(opts.storageKey, true);
}
}) : null;
const $btnTrue = $(`<button class="btn btn-primary ve-flex-v-center mr-3"><span class="glyphicon glyphicon-ok mr-2"></span><span>${opts.textYes || "OK"}</span></button>`)
.click(evt => {
evt.stopPropagation();
doClose(true, true);
});
const $btnFalse = opts.isAlert ? null : $(`<button class="btn btn-default btn-sm ve-flex-v-center"><span class="glyphicon glyphicon-remove mr-2"></span><span>${opts.textNo || "Cancel"}</span></button>`)
.click(evt => {
evt.stopPropagation();
doClose(true, false);
});
const $btnSkip = !opts.isSkippable ? null : $(`<button class="btn btn-default btn-sm ml-3"><span class="glyphicon glyphicon-forward"></span><span>${opts.textSkip || "Skip"}</span></button>`)
.click(evt => {
evt.stopPropagation();
doClose(VeCt.SYM_UI_SKIP);
});
const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({
title: opts.title || "Choose",
isMinHeight0: true,
});
if (opts.$eleDescription?.length) $$`<div class="ve-flex w-100 mb-1">${opts.$eleDescription}</div>`.appendTo($modalInner);
else if (opts.htmlDescription && opts.htmlDescription.trim()) $$`<div class="ve-flex w-100 mb-1">${opts.htmlDescription}</div>`.appendTo($modalInner);
$$`<div class="ve-flex-v-center ve-flex-h-right py-1 px-1">${$btnTrueRemember}${$btnTrue}${$btnFalse}${$btnSkip}</div>`.appendTo($modalInner);
if (doAutoResizeModal) doAutoResizeModal();
$btnTrue.focus();
$btnTrue.select();
// region Output
const [isDataEntered, out] = await pGetResolved();
if (typeof isDataEntered === "symbol") return isDataEntered;
if (!isDataEntered) return null;
if (out == null) throw new Error(`Callback must receive a value!`); // sanity check
return out;
// endregion
}
/**
* @param opts Options.
* @param opts.values Array of values.

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.4"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.VERSION_NUMBER = /* 5ETOOLS_VERSION__OPEN */"1.199.0"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.DEPLOYED_IMG_ROOT = undefined;
// for the roll20 script to set
globalThis.IS_VTT = false;
@@ -300,8 +300,8 @@ globalThis.StrUtil = {
// Certain minor words should be left lowercase unless they are the first or last words in the string
TITLE_LOWER_WORDS: ["a", "an", "the", "and", "but", "or", "for", "nor", "as", "at", "by", "for", "from", "in", "into", "near", "of", "on", "onto", "to", "with", "over", "von"],
// Certain words such as initialisms or acronyms should be left uppercase
TITLE_UPPER_WORDS: ["Id", "Tv", "Dm", "Ok", "Npc", "Pc", "Tpk", "Wip", "Dc"],
TITLE_UPPER_WORDS_PLURAL: ["Ids", "Tvs", "Dms", "Oks", "Npcs", "Pcs", "Tpks", "Wips", "Dcs"], // (Manually pluralize, to avoid infinite loop)
TITLE_UPPER_WORDS: ["Id", "Tv", "Dm", "Ok", "Npc", "Pc", "Tpk", "Wip", "Dc", "D&d"],
TITLE_UPPER_WORDS_PLURAL: ["Ids", "Tvs", "Dms", "Oks", "Npcs", "Pcs", "Tpks", "Wips", "Dcs", "D&d"], // (Manually pluralize, to avoid infinite loop)
IRREGULAR_PLURAL_WORDS: {
"cactus": "cacti",
@@ -542,6 +542,16 @@ globalThis.SourceUtil = class {
return SourceUtil.FILTER_GROUP_STANDARD;
}
static getFilterGroupName (group) {
switch (group) {
case SourceUtil.FILTER_GROUP_NON_STANDARD: return "Other/Prerelease";
case SourceUtil.FILTER_GROUP_HOMEBREW: return "Homebrew";
case SourceUtil.FILTER_GROUP_PARTNERED: return "Partnered";
case SourceUtil.FILTER_GROUP_STANDARD: return null;
default: throw new Error(`Unhandled source filter group "${group}"`);
}
}
static getAdventureBookSourceHref (source, page) {
if (!source) return null;
source = source.toLowerCase();
@@ -2052,13 +2062,13 @@ globalThis.MiscUtil = {
};
// EVENT HANDLERS ======================================================================================================
globalThis.EventUtil = {
_mouseX: 0,
_mouseY: 0,
_isUsingTouch: false,
_isSetCssVars: false,
globalThis.EventUtil = class {
static _mouseX = 0;
static _mouseY = 0;
static _isUsingTouch = false;
static _isSetCssVars = false;
init () {
static init () {
document.addEventListener("mousemove", evt => {
EventUtil._mouseX = evt.clientX;
EventUtil._mouseY = evt.clientY;
@@ -2067,46 +2077,50 @@ globalThis.EventUtil = {
document.addEventListener("touchstart", () => {
EventUtil._isUsingTouch = true;
});
},
}
_eleDocRoot: null,
_onMouseMove_setCssVars () {
static _eleDocRoot = null;
static _onMouseMove_setCssVars () {
if (!EventUtil._isSetCssVars) return;
EventUtil._eleDocRoot = EventUtil._eleDocRoot || document.querySelector(":root");
EventUtil._eleDocRoot.style.setProperty("--mouse-position-x", EventUtil._mouseX);
EventUtil._eleDocRoot.style.setProperty("--mouse-position-y", EventUtil._mouseY);
},
}
getClientX (evt) { return evt.touches && evt.touches.length ? evt.touches[0].clientX : evt.clientX; },
getClientY (evt) { return evt.touches && evt.touches.length ? evt.touches[0].clientY : evt.clientY; },
/* -------------------------------------------- */
getOffsetY (evt) {
static getClientX (evt) { return evt.touches && evt.touches.length ? evt.touches[0].clientX : evt.clientX; }
static getClientY (evt) { return evt.touches && evt.touches.length ? evt.touches[0].clientY : evt.clientY; }
static getOffsetY (evt) {
if (!evt.touches?.length) return evt.offsetY;
const bounds = evt.target.getBoundingClientRect();
return evt.targetTouches[0].clientY - bounds.y;
},
}
getMousePos () {
static getMousePos () {
return {x: EventUtil._mouseX, y: EventUtil._mouseY};
},
}
isUsingTouch () { return !!EventUtil._isUsingTouch; },
/* -------------------------------------------- */
isInInput (evt) {
static isUsingTouch () { return !!EventUtil._isUsingTouch; }
static isInInput (evt) {
return evt.target.nodeName === "INPUT" || evt.target.nodeName === "TEXTAREA"
|| evt.target.getAttribute("contenteditable") === "true";
},
}
isCtrlMetaKey (evt) {
static isCtrlMetaKey (evt) {
return evt.ctrlKey || evt.metaKey;
},
}
noModifierKeys (evt) { return !evt.ctrlKey && !evt.altKey && !evt.metaKey; },
static noModifierKeys (evt) { return !evt.ctrlKey && !evt.altKey && !evt.metaKey; }
getKeyIgnoreCapsLock (evt) {
static getKeyIgnoreCapsLock (evt) {
if (!evt.key) return null;
if (evt.key.length !== 1) return evt.key;
const isCaps = (evt.originalEvent || evt).getModifierState("CapsLock");
@@ -2116,7 +2130,29 @@ globalThis.EventUtil = {
const isLowerCase = asciiCode >= 97 && asciiCode <= 122;
if (!isUpperCase && !isLowerCase) return evt.key;
return isUpperCase ? evt.key.toLowerCase() : evt.key.toUpperCase();
},
}
/* -------------------------------------------- */
// In order of preference/priority.
// Note: `"application/json"`, as e.g. Founrdy's TinyMCE blocks drops which are not plain text.
static _MIME_TYPES_DROP_JSON = ["application/json", "text/plain"];
static getDropJson (evt) {
let data;
for (const mimeType of EventUtil._MIME_TYPES_DROP_JSON) {
if (!evt.dataTransfer.types.includes(mimeType)) continue;
try {
const rawJson = evt.dataTransfer.getData(mimeType);
if (!rawJson) return;
data = JSON.parse(rawJson);
} catch (e) {
// Do nothing
}
}
return data;
}
};
if (typeof window !== "undefined") window.addEventListener("load", EventUtil.init);
@@ -4639,59 +4675,65 @@ globalThis.DataUtil = {
static _getCleanMathExpression (str) { return str.replace(/[^-+/*0-9.,]+/g, ""); }
static _WALKER = null;
static resolve ({obj, ent, msgPtFailed = null}) {
return JSON.parse(
JSON.stringify(obj)
.replace(/<\$(?<variable>[^$]+)\$>/g, (...m) => {
const [mode, detail] = m.last().variable.split("__");
DataUtil.generic.variableResolver._WALKER ||= MiscUtil.getWalker();
switch (mode) {
case "name": return ent.name;
case "short_name":
case "title_short_name": {
return Renderer.monster.getShortName(ent, {isTitleCase: mode === "title_short_name"});
return DataUtil.generic.variableResolver._WALKER
.walk(
obj,
{
string: str => str.replace(/<\$(?<variable>[^$]+)\$>/g, (...m) => {
const [mode, detail] = m.last().variable.split("__");
switch (mode) {
case "name": return ent.name;
case "short_name":
case "title_short_name": {
return Renderer.monster.getShortName(ent, {isTitleCase: mode === "title_short_name"});
}
case "dc":
case "spell_dc": {
if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`);
return 8 + Parser.getAbilityModNumber(Number(ent[detail])) + Parser.crToPb(ent.cr);
}
case "to_hit": {
if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`);
const total = Parser.crToPb(ent.cr) + Parser.getAbilityModNumber(Number(ent[detail]));
return total >= 0 ? `+${total}` : total;
}
case "damage_mod": {
if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`);
const total = Parser.getAbilityModNumber(Number(ent[detail]));
return total === 0 ? "" : total > 0 ? ` + ${total}` : ` - ${Math.abs(total)}`;
}
case "damage_avg": {
const replaced = detail
.replace(/\b(?<abil>str|dex|con|int|wis|cha)\b/gi, (...m) => Parser.getAbilityModNumber(Number(ent[m.last().abil])))
.replace(/\bsize_mult\b/g, () => this._getSizeMult(this._getSize({ent})));
// eslint-disable-next-line no-eval
return Math.floor(eval(this._getCleanMathExpression(replaced)));
}
case "size_mult": {
const mult = this._getSizeMult(this._getSize({ent}));
if (!detail) return mult;
// eslint-disable-next-line no-eval
return Math.floor(eval(`${mult} * ${this._getCleanMathExpression(detail)}`));
}
default: return m[0];
}
case "dc":
case "spell_dc": {
if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`);
return 8 + Parser.getAbilityModNumber(Number(ent[detail])) + Parser.crToPb(ent.cr);
}
case "to_hit": {
if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`);
const total = Parser.crToPb(ent.cr) + Parser.getAbilityModNumber(Number(ent[detail]));
return total >= 0 ? `+${total}` : total;
}
case "damage_mod": {
if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`);
const total = Parser.getAbilityModNumber(Number(ent[detail]));
return total === 0 ? "" : total > 0 ? ` + ${total}` : ` - ${Math.abs(total)}`;
}
case "damage_avg": {
const replaced = detail
.replace(/\b(?<abil>str|dex|con|int|wis|cha)\b/gi, (...m) => Parser.getAbilityModNumber(Number(ent[m.last().abil])))
.replace(/\bsize_mult\b/g, () => this._getSizeMult(this._getSize({ent})));
// eslint-disable-next-line no-eval
return Math.floor(eval(this._getCleanMathExpression(replaced)));
}
case "size_mult": {
const mult = this._getSizeMult(this._getSize({ent}));
if (!detail) return mult;
// eslint-disable-next-line no-eval
return Math.floor(eval(`${mult} * ${this._getCleanMathExpression(detail)}`));
}
default: return m[0];
}
}),
);
}),
},
);
}
},
@@ -5929,7 +5971,11 @@ globalThis.RollerUtil = {
getColRollType (colLabel) {
if (typeof colLabel !== "string") return false;
colLabel = Renderer.stripTags(colLabel);
colLabel = colLabel.trim();
const mDice = /^{@dice (?<exp>[^}|]+)([^}]+)?}$/.exec(colLabel);
colLabel = mDice ? mDice.groups.exp : Renderer.stripTags(colLabel);
if (Renderer.dice.lang.getTree3(colLabel)) return RollerUtil.ROLL_COL_STANDARD;

View File

@@ -118,8 +118,7 @@ class VehiclesPage extends ListPage {
(this._$dispToken = this._$dispToken || $(`#float-token`)).empty();
if (ent.vehicleType) {
const hasToken = ent.tokenUrl || ent.hasToken;
if (hasToken) {
if (Renderer.vehicle.hasToken(ent)) {
const imgLink = Renderer.vehicle.getTokenUrl(ent);
this._$dispToken.append(`<a href="${imgLink}" target="_blank" rel="noopener noreferrer"><img src="${imgLink}" id="token_image" class="token" alt="Token Image: ${(ent.name || "").qq()}" ${ent.tokenCredit ? `title="Credit: ${ent.tokenCredit.qq()}"` : ""} loading="lazy"></a>`);
}