TheArtificer/src/artigen/dice/getModifiers.ts

232 lines
8.2 KiB
TypeScript

import { log, LogTypes as LT } from '@Log4Deno';
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({
Count: '-c',
NoDetails: '-nd',
SuperNoDetails: '-snd',
HideRaw: '-hr',
Spoiler: '-s',
Max: '-max',
MaxShorthand: '-m',
Min: '-min',
Nominal: '-n',
SimulatedNominal: '-sn',
GM: '-gm',
Order: '-o',
CommaTotals: '-ct',
ConfirmCrit: '-cc',
RollDistribution: '-rd',
NumberVariables: '-nv',
VariablesNumber: '-vn',
CustomDiceShapes: '-cd',
});
export const getModifiers = (args: string[]): [RollModifiers, string[]] => {
const modifiers: RollModifiers = {
noDetails: false,
superNoDetails: false,
hideRaw: false,
spoiler: '',
maxRoll: false,
minRoll: false,
nominalRoll: false,
simulatedNominal: 0,
gmRoll: false,
gms: [],
order: '',
count: false,
commaTotals: false,
confirmCrit: false,
rollDist: false,
numberVariables: false,
customDiceShapes: new Map<string, number[]>(),
apiWarn: '',
valid: true,
error: new Error(),
};
// 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++) {
log(LT.LOG, `Checking ${args.join(' ')} for command modifiers ${i} | ${args[i]}`);
let defaultCase = false;
switch (args[i].toLowerCase()) {
case Modifiers.Count:
modifiers.count = true;
break;
case Modifiers.NoDetails:
modifiers.noDetails = true;
break;
case Modifiers.SuperNoDetails:
modifiers.superNoDetails = true;
break;
case Modifiers.HideRaw:
modifiers.hideRaw = true;
break;
case Modifiers.Spoiler:
modifiers.spoiler = '||';
break;
case Modifiers.Max:
case Modifiers.MaxShorthand:
modifiers.maxRoll = true;
break;
case Modifiers.Min:
modifiers.minRoll = true;
break;
case Modifiers.Nominal:
modifiers.nominalRoll = true;
break;
case Modifiers.SimulatedNominal:
if (args[i + 1] && parseInt(args[i + 1]).toString() === args[i + 1]) {
// Shift the -sn out so the next item is the amount
args.splice(i, 1);
modifiers.simulatedNominal = parseInt(args[i]);
} else {
modifiers.simulatedNominal = 10000;
}
break;
case Modifiers.ConfirmCrit:
modifiers.confirmCrit = true;
break;
case Modifiers.GM:
modifiers.gmRoll = true;
// -gm is a little more complex, as we must get all of the GMs that need to be DMd
while (i + 1 < args.length && args[i + 1].startsWith('<@')) {
log(LT.LOG, `Finding all GMs, checking args ${JSON.stringify(args)}`);
// Keep looping thru the rest of the args until one does not start with the discord mention code
modifiers.gms.push(args[i + 1].replace(/!/g, ''));
args.splice(i + 1, 1);
}
if (modifiers.gms.length < 1) {
// If -gm is on and none were found, throw an error
modifiers.error.name = 'NoGMsFound';
modifiers.error.message = 'Must specify at least one GM by @mentioning them';
modifiers.valid = false;
return [modifiers, args];
}
break;
case Modifiers.Order:
// Shift the -o out of the array so the next item is the direction
args.splice(i, 1);
if (!args[i] || (args[i].toLowerCase()[0] !== 'd' && args[i].toLowerCase()[0] !== 'a')) {
// If -o is on and asc or desc was not specified, error out
modifiers.error.name = 'NoOrderFound';
modifiers.error.message = 'Must specify `a` or `d` to order the rolls ascending or descending';
modifiers.valid = false;
return [modifiers, args];
}
modifiers.order = args[i].toLowerCase()[0];
break;
case Modifiers.CommaTotals:
modifiers.commaTotals = true;
break;
case Modifiers.RollDistribution:
modifiers.rollDist = true;
break;
case Modifiers.NumberVariables:
case Modifiers.VariablesNumber:
modifiers.numberVariables = true;
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 case should not mess with the array
defaultCase = true;
break;
}
if (!defaultCase) {
args.splice(i, 1);
i--;
}
}
// maxRoll, minRoll, nominalRoll, simulatedNominal cannot be on at same time, throw an error
if ([modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll, modifiers.simulatedNominal].filter((b) => b).length > 1) {
modifiers.error.name = 'MaxAndNominal';
modifiers.error.message = 'Can only use one of the following at a time:\n`maximize`, `minimize`, `nominal`, `simulatedNominal`';
modifiers.valid = false;
}
// simulatedNominal and confirmCrit cannot be used at same time, throw an error
if ([modifiers.confirmCrit, modifiers.simulatedNominal].filter((b) => b).length > 1) {
modifiers.error.name = 'SimNominalAndCC';
modifiers.error.message = 'Cannot use the following at the same time:\n`confirmCrit`, `simulatedNominal`';
modifiers.valid = false;
}
// simulatedNominal cannot be greater than config.limits.simulatedNominal
if (modifiers.simulatedNominal > config.limits.simulatedNominal) {
modifiers.error.name = 'SimNominalTooBig';
modifiers.error.message = `Number of iterations for \`simulatedNominal\` cannot be greater than \`${config.limits.simulatedNominal}\``;
modifiers.valid = false;
}
return [modifiers, args];
};