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(``)); 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; $$`
${tabs.map(it => it.$btnTab)}
`.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(""At Higher Levels" 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); $(`
Note: the following data is used by filters on the Spells page.
`).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 = $(`
`).appendTo($rowInner); (this._state.otherSources || []).forEach(it => this.__$getOtherSourcesInput__getOtherSourceRow(doUpdateState, otherSourceRows, it).$wrp.appendTo($wrpRows)); const $wrpBtnAdd = $(`
`).appendTo($rowInner); $(``) .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 = $(``) .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 = $(`
`); const $wrp = $$`
Source${$selSource}
Page${$iptPage}
${$wrpBtnRemove}
`; 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 = $(`
`).appendTo($rowInner); this._state.time.forEach(time => SpellBuilder.__$getTimeInput__getTimeRow(doUpdateState, timeRows, time).$wrp.appendTo($wrpRows)); const $wrpBtnAdd = $(`
`).appendTo($rowInner); $(``) .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 = $(``) .change(() => doUpdateState()) .val(time.number); const ixInitial = keys.indexOf(time.unit); const $selUnit = $(``) .val(~ixInitial ? `${ixInitial}` : "0") .change(() => { const isReaction = keys[$selUnit.val()] === Parser.SP_TM_REACTION; $stageCond.toggleVe(isReaction); doUpdateState(); }); const $iptCond = $(``) .change(() => doUpdateState()) .val(time.condition); const out = {getTime}; const $stageCond = $$`
Condition${$iptCond}
`.toggleVe(ixInitial === 2); const $wrpBtnRemove = $(`
`); const $wrp = $$`
${$iptNum}${$selUnit}
${$stageCond} ${$wrpBtnRemove}
`; 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 = $(``).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(); }); $$`
Range Type ${$selRange}
`.appendTo($rowInner); // DISTANCE TYPE const ixInitialDist = this._state.range.distance ? DIST_TYPES.findIndex(it => it.type === this._state.range.distance.type) : -1; const $selDistance = $(``).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 = $$`
Distance Type ${$selDistance}
`.appendTo($rowInner).toggleVe(isInitialDistance); // AMOUNT const initialAmount = MiscUtil.get(this._state, "range", "distance", "amount"); const $iptAmount = $(``) .change(() => doUpdateState()) .val(initialAmount); const $stageAmount = $$`
Distance Amount ${$iptAmount}
`.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 = $(``) .prop("checked", this._state.components && this._state.components.v) .change(() => doUpdateState()); const $cbSomatic = $(``) .prop("checked", this._state.components && this._state.components.s) .change(() => doUpdateState()); const $cbRoyalty = $(``) .prop("checked", this._state.components && this._state.components.r) .change(() => doUpdateState()); const $iptMaterial = $(``) .val(initialMaterialMode === "1" ? this._state.components.m : initialMaterialMode === "2" ? this._state.components.m.text : null) .change(() => doUpdateState()); const $selMaterial = $(``).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); $$`
Verbal
${$cbVerbal}
Somatic
${$cbSomatic}
Royalty
${$cbRoyalty}
Material Type
${$selMaterial}
`.appendTo($rowInner); // BASIC MATERIAL const $stageMaterial = $$`
Materials
${$iptMaterial}
`.appendTo($rowInner).toggleVe(initialMaterialMode === "1" || initialMaterialMode === "2"); // CONSUMABLE MATERIAL const $cbConsumed = $(``) .prop("checked", this._state.components && this._state.components.m && this._state.components.m.consume) .change(() => doUpdateState()); const $iptCost = $(``) .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 "Materials" field, above."; const $stageMaterialConsumable = $$`
Is Consumed
${$cbConsumed}
Component Cost
${$iptCost}
cp
`.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 = $(``) .prop("checked", this._state.meta && this._state.meta.ritual) .change(() => doUpdateState()); const $cbTechnomagic = $(``) .prop("checked", this._state.meta && this._state.meta.technomagic) .change(() => doUpdateState()); $$`
Ritual
${$cbRitual}
Technomagic
${$cbTechnomagic}
`.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 = $(`
`).appendTo($rowInner); this._state.duration.forEach(duration => SpellBuilder.__$getDurationInput__getDurationRow(doUpdateState, durationRows, duration).$wrp.appendTo($wrpRows)); const $wrpBtnAdd = $(`
`).appendTo($rowInner); $(``) .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 = $(``).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 = $(``).val(ixInitialAmount).change(() => doUpdateState()); const $iptAmount = $(``) .val(duration.duration ? duration.duration.amount : null) .change(() => doUpdateState()); const $cbConc = $(``) .prop("checked", !!duration.concentration) .change(() => doUpdateState()); const $cbUpTo = $(``) .prop("checked", duration.duration ? duration.duration.upTo : false) .change(() => doUpdateState()); const $stageAmount = $$`
Concentration${$cbConc}
Up To...${$cbUpTo}
${$iptAmount}${$selAmountType}
`.toggleVe(!!typeInitial.hasAmount); // ENDS const endRows = []; const $wrpEndRows = $(`
`); const $btnAddEnd = $(``) .click(() => { SpellBuilder.__$getDurationInput__getDurationRow__getEndRow(doUpdateState, endRows, "dispel").$wrp.appendTo($wrpEndRows); doUpdateState(); }); const $stageEnds = $$`
${$wrpEndRows}
${$btnAddEnd}
`.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 = $(`
`); const $wrp = $$`
Duration Type${$selDurationType}
${$stageAmount} ${$stageEnds} ${$wrpBtnRemove}
`; 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 = $(``).val(ixInitialEnd).change(() => doUpdateState()); const out = {getEnd}; const $wrpBtnRemove = $(`
`); const $wrp = $$`
${$selEndType}${$wrpBtnRemove}
`; 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 = $(`
`).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 = $(`
`).appendTo($rowInnerCls); $(``) .appendTo($wrpBtnAddCls) .click(() => { this.__$getClassesInputs__getClassRow(doUpdateState, classRows, MiscUtil.copy(DEFAULT_CLASS)).$wrp.appendTo($wrpRowsCls); doUpdateState(); }); // SUBCLASSES const $wrpRowsSc = $(`
`).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 = $(`
`).appendTo($rowInnerSc); $(``) .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 = $(``) .change(() => doUpdateState()) .val(cls.name); const $selClassSource = this._$getSelSource("$selClassSources", doUpdateState, cls.source.escapeQuotes()); const out = {getClass}; const $wrpBtnRemove = $(`
`); const $wrp = $$`
Class Name${$iptClass}
Class Source${$selClassSource}
${$wrpBtnRemove}
`; 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 = $(``) .change(() => doUpdateState()) .val(subclass.class.name); const $selClassSource = this._$getSelSource("$selClassSources", doUpdateState, subclass.class.source.escapeQuotes()); const $iptSubclass = $(``) .change(() => doUpdateState()) .val(subclass.subclass.name); const $iptSubclassShort = $(``) .change(() => doUpdateState()) .val(subclass.subclass.shortName); const $selSubclassSource = this._$getSelSource("$selSubclassSources", doUpdateState, subclass.subclass.source.escapeQuotes()); const $iptSubSubclass = $(``) .change(() => doUpdateState()) .val(subclass.subclass.subSubclass ? subclass.subclass.subSubclass : null); const out = {getSubclass}; const $wrpBtnRemove = $(`
`); const $wrp = $$`
Class Name${$iptClass}
Class Source${$selClassSource}
Subclass Name${$iptSubclass}
Subclass Short Name${$iptSubclassShort}
Subclass Source${$selSubclassSource}
Sub-Subclass Name${$iptSubSubclass}
${$wrpBtnRemove}
`; 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 = $(`
`).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 = $(`
`).appendTo($rowInner); $(``) .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 = $(``) .change(() => doUpdateState()) .val(race ? race.name : null); const $iptBaseRace = $(``) .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 = $(`
`); const $wrp = $$`
Name${$iptRace}
Source${$selSource}
Base Name${$iptBaseRace}
Base Source${$selBaseSource}
${$wrpBtnRemove}
`; 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 = $(`
`).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 = $(`
`).appendTo($rowInner); $(``) .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 = $(``) .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 = $(`
`); const $wrp = $$`
Name${$iptName}
Source${$selSource}
${$wrpBtnRemove}
`; 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 = $(``) .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(); $$`
${$btnAdd}
`.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; $$`
${tabs.map(it => it.$btnTab)}
`.appendTo($wrp); tabs.forEach(it => it.$wrpTab.appendTo($wrp)); // Spell const $tblSpell = $(``).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 = $(`
`).appendTo(infoTab.$wrpTab); Renderer.utils.pBuildFluffTab({ isImageTab: false, $content: $tblInfo, entity: this._state, pFnGetFluff: Renderer.spell.pGetFluff, }); // Images const $tblImages = $(`
`).appendTo(imageTab.$wrpTab); Renderer.utils.pBuildFluffTab({ isImageTab: true, $content: $tblImages, entity: this._state, pFnGetFluff: Renderer.spell.pGetFluff, }); // Data const $tblData = $(`
`).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(``); $tblData.append(Renderer.utils.getBorderTr()); // Markdown const $tblMarkdown = $(`
${asCode}
`).appendTo(markdownTab.$wrpTab); $tblMarkdown.append(Renderer.utils.getBorderTr()); $tblMarkdown.append(``); $tblMarkdown.append(Renderer.utils.getBorderTr()); } } const spellBuilder = new SpellBuilder(); ui.spellBuilder = spellBuilder; spellBuilder.ui = ui;
${this._getRenderedMarkdownCode()}