mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2025-10-28 20:45:35 -05:00
v1.198.1
This commit is contained in:
39
js/bestiary/bestiary-encounterbuilder-cache.js
Normal file
39
js/bestiary/bestiary-encounterbuilder-cache.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import {EncounterBuilderCacheBase} from "../encounterbuilder/encounterbuilder-cache.js";
|
||||
|
||||
export class EncounterBuilderCacheBestiaryPage extends EncounterBuilderCacheBase {
|
||||
_cache = null;
|
||||
|
||||
constructor ({bestiaryPage}) {
|
||||
super();
|
||||
this._bestiaryPage = bestiaryPage;
|
||||
}
|
||||
|
||||
_build () {
|
||||
if (this._cache != null) return;
|
||||
// create a map of {XP: [monster list]}
|
||||
this._cache = this._getBuiltCache();
|
||||
}
|
||||
|
||||
_getBuiltCache () {
|
||||
const out = {};
|
||||
this._bestiaryPage.list_.visibleItems
|
||||
.map(li => this._bestiaryPage.dataList_[li.ix])
|
||||
.filter(mon => !this._isUnwantedCreature(mon))
|
||||
.forEach(mon => {
|
||||
(out[Parser.crToXpNumber(mon.cr)] ||= []).push(mon);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
reset () { this._cache = null; }
|
||||
|
||||
getCreaturesByXp (xp) {
|
||||
this._build();
|
||||
return this._cache[xp] || [];
|
||||
}
|
||||
|
||||
getXpKeys () {
|
||||
this._build();
|
||||
return Object.keys(this._cache).map(it => Number(it));
|
||||
}
|
||||
}
|
||||
37
js/bestiary/bestiary-encounterbuilder-component.js
Normal file
37
js/bestiary/bestiary-encounterbuilder-component.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import {EncounterBuilderComponent} from "../encounterbuilder/encounterbuilder-component.js";
|
||||
|
||||
export class EncounterBuilderComponentBestiary extends EncounterBuilderComponent {
|
||||
getSublistPluginState () {
|
||||
return {
|
||||
// region Special handling for `creatureMetas`
|
||||
items: this._state.creatureMetas
|
||||
.map(creatureMeta => ({
|
||||
h: creatureMeta.getHash(),
|
||||
c: creatureMeta.count,
|
||||
customHashId: creatureMeta.customHashId || undefined,
|
||||
l: creatureMeta.isLocked,
|
||||
})),
|
||||
sources: this._state.creatureMetas
|
||||
.map(creatureMeta => creatureMeta.creature.source)
|
||||
.unique(),
|
||||
// endregion
|
||||
|
||||
...Object.fromEntries(
|
||||
Object.entries(this._state)
|
||||
.filter(([k]) => k !== "creatureMetas")
|
||||
.map(([k, v]) => [k, MiscUtil.copyFast(v)]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** Get a generic representation of the encounter, which can be used elsewhere. */
|
||||
static getStateFromExportedSublist ({exportedSublist}) {
|
||||
exportedSublist = MiscUtil.copyFast(exportedSublist);
|
||||
|
||||
const out = this._getDefaultState();
|
||||
Object.keys(out)
|
||||
.filter(k => exportedSublist[k] != null)
|
||||
.forEach(k => out[k] = exportedSublist[k]);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
305
js/bestiary/bestiary-encounterbuilder-sublistplugin.js
Normal file
305
js/bestiary/bestiary-encounterbuilder-sublistplugin.js
Normal file
@@ -0,0 +1,305 @@
|
||||
import {EncounterBuilderHelpers} from "../utils-list-bestiary.js";
|
||||
import {EncounterBuilderCreatureMeta} from "../encounterbuilder/encounterbuilder-models.js";
|
||||
import {EncounterBuilderComponentBestiary} from "./bestiary-encounterbuilder-component.js";
|
||||
|
||||
/**
|
||||
* Serialize/deserialize state from the encounter builder.
|
||||
*/
|
||||
export class EncounterBuilderSublistPlugin extends SublistPlugin {
|
||||
constructor ({sublistManager, encounterBuilder, encounterBuilderComp}) {
|
||||
super();
|
||||
this._sublistManager = sublistManager;
|
||||
this._encounterBuilder = encounterBuilder;
|
||||
this._encounterBuilderComp = encounterBuilderComp;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pLoadData ({exportedSublist, isMemoryOnly}) {
|
||||
const nxt = {};
|
||||
|
||||
// Allow URLified versions of keys
|
||||
const keyLookup = this._encounterBuilderComp.getDefaultStateKeys()
|
||||
.mergeMap(k => ({[k]: k, [k.toUrlified()]: k}));
|
||||
|
||||
if (exportedSublist) {
|
||||
Object.entries(exportedSublist)
|
||||
.filter(([, v]) => v != null)
|
||||
.map(([k, v]) => {
|
||||
// Only add specific keys, as we do not want to track e.g. sublist state
|
||||
k = keyLookup[k];
|
||||
if (!k) return null;
|
||||
|
||||
return [k, v];
|
||||
})
|
||||
.filter(Boolean)
|
||||
// Always process `colsExtraAdvanced` first (if available), as used in `playersAdvanced`
|
||||
.sort(([kA], [kB]) => kA === "colsExtraAdvanced" ? -1 : kB === "colsExtraAdvanced" ? 1 : 0)
|
||||
.forEach(([k, v]) => {
|
||||
if (isMemoryOnly) return nxt[k] = MiscUtil.copyFast(v);
|
||||
|
||||
// When loading from non-memory sources, expand the data
|
||||
switch (k) {
|
||||
case "playersSimple": return nxt[k] = v.map(it => EncounterBuilderComponentBestiary.getDefaultPlayerRow_simple(it));
|
||||
case "colsExtraAdvanced": return nxt[k] = v.map(it => EncounterBuilderComponentBestiary.getDefaultColExtraAdvanced(it));
|
||||
case "playersAdvanced": return nxt[k] = v.map(it => EncounterBuilderComponentBestiary.getDefaultPlayerRow_advanced({
|
||||
...it,
|
||||
extras: it.extras.map(x => EncounterBuilderComponentBestiary.getDefaultPlayerAdvancedExtra(x)),
|
||||
colsExtraAdvanced: nxt.colsExtraAdvanced || this._encounterBuilderComp.colsExtraAdvanced,
|
||||
}));
|
||||
|
||||
default: return nxt[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
if (nxt.playersSimple) {
|
||||
nxt.playersSimple
|
||||
.forEach(wrapped => {
|
||||
wrapped.entity.count = wrapped.entity.count || 1;
|
||||
wrapped.entity.level = wrapped.entity.level || 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (nxt.playersAdvanced) {
|
||||
nxt.playersAdvanced
|
||||
.forEach(wrapped => {
|
||||
wrapped.entity.name = wrapped.entity.name || "";
|
||||
wrapped.entity.level = wrapped.entity.level || 1;
|
||||
wrapped.entity.extraCols = wrapped.entity.extraCols
|
||||
|| (nxt.colsExtraAdvanced || this._encounterBuilderComp.colsExtraAdvanced.map(() => ""));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we do not set `creatureMetas` here, as `onSublistUpdate` handles this
|
||||
this._encounterBuilderComp.setStateFromLoaded(nxt);
|
||||
}
|
||||
|
||||
async _pLoadData_getCreatureMetas ({exportedSublist}) {
|
||||
if (!exportedSublist.items?.length) return [];
|
||||
|
||||
return exportedSublist.items
|
||||
.pSerialAwaitMap(async serialItem => {
|
||||
const {
|
||||
entity,
|
||||
entityBase,
|
||||
count,
|
||||
isLocked,
|
||||
customHashId,
|
||||
} = await SublistManager.pDeserializeExportedSublistItem(serialItem);
|
||||
|
||||
return new EncounterBuilderCreatureMeta({
|
||||
creature: entity,
|
||||
count,
|
||||
|
||||
isLocked,
|
||||
|
||||
customHashId: customHashId,
|
||||
baseCreature: entityBase,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static async pMutLegacyData ({exportedSublist, isMemoryOnly}) {
|
||||
if (!exportedSublist) return;
|
||||
|
||||
// region Legacy Bestiary Encounter Builder format
|
||||
if (exportedSublist.p) {
|
||||
exportedSublist.playersSimple = exportedSublist.p.map(it => EncounterBuilderComponentBestiary.getDefaultPlayerRow_simple(it));
|
||||
if (!isMemoryOnly) this._mutExternalize({obj: exportedSublist, k: "playersSimple"});
|
||||
delete exportedSublist.p;
|
||||
}
|
||||
|
||||
if (exportedSublist.l) {
|
||||
Object.assign(exportedSublist, exportedSublist.l);
|
||||
delete exportedSublist.l;
|
||||
}
|
||||
|
||||
if (exportedSublist.a != null) {
|
||||
exportedSublist.isAdvanced = !!exportedSublist.a;
|
||||
delete exportedSublist.a;
|
||||
}
|
||||
|
||||
if (exportedSublist.c) {
|
||||
exportedSublist.colsExtraAdvanced = exportedSublist.c.map(name => EncounterBuilderComponentBestiary.getDefaultColExtraAdvanced({name}));
|
||||
if (!isMemoryOnly) this._mutExternalize({obj: exportedSublist, k: "colsExtraAdvanced"});
|
||||
delete exportedSublist.c;
|
||||
}
|
||||
|
||||
if (exportedSublist.d) {
|
||||
exportedSublist.playersAdvanced = exportedSublist.d.map(({n, l, x}) => EncounterBuilderComponentBestiary.getDefaultPlayerRow_advanced({
|
||||
name: n,
|
||||
level: l,
|
||||
extras: x.map(value => EncounterBuilderComponentBestiary.getDefaultPlayerAdvancedExtra({value})),
|
||||
colsExtraAdvanced: exportedSublist.colsExtraAdvanced,
|
||||
}));
|
||||
if (!isMemoryOnly) this._mutExternalize({obj: exportedSublist, k: "playersAdvanced"});
|
||||
delete exportedSublist.d;
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Legacy "reference" format
|
||||
// These are general save manager properties, but we set them here, as encounter data was the only thing to make
|
||||
// use of this system.
|
||||
if (exportedSublist.bestiaryId) {
|
||||
exportedSublist.saveId = exportedSublist.bestiaryId;
|
||||
delete exportedSublist.bestiaryId;
|
||||
}
|
||||
|
||||
if (exportedSublist.isRef) {
|
||||
exportedSublist.managerClient_isReferencable = true;
|
||||
exportedSublist.managerClient_isLoadAsCopy = false;
|
||||
}
|
||||
delete exportedSublist.isRef;
|
||||
// endregion
|
||||
}
|
||||
|
||||
async pMutLegacyData ({exportedSublist, isMemoryOnly}) {
|
||||
await this.constructor.pMutLegacyData({exportedSublist, isMemoryOnly});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pMutSaveableData ({exportedSublist, isForce = false, isMemoryOnly = false}) {
|
||||
if (!isForce && !this._encounterBuilder.isActive()) return;
|
||||
|
||||
[
|
||||
"playersSimple",
|
||||
"isAdvanced",
|
||||
"colsExtraAdvanced",
|
||||
"playersAdvanced",
|
||||
].forEach(k => {
|
||||
exportedSublist[k] = MiscUtil.copyFast(this._encounterBuilderComp[k]);
|
||||
|
||||
if (isMemoryOnly) return;
|
||||
|
||||
this.constructor._mutExternalize({obj: exportedSublist, k});
|
||||
});
|
||||
}
|
||||
|
||||
static _WALKER_EXTERNALIZE = null;
|
||||
static _HANDLERS_EXTERNALIZE = {
|
||||
array: (arr) => {
|
||||
if (arr.some(it => !it.id || !it.entity)) return arr;
|
||||
return arr.map(({entity}) => entity);
|
||||
},
|
||||
};
|
||||
static _mutExternalize ({obj, k}) {
|
||||
this._WALKER_EXTERNALIZE = this._WALKER_EXTERNALIZE || MiscUtil.getWalker();
|
||||
|
||||
obj[k] = this._WALKER_EXTERNALIZE.walk(
|
||||
obj[k],
|
||||
this._HANDLERS_EXTERNALIZE,
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async pDoInitNewState ({prevExportableSublist, evt}) {
|
||||
// If SHIFT pressed, reset players
|
||||
const nxt = {
|
||||
playersSimple: evt.shiftKey ? [] : MiscUtil.copyFast(prevExportableSublist.playersSimple),
|
||||
playersAdvanced: evt.shiftKey ? [] : MiscUtil.copyFast(prevExportableSublist.playersAdvanced),
|
||||
};
|
||||
|
||||
this._encounterBuilderComp.setPartialStateFromLoaded(nxt);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
getDownloadName () {
|
||||
if (!this._encounterBuilder.isActive()) return null;
|
||||
return "encounter";
|
||||
}
|
||||
|
||||
getDownloadFileType () {
|
||||
if (!this._encounterBuilder.isActive()) return null;
|
||||
return "encounter";
|
||||
}
|
||||
|
||||
getUploadFileTypes ({downloadFileTypeBase}) {
|
||||
if (!this._encounterBuilder.isActive()) return null;
|
||||
return [this.getDownloadFileType(), downloadFileTypeBase];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
onSublistUpdate () {
|
||||
this._encounterBuilder.withSublistSyncSuppressed(() => {
|
||||
// Note that we only update `creatureMetas` here, as this only triggers on direct updates to the underlying sublist.
|
||||
// For everything else, the `pLoadData` path is used.
|
||||
this._encounterBuilderComp.creatureMetas = this._sublistManager.sublistItems
|
||||
.map(sublistItem => EncounterBuilderHelpers.getSublistedCreatureMeta({sublistItem}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class EncounterBuilderLegacyStorageMigration {
|
||||
static _VERSION = 2;
|
||||
|
||||
static _STORAGE_KEY_LEGACY_SAVED_ENCOUNTERS = "ENCOUNTER_SAVED_STORAGE";
|
||||
static _STORAGE_KEY_LEGACY_ENCOUNTER = "ENCOUNTER_STORAGE";
|
||||
|
||||
static _STORAGE_KEY_LEGACY_ENCOUNTER_MIGRATION_VERSION = "ENCOUNTER_STORAGE_MIGRATION_VERSION";
|
||||
static _STORAGE_KEY_LEGACY_SAVED_ENCOUNTER_MIGRATION_VERSION = "ENCOUNTER_SAVED_STORAGE_MIGRATION_VERSION";
|
||||
|
||||
static register () {
|
||||
SublistPersistor._LEGACY_MIGRATOR.registerLegacyMigration(this._pMigrateSublist.bind(this));
|
||||
SaveManager._LEGACY_MIGRATOR.registerLegacyMigration(this._pMigrateSaves.bind(this));
|
||||
}
|
||||
|
||||
static async _pMigrateSublist (stored) {
|
||||
let version = await StorageUtil.pGet(this._STORAGE_KEY_LEGACY_ENCOUNTER_MIGRATION_VERSION);
|
||||
if (version && version >= 2) return false;
|
||||
if (!version) version = 1;
|
||||
|
||||
const encounter = await StorageUtil.pGet(this._STORAGE_KEY_LEGACY_ENCOUNTER);
|
||||
if (!encounter) return false;
|
||||
|
||||
Object.entries(encounter)
|
||||
.forEach(([k, v]) => {
|
||||
if (stored[k] != null) return;
|
||||
stored[k] = v;
|
||||
});
|
||||
|
||||
await EncounterBuilderSublistPlugin.pMutLegacyData({exportedSublist: stored});
|
||||
|
||||
await StorageUtil.pSet(this._STORAGE_KEY_LEGACY_ENCOUNTER_MIGRATION_VERSION, this._VERSION);
|
||||
|
||||
JqueryUtil.doToast(`Migrated active Bestiary encounter from version ${version} to version ${this._VERSION}!`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static async _pMigrateSaves (stored) {
|
||||
let version = await StorageUtil.pGet(this._STORAGE_KEY_LEGACY_SAVED_ENCOUNTER_MIGRATION_VERSION);
|
||||
if (version && version >= 2) return false;
|
||||
if (!version) version = 1;
|
||||
|
||||
const encounters = await StorageUtil.pGet(this._STORAGE_KEY_LEGACY_SAVED_ENCOUNTERS);
|
||||
if (!encounters) return false;
|
||||
|
||||
await Object.entries(encounters.savedEncounters || {})
|
||||
.pSerialAwaitMap(async ([id, enc]) => {
|
||||
const legacyData = MiscUtil.copyFast(enc.data || {});
|
||||
legacyData.name = enc.name || "(Unnamed encounter)";
|
||||
legacyData.saveId = id;
|
||||
legacyData.manager_isSaved = true;
|
||||
await EncounterBuilderSublistPlugin.pMutLegacyData({exportedSublist: legacyData});
|
||||
|
||||
const tgt = MiscUtil.getOrSet(stored, "state", "saves", []);
|
||||
tgt.push({
|
||||
id: CryptUtil.uid(),
|
||||
entity: legacyData,
|
||||
});
|
||||
});
|
||||
|
||||
await StorageUtil.pSet(this._STORAGE_KEY_LEGACY_SAVED_ENCOUNTER_MIGRATION_VERSION, this._VERSION);
|
||||
|
||||
JqueryUtil.doToast(`Migrated saved Bestiary encounters from version ${version} to version ${this._VERSION}!`);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
EncounterBuilderLegacyStorageMigration.register();
|
||||
363
js/bestiary/bestiary-encounterbuilder-ui.js
Normal file
363
js/bestiary/bestiary-encounterbuilder-ui.js
Normal file
@@ -0,0 +1,363 @@
|
||||
import {EncounterBuilderUi} from "../encounterbuilder/encounterbuilder-ui.js";
|
||||
import {EncounterBuilderCreatureMeta} from "../encounterbuilder/encounterbuilder-models.js";
|
||||
import {EncounterBuilderHelpers} from "../utils-list-bestiary.js";
|
||||
|
||||
export class EncounterBuilderUiBestiary extends EncounterBuilderUi {
|
||||
static _HASH_KEY = "encounterbuilder";
|
||||
|
||||
_isSuspendSyncToSublist = false;
|
||||
|
||||
constructor ({cache, comp, bestiaryPage, sublistManager}) {
|
||||
super({cache, comp});
|
||||
|
||||
this._bestiaryPage = bestiaryPage;
|
||||
this._sublistManager = sublistManager;
|
||||
|
||||
this._lock = new VeLock();
|
||||
|
||||
this._cachedTitle = null;
|
||||
}
|
||||
|
||||
initUi () {
|
||||
document.getElementById("stat-tabs").classList.add("best-ecgen__hidden");
|
||||
document.getElementById("float-token").classList.add("best-ecgen__hidden");
|
||||
document.getElementById("wrp-pagecontent").classList.add("best-ecgen__hidden");
|
||||
|
||||
$(`#btn-encounterbuild`).click(() => Hist.setSubhash(this.constructor._HASH_KEY, true));
|
||||
}
|
||||
|
||||
render () {
|
||||
super.render({
|
||||
$parentRandomAndAdjust: $("#wrp-encounterbuild-random-and-adjust"),
|
||||
$parentGroupAndDifficulty: $("#wrp-encounterbuild-group-and-difficulty"),
|
||||
});
|
||||
this._render_saveLoad();
|
||||
}
|
||||
|
||||
_render_saveLoad () {
|
||||
const $btnSave = $(`<button class="btn btn-default btn-xs">Save Encounter</button>`)
|
||||
.click(evt => this._sublistManager.pHandleClick_save(evt));
|
||||
|
||||
const $btnLoad = $(`<button class="btn btn-default btn-xs">Load Encounter</button>`)
|
||||
.click(evt => this._sublistManager.pHandleClick_load(evt));
|
||||
|
||||
$$(document.getElementById("best-ecgen__wrp-save-controls"))`<div class="ve-flex-col">
|
||||
<div class="ve-flex-h-right btn-group">
|
||||
${$btnSave}
|
||||
${$btnLoad}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_handleClickCopyAsText (evt) {
|
||||
let xpTotal = 0;
|
||||
const ptsCreature = this._sublistManager.sublistItems
|
||||
.sort((a, b) => SortUtil.ascSortLower(a.name, b.name))
|
||||
.map(it => {
|
||||
xpTotal += Parser.crToXpNumber(it.values.cr) * it.data.count;
|
||||
return `${it.data.count}× ${it.name}`;
|
||||
});
|
||||
const ptXp = `${xpTotal.toLocaleString()} XP`;
|
||||
|
||||
if (evt.shiftKey) {
|
||||
MiscUtil.pCopyTextToClipboard([...ptsCreature, ptXp].join("\n")).then(null);
|
||||
} else {
|
||||
MiscUtil.pCopyTextToClipboard(`${ptsCreature.join(", ")} (${ptXp})`).then(null);
|
||||
}
|
||||
JqueryUtil.showCopiedEffect(evt.currentTarget);
|
||||
}
|
||||
|
||||
_handleClickBackToStatblocks () {
|
||||
Hist.setSubhash(this.constructor._HASH_KEY, null);
|
||||
}
|
||||
|
||||
_render_groupAndDifficulty ({rdState, $parentGroupAndDifficulty}) {
|
||||
super._render_groupAndDifficulty({rdState, $parentGroupAndDifficulty});
|
||||
|
||||
const $btnSaveToUrl = $(`<button class="btn btn-default btn-xs mr-2">Save to URL</button>`)
|
||||
.click(() => this._sublistManager.pHandleClick_download({isUrl: true, $eleCopyEffect: $btnSaveToUrl}));
|
||||
const $btnSaveToFile = $(`<button class="btn btn-default btn-xs">Save to File</button>`)
|
||||
.click(() => this._sublistManager.pHandleClick_download());
|
||||
const $btnLoadFromFile = $(`<button class="btn btn-default btn-xs">Load from File</button>`)
|
||||
.click(evt => this._sublistManager.pHandleClick_upload({isAdditive: evt.shiftKey}));
|
||||
const $btnCopyAsText = $(`<button class="btn btn-default btn-xs mr-2" title="SHIFT for Multi-Line Format">Copy as Text</button>`).click((evt) => this._handleClickCopyAsText(evt));
|
||||
const $btnReset = $(`<button class="btn btn-danger btn-xs" title="SHIFT to Reset Players">Reset</button>`)
|
||||
.click((evt) => this._sublistManager.pHandleClick_new(evt));
|
||||
|
||||
const $btnBackToStatblocks = $(`<button class="btn btn-success btn-xs">Back to Stat Blocks</button>`).click((evt) => this._handleClickBackToStatblocks(evt));
|
||||
|
||||
$$`<div class="ve-flex-col w-100">
|
||||
<hr class="hr-1">
|
||||
|
||||
<div class="ve-flex-v-center mb-2">
|
||||
${$btnSaveToUrl}
|
||||
<div class="btn-group ve-flex-v-center mr-2">
|
||||
${$btnSaveToFile}
|
||||
${$btnLoadFromFile}
|
||||
</div>
|
||||
${$btnCopyAsText}
|
||||
${$btnReset}
|
||||
</div>
|
||||
|
||||
<div class="ve-flex">
|
||||
${$btnBackToStatblocks}
|
||||
</div>
|
||||
</div>`
|
||||
.appendTo($parentGroupAndDifficulty);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
withSublistSyncSuppressed (fn) {
|
||||
try {
|
||||
this._isSuspendSyncToSublist = true;
|
||||
fn();
|
||||
} finally {
|
||||
this._isSuspendSyncToSublist = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On encounter builder state change, save to the sublist
|
||||
*/
|
||||
_render_hk_doUpdateExternalStates () {
|
||||
if (this._isSuspendSyncToSublist) return;
|
||||
this._render_hk_pDoUpdateExternalStates().then(null);
|
||||
}
|
||||
|
||||
async _render_hk_pDoUpdateExternalStates () {
|
||||
try {
|
||||
await this._lock.pLock();
|
||||
await this._render_hk_pDoUpdateExternalStates_();
|
||||
} finally {
|
||||
this._lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
async _render_hk_pDoUpdateExternalStates_ () {
|
||||
const nxtState = await this._sublistManager.pGetExportableSublist({isMemoryOnly: true});
|
||||
Object.assign(nxtState, this._comp.getSublistPluginState());
|
||||
await this._sublistManager.pDoLoadExportedSublist(nxtState, {isMemoryOnly: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
onSublistChange ({$dispCrTotal}) {
|
||||
const encounterXpInfo = EncounterBuilderCreatureMeta.getEncounterXpInfo(this._comp.creatureMetas, this._getPartyMeta());
|
||||
|
||||
const monCount = this._sublistManager.sublistItems.map(it => it.data.count).sum();
|
||||
$dispCrTotal.html(`${monCount} creature${monCount === 1 ? "" : "s"}; ${encounterXpInfo.baseXp.toLocaleString()} XP (<span class="help" title="Adjusted Encounter XP">Enc</span>: ${(encounterXpInfo.adjustedXp).toLocaleString()} XP)`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
resetCache () { this._cache.reset(); }
|
||||
|
||||
isActive () {
|
||||
return Hist.getSubHash(this.constructor._HASH_KEY) === "true";
|
||||
}
|
||||
|
||||
_showBuilder () {
|
||||
this._cachedTitle = this._cachedTitle || document.title;
|
||||
document.title = "Encounter Builder - 5etools";
|
||||
$(document.body).addClass("best__ecgen-active");
|
||||
this._bestiaryPage.doDeselectAll();
|
||||
this._sublistManager.doSublistDeselectAll();
|
||||
}
|
||||
|
||||
_hideBuilder () {
|
||||
if (this._cachedTitle) {
|
||||
document.title = this._cachedTitle;
|
||||
this._cachedTitle = null;
|
||||
}
|
||||
$(document.body).removeClass("best__ecgen-active");
|
||||
}
|
||||
|
||||
_handleClick ({evt, mode, entity}) {
|
||||
if (mode === "add") {
|
||||
return this._sublistManager.pDoSublistAdd({entity, doFinalize: true, addCount: evt.shiftKey ? 5 : 1});
|
||||
}
|
||||
|
||||
return this._sublistManager.pDoSublistSubtract({entity, subtractCount: evt.shiftKey ? 5 : 1});
|
||||
}
|
||||
|
||||
async _pHandleShuffleClick ({evt, sublistItem}) {
|
||||
const creatureMeta = EncounterBuilderHelpers.getSublistedCreatureMeta({sublistItem});
|
||||
this._doShuffle({creatureMeta});
|
||||
}
|
||||
|
||||
handleSubhash () {
|
||||
if (Hist.getSubHash(this.constructor._HASH_KEY) === "true") this._showBuilder();
|
||||
else this._hideBuilder();
|
||||
}
|
||||
|
||||
async doStatblockMouseOver ({evt, ele, source, hash, customHashId}) {
|
||||
return Renderer.hover.pHandleLinkMouseOver(
|
||||
evt,
|
||||
ele,
|
||||
{
|
||||
page: UrlUtil.PG_BESTIARY,
|
||||
source,
|
||||
hash,
|
||||
customHashId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static getTokenHoverMeta (mon) {
|
||||
const hasToken = mon.tokenUrl || mon.hasToken;
|
||||
if (!hasToken) return null;
|
||||
|
||||
return Renderer.hover.getMakePredefinedHover(
|
||||
{
|
||||
type: "image",
|
||||
href: {
|
||||
type: "external",
|
||||
url: Renderer.monster.getTokenUrl(mon),
|
||||
},
|
||||
data: {
|
||||
hoverTitle: `Token \u2014 ${mon.name}`,
|
||||
},
|
||||
},
|
||||
{isBookContent: true},
|
||||
);
|
||||
}
|
||||
|
||||
static _getFauxMon (name, source, scaledTo) {
|
||||
return {name, source, _isScaledCr: scaledTo != null, _scaledCr: scaledTo};
|
||||
}
|
||||
|
||||
async pDoCrChange ($iptCr, monScaled, scaledTo) {
|
||||
if (!$iptCr) return; // Should never occur, but if the creature has a non-adjustable CR, this field will not exist
|
||||
|
||||
try {
|
||||
await this._lock.pLock();
|
||||
await this._pDoCrChange({$iptCr, monScaled, scaledTo});
|
||||
} finally {
|
||||
this._lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
async _pDoCrChange ({$iptCr, monScaled, scaledTo}) {
|
||||
// Fetch original
|
||||
const mon = await DataLoader.pCacheAndGetHash(
|
||||
UrlUtil.PG_BESTIARY,
|
||||
UrlUtil.autoEncodeHash(monScaled),
|
||||
);
|
||||
|
||||
const baseCr = mon.cr.cr || mon.cr;
|
||||
if (baseCr == null) return;
|
||||
const baseCrNum = Parser.crToNumber(baseCr);
|
||||
const targetCr = $iptCr.val();
|
||||
|
||||
if (!Parser.isValidCr(targetCr)) {
|
||||
JqueryUtil.doToast({
|
||||
content: `"${$iptCr.val()}" is not a valid Challenge Rating! Please enter a valid CR (0-30). For fractions, "1/X" should be used.`,
|
||||
type: "danger",
|
||||
});
|
||||
$iptCr.val(Parser.numberToCr(scaledTo || baseCr));
|
||||
return;
|
||||
}
|
||||
|
||||
const targetCrNum = Parser.crToNumber(targetCr);
|
||||
|
||||
if (targetCrNum === scaledTo) return;
|
||||
|
||||
const state = await this._sublistManager.pGetExportableSublist({isForceIncludePlugins: true, isMemoryOnly: true});
|
||||
const toFindHash = UrlUtil.autoEncodeHash(mon);
|
||||
|
||||
const toFindUid = !(scaledTo == null || baseCrNum === scaledTo) ? Renderer.monster.getCustomHashId(this.constructor._getFauxMon(mon.name, mon.source, scaledTo)) : null;
|
||||
const ixCurrItem = state.items.findIndex(it => {
|
||||
if (scaledTo == null || scaledTo === baseCrNum) return !it.customHashId && it.h === toFindHash;
|
||||
else return it.customHashId === toFindUid;
|
||||
});
|
||||
if (!~ixCurrItem) throw new Error(`Could not find previously sublisted item!`);
|
||||
|
||||
const toFindNxtUid = baseCrNum !== targetCrNum ? Renderer.monster.getCustomHashId(this.constructor._getFauxMon(mon.name, mon.source, targetCrNum)) : null;
|
||||
const nextItem = state.items.find(it => {
|
||||
if (targetCrNum === baseCrNum) return !it.customHashId && it.h === toFindHash;
|
||||
else return it.customHashId === toFindNxtUid;
|
||||
});
|
||||
|
||||
// if there's an existing item with a matching UID (or lack of), merge into it
|
||||
if (nextItem) {
|
||||
const curr = state.items[ixCurrItem];
|
||||
nextItem.c = `${Number(nextItem.c || 1) + Number(curr.c || 1)}`;
|
||||
state.items.splice(ixCurrItem, 1);
|
||||
} else {
|
||||
// if we're returning to the original CR, wipe the existing UID. Otherwise, adjust it
|
||||
if (targetCrNum === baseCrNum) delete state.items[ixCurrItem].customHashId;
|
||||
else state.items[ixCurrItem].customHashId = Renderer.monster.getCustomHashId(this.constructor._getFauxMon(mon.name, mon.source, targetCrNum));
|
||||
}
|
||||
|
||||
await this._sublistManager.pDoLoadExportedSublist(state, {isMemoryOnly: true});
|
||||
}
|
||||
|
||||
getButtons (monId) {
|
||||
return e_({
|
||||
tag: "span",
|
||||
clazz: `best-ecgen__visible col-1 no-wrap pl-0 btn-group`,
|
||||
click: evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
children: [
|
||||
e_({
|
||||
tag: "button",
|
||||
title: `Add (SHIFT for 5)`,
|
||||
clazz: `btn btn-success btn-xs best-ecgen__btn-list`,
|
||||
click: evt => this._handleClick({evt, entity: this._bestiaryPage.dataList_[monId], mode: "add"}),
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: `glyphicon glyphicon-plus`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
e_({
|
||||
tag: "button",
|
||||
title: `Subtract (SHIFT for 5)`,
|
||||
clazz: `btn btn-danger btn-xs best-ecgen__btn-list`,
|
||||
click: evt => this._handleClick({evt, entity: this._bestiaryPage.dataList_[monId], mode: "subtract"}),
|
||||
children: [
|
||||
e_({
|
||||
tag: "span",
|
||||
clazz: `glyphicon glyphicon-minus`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
getSublistButtonsMeta (sublistItem) {
|
||||
const $btnAdd = $(`<button title="Add (SHIFT for 5)" class="btn btn-success btn-xs best-ecgen__btn-list"><span class="glyphicon glyphicon-plus"></span></button>`)
|
||||
.click(evt => this._handleClick({evt, entity: sublistItem.data.entity, mode: "add"}));
|
||||
|
||||
const $btnSub = $(`<button title="Subtract (SHIFT for 5)" class="btn btn-danger btn-xs best-ecgen__btn-list"><span class="glyphicon glyphicon-minus"></span></button>`)
|
||||
.click(evt => this._handleClick({evt, entity: sublistItem.data.entity, mode: "subtract"}));
|
||||
|
||||
const $btnRandomize = $(`<button title="Randomize Monster" class="btn btn-default btn-xs best-ecgen__btn-list"><span class="glyphicon glyphicon-random"></span></button>`)
|
||||
.click(evt => this._pHandleShuffleClick({evt, sublistItem}));
|
||||
|
||||
const $btnLock = $(`<button title="Lock Monster against Randomizing/Adjusting" class="btn btn-default btn-xs best-ecgen__btn-list"><span class="glyphicon glyphicon-lock"></span></button>`)
|
||||
.click(() => this._sublistManager.pSetDataEntry({sublistItem, key: "isLocked", value: !sublistItem.data.isLocked}))
|
||||
.toggleClass("active", sublistItem.data.isLocked);
|
||||
|
||||
const $wrp = $$`<span class="best-ecgen__visible col-1-5 no-wrap pl-0 btn-group">
|
||||
${$btnAdd}
|
||||
${$btnSub}
|
||||
${$btnRandomize}
|
||||
${$btnLock}
|
||||
</span>`
|
||||
.click(evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
});
|
||||
|
||||
return {
|
||||
$wrp,
|
||||
fnUpdate: () => $btnLock.toggleClass("active", sublistItem.data.isLocked),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user