@planarally/dice
Version:
3D dice rolling functionality for babylon.js.
230 lines (229 loc) • 8.3 kB
JavaScript
import { Status } from "../../core/types";
import { DxSegmentType, } from "./types";
export * from "./types";
function selects(index, rolls, part) {
const value = rolls[index].roll;
if (part.selector === undefined)
return false;
if (part.selector === "=" || part.selectorValue === undefined)
return value === part.selectorValue;
else if (part.selector === "<")
return value < part.selectorValue;
else if (part.selector === ">")
return value > part.selectorValue;
else if (part.selector === "highest")
return [...rolls.entries()]
.sort((a, b) => b[1].roll - a[1].roll)
.slice(0, part.selectorValue)
.some(([i]) => i === index);
else if (part.selector === "lowest")
return [...rolls.entries()]
.sort((a, b) => a[1].roll - b[1].roll)
.slice(0, part.selectorValue)
.some(([i]) => i === index);
return false;
}
function randomInterval(min, max) {
return Math.random() * (max - min) + min;
}
export async function roll(part, rollOptions) {
if (part.type !== DxSegmentType.Die) {
throw new Error(`Received a part of an unexpected type (${part.type})`);
}
return Promise.resolve({
...part,
output: Array.from({ length: part.amount }, () => {
const result = Math.round(randomInterval(1, Number.parseInt(part.die.slice(1), 10)));
if (part.die === "d100" && result === 100 && rollOptions.d100Mode === 0)
return 0;
return result;
}),
status: Status.PendingEvaluation,
});
}
function collect(parts) {
let total = 0;
let opMode = "+";
const partsWithResults = [];
for (const part of parts) {
let shortResult = "";
let longResult = undefined;
if (part.type === DxSegmentType.Literal) {
total += part.value * (opMode === "+" ? 1 : -1);
shortResult += part.value.toString();
}
else if (part.type === DxSegmentType.Operator) {
opMode = part.input;
shortResult += part.input.toString();
}
else if (part.type === DxSegmentType.Die) {
let subSum = 0;
longResult = "";
for (const result of part.output) {
if (longResult.length > 0)
longResult += ",";
let syntaxWrap = "";
if (result.status === "overridden") {
syntaxWrap = "~";
}
else if (part.operator === "keep") {
if (result.status === "kept") {
subSum += result.roll;
syntaxWrap = "*";
}
}
else if (part.operator === "drop") {
if (result.status !== "dropped") {
subSum += result.roll;
syntaxWrap = "~";
}
}
else {
subSum += result.roll;
}
longResult += `${syntaxWrap}${result.roll}${syntaxWrap}`;
}
shortResult += subSum.toString();
total += subSum * (opMode === "+" ? 1 : -1);
}
partsWithResults.push({ ...part, longResult, shortResult });
}
return {
parts: partsWithResults,
result: total.toString(),
};
}
function evaluate(part) {
if (part.type !== DxSegmentType.Die) {
throw new Error(`Received a part of an unexpected type (${part.type})`);
}
const rolls = [];
// First resolve all dice minima/maxima
for (let result of part.output) {
let newResult = result;
if (part.operator === "min" && result < part.selectorValue)
newResult = part.selectorValue;
if (part.operator === "max" && result > part.selectorValue)
newResult = part.selectorValue;
if (result !== newResult)
rolls.push({ roll: result, status: "overridden" });
rolls.push({ roll: newResult });
}
// Then resolve all selectors
for (const [i, roll] of rolls.entries()) {
if (part.operator === "keep" || part.operator === "drop") {
if (selects(i, rolls, part)) {
roll.status = part.operator === "keep" ? "kept" : "dropped";
}
}
}
return {
...part,
output: rolls,
status: Status.Resolved,
};
}
function parse(input) {
const data = [];
/*
(?:^|(?<op>[+-]))\s* // Operator
(?:
(?<dice>
(?<numDice>\d+)d(?<diceSize>\d+) // XdY
)
(?:
(?: // Start of optional modifiers
(?:
(?<selMod> // Modifiers that can use selectors
[kpe]
|
(?:r[aor])
)
(?<selector>[hl<>=])? // selectors
)
|
(?<nselModifier>m[ai]) // modifiers that only work on literal values
)
(?<selval>\d+) // literal value for modifier
)?
|
(?<fixed>\d+) // literal value instead of XdY
)
*/
const regex = /(?:^|(?<op>[+-]))\s*(?:(?<dice>(?<numDice>\d+)d(?<diceSize>\d+))(?:(?:(?:(?<selMod>[kpe]|(?:r[aor]))(?<selector>[hl<>=])?)|(?<nselMod>m[ai]))(?<selval>\d+))?|(?<fixed>\d+))/g;
for (const part of input.matchAll(regex)) {
if (part.groups?.op !== undefined) {
data.push({
input: part.groups.op,
status: Status.Resolved,
type: DxSegmentType.Operator,
});
}
if (part.groups?.fixed !== undefined) {
data.push({
input: part.groups.fixed,
status: Status.Resolved,
type: DxSegmentType.Literal,
value: Number.parseInt(part.groups.fixed, 10),
});
}
else if (part.groups?.dice !== undefined) {
let operator;
if (part.groups.selMod !== undefined) {
const m = part.groups.selMod;
if (m === "k")
operator = "keep";
else if (m === "p")
operator = "drop";
else if (m === "rr")
operator = "inf";
else if (m === "ro")
operator = "once";
else if (m === "ra")
operator = "add";
else if (m === "e")
operator = "explode";
}
else if (part.groups?.nselMod !== undefined) {
const m = part.groups.nselMod;
if (m === "mi")
operator = "min";
else if (m === "ma")
operator = "max";
}
let selector;
if (part.groups.selector !== undefined) {
const s = part.groups.selector;
if (s === ">")
selector = ">";
else if (s === "<")
selector = "<";
else if (s === "=")
selector = "=";
else if (s === "h")
selector = "highest";
else if (s === "l")
selector = "lowest";
}
const die = `d${part.groups.diceSize}`;
const amount = part.groups.numDice;
data.push({
amount: Number.parseInt(amount, 10),
die,
input: `${amount}${die}${part.groups?.selMod ?? ""}${part.groups?.selector ?? ""}${part.groups?.nselMod ?? ""}${part.groups?.selval ?? ""}`,
operator,
selector,
selectorValue: part.groups.selval === undefined ? undefined : Number.parseInt(part.groups.selval, 10),
status: Status.PendingRoll,
type: DxSegmentType.Die,
});
}
}
return data;
}
export const DX = {
collect,
evaluate,
parse,
roll,
};