"use strict";
class LootGenUi extends BaseComponent {
static _CHALLENGE_RATING_RANGES = {
0: "0\u20134",
5: "5\u201310",
11: "11\u201316",
17: "17+",
};
static _PARTY_LOOT_LEVEL_RANGES = {
4: "1\u20134",
10: "5\u201310",
16: "11\u201316",
20: "17+",
};
static _PARTY_LOOT_ITEMS_PER_LEVEL = {
1: {
"major": {
"uncommon": 0,
"rare": 0,
"very rare": 0,
"legendary": 0,
},
"minor": {
"common": 0,
"uncommon": 0,
"rare": 0,
"very rare": 0,
"legendary": 0,
},
},
4: {
"major": {
"uncommon": 2,
"rare": 0,
"very rare": 0,
"legendary": 0,
},
"minor": {
"common": 6,
"uncommon": 2,
"rare": 1,
"very rare": 0,
"legendary": 0,
},
},
10: {
"major": {
"uncommon": 5,
"rare": 1,
"very rare": 0,
"legendary": 0,
},
"minor": {
"common": 10,
"uncommon": 12,
"rare": 5,
"very rare": 1,
"legendary": 0,
},
},
16: {
"major": {
"uncommon": 1,
"rare": 2,
"very rare": 2,
"legendary": 1,
},
"minor": {
"common": 3,
"uncommon": 6,
"rare": 9,
"very rare": 5,
"legendary": 1,
},
},
20: {
"major": {
"uncommon": 0,
"rare": 1,
"very rare": 2,
"legendary": 3,
},
"minor": {
"common": 0,
"uncommon": 0,
"rare": 4,
"very rare": 9,
"legendary": 6,
},
},
};
static _DRAGON_AGES = [
"Wyrmling",
"Young",
"Adult",
"Ancient",
];
constructor ({spells, items, ClsLootGenOutput}) {
super();
TabUiUtil.decorate(this, {isInitMeta: true});
this._ClsLootGenOutput = ClsLootGenOutput || LootGenOutput;
this._modalFilterSpells = new ModalFilterSpells({namespace: "LootGenUi.spells", allData: spells});
this._modalFilterItems = new ModalFilterItems({
namespace: "LootGenUi.items",
allData: items,
});
this._data = null;
this._dataSpells = spells;
this._dataItems = items;
this._dataSpellsFiltered = [...spells];
this._dataItemsFiltered = [...items];
this._lt_tableMetas = null;
this._pl_xgeTableLookup = null;
this._$wrpOutputRows = null;
this._lootOutputs = [];
}
static _er (...args) { return Renderer.get().setFirstSection(true).render(...args); }
getSaveableState () {
return {
...super.getSaveableState(),
meta: this.__meta,
};
}
setStateFrom (toLoad, isOverwrite = false) {
super.setStateFrom(toLoad, isOverwrite);
toLoad.meta && this._proxyAssignSimple("meta", toLoad.meta, isOverwrite);
}
addHookAll (hookProp, hook) { return this._addHookAll(hookProp, hook); }
async pInit () {
await this._modalFilterSpells.pPreloadHidden();
await this._modalFilterItems.pPreloadHidden();
await this._modalFilterSpells.pPopulateHiddenWrapper();
await this._modalFilterItems.pPopulateHiddenWrapper();
this._data = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/loot.json`);
await this._pInit_pBindFilterHooks();
}
async _pInit_pBindFilterHooks () {
const tablesMagicItems = await ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
.pMap(async letter => {
return {
letter,
tableEntry: await DataLoader.pCacheAndGet(UrlUtil.PG_TABLES, Parser.SRC_DMG, UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TABLES]({name: `Magic Item Table ${letter}`, source: Parser.SRC_DMG})),
};
});
const hkFilterChangeSpells = () => this._handleFilterChangeSpells();
this._modalFilterSpells.pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, hkFilterChangeSpells);
hkFilterChangeSpells();
const hkFilterChangeItems = () => this._handleFilterChangeItems({tablesMagicItems});
this._modalFilterItems.pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, hkFilterChangeItems);
hkFilterChangeItems();
}
_handleFilterChangeSpells () {
const f = this._modalFilterSpells.pageFilter.filterBox.getValues();
this._dataSpellsFiltered = this._dataSpells.filter(it => this._modalFilterSpells.pageFilter.toDisplay(f, it));
this._state.pulseSpellsFiltered = !this._state.pulseSpellsFiltered;
}
_handleFilterChangeItems ({tablesMagicItems}) {
const f = this._modalFilterItems.pageFilter.filterBox.getValues();
this._dataItemsFiltered = this._dataItems.filter(it => this._modalFilterItems.pageFilter.toDisplay(f, it));
const xgeTables = this._getXgeFauxTables();
this._lt_tableMetas = [
null,
...tablesMagicItems.map(({letter, tableEntry}) => {
tableEntry = MiscUtil.copy(tableEntry);
tableEntry.type = "table";
delete tableEntry.chapter;
return {
type: "DMG",
dmgTableType: letter,
tableEntry,
table: this._data.magicItems.find(it => it.type === letter),
};
}),
...xgeTables,
];
this._pl_xgeTableLookup = {};
xgeTables.forEach(({tier, rarity, table}) => MiscUtil.set(this._pl_xgeTableLookup, tier, rarity, table));
this._state.pulseItemsFiltered = !this._state.pulseItemsFiltered;
}
/** Create fake tables for the XGE rules */
_getXgeFauxTables () {
const byTier = {};
this._dataItemsFiltered
.forEach(item => {
const tier = item.tier || "other";
const rarity = item.rarity || (Renderer.item.isMundane(item) ? "unknown" : "unknown (magic)");
const tgt = MiscUtil.getOrSet(byTier, tier, rarity, []);
tgt.push(item);
});
return Object.entries(byTier)
.map(([tier, byRarity]) => {
return Object.entries(byRarity)
.sort(([rarityA], [rarityB]) => SortUtil.ascSortItemRarity(rarityA, rarityB))
.map(([rarity, items]) => {
const isMundane = Renderer.item.isMundane({rarity});
const caption = tier === "other"
? `Other ${isMundane ? "mundane" : "magic"} items of ${rarity} rarity`
: `${tier.toTitleCase()}-tier ${isMundane ? "mundane" : "magic"} items of ${rarity} rarity`;
return {
type: "XGE",
tier,
rarity,
tableEntry: {
type: "table",
caption,
colLabels: [
`d${items.length}`,
"Item",
],
colStyles: [
"col-2 text-center",
"col-10",
],
rows: items.map((it, i) => ([i + 1, `{@item ${it.name}|${it.source}}`])),
},
table: {
name: caption,
source: Parser.SRC_XGE,
page: 135,
diceType: items.length,
table: items.map((it, i) => ({min: i + 1, max: i + 1, item: `{@item ${it.name}|${it.source}}`})),
},
};
});
})
.flat();
}
render ({$stg, $stgLhs, $stgRhs}) {
if ($stg && ($stgLhs || $stgRhs)) throw new Error(`Only one of "parent stage" and "LHS/RHS stages" may be specified!`);
const {$stgLhs: $stgLhs_, $stgRhs: $stgRhs_} = this._render_$getStages({$stg, $stgLhs, $stgRhs});
const iptTabMetas = [
new TabUiUtil.TabMeta({name: "Random Treasure by CR", hasBorder: true, hasBackground: true}),
new TabUiUtil.TabMeta({name: "Loot Tables", hasBorder: true, hasBackground: true}),
new TabUiUtil.TabMeta({name: "Party Loot", hasBorder: true, hasBackground: true}),
new TabUiUtil.TabMeta({name: "Dragon Hoard", hasBorder: true, hasBackground: true}),
new TabUiUtil.TabMeta({name: "Gems/Art Objects Generator", isHeadHidden: true, hasBackground: true}),
new TabUiUtil.TabMeta({
type: "buttons",
buttons: [
{
html: ` `,
title: "Other Generators",
type: "default",
pFnClick: null, // This is assigned later
},
],
}),
];
const tabMetas = this._renderTabs(iptTabMetas, {$parent: $stgLhs_});
const [tabMetaFindTreasure, tabMetaLootTables, tabMetaPartyLoot, tabMetaDragonHoard, tabMetaGemsArtObjects, tabMetaOptions] = tabMetas;
this._render_tabFindTreasure({tabMeta: tabMetaFindTreasure});
this._render_tabLootTables({tabMeta: tabMetaLootTables});
this._render_tabPartyLoot({tabMeta: tabMetaPartyLoot});
this._render_tabDragonHoard({tabMeta: tabMetaDragonHoard});
this._render_tabGemsArtObjects({tabMeta: tabMetaGemsArtObjects});
this._render_tabOptions({tabMeta: tabMetaOptions, tabMetaGemsArtObjects});
this._render_output({$wrp: $stgRhs_});
}
/**
* If we have been provided an existing pair of left-/right-hand stages, use them.
* Otherwise, render a two-column UI, and return each column as a stage.
* This allows us to cater for both the pre-baked layout of the Lootgen page, and other, more general,
* components.
*/
_render_$getStages ({$stg, $stgLhs, $stgRhs}) {
if (!$stg) return {$stgLhs, $stgRhs};
$stgLhs = $(`
`);
$stgRhs = $(`
`);
$$``.appendTo($stg.empty());
return {$stgLhs, $stgRhs};
}
_render_tabFindTreasure ({tabMeta}) {
const $selChallenge = ComponentUiUtil.$getSelEnum(
this,
"ft_challenge",
{
values: Object.keys(LootGenUi._CHALLENGE_RATING_RANGES).map(it => Number(it)),
fnDisplay: it => LootGenUi._CHALLENGE_RATING_RANGES[it],
},
);
const $cbIsHoard = ComponentUiUtil.$getCbBool(this, "ft_isHoard");
const $btnRoll = $(`Roll Loot `)
.click(() => this._ft_pDoHandleClickRollLoot());
const $btnClear = $(`Clear Output `)
.click(() => this._doClearOutput());
$$`
Challenge Rating
${$selChallenge}
Is Treasure Hoard?
${$cbIsHoard}
${$btnRoll}
${$btnClear}
${this.constructor._er(`Based on the tables and rules in the {@book Dungeon Master's Guide|DMG|7|Treasure Tables}`)}, pages 133-149.
`.appendTo(tabMeta.$wrpTab);
}
_ft_pDoHandleClickRollLoot () {
if (this._state.ft_isHoard) return this._ft_doHandleClickRollLoot_pHoard();
return this._ft_doHandleClickRollLoot_single();
}
_ft_doHandleClickRollLoot_single () {
const tableMeta = this._data.individual.find(it => it.crMin === this._state.ft_challenge);
const rowRoll = RollerUtil.randomise(100);
const row = tableMeta.table.find(it => rowRoll >= it.min && rowRoll <= it.max);
const coins = this._getConvertedCoins(
Object.entries(row.coins)
.mergeMap(([type, formula]) => ({[type]: Renderer.dice.parseRandomise2(formula)})),
);
const lootOutput = new this._ClsLootGenOutput({
type: `Individual Treasure: ${LootGenUi._CHALLENGE_RATING_RANGES[this._state.ft_challenge]}`,
name: `{@b Individual Treasure} for challenge rating {@b ${LootGenUi._CHALLENGE_RATING_RANGES[this._state.ft_challenge]}}`,
coins,
});
this._doAddOutput({lootOutput});
}
async _ft_doHandleClickRollLoot_pHoard () {
const tableMeta = this._data.hoard.find(it => it.crMin === this._state.ft_challenge);
const rowRoll = RollerUtil.randomise(100);
const row = tableMeta.table.find(it => rowRoll >= it.min && rowRoll <= it.max);
const coins = this._getConvertedCoins(
Object.entries(tableMeta.coins || {})
.mergeMap(([type, formula]) => ({[type]: Renderer.dice.parseRandomise2(formula)})),
);
const gems = this._doHandleClickRollLoot_hoard_gemsArtObjects({row, prop: "gems"});
const artObjects = this._doHandleClickRollLoot_hoard_gemsArtObjects({row, prop: "artObjects"});
const magicItemsByTable = await this._doHandleClickRollLoot_hoard_pMagicItems({row});
const lootOutput = new this._ClsLootGenOutput({
type: `Treasure Hoard: ${LootGenUi._CHALLENGE_RATING_RANGES[this._state.ft_challenge]}`,
name: `{@b Hoard} for challenge rating {@b ${LootGenUi._CHALLENGE_RATING_RANGES[this._state.ft_challenge]}}`,
coins,
gems,
artObjects,
magicItemsByTable,
});
this._doAddOutput({lootOutput});
}
_doHandleClickRollLoot_hoard_gemsArtObjects ({row, prop}) {
if (!row[prop]) return null;
const lootMeta = row[prop];
const {type, typeRoll} = this._doHandleClickRollLoot_hoard_gemsArtObjects_getTypeInfo({lootMeta});
const specificTable = this._data[prop].find(it => it.type === type);
const count = Renderer.dice.parseRandomise2(lootMeta.amount);
const breakdown = {};
[...new Array(count)]
.forEach(() => {
const type = RollerUtil.rollOnArray(specificTable.table);
breakdown[type] = (breakdown[type] || 0) + 1;
});
return [
new LootGenOutputGemsArtObjects({
type,
typeRoll,
typeTable: lootMeta.typeTable,
count,
breakdown,
}),
];
}
/** Alternate version, which rolls for type for each item. */
_doHandleClickRollLoot_hoard_gemsArtObjectsMulti ({row, prop}) {
if (!row[prop]) return null;
const lootMeta = row[prop];
const count = Renderer.dice.parseRandomise2(lootMeta.amount);
const byType = {};
[...new Array(count)]
.forEach(() => {
const {type} = this._doHandleClickRollLoot_hoard_gemsArtObjects_getTypeInfo({lootMeta});
if (!byType[type]) {
byType[type] = {
breakdown: {},
count: 0,
};
}
const meta = byType[type];
meta.count++;
const specificTable = this._data[prop].find(it => it.type === type);
const type2 = RollerUtil.rollOnArray(specificTable.table);
meta.breakdown[type2] = (meta.breakdown[type2] || 0) + 1;
});
return Object.entries(byType)
.map(([type, meta]) => {
return new LootGenOutputGemsArtObjects({
type,
typeRoll: null,
typeTable: lootMeta.typeTable,
count: meta.count,
breakdown: meta.breakdown,
});
});
}
_doHandleClickRollLoot_hoard_gemsArtObjects_getTypeInfo ({lootMeta}) {
if (lootMeta.type) return {type: lootMeta.type};
const typeRoll = RollerUtil.randomise(100);
const typeMeta = lootMeta.typeTable.find(it => typeRoll >= it.min && typeRoll <= it.max);
return {type: typeMeta.type, typeRoll};
}
async _doHandleClickRollLoot_hoard_pMagicItems ({row, fnGetIsPreferAltChoose = null}) {
if (!row.magicItems) return null;
return row.magicItems.pMap(async magicItemsObj => {
const {type, typeRoll, typeAltChoose} = this._doHandleClickRollLoot_hoard_pMagicItems_getTypeInfo({magicItemsObj});
const magicItemTable = this._data.magicItems.find(it => it.type === type);
const count = Renderer.dice.parseRandomise2(magicItemsObj.amount);
const itemsAltChoose = this._doHandleClickRollLoot_hoard_getAltChooseList({typeAltChoose});
const itemsAltChooseDisplayText = this._doHandleClickRollLoot_hoard_getAltChooseDisplayText({typeAltChoose});
const breakdown = [];
await ([...new Array(count)].pSerialAwaitMap(async () => {
const lootItem = await LootGenMagicItem.pGetMagicItemRoll({
lootGenMagicItems: breakdown,
spells: this._dataSpellsFiltered,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll: fnGetIsPreferAltChoose ? fnGetIsPreferAltChoose() : false,
fnGetIsPreferAltChoose,
});
breakdown.push(lootItem);
}));
return new LootGenOutputMagicItems({
type,
count,
typeRoll,
typeTable: magicItemsObj.typeTable,
breakdown,
});
});
}
async _doHandleClickRollLoot_hoard_pMagicItemsMulti ({row, fnGetIsPreferAltChoose = null}) {
if (!row.magicItems) return null;
const byType = {};
await row.magicItems.pMap(async magicItemsObj => {
const count = Renderer.dice.parseRandomise2(magicItemsObj.amount);
await [...new Array(count)]
.pSerialAwaitMap(async () => {
const {type, typeAltChoose} = this._doHandleClickRollLoot_hoard_pMagicItems_getTypeInfo({magicItemsObj});
if (!byType[type]) {
byType[type] = {
breakdown: [],
count: 0,
typeTable: magicItemsObj.typeTable,
};
}
const meta = byType[type];
const magicItemTable = this._data.magicItems.find(it => it.type === type);
const itemsAltChoose = this._doHandleClickRollLoot_hoard_getAltChooseList({typeAltChoose});
const itemsAltChooseDisplayText = this._doHandleClickRollLoot_hoard_getAltChooseDisplayText({typeAltChoose});
const lootItem = await LootGenMagicItem.pGetMagicItemRoll({
lootGenMagicItems: meta.breakdown,
spells: this._dataSpellsFiltered,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll: fnGetIsPreferAltChoose ? fnGetIsPreferAltChoose() : false,
fnGetIsPreferAltChoose,
});
meta.breakdown.push(lootItem);
});
});
return Object.entries(byType)
.map(([type, meta]) => {
return new LootGenOutputMagicItems({
type,
count: meta.count,
typeRoll: null,
typeTable: meta.typeTable,
breakdown: meta.breakdown,
});
});
}
_doHandleClickRollLoot_hoard_pMagicItems_getTypeInfo ({magicItemsObj}) {
if (magicItemsObj.type) return {type: magicItemsObj.type, typeAltChoose: magicItemsObj.typeAltChoose};
const typeRoll = RollerUtil.randomise(100);
const typeMeta = magicItemsObj.typeTable.find(it => typeRoll >= it.min && typeRoll <= it.max);
return {type: typeMeta.type, typeRoll, typeAltChoose: typeMeta.typeAltChoose};
}
_doHandleClickRollLoot_hoard_getAltChooseList ({typeAltChoose}) {
if (!typeAltChoose) return null;
return this._dataItemsFiltered.filter(it => it.type !== "GV" && Object.entries(typeAltChoose).every(([k, v]) => it[k] === v));
}
_doHandleClickRollLoot_hoard_getAltChooseDisplayText ({typeAltChoose}) {
if (!typeAltChoose) return null;
return [
typeAltChoose.rarity,
typeAltChoose.tier,
].filter(Boolean).join(" ");
}
_render_tabLootTables ({tabMeta}) {
let cacheTableMetas = [...this._lt_tableMetas];
const getSelTableValues = () => this._lt_tableMetas.map((_, i) => i);
const {$sel: $selTable, setValues: setSelTableValues} = ComponentUiUtil.$getSelEnum(
this,
"lt_ixTable",
{
asMeta: true,
values: getSelTableValues(),
fnDisplay: ix => this._lt_tableMetas[ix] == null
? `\u2014`
: this._lt_tableMetas[ix].tier
? `Tier: ${this._lt_tableMetas[ix].tier}; Rarity: ${this._lt_tableMetas[ix].rarity}`
: this._lt_tableMetas[ix].tableEntry.caption,
},
);
const hkPulseItem = () => {
const curVal = cacheTableMetas[this._state.lt_ixTable];
cacheTableMetas = [...this._lt_tableMetas];
const values = getSelTableValues();
setSelTableValues(values);
if (
!values.includes(this._state.lt_ixTable)
|| !curVal
|| !curVal.tier || !curVal.rarity
) return this._state.lt_ixTable = 0;
// Try to restore the previous selection (as it may have moved due to the array resizing)
const ix = this._lt_tableMetas.findIndex(it => it && it.tier === curVal.tier && it.rarity === curVal.rarity);
if (~ix) this._state.lt_ixTable = ix;
else this._state.lt_ixTable = 0;
};
this._addHookBase("pulseItemsFiltered", hkPulseItem);
const $btnRoll = $(`Roll Loot `)
.click(() => this._lt_pDoHandleClickRollLoot());
const $btnClear = $(`Clear Output `)
.click(() => this._doClearOutput());
const $hrHelp = $(` `);
const $dispHelp = $(`
`);
const $hrTable = $(` `);
const $dispTable = $(`
`);
const hkTable = () => {
const tableMeta = this._lt_tableMetas[this._state.lt_ixTable];
$dispHelp.toggleVe(tableMeta != null);
$dispTable.toggleVe(tableMeta != null);
$hrHelp.toggleVe(tableMeta != null);
$hrTable.toggleVe(tableMeta != null);
if (tableMeta == null) return;
$dispHelp
.html(tableMeta.type === "DMG" ? this.constructor._er(`Based on the tables and rules in the {@book Dungeon Master's Guide|DMG|7|Treasure Tables}, pages 133-149.`) : this.constructor._er(`Tables auto-generated based on the rules in {@book Xanathar's Guide to Everything (Choosing Items Piecemeal)|XGE|2|choosing items piecemeal}, pages 135-136.`));
$dispTable.html(this.constructor._er(tableMeta.tableEntry));
};
this._addHookBase("lt_ixTable", hkTable);
hkTable();
$$`
Table
${$selTable}
${$btnRoll}
${$btnClear}
${$hrHelp}
${$dispHelp}
${$hrTable}
${$dispTable}
`.appendTo(tabMeta.$wrpTab);
}
async _lt_pDoHandleClickRollLoot () {
const tableMeta = this._lt_tableMetas[this._state.lt_ixTable];
if (!tableMeta) return JqueryUtil.doToast({type: "warning", content: `Please select a table first!`});
const lootOutput = new this._ClsLootGenOutput({
type: `Treasure Table Roll: ${tableMeta.type === "DMG" ? tableMeta.tableEntry.caption : `${tableMeta.tier} ${tableMeta.rarity}`}`,
name: tableMeta.type === "DMG"
? `Rolled against {@b {@table ${tableMeta.tableEntry.caption}|${Parser.SRC_DMG}}}`
: `Rolled on the table for {@b ${tableMeta.tier} ${tableMeta.rarity}} items`,
magicItemsByTable: await this._lt_pDoHandleClickRollLoot_pGetMagicItemMetas({tableMeta}),
});
this._doAddOutput({lootOutput});
}
async _lt_pDoHandleClickRollLoot_pGetMagicItemMetas ({tableMeta}) {
const breakdown = [];
const lootItem = await LootGenMagicItem.pGetMagicItemRoll({
lootGenMagicItems: breakdown,
spells: this._dataSpellsFiltered,
magicItemTable: tableMeta.table,
});
breakdown.push(lootItem);
return [
new LootGenOutputMagicItems({
type: tableMeta.dmgTableType,
count: 1,
breakdown,
}),
];
}
_render_tabPartyLoot ({tabMeta}) {
const $cbIsExactLevel = ComponentUiUtil.$getCbBool(this, "pl_isExactLevel");
const $cbIsCumulative = ComponentUiUtil.$getCbBool(this, "pl_isCumulative");
// region Default
const $selCharLevel = ComponentUiUtil.$getSelEnum(
this,
"pl_charLevel",
{
values: Object.keys(LootGenUi._PARTY_LOOT_LEVEL_RANGES).map(it => Number(it)),
fnDisplay: it => LootGenUi._PARTY_LOOT_LEVEL_RANGES[it],
},
);
const $stgDefault = $$`
Character Level
${$selCharLevel}
`;
// endregion
// region Exact level
const $sliderLevel = ComponentUiUtil.$getSliderRange(
this,
{
propMin: "pl_exactLevelMin",
propMax: "pl_exactLevelMax",
propCurMin: "pl_exactLevel",
},
);
const $stgExactLevel = $$`
Character Level
${$sliderLevel}
`;
// endregion
// region Buttons
const $btnRoll = $(`Roll Loot `)
.click(() => this._pl_pDoHandleClickRollLoot());
const $btnClear = $(`Clear Output `)
.click(() => this._doClearOutput());
// endregion
const hkIsExactLevel = () => {
$stgDefault.toggleVe(!this._state.pl_isExactLevel);
$stgExactLevel.toggleVe(this._state.pl_isExactLevel);
};
this._addHookBase("pl_isExactLevel", hkIsExactLevel);
hkIsExactLevel();
$$`
Generates a set of magical items for a party, based on the tables and rules in ${this.constructor._er(`{@book Xanathar's Guide to Everything|XGE|2|awarding magic items}`)}, pages 135-136.
If "Exact Level" is selected, the output will include a proportional number of items for any partially-completed tier.
${$stgDefault}
${$stgExactLevel}
Cumulative with Previous Tiers
${$cbIsCumulative}
Is Exact Level
${$cbIsExactLevel}
${$btnRoll}
${$btnClear}
`.appendTo(tabMeta.$wrpTab);
}
async _pl_pDoHandleClickRollLoot () {
const template = this._pl_getLootTemplate();
const magicItemsByTable = [];
for (const [tier, byRarity] of Object.entries(template)) {
const breakdown = [];
for (const [rarity, cntItems] of Object.entries(byRarity)) {
const tableMeta = this._pl_xgeTableLookup[tier]?.[rarity];
for (let i = 0; i < cntItems; ++i) {
const lootItem = await LootGenMagicItem.pGetMagicItemRoll({
lootGenMagicItems: breakdown,
spells: this._dataSpellsFiltered,
magicItemTable: tableMeta,
});
breakdown.push(lootItem);
}
}
magicItemsByTable.push(
new LootGenOutputMagicItems({
count: breakdown.length,
breakdown,
tier,
}),
);
}
const ptLevel = this._state.pl_isExactLevel
? this._state.pl_exactLevel
: LootGenUi._PARTY_LOOT_LEVEL_RANGES[this._state.pl_charLevel];
const lootOutput = new this._ClsLootGenOutput({
type: `Party Loot: Level ${ptLevel}`,
name: `Magic items for a {@b Level ${ptLevel}} Party`,
magicItemsByTable,
});
this._doAddOutput({lootOutput});
}
_pl_getLootTemplate () {
const {template, levelLow} = this._state.pl_isExactLevel
? this._pl_getLootTemplate_exactLevel()
: {template: MiscUtil.copy(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL[this._state.pl_charLevel]), levelLow: this._state.pl_charLevel};
if (this._state.pl_isCumulative) this._pl_mutAccumulateLootTemplate({template, levelLow});
return template;
}
_pl_getLootTemplate_exactLevel () {
if (LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL[this._state.pl_exactLevel]) {
return {
template: MiscUtil.copy(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL[this._state.pl_exactLevel]),
levelLow: this._state.pl_exactLevel,
};
}
let levelLow = 1;
let levelHigh = 20;
Object.keys(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL)
.forEach(level => {
level = Number(level);
if (level < this._state.pl_exactLevel && (this._state.pl_exactLevel - level) < (this._state.pl_exactLevel - levelLow)) {
levelLow = level;
}
if (level > this._state.pl_exactLevel && (level - this._state.pl_exactLevel) < (levelHigh - this._state.pl_exactLevel)) {
levelHigh = level;
}
});
const templateLow = MiscUtil.copy(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL[levelLow]);
const templateHigh = MiscUtil.copy(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL[levelHigh]);
const ratio = (this._state.pl_exactLevel - levelLow) / (levelHigh - levelLow);
const out = {major: {}, minor: {}};
Object.entries(out)
.forEach(([tier, byTier]) => {
Object.keys(templateLow[tier])
.forEach(rarity => {
byTier[rarity] = Math.floor(
((templateLow[tier]?.[rarity] || 0) * (1 - ratio))
+ ((templateHigh[tier]?.[rarity] || 0) * ratio),
);
});
});
return {template: out, levelLow};
}
_pl_mutAccumulateLootTemplate ({template, levelLow}) {
const toAccumulate = Object.keys(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL)
.filter(it => Number(it) < levelLow);
if (!toAccumulate.length) return;
toAccumulate.forEach(level => {
Object.entries(LootGenUi._PARTY_LOOT_ITEMS_PER_LEVEL[level])
.forEach(([tier, byRarity]) => {
Object.entries(byRarity)
.forEach(([rarity, cntItems]) => {
const existing = MiscUtil.get(template, tier, rarity) || 0;
MiscUtil.set(template, tier, rarity, existing + (cntItems || 0));
});
});
});
}
_render_tabDragonHoard ({tabMeta}) {
const $selDragonAge = ComponentUiUtil.$getSelEnum(
this,
"dh_dragonAge",
{
values: LootGenUi._DRAGON_AGES,
},
);
const $cbIsPreferRandomMagicItems = ComponentUiUtil.$getCbBool(this, "dh_isPreferRandomMagicItems");
const $btnRoll = $(`Roll Loot `)
.click(() => this._dh_pDoHandleClickRollLoot());
const $btnClear = $(`Clear Output `)
.click(() => this._doClearOutput());
$$`
Dragon Age
${$selDragonAge}
Prefer Random Magic Items
${$cbIsPreferRandomMagicItems}
${$btnRoll}
${$btnClear}
${this.constructor._er(`Based on the tables and rules in {@book Fizban's Treasury of Dragons|FTD|4|Creating a Hoard}`)}, pages 72.
`.appendTo(tabMeta.$wrpTab);
}
async _dh_pDoHandleClickRollLoot () {
const tableMeta = this._data.dragon.find(it => it.name === this._state.dh_dragonAge);
const coins = this._getConvertedCoins(
Object.entries(tableMeta.coins || {})
.mergeMap(([type, formula]) => ({[type]: Renderer.dice.parseRandomise2(formula)})),
);
const dragonMundaneItems = this._dh_doHandleClickRollLoot_mundaneItems({dragonMundaneItems: tableMeta.dragonMundaneItems});
const gems = this._doHandleClickRollLoot_hoard_gemsArtObjectsMulti({row: tableMeta, prop: "gems"});
const artObjects = this._doHandleClickRollLoot_hoard_gemsArtObjectsMulti({row: tableMeta, prop: "artObjects"});
const magicItemsByTable = await this._doHandleClickRollLoot_hoard_pMagicItemsMulti({
row: tableMeta,
fnGetIsPreferAltChoose: () => !!this._state.dh_isPreferRandomMagicItems,
});
const lootOutput = new this._ClsLootGenOutput({
type: `Dragon Hoard: ${LootGenUi._CHALLENGE_RATING_RANGES[this._state.ft_challenge]}`,
name: `${this._state.dh_dragonAge} Dragon's Hoard`,
coins,
gems,
artObjects,
dragonMundaneItems,
magicItemsByTable,
});
this._doAddOutput({lootOutput});
}
_dh_doHandleClickRollLoot_mundaneItems ({dragonMundaneItems}) {
if (!dragonMundaneItems) return null;
const count = Renderer.dice.parseRandomise2(dragonMundaneItems.amount);
const breakdown = [];
[...new Array(count)]
.forEach(() => {
const roll = RollerUtil.randomise(100);
const result = this._data.dragonMundaneItems.find(it => roll >= it.min && roll <= it.max);
breakdown.push(result.item);
});
return new LootGenOutputDragonMundaneItems({
count: count,
breakdown,
});
}
_render_tabGemsArtObjects ({tabMeta}) {
const $cbIsUseGems = ComponentUiUtil.$getCbBool(this, "gao_isUseGems");
const $cbIsUseArtObjects = ComponentUiUtil.$getCbBool(this, "gao_isUseArtObjects");
const $iptTargetGoldAmount = ComponentUiUtil.$getIptInt(this, "gao_targetGoldAmount", 0, {min: 0})
.keydown(evt => {
if (evt.key !== "Enter") return;
$iptTargetGoldAmount.change();
$btnRoll.click();
});
const $btnRoll = $(`Roll Loot `)
.click(() => this._goa_pDoHandleClickRollLoot());
const $btnClear = $(`Clear Output `)
.click(() => this._doClearOutput());
$$`
Gem/Art Object Generator
Include Gems
${$cbIsUseGems}
Include Art Objects
${$cbIsUseArtObjects}
Target Gold Amount
${$iptTargetGoldAmount}
${$btnRoll}
${$btnClear}
${this.constructor._er(`This custom generator randomly selects gems/art objects up to the target gold amount.`)}
`.appendTo(tabMeta.$wrpTab);
}
async _goa_pDoHandleClickRollLoot () {
if (this._state.gao_targetGoldAmount <= 0) return JqueryUtil.doToast({content: "Please enter a target gold amount!", type: "warning"});
if (!this._state.gao_isUseGems && !this._state.gao_isUseArtObjects) return JqueryUtil.doToast({content: `Please select at least one of "Include Gems" and/or "Include Art Objects"`, type: "warning"});
const typeMap = {};
[{prop: "gems", stateProp: "gao_isUseGems"}, {prop: "artObjects", stateProp: "gao_isUseArtObjects"}]
.forEach(({prop, stateProp}) => {
if (!this._state[stateProp]) return;
this._data[prop]
.forEach(({type, table}) => {
(typeMap[type] = typeMap[type] || []).push({prop, table});
});
});
const types = Object.keys(typeMap).map(it => Number(it)).sort(SortUtil.ascSort).reverse();
if (this._state.gao_targetGoldAmount < types.last()) return JqueryUtil.doToast({content: `Could not generate any gems/art objects for a gold amount of ${this._state.gao_targetGoldAmount}! Please increase the target gold amount.`, type: "warning"});
// Map of -> -> {, }
const generated = {};
let budget = this._state.gao_targetGoldAmount;
while (budget >= types.last()) {
const validTypes = types.filter(it => it <= budget);
const type = RollerUtil.rollOnArray(validTypes);
const typeMetas = typeMap[type];
const {prop, table} = RollerUtil.rollOnArray(typeMetas);
const rolled = RollerUtil.rollOnArray(table);
const genMeta = MiscUtil.getOrSet(generated, prop, type, {});
genMeta.count = (genMeta.count || 0) + 1;
genMeta.breakdown = genMeta.breakdown || {};
genMeta.breakdown[rolled] = (genMeta.breakdown[rolled] || 0) + 1;
budget -= type;
}
const [gems, artObjects] = ["gems", "artObjects"]
.map(prop => {
return generated[prop]
? Object.entries(generated[prop])
.sort(([typeA], [typeB]) => SortUtil.ascSort(Number(typeB), Number(typeA)))
.map(([type, {count, breakdown}]) => {
type = Number(type);
return new LootGenOutputGemsArtObjects({
type,
count,
breakdown,
});
})
: null;
});
const lootOutput = new this._ClsLootGenOutput({
type: `Gems/Art Objects`,
name: `Gems/Art Objects: Roughly ${this._state.gao_targetGoldAmount.toLocaleString()} gp`,
gems,
artObjects,
});
this._doAddOutput({lootOutput});
}
_render_tabOptions ({tabMeta, tabMetaGemsArtObjects}) {
const menuOthers = ContextUtil.getMenu([
new ContextUtil.Action(
"Gems/Art Objects Generator",
() => {
this._setActiveTab({tab: tabMetaGemsArtObjects});
},
),
null,
new ContextUtil.Action(
"Set Random Item Filters",
() => {
this._modalFilterItems.handleHiddenOpenButtonClick();
},
{
title: `Set the filtering parameters used to determine which items can be randomly rolled for some results. Note that this does not, for example, remove these items from standard loot tables.`,
fnActionAlt: async (evt) => {
this._modalFilterItems.handleHiddenResetButtonClick(evt);
JqueryUtil.doToast(`Reset${evt.shiftKey ? " all" : ""}!`);
},
textAlt: ` `,
titleAlt: FilterBox.TITLE_BTN_RESET,
},
),
new ContextUtil.Action(
"Set Random Spell Filters",
() => {
this._modalFilterSpells.handleHiddenOpenButtonClick();
},
{
title: `Set the filtering parameters used to determine which spells can be randomly rolled for some results.`,
fnActionAlt: async (evt) => {
this._modalFilterSpells.handleHiddenResetButtonClick(evt);
JqueryUtil.doToast(`Reset${evt.shiftKey ? " all" : ""}!`);
},
textAlt: ` `,
titleAlt: FilterBox.TITLE_BTN_RESET,
},
),
null,
new ContextUtil.Action(
"Settings",
() => {
this._opts_pDoOpenSettings();
},
),
]);
// Update the tab button on-click
tabMeta.buttons[0].pFnClick = evt => ContextUtil.pOpenMenu(evt, menuOthers);
const hkIsActive = () => {
const tab = this._getActiveTab();
tabMeta.$btns[0].toggleClass("active", !!tab.isHeadHidden);
};
this._addHookActiveTab(hkIsActive);
hkIsActive();
}
async _opts_pDoOpenSettings () {
const {$modalInner} = await UiUtil.pGetShowModal({title: "Settings"});
const $rowsCurrency = Parser.COIN_ABVS
.map(it => {
const {propIsAllowed} = this._getPropsCoins(it);
const $cb = ComponentUiUtil.$getCbBool(this, propIsAllowed);
return $$`
${Parser.coinAbvToFull(it).toTitleCase()}
${$cb}
`;
});
$$($modalInner)`
Allowed Currencies:
${$rowsCurrency}
`;
}
_render_output ({$wrp}) {
this._$wrpOutputRows = $(`
`);
$$`
Output
${this._$wrpOutputRows}
`
.appendTo($wrp);
}
_getPropsCoins (coin) {
return {
propIsAllowed: `isAllowCurrency${coin.uppercaseFirst()}`,
};
}
_getConvertedCoins (coins) {
if (!coins) return coins;
if (Parser.COIN_ABVS.every(it => this._state[this._getPropsCoins(it).propIsAllowed])) return coins;
if (Parser.COIN_ABVS.every(it => !this._state[this._getPropsCoins(it).propIsAllowed])) {
JqueryUtil.doToast({content: "All currencies are disabled! Generated currency has been discarded.", type: "warning"});
return {};
}
coins = MiscUtil.copy(coins);
let coinsRemoved = {};
Parser.COIN_ABVS
.forEach(it => {
const {propIsAllowed} = this._getPropsCoins(it);
if (this._state[propIsAllowed]) return;
if (!coins[it]) return;
coinsRemoved[it] = coins[it];
delete coins[it];
});
if (!Object.keys(coinsRemoved).length) return coins;
coinsRemoved = {cp: CurrencyUtil.getAsCopper(coinsRemoved)};
const conversionTableFiltered = MiscUtil.copy(Parser.FULL_CURRENCY_CONVERSION_TABLE)
.filter(({coin}) => this._state[this._getPropsCoins(coin).propIsAllowed]);
if (!conversionTableFiltered.some(it => it.isFallback)) conversionTableFiltered[0].isFallback = true;
// If we have filtered out copper, upgrade our copper amount to the nearest currency
if (!conversionTableFiltered.some(it => it.coin === "cp")) {
const conv = conversionTableFiltered[0];
coinsRemoved = {[conv.coin]: coinsRemoved.cp * conv.mult};
}
const coinsRemovedSimplified = CurrencyUtil.doSimplifyCoins(coinsRemoved, {currencyConversionTable: conversionTableFiltered});
Object.entries(coinsRemovedSimplified).forEach(([coin, count]) => {
if (!count) return;
coins[coin] = (coins[coin] || 0) + count;
});
return coins;
}
_doAddOutput ({lootOutput}) {
this._lootOutputs.push(lootOutput);
lootOutput.render(this._$wrpOutputRows);
}
_doClearOutput () {
this._lootOutputs.forEach(it => it.doRemove());
this._lootOutputs = [];
}
_getDefaultState () {
return {
...super._getDefaultState(),
// region Shared
pulseSpellsFiltered: false,
pulseItemsFiltered: false,
isAllowCurrencyCp: true,
isAllowCurrencySp: true,
isAllowCurrencyEp: true,
isAllowCurrencyGp: true,
isAllowCurrencyPp: true,
// endregion
// region Find Treasure
ft_challenge: 0,
ft_isHoard: false,
// endregion
// region Loot Tables
lt_ixTable: null,
// endregion
// region Party Loot
pl_isExactLevel: false,
pl_isCumulative: false,
pl_charLevel: 4,
pl_exactLevelMin: 1,
pl_exactLevelMax: 20,
pl_exactLevel: 1,
// endregion
// region Dragon hoard
dh_dragonAge: "Wyrmling",
dh_isPreferRandomMagicItems: false,
// endregion
// region Gems/Art Objects
gao_isUseGems: true,
gao_isUseArtObjects: true,
gao_targetGoldAmount: 100,
// endregion
};
}
}
globalThis.LootGenUi = LootGenUi;
class LootGenOutput {
static _TIERS = ["other", "minor", "major"];
constructor (
{
type,
name,
coins,
gems,
artObjects,
magicItemsByTable,
dragonMundaneItems,
},
) {
this._type = type;
this._name = name;
this._coins = coins;
this._gems = gems;
this._artObjects = artObjects;
this._magicItemsByTable = magicItemsByTable;
this._dragonMundaneItems = dragonMundaneItems;
this._datetimeGenerated = Date.now();
}
_$getEleTitleSplit () {
const $btnRivet = !IS_VTT && ExtensionUtil.ACTIVE
? $(` `)
.click(evt => this._pDoSendToFoundry({isTemp: !!evt.shiftKey}))
: null;
const $btnDownload = $(` `)
.click(() => this._pDoSaveAsJson());
return $$`
${$btnRivet}
${$btnDownload}
`;
}
render ($parent) {
const $eleTitleSplit = this._$getEleTitleSplit();
const $dispTitle = $$`
${Renderer.get().render(this._name)}
${$eleTitleSplit}
`;
const $parts = [
this._render_$getPtValueSummary(),
this._render_$getPtCoins(),
...this._render_$getPtGemsArtObjects({loot: this._gems, name: "gemstones"}),
...this._render_$getPtGemsArtObjects({loot: this._artObjects, name: "art objects"}),
this._render_$getPtDragonMundaneItems(),
...this._render_$getPtMagicItems(),
].filter(Boolean);
this._$wrp = $$`
${$dispTitle}
${$parts.length ? $$`
` : null}
${!$parts.length ? `
(No loot!)
` : null}
`
.prependTo($parent);
(IS_VTT ? this._$wrp : $dispTitle)
.attr("draggable", true)
.on("dragstart", evt => {
const meta = {
type: VeCt.DRAG_TYPE_LOOT,
data: dropData,
};
evt.originalEvent.dataTransfer.setData("application/json", JSON.stringify(meta));
});
// Preload the drop data in the background, to lessen the chance that the user drops the card before it has time
// to load.
let dropData;
this._pGetFoundryForm().then(it => dropData = it);
}
async _pDoSendToFoundry ({isTemp} = {}) {
const toSend = await this._pGetFoundryForm();
if (isTemp) toSend.isTemp = isTemp;
if (toSend.currency || toSend.entityInfos) return ExtensionUtil.pDoSend({type: "5etools.lootgen.loot", data: toSend});
JqueryUtil.doToast({content: `Nothing to send!`, type: "warning"});
}
async _pDoSaveAsJson () {
const serialized = await this._pGetFoundryForm();
await DataUtil.userDownload("loot", serialized);
}
async _pGetFoundryForm () {
const toSend = {name: this._name, type: this._type, dateTimeGenerated: this._datetimeGenerated};
if (this._coins) toSend.currency = this._coins;
const entityInfos = [];
if (this._gems?.length) entityInfos.push(...await this._pDoSendToFoundry_getGemsArtObjectsMetas({loot: this._gems}));
if (this._artObjects?.length) entityInfos.push(...await this._pDoSendToFoundry_getGemsArtObjectsMetas({loot: this._artObjects}));
if (this._magicItemsByTable?.length) {
for (const magicItemsByTable of this._magicItemsByTable) {
for (const lootItem of magicItemsByTable.breakdown) {
const exportMeta = lootItem.getExtensionExportMeta();
if (!exportMeta) continue;
const {page, entity, options} = exportMeta;
entityInfos.push({
page,
entity,
options,
});
}
}
}
if (this._dragonMundaneItems?.breakdown?.length) {
for (const str of this._dragonMundaneItems.breakdown) {
entityInfos.push({
page: UrlUtil.PG_ITEMS,
entity: {
name: Renderer.stripTags(str).uppercaseFirst(),
source: Parser.SRC_FTD,
type: "OTH",
rarity: "unknown",
},
});
}
}
if (entityInfos.length) toSend.entityInfos = entityInfos;
return toSend;
}
async _pDoSendToFoundry_getGemsArtObjectsMetas ({loot}) {
const uidToCount = {};
const specialItemMetas = {}; // For any rows which don't actually map to an item
loot.forEach(lt => {
Object.entries(lt.breakdown)
.forEach(([entry, count]) => {
let cntFound = 0;
entry.replace(/{@item ([^}]+)}/g, (...m) => {
cntFound++;
const [name, source] = m[1].toLowerCase().split("|").map(it => it.trim()).filter(Boolean);
const uid = `${name}|${source || Parser.SRC_DMG}`.toLowerCase();
uidToCount[uid] = (uidToCount[uid] || 0) + count;
return "";
});
if (cntFound) return;
// If we couldn't find any real items in this row, prepare a dummy item
const uidFaux = entry.toLowerCase().trim();
specialItemMetas[uidFaux] = specialItemMetas[uidFaux] || {
count: 0,
item: {
name: Renderer.stripTags(entry).uppercaseFirst(),
source: Parser.SRC_DMG,
type: "OTH",
rarity: "unknown",
},
};
specialItemMetas[uidFaux].count += count;
});
});
const out = [];
for (const [uid, count] of Object.entries(uidToCount)) {
const [name, source] = uid.split("|");
const item = await DataLoader.pCacheAndGet(UrlUtil.PG_ITEMS, source, UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({name, source}));
out.push({
page: UrlUtil.PG_ITEMS,
entity: item,
options: {
quantity: count,
},
});
}
for (const {count, item} of Object.values(specialItemMetas)) {
out.push({
page: UrlUtil.PG_ITEMS,
entity: item,
options: {
quantity: count,
},
});
}
return out;
}
_render_$getPtValueSummary () {
if ([this._coins, this._gems, this._artObjects].filter(Boolean).length <= 1) return null;
const totalValue = [
this._coins ? CurrencyUtil.getAsCopper(this._coins) : 0,
this._gems?.length ? this._gems.map(it => it.type * it.count * 100).sum() : 0,
this._artObjects?.length ? this._artObjects.map(it => it.type * it.count * 100).sum() : 0,
].sum();
return $(`A total of ${(totalValue / 100).toLocaleString()} gp worth of coins, art objects, and/or gems, as follows: `);
}
_render_$getPtCoins () {
if (!this._coins) return null;
const total = CurrencyUtil.getAsCopper(this._coins);
const breakdown = [...Parser.COIN_ABVS]
.reverse()
.filter(it => this._coins[it])
.map(it => `${this._coins[it].toLocaleString()} ${it}`);
return $$`
${(total / 100).toLocaleString()} gp in coinage:
${breakdown.map(it => `${it} `).join("")}
`;
}
_render_$getPtDragonMundaneItems () {
if (!this._dragonMundaneItems) return null;
return $$`
${this._dragonMundaneItems.count} mundane item${this._dragonMundaneItems.count !== 1 ? "s" : ""}:
${this._dragonMundaneItems.breakdown.map(it => `${it} `).join("")}
`;
}
_render_$getPtGemsArtObjects ({loot, name}) {
if (!loot?.length) return [];
return loot.map(lt => {
return $$`
${(lt.type).toLocaleString()} gp ${name} (×${lt.count}; worth ${((lt.type * lt.count)).toLocaleString()} gp total):
${Object.entries(lt.breakdown).map(([result, count]) => `${Renderer.get().render(result)}${count > 1 ? `, ×${count}` : ""} `).join("")}
`;
});
}
_render_$getPtMagicItems () {
if (!this._magicItemsByTable?.length) return [];
return [...this._magicItemsByTable]
.sort(({tier: tierA, type: typeA}, {tier: tierB, type: typeB}) => this.constructor._ascSortTier(tierB, tierA) || SortUtil.ascSortLower(typeA || "", typeB || ""))
.map(magicItems => {
// If we're in "tier" mode, sort the items into groups by rarity
if (magicItems.tier) {
const byRarity = {};
magicItems.breakdown
.forEach(lootItem => {
if (!lootItem.item) return;
const tgt = MiscUtil.getOrSet(byRarity, lootItem.item.rarity, []);
tgt.push(lootItem);
});
const $ulsByRarity = Object.entries(byRarity)
.sort(([rarityA], [rarityB]) => SortUtil.ascSortItemRarity(rarityB, rarityA))
.map(([rarity, lootItems]) => {
return $$`
${rarity.toTitleCase()} items (×${lootItems.length}):
${lootItems.map(it => it.$getRender())}
`;
});
if (!$ulsByRarity.length) return null;
return $$`
${magicItems.tier.toTitleCase()} items:
`;
}
return $$`
Magic Items${magicItems.type ? ` (${Renderer.get().render(`{@table Magic Item Table ${magicItems.type}||Table ${magicItems.type}}`)})` : ""}${(magicItems.count || 0) > 1 ? ` (×${magicItems.count})` : ""}
${magicItems.breakdown.map(it => it.$getRender())}
`;
});
}
doRemove () {
if (this._$wrp) this._$wrp.remove();
}
static _ascSortTier (a, b) { return LootGenOutput._TIERS.indexOf(a) - LootGenOutput._TIERS.indexOf(b); }
}
globalThis.LootGenOutput = LootGenOutput;
class LootGenOutputGemsArtObjects {
constructor (
{
type,
typeRoll,
typeTable,
count,
breakdown,
},
) {
this.type = type;
this.count = count;
// region Unused--potential for wiring up rerolls from `LootGenOutput` UI, if required
this.typeRoll = typeRoll;
this.typeTable = typeTable;
// endregion
this.breakdown = breakdown;
}
}
class LootGenOutputDragonMundaneItems {
constructor (
{
count,
breakdown,
},
) {
this.count = count;
this.breakdown = breakdown;
}
}
class LootGenOutputMagicItems {
constructor (
{
type,
count,
typeRoll,
typeTable,
breakdown,
tier,
},
) {
this.type = type;
this.count = count;
// region Unused--potential for wiring up rerolls from `LootGenOutput` UI, if required
this.typeRoll = typeRoll;
this.typeTable = typeTable;
// endregion
this.breakdown = breakdown;
this.tier = tier;
}
}
class LootGenMagicItem extends BaseComponent {
static async pGetMagicItemRoll (
{
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll = false,
fnGetIsPreferAltChoose = null,
},
) {
isItemsAltChooseRoll = isItemsAltChooseRoll && !!itemsAltChoose;
if (isItemsAltChooseRoll) {
const item = RollerUtil.rollOnArray(itemsAltChoose);
return this._pGetMagicItemRoll_singleItem({
item,
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
});
}
if (!magicItemTable?.table) {
return new LootGenMagicItemNull({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
});
}
const rowRoll = RollerUtil.randomise(magicItemTable.diceType ?? 100);
const row = magicItemTable.table.find(it => rowRoll >= it.min && rowRoll <= (it.max ?? it.min));
if (row.spellLevel != null) {
return new LootGenMagicItemSpellScroll({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: row.item,
item: await this._pGetMagicItemRoll_pGetItem({nameOrUid: row.item}),
roll: rowRoll,
spellLevel: row.spellLevel,
spell: RollerUtil.rollOnArray(spells.filter(it => it.level === row.spellLevel)),
});
}
if (row.choose?.fromGeneric) {
const subItems = (await row.choose.fromGeneric.pMap(nameOrUid => this._pGetMagicItemRoll_pGetItem({nameOrUid})))
.map(it => it.variants.map(({specificVariant}) => specificVariant))
.flat();
return new LootGenMagicItemSubItems({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: row.item ?? `{@item ${row.choose.fromGeneric[0]}}`,
item: RollerUtil.rollOnArray(subItems),
roll: rowRoll,
subItems,
});
}
if (row.choose?.fromGroup) {
const subItems = (await ((await row.choose.fromGroup.pMap(nameOrUid => this._pGetMagicItemRoll_pGetItem({nameOrUid})))
.pMap(it => it.items.pMap(x => this._pGetMagicItemRoll_pGetItem({nameOrUid: x})))))
.flat();
return new LootGenMagicItemSubItems({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: row.item ?? `{@item ${row.choose.fromGroup[0]}}`,
item: RollerUtil.rollOnArray(subItems),
roll: rowRoll,
subItems,
});
}
if (row.choose?.fromItems) {
const subItems = await row.choose?.fromItems.pMap(nameOrUid => this._pGetMagicItemRoll_pGetItem({nameOrUid}));
return new LootGenMagicItemSubItems({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: row.item,
item: RollerUtil.rollOnArray(subItems),
roll: rowRoll,
subItems,
});
}
if (row.table) {
const min = Math.min(...row.table.map(it => it.min));
const max = Math.max(...row.table.map(it => it.max ?? min));
const {subRowRoll, subRow, subItem} = await LootGenMagicItemTable.pGetSubRollMeta({
min,
max,
subTable: row.table,
});
return new LootGenMagicItemTable({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: row.item,
item: subItem,
roll: rowRoll,
table: row.table,
tableMinRoll: min,
tableMaxRoll: max,
tableEntry: subRow.item,
tableRoll: subRowRoll,
});
}
return this._pGetMagicItemRoll_singleItem({
item: await this._pGetMagicItemRoll_pGetItem({nameOrUid: row.item}),
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: row.item,
roll: rowRoll,
});
}
static async _pGetMagicItemRoll_singleItem (
{
item,
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll = false,
fnGetIsPreferAltChoose = null,
baseEntry,
roll,
},
) {
baseEntry = baseEntry || item
? `{@item ${item.name}|${item.source}}`
: `(no item) `;
if (item?.spellScrollLevel != null) {
return new LootGenMagicItemSpellScroll({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry,
item,
spellLevel: item.spellScrollLevel,
spell: RollerUtil.rollOnArray(spells.filter(it => it.level === item.spellScrollLevel)),
roll,
});
}
if (item?.variants?.length) {
const subItems = item.variants.map(({specificVariant}) => specificVariant);
return new LootGenMagicItemSubItems({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry: baseEntry,
item: RollerUtil.rollOnArray(subItems),
roll,
subItems,
});
}
return new LootGenMagicItem({
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry,
item,
roll,
});
}
static async _pGetMagicItemRoll_pGetItem ({nameOrUid}) {
nameOrUid = nameOrUid.replace(/{@item ([^}]+)}/g, (...m) => m[1]);
const uid = (nameOrUid.includes("|") ? nameOrUid : `${nameOrUid}|${Parser.SRC_DMG}`).toLowerCase();
const [name, source] = uid.split("|");
return DataLoader.pCacheAndGet(UrlUtil.PG_ITEMS, source, UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({name, source}));
}
/**
* @param lootGenMagicItems The parent array in which this item is stored.
* @param spells Spell data list.
* @param magicItemTable The table this result was rolled form.
* @param itemsAltChoose Item list from which alternate rolls can be made.
* @param itemsAltChooseDisplayText Summary display text for the alternate roll options.
* @param isItemsAltChooseRoll If this item was rolled by an alt-choose roll.
* @param fnGetIsPreferAltChoose Function to call when checking if this should default to the "alt choose" item set.
* @param baseEntry The text, which may be an item itself, supplied by the `"item"` property in the row.
* @param item The rolled item.
* @param roll The roll result used to get this row.
*/
constructor (
{
lootGenMagicItems,
spells,
magicItemTable,
itemsAltChoose,
itemsAltChooseDisplayText,
isItemsAltChooseRoll,
fnGetIsPreferAltChoose,
baseEntry,
item,
roll,
},
) {
super();
this._lootGenMagicItems = lootGenMagicItems;
this._spells = spells;
this._magicItemTable = magicItemTable;
this._itemsAltChoose = itemsAltChoose;
this._itemsAltChooseDisplayText = itemsAltChooseDisplayText;
this._fnGetIsPreferAltChoose = fnGetIsPreferAltChoose;
this._state.baseEntry = baseEntry;
this._state.item = item;
this._state.roll = roll;
this._state.isItemsAltChooseRoll = isItemsAltChooseRoll;
this._$render = null;
}
get item () { return this._state.item; }
getExtensionExportMeta () {
return {
page: UrlUtil.PG_ITEMS,
entity: this._state.item,
};
}
async _pDoReroll ({isAltRoll = false} = {}) {
const nxt = await this.constructor.pGetMagicItemRoll({
lootGenMagicItems: this._lootGenMagicItems,
spells: this._spells,
magicItemTable: this._magicItemTable,
itemsAltChoose: this._itemsAltChoose,
itemsAltChooseDisplayText: this._itemsAltChooseDisplayText,
isItemsAltChooseRoll: isAltRoll,
fnGetIsPreferAltChoose: this._fnGetIsPreferAltChoose,
});
this._lootGenMagicItems.splice(this._lootGenMagicItems.indexOf(this), 1, nxt);
if (!this._$render) return;
this._$render.replaceWith(nxt.$getRender());
}
_$getBtnReroll () {
if (!this._magicItemTable && !this._itemsAltChoose) return null;
const isAltModeDefault = this._fnGetIsPreferAltChoose && this._fnGetIsPreferAltChoose();
const title = this._itemsAltChoose
? isAltModeDefault ? `SHIFT to roll on Magic Item Table ${this._magicItemTable.type}` : `SHIFT to roll ${Parser.getArticle(this._itemsAltChooseDisplayText)} ${this._itemsAltChooseDisplayText} item`
: null;
return $(`[reroll] `)
.mousedown(evt => evt.preventDefault())
.click(evt => this._pDoReroll({isAltRoll: isAltModeDefault ? !evt.shiftKey : evt.shiftKey}));
}
$getRender () {
if (this._$render) return this._$render;
return this._$render = this._$getRender();
}
_$getRender () {
const $dispBaseEntry = this._$getRender_$getDispBaseEntry();
const $dispRoll = this._$getRender_$getDispRoll();
const $btnReroll = this._$getBtnReroll();
return $$`
${$dispBaseEntry}
${$dispRoll}
${$btnReroll}
`;
}
_$getRender_$getDispBaseEntry ({prop = "baseEntry"} = {}) {
const $dispBaseEntry = $(`
`);
const hkBaseEntry = () => $dispBaseEntry.html(Renderer.get().render(this._state.isItemsAltChooseRoll ? `{@i ${this._state[prop]}}` : this._state[prop]));
this._addHookBase(prop, hkBaseEntry);
hkBaseEntry();
return $dispBaseEntry;
}
_$getRender_$getDispRoll ({prop = "roll"} = {}) {
const $dispRoll = $(`
`);
const hkRoll = () => $dispRoll.text(this._state.isItemsAltChooseRoll ? `(${this._itemsAltChooseDisplayText} item)` : `(Rolled ${this._state[prop]})`);
this._addHookBase(prop, hkRoll);
hkRoll();
return $dispRoll;
}
_getDefaultState () {
return {
...super._getDefaultState(),
baseEntry: null,
item: null,
roll: null,
isItemsAltChooseRoll: false,
};
}
}
class LootGenMagicItemNull extends LootGenMagicItem {
static TOOLTIP_NOTHING = `Failed to generate a result! This is normally due to all potential matches being filtered out. You may want to adjust your filters to be more permissive.`;
getExtensionExportMeta () { return null; }
_$getRender () {
return $$`
—
`;
}
}
class LootGenMagicItemSpellScroll extends LootGenMagicItem {
constructor (
{
spellLevel,
spell,
...others
},
) {
super(others);
this._state.spellLevel = spellLevel;
this._state.spell = spell;
}
getExtensionExportMeta () {
if (this._state.spell == null) return null;
return {
page: UrlUtil.PG_SPELLS,
entity: this._state.spell,
options: {
isSpellScroll: true,
},
};
}
_$getRender () {
const $dispBaseEntry = this._$getRender_$getDispBaseEntry();
const $dispRoll = this._$getRender_$getDispRoll();
const $btnRerollSpell = $(`[reroll] `)
.mousedown(evt => evt.preventDefault())
.click(() => {
this._state.spell = RollerUtil.rollOnArray(this._spells.filter(it => it.level === this._state.spellLevel));
});
const $dispSpell = $(`
`);
const hkSpell = () => {
if (!this._state.spell) return $dispSpell.html(`(no spell) `);
$dispSpell.html(Renderer.get().render(`{@spell ${this._state.spell.name}|${this._state.spell.source}}`));
};
this._addHookBase("spell", hkSpell);
hkSpell();
const $btnReroll = this._$getBtnReroll();
return $$`
${$dispBaseEntry}
(
${$btnRerollSpell}
${$dispSpell}
-or-
${Renderer.get().render(`{@filter see all ${Parser.spLevelToFullLevelText(this._state.spellLevel, {isDash: true})} spells|spells|level=${this._state.spellLevel}}`)}
)
${$dispRoll}
${$btnReroll}
`;
}
_getDefaultState () {
return {
...super._getDefaultState(),
spellLevel: null,
spell: null,
};
}
}
class LootGenMagicItemSubItems extends LootGenMagicItem {
constructor (
{
subItems,
...others
},
) {
super(others);
this._subItems = subItems;
}
_$getRender () {
const $dispBaseEntry = this._$getRender_$getDispBaseEntry();
const $dispRoll = this._$getRender_$getDispRoll();
const $btnRerollSubItem = $(`[reroll] `)
.mousedown(evt => evt.preventDefault())
.click(() => {
this._state.item = RollerUtil.rollOnArray(this._subItems);
});
const $dispSubItem = $(`
`);
const hkItem = () => $dispSubItem.html(Renderer.get().render(`{@item ${this._state.item.name}|${this._state.item.source}}`));
this._addHookBase("item", hkItem);
hkItem();
const $btnReroll = this._$getBtnReroll();
return $$`
${$dispBaseEntry}
(
${$btnRerollSubItem}
${$dispSubItem}
)
${$dispRoll}
${$btnReroll}
`;
}
}
class LootGenMagicItemTable extends LootGenMagicItem {
static async pGetSubRollMeta ({min, max, subTable}) {
const subRowRoll = RollerUtil.randomise(max, min);
const subRow = subTable.find(it => subRowRoll >= it.min && subRowRoll <= (it.max ?? it.min));
return {
subRowRoll,
subRow,
subItem: await this._pGetMagicItemRoll_pGetItem({nameOrUid: subRow.item}),
};
}
constructor (
{
table,
tableMinRoll,
tableMaxRoll,
tableEntry,
tableRoll,
...others
},
) {
super(others);
this._table = table;
this._tableMinRoll = tableMinRoll;
this._tableMaxRoll = tableMaxRoll;
this._state.tableEntry = tableEntry;
this._state.tableRoll = tableRoll;
}
_$getRender () {
const $dispBaseEntry = this._$getRender_$getDispBaseEntry();
const $dispRoll = this._$getRender_$getDispRoll();
const $dispTableEntry = this._$getRender_$getDispBaseEntry({prop: "tableEntry"});
const $dispTableRoll = this._$getRender_$getDispRoll({prop: "tableRoll"});
const $btnReroll = this._$getBtnReroll();
const $btnRerollSub = $(`[reroll] `)
.mousedown(evt => evt.preventDefault())
.click(async () => {
const {subRowRoll, subRow, subItem} = await LootGenMagicItemTable.pGetSubRollMeta({
min: this._tableMinRoll,
max: this._tableMaxRoll,
subTable: this._table,
});
this._state.item = subItem;
this._state.tableEntry = subRow.item;
this._state.tableRoll = subRowRoll;
});
return $$`
${$dispBaseEntry}
${$dispRoll}
${$btnReroll}
→
${$dispTableEntry}
${$dispTableRoll}
${$btnRerollSub}
`;
}
}