"use strict"; Renderer.dice = { SYSTEM_USER: { name: "Avandra", // goddess of luck }, POS_INFINITE: 100000000000000000000, // larger than this, and we start to see "e" numbers appear _SYMBOL_PARSE_FAILED: Symbol("parseFailed"), _$wrpRoll: null, _$minRoll: null, _$iptRoll: null, _$outRoll: null, _$head: null, _hist: [], _histIndex: null, _$lastRolledBy: null, _storage: null, _isManualMode: false, /* -------------------------------------------- */ // region Utilities DICE: [4, 6, 8, 10, 12, 20, 100], getNextDice (faces) { const idx = Renderer.dice.DICE.indexOf(faces); if (~idx) return Renderer.dice.DICE[idx + 1]; else return null; }, getPreviousDice (faces) { const idx = Renderer.dice.DICE.indexOf(faces); if (~idx) return Renderer.dice.DICE[idx - 1]; else return null; }, // endregion /* -------------------------------------------- */ // region DM Screen integration _panel: null, bindDmScreenPanel (panel, title) { if (Renderer.dice._panel) { // there can only be one roller box Renderer.dice.unbindDmScreenPanel(); } Renderer.dice._showBox(); Renderer.dice._panel = panel; panel.doPopulate_Rollbox(title); }, unbindDmScreenPanel () { if (Renderer.dice._panel) { $(`body`).append(Renderer.dice._$wrpRoll); Renderer.dice._panel.close$TabContent(); Renderer.dice._panel = null; Renderer.dice._hideBox(); Renderer.dice._$wrpRoll.removeClass("rollbox-panel"); } }, get$Roller () { return Renderer.dice._$wrpRoll; }, // endregion /* -------------------------------------------- */ bindOnclickListener (ele) { ele.addEventListener("click", (evt) => { const eleDice = evt.target.hasAttribute("data-packed-dice") ? evt.target // Tolerate e.g. Bestiary wrapped proficiency dice rollers : evt.target.parentElement?.hasAttribute("data-packed-dice") ? evt.target.parentElement : null; if (!eleDice) return; evt.preventDefault(); evt.stopImmediatePropagation(); Renderer.dice.pRollerClickUseData(evt, eleDice).then(null); }); }, /* -------------------------------------------- */ /** * Silently roll an expression and get the result. * Note that this does not support dynamic variables (e.g. user proficiency bonus). */ parseRandomise2 (str) { if (!str || !str.trim()) return null; const wrpTree = Renderer.dice.lang.getTree3(str); if (wrpTree) return wrpTree.tree.evl({}); else return null; }, /** * Silently get the average of an expression. * Note that this does not support dynamic variables (e.g. user proficiency bonus). */ parseAverage (str) { if (!str || !str.trim()) return null; const wrpTree = Renderer.dice.lang.getTree3(str); if (wrpTree) return wrpTree.tree.avg({}); else return null; }, // region Roll box UI _showBox () { Renderer.dice._$minRoll.hideVe(); Renderer.dice._$wrpRoll.showVe(); Renderer.dice._$iptRoll.prop("placeholder", `${Renderer.dice._getRandomPlaceholder()} or "/help"`); }, _hideBox () { Renderer.dice._$minRoll.showVe(); Renderer.dice._$wrpRoll.hideVe(); }, _getRandomPlaceholder () { const count = RollerUtil.randomise(10); const faces = Renderer.dice.DICE[RollerUtil.randomise(Renderer.dice.DICE.length - 1)]; const mod = (RollerUtil.randomise(3) - 2) * RollerUtil.randomise(10); const drop = (count > 1) && RollerUtil.randomise(5) === 5; const dropDir = drop ? RollerUtil.randomise(2) === 2 ? "h" : "l" : ""; const dropAmount = drop ? RollerUtil.randomise(count - 1) : null; return `${count}d${faces}${drop ? `d${dropDir}${dropAmount}` : ""}${mod < 0 ? mod : mod > 0 ? `+${mod}` : ""}`; }, /** Initialise the roll box UI. */ async _pInit () { const $wrpRoll = $(`
`).hideVe(); const $minRoll = $(``).on("click", () => { Renderer.dice._showBox(); Renderer.dice._$iptRoll.focus(); }); const $head = $(`
Dice Roller
`) .on("click", () => { if (!Renderer.dice._panel) Renderer.dice._hideBox(); }); const $outRoll = $(`
`); const $iptRoll = $(``) .on("keypress", async evt => { evt.stopPropagation(); if (evt.key !== "Enter") return; const strDice = $iptRoll.val(); const result = await Renderer.dice.pRoll2( strDice, { isUser: true, name: "Anon", }, ); $iptRoll.val(""); if (result === Renderer.dice._SYMBOL_PARSE_FAILED) { Renderer.dice._showInvalid(); $iptRoll.addClass("form-control--error"); } }).on("keydown", (evt) => { $iptRoll.removeClass("form-control--error"); // arrow keys only work on keydown if (evt.key === "ArrowUp") { evt.preventDefault(); Renderer.dice._prevHistory(); return; } if (evt.key === "ArrowDown") { evt.preventDefault(); Renderer.dice._nextHistory(); } }); $wrpRoll.append($head).append($outRoll).append($iptRoll); Renderer.dice._$wrpRoll = $wrpRoll; Renderer.dice._$minRoll = $minRoll; Renderer.dice._$head = $head; Renderer.dice._$outRoll = $outRoll; Renderer.dice._$iptRoll = $iptRoll; $(`body`).append($minRoll).append($wrpRoll); $wrpRoll.on("click", ".out-roll-item-code", (evt) => Renderer.dice._$iptRoll.val($(evt.target).text()).focus()); Renderer.dice.storage = await StorageUtil.pGet(VeCt.STORAGE_ROLLER_MACRO) || {}; }, _prevHistory () { Renderer.dice._histIndex--; Renderer.dice._prevNextHistory_load(); }, _nextHistory () { Renderer.dice._histIndex++; Renderer.dice._prevNextHistory_load(); }, _prevNextHistory_load () { Renderer.dice._cleanHistoryIndex(); const nxtVal = Renderer.dice._hist[Renderer.dice._histIndex]; Renderer.dice._$iptRoll.val(nxtVal); if (nxtVal) Renderer.dice._$iptRoll[0].selectionStart = Renderer.dice._$iptRoll[0].selectionEnd = nxtVal.length; }, _cleanHistoryIndex: () => { if (!Renderer.dice._hist.length) { Renderer.dice._histIndex = null; } else { Renderer.dice._histIndex = Math.min(Renderer.dice._hist.length, Math.max(Renderer.dice._histIndex, 0)); } }, _addHistory: (str) => { Renderer.dice._hist.push(str); // point index at the top of the stack Renderer.dice._histIndex = Renderer.dice._hist.length; }, _scrollBottom: () => { Renderer.dice._$outRoll.scrollTop(1e10); }, // endregion // region Event handling RE_PROMPT: /#\$prompt_number:?([^$]*)\$#/g, async pRollerClickUseData (evt, ele) { evt.stopPropagation(); evt.preventDefault(); const $ele = $(ele); const rollData = $ele.data("packed-dice"); let name = $ele.data("roll-name"); let shiftKey = evt.shiftKey; let ctrlKey = EventUtil.isCtrlMetaKey(evt); const options = rollData.toRoll.split(";").map(it => it.trim()).filter(Boolean); let chosenRollData; if (options.length > 1) { const cpyRollData = MiscUtil.copyFast(rollData); const menu = ContextUtil.getMenu([ new ContextUtil.Action( "Choose Roll", null, {isDisabled: true}, ), null, ...options.map(it => new ContextUtil.Action( `Roll ${it}`, evt => { shiftKey = shiftKey || evt.shiftKey; ctrlKey = ctrlKey || (EventUtil.isCtrlMetaKey(evt)); cpyRollData.toRoll = it; return cpyRollData; }, )), ]); chosenRollData = await ContextUtil.pOpenMenu(evt, menu); } else chosenRollData = rollData; if (!chosenRollData) return; const results = []; for (const m of chosenRollData.toRoll.matchAll(Renderer.dice.RE_PROMPT)) { const optionsRaw = m[1]; const opts = {}; if (optionsRaw) { const spl = optionsRaw.split(","); spl.map(it => it.trim()).forEach(part => { const [k, v] = part.split("=").map(it => it.trim()); switch (k) { case "min": case "max": opts[k] = Number(v); break; default: opts[k] = v; break; } }); } if (opts.min == null) opts.min = 0; if (opts.max == null) opts.max = Renderer.dice.POS_INFINITE; if (opts.default == null) opts.default = 0; const input = await InputUiUtil.pGetUserNumber(opts); if (input == null) return; results.push(input); } const rollDataCpy = MiscUtil.copyFast(chosenRollData); rollDataCpy.toRoll = rollDataCpy.toRoll.replace(Renderer.dice.RE_PROMPT, () => results.shift()); // If there's a prompt, prompt the user to select the dice let rollDataCpyToRoll; if (rollData.prompt) { const sortedKeys = Object.keys(rollDataCpy.prompt.options).sort(SortUtil.ascSortLower); const menu = ContextUtil.getMenu([ new ContextUtil.Action(rollDataCpy.prompt.entry, null, {isDisabled: true}), null, ...sortedKeys .map(it => { const title = rollDataCpy.prompt.mode === "psi" ? `${it} point${it === "1" ? "" : "s"}` : `${Parser.spLevelToFull(it)} level`; return new ContextUtil.Action( title, evt => { shiftKey = shiftKey || evt.shiftKey; ctrlKey = ctrlKey || (EventUtil.isCtrlMetaKey(evt)); const fromScaling = rollDataCpy.prompt.options[it]; if (!fromScaling) { name = ""; return rollDataCpy; } else { name = rollDataCpy.prompt.mode === "psi" ? `${it} psi activation` : `${Parser.spLevelToFull(it)}-level cast`; rollDataCpy.toRoll += `+${fromScaling}`; return rollDataCpy; } }, ); }), ]); rollDataCpyToRoll = await ContextUtil.pOpenMenu(evt, menu); } else rollDataCpyToRoll = rollDataCpy; if (!rollDataCpyToRoll) return; await Renderer.dice.pRollerClick({shiftKey, ctrlKey}, ele, JSON.stringify(rollDataCpyToRoll), name); }, __rerollNextInlineResult (ele) { const $ele = $(ele); const $result = $ele.next(`.result`); const r = Renderer.dice.__rollPackedData($ele); $result.text(r); }, __rollPackedData ($ele) { // Note that this does not support dynamic variables (e.g. user proficiency bonus) const wrpTree = Renderer.dice.lang.getTree3($ele.data("packed-dice").toRoll); return wrpTree.tree.evl({}); }, $getEleUnknownTableRoll (total) { return $(Renderer.dice._pRollerClick_getMsgBug(total)); }, _pRollerClick_getMsgBug (total) { return `No result found matching roll ${total}?! 🐛`; }, async pRollerClick (evtMock, ele, packed, name) { const $ele = $(ele); const entry = JSON.parse(packed); const additionalData = {...ele.dataset}; const rolledBy = { name: Renderer.dice._pRollerClick_attemptToGetNameOfRoller({$ele}), label: name != null ? name : Renderer.dice._pRollerClick_attemptToGetNameOfRoll({entry, $ele}), }; const modRollMeta = Renderer.dice.getEventModifiedRollMeta(evtMock, entry); const $parent = $ele.closest("th, p, table"); const rollResult = await this._pRollerClick_pGetResult({ $parent, $ele, entry, modRollMeta, rolledBy, additionalData, }); if (!entry.autoRoll) return; const $tgt = $ele.next(`[data-rd-is-autodice-result="true"]`); const curTxt = $tgt.text(); $tgt.text(rollResult); JqueryUtil.showCopiedEffect($tgt, curTxt, true); }, async _pRollerClick_pGetResult ({$parent, $ele, entry, modRollMeta, rolledBy, additionalData}) { const sharedRollOpts = { rollCount: modRollMeta.rollCount, additionalData, isHidden: !!entry.autoRoll, }; if ($parent.is("th") && $parent.attr("data-rd-isroller") === "true") { if ($parent.attr("data-rd-namegeneratorrolls")) { return Renderer.dice._pRollerClick_pRollGeneratorTable({ $parent, $ele, rolledBy, modRollMeta, rollOpts: sharedRollOpts, }); } return Renderer.dice.pRollEntry( modRollMeta.entry, rolledBy, { ...sharedRollOpts, fnGetMessage: Renderer.dice._pRollerClick_fnGetMessageTable.bind(Renderer.dice, $ele), }, ); } return Renderer.dice.pRollEntry( modRollMeta.entry, rolledBy, { ...sharedRollOpts, }, ); }, _pRollerClick_fnGetMessageTable ($ele, total) { const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, total); if (elesTd) { const tableRow = elesTd.map(ele => ele.innerHTML.trim()).filter(it => it).join(" | "); const $row = $(`${tableRow}`); Renderer.dice._pRollerClick_rollInlineRollers($ele); return $row.html(); } return Renderer.dice._pRollerClick_getMsgBug(total); }, // Aka "getTableName", probably _pRollerClick_attemptToGetNameOfRoll ({entry, $ele}) { // Try to use the entry's built-in name if (entry.name) return entry.name; // try to use table caption let titleMaybe = $ele.closest(`table:not(.stats)`).children(`caption`).text(); if (titleMaybe) return titleMaybe.trim(); // try to use list item title titleMaybe = $ele.parent().children(`.rd__list-item-name`).text(); if (titleMaybe) return titleMaybe.trim().replace(/[.,:]$/, ""); // use the section title, where applicable titleMaybe = $ele.closest(`div`).children(`.rd__h`).first().find(`.entry-title-inner`).text(); if (titleMaybe) { titleMaybe = titleMaybe.trim().replace(/[.,:]$/, ""); return titleMaybe; } // try to use stats table name row titleMaybe = $ele.closest(`table.stats`).children(`tbody`).first().children(`tr`).first().find(`.rnd-name .stats-name`).text(); if (titleMaybe) return titleMaybe.trim(); if (UrlUtil.getCurrentPage() === UrlUtil.PG_CHARACTERS) { // try use mini-entity name titleMaybe = ($ele.closest(`.chr-entity__row`).find(".chr-entity__ipt-name").val() || "").trim(); if (titleMaybe) return titleMaybe; } return titleMaybe; }, _pRollerClick_attemptToGetNameOfRoller ({$ele}) { const $hov = $ele.closest(`.hwin`); if ($hov.length) return $hov.find(`.stats-name`).first().text(); const $roll = $ele.closest(`.out-roll-wrp`); if ($roll.length) return $roll.data("name"); const $dispPanelTitle = $ele.closest(`.dm-screen-panel`).children(`.panel-control-title`); if ($dispPanelTitle.length) return $dispPanelTitle.text().trim(); let name = document.title.replace("- 5etools", "").trim(); return name === "DM Screen" ? "Dungeon Master" : name; }, _pRollerClick_$getTdsFromTotal ($ele, total) { const $table = $ele.closest(`table`); const $tdRoll = $table.find(`td`).filter((i, e) => { const $e = $(e); if (!$e.closest(`table`).is($table)) return false; return total >= Number($e.data("roll-min")) && total <= Number($e.data("roll-max")); }); if ($tdRoll.length && $tdRoll.nextAll().length) { return $tdRoll.nextAll().get(); } return null; }, // TODO erm _pRollerClick_rollInlineRollers ($ele) { $ele.find(`.render-roller`).each((i, e) => { const $e = $(e); const r = Renderer.dice.__rollPackedData($e); $e.attr("onclick", `Renderer.dice.__rerollNextInlineResult(this)`); $e.after(` (${r})`); }); }, _pRollerClick_fnGetMessageGeneratorTable ($ele, ix, total) { const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, total); if (elesTd) { const $row = $(`${elesTd[ix].innerHTML.trim()}`); Renderer.dice._pRollerClick_rollInlineRollers($ele); return $row.html(); } return Renderer.dice._pRollerClick_getMsgBug(total); }, async _pRollerClick_pRollGeneratorTable ({$parent, $ele, rolledBy, modRollMeta, rollOpts}) { Renderer.dice.addElement({rolledBy, html: `${rolledBy.label}:`, isMessage: true}); // Track a total of all rolls--this is a bit meaningless, but this method is expected to return a result value let total = 0; const out = []; const numRolls = Number($parent.attr("data-rd-namegeneratorrolls")); const $ths = $ele.closest(`table`).find(`th`); for (let i = 0; i < numRolls; ++i) { const cpyRolledBy = MiscUtil.copyFast(rolledBy); cpyRolledBy.label = $($ths.get(i + 1)).text().trim(); const result = await Renderer.dice.pRollEntry( modRollMeta.entry, cpyRolledBy, { ...rollOpts, fnGetMessage: Renderer.dice._pRollerClick_fnGetMessageGeneratorTable.bind(Renderer.dice, $ele, i), }, ); total += result; const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, result); if (!elesTd) { out.push(`(no result)`); continue; } out.push(elesTd[i].innerHTML.trim()); } Renderer.dice.addElement({rolledBy, html: `= ${out.join(" ")}`, isMessage: true}); return total; }, getEventModifiedRollMeta (evt, entry) { // Change roll type/count depending on CTRL/SHIFT status const out = {rollCount: 1, entry}; if (evt.shiftKey) { if (entry.subType === "damage") { // If SHIFT is held, roll crit const dice = []; // TODO(future) in order for this to correctly catch everything, would need to parse the toRoll as a tree and then pull all dice expressions from the first level of that tree entry.toRoll .replace(/\s+/g, "") // clean whitespace .replace(/\d*?d\d+/gi, m0 => dice.push(m0)); entry.toRoll = `${entry.toRoll}${dice.length ? `+${dice.join("+")}` : ""}`; } else if (entry.subType === "d20") { // If SHIFT is held, roll advantage // If we have a cached d20mod value, use it if (entry.d20mod != null) entry.toRoll = `2d20dl1${entry.d20mod}`; else entry.toRoll = entry.toRoll.replace(/^\s*1?\s*d\s*20/, "2d20dl1"); } else out.rollCount = 2; // otherwise, just roll twice } if (EventUtil.isCtrlMetaKey(evt)) { if (entry.subType === "damage") { // If CTRL is held, half the damage entry.toRoll = `floor((${entry.toRoll}) / 2)`; } else if (entry.subType === "d20") { // If CTRL is held, roll disadvantage (assuming SHIFT is not held) // If we have a cached d20mod value, use it if (entry.d20mod != null) entry.toRoll = `2d20dh1${entry.d20mod}`; else entry.toRoll = entry.toRoll.replace(/^\s*1?\s*d\s*20/, "2d20dh1"); } else out.rollCount = 2; // otherwise, just roll twice } return out; }, // endregion /** * Parse and roll a string, and display the result in the roll box. * Returns the total rolled, if available. * @param str * @param rolledBy * @param rolledBy.isUser * @param rolledBy.name The name of the roller. * @param rolledBy.label The label for this roll. * @param [opts] Options object. * @param [opts.isResultUsed] If an input box should be provided for the user to enter the result (manual mode only). */ async pRoll2 (str, rolledBy, opts) { opts = opts || {}; str = str .trim() .replace(/\/r(?:oll)? /gi, "").trim() // Remove any leading "/r"s, for ease of use ; if (!str) return; if (rolledBy.isUser) Renderer.dice._addHistory(str); if (str.startsWith("/")) return Renderer.dice._pHandleCommand(str, rolledBy); if (str.startsWith("#")) return Renderer.dice._pHandleSavedRoll(str, rolledBy, opts); const [head, ...tail] = str.split(":"); if (tail.length) { str = tail.join(":"); rolledBy.label = head; } const wrpTree = Renderer.dice.lang.getTree3(str); if (!wrpTree) return Renderer.dice._SYMBOL_PARSE_FAILED; return Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); }, /** * Parse and roll an entry, and display the result in the roll box. * Returns the total rolled, if available. * @param entry * @param rolledBy * @param [opts] Options object. * @param [opts.isResultUsed] If an input box should be provided for the user to enter the result (manual mode only). * @param [opts.rollCount] * @param [opts.additionalData] * @param [opts.isHidden] If the result should not be posted to the rollbox. */ async pRollEntry (entry, rolledBy, opts) { opts = opts || {}; const rollCount = Math.round(opts.rollCount || 1); delete opts.rollCount; if (rollCount <= 0) throw new Error(`Invalid roll count: ${rollCount} (must be a positive integer)`); const wrpTree = Renderer.dice.lang.getTree3(entry.toRoll); wrpTree.tree.successThresh = entry.successThresh; wrpTree.tree.successMax = entry.successMax; wrpTree.tree.chanceSuccessText = entry.chanceSuccessText; wrpTree.tree.chanceFailureText = entry.chanceFailureText; wrpTree.tree.isColorSuccessFail = entry.isColorSuccessFail; // arbitrarily return the result of the highest roll if we roll multiple times const results = []; if (rollCount > 1 && !opts.isHidden) Renderer.dice._showMessage(`Rolling twice...`, rolledBy); for (let i = 0; i < rollCount; ++i) { const result = await Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); if (result == null) return null; results.push(result); } return Math.max(...results); }, /** * @param wrpTree * @param rolledBy * @param [opts] Options object. * @param [opts.fnGetMessage] * @param [opts.isResultUsed] * @param [opts.additionalData] */ async _pHandleRoll2 (wrpTree, rolledBy, opts) { opts = {...opts}; if (wrpTree.meta && wrpTree.meta.hasPb) { const userPb = await InputUiUtil.pGetUserNumber({ min: 0, int: true, title: "Enter Proficiency Bonus", default: 2, storageKey_default: "dice.playerProficiencyBonus", isGlobal_default: true, }); if (userPb == null) return null; opts.pb = userPb; } if (wrpTree.meta && wrpTree.meta.hasSummonSpellLevel) { const predefinedSpellLevel = opts.additionalData?.summonedBySpellLevel != null && !isNaN(opts.additionalData?.summonedBySpellLevel) ? Number(opts.additionalData.summonedBySpellLevel) : null; const userSummonSpellLevel = await InputUiUtil.pGetUserNumber({ min: predefinedSpellLevel ?? 0, int: true, title: "Enter Spell Level", default: predefinedSpellLevel ?? 1, }); if (userSummonSpellLevel == null) return null; opts.summonSpellLevel = userSummonSpellLevel; } if (wrpTree.meta && wrpTree.meta.hasSummonClassLevel) { const predefinedClassLevel = opts.additionalData?.summonedByClassLevel != null && !isNaN(opts.additionalData?.summonedByClassLevel) ? Number(opts.additionalData.summonedByClassLevel) : null; const userSummonClassLevel = await InputUiUtil.pGetUserNumber({ min: predefinedClassLevel ?? 0, int: true, title: "Enter Class Level", default: predefinedClassLevel ?? 1, }); if (userSummonClassLevel == null) return null; opts.summonClassLevel = userSummonClassLevel; } if (Renderer.dice._isManualMode) return Renderer.dice._pHandleRoll2_manual(wrpTree.tree, rolledBy, opts); else return Renderer.dice._pHandleRoll2_automatic(wrpTree.tree, rolledBy, opts); }, /** * @param tree * @param rolledBy * @param [opts] Options object. * @param [opts.fnGetMessage] * @param [opts.pb] User-entered proficiency bonus, to be propagated to the meta. * @param [opts.summonSpellLevel] User-entered summon spell level, to be propagated to the meta. * @param [opts.summonClassLevel] User-entered summon class level, to be propagated to the meta. * @param [opts.target] Generic target number (e.g. save DC, AC) to meet/beat. * @param [opts.isHidden] If the result should not be posted to the rollbox. */ _pHandleRoll2_automatic (tree, rolledBy, opts) { opts = opts || {}; if (!opts.isHidden) Renderer.dice._showBox(); Renderer.dice._checkHandleName(rolledBy.name); const $out = Renderer.dice._$lastRolledBy; if (tree) { const meta = {}; if (opts.pb) meta.pb = opts.pb; if (opts.summonSpellLevel) meta.summonSpellLevel = opts.summonSpellLevel; if (opts.summonClassLevel) meta.summonClassLevel = opts.summonClassLevel; const result = tree.evl(meta); const fullHtml = (meta.html || []).join(""); const allMax = meta.allMax && meta.allMax.length && !(meta.allMax.filter(it => !it).length); const allMin = meta.allMin && meta.allMin.length && !(meta.allMin.filter(it => !it).length); const lbl = rolledBy.label && (!rolledBy.name || rolledBy.label.trim().toLowerCase() !== rolledBy.name.trim().toLowerCase()) ? rolledBy.label : null; const ptTarget = opts.target != null ? result >= opts.target ? ` ≥${opts.target}` : ` <${opts.target}` : ""; const isThreshSuccess = tree.successThresh != null && result > (tree.successMax || 100) - tree.successThresh; const isColorSuccess = tree.isColorSuccessFail || !tree.chanceSuccessText; const isColorFail = tree.isColorSuccessFail || !tree.chanceFailureText; const totalPart = tree.successThresh != null ? `${isThreshSuccess ? Renderer.get().render(tree.chanceSuccessText || "Success!") : Renderer.get().render(tree.chanceFailureText || "Failure")}` : `${result}`; const title = `${rolledBy.name ? `${rolledBy.name} \u2014 ` : ""}${lbl ? `${lbl}: ` : ""}${tree}`; const message = opts.fnGetMessage ? opts.fnGetMessage(result) : null; ExtensionUtil.doSendRoll({ dice: tree.toString(), result, rolledBy: rolledBy.name, label: [lbl, message].filter(Boolean).join(" \u2013 "), }); if (!opts.isHidden) { $out.append(`
${lbl ? `${lbl}: ` : ""} ${totalPart} ${ptTarget} ${fullHtml} ${message ? `${message}` : ""}
`); Renderer.dice._scrollBottom(); } return result; } else { if (!opts.isHidden) { $out.append(`
Invalid input! Try "/help"
`); Renderer.dice._scrollBottom(); } return null; } }, _pHandleRoll2_manual (tree, rolledBy, opts) { opts = opts || {}; if (!tree) return JqueryUtil.doToast({type: "danger", content: `Invalid roll input!`}); const title = (rolledBy.label || "").toTitleCase() || "Roll Dice"; const $dispDice = $(`
${tree.toString()}
`); if (opts.isResultUsed) { return InputUiUtil.pGetUserNumber({ title, $elePre: $dispDice, }); } else { const {$modalInner} = UiUtil.getShowModal({ title, isMinHeight0: true, }); $dispDice.appendTo($modalInner); return null; } }, _showMessage (message, rolledBy) { Renderer.dice._showBox(); Renderer.dice._checkHandleName(rolledBy.name); const $out = Renderer.dice._$lastRolledBy; $out.append(`
${message}
`); Renderer.dice._scrollBottom(); }, _showInvalid () { Renderer.dice._showMessage("Invalid input! Try "/help"", Renderer.dice.SYSTEM_USER); }, _validCommands: new Set(["/c", "/cls", "/clear", "/iterroll"]), async _pHandleCommand (com, rolledBy) { Renderer.dice._showMessage(`${com}`, rolledBy); // parrot the user's command back to them const comParsed = Renderer.dice._getParsedCommand(com); const [comOp] = comParsed; if (comOp === "/help" || comOp === "/h") { Renderer.dice._showMessage( ` Up and down arrow keys cycle input history.
Anything before a colon is treated as a label (Fireball: 8d6)
Use /macro list to list saved macros.
Use /macro add myName 1d2+3 to add (or update) a macro. Macro names should not contain spaces or hashes.
Use /macro remove myName to remove a macro.
Use #myName to roll a macro.
Use /iterroll roll count [target] to roll multiple times, optionally against a target. Use /clear to clear the roller.`, Renderer.dice.SYSTEM_USER, ); return; } if (comOp === "/macro") { const [, mode, ...others] = comParsed; if (!["list", "add", "remove", "clear"].includes(mode)) Renderer.dice._showInvalid(); else { switch (mode) { case "list": if (!others.length) { Object.keys(Renderer.dice.storage).forEach(name => { Renderer.dice._showMessage(`#${name} \u2014 ${Renderer.dice.storage[name]}`, Renderer.dice.SYSTEM_USER); }); } else { Renderer.dice._showInvalid(); } break; case "add": { if (others.length === 2) { const [name, macro] = others; if (name.includes(" ") || name.includes("#")) Renderer.dice._showInvalid(); else { Renderer.dice.storage[name] = macro; await Renderer.dice._pSaveMacros(); Renderer.dice._showMessage(`Saved macro #${name}`, Renderer.dice.SYSTEM_USER); } } else { Renderer.dice._showInvalid(); } break; } case "remove": if (others.length === 1) { if (Renderer.dice.storage[others[0]]) { delete Renderer.dice.storage[others[0]]; await Renderer.dice._pSaveMacros(); Renderer.dice._showMessage(`Removed macro #${others[0]}`, Renderer.dice.SYSTEM_USER); } else { Renderer.dice._showMessage(`Macro #${others[0]} not found`, Renderer.dice.SYSTEM_USER); } } else { Renderer.dice._showInvalid(); } break; } } return; } if (Renderer.dice._validCommands.has(comOp)) { switch (comOp) { case "/c": case "/cls": case "/clear": Renderer.dice._$outRoll.empty(); Renderer.dice._$lastRolledBy.empty(); Renderer.dice._$lastRolledBy = null; return; case "/iterroll": { let [, exp, count, target] = comParsed; if (!exp) return Renderer.dice._showInvalid(); const wrpTree = Renderer.dice.lang.getTree3(exp); if (!wrpTree) return Renderer.dice._showInvalid(); count = count && !isNaN(count) ? Number(count) : 1; target = target && !isNaN(target) ? Number(target) : undefined; for (let i = 0; i < count; ++i) { await Renderer.dice.pRoll2( exp, { name: "Anon", }, { target, }, ); } } } return; } Renderer.dice._showInvalid(); }, async _pSaveMacros () { await StorageUtil.pSet(VeCt.STORAGE_ROLLER_MACRO, Renderer.dice.storage); }, _getParsedCommand (str) { // TODO(Future) this is probably too naive return str.split(/\s+/); }, _pHandleSavedRoll (id, rolledBy, opts) { id = id.replace(/^#/, ""); const macro = Renderer.dice.storage[id]; if (macro) { rolledBy.label = id; const wrpTree = Renderer.dice.lang.getTree3(macro); return Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); } else Renderer.dice._showMessage(`Macro #${id} not found`, Renderer.dice.SYSTEM_USER); }, addRoll ({rolledBy, html, $ele}) { if (html && $ele) throw new Error(`Must specify one of html or $ele!`); if (html != null && !html.trim()) return; Renderer.dice._showBox(); Renderer.dice._checkHandleName(rolledBy.name); if (html) { Renderer.dice._$lastRolledBy.append(`
${html}
`); } else { $$`
${$ele}
` .appendTo(Renderer.dice._$lastRolledBy); } Renderer.dice._scrollBottom(); }, addElement ({rolledBy, html, $ele}) { if (html && $ele) throw new Error(`Must specify one of html or $ele!`); if (html != null && !html.trim()) return; Renderer.dice._showBox(); Renderer.dice._checkHandleName(rolledBy.name); if (html) { Renderer.dice._$lastRolledBy.append(`
${html}
`); } else { $$`
${$ele}
` .appendTo(Renderer.dice._$lastRolledBy); } Renderer.dice._scrollBottom(); }, _checkHandleName (name) { if (!Renderer.dice._$lastRolledBy || Renderer.dice._$lastRolledBy.data("name") !== name) { Renderer.dice._$outRoll.prepend(`
${name}
`); Renderer.dice._$lastRolledBy = $(`
`).data("name", name); Renderer.dice._$outRoll.prepend(Renderer.dice._$lastRolledBy); } }, }; Renderer.dice.util = { getReducedMeta (meta) { return {pb: meta.pb}; }, }; Renderer.dice.lang = { // region Public API validate3 (str) { str = str.trim(); // region Lexing let lexed; try { lexed = Renderer.dice.lang._lex3(str).lexed; } catch (e) { return e.message; } // endregion // region Parsing try { Renderer.dice.lang._parse3(lexed); } catch (e) { return e.message; } // endregion return null; }, getTree3 (str, isSilent = true) { str = str.trim(); if (isSilent) { try { const {lexed, lexedMeta} = Renderer.dice.lang._lex3(str); return {tree: Renderer.dice.lang._parse3(lexed), meta: lexedMeta}; } catch (e) { return null; } } else { const {lexed, lexedMeta} = Renderer.dice.lang._lex3(str); return {tree: Renderer.dice.lang._parse3(lexed), meta: lexedMeta}; } }, // endregion // region Lexer _M_NUMBER_CHAR: /[0-9.]/, _M_SYMBOL_CHAR: /[-+/*^=>) without opening (`); this._lex3_outputToken(self); self.token = ")"; this._lex3_outputToken(self); break; case "{": self.braceCount++; this._lex3_outputToken(self); self.token = "{"; this._lex3_outputToken(self); break; case "}": self.braceCount--; if (self.parenCount < 0) throw new Error(`Syntax error: closing } without opening (`); this._lex3_outputToken(self); self.token = "}"; this._lex3_outputToken(self); break; // single-character operators case "+": case "-": case "*": case "/": case "^": case ",": this._lex3_outputToken(self); self.token += c; this._lex3_outputToken(self); break; default: { if (Renderer.dice.lang._M_NUMBER_CHAR.test(c)) { if (self.mode === "symbol") this._lex3_outputToken(self); self.token += c; self.mode = "text"; } else if (Renderer.dice.lang._M_SYMBOL_CHAR.test(c)) { if (self.mode === "text") this._lex3_outputToken(self); self.token += c; self.mode = "symbol"; } else throw new Error(`Syntax error: unexpected character ${c}`); break; } } } // empty the stack of any remaining content this._lex3_outputToken(self); }, _lex3_outputToken (self) { if (!self.token) return; switch (self.token) { case "(": self.tokenStack.push(Renderer.dice.tk.PAREN_OPEN); break; case ")": self.tokenStack.push(Renderer.dice.tk.PAREN_CLOSE); break; case "{": self.tokenStack.push(Renderer.dice.tk.BRACE_OPEN); break; case "}": self.tokenStack.push(Renderer.dice.tk.BRACE_CLOSE); break; case ",": self.tokenStack.push(Renderer.dice.tk.COMMA); break; case "+": self.tokenStack.push(Renderer.dice.tk.ADD); break; case "-": self.tokenStack.push(Renderer.dice.tk.SUB); break; case "*": self.tokenStack.push(Renderer.dice.tk.MULT); break; case "/": self.tokenStack.push(Renderer.dice.tk.DIV); break; case "^": self.tokenStack.push(Renderer.dice.tk.POW); break; case "pb": self.tokenStack.push(Renderer.dice.tk.PB); self.hasPb = true; break; case "summonspelllevel": self.tokenStack.push(Renderer.dice.tk.SUMMON_SPELL_LEVEL); self.hasSummonSpellLevel = true; break; case "summonclasslevel": self.tokenStack.push(Renderer.dice.tk.SUMMON_CLASS_LEVEL); self.hasSummonClassLevel = true; break; case "floor": self.tokenStack.push(Renderer.dice.tk.FLOOR); break; case "ceil": self.tokenStack.push(Renderer.dice.tk.CEIL); break; case "round": self.tokenStack.push(Renderer.dice.tk.ROUND); break; case "avg": self.tokenStack.push(Renderer.dice.tk.AVERAGE); break; case "dmax": self.tokenStack.push(Renderer.dice.tk.DMAX); break; case "dmin": self.tokenStack.push(Renderer.dice.tk.DMIN); break; case "sign": self.tokenStack.push(Renderer.dice.tk.SIGN); break; case "abs": self.tokenStack.push(Renderer.dice.tk.ABS); break; case "cbrt": self.tokenStack.push(Renderer.dice.tk.CBRT); break; case "sqrt": self.tokenStack.push(Renderer.dice.tk.SQRT); break; case "exp": self.tokenStack.push(Renderer.dice.tk.EXP); break; case "log": self.tokenStack.push(Renderer.dice.tk.LOG); break; case "random": self.tokenStack.push(Renderer.dice.tk.RANDOM); break; case "trunc": self.tokenStack.push(Renderer.dice.tk.TRUNC); break; case "pow": self.tokenStack.push(Renderer.dice.tk.POW); break; case "max": self.tokenStack.push(Renderer.dice.tk.MAX); break; case "min": self.tokenStack.push(Renderer.dice.tk.MIN); break; case "d": self.tokenStack.push(Renderer.dice.tk.DICE); break; case "dh": self.tokenStack.push(Renderer.dice.tk.DROP_HIGHEST); break; case "kh": self.tokenStack.push(Renderer.dice.tk.KEEP_HIGHEST); break; case "dl": self.tokenStack.push(Renderer.dice.tk.DROP_LOWEST); break; case "kl": self.tokenStack.push(Renderer.dice.tk.KEEP_LOWEST); break; case "r": self.tokenStack.push(Renderer.dice.tk.REROLL_EXACT); break; case "r>": self.tokenStack.push(Renderer.dice.tk.REROLL_GT); break; case "r>=": self.tokenStack.push(Renderer.dice.tk.REROLL_GTEQ); break; case "r<": self.tokenStack.push(Renderer.dice.tk.REROLL_LT); break; case "r<=": self.tokenStack.push(Renderer.dice.tk.REROLL_LTEQ); break; case "x": self.tokenStack.push(Renderer.dice.tk.EXPLODE_EXACT); break; case "x>": self.tokenStack.push(Renderer.dice.tk.EXPLODE_GT); break; case "x>=": self.tokenStack.push(Renderer.dice.tk.EXPLODE_GTEQ); break; case "x<": self.tokenStack.push(Renderer.dice.tk.EXPLODE_LT); break; case "x<=": self.tokenStack.push(Renderer.dice.tk.EXPLODE_LTEQ); break; case "cs=": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_EXACT); break; case "cs>": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_GT); break; case "cs>=": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_GTEQ); break; case "cs<": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_LT); break; case "cs<=": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_LTEQ); break; case "ms=": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_EXACT); break; case "ms>": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_GT); break; case "ms>=": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_GTEQ); break; case "ms<": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_LT); break; case "ms<=": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_LTEQ); break; default: { if (Renderer.dice.lang._M_NUMBER.test(self.token)) { if (self.token.split(Parser._decimalSeparator).length > 2) throw new Error(`Syntax error: too many decimal separators ${self.token}`); self.tokenStack.push(Renderer.dice.tk.NUMBER(self.token)); } else throw new Error(`Syntax error: unexpected token ${self.token}`); } } self.token = ""; }, // endregion // region Parser _parse3 (lexed) { const self = { ixSym: -1, syms: lexed, sym: null, lastAccepted: null, // Workaround for comma-separated numbers--if we're e.g. inside a dice pool, treat the commas as dice pool // separators. Otherwise, merge together adjacent numbers, to convert e.g. "1,000,000" to "1000000". isIgnoreCommas: true, }; this._parse3_nextSym(self); return this._parse3_expression(self); }, _parse3_nextSym (self) { const cur = self.syms[self.ixSym]; self.ixSym++; self.sym = self.syms[self.ixSym]; return cur; }, _parse3_match (self, symbol) { if (self.sym == null) return false; if (symbol.type) symbol = symbol.type; // If it's a typed token, convert it to its underlying type return self.sym.type === symbol; }, _parse3_accept (self, symbol) { if (this._parse3_match(self, symbol)) { const out = self.sym; this._parse3_nextSym(self); self.lastAccepted = out; return out; } return false; }, _parse3_expect (self, symbol) { const accepted = this._parse3_accept(self, symbol); if (accepted) return accepted; if (self.sym) throw new Error(`Unexpected input: Expected ${symbol} but found ${self.sym}`); else throw new Error(`Unexpected end of input: Expected ${symbol}`); }, _parse3_factor (self, {isSilent = false} = {}) { if (this._parse3_accept(self, Renderer.dice.tk.TYP_NUMBER)) { // Workaround for comma-separated numbers if (self.isIgnoreCommas) { // Combine comma-separated parts const syms = [self.lastAccepted]; while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) { const sym = this._parse3_expect(self, Renderer.dice.tk.TYP_NUMBER); syms.push(sym); } const sym = Renderer.dice.tk.NUMBER(syms.map(it => it.value).join("")); return new Renderer.dice.parsed.Factor(sym); } return new Renderer.dice.parsed.Factor(self.lastAccepted); } else if (this._parse3_accept(self, Renderer.dice.tk.PB)) { return new Renderer.dice.parsed.Factor(Renderer.dice.tk.PB); } else if (this._parse3_accept(self, Renderer.dice.tk.SUMMON_SPELL_LEVEL)) { return new Renderer.dice.parsed.Factor(Renderer.dice.tk.SUMMON_SPELL_LEVEL); } else if (this._parse3_accept(self, Renderer.dice.tk.SUMMON_CLASS_LEVEL)) { return new Renderer.dice.parsed.Factor(Renderer.dice.tk.SUMMON_CLASS_LEVEL); } else if ( // Single-arg functions this._parse3_match(self, Renderer.dice.tk.FLOOR) || this._parse3_match(self, Renderer.dice.tk.CEIL) || this._parse3_match(self, Renderer.dice.tk.ROUND) || this._parse3_match(self, Renderer.dice.tk.AVERAGE) || this._parse3_match(self, Renderer.dice.tk.DMAX) || this._parse3_match(self, Renderer.dice.tk.DMIN) || this._parse3_match(self, Renderer.dice.tk.SIGN) || this._parse3_match(self, Renderer.dice.tk.ABS) || this._parse3_match(self, Renderer.dice.tk.CBRT) || this._parse3_match(self, Renderer.dice.tk.SQRT) || this._parse3_match(self, Renderer.dice.tk.EXP) || this._parse3_match(self, Renderer.dice.tk.LOG) || this._parse3_match(self, Renderer.dice.tk.RANDOM) || this._parse3_match(self, Renderer.dice.tk.TRUNC) ) { const children = []; children.push(this._parse3_nextSym(self)); this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); children.push(this._parse3_expression(self)); this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); return new Renderer.dice.parsed.Function(children); } else if ( // 2-arg functions this._parse3_match(self, Renderer.dice.tk.POW) ) { self.isIgnoreCommas = false; const children = []; children.push(this._parse3_nextSym(self)); this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); children.push(this._parse3_expression(self)); this._parse3_expect(self, Renderer.dice.tk.COMMA); children.push(this._parse3_expression(self)); this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); self.isIgnoreCommas = true; return new Renderer.dice.parsed.Function(children); } else if ( // N-arg functions this._parse3_match(self, Renderer.dice.tk.MAX) || this._parse3_match(self, Renderer.dice.tk.MIN) ) { self.isIgnoreCommas = false; const children = []; children.push(this._parse3_nextSym(self)); this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); children.push(this._parse3_expression(self)); while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) children.push(this._parse3_expression(self)); this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); self.isIgnoreCommas = true; return new Renderer.dice.parsed.Function(children); } else if (this._parse3_accept(self, Renderer.dice.tk.PAREN_OPEN)) { const exp = this._parse3_expression(self); this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); return new Renderer.dice.parsed.Factor(exp, {hasParens: true}); } else if (this._parse3_accept(self, Renderer.dice.tk.BRACE_OPEN)) { self.isIgnoreCommas = false; const children = []; children.push(this._parse3_expression(self)); while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) children.push(this._parse3_expression(self)); this._parse3_expect(self, Renderer.dice.tk.BRACE_CLOSE); self.isIgnoreCommas = true; const modPart = []; this._parse3__dice_modifiers(self, modPart); return new Renderer.dice.parsed.Pool(children, modPart[0]); } else { if (isSilent) return null; if (self.sym) throw new Error(`Unexpected input: ${self.sym}`); else throw new Error(`Unexpected end of input`); } }, _parse3_dice (self) { const children = []; // if we've omitted the X in XdY, add it here if (this._parse3_match(self, Renderer.dice.tk.DICE)) children.push(new Renderer.dice.parsed.Factor(Renderer.dice.tk.NUMBER(1))); else children.push(this._parse3_factor(self)); while (this._parse3_match(self, Renderer.dice.tk.DICE)) { this._parse3_nextSym(self); children.push(this._parse3_factor(self)); this._parse3__dice_modifiers(self, children); } return new Renderer.dice.parsed.Dice(children); }, _parse3__dice_modifiers (self, children) { // used in both dice and dice pools // Collect together all dice mods const modsMeta = new Renderer.dice.lang.DiceModMeta(); while ( this._parse3_match(self, Renderer.dice.tk.DROP_HIGHEST) || this._parse3_match(self, Renderer.dice.tk.KEEP_HIGHEST) || this._parse3_match(self, Renderer.dice.tk.DROP_LOWEST) || this._parse3_match(self, Renderer.dice.tk.KEEP_LOWEST) || this._parse3_match(self, Renderer.dice.tk.REROLL_EXACT) || this._parse3_match(self, Renderer.dice.tk.REROLL_GT) || this._parse3_match(self, Renderer.dice.tk.REROLL_GTEQ) || this._parse3_match(self, Renderer.dice.tk.REROLL_LT) || this._parse3_match(self, Renderer.dice.tk.REROLL_LTEQ) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_EXACT) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_GT) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_GTEQ) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_LT) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_LTEQ) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_EXACT) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_GT) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_GTEQ) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_LT) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_LTEQ) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_EXACT) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_GT) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_GTEQ) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_LT) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_LTEQ) ) { const nxtSym = this._parse3_nextSym(self); const nxtFactor = this._parse3__dice_modifiers_nxtFactor(self, nxtSym); if (nxtSym.isSuccessMode) modsMeta.isSuccessMode = true; modsMeta.mods.push({modSym: nxtSym, numSym: nxtFactor}); } if (modsMeta.mods.length) children.push(modsMeta); }, _parse3__dice_modifiers_nxtFactor (self, nxtSym) { if (nxtSym.diceModifierImplicit == null) return this._parse3_factor(self, {isSilent: true}); const fallback = new Renderer.dice.parsed.Factor(Renderer.dice.tk.NUMBER(nxtSym.diceModifierImplicit)); if (self.sym == null) return fallback; const out = this._parse3_factor(self, {isSilent: true}); if (out) return out; return fallback; }, _parse3_exponent (self) { const children = []; children.push(this._parse3_dice(self)); while (this._parse3_match(self, Renderer.dice.tk.POW)) { this._parse3_nextSym(self); children.push(this._parse3_dice(self)); } return new Renderer.dice.parsed.Exponent(children); }, _parse3_term (self) { const children = []; children.push(this._parse3_exponent(self)); while (this._parse3_match(self, Renderer.dice.tk.MULT) || this._parse3_match(self, Renderer.dice.tk.DIV)) { children.push(this._parse3_nextSym(self)); children.push(this._parse3_exponent(self)); } return new Renderer.dice.parsed.Term(children); }, _parse3_expression (self) { const children = []; if (this._parse3_match(self, Renderer.dice.tk.ADD) || this._parse3_match(self, Renderer.dice.tk.SUB)) children.push(this._parse3_nextSym(self)); children.push(this._parse3_term(self)); while (this._parse3_match(self, Renderer.dice.tk.ADD) || this._parse3_match(self, Renderer.dice.tk.SUB)) { children.push(this._parse3_nextSym(self)); children.push(this._parse3_term(self)); } return new Renderer.dice.parsed.Expression(children); }, // endregion // region Utilities DiceModMeta: class { constructor () { this.isDiceModifierGroup = true; this.isSuccessMode = false; this.mods = []; } }, // endregion }; Renderer.dice.tk = { Token: class { /** * @param type * @param value * @param asString * @param [opts] Options object. * @param [opts.isDiceModifier] If the token is a dice modifier, e.g. "dl" * @param [opts.diceModifierImplicit] If the dice modifier has an implicit value (e.g. "kh" is shorthand for "kh1") * @param [opts.isSuccessMode] If the token is a "success"-based dice modifier, e.g. "cs=" */ constructor (type, value, asString, opts) { opts = opts || {}; this.type = type; this.value = value; this._asString = asString; if (opts.isDiceModifier) this.isDiceModifier = true; if (opts.diceModifierImplicit) this.diceModifierImplicit = true; if (opts.isSuccessMode) this.isSuccessMode = true; } eq (other) { return other && other.type === this.type; } toString () { if (this._asString) return this._asString; return this.toDebugString(); } toDebugString () { return `${this.type}${this.value ? ` :: ${this.value}` : ""}`; } }, _new (type, asString, opts) { return new Renderer.dice.tk.Token(type, null, asString, opts); }, TYP_NUMBER: "NUMBER", TYP_DICE: "DICE", TYP_SYMBOL: "SYMBOL", // Cannot be created by lexing, only parsing NUMBER (val) { return new Renderer.dice.tk.Token(Renderer.dice.tk.TYP_NUMBER, val); }, }; Renderer.dice.tk.PAREN_OPEN = Renderer.dice.tk._new("PAREN_OPEN", "("); Renderer.dice.tk.PAREN_CLOSE = Renderer.dice.tk._new("PAREN_CLOSE", ")"); Renderer.dice.tk.BRACE_OPEN = Renderer.dice.tk._new("BRACE_OPEN", "{"); Renderer.dice.tk.BRACE_CLOSE = Renderer.dice.tk._new("BRACE_CLOSE", "}"); Renderer.dice.tk.COMMA = Renderer.dice.tk._new("COMMA", ","); Renderer.dice.tk.ADD = Renderer.dice.tk._new("ADD", "+"); Renderer.dice.tk.SUB = Renderer.dice.tk._new("SUB", "-"); Renderer.dice.tk.MULT = Renderer.dice.tk._new("MULT", "*"); Renderer.dice.tk.DIV = Renderer.dice.tk._new("DIV", "/"); Renderer.dice.tk.POW = Renderer.dice.tk._new("POW", "^"); Renderer.dice.tk.PB = Renderer.dice.tk._new("PB", "pb"); Renderer.dice.tk.SUMMON_SPELL_LEVEL = Renderer.dice.tk._new("SUMMON_SPELL_LEVEL", "summonspelllevel"); Renderer.dice.tk.SUMMON_CLASS_LEVEL = Renderer.dice.tk._new("SUMMON_CLASS_LEVEL", "summonclasslevel"); Renderer.dice.tk.FLOOR = Renderer.dice.tk._new("FLOOR", "floor"); Renderer.dice.tk.CEIL = Renderer.dice.tk._new("CEIL", "ceil"); Renderer.dice.tk.ROUND = Renderer.dice.tk._new("ROUND", "round"); Renderer.dice.tk.AVERAGE = Renderer.dice.tk._new("AVERAGE", "avg"); Renderer.dice.tk.DMAX = Renderer.dice.tk._new("DMAX", "avg"); Renderer.dice.tk.DMIN = Renderer.dice.tk._new("DMIN", "avg"); Renderer.dice.tk.SIGN = Renderer.dice.tk._new("SIGN", "sign"); Renderer.dice.tk.ABS = Renderer.dice.tk._new("ABS", "abs"); Renderer.dice.tk.CBRT = Renderer.dice.tk._new("CBRT", "cbrt"); Renderer.dice.tk.SQRT = Renderer.dice.tk._new("SQRT", "sqrt"); Renderer.dice.tk.EXP = Renderer.dice.tk._new("EXP", "exp"); Renderer.dice.tk.LOG = Renderer.dice.tk._new("LOG", "log"); Renderer.dice.tk.RANDOM = Renderer.dice.tk._new("RANDOM", "random"); Renderer.dice.tk.TRUNC = Renderer.dice.tk._new("TRUNC", "trunc"); Renderer.dice.tk.POW = Renderer.dice.tk._new("POW", "pow"); Renderer.dice.tk.MAX = Renderer.dice.tk._new("MAX", "max"); Renderer.dice.tk.MIN = Renderer.dice.tk._new("MIN", "min"); Renderer.dice.tk.DICE = Renderer.dice.tk._new("DICE", "d"); Renderer.dice.tk.DROP_HIGHEST = Renderer.dice.tk._new("DH", "dh", {isDiceModifier: true, diceModifierImplicit: 1}); Renderer.dice.tk.KEEP_HIGHEST = Renderer.dice.tk._new("KH", "kh", {isDiceModifier: true, diceModifierImplicit: 1}); Renderer.dice.tk.DROP_LOWEST = Renderer.dice.tk._new("DL", "dl", {isDiceModifier: true, diceModifierImplicit: 1}); Renderer.dice.tk.KEEP_LOWEST = Renderer.dice.tk._new("KL", "kl", {isDiceModifier: true, diceModifierImplicit: 1}); Renderer.dice.tk.REROLL_EXACT = Renderer.dice.tk._new("REROLL", "r", {isDiceModifier: true}); Renderer.dice.tk.REROLL_GT = Renderer.dice.tk._new("REROLL_GT", "r>", {isDiceModifier: true}); Renderer.dice.tk.REROLL_GTEQ = Renderer.dice.tk._new("REROLL_GTEQ", "r>=", {isDiceModifier: true}); Renderer.dice.tk.REROLL_LT = Renderer.dice.tk._new("REROLL_LT", "r<", {isDiceModifier: true}); Renderer.dice.tk.REROLL_LTEQ = Renderer.dice.tk._new("REROLL_LTEQ", "r<=", {isDiceModifier: true}); Renderer.dice.tk.EXPLODE_EXACT = Renderer.dice.tk._new("EXPLODE", "x", {isDiceModifier: true}); Renderer.dice.tk.EXPLODE_GT = Renderer.dice.tk._new("EXPLODE_GT", "x>", {isDiceModifier: true}); Renderer.dice.tk.EXPLODE_GTEQ = Renderer.dice.tk._new("EXPLODE_GTEQ", "x>=", {isDiceModifier: true}); Renderer.dice.tk.EXPLODE_LT = Renderer.dice.tk._new("EXPLODE_LT", "x<", {isDiceModifier: true}); Renderer.dice.tk.EXPLODE_LTEQ = Renderer.dice.tk._new("EXPLODE_LTEQ", "x<=", {isDiceModifier: true}); Renderer.dice.tk.COUNT_SUCCESS_EXACT = Renderer.dice.tk._new("COUNT_SUCCESS_EXACT", "cs=", {isDiceModifier: true, isSuccessMode: true}); Renderer.dice.tk.COUNT_SUCCESS_GT = Renderer.dice.tk._new("COUNT_SUCCESS_GT", "cs>", {isDiceModifier: true, isSuccessMode: true}); Renderer.dice.tk.COUNT_SUCCESS_GTEQ = Renderer.dice.tk._new("COUNT_SUCCESS_GTEQ", "cs>=", {isDiceModifier: true, isSuccessMode: true}); Renderer.dice.tk.COUNT_SUCCESS_LT = Renderer.dice.tk._new("COUNT_SUCCESS_LT", "cs<", {isDiceModifier: true, isSuccessMode: true}); Renderer.dice.tk.COUNT_SUCCESS_LTEQ = Renderer.dice.tk._new("COUNT_SUCCESS_LTEQ", "cs<=", {isDiceModifier: true, isSuccessMode: true}); Renderer.dice.tk.MARGIN_SUCCESS_EXACT = Renderer.dice.tk._new("MARGIN_SUCCESS_EXACT", "ms=", {isDiceModifier: true}); Renderer.dice.tk.MARGIN_SUCCESS_GT = Renderer.dice.tk._new("MARGIN_SUCCESS_GT", "ms>", {isDiceModifier: true}); Renderer.dice.tk.MARGIN_SUCCESS_GTEQ = Renderer.dice.tk._new("MARGIN_SUCCESS_GTEQ", "ms>=", {isDiceModifier: true}); Renderer.dice.tk.MARGIN_SUCCESS_LT = Renderer.dice.tk._new("MARGIN_SUCCESS_LT", "ms<", {isDiceModifier: true}); Renderer.dice.tk.MARGIN_SUCCESS_LTEQ = Renderer.dice.tk._new("MARGIN_SUCCESS_LTEQ", "ms<=", {isDiceModifier: true}); Renderer.dice.AbstractSymbol = class { constructor () { this.type = Renderer.dice.tk.TYP_SYMBOL; } eq (symbol) { return symbol && this.type === symbol.type; } evl (meta) { this.meta = meta; return this._evl(meta); } avg (meta) { this.meta = meta; return this._avg(meta); } min (meta) { this.meta = meta; return this._min(meta); } // minimum value of all _rolls_, not the minimum possible result max (meta) { this.meta = meta; return this._max(meta); } // maximum value of all _rolls_, not the maximum possible result _evl () { throw new Error("Unimplemented!"); } _avg () { throw new Error("Unimplemented!"); } _min () { throw new Error("Unimplemented!"); } // minimum value of all _rolls_, not the minimum possible result _max () { throw new Error("Unimplemented!"); } // maximum value of all _rolls_, not the maximum possible result toString () { throw new Error("Unimplemented!"); } addToMeta (meta, {text, html = null, md = null} = {}) { if (!meta) return; html = html || text; md = md || text; (meta.html = meta.html || []).push(html); (meta.text = meta.text || []).push(text); (meta.md = meta.md || []).push(md); } }; Renderer.dice.parsed = { _PARTITION_EQ: (r, compareTo) => r === compareTo, _PARTITION_GT: (r, compareTo) => r > compareTo, _PARTITION_GTEQ: (r, compareTo) => r >= compareTo, _PARTITION_LT: (r, compareTo) => r < compareTo, _PARTITION_LTEQ: (r, compareTo) => r <= compareTo, /** * @param fnName * @param meta * @param vals * @param nodeMod * @param opts Options object. * @param [opts.fnGetRerolls] Function which takes a set of rolls to be rerolled and generates the next set of rolls. * @param [opts.fnGetExplosions] Function which takes a set of rolls to be exploded and generates the next set of rolls. * @param [opts.faces] */ _handleModifiers (fnName, meta, vals, nodeMod, opts) { opts = opts || {}; const displayVals = vals.slice(); // copy the array so we can sort the original const {mods} = nodeMod; for (const mod of mods) { vals.sort(SortUtil.ascSortProp.bind(null, "val")).reverse(); const valsAlive = vals.filter(it => !it.isDropped); const modNum = mod.numSym[fnName](); switch (mod.modSym.type) { case Renderer.dice.tk.DROP_HIGHEST.type: case Renderer.dice.tk.KEEP_HIGHEST.type: case Renderer.dice.tk.DROP_LOWEST.type: case Renderer.dice.tk.KEEP_LOWEST.type: { const isHighest = mod.modSym.type.endsWith("H"); const splitPoint = isHighest ? modNum : valsAlive.length - modNum; const highSlice = valsAlive.slice(0, splitPoint); const lowSlice = valsAlive.slice(splitPoint, valsAlive.length); switch (mod.modSym.type) { case Renderer.dice.tk.DROP_HIGHEST.type: case Renderer.dice.tk.KEEP_LOWEST.type: highSlice.forEach(val => val.isDropped = true); break; case Renderer.dice.tk.KEEP_HIGHEST.type: case Renderer.dice.tk.DROP_LOWEST.type: lowSlice.forEach(val => val.isDropped = true); break; default: throw new Error(`Unimplemented!`); } break; } case Renderer.dice.tk.REROLL_EXACT.type: case Renderer.dice.tk.REROLL_GT.type: case Renderer.dice.tk.REROLL_GTEQ.type: case Renderer.dice.tk.REROLL_LT.type: case Renderer.dice.tk.REROLL_LTEQ.type: { let fnPartition; switch (mod.modSym.type) { case Renderer.dice.tk.REROLL_EXACT.type: fnPartition = Renderer.dice.parsed._PARTITION_EQ; break; case Renderer.dice.tk.REROLL_GT.type: fnPartition = Renderer.dice.parsed._PARTITION_GT; break; case Renderer.dice.tk.REROLL_GTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; break; case Renderer.dice.tk.REROLL_LT.type: fnPartition = Renderer.dice.parsed._PARTITION_LT; break; case Renderer.dice.tk.REROLL_LTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; break; default: throw new Error(`Unimplemented!`); } const toReroll = valsAlive.filter(val => fnPartition(val.val, modNum)); toReroll.forEach(val => val.isDropped = true); const nuVals = opts.fnGetRerolls(toReroll); vals.push(...nuVals); displayVals.push(...nuVals); break; } case Renderer.dice.tk.EXPLODE_EXACT.type: case Renderer.dice.tk.EXPLODE_GT.type: case Renderer.dice.tk.EXPLODE_GTEQ.type: case Renderer.dice.tk.EXPLODE_LT.type: case Renderer.dice.tk.EXPLODE_LTEQ.type: { let fnPartition; switch (mod.modSym.type) { case Renderer.dice.tk.EXPLODE_EXACT.type: fnPartition = Renderer.dice.parsed._PARTITION_EQ; break; case Renderer.dice.tk.EXPLODE_GT.type: fnPartition = Renderer.dice.parsed._PARTITION_GT; break; case Renderer.dice.tk.EXPLODE_GTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; break; case Renderer.dice.tk.EXPLODE_LT.type: fnPartition = Renderer.dice.parsed._PARTITION_LT; break; case Renderer.dice.tk.EXPLODE_LTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; break; default: throw new Error(`Unimplemented!`); } let tries = 999; // limit the maximum explosions to a sane amount let lastLen; let toExplodeNext = valsAlive; do { lastLen = vals.length; const [toExplode] = toExplodeNext.partition(roll => !roll.isExploded && fnPartition(roll.val, modNum)); toExplode.forEach(roll => roll.isExploded = true); const nuVals = opts.fnGetExplosions(toExplode); // cache the new rolls, to improve performance over massive explosion sets toExplodeNext = nuVals; vals.push(...nuVals); displayVals.push(...nuVals); } while (tries-- > 0 && vals.length !== lastLen); if (!~tries) JqueryUtil.doToast({type: "warning", content: `Stopped exploding after 999 additional rolls.`}); break; } case Renderer.dice.tk.COUNT_SUCCESS_EXACT.type: case Renderer.dice.tk.COUNT_SUCCESS_GT.type: case Renderer.dice.tk.COUNT_SUCCESS_GTEQ.type: case Renderer.dice.tk.COUNT_SUCCESS_LT.type: case Renderer.dice.tk.COUNT_SUCCESS_LTEQ.type: { let fnPartition; switch (mod.modSym.type) { case Renderer.dice.tk.COUNT_SUCCESS_EXACT.type: fnPartition = Renderer.dice.parsed._PARTITION_EQ; break; case Renderer.dice.tk.COUNT_SUCCESS_GT.type: fnPartition = Renderer.dice.parsed._PARTITION_GT; break; case Renderer.dice.tk.COUNT_SUCCESS_GTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; break; case Renderer.dice.tk.COUNT_SUCCESS_LT.type: fnPartition = Renderer.dice.parsed._PARTITION_LT; break; case Renderer.dice.tk.COUNT_SUCCESS_LTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; break; default: throw new Error(`Unimplemented!`); } const successes = valsAlive.filter(val => fnPartition(val.val, modNum)); successes.forEach(val => val.isSuccess = true); break; } case Renderer.dice.tk.MARGIN_SUCCESS_EXACT.type: case Renderer.dice.tk.MARGIN_SUCCESS_GT.type: case Renderer.dice.tk.MARGIN_SUCCESS_GTEQ.type: case Renderer.dice.tk.MARGIN_SUCCESS_LT.type: case Renderer.dice.tk.MARGIN_SUCCESS_LTEQ.type: { const total = valsAlive.map(it => it.val).reduce((valA, valB) => valA + valB, 0); const subDisplayDice = displayVals.map(r => `[${Renderer.dice.parsed._rollToNumPart_html(r, opts.faces)}]`).join("+"); let delta; let subDisplay; switch (mod.modSym.type) { case Renderer.dice.tk.MARGIN_SUCCESS_EXACT.type: case Renderer.dice.tk.MARGIN_SUCCESS_GT.type: case Renderer.dice.tk.MARGIN_SUCCESS_GTEQ.type: { delta = total - modNum; subDisplay = `(${subDisplayDice})-${modNum}`; break; } case Renderer.dice.tk.MARGIN_SUCCESS_LT.type: case Renderer.dice.tk.MARGIN_SUCCESS_LTEQ.type: { delta = modNum - total; subDisplay = `${modNum}-(${subDisplayDice})`; break; } default: throw new Error(`Unimplemented!`); } while (vals.length) { vals.pop(); displayVals.pop(); } vals.push({val: delta}); displayVals.push({val: delta, htmlDisplay: subDisplay}); break; } default: throw new Error(`Unimplemented!`); } } return displayVals; }, _rollToNumPart_html (r, faces) { if (faces == null) return r.val; return r.val === faces ? `${r.val}` : r.val === 1 ? `${r.val}` : r.val; }, Function: class extends Renderer.dice.AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { const [symFunc] = this._nodes; switch (symFunc.type) { case Renderer.dice.tk.FLOOR.type: case Renderer.dice.tk.CEIL.type: case Renderer.dice.tk.ROUND.type: case Renderer.dice.tk.SIGN.type: case Renderer.dice.tk.CBRT.type: case Renderer.dice.tk.SQRT.type: case Renderer.dice.tk.EXP.type: case Renderer.dice.tk.LOG.type: case Renderer.dice.tk.RANDOM.type: case Renderer.dice.tk.TRUNC.type: case Renderer.dice.tk.POW.type: case Renderer.dice.tk.MAX.type: case Renderer.dice.tk.MIN.type: { const [, ...symExps] = this._nodes; this.addToMeta(meta, {text: `${symFunc.toString()}(`}); const args = []; symExps.forEach((symExp, i) => { if (i !== 0) this.addToMeta(meta, {text: `, `}); args.push(symExp[fnName](meta)); }); const out = Math[symFunc.toString()](...args); this.addToMeta(meta, {text: ")"}); return out; } case Renderer.dice.tk.AVERAGE.type: { const [, symExp] = this._nodes; return symExp.avg(meta); } case Renderer.dice.tk.DMAX.type: { const [, symExp] = this._nodes; return symExp.max(meta); } case Renderer.dice.tk.DMIN.type: { const [, symExp] = this._nodes; return symExp.min(meta); } default: throw new Error(`Unimplemented!`); } } toString () { let out; const [symFunc, symExp] = this._nodes; switch (symFunc.type) { case Renderer.dice.tk.FLOOR.type: case Renderer.dice.tk.CEIL.type: case Renderer.dice.tk.ROUND.type: case Renderer.dice.tk.AVERAGE.type: case Renderer.dice.tk.DMAX.type: case Renderer.dice.tk.DMIN.type: case Renderer.dice.tk.SIGN.type: case Renderer.dice.tk.ABS.type: case Renderer.dice.tk.CBRT.type: case Renderer.dice.tk.SQRT.type: case Renderer.dice.tk.EXP.type: case Renderer.dice.tk.LOG.type: case Renderer.dice.tk.RANDOM.type: case Renderer.dice.tk.TRUNC.type: case Renderer.dice.tk.POW.type: case Renderer.dice.tk.MAX.type: case Renderer.dice.tk.MIN.type: out = symFunc.toString(); break; default: throw new Error(`Unimplemented!`); } out += `(${symExp.toString()})`; return out; } }, Pool: class extends Renderer.dice.AbstractSymbol { constructor (nodesPool, nodeMod) { super(); this._nodesPool = nodesPool; this._nodeMod = nodeMod; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { const vals = this._nodesPool.map(it => { const subMeta = {}; return {node: it, val: it[fnName](subMeta), meta: subMeta}; }); if (this._nodeMod && vals.length) { const isSuccessMode = this._nodeMod.isSuccessMode; const modOpts = { fnGetRerolls: toReroll => toReroll.map(val => { const subMeta = {}; return {node: val.node, val: val.node[fnName](subMeta), meta: subMeta}; }), fnGetExplosions: toExplode => toExplode.map(val => { const subMeta = {}; return {node: val.node, val: val.node[fnName](subMeta), meta: subMeta}; }), }; const displayVals = Renderer.dice.parsed._handleModifiers(fnName, meta, vals, this._nodeMod, modOpts); const asHtml = displayVals.map(v => { const html = v.meta.html.join(""); if (v.isDropped) return `(${html})`; else if (v.isExploded) return `(${html})`; else if (v.isSuccess) return `(${html})`; else return `(${html})`; }).join("+"); const asText = displayVals.map(v => `(${v.meta.text.join("")})`).join("+"); const asMd = displayVals.map(v => `(${v.meta.md.join("")})`).join("+"); this.addToMeta(meta, {html: asHtml, text: asText, md: asMd}); if (isSuccessMode) { return vals.filter(it => !it.isDropped && it.isSuccess).length; } else { return vals.filter(it => !it.isDropped).map(it => it.val).sum(); } } else { this.addToMeta( meta, ["html", "text", "md"].mergeMap(prop => ({ [prop]: `${vals.map(it => `(${it.meta[prop].join("")})`).join("+")}`, })), ); return vals.map(it => it.val).sum(); } } toString () { return `{${this._nodesPool.map(it => it.toString()).join(", ")}}${this._nodeMod ? this._nodeMod.toString() : ""}`; } }, Factor: class extends Renderer.dice.AbstractSymbol { constructor (node, opts) { super(); opts = opts || {}; this._node = node; this._hasParens = !!opts.hasParens; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { switch (this._node.type) { case Renderer.dice.tk.TYP_NUMBER: { this.addToMeta(meta, {text: this.toString()}); return Number(this._node.value); } case Renderer.dice.tk.TYP_SYMBOL: { if (this._hasParens) this.addToMeta(meta, {text: "("}); const out = this._node[fnName](meta); if (this._hasParens) this.addToMeta(meta, {text: ")"}); return out; } case Renderer.dice.tk.PB.type: { this.addToMeta(meta, {text: this.toString(meta)}); return meta.pb == null ? 0 : meta.pb; } case Renderer.dice.tk.SUMMON_SPELL_LEVEL.type: { this.addToMeta(meta, {text: this.toString(meta)}); return meta.summonSpellLevel == null ? 0 : meta.summonSpellLevel; } case Renderer.dice.tk.SUMMON_CLASS_LEVEL.type: { this.addToMeta(meta, {text: this.toString(meta)}); return meta.summonClassLevel == null ? 0 : meta.summonClassLevel; } default: throw new Error(`Unimplemented!`); } } toString (indent) { let out; switch (this._node.type) { case Renderer.dice.tk.TYP_NUMBER: out = this._node.value; break; case Renderer.dice.tk.TYP_SYMBOL: out = this._node.toString(); break; case Renderer.dice.tk.PB.type: out = this.meta ? (this.meta.pb || 0) : "PB"; break; case Renderer.dice.tk.SUMMON_SPELL_LEVEL.type: out = this.meta ? (this.meta.summonSpellLevel || 0) : "the spell's level"; break; case Renderer.dice.tk.SUMMON_CLASS_LEVEL.type: out = this.meta ? (this.meta.summonClassLevel || 0) : "your class level"; break; default: throw new Error(`Unimplemented!`); } return this._hasParens ? `(${out})` : out; } }, Dice: class extends Renderer.dice.AbstractSymbol { static _facesToValue (faces, fnName) { switch (fnName) { case "evl": return RollerUtil.randomise(faces); case "avg": return (faces + 1) / 2; case "min": return 1; case "max": return faces; } } constructor (nodes) { super(); this._nodes = nodes; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { if (this._nodes.length === 1) return this._nodes[0][fnName](meta); // if it's just a factor // N.B. we don't pass the full "meta" to symbol evaluation inside the dice expression--we therefore won't see // the metadata from the nested rolls, but that's OK. const view = this._nodes.slice(); // Shift the first symbol and use that as our initial number of dice // e.g. the "2" in 2d3d5 const numSym = view.shift(); let tmp = numSym[fnName](Renderer.dice.util.getReducedMeta(meta)); while (view.length) { if (Math.round(tmp) !== tmp) throw new Error(`Number of dice to roll (${tmp}) was not an integer!`); // Use the next symbol as our number of faces // e.g. the "3" in `2d3d5` // When looping, the number of dice may have been a complex expression with modifiers; take the next // non-modifier symbol as the faces. // e.g. the "20" in `(2d3kh1r1)d20` (parentheses for emphasis) const facesSym = view.shift(); const faces = facesSym[fnName](); if (Math.round(faces) !== faces) throw new Error(`Dice face count (${faces}) was not an integer!`); const isLast = view.length === 0 || (view.length === 1 && view.last().isDiceModifierGroup); tmp = this._invoke_handlePart(fnName, meta, view, tmp, faces, isLast); } return tmp; } _invoke_handlePart (fnName, meta, view, num, faces, isLast) { const rolls = [...new Array(num)].map(() => ({val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName)})); let displayRolls; let isSuccessMode = false; if (view.length && view[0].isDiceModifierGroup) { const nodeMod = view[0]; if (fnName === "evl" || fnName === "min" || fnName === "max") { // avoid handling dice modifiers in "average" mode isSuccessMode = nodeMod.isSuccessMode; const modOpts = { faces, fnGetRerolls: toReroll => [...new Array(toReroll.length)].map(() => ({val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName)})), fnGetExplosions: toExplode => [...new Array(toExplode.length)].map(() => ({val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName)})), }; displayRolls = Renderer.dice.parsed._handleModifiers(fnName, meta, rolls, nodeMod, modOpts); } view.shift(); } else displayRolls = rolls; if (isLast) { // only display the dice for the final roll, e.g. in 2d3d4 show the Xd4 const asHtml = displayRolls.map(r => { if (r.htmlDisplay) return r.htmlDisplay; const numPart = Renderer.dice.parsed._rollToNumPart_html(r, faces); if (r.isDropped) return `[${numPart}]`; else if (r.isExploded) return `[${numPart}]`; else if (r.isSuccess) return `[${numPart}]`; else return `[${numPart}]`; }).join("+"); const asText = displayRolls.map(r => `[${r.val}]`).join("+"); const asMd = displayRolls.map(r => { if (r.isDropped) return `~~[${r.val}]~~`; else if (r.isExploded) return `_[${r.val}]_`; else if (r.isSuccess) return `**[${r.val}]**`; else return `[${r.val}]`; }).join("+"); this.addToMeta( meta, { html: asHtml, text: asText, md: asMd, }, ); } if (fnName === "evl") { const maxRolls = rolls.filter(it => it.val === faces && !it.isDropped); const minRolls = rolls.filter(it => it.val === 1 && !it.isDropped); meta.allMax = meta.allMax || []; meta.allMin = meta.allMin || []; meta.allMax.push(maxRolls.length && maxRolls.length === rolls.length); meta.allMin.push(minRolls.length && minRolls.length === rolls.length); } if (isSuccessMode) { return rolls.filter(it => !it.isDropped && it.isSuccess).length; } else { return rolls.filter(it => !it.isDropped).map(it => it.val).sum(); } } toString () { if (this._nodes.length === 1) return this._nodes[0].toString(); // if it's just a factor const [numSym, facesSym] = this._nodes; let out = `${numSym.toString()}d${facesSym.toString()}`; for (let i = 2; i < this._nodes.length; ++i) { const n = this._nodes[i]; if (n.isDiceModifierGroup) out += n.mods.map(it => `${it.modSym.toString()}${it.numSym.toString()}`).join(""); else out += `d${n.toString()}`; } return out; } }, Exponent: class extends Renderer.dice.AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { const view = this._nodes.slice(); let val = view.pop()[fnName](meta); while (view.length) { this.addToMeta(meta, {text: "^"}); val = view.pop()[fnName](meta) ** val; } return val; } toString () { const view = this._nodes.slice(); let out = view.pop().toString(); while (view.length) out = `${view.pop().toString()}^${out}`; return out; } }, Term: class extends Renderer.dice.AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { let out = this._nodes[0][fnName](meta); for (let i = 1; i < this._nodes.length; i += 2) { if (this._nodes[i].eq(Renderer.dice.tk.MULT)) { this.addToMeta(meta, {text: " × "}); out *= this._nodes[i + 1][fnName](meta); } else if (this._nodes[i].eq(Renderer.dice.tk.DIV)) { this.addToMeta(meta, {text: " ÷ "}); out /= this._nodes[i + 1][fnName](meta); } else throw new Error(`Unimplemented!`); } return out; } toString () { let out = this._nodes[0].toString(); for (let i = 1; i < this._nodes.length; i += 2) { if (this._nodes[i].eq(Renderer.dice.tk.MULT)) out += ` * ${this._nodes[i + 1].toString()}`; else if (this._nodes[i].eq(Renderer.dice.tk.DIV)) out += ` / ${this._nodes[i + 1].toString()}`; else throw new Error(`Unimplemented!`); } return out; } }, Expression: class extends Renderer.dice.AbstractSymbol { constructor (nodes) { super(); this._nodes = nodes; } _evl (meta) { return this._invoke("evl", meta); } _avg (meta) { return this._invoke("avg", meta); } _min (meta) { return this._invoke("min", meta); } _max (meta) { return this._invoke("max", meta); } _invoke (fnName, meta) { const view = this._nodes.slice(); let isNeg = false; if (view[0].eq(Renderer.dice.tk.ADD) || view[0].eq(Renderer.dice.tk.SUB)) { isNeg = view.shift().eq(Renderer.dice.tk.SUB); if (isNeg) this.addToMeta(meta, {text: "-"}); } let out = view[0][fnName](meta); if (isNeg) out = -out; for (let i = 1; i < view.length; i += 2) { if (view[i].eq(Renderer.dice.tk.ADD)) { this.addToMeta(meta, {text: " + "}); out += view[i + 1][fnName](meta); } else if (view[i].eq(Renderer.dice.tk.SUB)) { this.addToMeta(meta, {text: " - "}); out -= view[i + 1][fnName](meta); } else throw new Error(`Unimplemented!`); } return out; } toString (indent = 0) { let out = ""; const view = this._nodes.slice(); let isNeg = false; if (view[0].eq(Renderer.dice.tk.ADD) || view[0].eq(Renderer.dice.tk.SUB)) { isNeg = view.shift().eq(Renderer.dice.tk.SUB); if (isNeg) out += "-"; } out += view[0].toString(indent); for (let i = 1; i < view.length; i += 2) { if (view[i].eq(Renderer.dice.tk.ADD)) out += ` + ${view[i + 1].toString(indent)}`; else if (view[i].eq(Renderer.dice.tk.SUB)) out += ` - ${view[i + 1].toString(indent)}`; else throw new Error(`Unimplemented!`); } return out; } }, }; if (!IS_VTT && typeof window !== "undefined") { window.addEventListener("load", Renderer.dice._pInit); }