"use strict"; class StatGenUi extends BaseComponent { static _PROPS_POINT_BUY_CUSTOM = [ "pb_rules", "pb_budget", "pb_isCustom", ]; /** * @param opts * @param opts.races * @param opts.backgrounds * @param opts.feats * @param [opts.tabMetasAdditional] * @param [opts.isCharacterMode] Disables some functionality (e.g. changing number of ability scores) * @param [opts.isFvttMode] * @param [opts.modalFilterRaces] * @param [opts.modalFilterBackgrounds] * @param [opts.modalFilterFeats] * @param [opts.existingScores] */ constructor (opts) { super(); opts = opts || {}; TabUiUtilSide.decorate(this, {isInitMeta: true}); this._races = opts.races; this._backgrounds = opts.backgrounds; this._feats = opts.feats; this._tabMetasAdditional = opts.tabMetasAdditional; this._isCharacterMode = opts.isCharacterMode; this._isFvttMode = opts.isFvttMode; this._MODES = this._isFvttMode ? StatGenUi.MODES_FVTT : StatGenUi.MODES; if (this._isFvttMode) { let cnt = 0; this._IX_TAB_NONE = cnt++; this._IX_TAB_ROLLED = cnt++; this._IX_TAB_ARRAY = cnt++; this._IX_TAB_PB = cnt++; this._IX_TAB_MANUAL = cnt; } else { this._IX_TAB_NONE = -1; let cnt = 0; this._IX_TAB_ROLLED = cnt++; this._IX_TAB_ARRAY = cnt++; this._IX_TAB_PB = cnt++; this._IX_TAB_MANUAL = cnt; } this._modalFilterRaces = opts.modalFilterRaces || new ModalFilterRaces({namespace: "statgen.races", isRadio: true, allData: this._races}); this._modalFilterBackgrounds = opts.modalFilterBackgrounds || new ModalFilterBackgrounds({namespace: "statgen.backgrounds", isRadio: true, allData: this._backgrounds}); this._modalFilterFeats = opts.modalFilterFeats || new ModalFilterFeats({namespace: "statgen.feats", isRadio: true, allData: this._feats}); this._isLevelUp = !!opts.existingScores; this._existingScores = opts.existingScores; // region Rolled this._$rollIptFormula = null; // endregion // region Point buy this._compAsi = new StatGenUi.CompAsi({parent: this}); // endregion } get MODES () { return this._MODES; } get ixActiveTab () { return this._getIxActiveTab(); } set ixActiveTab (ix) { this._setIxActiveTab({ixActiveTab: ix}); } // region Expose for external use addHookPointBuyCustom (hook) { this.constructor._PROPS_POINT_BUY_CUSTOM.forEach(prop => this._addHookBase(prop, hook)); } addHookAbilityScores (hook) { Parser.ABIL_ABVS.forEach(ab => this._addHookBase(`common_export_${ab}`, hook)); } addHookPulseAsi (hook) { this._addHookBase("common_pulseAsi", hook); } getFormDataAsi () { return this._compAsi.getFormData(); } getMode (ix, namespace) { const {propMode} = this.getPropsAsi(ix, namespace); return this._state[propMode]; } setIxFeat (ix, namespace, ixFeat) { const {propMode, propIxFeat} = this.getPropsAsi(ix, namespace); if (ixFeat == null && (this._state[propMode] === "asi" || this._state[propMode] == null)) { this._state[propIxFeat] = null; return; } this._state[propMode] = "feat"; this._state[propIxFeat] = ixFeat; } setIxFeatSet (namespace, ixSet) { const {propIxSel} = this.getPropsAdditionalFeats_(namespace); this._state[propIxSel] = ixSet; } setIxFeatSetIxFeats (namespace, metaFeats) { const nxtState = {}; metaFeats.forEach(({ix, ixFeat}) => { const {propIxFeat} = this.getPropsAdditionalFeatsFeatSet_(namespace, "fromFilter", ix); nxtState[propIxFeat] = ixFeat; }); this._proxyAssignSimple("state", nxtState); } set common_cntAsi (val) { this._state.common_cntAsi = val; } addHookIxRace (hook) { this._addHookBase("common_ixRace", hook); } get ixRace () { return this._state.common_ixRace; } set ixRace (ixRace) { this._state.common_ixRace = ixRace; } addHookIxBackground (hook) { this._addHookBase("common_ixBackground", hook); } get ixBackground () { return this._state.common_ixBackground; } set ixBackground (ixBackground) { this._state.common_ixBackground = ixBackground; } addCustomFeat () { this._state.common_cntFeatsCustom = Math.min(StatGenUi._MAX_CUSTOM_FEATS, (this._state.common_cntFeatsCustom || 0) + 1); } setCntCustomFeats (val) { this._state.common_cntFeatsCustom = Math.min(StatGenUi._MAX_CUSTOM_FEATS, val || 0); } // endregion // region Expose for ASI component get isCharacterMode () { return this._isCharacterMode; } get state () { return this._state; } get modalFilterFeats () { return this._modalFilterFeats; } get feats () { return this._feats; } addHookBase (prop, hook) { return this._addHookBase(prop, hook); } removeHookBase (prop, hook) { return this._removeHookBase(prop, hook); } proxyAssignSimple (hookProp, toObj, isOverwrite) { return this._proxyAssignSimple(hookProp, toObj, isOverwrite); } get race () { return this._races[this._state.common_ixRace]; } get background () { return this._backgrounds[this._state.common_ixBackground]; } get isLevelUp () { return this._isLevelUp; } // endregion getTotals () { if (this._isLevelUp) { return { mode: "levelUp", totals: { levelUp: this._getTotals_levelUp(), }, }; } return { mode: this._MODES[this.ixActiveTab || 0], totals: { rolled: this._getTotals_rolled(), array: this._getTotals_array(), pointbuy: this._getTotals_pb(), manual: this._getTotals_manual(), }, }; } _getTotals_rolled () { return Parser.ABIL_ABVS.mergeMap(ab => ({[ab]: this._rolled_getTotalScore(ab)})); } _getTotals_array () { return Parser.ABIL_ABVS.mergeMap(ab => ({[ab]: this._array_getTotalScore(ab)})); } _getTotals_pb () { return Parser.ABIL_ABVS.mergeMap(ab => ({[ab]: this._pb_getTotalScore(ab)})); } _getTotals_manual () { return Parser.ABIL_ABVS.mergeMap(ab => ({[ab]: this._manual_getTotalScore(ab)})); } _getTotals_levelUp () { return Parser.ABIL_ABVS.mergeMap(ab => ({[ab]: this._levelUp_getTotalScore(ab)})); } addHook (hookProp, prop, hook) { return this._addHook(hookProp, prop, hook); } addHookAll (hookProp, hook) { this._addHookAll(hookProp, hook); this._compAsi._addHookAll(hookProp, hook); } addHookActiveTag (hook) { this._addHookActiveTab(hook); } async pInit () { await this._modalFilterRaces.pPreloadHidden(); await this._modalFilterBackgrounds.pPreloadHidden(); await this._modalFilterFeats.pPreloadHidden(); } getPropsAsi (ix, namespace) { return { prefix: `common_asi_${namespace}_${ix}_`, propMode: `common_asi_${namespace}_${ix}_mode`, propIxAsiPointOne: `common_asi_${namespace}_${ix}_asiPointOne`, propIxAsiPointTwo: `common_asi_${namespace}_${ix}_asiPointTwo`, propIxFeat: `common_asi_${namespace}_${ix}_ixFeat`, propIxFeatAbility: `common_asi_${namespace}_${ix}_ixFeatAbility`, propFeatAbilityChooseFrom: `common_asi_${namespace}_${ix}_featAbilityChooseFrom`, }; } getPropsAdditionalFeats_ (namespace) { return { propPrefix: `common_additionalFeats_${namespace}_`, propIxSel: `common_additionalFeats_${namespace}_ixSel`, }; } getPropsAdditionalFeatsFeatSet_ (namespace, type, ix) { return { propIxFeat: `common_additionalFeats_${namespace}_${type}_${ix}_ixFeat`, propIxFeatAbility: `common_additionalFeats_${namespace}_${type}_${ix}_ixFeatAbility`, propFeatAbilityChooseFrom: `common_additionalFeats_${namespace}_${type}_${ix}_featAbilityChooseFrom`, }; } _roll_getRolledStats () { const wrpTree = Renderer.dice.lang.getTree3(this._state.rolled_formula); if (!wrpTree) return this._$rollIptFormula.addClass("form-control--error"); const rolls = []; for (let i = 0; i < this._state.rolled_rollCount; i++) { const meta = {}; meta.total = wrpTree.tree.evl(meta); rolls.push(meta); } rolls.sort((a, b) => SortUtil.ascSort(b.total, a.total)); return rolls.map(r => ({total: r.total, text: (r.text || []).join("")})); } render ($parent) { $parent.empty().addClass("statgen"); const iptTabMetas = this._isLevelUp ? [ new TabUiUtil.TabMeta({name: "Existing", icon: this._isFvttMode ? `fas fa-fw fa-user` : `far fa-fw fa-user`, hasBorder: true}), ...this._tabMetasAdditional || [], ] : [ this._isFvttMode ? new TabUiUtil.TabMeta({name: "Select...", icon: this._isFvttMode ? `fas fa-fw fa-square` : `far fa-fw fa-square`, hasBorder: true, isNoPadding: this._isFvttMode}) : null, new TabUiUtil.TabMeta({name: "Roll", icon: this._isFvttMode ? `fas fa-fw fa-dice` : `far fa-fw fa-dice`, hasBorder: true, isNoPadding: this._isFvttMode}), new TabUiUtil.TabMeta({name: "Standard Array", icon: this._isFvttMode ? `fas fa-fw fa-signal` : `far fa-fw fa-signal-alt`, hasBorder: true, isNoPadding: this._isFvttMode}), new TabUiUtil.TabMeta({name: "Point Buy", icon: this._isFvttMode ? `fas fa-fw fa-chart-bar` : `far fa-fw fa-chart-bar`, hasBorder: true, isNoPadding: this._isFvttMode}), new TabUiUtil.TabMeta({name: "Manual", icon: this._isFvttMode ? `fas fa-fw fa-tools` : `far fa-fw fa-tools`, hasBorder: true, isNoPadding: this._isFvttMode}), ...this._tabMetasAdditional || [], ].filter(Boolean); const tabMetas = this._renderTabs(iptTabMetas, {$parent: this._isFvttMode ? null : $parent}); if (this._isFvttMode) { if (!this._isLevelUp) { const {propActive: propActiveTab, propProxy: propProxyTabs} = this._getTabProps(); const $selMode = ComponentUiUtil.$getSelEnum( this, propActiveTab, { values: iptTabMetas.map((_, ix) => ix), fnDisplay: ix => iptTabMetas[ix].name, propProxy: propProxyTabs, }, ) .addClass("max-w-200p"); $$`
Mode
${$selMode}

`.appendTo($parent); } tabMetas.forEach(it => it.$wrpTab.appendTo($parent)); } const $wrpAll = $(`
`); this._render_all($wrpAll); const hkTab = () => { tabMetas[this.ixActiveTab || 0].$wrpTab.append($wrpAll); }; this._addHookActiveTab(hkTab); hkTab(); this._addHookBase("common_cntAsi", () => this._state.common_pulseAsi = !this._state.common_pulseAsi); this._addHookBase("common_cntFeatsCustom", () => this._state.common_pulseAsi = !this._state.common_pulseAsi); } _render_$getStgRolledHeader () { this._$rollIptFormula = ComponentUiUtil.$getIptStr(this, "rolled_formula") .addClass("ve-text-center max-w-100p") .keydown(evt => { if (evt.key === "Enter") setTimeout(() => $btnRoll.click()); // Defer to allow `.change` to fire first }) .change(() => this._$rollIptFormula.removeClass("form-control--error")); const $iptRollCount = this._isCharacterMode ? null : ComponentUiUtil.$getIptInt(this, "rolled_rollCount", 1, {min: 1, fallbackOnNaN: 1, html: ``}) .keydown(evt => { if (evt.key === "Enter") setTimeout(() => $btnRoll.click()); // Defer to allow `.change` to fire first }) .change(() => this._$rollIptFormula.removeClass("form-control--error")); const $btnRoll = $(``) .click(() => { this._state.rolled_rolls = this._roll_getRolledStats(); }); const $btnRandom = $(``) .hideVe() .click(() => { const abs = [...Parser.ABIL_ABVS].shuffle(); abs.forEach((ab, i) => { const {propAbilSelectedRollIx} = this.constructor._rolled_getProps(ab); this._state[propAbilSelectedRollIx] = i; }); }); const $wrpRolled = $(`
`); const $wrpRolledOuter = $$`
=
${$wrpRolled}
`; const hkRolled = () => { $wrpRolledOuter.toggleVe(this._state.rolled_rolls.length); $btnRandom.toggleVe(this._state.rolled_rolls.length); $wrpRolled.html(this._state.rolled_rolls.map((it, i) => { const cntPrevRolls = this._state.rolled_rolls.slice(0, i).filter(r => r.total === it.total).length; return `
[
${it.total}${cntPrevRolls ? Parser.numberToSubscript(cntPrevRolls) : ""}
]
`; })); }; this._addHookBase("rolled_rolls", hkRolled); hkRolled(); return $$`
${this._isCharacterMode ? null : $$``}
${$btnRoll}
${$wrpRolledOuter}
${$btnRandom}
`; } _render_$getStgArrayHeader () { const $btnRandom = $(``) .click(() => { const abs = [...Parser.ABIL_ABVS].shuffle(); abs.forEach((ab, i) => { const {propAbilSelectedScoreIx} = this.constructor._array_getProps(ab); this._state[propAbilSelectedScoreIx] = i; }); }); return $$`
Assign these numbers to your abilities as desired:
${StatGenUi._STANDARD_ARRAY.join(", ")}
${$btnRandom}
`; } _render_$getStgManualHeader () { return $$`
Enter your desired ability scores in the "Base" column below.
`; } _doReset () { if (this._isLevelUp) return; // Should never occur const nxtState = this._getDefaultStateCommonResettable(); switch (this.ixActiveTab) { case this._IX_TAB_NONE: Object.assign(nxtState, this._getDefaultStateNoneResettable()); break; case this._IX_TAB_ROLLED: Object.assign(nxtState, this._getDefaultStateRolledResettable()); break; case this._IX_TAB_ARRAY: Object.assign(nxtState, this._getDefaultStateArrayResettable()); break; case this._IX_TAB_PB: Object.assign(nxtState, this._getDefaultStatePointBuyResettable()); break; case this._IX_TAB_MANUAL: Object.assign(nxtState, this._getDefaultStateManualResettable()); break; } this._proxyAssignSimple("state", nxtState); } doResetAll () { this._proxyAssignSimple("state", this._getDefaultState(), true); } _render_$getStgPbHeader () { const $iptBudget = ComponentUiUtil.$getIptInt( this, "pb_budget", 0, { html: ``, min: 0, fallbackOnNaN: 0, }, ); const hkIsCustom = () => { $iptBudget.attr("readonly", !this._state.pb_isCustom); }; this._addHookBase("pb_isCustom", hkIsCustom); hkIsCustom(); const $iptRemaining = ComponentUiUtil.$getIptInt( this, "pb_points", 0, { html: ``, min: 0, fallbackOnNaN: 0, }, ).attr("readonly", true); const hkPoints = () => { this._state.pb_points = this._pb_getPointsRemaining(this._state); $iptRemaining.toggleClass(`statgen-pb__ipt-budget--error`, this._state.pb_points < 0); }; this._addHookAll("state", hkPoints); hkPoints(); const $btnReset = $(``) .click(() => this._doReset()); const $btnRandom = $(``) .click(() => { this._doReset(); let canIncrease = Parser.ABIL_ABVS.map(it => `pb_${it}`); const cpyBaseState = canIncrease.mergeMap(it => ({[it]: this._state[it]})); const cntRemaining = this._pb_getPointsRemaining(cpyBaseState); if (cntRemaining <= 0) return; for (let step = 0; step < 10000; ++step) { if (!canIncrease.length) break; const prop = RollerUtil.rollOnArray(canIncrease); if (!this._state.pb_rules.some(rule => rule.entity.score === cpyBaseState[prop] + 1)) { canIncrease = canIncrease.filter(it => it !== prop); continue; } const draftCpyBaseState = MiscUtil.copy(cpyBaseState); draftCpyBaseState[prop]++; const cntRemaining = this._pb_getPointsRemaining(draftCpyBaseState); if (cntRemaining > 0) { Object.assign(cpyBaseState, draftCpyBaseState); } else if (cntRemaining === 0) { this._proxyAssignSimple("state", draftCpyBaseState); break; } else { canIncrease = canIncrease.filter(it => it !== prop); } } }); return $$`
 
${$btnReset}
 
${$btnRandom}
`; } _render_$getStgPbCustom () { const $btnAddLower = $(``) .click(() => { const prevLowest = this._state.pb_rules[0]; const score = prevLowest.entity.score - 1; const cost = prevLowest.entity.cost; this._state.pb_rules = [this._getDefaultState_pb_rule(score, cost), ...this._state.pb_rules]; }); const $btnAddHigher = $(``) .click(() => { const prevHighest = this._state.pb_rules.last(); const score = prevHighest.entity.score + 1; const cost = prevHighest.entity.cost; this._state.pb_rules = [...this._state.pb_rules, this._getDefaultState_pb_rule(score, cost)]; }); const $btnResetRules = $(``) .click(() => { this._state.pb_rules = this._getDefaultStatePointBuyCosts().pb_rules; }); const menuCustom = ContextUtil.getMenu([ new ContextUtil.Action( "Export as Code", async () => { await MiscUtil.pCopyTextToClipboard(this._serialize_pb_rules()); JqueryUtil.showCopiedEffect($btnContext); }, ), new ContextUtil.Action( "Import from Code", async () => { const raw = await InputUiUtil.pGetUserString({title: "Enter Code", isCode: true}); if (raw == null) return; const parsed = this._deserialize_pb_rules(raw); if (parsed == null) return; const {pb_rules, pb_budget} = parsed; this._proxyAssignSimple( "state", { pb_rules, pb_budget, pb_isCustom: true, }, ); JqueryUtil.doToast("Imported!"); }, ), ]); const $btnContext = $(``) .click(evt => ContextUtil.pOpenMenu(evt, menuCustom)); const $stgCustomCostControls = $$`
${$btnAddLower}${$btnAddHigher}
${$btnResetRules} ${$btnContext}
`; const $stgCostRows = $$`
`; const renderableCollectionRules = new StatGenUi.RenderableCollectionPbRules( this, $stgCostRows, ); const hkRules = () => { renderableCollectionRules.render(); // region Clamp values between new min/max scores const {min: minScore, max: maxScore} = this._pb_getMinMaxScores(); Parser.ABIL_ABVS.forEach(it => { const prop = `pb_${it}`; this._state[prop] = Math.min(maxScore, Math.max(minScore, this._state[prop])); }); // endregion }; this._addHookBase("pb_rules", hkRules); hkRules(); let lastIsCustom = this._state.pb_isCustom; const hkIsCustomReset = () => { $stgCustomCostControls.toggleVe(this._state.pb_isCustom); if (lastIsCustom === this._state.pb_isCustom) return; lastIsCustom = this._state.pb_isCustom; // On resetting to non-custom, reset the rules if (!this._state.pb_isCustom) this._state.pb_rules = this._getDefaultStatePointBuyCosts().pb_rules; }; this._addHookBase("pb_isCustom", hkIsCustomReset); hkIsCustomReset(); return $$`

Ability Score Point Cost

Score
Modifier
Point Cost
${$stgCostRows}
${$stgCustomCostControls}

`; } _serialize_pb_rules () { const out = [ this._state.pb_budget, ...MiscUtil.copyFast(this._state.pb_rules).map(it => [it.entity.score, it.entity.cost]), ]; return JSON.stringify(out); } static _DESERIALIZE_MSG_INVALID = "Code was not valid!"; _deserialize_pb_rules (raw) { let json; try { json = JSON.parse(raw); } catch (e) { JqueryUtil.doToast({type: "danger", content: `Failed to decode JSON! ${e.message}`}); return null; } if (!(json instanceof Array)) return void JqueryUtil.doToast({type: "danger", content: this.constructor._DESERIALIZE_MSG_INVALID}); const [budget, ...rules] = json; if (isNaN(budget)) return void JqueryUtil.doToast({type: "danger", content: this.constructor._DESERIALIZE_MSG_INVALID}); if ( !rules .every(it => it instanceof Array && it[0] != null && !isNaN(it[0]) && it[1] != null && !isNaN(it[1])) ) return void JqueryUtil.doToast({type: "danger", content: this.constructor._DESERIALIZE_MSG_INVALID}); return { pb_budget: budget, pb_rules: rules.map(it => this._getDefaultState_pb_rule(it[0], it[1])), }; } _render_all ($wrpTab) { if (this._isLevelUp) return this._render_isLevelUp($wrpTab); this._render_isLevelOne($wrpTab); } _render_isLevelOne ($wrpTab) { let $stgNone; let $stgMain; const $elesRolled = []; const $elesArray = []; const $elesPb = []; const $elesManual = []; // region Rolled header const $stgRolledHeader = this._render_$getStgRolledHeader(); const hkStgRolled = () => $stgRolledHeader.toggleVe(this.ixActiveTab === this._IX_TAB_ROLLED); this._addHookActiveTab(hkStgRolled); hkStgRolled(); // endregion // region Point Buy stages const $stgPbHeader = this._render_$getStgPbHeader(); const $stgPbCustom = this._render_$getStgPbCustom(); const $vrPbCustom = $(`
`); const $hrPbCustom = $(`
`); const hkStgPb = () => { $stgPbHeader.toggleVe(this.ixActiveTab === this._IX_TAB_PB); $stgPbCustom.toggleVe(this.ixActiveTab === this._IX_TAB_PB); $vrPbCustom.toggleVe(this.ixActiveTab === this._IX_TAB_PB); $hrPbCustom.toggleVe(this.ixActiveTab === this._IX_TAB_PB); }; this._addHookActiveTab(hkStgPb); hkStgPb(); // endregion // region Array header const $stgArrayHeader = this._render_$getStgArrayHeader(); const hkStgArray = () => $stgArrayHeader.toggleVe(this.ixActiveTab === this._IX_TAB_ARRAY); this._addHookActiveTab(hkStgArray); hkStgArray(); // endregion // region Manual header const $stgManualHeader = this._render_$getStgManualHeader(); const hkStgManual = () => $stgManualHeader.toggleVe(this.ixActiveTab === this._IX_TAB_MANUAL); this._addHookActiveTab(hkStgManual); hkStgManual(); // endregion // region Other elements const hkElesMode = () => { $stgNone.toggleVe(this.ixActiveTab === this._IX_TAB_NONE); $stgMain.toggleVe(this.ixActiveTab !== this._IX_TAB_NONE); $elesRolled.forEach($ele => $ele.toggleVe(this.ixActiveTab === this._IX_TAB_ROLLED)); $elesArray.forEach($ele => $ele.toggleVe(this.ixActiveTab === this._IX_TAB_ARRAY)); $elesPb.forEach($ele => $ele.toggleVe(this.ixActiveTab === this._IX_TAB_PB)); $elesManual.forEach($ele => $ele.toggleVe(this.ixActiveTab === this._IX_TAB_MANUAL)); }; this._addHookActiveTab(hkElesMode); // endregion const $btnResetRolledOrArrayOrManual = $(``) .click(() => this._doReset()); const hkRolledOrArray = () => $btnResetRolledOrArrayOrManual.toggleVe(this.ixActiveTab === this._IX_TAB_ROLLED || this.ixActiveTab === this._IX_TAB_ARRAY || this.ixActiveTab === this._IX_TAB_MANUAL); this._addHookActiveTab(hkRolledOrArray); hkRolledOrArray(); const $wrpsBase = Parser.ABIL_ABVS.map(ab => { // region Rolled const {propAbilSelectedRollIx} = this.constructor._rolled_getProps(ab); const $selRolled = $(``) .change(() => { const ix = Number($selRolled.val()); const nxtState = { ...Parser.ABIL_ABVS .map(ab => this.constructor._rolled_getProps(ab).propAbilSelectedRollIx) .filter(prop => ix != null && this._state[prop] === ix) .mergeMap(prop => ({[prop]: null})), [propAbilSelectedRollIx]: ~ix ? ix : null, }; this._proxyAssignSimple("state", nxtState); }); $(`