Implement Custom Dice Shapes

This commit is contained in:
Ean Milligan 2025-07-09 12:53:10 -04:00
parent b2d4d0c0a4
commit 30f0314695
12 changed files with 174 additions and 44 deletions

View File

@ -5,7 +5,7 @@ meta {
} }
get { get {
url: http://localhost:8166/api/roll?user=[discord-user-id]&channel=[discord-channel-id]&rollstr=[artificer-roll-cmd]&documentation=All items below are optional. Flags do not need values.&nd=[no-details-flag]&snd=[super-no-details-flag]&hr=[hide-raw-roll-details-flag]&s=[spoiler-results-flag]&m-or-max=[max-roll-flag, cannot be used with n flag]&min=[min-roll-flag, cannot be used with n, sn, or max]&n=[nominal-roll-flag, cannot be used with sn, max or min flag]&sn=[simulated-nominal-flag, can pass number with it, cannot be used with max, min, n. or cc]&gms=[csv-of-discord-user-ids-to-be-dmed-results]&o=[order-rolls, must be a or d]&c=[count-flag]&cc=[confirm-crit-flag, cannot be used with sn]&rd=[roll-dist-flag]&nv-or-vn=[number-variables-flag] url: http://localhost:8166/api/roll?user=[discord-user-id]&channel=[discord-channel-id]&rollstr=[artificer-roll-cmd]&documentation=All items below are optional. Flags do not need values.&nd=[no-details-flag]&snd=[super-no-details-flag]&hr=[hide-raw-roll-details-flag]&s=[spoiler-results-flag]&m-or-max=[max-roll-flag, cannot be used with n flag]&min=[min-roll-flag, cannot be used with n, sn, or max]&n=[nominal-roll-flag, cannot be used with sn, max or min flag]&sn=[simulated-nominal-flag, can pass number with it, cannot be used with max, min, n. or cc]&gms=[csv-of-discord-user-ids-to-be-dmed-results]&o=[order-rolls, must be a or d]&c=[count-flag]&cc=[confirm-crit-flag, cannot be used with sn]&rd=[roll-dist-flag]&nv-or-vn=[number-variables-flag]&cd=[custom-dice, format value as name:[side1,side2,...,sideN], use ; to separate multiple custom dice]
body: none body: none
auth: inherit auth: inherit
} }
@ -29,4 +29,5 @@ params:query {
cc: [confirm-crit-flag, cannot be used with sn] cc: [confirm-crit-flag, cannot be used with sn]
rd: [roll-dist-flag] rd: [roll-dist-flag]
nv-or-vn: [number-variables-flag] nv-or-vn: [number-variables-flag]
cd: [custom-dice, format value as name:[side1,side2,...,sideN], use ; to separate multiple custom dice]
} }

View File

@ -149,6 +149,7 @@ The Artificer comes with a few supplemental commands to the main rolling command
* `-rd` - Roll Distribution - Shows a raw roll distribution of all dice in roll * `-rd` - Roll Distribution - Shows a raw roll distribution of all dice in roll
* `-hr` - Hide Raw - Hide the raw input, showing only the results/details of the roll * `-hr` - Hide Raw - Hide the raw input, showing only the results/details of the roll
* `-nv` or `-vn` - Number Variables - Adds `xN` before each roll command in the details section for debug reasons * `-nv` or `-vn` - Number Variables - Adds `xN` before each roll command in the details section for debug reasons
* `-cd` - Custom Dice shapes - Allows a list of `name:[side1,side2,...,sideN]` separated by `;` to be passed to create special shaped dice
* The results have some formatting applied on them to provide details on what happened during this roll. * The results have some formatting applied on them to provide details on what happened during this roll.
* Critical successes will be **bolded** * Critical successes will be **bolded**
* Critical fails will be <ins>underlined</ins> * Critical fails will be <ins>underlined</ins>

View File

@ -1,7 +1,7 @@
import { SolvedStep } from 'artigen/math/math.d.ts'; import { SolvedStep } from 'artigen/math/math.d.ts';
// Available Roll Types // Available Roll Types
type RollType = '' | 'roll20' | 'fate' | 'cwod' | 'ova'; type RollType = '' | 'custom' | 'roll20' | 'fate' | 'cwod' | 'ova';
// RollSet is used to preserve all information about a calculated roll // RollSet is used to preserve all information about a calculated roll
export interface RollSet { export interface RollSet {
@ -35,6 +35,8 @@ export interface CountDetails {
// use rollDistKey to generate the key // use rollDistKey to generate the key
export type RollDistributionMap = Map<string, number[]>; export type RollDistributionMap = Map<string, number[]>;
export type CustomDiceShapes = Map<string, number[]>;
// RollFormat is the return structure for the rollFormatter // RollFormat is the return structure for the rollFormatter
export interface FormattedRoll { export interface FormattedRoll {
solvedStep: SolvedStep; solvedStep: SolvedStep;
@ -60,6 +62,7 @@ export interface RollModifiers {
confirmCrit: boolean; confirmCrit: boolean;
rollDist: boolean; rollDist: boolean;
numberVariables: boolean; numberVariables: boolean;
customDiceShapes: CustomDiceShapes;
apiWarn: string; apiWarn: string;
valid: boolean; valid: boolean;
error: Error; error: Error;
@ -115,6 +118,7 @@ export interface GroupConf extends BaseConf {
// RollConf carries the machine readable roll configuration the user specified // RollConf carries the machine readable roll configuration the user specified
export interface RollConf extends BaseConf { export interface RollConf extends BaseConf {
type: RollType; type: RollType;
customType: string | null;
dieCount: number; dieCount: number;
dieSize: number; dieSize: number;
dPercent: DPercentConf; dPercent: DPercentConf;

View File

@ -1,7 +1,7 @@
import { log, LogTypes as LT } from '@Log4Deno'; import { log, LogTypes as LT } from '@Log4Deno';
import { ExecutedRoll, RollModifiers, RollSet, SumOverride } from 'artigen/dice/dice.d.ts'; import { ExecutedRoll, RollModifiers, RollSet, SumOverride } from 'artigen/dice/dice.d.ts';
import { genFateRoll, genRoll } from 'artigen/dice/randomRoll.ts'; import { generateRoll } from 'artigen/dice/randomRoll.ts';
import { getRollConf } from 'artigen/dice/getRollConf.ts'; import { getRollConf } from 'artigen/dice/getRollConf.ts';
import { loggingEnabled } from 'artigen/utils/logFlag.ts'; import { loggingEnabled } from 'artigen/utils/logFlag.ts';
@ -24,7 +24,7 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed
rollStr = rollStr.toLowerCase(); rollStr = rollStr.toLowerCase();
// Turn the rollStr into a machine readable rollConf // Turn the rollStr into a machine readable rollConf
const rollConf = getRollConf(rollStr); const rollConf = getRollConf(rollStr, modifiers.customDiceShapes);
// Roll the roll // Roll the roll
const rollSet: RollSet[] = []; const rollSet: RollSet[] = [];
@ -85,12 +85,12 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const rolling = getTemplateRoll(); const rolling = getTemplateRoll();
// If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
rolling.roll = rollConf.type === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); rolling.roll = generateRoll(rollConf, modifiers);
rolling.size = rollConf.dieSize; rolling.size = rollConf.dieSize;
// Set origIdx of roll // Set origIdx of roll
rolling.origIdx = i; rolling.origIdx = i;
flagRoll(rollConf, rolling); flagRoll(rollConf, rolling, modifiers.customDiceShapes);
loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(rolling)}`); loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(rolling)}`);
@ -140,10 +140,10 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed
newReroll.roll = minMaxOverride; newReroll.roll = minMaxOverride;
} else { } else {
// If nominalRoll is on, set the roll to the average roll of dieSize, otherwise generate a new random roll // If nominalRoll is on, set the roll to the average roll of dieSize, otherwise generate a new random roll
newReroll.roll = rollConf.type === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); newReroll.roll = generateRoll(rollConf, modifiers);
} }
flagRoll(rollConf, newReroll); flagRoll(rollConf, newReroll, modifiers.customDiceShapes);
loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newReroll)}`); loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newReroll)}`);
@ -161,12 +161,12 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): Executed
// Copy the template to fill out for this iteration // Copy the template to fill out for this iteration
const newExplodingRoll = getTemplateRoll(); const newExplodingRoll = getTemplateRoll();
// If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll // If maximizeRoll is on, set the roll to the dieSize, else if nominalRoll is on, set the roll to the average roll of dieSize, else generate a new random roll
newExplodingRoll.roll = rollConf.type === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent); newExplodingRoll.roll = generateRoll(rollConf, modifiers);
newExplodingRoll.size = rollConf.dieSize; newExplodingRoll.size = rollConf.dieSize;
// Always mark this roll as exploding // Always mark this roll as exploding
newExplodingRoll.exploding = true; newExplodingRoll.exploding = true;
flagRoll(rollConf, newExplodingRoll); flagRoll(rollConf, newExplodingRoll, modifiers.customDiceShapes);
loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newExplodingRoll)}`); loggingEnabled && log(LT.LOG, `${getLoopCount()} Roll done ${JSON.stringify(newExplodingRoll)}`);

View File

@ -28,16 +28,7 @@ export const formatRoll = (executedRoll: ExecutedRoll, modifiers: RollModifiers)
if (!e.dropped && !e.rerolled) { if (!e.dropped && !e.rerolled) {
// If the roll was not dropped or rerolled, add it to the stepTotal and flag the critHit/critFail // If the roll was not dropped or rerolled, add it to the stepTotal and flag the critHit/critFail
switch (e.type) {
case 'ova':
case 'roll20':
case 'fate':
tempTotal += e.roll; tempTotal += e.roll;
break;
case 'cwod':
tempTotal += e.success ? 1 : 0;
break;
}
if (e.critHit) { if (e.critHit) {
tempCrit = true; tempCrit = true;
} }

View File

@ -1,8 +1,10 @@
import { log, LogTypes as LT } from '@Log4Deno'; import { log, LogTypes as LT } from '@Log4Deno';
import { RollModifiers } from 'artigen/dice/dice.d.ts';
import config from '~config'; import config from '~config';
import { RollModifiers } from 'artigen/dice/dice.d.ts';
export const reservedCharacters = ['d', '%', '^', '*', '(', ')', '{', '}', '/', '+', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
export const Modifiers = Object.freeze({ export const Modifiers = Object.freeze({
Count: '-c', Count: '-c',
NoDetails: '-nd', NoDetails: '-nd',
@ -21,6 +23,7 @@ export const Modifiers = Object.freeze({
RollDistribution: '-rd', RollDistribution: '-rd',
NumberVariables: '-nv', NumberVariables: '-nv',
VariablesNumber: '-vn', VariablesNumber: '-vn',
CustomDiceShapes: '-cd',
}); });
export const getModifiers = (args: string[]): [RollModifiers, string[]] => { export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
@ -41,6 +44,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
confirmCrit: false, confirmCrit: false,
rollDist: false, rollDist: false,
numberVariables: false, numberVariables: false,
customDiceShapes: new Map<string, number[]>(),
apiWarn: '', apiWarn: '',
valid: true, valid: true,
error: new Error(), error: new Error(),
@ -48,7 +52,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
// Check if any of the args are command flags and pull those out into the modifiers object // Check if any of the args are command flags and pull those out into the modifiers object
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
log(LT.LOG, `Checking ${args.join(' ')} for command modifiers ${i}`); log(LT.LOG, `Checking ${args.join(' ')} for command modifiers ${i} | ${args[i]}`);
let defaultCase = false; let defaultCase = false;
switch (args[i].toLowerCase()) { switch (args[i].toLowerCase()) {
case Modifiers.Count: case Modifiers.Count:
@ -103,6 +107,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
// If -gm is on and none were found, throw an error // If -gm is on and none were found, throw an error
modifiers.error.name = 'NoGMsFound'; modifiers.error.name = 'NoGMsFound';
modifiers.error.message = 'Must specify at least one GM by @mentioning them'; modifiers.error.message = 'Must specify at least one GM by @mentioning them';
modifiers.valid = false;
return [modifiers, args]; return [modifiers, args];
} }
break; break;
@ -114,6 +119,7 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
// If -o is on and asc or desc was not specified, error out // If -o is on and asc or desc was not specified, error out
modifiers.error.name = 'NoOrderFound'; modifiers.error.name = 'NoOrderFound';
modifiers.error.message = 'Must specify `a` or `d` to order the rolls ascending or descending'; modifiers.error.message = 'Must specify `a` or `d` to order the rolls ascending or descending';
modifiers.valid = false;
return [modifiers, args]; return [modifiers, args];
} }
@ -129,6 +135,65 @@ export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
case Modifiers.VariablesNumber: case Modifiers.VariablesNumber:
modifiers.numberVariables = true; modifiers.numberVariables = true;
break; break;
case Modifiers.CustomDiceShapes: {
// Shift the -cd out of the array so the dice shapes are next
args.splice(i, 1);
const cdSyntaxMessage =
'Must specify at least one custom dice shape using the `name:[side1,side2,...,sideN]` syntax. If multiple custom dice shapes are needed, use a `;` to separate the list.';
const shapes = (args[i] ?? '').split(';').filter((x) => x);
if (!shapes.length) {
modifiers.error.name = 'NoShapesSpecified';
modifiers.error.message = `No custom shaped dice found.\n\n${cdSyntaxMessage}`;
modifiers.valid = false;
return [modifiers, args];
}
for (const shape of shapes) {
const [name, rawSides] = shape.split(':').filter((x) => x);
if (!name || !rawSides || !rawSides.includes('[') || !rawSides.includes(']')) {
modifiers.error.name = 'InvalidShapeSpecified';
modifiers.error.message = `One of the custom dice is not formatted correctly.\n\n${cdSyntaxMessage}`;
modifiers.valid = false;
return [modifiers, args];
}
if (modifiers.customDiceShapes.has(name)) {
modifiers.error.name = 'ShapeAlreadySpecified';
modifiers.error.message = `Shape \`${name}\` is already specified, please give it a different name.\n\n${cdSyntaxMessage}`;
modifiers.valid = false;
return [modifiers, args];
}
if (reservedCharacters.some((char) => name.includes(char))) {
modifiers.error.name = 'InvalidCharacterInCDName';
modifiers.error.message = `Custom dice names cannot include any of the following characters:\n${JSON.stringify(
reservedCharacters
)}\n\n${cdSyntaxMessage}`;
modifiers.valid = false;
return [modifiers, args];
}
const sides = rawSides
.replaceAll('[', '')
.replaceAll(']', '')
.split(',')
.filter((x) => x)
.map((side) => parseInt(side));
if (!sides.length) {
modifiers.error.name = 'NoCustomSidesSpecified';
modifiers.error.message = `No sides found for \`${name}\`.\n\n${cdSyntaxMessage}`;
modifiers.valid = false;
return [modifiers, args];
}
modifiers.customDiceShapes.set(name, sides);
}
log(LT.LOG, `Generated Custom Dice: ${JSON.stringify(modifiers.customDiceShapes.entries().toArray())}`);
break;
}
default: default:
// Default case should not mess with the array // Default case should not mess with the array
defaultCase = true; defaultCase = true;

View File

@ -1,6 +1,6 @@
import { log, LogTypes as LT } from '@Log4Deno'; import { log, LogTypes as LT } from '@Log4Deno';
import { RollConf } from 'artigen/dice/dice.d.ts'; import { CustomDiceShapes, RollConf } from 'artigen/dice/dice.d.ts';
import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/rollOptions.ts'; import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/rollOptions.ts';
@ -14,13 +14,14 @@ const throwDoubleSepError = (sep: string): void => {
}; };
// Converts a rollStr into a machine readable rollConf // Converts a rollStr into a machine readable rollConf
export const getRollConf = (rollStr: string): RollConf => { export const getRollConf = (rollStr: string, customTypes: CustomDiceShapes = new Map<string, number[]>()): RollConf => {
// Split the roll on the die size (and the drop if its there) // Split the roll on the die size (and the drop if its there)
const dPts = rollStr.split('d'); const dPts = rollStr.split('d');
// Initialize the configuration to store the parsed data // Initialize the configuration to store the parsed data
const rollConf: RollConf = { const rollConf: RollConf = {
type: '', type: '',
customType: null,
dieCount: 0, dieCount: 0,
dieSize: 0, dieSize: 0,
dPercent: { dPercent: {
@ -92,10 +93,12 @@ export const getRollConf = (rollStr: string): RollConf => {
const rawDC = dPts.shift() || '1'; const rawDC = dPts.shift() || '1';
if (rawDC.includes('.')) { if (rawDC.includes('.')) {
throw new Error('WholeDieCountSizeOnly'); throw new Error('WholeDieCountSizeOnly');
} else if (!rawDC.endsWith('cwo') && !rawDC.endsWith('ova') && rawDC.match(/\D/)) {
throw new Error(`CannotParseDieCount_${rawDC}`);
} }
const tempDC = rawDC.replace(/\D/g, ''); const tempDC = rawDC.replace(/\D/g, '');
const numberlessRawDC = rawDC.replace(/\d/g, '');
if (!tempDC && !numberlessRawDC) {
throw new Error(`CannotParseDieCount_${rawDC}`);
}
// Rejoin all remaining parts // Rejoin all remaining parts
let remains = dPts.join('d'); let remains = dPts.join('d');
@ -160,6 +163,12 @@ export const getRollConf = (rollStr: string): RollConf => {
// remove F from the remains // remove F from the remains
remains = remains.slice(1); remains = remains.slice(1);
} else if (customTypes.has(numberlessRawDC)) {
// custom dice setup
rollConf.type = 'custom';
rollConf.customType = numberlessRawDC;
rollConf.dieCount = isNaN(parseInt(tempDC ?? '1')) ? 1 : parseInt(tempDC ?? '1');
rollConf.dieSize = Math.max(...(customTypes.get(numberlessRawDC) ?? []));
} else { } else {
// roll20 dice setup // roll20 dice setup
rollConf.type = 'roll20'; rollConf.type = 'roll20';

View File

@ -1,8 +1,10 @@
import { DPercentConf, RollModifiers } from 'artigen/dice/dice.d.ts'; import { DPercentConf, RollConf, RollModifiers } from 'artigen/dice/dice.d.ts';
// genRoll(size) returns number import { basicReducer } from 'artigen/utils/reducers.ts';
// genRoll rolls a die of size size and returns the result
export const genRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => { // genBasicRoll(size, modifiers, dPercent) returns number
// genBasicRoll rolls a die of size size and returns the result
const genBasicRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => {
let result; let result;
if (modifiers.maxRoll) { if (modifiers.maxRoll) {
result = size; result = size;
@ -15,13 +17,25 @@ export const genRoll = (size: number, modifiers: RollModifiers, dPercent: DPerce
return dPercent.on ? (result - 1) * dPercent.sizeAdjustment : result; return dPercent.on ? (result - 1) * dPercent.sizeAdjustment : result;
}; };
// genFateRoll returns -1|0|1 const getRollFromArray = (sides: number[], modifiers: RollModifiers): number => {
// genFateRoll turns a d6 into a fate die, with sides: -1, -1, 0, 0, 1, 1
export const genFateRoll = (modifiers: RollModifiers): number => {
if (modifiers.nominalRoll) { if (modifiers.nominalRoll) {
return 0; return sides.reduce(basicReducer, 0) / sides.length;
} else { } else if (modifiers.maxRoll) {
const sides = [-1, -1, 0, 0, 1, 1]; return Math.max(...sides);
return sides[genRoll(6, modifiers, <DPercentConf> { on: false }) - 1]; } else if (modifiers.minRoll) {
return Math.min(...sides);
}
return sides[genBasicRoll(sides.length, modifiers, <DPercentConf>{ on: false }) - 1];
};
export const generateRoll = (rollConf: RollConf, modifiers: RollModifiers): number => {
switch (rollConf.type) {
case 'fate':
return getRollFromArray([-1, -1, 0, 0, 1, 1], modifiers);
case 'custom':
return getRollFromArray(modifiers.customDiceShapes.get(rollConf.customType ?? '') ?? [], modifiers);
default:
return genBasicRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
} }
}; };

View File

@ -310,7 +310,9 @@ export const tokenizeMath = (
} }
// Now that mathConf is parsed, send it into the solver // Now that mathConf is parsed, send it into the solver
loggingEnabled && log(LT.LOG, `Sending mathConf to solver ${JSON.stringify(mathConf)}`);
const tempSolved = mathSolver(mathConf); const tempSolved = mathSolver(mathConf);
loggingEnabled && log(LT.LOG, `SolvedStep back from mathSolver ${JSON.stringify(tempSolved)}`);
// Push all of this step's solved data into the temp array // Push all of this step's solved data into the temp array
return [ return [

View File

@ -1,6 +1,6 @@
import { RollConf, RollSet } from 'artigen/dice/dice.d.ts'; import { CustomDiceShapes, RollConf, RollSet } from 'artigen/dice/dice.d.ts';
export const flagRoll = (rollConf: RollConf, rollSet: RollSet) => { export const flagRoll = (rollConf: RollConf, rollSet: RollSet, customDiceShapes: CustomDiceShapes) => {
// If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size // If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size
if (rollConf.critScore.on && rollConf.critScore.range.includes(rollSet.roll)) { if (rollConf.critScore.on && rollConf.critScore.range.includes(rollSet.roll)) {
rollSet.critHit = true; rollSet.critHit = true;
@ -14,6 +14,8 @@ export const flagRoll = (rollConf: RollConf, rollSet: RollSet) => {
} else if (!rollConf.critFail.on) { } else if (!rollConf.critFail.on) {
if (rollConf.type === 'fate') { if (rollConf.type === 'fate') {
rollSet.critFail = rollSet.roll === -1; rollSet.critFail = rollSet.roll === -1;
} else if (rollConf.type === 'custom') {
rollSet.critFail = rollSet.roll === Math.min(...(customDiceShapes.get(rollConf.customType ?? '') ?? []));
} else { } else {
rollSet.critFail = rollSet.roll === (rollConf.dPercent.on ? 0 : 1); rollSet.critFail = rollSet.roll === (rollConf.dPercent.on ? 0 : 1);
} }

View File

@ -103,7 +103,8 @@ const getDistName = (key: string) => {
return `CWOD d${size}`; return `CWOD d${size}`;
case 'ova': case 'ova':
return `OVA d${size}`; return `OVA d${size}`;
case 'roll20': case 'custom':
return `Custom d${size}`;
default: default:
return `d${size}`; return `d${size}`;
} }
@ -113,19 +114,21 @@ export const generateRollDistsEmbed = (rollDists: RollDistributionMap): ArtigenE
const fields = rollDists const fields = rollDists
.entries() .entries()
.toArray() .toArray()
.slice(0, 25)
.map(([key, distArr]) => { .map(([key, distArr]) => {
const total = distArr.reduce(basicReducer, 0); const total = distArr.reduce(basicReducer, 0);
return { return {
name: `${getDistName(key)} (Total rolls: ${total}):`, name: `${getDistName(key)} (Total rolls: ${total}):`,
value: distArr.map((cnt, dieIdx) => `${key.startsWith('fate') ? dieIdx - 1 : dieIdx + 1}: ${cnt} (${((cnt / total) * 100).toFixed(1)}%)`).join('\n'), value: distArr
.map((cnt, dieIdx) => key.startsWith('custom') && cnt === 0 ? '' : `${key.startsWith('fate') ? dieIdx - 1 : dieIdx + 1}: ${cnt} (${((cnt / total) * 100).toFixed(1)}%)`)
.filter((x) => x)
.join('\n'),
inline: true, inline: true,
}; };
}); });
const rollDistTitle = 'Roll Distributions:'; const rollDistTitle = 'Roll Distributions:';
const totalSize = fields.map((field) => field.name.length + field.value.length).reduce(basicReducer, 0); const totalSize = fields.map((field) => field.name.length + field.value.length).reduce(basicReducer, 0);
if (totalSize > 4000 || fields.some((field) => field.name.length > 256 || field.value.length > 1024)) { if (totalSize > 4000 || fields.length > 25 || fields.some((field) => field.name.length > 256 || field.value.length > 1024)) {
const rollDistBlob = new Blob([fields.map((field) => `# ${field.name}\n${field.value}`).join('\n\n') as BlobPart], { type: 'text' }); const rollDistBlob = new Blob([fields.map((field) => `# ${field.name}\n${field.value}`).join('\n\n') as BlobPart], { type: 'text' });
if (rollDistBlob.size > config.maxFileSize) { if (rollDistBlob.size > config.maxFileSize) {
const rollDistErrDesc = const rollDistErrDesc =

View File

@ -4,6 +4,7 @@ import { log, LogTypes as LT } from '@Log4Deno';
import config from '~config'; import config from '~config';
import { RollModifiers } from 'artigen/dice/dice.d.ts'; import { RollModifiers } from 'artigen/dice/dice.d.ts';
import { reservedCharacters } from 'artigen/dice/getModifiers.ts';
import { sendRollRequest } from 'artigen/managers/queueManager.ts'; import { sendRollRequest } from 'artigen/managers/queueManager.ts';
@ -100,11 +101,48 @@ export const apiRoll = async (query: Map<string, string>, apiUserid: bigint): Pr
confirmCrit: query.has('cc'), confirmCrit: query.has('cc'),
rollDist: query.has('rd'), rollDist: query.has('rd'),
numberVariables: query.has('nv') || query.has('vn'), numberVariables: query.has('nv') || query.has('vn'),
customDiceShapes: new Map<string, number[]>(),
apiWarn: hideWarn ? '' : apiWarning, apiWarn: hideWarn ? '' : apiWarning,
valid: true, valid: true,
error: new Error(), error: new Error(),
}; };
// Handle adding all sides into the cds array
if (query.has('cd')) {
const shapes = (query.get('cd') ?? '').split(';').filter((x) => x);
if (!shapes.length) {
return stdResp.BadRequest('cd specified without any shapes provided');
}
for (const shape of shapes) {
const [name, rawSides] = shape.split(':').filter((x) => x);
if (!name || !rawSides) {
return stdResp.BadRequest(
'cd specified with invalid pattern. Must be in format of `name:[side1,side2,...,sideN]`. If multiple custom dice shapes are needed, use a `;` to separate the list'
);
}
if (modifiers.customDiceShapes.has(name)) {
return stdResp.BadRequest('cd specified, cannot repeat names');
}
if (reservedCharacters.some((char) => name.includes(char))) {
return stdResp.BadRequest('cd specified, die name includes invalid characters. Reserved Character List: ' + JSON.stringify(reservedCharacters));
}
const sides = rawSides
.replaceAll('[', '')
.replaceAll(']', '')
.split(',')
.filter((x) => x)
.map((side) => parseInt(side));
if (!sides.length) {
return stdResp.BadRequest('cd specified without any sides provided');
}
modifiers.customDiceShapes.set(name, sides);
}
}
// maxRoll, minRoll, and nominalRoll cannot be on at same time, throw an error // maxRoll, minRoll, and nominalRoll cannot be on at same time, throw an error
if ([modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll, modifiers.simulatedNominal].filter((b) => b).length > 1) { if ([modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll, modifiers.simulatedNominal].filter((b) => b).length > 1) {
return stdResp.BadRequest('Can only use one of the following at a time:\n`maximize`, `minimize`, `nominal`, `simulatedNominal`'); return stdResp.BadRequest('Can only use one of the following at a time:\n`maximize`, `minimize`, `nominal`, `simulatedNominal`');
@ -139,7 +177,7 @@ export const apiRoll = async (query: Map<string, string>, apiUserid: bigint): Pr
} else { } else {
// Alert API user that they messed up // Alert API user that they messed up
return stdResp.Forbidden( return stdResp.Forbidden(
`Verify you are a member of the guild you are sending this roll to. If you are, the ${config.name} may not have that registered, please send a message in the guild so ${config.name} can register this. This registration is temporary, so if you see this error again, just poke your server again.`, `Verify you are a member of the guild you are sending this roll to. If you are, the ${config.name} may not have that registered, please send a message in the guild so ${config.name} can register this. This registration is temporary, so if you see this error again, just poke your server again.`
); );
} }
} else { } else {