This commit is contained in:
TheGiddyLimit
2024-02-01 15:54:34 +00:00
parent a4e391a3e7
commit 7c341dc1a7
54 changed files with 5931 additions and 121 deletions

View File

@@ -52,6 +52,8 @@ class BaseParserFeature extends BaseParser {
parts.forEach(pt => {
pt = pt.trim();
if (/^the ability to cast at least one spell$/i.test(pt)) return pre.spellcasting = true;
if (/^spellcasting$/i.test(pt)) return pre.spellcasting2020 = true;
if (/^pact magic feature$/i.test(pt)) return pre.spellcasting2020 = true;

View File

@@ -126,7 +126,7 @@ class SpellParser extends BaseParser {
const statsOut = this._getFinalState(spell, options);
const missingProps = this._REQUIRED_PROPS.filter(prop => !statsOut[prop]);
const missingProps = this._REQUIRED_PROPS.filter(prop => statsOut[prop] == null);
if (missingProps.length) options.cbWarning(`${statsOut.name ? `(${statsOut.name}) ` : ""}Missing properties: ${missingProps.join(", ")}`);
options.cbOutput(statsOut, options.isAppend);
@@ -303,7 +303,7 @@ class SpellParser extends BaseParser {
}
static _setCleanRange (stats, line, options) {
const getUnit = (str) => str.toLowerCase().includes("mile") ? "miles" : "feet";
const getUnit = (str) => /\b(miles|mi\.)\b/.test(str.toLowerCase()) ? "miles" : "feet";
const range = ConvertUtil.cleanDashes(ConvertUtil.getStatblockLineHeaderText({reStartStr: this._RE_START_RANGE, line}));
@@ -316,29 +316,29 @@ class SpellParser extends BaseParser {
const cleanRange = range.replace(/(\d),(\d)/g, "$1$2");
const mFeetMiles = /^(\d+) (feet|foot|miles?)$/i.exec(cleanRange);
if (mFeetMiles) return stats.range = {type: "point", distance: {type: getUnit(mFeetMiles[2]), amount: Number(mFeetMiles[1])}};
const mFeetMiles = /^(?<amount>\d+) (?<unit>feet|foot|ft\.?|miles?|mi\.?)$/i.exec(cleanRange);
if (mFeetMiles) return stats.range = {type: "point", distance: {type: getUnit(mFeetMiles.groups.unit), amount: Number(mFeetMiles.groups.amount)}};
const mSelfRadius = /^self \((\d+)-(foot|mile) radius\)$/i.exec(cleanRange);
const mSelfRadius = /^self \((\d+)-(foot|ft\.?|mile|mi\.?) radius\)$/i.exec(cleanRange);
if (mSelfRadius) return stats.range = {type: "radius", distance: {type: getUnit(mSelfRadius[2]), amount: Number(mSelfRadius[1])}};
const mSelfSphere = /^self \((\d+)-(foot|mile)(?:-radius)? sphere\)$/i.exec(cleanRange);
const mSelfSphere = /^self \((\d+)-(foot|ft\.?|mile|mi\.?)(?:-radius)? sphere\)$/i.exec(cleanRange);
if (mSelfSphere) return stats.range = {type: "sphere", distance: {type: getUnit(mSelfSphere[2]), amount: Number(mSelfSphere[1])}};
const mSelfCone = /^self \((\d+)-(foot|mile) cone\)$/i.exec(cleanRange);
const mSelfCone = /^self \((\d+)-(foot|ft\.?|mile|mi\.?) cone\)$/i.exec(cleanRange);
if (mSelfCone) return stats.range = {type: "cone", distance: {type: getUnit(mSelfCone[2]), amount: Number(mSelfCone[1])}};
const mSelfLine = /^self \((\d+)-(foot|mile) line\)$/i.exec(cleanRange);
const mSelfLine = /^self \((\d+)-(foot|ft\.?|mile|mi\.?) line\)$/i.exec(cleanRange);
if (mSelfLine) return stats.range = {type: "line", distance: {type: getUnit(mSelfLine[2]), amount: Number(mSelfLine[1])}};
const mSelfCube = /^self \((\d+)-(foot|mile) cube\)$/i.exec(cleanRange);
const mSelfCube = /^self \((\d+)-(foot|ft\.?|mile|mi\.?) cube\)$/i.exec(cleanRange);
if (mSelfCube) return stats.range = {type: "cube", distance: {type: getUnit(mSelfCube[2]), amount: Number(mSelfCube[1])}};
const mSelfHemisphere = /^self \((\d+)-(foot|mile)(?:-radius)? hemisphere\)$/i.exec(cleanRange);
const mSelfHemisphere = /^self \((\d+)-(foot|ft\.?|mile|mi\.?)(?:-radius)? hemisphere\)$/i.exec(cleanRange);
if (mSelfHemisphere) return stats.range = {type: "hemisphere", distance: {type: getUnit(mSelfHemisphere[2]), amount: Number(mSelfHemisphere[1])}};
// region Homebrew
const mPointCube = /^(?<point>\d+) (?<unit>feet|foot|miles?) \((\d+)-(foot|mile) cube\)$/i.exec(cleanRange);
const mPointCube = /^(?<point>\d+) (?<unit>feet|foot|ft\.?|miles?|mi\.?) \((\d+)-(foot|ft\.?|mile|mi\.?) cube\)$/i.exec(cleanRange);
if (mPointCube) return stats.range = {type: "point", distance: {type: getUnit(mPointCube.groups.unit), amount: Number(mPointCube.groups.point)}};
// endregion

View File

@@ -14,6 +14,23 @@ class PageFilterOptionalFeatures extends PageFilter {
}
return SortUtil.listSort(itemA, itemB, options);
}
static getLevelFilterItem (prereq) {
const lvlMeta = prereq.level;
if (typeof lvlMeta === "number") {
return new FilterItem({
item: `Level ${lvlMeta}`,
nest: `(No Class)`,
});
}
const className = lvlMeta.class ? lvlMeta.class.name : `(No Class)`;
return new FilterItem({
item: `${lvlMeta.class ? className : ""}${lvlMeta.subclass ? ` (${lvlMeta.subclass.name})` : ""} Level ${lvlMeta.level}`,
nest: className,
});
}
// endregion
constructor () {
@@ -95,27 +112,7 @@ class PageFilterOptionalFeatures extends PageFilter {
});
});
it._fprereqFeature = it.prerequisite.filter(it => it.feature).map(it => it.feature);
it._fPrereqLevel = it.prerequisite.filter(it => it.level).map(it => {
const lvlMeta = it.level;
let item;
let className;
if (typeof lvlMeta === "number") {
className = `(No Class)`;
item = new FilterItem({
item: `Level ${lvlMeta}`,
nest: className,
});
} else {
className = lvlMeta.class ? lvlMeta.class.name : `(No Class)`;
item = new FilterItem({
item: `${lvlMeta.class ? className : ""}${lvlMeta.subclass ? ` (${lvlMeta.subclass.name})` : ""} Level ${lvlMeta.level}`,
nest: className,
});
}
return item;
});
it._fPrereqLevel = it.prerequisite.filter(it => it.level).map(PageFilterOptionalFeatures.getLevelFilterItem.bind(PageFilterOptionalFeatures));
}
it._dFeatureType = it.featureType.map(ft => Parser.optFeatureTypeToFull(ft));

View File

@@ -25,6 +25,13 @@ class PageFilterRecipes extends PageFilter {
itemSortFn: SortUtil.ascSortLower,
});
this._servesFilter = new RangeFilter({header: "Serves", min: 1, max: 1});
this._timeFilterTotal = new RangeFilter({header: "Total", min: 10, max: 60, displayFn: time => Parser.getMinutesToFull(time, {isShort: true}), displayFnTooltip: Parser.getMinutesToFull});
this._timeFilterPreparation = new RangeFilter({header: "Preparation", min: 10, max: 60, displayFn: time => Parser.getMinutesToFull(time, {isShort: true}), displayFnTooltip: Parser.getMinutesToFull});
this._timeFilterCooking = new RangeFilter({header: "Cooking", min: 10, max: 60, displayFn: time => Parser.getMinutesToFull(time, {isShort: true}), displayFnTooltip: Parser.getMinutesToFull});
this._timeFilter = new MultiFilter({
header: "Time",
filters: [this._timeFilterTotal, this._timeFilterPreparation, this._timeFilterCooking],
});
this._dietFilter = new Filter({
header: "Diet",
displayFn: PageFilterRecipes._dietToFull,
@@ -48,11 +55,52 @@ class PageFilterRecipes extends PageFilter {
if (SourceUtil.isLegacySourceWotc(it.source)) it._fMisc.push("Legacy");
if (it.miscTags) it._fMisc.push(...it.miscTags);
it._fServes = (it.serves?.min != null && it.serves?.max != null) ? [it.serves.min, it.serves.max] : (it.serves?.exact ?? null);
const totalTime = it.time?.total ?? this._mutateForFilters_getTotalTime(it.time);
it._fTimeTotal = totalTime != null ? this._mutateForFilters_getFilterTime(totalTime) : null;
it._fTimePreparation = it.time?.preparation ? this._mutateForFilters_getFilterTime(it.time.preparation) : null;
it._fTimeCooking = it.time?.cooking ? this._mutateForFilters_getFilterTime(it.time.cooking) : null;
it._fDiet = it.diet ? PageFilterRecipes._DIET_TO_FULL[it.diet] || it.diet : null;
if (it.hasFluff || it.fluff?.entries) it._fMisc.push("Has Info");
if (it.hasFluffImages || it.fluff?.images) it._fMisc.push("Has Images");
}
static _ONE_DAY_MINS = 24 * 60;
static _mutateForFilters_getTotalTime (time) {
if (time == null) return null;
let min = 0; let max = 0;
Object.values(time)
.forEach(val => {
if (val.min != null && val.max != null) {
min += val.min;
max += val.max;
return;
}
if (typeof val === "number") {
min += val;
max += val;
}
});
if (!min && !max) return null;
// Heuristic -- if calculated total time is longer than a day, we probably don't want to display it
// e.g. when including long "fermentation" time
if (min >= this._ONE_DAY_MINS || max >= this._ONE_DAY_MINS) return null;
if (min === max) return min;
return {min, max};
}
static _mutateForFilters_getFilterTime (val) {
if (val.min != null && val.max != null) return [val.min, val.max];
if (typeof val === "number") return val;
return null;
}
addToFilters (it, isExcluded) {
if (isExcluded) return;
@@ -60,6 +108,9 @@ class PageFilterRecipes extends PageFilter {
this._typeFilter.addItem(it.type);
this._dishTypeFilter.addItem(it.dishTypes);
this._servesFilter.addItem(it._fServes);
this._timeFilterTotal.addItem(it._fTimeTotal);
this._timeFilterPreparation.addItem(it._fTimePreparation);
this._timeFilterCooking.addItem(it._fTimeCooking);
this._dietFilter.addItem(it._fDiet);
this._allergensFilter.addItem(it.allergenGroups);
this._miscFilter.addItem(it._fMisc);
@@ -71,6 +122,7 @@ class PageFilterRecipes extends PageFilter {
this._typeFilter,
this._dishTypeFilter,
this._servesFilter,
this._timeFilter,
this._dietFilter,
this._allergensFilter,
this._miscFilter,
@@ -84,6 +136,11 @@ class PageFilterRecipes extends PageFilter {
it.type,
it.dishTypes,
it._fServes,
[
it._fTimeTotal,
it._fTimePreparation,
it._fTimeCooking,
],
it._fDiet,
it.allergenGroups,
it._fMisc,

View File

@@ -1062,7 +1062,7 @@ Parser.getTimeToFull = function (time) {
return `${time.number ? `${time.number} ` : ""}${time.unit === "bonus" ? "bonus action" : time.unit}${time.number > 1 ? "s" : ""}`;
};
Parser.getMinutesToFull = function (mins) {
Parser.getMinutesToFull = function (mins, {isShort = false} = {}) {
const days = Math.floor(mins / (24 * 60));
mins = mins % (24 * 60);
@@ -1070,9 +1070,9 @@ Parser.getMinutesToFull = function (mins) {
mins = mins % 60;
return [
days ? `${days} day${days > 1 ? "s" : ""}` : null,
hours ? `${hours} hour${hours > 1 ? "s" : ""}` : null,
mins ? `${mins} minute${mins > 1 ? "s" : ""}` : null,
days ? `${days} ${isShort ? `d` : `day${days > 1 ? "s" : ""}`}` : null,
hours ? `${hours} ${isShort ? `h` : `hour${hours > 1 ? "s" : ""}`}` : null,
mins ? `${mins} ${isShort ? `m` : `minute${mins > 1 ? "s" : ""}`}` : null,
].filter(Boolean)
.join(" ");
};
@@ -1480,6 +1480,7 @@ Parser.monTypeToFullObj = function (type) {
tagsSidekick: [],
asTextSidekick: null,
};
if (type == null) return out;
// handles e.g. "fey"
if (typeof type === "string") {
@@ -1896,7 +1897,7 @@ Parser.weightToFull = function (lbs, isSmallUnit) {
};
Parser.RARITIES = ["common", "uncommon", "rare", "very rare", "legendary", "artifact"];
Parser.ITEM_RARITIES = ["none", ...Parser.RARITIES, "unknown", "unknown (magic)", "other"];
Parser.ITEM_RARITIES = ["none", ...Parser.RARITIES, "varies", "unknown", "unknown (magic)", "other"];
Parser.CAT_ID_CREATURE = 1;
Parser.CAT_ID_SPELL = 2;

View File

@@ -20,11 +20,11 @@ class RenderDecks {
}),
};
static getCardTextHtml ({card}) {
static getCardTextHtml ({card, deck = null}) {
const ptText = Renderer.get()
.setFirstSection(true)
.setPartPageExpandCollapseDisabled(true)
.render({name: card.name, entries: Renderer.card.getFullEntries(card)}, 1);
.render({name: card.name, entries: Renderer.card.getFullEntries(card, {backCredit: deck?.back?.credit})}, 1);
Renderer.get().setPartPageExpandCollapseDisabled(false);
return ptText;
}
@@ -45,6 +45,12 @@ class RenderDecks {
.map((card, ixCard) => {
const ptText = this.getCardTextHtml({card});
const $btnMarkDrawn = $(`<button class="btn btn-default btn-xs" title="Mark Card as Drawn"><i class="fas fa-fw fa-cards"></i></button>`)
.click(async evt => {
evt.stopPropagation();
await cardStateManager.pDrawCard(ent, card);
});
const $btnReplace = $(`<button class="btn btn-default btn-xs" title="Return Card to Deck"><i class="fas fa-arrow-rotate-left"></i></button>`)
.click(async evt => {
evt.stopPropagation();
@@ -65,6 +71,7 @@ class RenderDecks {
const $wrpFace = $$`<div class="no-shrink px-1 decks__wrp-card-face relative">
<div class="absolute pt-2 pr-2 decks__wrp-btn-show-card">
<div class="btn-group ve-flex-v-center">
${$btnMarkDrawn}
${$btnReplace}
${$btnViewer}
</div>
@@ -72,12 +79,15 @@ class RenderDecks {
${Renderer.get().setFirstSection(true).render({...card.face, title: card.name, altText: card.name})}
</div>`;
const $imgFace = $wrpFace.find("img");
const title = $imgFace.closest(`[title]`).title();
const propCardDrawn = cardStateManager.getPropCardDrawn({hashDeck, ixCard});
const hkCardDrawn = cardStateManager.addHookBase(propCardDrawn, () => {
const isDrawn = !!cardStateManager.get(propCardDrawn);
$btnMarkDrawn.prop("disabled", isDrawn);
$btnReplace.prop("disabled", !isDrawn);
$wrpFace.toggleClass("decks__wrp-card-face--drawn", isDrawn);
$wrpFace.find("img").title(isDrawn ? `${card.name} (Drawn)` : card.name);
$imgFace.title(isDrawn ? `${title} (Drawn)` : title);
});
fnsCleanup.push(() => cardStateManager.removeHookBase(propCardDrawn, hkCardDrawn));
hkCardDrawn();
@@ -208,7 +218,7 @@ class RenderDecks {
${$wrpCardSway}
</div>`;
const ptText = RenderDecks.getCardTextHtml({card});
const ptText = RenderDecks.getCardTextHtml({card, deck});
const $wrpInfo = $$`<div class="stats stats--book decks-draw__wrp-desc mobile__hidden px-2 ve-text-center mb-4">${ptText}</div>`
.click(evt => evt.stopPropagation());

View File

@@ -8160,8 +8160,15 @@ Renderer.item = class {
// armor
if (item.ac != null) {
const itemType = item.bardingType || item.type;
const dexterityMax = (itemType === "MA" && item.dexterityMax == null)
? 2
: item.dexterityMax;
const isAddDex = item.dexterityMax != null || itemType !== "HA";
const prefix = item.type === "S" ? "+" : "";
const suffix = (item.type === "LA" || item.bardingType === "LA") || ((item.type === "MA" || item.bardingType === "MA") && item.dexterityMax === null) ? " + Dex" : (item.type === "MA" || item.bardingType === "MA") ? ` + Dex (max ${item.dexterityMax ?? 2})` : "";
const suffix = isAddDex ? ` + Dex${dexterityMax ? ` (max ${dexterityMax})` : ""}` : "";
damageParts.push(`AC ${prefix}${item.ac}${suffix}`);
}
if (item.acSpecial != null) damageParts.push(item.ac != null ? item.acSpecial : `AC ${item.acSpecial}`);
@@ -9083,7 +9090,7 @@ Renderer.item = class {
}
// handle item groups
if (item._isItemGroup) {
if (item._isItemGroup && item.items?.length) {
Renderer.item._initFullEntries(item);
item._fullEntries.push({type: "wrapper", wrapped: "Multiple variations of this item exist, as listed below:", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant"}});
item._fullEntries.push({
@@ -10570,12 +10577,22 @@ Renderer.recipe = class {
};
Renderer.card = class {
static getFullEntries (ent) {
static getFullEntries (ent, {backCredit = null} = {}) {
const entries = [...ent.entries || []];
if (ent.suit && (ent.valueName || ent.value)) {
const suitAndValue = `${((ent.valueName || "") || Parser.numberToText(ent.value)).toTitleCase()} of ${ent.suit.toTitleCase()}`;
if (suitAndValue.toLowerCase() !== ent.name.toLowerCase()) entries.unshift(`{@i ${suitAndValue}}`);
}
const ptCredits = [
ent.face?.credit ? `art credit: ${ent.face?.credit}` : null,
(backCredit || ent.back?.credit) ? `art credit (reverse): ${backCredit || ent.back?.credit}` : null,
]
.filter(Boolean)
.join(", ")
.uppercaseFirst();
if (ptCredits) entries.push(`{@note {@style ${ptCredits}|small}}`);
return entries;
}

View File

@@ -350,6 +350,7 @@ globalThis.ScaleCreatureDamageExpression = class {
ScaleCreatureUtils.getDiceExpressionAverage(
state.getDiceExpression({
numDice: numDiceTemp,
diceFaces: diceFacesTemp,
}),
),
)
@@ -435,7 +436,7 @@ globalThis.ScaleCreatureDamageExpression = class {
});
const avgDamOut = Math.floor(ScaleCreatureUtils.getDiceExpressionAverage(diceExpOut));
if (avgDamOut <= 0 || diceExpOut === "1") return `1 ${suffix.replace(/^\W+/, " ").replace(/ +/, " ")}`;
if (avgDamOut <= 0 || diceExpOut === "1") return `1 ${state.suffix.replace(/^\W+/, " ").replace(/ +/, " ")}`;
const expression = [
Math.floor(ScaleCreatureUtils.getDiceExpressionAverage(diceExpOut)),

View File

@@ -1433,6 +1433,8 @@ PropOrder._MAGICVARIANT = [
"requires",
"excludes",
"rarity",
"ammo",
"entries",

View File

@@ -2408,7 +2408,7 @@ class InputUiUtil {
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!`);
if (this._isRemember && !storageKey && !fnRemember) throw new Error(`No "storageKey" or "fnRemember" 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>

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.199.0"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.VERSION_NUMBER = /* 5ETOOLS_VERSION__OPEN */"1.199.1"/* 5ETOOLS_VERSION__CLOSE */;
globalThis.DEPLOYED_IMG_ROOT = undefined;
// for the roll20 script to set
globalThis.IS_VTT = false;