Files
5etools-mirror-2.github.io/js/makebrew-spell.js
TheGiddyLimit 8117ebddc5 v1.198.1
2024-01-01 19:34:49 +00:00

1256 lines
45 KiB
JavaScript

import {SpellSourceLookupBuilder} from "./converterutils-spell-sources.js";
class SpellBuilder extends Builder {
constructor () {
super({
titleSidebarLoadExisting: "Copy Existing Spell",
titleSidebarDownloadJson: "Download Spells as JSON",
prop: "spell",
titleSelectDefaultSource: "(Same as Spell)",
});
this._subclassLookup = {};
this._renderOutputDebounced = MiscUtil.debounce(() => this._renderOutput(), 50);
}
static _getAsMarkdown (sp) {
return RendererMarkdown.get().render({entries: [{type: "statblockInline", dataType: "spell", data: sp}]});
}
async pHandleSidebarLoadExistingClick () {
const result = await SearchWidget.pGetUserSpellSearch();
if (result) {
const spell = MiscUtil.copy(await DataLoader.pCacheAndGet(result.page, result.source, result.hash));
return this.pHandleSidebarLoadExistingData(spell);
}
}
/**
* @param spell
* @param [opts]
* @param [opts.meta]
*/
async pHandleSidebarLoadExistingData (spell, opts) {
opts = opts || {};
spell.source = this._ui.source;
delete spell.srd;
delete spell.uniqueId;
delete spell.reprintedAs;
const meta = {...(opts.meta || {}), ...this._getInitialMetaState()};
this.setStateFromLoaded({s: spell, m: meta});
this.renderInput();
this.renderOutput();
}
async _pInit () {
this._subclassLookup = await DataUtil.class.pGetSubclassLookup();
}
_getInitialState () {
return {
...super._getInitialState(),
name: "New Spell",
level: 1,
school: "A",
time: [
{
number: 1,
unit: "action",
},
],
range: {
type: "point",
distance: {
type: "self",
},
},
duration: [
{
type: "instant",
},
],
classes: {
fromClassList: [
{
name: "Wizard",
source: Parser.SRC_PHB,
},
],
},
entries: [],
source: this._ui ? this._ui.source : "",
};
}
setStateFromLoaded (state) {
if (!state?.s || !state?.m) return;
this._doResetProxies();
if (!state.s.uniqueId) state.s.uniqueId = CryptUtil.uid();
this.__state = state.s;
this.__meta = state.m;
}
doHandleSourcesAdd () {
this._doHandleSourcesAdd_handleSelProp("$selClassSources");
this._doHandleSourcesAdd_handleSelProp("$selSubclassSources");
}
_doHandleSourcesAdd_handleSelProp (prop) {
(this._$eles[prop] || []).map($sel => {
const currSrcJson = $sel.val();
$sel.empty();
[...Object.keys(Parser.SOURCE_JSON_TO_FULL), ...this._ui.allSources]
.forEach(srcJson => $sel.append(`<option value="${srcJson.escapeQuotes()}">${Parser.sourceJsonToFull(srcJson).escapeQuotes()}</option>`));
if (this._ui.allSources.indexOf(currSrcJson)) $sel.val(currSrcJson);
else $sel.val(Parser.SRC_PHB);
return $sel;
}).forEach($sel => $sel.change());
}
_renderInputImpl () {
this.doCreateProxies();
this.renderInputControls();
this._renderInputMain();
}
_renderInputMain () {
this._sourcesCache = MiscUtil.copy(this._ui.allSources);
const $wrp = this._ui.$wrpInput.empty();
const _cb = () => {
// Prefer numerical pages if possible
if (!isNaN(this._state.page)) this._state.page = Number(this._state.page);
// do post-processing
TagCondition.tryTagConditions(this._state, {isTagInflicted: true});
this.renderOutput();
this.doUiSave();
this._meta.isModified = true;
};
const cb = MiscUtil.debounce(_cb, 33);
this._cbCache = cb; // cache for use when updating sources
// initialise tabs
this._resetTabs({tabGroup: "input"});
const tabs = this._renderTabs(
[
new TabUiUtil.TabMeta({name: "Info", hasBorder: true}),
new TabUiUtil.TabMeta({name: "Details", hasBorder: true}),
new TabUiUtil.TabMeta({name: "Sources", hasBorder: true}),
new TabUiUtil.TabMeta({name: "Flavor/Misc", hasBorder: true}),
],
{
tabGroup: "input",
cbTabChange: this.doUiSave.bind(this),
},
);
const [infoTab, detailsTab, sourcesTab, miscTab] = tabs;
$$`<div class="ve-flex-v-center w-100 no-shrink ui-tab__wrp-tab-heads--border">${tabs.map(it => it.$btnTab)}</div>`.appendTo($wrp);
tabs.forEach(it => it.$wrpTab.appendTo($wrp));
// INFO
BuilderUi.$getStateIptString("Name", cb, this._state, {nullable: false, callback: () => this.pRenderSideMenu()}, "name").appendTo(infoTab.$wrpTab);
this._$selSource = this.$getSourceInput(cb).appendTo(infoTab.$wrpTab);
this.__$getOtherSourcesInput(cb).appendTo(infoTab.$wrpTab);
BuilderUi.$getStateIptString("Page", cb, this._state, {}, "page").appendTo(infoTab.$wrpTab);
BuilderUi.$getStateIptEnum("Level", cb, this._state, {nullable: false, fnDisplay: (it) => Parser.spLevelToFull(it), vals: [...new Array(21)].map((_, i) => i)}, "level").appendTo(infoTab.$wrpTab);
BuilderUi.$getStateIptEnum("School", cb, this._state, {nullable: false, fnDisplay: (it) => Parser.spSchoolAbvToFull(it), vals: [...Parser.SKL_ABVS]}, "school").appendTo(infoTab.$wrpTab);
BuilderUi.$getStateIptStringArray(
"Subschools",
cb,
this._state,
{
shortName: "Subschool",
title: "Found in some homebrew, for example the 'Clockwork' sub-school.",
},
"subschools",
).appendTo(infoTab.$wrpTab);
// TEXT
this.__$getTimeInput(cb).appendTo(detailsTab.$wrpTab);
this.__$getRangeInput(cb).appendTo(detailsTab.$wrpTab);
this.__$getComponentInput(cb).appendTo(detailsTab.$wrpTab);
this.__$getMetaInput(cb).appendTo(detailsTab.$wrpTab);
this.__$getDurationInput(cb).appendTo(detailsTab.$wrpTab);
BuilderUi.$getStateIptEntries("Text", cb, this._state, {fnPostProcess: BuilderUi.fnPostProcessDice}, "entries").appendTo(detailsTab.$wrpTab);
BuilderUi.$getStateIptEntries("&quot;At Higher Levels&quot; Text", cb, this._state, {nullable: true, withHeader: "At Higher Levels", fnPostProcess: BuilderUi.fnPostProcessDice}, "entriesHigherLevel").appendTo(detailsTab.$wrpTab);
// SOURCES
const [
{$row: $rowClasses, doRefresh: doRefreshClasses},
{$row: $rowSubclasses, doRefresh: doRefreshSubclasses},
] = this.__$getClassesInputs(cb);
$rowClasses.appendTo(sourcesTab.$wrpTab);
$rowSubclasses.appendTo(sourcesTab.$wrpTab);
const {$row: $rowRaces, doRefresh: doRefreshRaces} = this.__$getRaces(cb);
$rowRaces.appendTo(sourcesTab.$wrpTab);
const {$row: $rowBackgrounds, doRefresh: doRefreshBackgrounds} = this.__$getBackgrounds(cb);
$rowBackgrounds.appendTo(sourcesTab.$wrpTab);
const {$row: $rowOptionalFeatures, doRefresh: doRefreshOptionalFeatures} = this.__$getOptionalfeatures(cb);
$rowOptionalFeatures.appendTo(sourcesTab.$wrpTab);
const {$row: $rowFeats, doRefresh: doRefreshFeats} = this.__$getFeats(cb);
$rowFeats.appendTo(sourcesTab.$wrpTab);
const {$row: $rowChatoptions, doRefresh: doRefreshChatoptions} = this.__$getCharoptions(cb);
$rowChatoptions.appendTo(sourcesTab.$wrpTab);
const {$row: $rowRewards, doRefresh: doRefreshRewards} = this.__$getRewards(cb);
$rowRewards.appendTo(sourcesTab.$wrpTab);
const fnsDoRefreshSources = [
doRefreshClasses,
doRefreshSubclasses,
doRefreshRaces,
doRefreshBackgrounds,
doRefreshOptionalFeatures,
doRefreshFeats,
doRefreshChatoptions,
doRefreshRewards,
];
this.__$getSourcesGenerated(cb, fnsDoRefreshSources).appendTo(sourcesTab.$wrpTab);
// FLAVOR/MISC
this.$getFluffInput(cb).appendTo(miscTab.$wrpTab);
$(`<div class="ve-flex-vh-center w-100 mb-2"><i>Note: the following data is used by filters on the Spells page.</i></div>`).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Damage Inflicted",
cb,
this._state,
{
vals: MiscUtil.copy(Parser.DMG_TYPES),
nullable: true,
fnDisplay: StrUtil.uppercaseFirst,
},
"damageInflict",
).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Conditions Inflicted",
cb,
this._state,
{
vals: MiscUtil.copy(Parser.CONDITIONS),
nullable: true,
fnDisplay: StrUtil.uppercaseFirst,
},
"conditionInflict",
).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Spell Attack Type",
cb,
this._state,
{
vals: ["M", "R", "O"],
nullable: true,
fnDisplay: Parser.spAttackTypeToFull,
},
"spellAttack",
).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Saving Throw",
cb,
this._state,
{
vals: Object.values(Parser.ATB_ABV_TO_FULL).map(it => it.toLowerCase()),
nullable: true,
fnDisplay: StrUtil.uppercaseFirst,
},
"savingThrow",
).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Ability Check",
cb,
this._state,
{
vals: Object.values(Parser.ATB_ABV_TO_FULL).map(it => it.toLowerCase()),
nullable: true,
fnDisplay: StrUtil.uppercaseFirst,
},
"abilityCheck",
).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Area Type",
cb,
this._state,
{
vals: Object.keys(Parser.SPELL_AREA_TYPE_TO_FULL),
nullable: true,
fnDisplay: Parser.spAreaTypeToFull,
},
"areaTags",
).appendTo(miscTab.$wrpTab);
BuilderUi.$getStateIptBooleanArray(
"Misc Tags",
cb,
this._state,
{
vals: Object.keys(Parser.SP_MISC_TAG_TO_FULL),
nullable: true,
fnDisplay: Parser.spMiscTagToFull,
},
"miscTags",
).appendTo(miscTab.$wrpTab);
// The following aren't included, as they are not used in the site:
/*
damageResist
damageImmune
damageVulnerable
*/
}
__$getOtherSourcesInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Other Sources", {isMarked: true, title: "For example, various spells in Xanathar's Guide to Everything can also be found in the Elemental Evil Player's Companion."});
const doUpdateState = () => {
const out = otherSourceRows.map(row => row.getOtherSource()).filter(Boolean);
if (out.length) this._state.otherSources = out;
else delete this._state.otherSources;
cb();
};
const otherSourceRows = [];
const $wrpRows = $(`<div/>`).appendTo($rowInner);
(this._state.otherSources || []).forEach(it => this.__$getOtherSourcesInput__getOtherSourceRow(doUpdateState, otherSourceRows, it).$wrp.appendTo($wrpRows));
const $wrpBtnAdd = $(`<div/>`).appendTo($rowInner);
$(`<button class="btn btn-xs btn-default">Add Other Source</button>`)
.appendTo($wrpBtnAdd)
.click(() => {
this.__$getOtherSourcesInput__getOtherSourceRow(doUpdateState, otherSourceRows, null).$wrp.appendTo($wrpRows);
doUpdateState();
});
return $row;
}
__$getOtherSourcesInput__getOtherSourceRow (doUpdateState, otherSourceRows, os) {
const getOtherSource = () => {
const out = {source: $selSource.val()};
const pageRaw = $iptPage.val();
if (pageRaw) {
const page = !isNaN(pageRaw) ? UiUtil.strToInt(pageRaw) : pageRaw;
if (page) {
out.page = page;
$iptPage.val(page);
}
}
return out;
};
const $iptPage = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(os && os.page ? os.page : null);
const $selSource = this._$getSelSource("$selOtherSourceSources", doUpdateState, os ? os.source.escapeQuotes() : Parser.SRC_PHB);
const out = {getOtherSource};
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows mkbru__wrp-rows--removable">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Source</span>${$selSource}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Page</span>${$iptPage}</div>
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, otherSourceRows, out, $wrp, "Other Source").appendTo($wrpBtnRemove);
out.$wrp = $wrp;
otherSourceRows.push(out);
return out;
}
__$getTimeInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Casting Time", {isMarked: true});
const doUpdateState = () => {
this._state.time = timeRows.map(row => row.getTime());
cb();
};
const timeRows = [];
const $wrpRows = $(`<div/>`).appendTo($rowInner);
this._state.time.forEach(time => SpellBuilder.__$getTimeInput__getTimeRow(doUpdateState, timeRows, time).$wrp.appendTo($wrpRows));
const $wrpBtnAdd = $(`<div/>`).appendTo($rowInner);
$(`<button class="btn btn-xs btn-default">Add Casting Time</button>`)
.appendTo($wrpBtnAdd)
.click(() => {
SpellBuilder.__$getTimeInput__getTimeRow(doUpdateState, timeRows, {number: 1, unit: Parser.SP_TM_ACTION}).$wrp.appendTo($wrpRows);
doUpdateState();
});
return $row;
}
static __$getTimeInput__getTimeRow (doUpdateState, timeRows, time) {
const keys = Object.keys(Parser.SP_TIME_TO_FULL);
const getTime = () => {
const out = {number: UiUtil.strToInt($iptNum.val()), unit: keys[$selUnit.val()]};
const condition = $iptCond.val().trim();
if (condition && keys[$selUnit.val()] === Parser.SP_TM_REACTION) out.condition = condition;
$iptNum.val(out.number);
return out;
};
const $iptNum = $(`<input class="form-control form-control--minimal input-xs mr-2">`)
.change(() => doUpdateState())
.val(time.number);
const ixInitial = keys.indexOf(time.unit);
const $selUnit = $(`<select class="form-control input-xs">
${keys.map((it, i) => `<option value="${i}">${Parser.spTimeUnitToFull(it)}</option>`).join("")}
</select>`)
.val(~ixInitial ? `${ixInitial}` : "0")
.change(() => {
const isReaction = keys[$selUnit.val()] === Parser.SP_TM_REACTION;
$stageCond.toggleVe(isReaction);
doUpdateState();
});
const $iptCond = $(`<input class="form-control form-control--minimal input-xs" placeholder="which you take when...">`)
.change(() => doUpdateState())
.val(time.condition);
const out = {getTime};
const $stageCond = $$`<div class="ve-flex-v-center mb-2">
<span class="mr-2 mkbru__sub-name--33">Condition</span>${$iptCond}
</div>`.toggleVe(ixInitial === 2);
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows mkbru__wrp-rows--removable">
<div class="ve-flex-v-center mb-2">${$iptNum}${$selUnit}</div>
${$stageCond}
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, timeRows, out, $wrp, "Time", {isProtectLast: true}).appendTo($wrpBtnRemove);
out.$wrp = $wrp;
timeRows.push(out);
return out;
}
__$getRangeInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Range", {isMarked: true});
const isInitialDistance = !!this._state.range.distance;
const isInitialAmount = this._state.range.distance && this._state.range.distance.amount != null;
const RANGE_TYPES = Parser.RANGE_TYPES;
const DIST_TYPES = Parser.DIST_TYPES;
const doUpdateState = () => {
const rangeMeta = RANGE_TYPES[$selRange.val()];
const out = {type: rangeMeta.type};
if (rangeMeta.hasDistance) {
const distMeta = DIST_TYPES[$selDistance.val()];
out.distance = {type: distMeta.type};
if (distMeta.hasAmount) {
out.distance.amount = UiUtil.strToInt($iptAmount.val());
$iptAmount.val(out.distance.amount);
}
}
this._state.range = out;
cb();
};
const ixInitialRange = RANGE_TYPES.findIndex(it => it.type === this._state.range.type);
const $selRange = $(`<select class="form-control input-xs">
${RANGE_TYPES.map((it, i) => `<option value="${i}">${Parser.spRangeTypeToFull(it.type)}</option>`).join("")}
</select>`).val(~ixInitialRange ? `${ixInitialRange}` : "0").change(() => {
const meta = RANGE_TYPES[$selRange.val()];
$stageDistance.toggleVe(meta.hasDistance);
if (meta.isRequireAmount && !DIST_TYPES[$selDistance.val()].hasAmount) {
$selDistance.val(`${DIST_TYPES.findIndex(it => it.hasAmount)}`).change();
} else doUpdateState();
});
$$`<div class="ve-flex-v-center">
<span class="mr-2 mkbru__sub-name--33">Range Type</span>
${$selRange}
</div>`.appendTo($rowInner);
// DISTANCE TYPE
const ixInitialDist = this._state.range.distance ? DIST_TYPES.findIndex(it => it.type === this._state.range.distance.type) : -1;
const $selDistance = $(`<select class="form-control input-xs">
${DIST_TYPES.map((it, i) => `<option value="${i}">${Parser.spDistanceTypeToFull(it.type)}</option>`).join("")}
</select>`).val(~ixInitialDist ? `${ixInitialDist}` : "0").change(() => {
const meta = DIST_TYPES[$selDistance.val()];
$stageAmount.toggleVe(meta.hasAmount);
if (!meta.hasAmount && RANGE_TYPES[$selRange.val()].isRequireAmount) {
$selDistance.val(`${DIST_TYPES.findIndex(it => it.hasAmount)}`).change();
} else doUpdateState();
});
const $stageDistance = $$`<div class="ve-flex-v-center mt-2">
<span class="mr-2 mkbru__sub-name--33">Distance Type</span>
${$selDistance}
</div>`.appendTo($rowInner).toggleVe(isInitialDistance);
// AMOUNT
const initialAmount = MiscUtil.get(this._state, "range", "distance", "amount");
const $iptAmount = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(initialAmount);
const $stageAmount = $$`<div class="ve-flex-v-center mt-2">
<span class="mr-2 mkbru__sub-name--33">Distance Amount</span>
${$iptAmount}
</div>`.appendTo($rowInner).toggleVe(isInitialAmount);
return $row;
}
__$getComponentInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Components", {isMarked: true});
const initialMaterialMode = (!this._state.components || this._state.components.m == null)
? "0"
: (this._state.components.m.consume != null || this._state.components.m.cost != null || this._state.components.m.text != null)
? "2"
: typeof this._state.components.m === "string" ? "1" : "3";
const doUpdateState = () => {
const out = {};
if ($cbVerbal.prop("checked")) out.v = true;
if ($cbSomatic.prop("checked")) out.s = true;
if ($cbRoyalty.prop("checked")) out.r = true;
const materialMode = $selMaterial.val();
// Use spaces
switch (materialMode) {
case "1": out.m = $iptMaterial.val().trim() || true; break;
case "2": {
out.m = {
text: $iptMaterial.val().trim() || true,
};
// TODO add support for "optional" consume type
if ($cbConsumed.prop("checked")) out.m.consume = true;
if ($iptCost.val().trim()) {
out.m.cost = UiUtil.strToInt($iptCost.val());
$iptCost.val(out.m.cost);
}
break;
}
case "3": out.m = true; break;
}
if (Object.keys(out).length) this._state.components = out;
else delete this._state.components;
cb();
};
const $cbVerbal = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", this._state.components && this._state.components.v)
.change(() => doUpdateState());
const $cbSomatic = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", this._state.components && this._state.components.s)
.change(() => doUpdateState());
const $cbRoyalty = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", this._state.components && this._state.components.r)
.change(() => doUpdateState());
const $iptMaterial = $(`<input class="form-control form-control--minimal input-xs">`)
.val(initialMaterialMode === "1" ? this._state.components.m : initialMaterialMode === "2" ? this._state.components.m.text : null)
.change(() => doUpdateState());
const $selMaterial = $(`<select class="form-control input-xs">
<option value="0">(None)</option>
<option value="1">Has Material Component</option>
<option value="2">Has Consumable/Costed Material Component</option>
<option value="3">Has Generic Material Component</option>
</select>`).val(initialMaterialMode).change(() => {
switch ($selMaterial.val()) {
case "0": $stageMaterial.hideVe(); $stageMaterialConsumable.hideVe(); break;
case "1": $stageMaterial.showVe(); $stageMaterialConsumable.hideVe(); break;
case "2": $stageMaterial.showVe(); $stageMaterialConsumable.showVe(); break;
case "3": $stageMaterial.hideVe(); $stageMaterialConsumable.hideVe(); break;
}
doUpdateState();
});
$$``.appendTo($rowInner);
$$`<div>
<div class="ve-flex-v-center mb-2"><div class="mr-2 mkbru__sub-name--33">Verbal</div>${$cbVerbal}</div>
<div class="ve-flex-v-center mb-2"><div class="mr-2 mkbru__sub-name--33">Somatic</div>${$cbSomatic}</div>
<div class="ve-flex-v-center mt-2"><div class="mr-2 mkbru__sub-name--33">Royalty</div>${$cbRoyalty}</div>
<div class="ve-flex-v-center"><div class="mr-2 mkbru__sub-name--33">Material Type</div>${$selMaterial}</div>
</div>`.appendTo($rowInner);
// BASIC MATERIAL
const $stageMaterial = $$`<div class="ve-flex-v-center mt-2"><div class="mr-2 mkbru__sub-name--33">Materials</div>${$iptMaterial}</div>`.appendTo($rowInner).toggleVe(initialMaterialMode === "1" || initialMaterialMode === "2");
// CONSUMABLE MATERIAL
const $cbConsumed = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", this._state.components && this._state.components.m && this._state.components.m.consume)
.change(() => doUpdateState());
const $iptCost = $(`<input class="form-control form-control--minimal input-xs mr-1">`)
.val(this._state.components && this._state.components.m && this._state.components.m.cost ? this._state.components.m.cost : null)
.change(() => doUpdateState());
const TITLE_FILTERS_EXTERNAL = "Used in filtering/external applications. The full text of the material component should be entered in the &quot;Materials&quot; field, above.";
const $stageMaterialConsumable = $$`<div class="mt-2">
<div class="ve-flex-v-center mb-2"><div class="mr-2 mkbru__sub-name--33 help" title="${TITLE_FILTERS_EXTERNAL}">Is Consumed</div>${$cbConsumed}</div>
<div class="ve-flex-v-center"><div class="mr-2 mkbru__sub-name--33 help" title="${TITLE_FILTERS_EXTERNAL} Specified in copper pieces (1gp = 100cp).">Component Cost</div>${$iptCost}<div>cp</div></div>
</div>`.appendTo($rowInner).toggleVe(initialMaterialMode === "2");
return $row;
}
__$getMetaInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Tags", {isMarked: true});
const doUpdateState = () => {
const out = {};
if ($cbRitual.prop("checked")) out.ritual = true;
if ($cbTechnomagic.prop("checked")) out.technomagic = true;
if (Object.keys(out).length) this._state.meta = out;
else delete this._state.meta;
cb();
};
const $cbRitual = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", this._state.meta && this._state.meta.ritual)
.change(() => doUpdateState());
const $cbTechnomagic = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", this._state.meta && this._state.meta.technomagic)
.change(() => doUpdateState());
$$`<div>
<div class="ve-flex-v-center mb-2"><div class="mr-2 mkbru__sub-name--33">Ritual</div>${$cbRitual}</div>
<div class="ve-flex-v-center"><div class="mr-2 mkbru__sub-name--33">Technomagic</div>${$cbTechnomagic}</div>
</div>`.appendTo($rowInner);
return $row;
}
__$getDurationInput (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Duration", {isMarked: true});
const doUpdateState = () => {
this._state.duration = durationRows.map(row => row.getDuration());
cb();
};
const durationRows = [];
const $wrpRows = $(`<div/>`).appendTo($rowInner);
this._state.duration.forEach(duration => SpellBuilder.__$getDurationInput__getDurationRow(doUpdateState, durationRows, duration).$wrp.appendTo($wrpRows));
const $wrpBtnAdd = $(`<div/>`).appendTo($rowInner);
$(`<button class="btn btn-xs btn-default">Add Duration</button>`)
.appendTo($wrpBtnAdd)
.click(() => {
SpellBuilder.__$getDurationInput__getDurationRow(doUpdateState, durationRows, {type: "instant"}).$wrp.appendTo($wrpRows);
doUpdateState();
});
return $row;
}
static __$getDurationInput__getDurationRow (doUpdateState, durationRows, duration) {
const DURATION_TYPES = Parser.DURATION_TYPES;
const AMOUNT_TYPES = Parser.DURATION_AMOUNT_TYPES;
const typeInitial = DURATION_TYPES.find(it => it.type === duration.type);
const getDuration = () => {
const ixType = $selDurationType.val();
const out = {type: DURATION_TYPES[ixType].type};
switch (ixType) {
case "1": {
out.duration = {
type: AMOUNT_TYPES[$selAmountType.val()],
amount: UiUtil.strToInt($iptAmount.val()),
};
$iptAmount.val(out.duration.amount);
if ($cbConc.prop("checked")) out.concentration = true;
if ($cbUpTo.prop("checked")) out.duration.upTo = true;
break;
}
case "2": {
if (endRows.length) out.ends = endRows.map(it => it.getEnd());
}
}
return out;
};
const ixInitialDuration = DURATION_TYPES.findIndex(it => it.type === duration.type);
const $selDurationType = $(`<select class="form-control input-xs">
${DURATION_TYPES.map((it, i) => `<option value="${i}">${it.full || it.type.toTitleCase()}</option>`).join("")}
</select>`).val(~ixInitialDuration ? `${ixInitialDuration}` : "0").change(() => {
const meta = DURATION_TYPES[$selDurationType.val()];
$stageAmount.toggleVe(!!meta.hasAmount);
$stageEnds.toggleVe(!!meta.hasEnds);
doUpdateState();
});
// AMOUNT
const ixInitialAmount = duration.duration ? AMOUNT_TYPES.indexOf(duration.duration.type) : "0";
const $selAmountType = $(`<select class="form-control input-xs">
${AMOUNT_TYPES.map((it, i) => `<option value="${i}">${it.toTitleCase()}s</option>`).join("")}
</select>`).val(ixInitialAmount).change(() => doUpdateState());
const $iptAmount = $(`<input class="form-control form-control--minimal input-xs mr-2">`)
.val(duration.duration ? duration.duration.amount : null)
.change(() => doUpdateState());
const $cbConc = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", !!duration.concentration)
.change(() => doUpdateState());
const $cbUpTo = $(`<input type="checkbox" class="mkbru__ipt-cb--plain">`)
.prop("checked", duration.duration ? duration.duration.upTo : false)
.change(() => doUpdateState());
const $stageAmount = $$`<div class="ve-flex-col mb-2">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Concentration</span>${$cbConc}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33 help" title="For a spell with Concentration, this has no effect, as it is assumed that the spell can be ended at any time by ending concentration.">Up To...</span>${$cbUpTo}</div>
<div class="ve-flex-v-center">${$iptAmount}${$selAmountType}</div>
</div>`.toggleVe(!!typeInitial.hasAmount);
// ENDS
const endRows = [];
const $wrpEndRows = $(`<div class="ve-flex-col"/>`);
const $btnAddEnd = $(`<button class="btn btn-xs btn-default">Add &quot;Until&quot; Clause</button>`)
.click(() => {
SpellBuilder.__$getDurationInput__getDurationRow__getEndRow(doUpdateState, endRows, "dispel").$wrp.appendTo($wrpEndRows);
doUpdateState();
});
const $stageEnds = $$`<div class="mb-2">
${$wrpEndRows}
<div class="text-right">${$btnAddEnd}</div>
</div>`.toggleVe(!!typeInitial.hasEnds);
if (duration.ends) duration.ends.forEach(end => SpellBuilder.__$getDurationInput__getDurationRow__getEndRow(doUpdateState, endRows, end).$wrp.appendTo($wrpEndRows));
const out = {getDuration};
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows mkbru__wrp-rows--removable">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Duration Type</span>${$selDurationType}</div>
${$stageAmount}
${$stageEnds}
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, durationRows, out, $wrp, "Duration", {isProtectLast: true}).appendTo($wrpBtnRemove);
out.$wrp = $wrp;
durationRows.push(out);
return out;
}
static __$getDurationInput__getDurationRow__getEndRow (doUpdateState, endRows, end) {
const keys = Object.keys(Parser.SP_END_TYPE_TO_FULL);
const getEnd = () => keys[$selEndType.val()];
const ixInitialEnd = end ? keys.indexOf(end) : "0";
const $selEndType = $(`<select class="form-control input-xs mr-2">
${keys.map((it, i) => `<option value="${i}">Until ${Parser.spEndTypeToFull(it)}</option>`).join("")}
</select>`).val(ixInitialEnd).change(() => doUpdateState());
const out = {getEnd};
const $wrpBtnRemove = $(`<div/>`);
const $wrp = $$`<div class="ve-flex">
<div class="mkbru__sub-name--33 mr-2"></div>
<div class="mb-2 ve-flex-v-center w-100">${$selEndType}${$wrpBtnRemove}</div>
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, endRows, out, $wrp, "Until Clause", {isExtraSmall: true}).appendTo($wrpBtnRemove);
out.$wrp = $wrp;
endRows.push(out);
return out;
}
__$getClassesInputs (cb) {
const DEFAULT_CLASS = {name: "Wizard", source: Parser.SRC_PHB};
const DEFAULT_SUBCLASS = {name: "Evocation", source: Parser.SRC_PHB};
const [$rowCls, $rowInnerCls] = BuilderUi.getLabelledRowTuple("Classes", {isMarked: true});
const [$rowSc, $rowInnerSc] = BuilderUi.getLabelledRowTuple("Subclasses", {isMarked: true});
const classRows = [];
const subclassRows = [];
const doUpdateState = () => {
const out = {fromClassList: classRows.map(it => it.getClass())};
const subclasses = subclassRows.map(it => it.getSubclass()).filter(Boolean);
if (subclasses.length) out.fromSubclass = subclasses;
this._state.classes = out;
cb();
};
// CLASSES
const $wrpRowsCls = $(`<div/>`).appendTo($rowInnerCls);
const doRefreshCls = () => {
$wrpRowsCls.empty();
classRows.splice(0, classRows.length);
((this._state.classes || {}).fromClassList || []).forEach(cls => this.__$getClassesInputs__getClassRow(doUpdateState, classRows, cls).$wrp.appendTo($wrpRowsCls));
};
doRefreshCls();
const $wrpBtnAddCls = $(`<div/>`).appendTo($rowInnerCls);
$(`<button class="btn btn-xs btn-default">Add Class</button>`)
.appendTo($wrpBtnAddCls)
.click(() => {
this.__$getClassesInputs__getClassRow(doUpdateState, classRows, MiscUtil.copy(DEFAULT_CLASS)).$wrp.appendTo($wrpRowsCls);
doUpdateState();
});
// SUBCLASSES
const $wrpRowsSc = $(`<div/>`).appendTo($rowInnerSc);
const doRefreshSc = () => {
$wrpRowsSc.empty();
subclassRows.splice(0, subclassRows.length);
((this._state.classes || {}).fromSubclass || []).forEach(sc => this.__$getClassesInputs__getSubclassRow(doUpdateState, subclassRows, sc).$wrp.appendTo($wrpRowsSc));
};
doRefreshSc();
const $wrpBtnAddSc = $(`<div/>`).appendTo($rowInnerSc);
$(`<button class="btn btn-xs btn-default">Add Subclass</button>`)
.appendTo($wrpBtnAddSc)
.click(() => {
this.__$getClassesInputs__getSubclassRow(doUpdateState, subclassRows, {class: MiscUtil.copy(DEFAULT_CLASS), subclass: MiscUtil.copy(DEFAULT_SUBCLASS)}).$wrp.appendTo($wrpRowsSc);
doUpdateState();
});
return [{$row: $rowCls, doRefresh: doRefreshCls}, {$row: $rowSc, doRefresh: doRefreshSc}];
}
__$getClassesInputs__getClassRow (doUpdateState, classRows, cls) {
const getClass = () => {
return {
name: $iptClass.val().trim(),
source: $selClassSource.val().unescapeQuotes(),
};
};
const $iptClass = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(cls.name);
const $selClassSource = this._$getSelSource("$selClassSources", doUpdateState, cls.source.escapeQuotes());
const out = {getClass};
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows mkbru__wrp-rows--removable">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Class Name</span>${$iptClass}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Class Source</span>${$selClassSource}</div>
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, classRows, out, $wrp, "Class").appendTo($wrpBtnRemove);
out.$wrp = $wrp;
classRows.push(out);
return out;
}
__$getClassesInputs__getSubclassRow (doUpdateState, subclassRows, subclass) {
const getSubclass = () => {
const className = $iptClass.val().trim();
const subclassName = $iptSubclass.val().trim();
const subclassShortName = $iptSubclassShort.val().trim();
if (!className || !subclassName) return null;
const out = {
class: {
name: className,
source: $selClassSource.val().unescapeQuotes(),
},
subclass: {
name: $iptSubclass.val(),
shortName: $iptSubclassShort.val(),
source: $selSubclassSource.val().unescapeQuotes(),
},
};
const subSubclassName = $iptSubSubclass.val().trim();
if (subSubclassName) out.subclass.subSubclass = subSubclassName;
return out;
};
const $iptClass = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(subclass.class.name);
const $selClassSource = this._$getSelSource("$selClassSources", doUpdateState, subclass.class.source.escapeQuotes());
const $iptSubclass = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(subclass.subclass.name);
const $iptSubclassShort = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(subclass.subclass.shortName);
const $selSubclassSource = this._$getSelSource("$selSubclassSources", doUpdateState, subclass.subclass.source.escapeQuotes());
const $iptSubSubclass = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(subclass.subclass.subSubclass ? subclass.subclass.subSubclass : null);
const out = {getSubclass};
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Class Name</span>${$iptClass}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Class Source</span>${$selClassSource}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Subclass Name</span>${$iptSubclass}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Subclass Short Name</span>${$iptSubclassShort}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Subclass Source</span>${$selSubclassSource}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33 help" title="For example, for a Circle of the Coast Land Druid, enter &quot;Coast&quot;">Sub-Subclass Name</span>${$iptSubSubclass}</div>
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, subclassRows, out, $wrp, "Subclass").appendTo($wrpBtnRemove);
out.$wrp = $wrp;
subclassRows.push(out);
return out;
}
__$getRaces (cb) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Races", {isMarked: true});
const doUpdateState = () => {
const races = raceRows.map(row => row.getRace()).filter(Boolean);
if (races.length) this._state.races = races;
else delete this._state.races;
cb();
};
const raceRows = [];
const $wrpRows = $(`<div/>`).appendTo($rowInner);
const doRefresh = () => {
$wrpRows.empty();
raceRows.splice(0, raceRows.length);
(this._state.races || []).forEach(race => this.__$getRaces__getRaceRow(doUpdateState, raceRows, race).$wrp.appendTo($wrpRows));
};
doRefresh();
const $wrpBtnAdd = $(`<div/>`).appendTo($rowInner);
$(`<button class="btn btn-xs btn-default">Add Race</button>`)
.appendTo($wrpBtnAdd)
.click(() => {
this.__$getRaces__getRaceRow(doUpdateState, raceRows, null).$wrp.appendTo($wrpRows);
doUpdateState();
});
return {$row, doRefresh};
}
__$getRaces__getRaceRow (doUpdateState, raceRows, race) {
const getRace = () => {
const raceName = $iptRace.val().trim();
if (raceName) {
const out = {
name: raceName,
source: $selSource.val().unescapeQuotes(),
};
const baseRaceName = $iptBaseRace.val().trim();
if (baseRaceName) {
out.baseName = baseRaceName;
out.baseSource = $selBaseSource.val().unescapeQuotes();
}
return out;
} else return null;
};
const $iptRace = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(race ? race.name : null);
const $iptBaseRace = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(race ? race.baseName : null);
const $selSource = this._$getSelSource("$selRaceSources", doUpdateState, race ? race.source.escapeQuotes() : Parser.SRC_PHB);
const $selBaseSource = this._$getSelSource("$selBaseRaceSources", doUpdateState, race && race.baseSource ? race.baseSource.escapeQuotes() : Parser.SRC_PHB);
const out = {getRace};
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Name</span>${$iptRace}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Source</span>${$selSource}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33 help" title="The name of the base race, e.g. &quot;Elf&quot;. This is used in filtering.">Base Name</span>${$iptBaseRace}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33 help" title="For example, the &quot;Elf&quot; base race has a source of &quot;${Parser.SRC_PHB}&quot;">Base Source</span>${$selBaseSource}</div>
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, raceRows, out, $wrp, "Race").appendTo($wrpBtnRemove);
out.$wrp = $wrp;
raceRows.push(out);
return out;
}
__$getSimpleSource ({cb, nameSingle, namePlural, prop, $propEles}) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple(namePlural, {isMarked: true});
const doUpdateState = () => {
const identObjs = rows.map(row => row.getIdentObject()).filter(Boolean);
if (identObjs.length) this._state[prop] = identObjs;
else delete this._state[prop];
cb();
};
const optsRow = {nameSingle, $propEles};
const rows = [];
const $wrpRows = $(`<div></div>`).appendTo($rowInner);
const doRefresh = () => {
$wrpRows.empty();
rows.splice(0, rows.length);
(this._state[prop] || []).forEach(idObj => this.__$getSimpleSource__getIdentRow(doUpdateState, rows, idObj, optsRow).$wrp.appendTo($wrpRows));
};
doRefresh();
const $wrpBtnAdd = $(`<div></div>`).appendTo($rowInner);
$(`<button class="btn btn-xs btn-default">Add ${nameSingle}</button>`)
.appendTo($wrpBtnAdd)
.click(() => {
this.__$getSimpleSource__getIdentRow(doUpdateState, rows, null, optsRow).$wrp.appendTo($wrpRows);
doUpdateState();
});
return {$row, doRefresh};
}
__$getSimpleSource__getIdentRow (doUpdateState, rows, identObj, {nameSingle, $propEles}) {
const getIdentObject = () => {
const name = $iptName.val().trim();
if (!name) return null;
return {
name: name,
source: $selSource.val().unescapeQuotes(),
};
};
const $iptName = $(`<input class="form-control form-control--minimal input-xs">`)
.change(() => doUpdateState())
.val(identObj ? identObj.name : null);
const $selSource = this._$getSelSource($propEles, doUpdateState, identObj ? identObj.source.escapeQuotes() : Parser.SRC_PHB);
const out = {getIdentObject};
const $wrpBtnRemove = $(`<div class="text-right mb-2"/>`);
const $wrp = $$`<div class="ve-flex-col mkbru__wrp-rows">
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Name</span>${$iptName}</div>
<div class="ve-flex-v-center mb-2"><span class="mr-2 mkbru__sub-name--33">Source</span>${$selSource}</div>
${$wrpBtnRemove}
</div>`;
Builder.$getBtnRemoveRow(doUpdateState, rows, out, $wrp, nameSingle).appendTo($wrpBtnRemove);
out.$wrp = $wrp;
rows.push(out);
return out;
}
__$getBackgrounds (cb) {
return this.__$getSimpleSource({
cb,
nameSingle: "Background",
namePlural: "Backgrounds",
prop: "backgrounds",
$propEles: "$selBackgroundSources",
});
}
__$getOptionalfeatures (cb) {
return this.__$getSimpleSource({
cb,
nameSingle: "Optional Feature",
namePlural: "Optional Features",
prop: "optionalfeatures",
$propEles: "$selOptionalfeatureSources",
});
}
__$getFeats (cb) {
return this.__$getSimpleSource({
cb,
nameSingle: "Feat",
namePlural: "Feats",
prop: "feats",
$propEles: "$selFeatSources",
});
}
__$getCharoptions (cb) {
return this.__$getSimpleSource({
cb,
nameSingle: "Character Creation Option",
namePlural: "Character Creation Options",
prop: "charoptions",
$propEles: "$selCharoptionSources",
});
}
__$getRewards (cb) {
return this.__$getSimpleSource({
cb,
nameSingle: "Supernatural Gift/Reward",
namePlural: "Supernatural Gifts and Rewards",
prop: "rewards",
$propEles: "$selRewardSources",
});
}
// TODO use this in creature builder (_$eles)
_$getSelSource (elesProp, doUpdateState, initialVal) {
const selSource = e_({
tag: "select",
clazz: "form-control input-xs",
children: [...Object.keys(Parser.SOURCE_JSON_TO_FULL), ...this._ui.allSources]
.map(srcJson => e_({tag: "option", val: srcJson, text: Parser.sourceJsonToFull(srcJson)})),
});
const $selSource = $(selSource)
.change(() => doUpdateState());
if (initialVal != null) $selSource.val(initialVal);
(this._$eles[elesProp] = this._$eles[elesProp] || []).push($selSource);
return $selSource;
}
__$getSourcesGenerated (cb, fnsDoRefreshSources) {
const [$row, $rowInner] = BuilderUi.getLabelledRowTuple("Generated", {isMarked: true});
const getBtnAdd = () => {
const $btn = $(`<button class="btn btn-xs btn-default" title="Generate additional spell sources based on the spell's current sources (for example, Eldritch Knight Fighter for a Wizard spell).">Generate Additional</button>`)
.click(async () => {
try {
$btn.prop("disabled", true);
const cpySp = MiscUtil.copyFast(this._state);
const fauxSpellSourceLookup = {};
// TODO(Future) expand to things beyond `class`?
if (cpySp.classes?.fromClassList?.length) MiscUtil.set(fauxSpellSourceLookup, cpySp.source, cpySp.name, "class", cpySp.classes.fromClassList.map(({name, source}) => ({name, source})));
const sourceLookup = await SpellSourceLookupBuilder.pGetLookup({
spells: [
// Load the default spell set to ensure all filters are populated
...MiscUtil.copyFast(await DataUtil.spell.pLoadAll()),
cpySp,
],
spellSourceLookupAdditional: fauxSpellSourceLookup,
});
DataUtil.spell.PROPS_SPELL_SOURCE.forEach(prop => {
cpySp[prop] = cpySp[prop] || MiscUtil.copyFast(this._state[prop]);
// Avoid duplicating existing values
delete cpySp[prop];
});
DataUtil.spell.mutEntityBrewBuilder(cpySp, sourceLookup);
DataUtil.spell.PROPS_SPELL_SOURCE.forEach(prop => this._state[prop] = cpySp[prop]);
cb();
fnsDoRefreshSources.forEach(fn => fn());
} finally {
$btn.prop("disabled", false);
}
});
return $btn;
};
const $btnAdd = getBtnAdd();
$$`<div class="ve-flex-v-center">
${$btnAdd}
</div>`.appendTo($rowInner);
return $row;
}
renderOutput () {
this._renderOutputDebounced();
}
_renderOutput () {
const $wrp = this._ui.$wrpOutput.empty();
// initialise tabs
this._resetTabs({tabGroup: "output"});
const tabs = this._renderTabs(
[
new TabUiUtil.TabMeta({name: "Spell"}),
new TabUiUtil.TabMeta({name: "Info"}),
new TabUiUtil.TabMeta({name: "Images"}),
new TabUiUtil.TabMeta({name: "Data"}),
new TabUiUtil.TabMeta({name: "Markdown"}),
],
{
tabGroup: "output",
cbTabChange: this.doUiSave.bind(this),
},
);
const [spellTab, infoTab, imageTab, dataTab, markdownTab] = tabs;
$$`<div class="ve-flex-v-center w-100 no-shrink">${tabs.map(it => it.$btnTab)}</div>`.appendTo($wrp);
tabs.forEach(it => it.$wrpTab.appendTo($wrp));
// Spell
const $tblSpell = $(`<table class="w-100 stats"/>`).appendTo(spellTab.$wrpTab);
// Make a copy of the spell, and add the data that would be displayed in the spells page
const procSpell = MiscUtil.copy(this._state);
Renderer.spell.initBrewSources(procSpell);
RenderSpells.$getRenderedSpell(procSpell, this._subclassLookup, {isSkipExcludesRender: true}).appendTo($tblSpell);
// Info
const $tblInfo = $(`<table class="w-100 stats"/>`).appendTo(infoTab.$wrpTab);
Renderer.utils.pBuildFluffTab({
isImageTab: false,
$content: $tblInfo,
entity: this._state,
pFnGetFluff: Renderer.spell.pGetFluff,
});
// Images
const $tblImages = $(`<table class="w-100 stats"/>`).appendTo(imageTab.$wrpTab);
Renderer.utils.pBuildFluffTab({
isImageTab: true,
$content: $tblImages,
entity: this._state,
pFnGetFluff: Renderer.spell.pGetFluff,
});
// Data
const $tblData = $(`<table class="w-100 stats stats--book mkbru__wrp-output-tab-data"/>`).appendTo(dataTab.$wrpTab);
const asCode = Renderer.get().render({
type: "entries",
entries: [
{
type: "code",
name: `Data`,
preformatted: JSON.stringify(DataUtil.cleanJson(MiscUtil.copy(this._state)), null, "\t"),
},
],
});
$tblData.append(Renderer.utils.getBorderTr());
$tblData.append(`<tr><td colspan="6">${asCode}</td></tr>`);
$tblData.append(Renderer.utils.getBorderTr());
// Markdown
const $tblMarkdown = $(`<table class="w-100 stats stats--book mkbru__wrp-output-tab-data"/>`).appendTo(markdownTab.$wrpTab);
$tblMarkdown.append(Renderer.utils.getBorderTr());
$tblMarkdown.append(`<tr><td colspan="6">${this._getRenderedMarkdownCode()}</td></tr>`);
$tblMarkdown.append(Renderer.utils.getBorderTr());
}
}
const spellBuilder = new SpellBuilder();
ui.spellBuilder = spellBuilder;
spellBuilder.ui = ui;