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

2605 lines
87 KiB
JavaScript

"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");
$$`<div class="ve-flex-v-center statgen-shared__wrp-header">
<div class="mr-2"><b>Mode</b></div>
${$selMode}
</div>
<hr class="hr-2">`.appendTo($parent);
}
tabMetas.forEach(it => it.$wrpTab.appendTo($parent));
}
const $wrpAll = $(`<div class="ve-flex-col w-100 h-100"></div>`);
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: `<input type="text" class="form-control input-xs form-control--minimal 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 $btnRoll = $(`<button class="btn btn-primary bold">Roll</button>`)
.click(() => {
this._state.rolled_rolls = this._roll_getRolledStats();
});
const $btnRandom = $(`<button class="btn btn-xs btn-default mt-2">Randomly Assign</button>`)
.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 = $(`<div class="ve-flex-v-center mr-auto statgen-rolled__wrp-results py-1"></div>`);
const $wrpRolledOuter = $$`<div class="ve-flex-v-center"><div class="mr-2">=</div>${$wrpRolled}</div>`;
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 `<div class="px-3 py-1 help-subtle ve-flex-vh-center" title="${it.text}"><div class="ve-muted">[</div><div class="ve-flex-vh-center statgen-rolled__disp-result">${it.total}${cntPrevRolls ? Parser.numberToSubscript(cntPrevRolls) : ""}</div><div class="ve-muted">]</div></div>`;
}));
};
this._addHookBase("rolled_rolls", hkRolled);
hkRolled();
return $$`<div class="ve-flex-col mb-3 mr-auto">
<div class="ve-flex mb-2">
<div class="ve-flex-col ve-flex-h-center mr-3">
<label class="ve-flex-v-center"><div class="mr-2 no-shrink w-100p">Formula:</div>${this._$rollIptFormula}</label>
${this._isCharacterMode ? null : $$`<label class="ve-flex-v-center mt-2"><div class="mr-2 no-shrink w-100p">Number of rolls:</div>${$iptRollCount}</label>`}
</div>
${$btnRoll}
</div>
${$wrpRolledOuter}
<div class="ve-flex-v-center">${$btnRandom}</div>
</div>`;
}
_render_$getStgArrayHeader () {
const $btnRandom = $(`<button class="btn btn-xs btn-default">Randomly Assign</button>`)
.click(() => {
const abs = [...Parser.ABIL_ABVS].shuffle();
abs.forEach((ab, i) => {
const {propAbilSelectedScoreIx} = this.constructor._array_getProps(ab);
this._state[propAbilSelectedScoreIx] = i;
});
});
return $$`<div class="ve-flex-col mb-3 mr-auto">
<div class="mb-2">Assign these numbers to your abilities as desired:</div>
<div class="bold mb-2">${StatGenUi._STANDARD_ARRAY.join(", ")}</div>
<div class="ve-flex">${$btnRandom}</div>
</div>`;
}
_render_$getStgManualHeader () {
return $$`<div class="ve-flex-col mb-3 mr-auto">
<div>Enter your desired ability scores in the &quot;Base&quot; column below.</div>
</div>`;
}
_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: `<input type="text" class="form-control statgen-pb__ipt-budget ve-text-center statgen-shared__ipt">`,
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: `<input type="text" class="form-control statgen-pb__ipt-budget ve-text-center statgen-shared__ipt">`,
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 = $(`<button class="btn btn-default">Reset</button>`)
.click(() => this._doReset());
const $btnRandom = $(`<button class="btn btn-default">Random</button>`)
.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 $$`<div class="ve-flex mobile__ve-flex-col mb-2">
<div class="ve-flex-v-center">
<div class="statgen-pb__cell mr-4 mobile__hidden"></div>
<label class="ve-flex-col mr-2">
<div class="mb-1 ve-text-center">Budget</div>
${$iptBudget}
</label>
<label class="ve-flex-col mr-2">
<div class="mb-1 ve-text-center">Remain</div>
${$iptRemaining}
</label>
</div>
<div class="ve-flex-v-center mobile__mt-2">
<div class="ve-flex-col mr-2">
<div class="mb-1 ve-text-center mobile__hidden">&nbsp;</div>
${$btnReset}
</div>
<div class="ve-flex-col">
<div class="mb-1 ve-text-center mobile__hidden">&nbsp;</div>
${$btnRandom}
</div>
</div>
</div>`;
}
_render_$getStgPbCustom () {
const $btnAddLower = $(`<button class="btn btn-default btn-xs">Add Lower Score</button>`)
.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 = $(`<button class="btn btn-default btn-xs">Add Higher Score</button>`)
.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 = $(`<button class="btn btn-danger btn-xs mr-2">Reset</button>`)
.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 = $(`<button class="btn btn-default btn-xs" title="Menu"><span class="glyphicon glyphicon-option-vertical"></span></button>`)
.click(evt => ContextUtil.pOpenMenu(evt, menuCustom));
const $stgCustomCostControls = $$`<div class="ve-flex-col mb-auto ml-2 mobile__ml-0 mobile__mt-3">
<div class="btn-group-vertical ve-flex-col mb-2">${$btnAddLower}${$btnAddHigher}</div>
<div class="ve-flex-v-center">
${$btnResetRules}
${$btnContext}
</div>
</div>`;
const $stgCostRows = $$`<div class="ve-flex-col"></div>`;
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 $$`<div class="ve-flex-col">
<h4>Ability Score Point Cost</h4>
<div class="ve-flex-col">
<div class="ve-flex mobile__ve-flex-col">
<div class="ve-flex-col mr-3mobile__mr-0">
<div class="ve-flex-v-center mb-1">
<div class="statgen-pb__col-cost ve-flex-vh-center bold">Score</div>
<div class="statgen-pb__col-cost ve-flex-vh-center bold">Modifier</div>
<div class="statgen-pb__col-cost ve-flex-vh-center bold">Point Cost</div>
<div class="statgen-pb__col-cost-delete"></div>
</div>
${$stgCostRows}
</div>
${$stgCustomCostControls}
</div>
</div>
<hr class="hr-4 mb-2">
<label class="ve-flex-v-center">
<div class="mr-2">Custom Rules</div>
${ComponentUiUtil.$getCbBool(this, "pb_isCustom")}
</label>
</div>`;
}
_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 = $(`<div class="vr-5 mobile-ish__hidden"></div>`);
const $hrPbCustom = $(`<hr class="hr-5 mobile-ish__visible">`);
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 = $(`<button class="btn btn-default btn-xxs relative statgen-shared__btn-reset" title="Reset"><span class="glyphicon glyphicon-refresh"></span></button>`)
.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 = $(`<select class="form-control input-xs form-control--minimal statgen-shared__ipt statgen-shared__ipt--sel"></select>`)
.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);
});
$(`<option/>`, {value: -1, text: "\u2014"}).appendTo($selRolled);
let $optionsRolled = [];
const hkRolls = () => {
$optionsRolled.forEach($opt => $opt.remove());
this._state.rolled_rolls.forEach((it, i) => {
const cntPrevRolls = this._state.rolled_rolls.slice(0, i).filter(r => r.total === it.total).length;
const $opt = $(`<option/>`, {value: i, text: `${it.total}${cntPrevRolls ? Parser.numberToSubscript(cntPrevRolls) : ""}`}).appendTo($selRolled);
$optionsRolled.push($opt);
});
let nxtSelIx = this._state[propAbilSelectedRollIx];
if (nxtSelIx >= this._state.rolled_rolls.length) nxtSelIx = null;
$selRolled.val(`${nxtSelIx == null ? -1 : nxtSelIx}`);
if ((nxtSelIx) !== this._state[propAbilSelectedRollIx]) this._state[propAbilSelectedRollIx] = nxtSelIx;
};
this._addHookBase("rolled_rolls", hkRolls);
hkRolls();
const hookIxRolled = () => {
const ix = this._state[propAbilSelectedRollIx] == null ? -1 : this._state[propAbilSelectedRollIx];
$selRolled.val(`${ix}`);
};
this._addHookBase(propAbilSelectedRollIx, hookIxRolled);
hookIxRolled();
$elesRolled.push($selRolled);
// endregion
// region Array
const {propAbilSelectedScoreIx} = this.constructor._array_getProps(ab);
const $selArray = $(`<select class="form-control input-xs form-control--minimal statgen-shared__ipt statgen-shared__ipt--sel"></select>`)
.change(() => {
const ix = Number($selArray.val());
const nxtState = {
...Parser.ABIL_ABVS
.map(ab => this.constructor._array_getProps(ab).propAbilSelectedScoreIx)
.filter(prop => ix != null && this._state[prop] === ix)
.mergeMap(prop => ({[prop]: null})),
[propAbilSelectedScoreIx]: ~ix ? ix : null,
};
this._proxyAssignSimple("state", nxtState);
});
$(`<option/>`, {value: -1, text: "\u2014"}).appendTo($selArray);
StatGenUi._STANDARD_ARRAY.forEach((it, i) => $(`<option/>`, {value: i, text: it}).appendTo($selArray));
const hookIxArray = () => {
const ix = this._state[propAbilSelectedScoreIx] == null ? -1 : this._state[propAbilSelectedScoreIx];
$selArray.val(`${ix}`);
};
this._addHookBase(propAbilSelectedScoreIx, hookIxArray);
hookIxArray();
$elesArray.push($selArray);
// endregion
// region Point buy
const propPb = `pb_${ab}`;
const $iptPb = ComponentUiUtil.$getIptInt(
this,
propPb,
0,
{
fallbackOnNaN: 0,
min: 0,
html: `<input class="form-control form-control--minimal statgen-shared__ipt text-right" type="number">`,
},
);
const hkPb = () => {
const {min: minScore, max: maxScore} = this._pb_getMinMaxScores();
this._state[propPb] = Math.min(maxScore, Math.max(minScore, this._state[propPb]));
};
this._addHookBase(propPb, hkPb);
hkPb();
$elesPb.push($iptPb);
// endregion
// region Manual
const {propAbilValue} = this.constructor._manual_getProps(ab);
const $iptManual = ComponentUiUtil.$getIptInt(
this,
propAbilValue,
0,
{
fallbackOnNaN: 0,
html: `<input class="form-control form-control--minimal statgen-shared__ipt text-right" type="number">`,
},
);
$elesManual.push($iptManual);
// endregion
return $$`<label class="my-1 statgen-pb__cell">
${$selRolled}
${$selArray}
${$iptPb}
${$iptManual}
</label>`;
});
const $wrpsUser = this._render_$getWrpsUser();
const metasTotalAndMod = this._render_getMetasTotalAndMod();
const {
$wrpOuter: $wrpRaceOuter,
$stgSel: $stgRaceSel,
$dispPreview: $dispPreviewRace,
$hrPreview: $hrPreviewRaceTashas,
$dispTashas,
} = this._renderLevelOneRace.render();
const {
$wrpOuter: $wrpBackgroundOuter,
$stgSel: $stgBackgroundSel,
$dispPreview: $dispPreviewBackground,
$hrPreview: $hrPreviewBackground,
} = this._renderLevelOneBackground.render();
const $wrpAsi = this._render_$getWrpAsi();
$stgNone = $$`<div class="ve-flex-col w-100 h-100">
<div class="ve-flex-v-center"><i>Please select a mode.</i></div>
</div>`;
$stgMain = $$`<div class="ve-flex-col w-100 h-100">
${$stgRolledHeader}
${$stgArrayHeader}
${$stgManualHeader}
<div class="ve-flex mobile-ish__ve-flex-col w-100 px-3">
<div class="ve-flex-col">
${$stgPbHeader}
<div class="ve-flex">
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header"></div>
<div class="my-1 statgen-pb__header ve-flex-h-right">${$btnResetRolledOrArrayOrManual}</div>
${Parser.ABIL_ABVS.map(it => `<div class="my-1 bold statgen-pb__cell ve-flex-v-center ve-flex-h-right" title="${Parser.attAbvToFull(it)}">${it.toUpperCase()}</div>`)}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header"></div>
<div class="my-1 bold statgen-pb__header ve-flex-vh-center">Base</div>
${$wrpsBase}
</div>
${$wrpRaceOuter}
${$wrpBackgroundOuter}
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header"></div>
<div class="my-1 statgen-pb__header ve-flex-vh-center help text-muted" title="Input any additional/custom bonuses here">User</div>
${$wrpsUser}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header"></div>
<div class="my-1 statgen-pb__header ve-flex-vh-center">Total</div>
${metasTotalAndMod.map(it => it.$wrpIptTotal)}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header"></div>
<div class="my-1 statgen-pb__header ve-flex-vh-center" title="Modifier">Mod.</div>
${metasTotalAndMod.map(it => it.$wrpIptMod)}
</div>
</div>
${$stgRaceSel}
${$stgBackgroundSel}
</div>
${$vrPbCustom}
${$hrPbCustom}
${$stgPbCustom}
</div>
<hr class="hr-3">
${$dispPreviewRace}
${$hrPreviewRaceTashas}
${$dispTashas}
${$dispPreviewBackground}
${$hrPreviewBackground}
${$wrpAsi}
</div>`;
hkElesMode();
$wrpTab
.append($stgMain)
.append($stgNone);
}
static _RenderLevelOneEntity = class {
_title;
_titleShort;
_propIxEntity;
_propIxAbilityScoreSet;
_propData;
_propModalFilter;
_propIsPreview;
_propEntity;
_page;
_propChoiceMetasFrom;
_propChoiceWeighted;
constructor ({parent}) {
this._parent = parent;
this._pbHookMetas = [];
}
render () {
const $wrp = $(`<div class="ve-flex"></div>`);
const $wrpOuter = $$`<div class="ve-flex-col">
<div class="my-1 statgen-pb__header statgen-pb__header--group mr-3 ve-text-center italic ve-small help-subtle" title="Ability Score Changes from ${this._title}">${this._titleShort}</div>
${$wrp}
</div>`;
// Ensure this is run first, and doesn't trigger further state changes
this._parent._addHookBase(this._propIxEntity, () => this._parent.__state[this._propIxAbilityScoreSet] = 0);
const hkIxEntity = (prop) => {
this._pb_unhookRender();
const isInitialLoad = prop == null;
if (!isInitialLoad) this._parent._state[this._propChoiceMetasFrom] = [];
if (!isInitialLoad) this._parent._state[this._propChoiceWeighted] = [];
const isAnyFromEntity = this._render_pointBuy($wrp);
$wrpOuter.toggleVe(isAnyFromEntity);
};
this._parent._addHookBase(this._propIxEntity, hkIxEntity);
this._bindAdditionalHooks_hkIxEntity(hkIxEntity);
this._parent._addHookBase(this._propIxAbilityScoreSet, hkIxEntity);
hkIxEntity();
const {$wrp: $selEntity, fnUpdateHidden: fnUpdateSelEntityHidden} = ComponentUiUtil.$getSelSearchable(
this._parent,
this._propIxEntity,
{
values: this._parent[this._propData].map((_, i) => i),
isAllowNull: true,
fnDisplay: ix => {
const r = this._parent[this._propData][ix];
if (!r) return "(Unknown)";
return `${r.name} ${r.source !== Parser.SRC_PHB ? `[${Parser.sourceJsonToAbv(r.source)}]` : ""}`;
},
asMeta: true,
},
);
const doApplyFilterToSelEntity = () => {
const f = this._parent[this._propModalFilter].pageFilter.filterBox.getValues();
const isHiddenPerEntity = this._parent[this._propData].map(it => !this._parent[this._propModalFilter].pageFilter.toDisplay(f, it));
fnUpdateSelEntityHidden(isHiddenPerEntity, false);
};
this._parent[this._propModalFilter].pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, () => doApplyFilterToSelEntity());
doApplyFilterToSelEntity();
const $btnFilterForEntity = $(`<button class="btn btn-xs btn-default br-0 pr-2" title="Filter for ${this._title}"><span class="glyphicon glyphicon-filter"></span> Filter</button>`)
.click(async () => {
const selected = await this._parent[this._propModalFilter].pGetUserSelection();
if (selected == null || !selected.length) return;
const selectedEntity = selected[0];
const ixEntity = this._parent[this._propData].findIndex(it => it.name === selectedEntity.name && it.source === selectedEntity.values.sourceJson);
if (!~ixEntity) throw new Error(`Could not find selected ${this._title.toLowerCase()}: ${JSON.stringify(selectedEntity)}`); // Should never occur
this._parent._state[this._propIxEntity] = ixEntity;
});
const $btnPreview = ComponentUiUtil.$getBtnBool(
this._parent,
this._propIsPreview,
{
html: `<button class="btn btn-xs btn-default" title="Toggle ${this._title} Preview"><span class="glyphicon glyphicon-eye-open"></span></button>`,
},
);
const hkBtnPreviewEntity = () => $btnPreview.toggleVe(this._parent._state[this._propIxEntity] != null && ~this._parent._state[this._propIxEntity]);
this._parent._addHookBase(this._propIxEntity, hkBtnPreviewEntity);
hkBtnPreviewEntity();
// region Ability score set selection
const {$sel: $selAbilitySet, setValues: setValuesSelAbilitySet} = ComponentUiUtil.$getSelEnum(
this._parent,
this._propIxAbilityScoreSet,
{
values: [],
asMeta: true,
fnDisplay: ixAbSet => {
const lst = this._pb_getAbilityList();
if (!lst?.[ixAbSet]) return "(Unknown)";
return Renderer.getAbilityData([lst[ixAbSet]]).asText;
},
},
);
const $stgAbilityScoreSet = $$`<div class="ve-flex-v-center mb-2">
<div class="mr-2">Ability Score Increase</div>
<div>${$selAbilitySet}</div>
</div>`;
const hkSetValuesSelAbilitySet = () => {
const entity = this._parent[this._propEntity];
$stgAbilityScoreSet.toggleVe(!!entity && entity.ability?.length > 1);
setValuesSelAbilitySet(
[...new Array(entity?.ability?.length || 0)].map((_, ix) => ix),
{isForce: true},
);
};
this._parent._addHookBase(this._propIxEntity, hkSetValuesSelAbilitySet);
this._bindAdditionalHooks_hkSetValuesSelAbilitySet(hkSetValuesSelAbilitySet);
hkSetValuesSelAbilitySet();
// endregion
const $dispPreview = $(`<div class="ve-flex-col mb-2"></div>`);
const hkPreviewEntity = () => {
if (!this._parent._state[this._propIsPreview]) return $dispPreview.hideVe();
const entity = this._parent._state[this._propIxEntity] != null ? this._parent[this._propData][this._parent._state[this._propIxEntity]] : null;
if (!entity) return $dispPreview.hideVe();
$dispPreview.empty().showVe().append(Renderer.hover.$getHoverContent_stats(this._page, entity));
};
this._parent._addHookBase(this._propIxEntity, hkPreviewEntity);
this._parent._addHookBase(this._propIsPreview, hkPreviewEntity);
hkPreviewEntity();
const {$hrPreview} = this._getHrPreviewMeta();
const $stgSel = $$`<div class="ve-flex-col mt-3">
<div class="mb-1">Select a ${this._title}</div>
<div class="ve-flex-v-center mb-2">
<div class="ve-flex-v-center btn-group w-100 mr-2">${$btnFilterForEntity}${$selEntity}</div>
<div>${$btnPreview}</div>
</div>
${$stgAbilityScoreSet}
</div>`;
return {
$wrpOuter,
$stgSel,
$dispPreview,
$hrPreview,
};
}
_pb_unhookRender () {
this._pbHookMetas.forEach(it => it.unhook());
this._pbHookMetas = [];
}
_render_pointBuy ($wrp) {
$wrp.empty();
const fromEntity = this._pb_getAbility();
if (fromEntity == null) return false;
let $ptBase = null;
if (Parser.ABIL_ABVS.some(it => fromEntity[it])) {
const $wrpsEntity = Parser.ABIL_ABVS.map(ab => {
return $$`<div class="my-1 statgen-pb__cell">
<input class="form-control form-control--minimal statgen-shared__ipt text-right" type="number" readonly value="${fromEntity[ab] || 0}">
</div>`;
});
$ptBase = $$`<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header ve-flex-vh-center">Static</div>
${$wrpsEntity}
</div>`;
}
let $ptChooseFrom = null;
if (fromEntity.choose && fromEntity.choose.from) {
const amount = fromEntity.choose.amount || 1;
const count = fromEntity.choose.count || 1;
const $wrpsChoose = Parser.ABIL_ABVS.map(ab => {
if (!fromEntity.choose.from.includes(ab)) return `<div class="my-1 statgen-pb__cell"></div>`;
const $cb = $(`<input type="checkbox">`)
.change(() => {
const existing = this._parent._state[this._propChoiceMetasFrom].find(it => it.ability === ab);
if (existing) {
this._parent._state[this._propChoiceMetasFrom] = this._parent._state[this._propChoiceMetasFrom].filter(it => it !== existing);
return;
}
// If we're already at the max number of choices, remove the oldest one
if (this._parent._state[this._propChoiceMetasFrom].length >= count) {
while (this._parent._state[this._propChoiceMetasFrom].length >= count) this._parent._state[this._propChoiceMetasFrom].shift();
this._parent._state[this._propChoiceMetasFrom] = [...this._parent._state[this._propChoiceMetasFrom]];
}
this._parent._state[this._propChoiceMetasFrom] = [
...this._parent._state[this._propChoiceMetasFrom],
{ability: ab, amount},
];
});
const hk = () => $cb.prop("checked", this._parent._state[this._propChoiceMetasFrom].some(it => it.ability === ab));
this._parent._addHookBase(this._propChoiceMetasFrom, hk);
this._pbHookMetas.push({unhook: () => this._parent._removeHookBase(this._propChoiceMetasFrom, hk)});
hk();
return $$`<label class="my-1 statgen-pb__cell ve-flex-vh-center">${$cb}</label>`;
});
$ptChooseFrom = $$`<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header statgen-pb__header--choose-from ve-flex-vh-center">
<div class="${count !== 1 ? `mr-1` : ""}">${UiUtil.intToBonus(amount, {isPretty: true})}</div>${count !== 1 ? `<div class="ve-small ve-muted">(x${count})</div>` : ""}
</div>
${$wrpsChoose}
</div>`;
}
let $ptsChooseWeighted = null;
if (fromEntity.choose && fromEntity.choose.weighted && fromEntity.choose.weighted.weights) {
$ptsChooseWeighted = fromEntity.choose.weighted.weights.map((weight, ixWeight) => {
const $wrpsChoose = Parser.ABIL_ABVS.map(ab => {
if (!fromEntity.choose.weighted.from.includes(ab)) return `<div class="my-1 statgen-pb__cell"></div>`;
const $cb = $(`<input type="checkbox">`)
.change(() => {
const existing = this._parent._state[this._propChoiceWeighted].find(it => it.ability === ab && it.ix === ixWeight);
if (existing) {
this._parent._state[this._propChoiceWeighted] = this._parent._state[this._propChoiceWeighted].filter(it => it !== existing);
return;
}
// Remove other selections for the same ability score, or selections for the same weight
const withSameAbil = this._parent._state[this._propChoiceWeighted].filter(it => it.ability === ab || it.ix === ixWeight);
if (withSameAbil.length) {
this._parent._state[this._propChoiceWeighted] = this._parent._state[this._propChoiceWeighted].filter(it => it.ability !== ab && it.ix !== ixWeight);
}
this._parent._state[this._propChoiceWeighted] = [
...this._parent._state[this._propChoiceWeighted],
{ability: ab, amount: weight, ix: ixWeight},
];
});
const hk = () => {
$cb.prop("checked", this._parent._state[this._propChoiceWeighted].some(it => it.ability === ab && it.ix === ixWeight));
};
this._parent._addHookBase(this._propChoiceWeighted, hk);
this._pbHookMetas.push({unhook: () => this._parent._removeHookBase(this._propChoiceWeighted, hk)});
hk();
return $$`<label class="my-1 statgen-pb__cell ve-flex-vh-center">${$cb}</label>`;
});
return $$`<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header statgen-pb__header--choose-from ve-flex-vh-center">${UiUtil.intToBonus(weight, {isPretty: true})}</div>
${$wrpsChoose}
</div>`;
});
}
$$($wrp)`
${$ptBase}
${$ptChooseFrom}
${$ptsChooseWeighted}
`;
return $ptBase || $ptChooseFrom || $ptsChooseWeighted;
}
/** @abstract */
_pb_getAbilityList () { throw new Error("Unimplemented!"); }
/** @abstract */
_pb_getAbility () { throw new Error("Unimplemented!"); }
_bindAdditionalHooks_hkIxEntity (hkIxEntity) { /* Implement as required */ }
_bindAdditionalHooks_hkSetValuesSelAbilitySet (hkSetValuesSelAbilitySet) { /* Implement as required */ }
_getHrPreviewMeta () {
const $hrPreview = $(`<hr class="hr-3">`);
const hkPreview = this._getHkPreview({$hrPreview});
this._parent._addHookBase(this._propIsPreview, hkPreview);
hkPreview();
return {
$hrPreview,
hkPreview,
};
}
_getHkPreview ({$hrPreview}) {
return () => $hrPreview.toggleVe(this._parent._state[this._propIsPreview]);
}
};
static _RenderLevelOneRace = class extends this._RenderLevelOneEntity {
_title = "Race";
_titleShort = "Race";
_propIxEntity = "common_ixRace";
_propIxAbilityScoreSet = "common_ixAbilityScoreSetRace";
_propData = "_races";
_propModalFilter = "_modalFilterRaces";
_propIsPreview = "common_isPreviewRace";
_propEntity = "race";
_page = UrlUtil.PG_RACES;
_propChoiceMetasFrom = "common_raceChoiceMetasFrom";
_propChoiceWeighted = "common_raceChoiceMetasWeighted";
render () {
const out = super.render();
const {$btnToggleTashasPin, $dispTashas} = this._$getPtsTashas();
out.$stgSel.append($$`<label class="ve-flex-v-center mb-1">
<div class="mr-2">Allow Origin Customization</div>
${ComponentUiUtil.$getCbBool(this._parent, "common_isTashas")}
</label>`);
out.$stgSel.append($$`<div class="ve-flex">
<div class="ve-small ve-muted italic mr-1">${Renderer.get().render(`An {@variantrule Customizing Your Origin|TCE|optional rule}`)}</div>
${$btnToggleTashasPin}
<div class="ve-small ve-muted italic ml-1">${Renderer.get().render(`from Tasha's Cauldron of Everything, page 8.`)}</div>
</div>`);
out.$dispTashas = $dispTashas;
return out;
}
_pb_getAbilityList () {
return this._parent._pb_getRaceAbilityList();
}
_pb_getAbility () {
return this._parent._pb_getRaceAbility();
}
_bindAdditionalHooks_hkIxEntity (hkIxEntity) {
this._parent._addHookBase("common_isTashas", hkIxEntity);
}
_bindAdditionalHooks_hkSetValuesSelAbilitySet (hkSetValuesSelAbilitySet) {
this._parent._addHookBase("common_isTashas", hkSetValuesSelAbilitySet);
}
_getHrPreviewMeta () {
const out = super._getHrPreviewMeta();
const {hkPreview} = out;
this._parent._addHookBase("common_isShowTashasRules", hkPreview);
return out;
}
_getHkPreview ({$hrPreview}) {
return () => $hrPreview.toggleVe(this._parent._state[this._propIsPreview] && this._parent._state.common_isShowTashasRules);
}
_$getPtsTashas () {
const $btnToggleTashasPin = ComponentUiUtil.$getBtnBool(
this._parent,
"common_isShowTashasRules",
{
html: `<button class="btn btn-xxs btn-default ve-small p-0 statgen-shared__btn-toggle-tashas-rules ve-flex-vh-center" title="Toggle &quot;Customizing Your Origin&quot; Section"><span class="glyphicon glyphicon-eye-open"></span></button>`,
},
);
const $dispTashas = $(`<div class="ve-flex-col"><div class="italic ve-muted">Loading...</div></div>`);
DataLoader.pCacheAndGet(UrlUtil.PG_VARIANTRULES, Parser.SRC_TCE, UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VARIANTRULES]({name: "Customizing Your Origin", source: Parser.SRC_TCE}))
.then(rule => {
$$($dispTashas.empty())`${Renderer.hover.$getHoverContent_stats(UrlUtil.PG_VARIANTRULES, rule)}<hr class="hr-3">`;
});
const hkIsShowTashas = () => {
$dispTashas.toggleVe(this._parent._state.common_isShowTashasRules);
};
this._parent._addHookBase("common_isShowTashasRules", hkIsShowTashas);
hkIsShowTashas();
return {
$btnToggleTashasPin,
$dispTashas,
};
}
};
_renderLevelOneRace = new this.constructor._RenderLevelOneRace({parent: this});
static _RenderLevelOneBackground = class extends this._RenderLevelOneEntity {
_title = "Background";
_titleShort = "Backg.";
_propIxEntity = "common_ixBackground";
_propIxAbilityScoreSet = "common_ixAbilityScoreSetBackground";
_propData = "_backgrounds";
_propModalFilter = "_modalFilterBackgrounds";
_propIsPreview = "common_isPreviewBackground";
_propEntity = "background";
_page = UrlUtil.PG_BACKGROUNDS;
_propChoiceMetasFrom = "common_backgroundChoiceMetasFrom";
_propChoiceWeighted = "common_backgroundChoiceMetasWeighted";
_pb_getAbilityList () {
return this._parent._pb_getBackgroundAbilityList();
}
_pb_getAbility () {
return this._parent._pb_getBackgroundAbility();
}
};
_renderLevelOneBackground = new this.constructor._RenderLevelOneBackground({parent: this});
_render_isLevelUp ($wrpTab) {
const $wrpsExisting = Parser.ABIL_ABVS.map(ab => {
const $iptExisting = $(`<input class="form-control form-control--minimal statgen-shared__ipt text-right" type="number" readonly>`)
.val(this._existingScores[ab]);
return $$`<label class="my-1 statgen-pb__cell">
${$iptExisting}
</label>`;
});
const $wrpsUser = this._render_$getWrpsUser();
const metasTotalAndMod = this._render_getMetasTotalAndMod();
const $wrpAsi = this._render_$getWrpAsi();
$$($wrpTab)`
<div class="ve-flex mobile-ish__ve-flex-col w-100 px-3">
<div class="ve-flex-col">
<div class="ve-flex">
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header"></div>
${Parser.ABIL_ABVS.map(it => `<div class="my-1 bold statgen-pb__cell ve-flex-v-center ve-flex-h-right" title="${Parser.attAbvToFull(it)}">${it.toUpperCase()}</div>`)}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 bold statgen-pb__header ve-flex-vh-center" title="Current">Curr.</div>
${$wrpsExisting}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header ve-flex-vh-center help text-muted" title="Input any additional/custom bonuses here">User</div>
${$wrpsUser}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header ve-flex-vh-center">Total</div>
${metasTotalAndMod.map(it => it.$wrpIptTotal)}
</div>
<div class="ve-flex-col mr-3">
<div class="my-1 statgen-pb__header ve-flex-vh-center" title="Modifier">Mod.</div>
${metasTotalAndMod.map(it => it.$wrpIptMod)}
</div>
</div>
</div>
</div>
<hr class="hr-3">
${$wrpAsi}
`;
}
_render_$getWrpsUser () {
return Parser.ABIL_ABVS.map(ab => {
const {propUserBonus} = this.constructor._common_getProps(ab);
const $ipt = ComponentUiUtil.$getIptInt(
this,
propUserBonus,
0,
{
fallbackOnNaN: 0,
html: `<input class="form-control form-control--minimal statgen-shared__ipt text-right" type="number">`,
},
);
return $$`<label class="my-1 statgen-pb__cell">${$ipt}</label>`;
});
}
_render_getMetasTotalAndMod () {
return Parser.ABIL_ABVS.map(ab => {
const $iptTotal = $(`<input class="form-control form-control--minimal statgen-shared__ipt ve-text-center" type="text" readonly>`);
const $iptMod = $(`<input class="form-control form-control--minimal statgen-shared__ipt ve-text-center" type="text" readonly>`);
const $wrpIptTotal = $$`<label class="my-1 statgen-pb__cell">${$iptTotal}</label>`;
const $wrpIptMod = $$`<label class="my-1 statgen-pb__cell">${$iptMod}</label>`;
const exportedStateProp = `common_export_${ab}`;
const getTotalScore = () => {
if (this._isLevelUp) return this._levelUp_getTotalScore(ab);
switch (this.ixActiveTab) {
case this._IX_TAB_ROLLED: return this._rolled_getTotalScore(ab);
case this._IX_TAB_ARRAY: return this._array_getTotalScore(ab);
case this._IX_TAB_PB: return this._pb_getTotalScore(ab);
case this._IX_TAB_MANUAL: return this._manual_getTotalScore(ab);
default: return 0;
}
};
const hk = () => {
const totalScore = getTotalScore();
const isOverLimit = totalScore > 20;
$iptTotal
.val(totalScore)
.toggleClass("form-control--error", isOverLimit)
.title(isOverLimit ? `In general, you can't increase an ability score above 20.` : "");
$iptMod.val(Parser.getAbilityModifier(totalScore));
this._state[exportedStateProp] = totalScore;
};
this._addHookAll("state", hk);
this._addHookActiveTab(hk);
hk();
return {
$wrpIptTotal,
$wrpIptMod,
};
});
}
_render_$getWrpAsi () {
const $wrpAsi = $(`<div class="ve-flex-col w-100"></div>`);
this._compAsi.render($wrpAsi);
return $wrpAsi;
}
static _common_getProps (ab) {
return {
propUserBonus: `${StatGenUi._PROP_PREFIX_COMMON}${ab}_user`,
};
}
static _rolled_getProps (ab) {
return {
propAbilSelectedRollIx: `${StatGenUi._PROP_PREFIX_ROLLED}${ab}_abilSelectedRollIx`,
};
}
static _array_getProps (ab) {
return {
propAbilSelectedScoreIx: `${StatGenUi._PROP_PREFIX_ARRAY}${ab}_abilSelectedScoreIx`,
};
}
static _manual_getProps (ab) {
return {
propAbilValue: `${StatGenUi._PROP_PREFIX_MANUAL}${ab}_abilValue`,
};
}
_pb_getRaceAbilityList () {
const race = this.race;
if (!race?.ability?.length) return null;
return race.ability
.map(fromRace => {
if (this._state.common_isTashas) {
const weights = [];
if (fromRace.choose && fromRace.choose.weighted && fromRace.choose.weighted.weights) {
weights.push(...fromRace.choose.weighted.weights);
}
Parser.ABIL_ABVS.forEach(it => {
if (fromRace[it]) weights.push(fromRace[it]);
});
if (fromRace.choose && fromRace.choose.from) {
const count = fromRace.choose.count || 1;
const amount = fromRace.choose.amount || 1;
for (let i = 0; i < count; ++i) weights.push(amount);
}
weights.sort((a, b) => SortUtil.ascSort(b, a));
fromRace = {
choose: {
weighted: {
from: [...Parser.ABIL_ABVS],
weights,
},
},
};
}
return fromRace;
});
}
_pb_getBackgroundAbilityList () {
const background = this.background;
if (!background?.ability?.length) return null;
return background.ability;
}
_pb_getRaceAbility () {
return this._pb_getRaceAbilityList()?.[this._state.common_ixAbilityScoreSetRace || 0];
}
_pb_getBackgroundAbility () {
return this._pb_getBackgroundAbilityList()?.[this._state.common_ixAbilityScoreSetBackground || 0];
}
_pb_getPointsRemaining (baseState) {
const spent = Parser.ABIL_ABVS.map(it => {
const prop = `pb_${it}`;
const score = baseState[prop];
const rule = this._state.pb_rules.find(it => it.entity.score === score);
if (!rule) return 0;
return rule.entity.cost;
}).reduce((a, b) => a + b, 0);
return this._state.pb_budget - spent;
}
_rolled_getTotalScore (ab) {
const {propAbilSelectedRollIx} = this.constructor._rolled_getProps(ab);
const {propUserBonus} = this.constructor._common_getProps(ab);
return (this._state.rolled_rolls[this._state[propAbilSelectedRollIx]] || {total: 0}).total + this._state[propUserBonus] + this._getTotalScore_getBonuses(ab);
}
_array_getTotalScore (ab) {
const {propAbilSelectedScoreIx} = this.constructor._array_getProps(ab);
const {propUserBonus} = this.constructor._common_getProps(ab);
return (StatGenUi._STANDARD_ARRAY[this._state[propAbilSelectedScoreIx]] || 0) + this._state[propUserBonus] + this._getTotalScore_getBonuses(ab);
}
_pb_getTotalScore (ab) {
const prop = `pb_${ab}`;
const {propUserBonus} = this.constructor._common_getProps(ab);
return this._state[prop] + this._state[propUserBonus] + this._getTotalScore_getBonuses(ab);
}
_manual_getTotalScore (ab) {
const {propAbilValue} = this.constructor._manual_getProps(ab);
const {propUserBonus} = this.constructor._common_getProps(ab);
return (this._state[propAbilValue] || 0) + this._state[propUserBonus] + this._getTotalScore_getBonuses(ab);
}
_levelUp_getTotalScore (ab) {
const {propUserBonus} = this.constructor._common_getProps(ab);
return (this._existingScores[ab] || 0) + this._state[propUserBonus] + this._getTotalScore_getBonuses(ab);
}
_getTotalScore_getBonuses (ab) {
let total = 0;
if (!this._isLevelUp) {
const handleEntityAbility = ({fromEntity, propChoiceMetasFrom, propChoiceWeighted}) => {
if (fromEntity) {
if (fromEntity[ab]) total += fromEntity[ab];
if (fromEntity.choose && fromEntity.choose.from) {
total += this._state[propChoiceMetasFrom]
.filter(it => it.ability === ab)
.map(it => it.amount)
.reduce((a, b) => a + b, 0);
}
if (fromEntity.choose && fromEntity.choose.weighted && fromEntity.choose.weighted.weights) {
total += this._state[propChoiceWeighted]
.filter(it => it.ability === ab)
.map(it => it.amount)
.reduce((a, b) => a + b, 0);
}
}
};
handleEntityAbility({
fromEntity: this._pb_getRaceAbility(),
propChoiceMetasFrom: "common_raceChoiceMetasFrom",
propChoiceWeighted: "common_raceChoiceMetasWeighted",
});
handleEntityAbility({
fromEntity: this._pb_getBackgroundAbility(),
propChoiceMetasFrom: "common_backgroundChoiceMetasFrom",
propChoiceWeighted: "common_backgroundChoiceMetasWeighted",
});
}
const formDataAsi = this._compAsi.getFormData();
if (formDataAsi) total += formDataAsi.data[ab] || 0;
return total;
}
getSaveableState () {
const out = super.getSaveableState();
const handleEntity = ({propIxEntity, page, propData, propHash}) => {
if (out[propIxEntity] != null && !~this._state[propIxEntity]) {
out[propHash] = UrlUtil.URL_TO_HASH_BUILDER[page](this[propData][out[propIxEntity]]);
delete out[propIxEntity];
}
};
handleEntity({
propIxEntity: "common_ixRace",
page: UrlUtil.PG_RACES,
propData: "_races",
propHash: "_pb_raceHash",
});
handleEntity({
propIxEntity: "common_ixBackground",
page: UrlUtil.PG_BACKGROUNDS,
propData: "_backgrounds",
propHash: "_pb_backgroundHash",
});
return out;
}
// region External use
getSaveableStatePointBuyCustom () {
const base = this.getSaveableState();
return {
state: this.constructor._PROPS_POINT_BUY_CUSTOM.mergeMap(k => ({[k]: base.state[k]})),
};
}
// endregion
setStateFrom (saved, isOverwrite = false) {
saved = MiscUtil.copy(saved);
MiscUtil.getOrSet(saved, "state", {});
const handleEntityHash = ({propHash, page, propData, propIxEntity}) => {
if (!saved[propHash]) return;
const ixEntity = this[propData].findIndex(it => {
const hash = UrlUtil.URL_TO_HASH_BUILDER[page](it);
return hash === saved[propHash];
});
if (~ixEntity) saved[propIxEntity] = ixEntity;
};
handleEntityHash({
propHash: "_pb_raceHash",
page: UrlUtil.PG_RACES,
propData: "_races",
propIxEntity: "common_ixRace",
});
handleEntityHash({
propHash: "_pb_backgroundHash",
page: UrlUtil.PG_BACKGROUNDS,
propData: "_backgrounds",
propIxEntity: "common_ixBackground",
});
const validKeys = new Set(Object.keys(this._getDefaultState()));
const validKeyPrefixes = [
StatGenUi._PROP_PREFIX_COMMON,
StatGenUi._PROP_PREFIX_ROLLED,
StatGenUi._PROP_PREFIX_ARRAY,
StatGenUi._PROP_PREFIX_MANUAL,
];
Object.keys(saved.state).filter(k => !validKeys.has(k) && !validKeyPrefixes.some(it => k.startsWith(it))).forEach(k => delete saved.state[k]);
// region Trim the ASI/feat state to the max count of ASIs/feats
for (let i = saved.state.common_cntAsi || 0; i < 1000; ++i) {
const {propMode, prefix} = this.getPropsAsi(i, "ability");
if (saved.state[propMode]) Object.keys(saved.state).filter(k => k.startsWith(prefix)).forEach(k => delete saved.state[k]);
}
for (let i = saved.state.common_cntFeatsCustom || 0; i < 1000; ++i) {
const {propMode, prefix} = this.getPropsAsi(i, "custom");
if (saved.state[propMode]) Object.keys(saved.state).filter(k => k.startsWith(prefix)).forEach(k => delete saved.state[k]);
}
// endregion
super.setStateFrom(saved, isOverwrite);
}
_pb_getMinMaxScores () {
return {
min: Math.min(...this._state.pb_rules.map(it => it.entity.score)),
max: Math.max(...this._state.pb_rules.map(it => it.entity.score)),
};
}
_getDefaultStateCommonResettable () {
return {
...Parser.ABIL_ABVS.mergeMap(ab => ({[this.constructor._common_getProps(ab).propUserBonus]: 0})),
common_raceChoiceMetasFrom: [],
common_raceChoiceMetasWeighted: [],
common_backgroundChoiceMetasFrom: [],
common_backgroundChoiceMetasWeighted: [],
};
}
_getDefaultStateNoneResettable () { return {}; }
_getDefaultStateRolledResettable () {
return {
...Parser.ABIL_ABVS.mergeMap(ab => ({[this.constructor._rolled_getProps(ab).propAbilSelectedRollIx]: null})),
};
}
_getDefaultStateArrayResettable () {
return {
...Parser.ABIL_ABVS.mergeMap(ab => ({[this.constructor._array_getProps(ab).propAbilSelectedScoreIx]: null})),
};
}
_getDefaultStatePointBuyResettable () {
return {
pb_str: 8,
pb_dex: 8,
pb_con: 8,
pb_int: 8,
pb_wis: 8,
pb_cha: 8,
};
}
_getDefaultStatePointBuyCosts () {
return {
pb_rules: [
{score: 8, cost: 0},
{score: 9, cost: 1},
{score: 10, cost: 2},
{score: 11, cost: 3},
{score: 12, cost: 4},
{score: 13, cost: 5},
{score: 14, cost: 7},
{score: 15, cost: 9},
].map(({score, cost}) => this._getDefaultState_pb_rule(score, cost)),
};
}
_getDefaultState_pb_rule (score, cost) {
return {
id: CryptUtil.uid(),
entity: {
score,
cost,
},
};
}
_getDefaultStateManualResettable () {
return {
...Parser.ABIL_ABVS.mergeMap(ab => ({[this.constructor._manual_getProps(ab).propAbilValue]: null})),
};
}
_getDefaultState () {
return {
// region Common
common_isPreviewRace: false,
common_isTashas: false,
common_isShowTashasRules: false,
common_ixRace: null,
common_ixAbilityScoreSet: 0,
common_isPreviewBackground: false,
common_ixBackground: null,
common_ixAbilityScoreSetBackground: 0,
common_pulseAsi: false, // Used as a general pulse for all changes in form data
common_cntAsi: 0,
common_cntFeatsCustom: 0,
// region Used to allow external components to hook onto score changes
common_export_str: null,
common_export_dex: null,
common_export_con: null,
common_export_int: null,
common_export_wis: null,
common_export_cha: null,
// endregion
...this._getDefaultStateCommonResettable(),
// endregion
// region Rolled stats
rolled_formula: "4d6dl1",
rolled_rollCount: 6,
rolled_rolls: [],
...this._getDefaultStateRolledResettable(),
// endregion
// region Standard array
...this._getDefaultStateArrayResettable(),
// endregion
// region Point buy
...this._getDefaultStatePointBuyResettable(),
...this._getDefaultStatePointBuyCosts(),
pb_points: 27,
pb_budget: 27,
pb_isCustom: false,
// endregion
// region Manual
...this._getDefaultStateManualResettable(),
// endregion
};
}
}
StatGenUi._STANDARD_ARRAY = [15, 14, 13, 12, 10, 8];
StatGenUi._PROP_PREFIX_COMMON = "common_";
StatGenUi._PROP_PREFIX_ROLLED = "rolled_";
StatGenUi._PROP_PREFIX_ARRAY = "array_";
StatGenUi._PROP_PREFIX_MANUAL = "manual_";
StatGenUi.MODE_NONE = "none";
StatGenUi.MODES = [
"rolled",
"array",
"pointbuy",
"manual",
];
StatGenUi.MODES_FVTT = [
StatGenUi.MODE_NONE,
...StatGenUi.MODES,
];
StatGenUi._MAX_CUSTOM_FEATS = 20;
globalThis.StatGenUi = StatGenUi;
class UtilAdditionalFeats {
static isNoChoice (available) {
if (!available?.length) return true;
if (available.length > 1) return false;
return !available[0].any;
}
static getUidsStatic (availableSet) {
return Object.entries(availableSet || {})
.filter(([k, v]) => k !== "any" && v)
.sort(([kA], [kB]) => SortUtil.ascSortLower(kA, kB))
.map(([k]) => k);
}
static getSelIxSetMeta ({comp, prop, available}) {
return ComponentUiUtil.$getSelEnum(
comp,
prop,
{
values: available.map((_, i) => i),
fnDisplay: ix => {
const featSet = available[ix];
const out = [];
if (featSet.any) {
out.push(`Choose any${featSet.any > 1 ? ` ${Parser.numberToText(featSet.any)}` : ""}`);
}
this.getUidsStatic(featSet)
.forEach(uid => {
const {name} = DataUtil.proxy.unpackUid("feat", uid, "feat", {isLower: true});
out.push(name.toTitleCase());
});
return out.filter(Boolean).join("; ");
},
asMeta: true,
},
);
}
}
globalThis.UtilAdditionalFeats = UtilAdditionalFeats;
StatGenUi.CompAsi = class extends BaseComponent {
constructor ({parent}) {
super();
this._parent = parent;
this._metasAsi = {ability: [], race: [], background: [], custom: []};
this._doPulseThrottled = MiscUtil.throttle(this._doPulse_.bind(this), 50);
}
/**
* Add this to UI interactions rather than state hooks, as there is a copy of this component per tab.
*/
_doPulse_ () { this._parent.state.common_pulseAsi = !this._parent.state.common_pulseAsi; }
_render_renderAsiFeatSection (propCnt, namespace, $wrpRows) {
const hk = () => {
let ix = 0;
for (; ix < this._parent.state[propCnt]; ++ix) {
const ix_ = ix;
const {propMode, propIxFeat, propIxAsiPointOne, propIxAsiPointTwo, propIxFeatAbility, propFeatAbilityChooseFrom} = this._parent.getPropsAsi(ix_, namespace);
if (!this._metasAsi[namespace][ix_]) {
this._parent.state[propMode] = this._parent.state[propMode] || (namespace === "ability" ? "asi" : "feat");
const $btnAsi = namespace !== "ability" ? null : $(`<button class="btn btn-xs btn-default w-50p">ASI</button>`)
.click(() => {
this._parent.state[propMode] = "asi";
this._doPulseThrottled();
});
const $btnFeat = namespace !== "ability" ? $(`<div class="w-100p ve-text-center">Feat</div>`) : $(`<button class="btn btn-xs btn-default w-50p">Feat</button>`)
.click(() => {
this._parent.state[propMode] = "feat";
this._doPulseThrottled();
});
// region ASI
let $stgAsi;
if (namespace === "ability") {
const $colsAsi = Parser.ABIL_ABVS.map((it, ixAsi) => {
const updateDisplay = () => $ipt.val(Number(this._parent.state[propIxAsiPointOne] === ixAsi) + Number(this._parent.state[propIxAsiPointTwo] === ixAsi));
const $ipt = $(`<input class="form-control form-control--minimal text-right input-xs statgen-shared__ipt" type="number" style="width: 42px;">`)
.disableSpellcheck()
.keydown(evt => { if (evt.key === "Escape") $ipt.blur(); })
.change(() => {
const raw = $ipt.val().trim();
const asNum = Number(raw);
const activeProps = [propIxAsiPointOne, propIxAsiPointTwo].filter(prop => this._parent.state[prop] === ixAsi);
if (isNaN(asNum) || asNum <= 0) {
this._parent.proxyAssignSimple(
"state",
{
...activeProps.mergeMap(prop => ({[prop]: null})),
},
);
updateDisplay();
return this._doPulseThrottled();
}
if (asNum >= 2) {
this._parent.proxyAssignSimple(
"state",
{
[propIxAsiPointOne]: ixAsi,
[propIxAsiPointTwo]: ixAsi,
},
);
updateDisplay();
return this._doPulseThrottled();
}
if (activeProps.length === 2) {
this._parent.state[propIxAsiPointTwo] = null;
updateDisplay();
return this._doPulseThrottled();
}
if (this._parent.state[propIxAsiPointOne] == null) {
this._parent.state[propIxAsiPointOne] = ixAsi;
updateDisplay();
return this._doPulseThrottled();
}
this._parent.state[propIxAsiPointTwo] = ixAsi;
updateDisplay();
this._doPulseThrottled();
});
const hkSelected = () => updateDisplay();
this._parent.addHookBase(propIxAsiPointOne, hkSelected);
this._parent.addHookBase(propIxAsiPointTwo, hkSelected);
hkSelected();
return $$`<div class="ve-flex-col h-100 mr-2">
<div class="statgen-asi__cell ve-text-center pb-1" title="${Parser.attAbvToFull(it)}">${it.toUpperCase()}</div>
<div class="ve-flex-vh-center statgen-asi__cell relative">
<div class="absolute no-events statgen-asi__disp-plus">+</div>
${$ipt}
</div>
</div>`;
});
$stgAsi = $$`<div class="ve-flex-v-center">
${$colsAsi}
</div>`;
}
// endregion
// region Feat
const {$stgFeat, $btnChooseFeat, hkIxFeat} = this._render_getMetaFeat({propIxFeat, propIxFeatAbility, propFeatAbilityChooseFrom});
// endregion
const hkMode = () => {
if (namespace === "ability") {
$btnAsi.toggleClass("active", this._parent.state[propMode] === "asi");
$btnFeat.toggleClass("active", this._parent.state[propMode] === "feat");
}
$btnChooseFeat.toggleVe(this._parent.state[propMode] === "feat");
if (namespace === "ability") $stgAsi.toggleVe(this._parent.state[propMode] === "asi");
$stgFeat.toggleVe(this._parent.state[propMode] === "feat");
hkIxFeat();
};
this._parent.addHookBase(propMode, hkMode);
hkMode();
const $row = $$`<div class="ve-flex-v-end py-3 px-1">
<div class="btn-group">${$btnAsi}${$btnFeat}</div>
<div class="vr-4"></div>
${$stgAsi}
${$stgFeat}
</div>`.appendTo($wrpRows);
this._metasAsi[namespace][ix_] = {
$row,
};
}
this._metasAsi[namespace][ix_].$row.showVe().addClass("statgen-asi__row");
}
// Remove border styling from the last visible row
if (this._metasAsi[namespace][ix - 1]) this._metasAsi[namespace][ix - 1].$row.removeClass("statgen-asi__row");
for (; ix < this._metasAsi[namespace].length; ++ix) {
if (!this._metasAsi[namespace][ix]) continue;
this._metasAsi[namespace][ix].$row.hideVe().removeClass("statgen-asi__row");
}
};
this._parent.addHookBase(propCnt, hk);
hk();
}
_render_renderAdditionalFeatSection ({namespace, $wrpRows, propEntity}) {
const fnsCleanupEnt = [];
const fnsCleanupGroup = [];
const {propIxSel, propPrefix} = this._parent.getPropsAdditionalFeats_(namespace);
const resetGroupState = () => {
const nxtState = Object.keys(this._parent.state)
.filter(k => k.startsWith(propPrefix) && k !== propIxSel)
.mergeMap(k => ({[k]: null}));
this._parent.proxyAssignSimple("state", nxtState);
};
const hkEnt = (isNotFirstRun) => {
fnsCleanupEnt.splice(0, fnsCleanupEnt.length).forEach(fn => fn());
fnsCleanupGroup.splice(0, fnsCleanupGroup.length).forEach(fn => fn());
$wrpRows.empty();
if (isNotFirstRun) resetGroupState();
const ent = this._parent[namespace]; // e.g. `this._parent.race`
if ((ent?.feats?.length || 0) > 1) {
const {$sel: $selGroup, unhook: unhookIxGroup} = UtilAdditionalFeats.getSelIxSetMeta({comp: this._parent, prop: propIxSel, available: ent.feats});
fnsCleanupEnt.push(unhookIxGroup);
$$`<div class="ve-flex-col mb-2">
<div class="ve-flex-v-center mb-2">
<div class="mr-2">Feat Set:</div>
${$selGroup.addClass("max-w-200p")}
</div>
</div>`.appendTo($wrpRows);
} else {
this._parent.state[propIxSel] = 0;
}
const $wrpRowsInner = $(`<div class="w-100 ve-flex-col min-h-0"></div>`).appendTo($wrpRows);
const hkIxSel = (isNotFirstRun) => {
fnsCleanupGroup.splice(0, fnsCleanupGroup.length).forEach(fn => fn());
$wrpRowsInner.empty();
if (isNotFirstRun) resetGroupState();
const featSet = ent?.feats?.[this._parent.state[propIxSel]];
const uidsStatic = UtilAdditionalFeats.getUidsStatic(featSet);
const $rows = [];
uidsStatic.map((uid, ix) => {
const {propIxFeatAbility, propFeatAbilityChooseFrom} = this._parent.getPropsAdditionalFeatsFeatSet_(namespace, "static", ix);
const {name, source} = DataUtil.proxy.unpackUid("feat", uid, "feat", {isLower: true});
const feat = this._parent.feats.find(it => it.name.toLowerCase() === name && it.source.toLowerCase() === source);
const {$stgFeat, hkIxFeat, cleanup} = this._render_getMetaFeat({featStatic: feat, propIxFeatAbility, propFeatAbilityChooseFrom});
fnsCleanupGroup.push(cleanup);
hkIxFeat();
const $row = $$`<div class="ve-flex-v-end py-3 px-1 statgen-asi__row">
<div class="btn-group"><div class="w-100p ve-text-center">Feat</div></div>
<div class="vr-4"></div>
${$stgFeat}
</div>`.appendTo($wrpRowsInner);
$rows.push($row);
});
[...new Array(featSet?.any || 0)].map((_, ix) => {
const {propIxFeat, propIxFeatAbility, propFeatAbilityChooseFrom} = this._parent.getPropsAdditionalFeatsFeatSet_(namespace, "fromFilter", ix);
const {$stgFeat, hkIxFeat, cleanup} = this._render_getMetaFeat({propIxFeat, propIxFeatAbility, propFeatAbilityChooseFrom});
fnsCleanupGroup.push(cleanup);
hkIxFeat();
const $row = $$`<div class="ve-flex-v-end py-3 px-1 statgen-asi__row">
<div class="btn-group"><div class="w-100p ve-text-center">Feat</div></div>
<div class="vr-4"></div>
${$stgFeat}
</div>`.appendTo($wrpRowsInner);
$rows.push($row);
});
// Remove border styling from the last row
if ($rows.last()) $rows.last().removeClass("statgen-asi__row");
this._doPulseThrottled();
};
this._parent.addHookBase(propIxSel, hkIxSel);
fnsCleanupEnt.push(() => this._parent.removeHookBase(propIxSel, hkIxSel));
hkIxSel();
this._doPulseThrottled();
};
this._parent.addHookBase(propEntity, hkEnt);
hkEnt();
}
_render_getMetaFeat ({featStatic = null, propIxFeat = null, propIxFeatAbility, propFeatAbilityChooseFrom}) {
if (featStatic && propIxFeat) throw new Error(`Cannot combine static feat and feat property!`);
if (featStatic == null && propIxFeat == null) throw new Error(`Either a static feat or a feat property must be specified!`);
const $btnChooseFeat = featStatic ? null : $(`<button class="btn btn-xxs btn-default mr-2" title="Choose a Feat"><span class="glyphicon glyphicon-search"></span></button>`)
.click(async () => {
const selecteds = await this._parent.modalFilterFeats.pGetUserSelection();
if (selecteds == null || !selecteds.length) return;
const selected = selecteds[0];
const ix = this._parent.feats.findIndex(it => it.name === selected.name && it.source === selected.values.sourceJson);
if (!~ix) throw new Error(`Could not find selected entity: ${JSON.stringify(selected)}`); // Should never occur
this._parent.state[propIxFeat] = ix;
this._doPulseThrottled();
});
// region Feat
const $dispFeat = $(`<div class="ve-flex-v-center mr-2"></div>`);
const $stgSelectAbilitySet = $$`<div class="ve-flex-v-center mr-2"></div>`;
const $stgFeatNoChoice = $$`<div class="ve-flex-v-center mr-2"></div>`;
const $stgFeatChooseAsiFrom = $$`<div class="ve-flex-v-end"></div>`;
const $stgFeatChooseAsiWeighted = $$`<div class="ve-flex-v-center"></div>`;
const $stgFeat = $$`<div class="ve-flex-v-center">
${$btnChooseFeat}
${$dispFeat}
${$stgSelectAbilitySet}
${$stgFeatNoChoice}
${$stgFeatChooseAsiFrom}
${$stgFeatChooseAsiWeighted}
</div>`;
const fnsCleanup = [];
const fnsCleanupFeat = [];
const fnsCleanupFeatAbility = [];
const hkIxFeat = (isNotFirstRun) => {
fnsCleanupFeat.splice(0, fnsCleanupFeat.length).forEach(fn => fn());
fnsCleanupFeatAbility.splice(0, fnsCleanupFeatAbility.length).forEach(fn => fn());
if (isNotFirstRun) {
const nxtState = Object.keys(this._parent.state).filter(it => it.startsWith(propFeatAbilityChooseFrom)).mergeMap(it => ({[it]: null}));
this._parent.proxyAssignSimple("state", nxtState);
}
const feat = featStatic || this._parent.feats[this._parent.state[propIxFeat]];
$stgFeat.removeClass("ve-flex-v-end").addClass("ve-flex-v-center");
$dispFeat.toggleClass("italic ve-muted", !feat);
$dispFeat.html(feat ? Renderer.get().render(`{@feat ${feat.name.toLowerCase()}|${feat.source}}`) : `(Choose a feat)`);
this._parent.state[propIxFeatAbility] = 0;
$stgSelectAbilitySet.hideVe();
if (feat) {
if (feat.ability && feat.ability.length > 1) {
const metaChooseAbilitySet = ComponentUiUtil.$getSelEnum(
this._parent,
propIxFeatAbility,
{
values: feat.ability.map((_, i) => i),
fnDisplay: ix => Renderer.getAbilityData([feat.ability[ix]]).asText,
asMeta: true,
},
);
$stgSelectAbilitySet.showVe().append(metaChooseAbilitySet.$sel);
metaChooseAbilitySet.$sel.change(() => this._doPulseThrottled());
fnsCleanupFeat.push(() => metaChooseAbilitySet.unhook());
}
const hkAbilitySet = () => {
fnsCleanupFeatAbility.splice(0, fnsCleanupFeatAbility.length).forEach(fn => fn());
if (!feat.ability) {
$stgFeatNoChoice.empty().hideVe();
$stgFeatChooseAsiFrom.empty().hideVe();
return;
}
const abilitySet = feat.ability[this._parent.state[propIxFeatAbility]];
// region Static/no choices
const ptsNoChoose = Parser.ABIL_ABVS.filter(ab => abilitySet[ab]).map(ab => `${Parser.attAbvToFull(ab)} ${UiUtil.intToBonus(abilitySet[ab], {isPretty: true})}`);
$stgFeatNoChoice.empty().toggleVe(ptsNoChoose.length).html(`<div><span class="mr-2">\u2014</span>${ptsNoChoose.join(", ")}</div>`);
// endregion
// region Choices
if (abilitySet.choose && abilitySet.choose.from) {
$stgFeat.removeClass("ve-flex-v-center").addClass("ve-flex-v-end");
$stgFeatChooseAsiFrom.showVe().empty();
$stgFeatChooseAsiWeighted.empty().hideVe();
const count = abilitySet.choose.count || 1;
const amount = abilitySet.choose.amount || 1;
const {rowMetas, cleanup: cleanupAsiPicker} = ComponentUiUtil.getMetaWrpMultipleChoice(
this._parent,
propFeatAbilityChooseFrom,
{
values: abilitySet.choose.from,
fnDisplay: v => `${Parser.attAbvToFull(v)} ${UiUtil.intToBonus(amount, {isPretty: true})}`,
count,
},
);
fnsCleanupFeatAbility.push(() => cleanupAsiPicker());
$stgFeatChooseAsiFrom.append(`<div><span class="mr-2">\u2014</span>choose ${count > 1 ? `${count} ` : ""}${UiUtil.intToBonus(amount, {isPretty: true})}</div>`);
rowMetas.forEach(meta => {
meta.$cb.change(() => this._doPulseThrottled());
$$`<label class="ve-flex-col no-select">
<div class="ve-flex-vh-center statgen-asi__cell-feat" title="${Parser.attAbvToFull(meta.value)}">${meta.value.toUpperCase()}</div>
<div class="ve-flex-vh-center statgen-asi__cell-feat">${meta.$cb}</div>
</label>`.appendTo($stgFeatChooseAsiFrom);
});
} else if (abilitySet.choose && abilitySet.choose.weighted) {
// TODO(Future) unsupported, for now
$stgFeatChooseAsiFrom.empty().hideVe();
$stgFeatChooseAsiWeighted.showVe().html(`<i class="ve-muted">The selected ability score format is currently unsupported. Please check back later!</i>`);
} else {
$stgFeatChooseAsiFrom.empty().hideVe();
$stgFeatChooseAsiWeighted.empty().hideVe();
}
// endregion
this._doPulseThrottled();
};
this._parent.addHookBase(propIxFeatAbility, hkAbilitySet);
fnsCleanupFeat.push(() => this._parent.removeHookBase(propIxFeatAbility, hkAbilitySet));
hkAbilitySet();
} else {
$stgFeatNoChoice.empty().hideVe();
$stgFeatChooseAsiFrom.empty().hideVe();
$stgFeatChooseAsiWeighted.empty().hideVe();
}
this._doPulseThrottled();
};
if (!featStatic) {
this._parent.addHookBase(propIxFeat, hkIxFeat);
fnsCleanup.push(() => this._parent.removeHookBase(propIxFeat, hkIxFeat));
}
const cleanup = () => {
fnsCleanup.splice(0, fnsCleanup.length).forEach(fn => fn());
fnsCleanupFeat.splice(0, fnsCleanupFeat.length).forEach(fn => fn());
fnsCleanupFeatAbility.splice(0, fnsCleanupFeatAbility.length).forEach(fn => fn());
};
return {$btnChooseFeat, $stgFeat, hkIxFeat, cleanup};
}
render ($wrpAsi) {
const $wrpRowsAsi = $(`<div class="ve-flex-col w-100 overflow-y-auto"></div>`);
const $wrpRowsRace = $(`<div class="ve-flex-col w-100 overflow-y-auto"></div>`);
const $wrpRowsBackground = $(`<div class="ve-flex-col w-100 overflow-y-auto"></div>`);
const $wrpRowsCustom = $(`<div class="ve-flex-col w-100 overflow-y-auto"></div>`);
this._render_renderAsiFeatSection("common_cntAsi", "ability", $wrpRowsAsi);
this._render_renderAsiFeatSection("common_cntFeatsCustom", "custom", $wrpRowsCustom);
this._render_renderAdditionalFeatSection({propEntity: "common_ixRace", namespace: "race", $wrpRows: $wrpRowsRace});
this._render_renderAdditionalFeatSection({propEntity: "common_ixBackground", namespace: "background", $wrpRows: $wrpRowsBackground});
const $getStgEntity = ({title, $wrpRows, propEntity, propIxEntity}) => {
const $stg = $$`<div class="ve-flex-col">
<hr class="hr-3 hr--dotted">
<h4 class="my-2 bold">${title} Feats</h4>
${$wrpRows}
</div>`;
const hkIxEntity = () => {
const entity = this._parent[propEntity];
$stg.toggleVe(!this._parent.isLevelUp && !!entity?.feats);
};
this._parent.addHookBase(propIxEntity, hkIxEntity);
hkIxEntity();
return $stg;
};
const $stgRace = $getStgEntity({title: "Race", $wrpRows: $wrpRowsRace, propEntity: "race", propIxEntity: "common_ixRace"});
const $stgBackground = $getStgEntity({title: "Background", $wrpRows: $wrpRowsBackground, propEntity: "background", propIxEntity: "common_ixBackground"});
const $iptCountFeatsCustom = ComponentUiUtil.$getIptInt(this._parent, "common_cntFeatsCustom", 0, {min: 0, max: StatGenUi._MAX_CUSTOM_FEATS})
.addClass("w-100p ve-text-center");
$$($wrpAsi)`
<h4 class="my-2 bold">Ability Score Increases</h4>
${this._render_$getStageCntAsi()}
${$wrpRowsAsi}
${$stgRace}
${$stgBackground}
<hr class="hr-3 hr--dotted">
<h4 class="my-2 bold">Additional Feats</h4>
<label class="w-100 ve-flex-v-center mb-2">
<div class="mr-2 no-shrink">Number of additional feats:</div>${$iptCountFeatsCustom}
</label>
${$wrpRowsCustom}
`;
}
_render_$getStageCntAsi () {
if (!this._parent.isCharacterMode) {
const $iptCountAsi = ComponentUiUtil.$getIptInt(this._parent, "common_cntAsi", 0, {min: 0, max: 20})
.addClass("w-100p ve-text-center");
return $$`<label class="w-100 ve-flex-v-center mb-2"><div class="mr-2 no-shrink">Number of Ability Score Increases to apply:</div>${$iptCountAsi}</label>`;
}
const $out = $$`<div class="w-100 ve-flex-v-center mb-2 italic ve-muted">No ability score increases available.</div>`;
const hkCntAsis = () => $out.toggleVe(this._parent.state.common_cntAsi === 0);
this._parent.addHookBase("common_cntAsi", hkCntAsis);
hkCntAsis();
return $out;
}
_getFormData_getForNamespace_basic (outs, outIsFormCompletes, outFeats, propCnt, namespace) {
for (let i = 0; i < this._parent.state[propCnt]; ++i) {
const {propMode, propIxFeat, propIxAsiPointOne, propIxAsiPointTwo, propIxFeatAbility, propFeatAbilityChooseFrom} = this._parent.getPropsAsi(i, namespace);
if (this._parent.state[propMode] === "asi") {
const out = {};
let ttlChosen = 0;
Parser.ABIL_ABVS.forEach((ab, abI) => {
const increase = [this._parent.state[propIxAsiPointOne] === abI, this._parent.state[propIxAsiPointTwo] === abI].filter(Boolean).length;
if (increase) out[ab] = increase;
ttlChosen += increase;
});
const isFormComplete = ttlChosen === 2;
outFeats[namespace].push(null); // Pad the array
outs.push(out);
outIsFormCompletes.push(isFormComplete);
} else if (this._parent.state[propMode] === "feat") {
const {isFormComplete, out} = this._getFormData_doAddFeatMeta({
namespace,
outFeats,
propIxFeat,
propIxFeatAbility,
propFeatAbilityChooseFrom,
type: "choose",
});
outs.push(out);
outIsFormCompletes.push(isFormComplete);
}
}
}
_getFormData_getForNamespace_additional (outs, outIsFormCompletes, outFeats, namespace) {
const ent = this._parent[namespace]; // e.g. `this._parent.race`
if (!ent?.feats?.length) return;
const {propIxSel} = this._parent.getPropsAdditionalFeats_(namespace);
const featSet = ent.feats[this._parent.state[propIxSel]];
if (!featSet) {
outIsFormCompletes.push(false);
return;
}
const uidsStatic = UtilAdditionalFeats.getUidsStatic(featSet);
uidsStatic.map((uid, ix) => {
const {propIxFeatAbility, propFeatAbilityChooseFrom} = this._parent.getPropsAdditionalFeatsFeatSet_(namespace, "static", ix);
const {name, source} = DataUtil.proxy.unpackUid("feat", uid, "feat", {isLower: true});
const feat = this._parent.feats.find(it => it.name.toLowerCase() === name && it.source.toLowerCase() === source);
const {isFormComplete, out} = this._getFormData_doAddFeatMeta({
namespace,
outFeats,
featStatic: feat,
propIxFeatAbility,
propFeatAbilityChooseFrom,
type: "static",
});
outs.push(out);
outIsFormCompletes.push(isFormComplete);
});
[...new Array(featSet.any || 0)].map((_, ix) => {
const {propIxFeat, propIxFeatAbility, propFeatAbilityChooseFrom} = this._parent.getPropsAdditionalFeatsFeatSet_(namespace, "fromFilter", ix);
const {isFormComplete, out} = this._getFormData_doAddFeatMeta({
namespace,
outFeats,
propIxFeat,
propIxFeatAbility,
propFeatAbilityChooseFrom,
type: "choose",
});
outs.push(out);
outIsFormCompletes.push(isFormComplete);
});
}
_getFormData_doAddFeatMeta ({namespace, outFeats, propIxFeat = null, featStatic = null, propIxFeatAbility, propFeatAbilityChooseFrom, type}) {
if (featStatic && propIxFeat) throw new Error(`Cannot combine static feat and feat property!`);
if (featStatic == null && propIxFeat == null) throw new Error(`Either a static feat or a feat property must be specified!`);
const out = {};
const feat = featStatic || this._parent.feats[this._parent.state[propIxFeat]];
const featMeta = feat
? {ix: this._parent.state[propIxFeat], uid: `${feat.name}|${feat.source}`, type}
: {ix: -1, uid: null, type};
outFeats[namespace].push(featMeta);
if (!~featMeta.ix) return {isFormComplete: false, out};
if (!feat.ability) return {isFormComplete: true, out};
const abilitySet = feat.ability[this._parent.state[propIxFeatAbility] || 0];
// Add static values
Parser.ABIL_ABVS.forEach(ab => { if (abilitySet[ab]) out[ab] = abilitySet[ab]; });
if (!abilitySet.choose) return {isFormComplete: true, out};
let isFormComplete = true;
// Track any bonuses chosen, so we can use `"inherit"` when handling a feats "additionalSpells" elsewhere
featMeta.abilityChosen = {};
if (abilitySet.choose.from) {
if (isFormComplete) isFormComplete = !!this._parent.state[ComponentUiUtil.getMetaWrpMultipleChoice_getPropIsAcceptable(propFeatAbilityChooseFrom)];
const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this._parent, propFeatAbilityChooseFrom);
ixs.map(it => abilitySet.choose.from[it]).forEach(ab => {
const amount = abilitySet.choose.amount || 1;
out[ab] = (out[ab] || 0) + amount;
featMeta.abilityChosen[ab] = amount;
});
}
return {isFormComplete, out};
}
getFormData () {
const outs = [];
const isFormCompletes = [];
const feats = {ability: [], race: [], background: [], custom: []};
this._getFormData_getForNamespace_basic(outs, isFormCompletes, feats, "common_cntAsi", "ability");
this._getFormData_getForNamespace_basic(outs, isFormCompletes, feats, "common_cntFeatsCustom", "custom");
this._getFormData_getForNamespace_additional(outs, isFormCompletes, feats, "race");
this._getFormData_getForNamespace_additional(outs, isFormCompletes, feats, "background");
const data = {};
outs.filter(Boolean).forEach(abilBonuses => Object.entries(abilBonuses).forEach(([ab, bonus]) => data[ab] = (data[ab] || 0) + bonus));
return {
isFormComplete: isFormCompletes.every(Boolean),
dataPerAsi: outs,
data,
feats,
};
}
};
StatGenUi.RenderableCollectionPbRules = class extends RenderableCollectionGenericRows {
constructor (statGenUi, $wrp) {
super(statGenUi, "pb_rules", $wrp);
}
getNewRender (rule, i) {
const parentComp = this._comp;
const comp = this._utils.getNewRenderComp(rule, i);
const $dispCost = $(`<div class="ve-flex-vh-center"></div>`);
const hkCost = () => $dispCost.text(comp._state.cost);
comp._addHookBase("cost", hkCost);
hkCost();
const $iptCost = ComponentUiUtil.$getIptInt(comp, "cost", 0, {html: `<input class="form-control input-xs form-control--minimal ve-text-center">`, fallbackOnNaN: 0});
const hkIsCustom = () => {
$dispCost.toggleVe(!parentComp.state.pb_isCustom);
$iptCost.toggleVe(parentComp.state.pb_isCustom);
};
parentComp._addHookBase("pb_isCustom", hkIsCustom);
hkIsCustom();
const $btnDelete = $(`<button class="btn btn-xxs btn-danger" title="Delete"><span class="glyphicon glyphicon-trash"></span></button>`)
.click(() => {
if (parentComp.state.pb_rules.length === 1) return; // Never delete the final item
parentComp.state.pb_rules = parentComp.state.pb_rules.filter(it => it !== rule);
});
const $wrpRow = $$`<div class="ve-flex py-1 stripe-even statgen-pb__row-cost">
<div class="statgen-pb__col-cost ve-flex-vh-center">${comp._state.score}</div>
<div class="statgen-pb__col-cost ve-flex-vh-center">${Parser.getAbilityModifier(comp._state.score)}</div>
<div class="statgen-pb__col-cost ve-flex-vh-center px-3">
${$dispCost}
${$iptCost}
</div>
<div class="statgen-pb__col-cost-delete">${$btnDelete}</div>
</div>`.appendTo(this._$wrpRows);
const hkRules = () => {
$btnDelete.toggleVe((parentComp.state.pb_rules[0] === rule || parentComp.state.pb_rules.last() === rule) && parentComp.state.pb_isCustom);
};
parentComp._addHookBase("pb_rules", hkRules);
parentComp._addHookBase("pb_isCustom", hkRules);
hkRules();
return {
comp,
$wrpRow,
fnCleanup: () => {
parentComp._removeHookBase("pb_isCustom", hkIsCustom);
parentComp._removeHookBase("pb_isCustom", hkRules);
parentComp._removeHookBase("pb_rules", hkRules);
},
};
}
doDeleteExistingRender (renderedMeta) {
renderedMeta.fnCleanup();
}
};