mirror of
https://github.com/Burn-E99/TheArtificer.git
synced 2026-06-04 09:03:50 -04:00
[untested] - start reorganizing the solver folder (renamed to artigen here), organize imports better since deno has support for it now
This commit is contained in:
2
src/artigen/artigen.README.md
Normal file
2
src/artigen/artigen.README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# artigen - The Artificer's Dice and Math Engine
|
||||
artigen is the core engine powering The Artificer.
|
||||
23
src/artigen/counter.ts
Normal file
23
src/artigen/counter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CountDetails, RollSet } from 'artigen/solver.d.ts';
|
||||
|
||||
export const rollCounter = (rollSet: RollSet[]): CountDetails => {
|
||||
const countDetails = {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
};
|
||||
|
||||
rollSet.forEach((roll) => {
|
||||
countDetails.total++;
|
||||
if (roll.critHit) countDetails.successful++;
|
||||
if (roll.critFail) countDetails.failed++;
|
||||
if (roll.rerolled) countDetails.rerolled++;
|
||||
if (roll.dropped) countDetails.dropped++;
|
||||
if (roll.exploding) countDetails.exploded++;
|
||||
});
|
||||
|
||||
return countDetails;
|
||||
};
|
||||
49
src/artigen/managers/queueManager.ts
Normal file
49
src/artigen/managers/queueManager.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import config from '/config.ts';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
import { infoColor2, rollingEmbed } from 'src/commandUtils.ts';
|
||||
import { QueuedRoll } from 'src/mod.d.ts';
|
||||
import utils from 'src/utils.ts';
|
||||
import { currentWorkers, handleRollWorker } from 'artigen/managers/workerManager.ts';
|
||||
|
||||
const rollQueue: Array<QueuedRoll> = [];
|
||||
|
||||
// Runs the roll or queues it depending on how many workers are currently running
|
||||
export const queueRoll = (rq: QueuedRoll) => {
|
||||
if (rq.apiRoll) {
|
||||
handleRollWorker(rq);
|
||||
} else if (!rollQueue.length && currentWorkers < config.limits.maxWorkers) {
|
||||
handleRollWorker(rq);
|
||||
} else {
|
||||
rollQueue.push(rq);
|
||||
rq.dd.m
|
||||
.edit({
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: `${config.name} currently has its hands full and has queued your roll.`,
|
||||
description: `There are currently ${currentWorkers + rollQueue.length} rolls ahead of this roll.
|
||||
|
||||
The results for this roll will replace this message when it is done.`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e: Error) => utils.commonLoggers.messageEditError('rollQueue.ts:197', rq.dd.m, e));
|
||||
}
|
||||
};
|
||||
|
||||
// Checks the queue constantly to make sure the queue stays empty
|
||||
setInterval(() => {
|
||||
log(
|
||||
LT.LOG,
|
||||
`Checking rollQueue for items, rollQueue length: ${rollQueue.length}, currentWorkers: ${currentWorkers}, config.limits.maxWorkers: ${config.limits.maxWorkers}`
|
||||
);
|
||||
if (rollQueue.length && currentWorkers < config.limits.maxWorkers) {
|
||||
const temp = rollQueue.shift();
|
||||
if (temp && !temp.apiRoll) {
|
||||
temp.dd.m.edit(rollingEmbed).catch((e: Error) => utils.commonLoggers.messageEditError('rollQueue.ts:208', temp.dd.m, e));
|
||||
handleRollWorker(temp);
|
||||
} else if (temp && temp.apiRoll) {
|
||||
handleRollWorker(temp);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
187
src/artigen/managers/workerManager.ts
Normal file
187
src/artigen/managers/workerManager.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import config from '/config.ts';
|
||||
import { generateCountDetailsEmbed, generateDMFailed, generateRollEmbed } from 'src/commandUtils.ts';
|
||||
import { QueuedRoll, RollModifiers } from 'src/mod.d.ts';
|
||||
import utils from 'src/utils.ts';
|
||||
import { SolvedRoll } from 'src/artigen/solver.d.ts';
|
||||
import { loggingEnabled } from 'src/artigen/rollUtils.ts';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
import { DEVMODE } from '/flags.ts';
|
||||
import dbClient from 'src/db/client.ts';
|
||||
import stdResp from 'src/endpoints/stdResponses.ts';
|
||||
import { DiscordenoMessage, sendDirectMessage, sendMessage } from '@discordeno';
|
||||
import { queries } from 'src/db/common.ts';
|
||||
|
||||
export let currentWorkers = 0;
|
||||
|
||||
// Handle setting up and calling the rollWorker
|
||||
export const handleRollWorker = (rq: QueuedRoll) => {
|
||||
currentWorkers++;
|
||||
|
||||
// gmModifiers used to create gmEmbed (basically just turn off the gmRoll)
|
||||
const gmModifiers = JSON.parse(JSON.stringify(rq.modifiers));
|
||||
gmModifiers.gmRoll = false;
|
||||
|
||||
const rollWorker = new Worker(new URL('../artigen/rollWorker.ts', import.meta.url).href, { type: 'module' });
|
||||
|
||||
const workerTimeout = setTimeout(async () => {
|
||||
rollWorker.terminate();
|
||||
currentWorkers--;
|
||||
if (rq.apiRoll) {
|
||||
rq.api.resolve(stdResp.RequestTimeout('Roll took too long to process, try breaking roll down into simpler parts'));
|
||||
} else {
|
||||
rq.dd.m
|
||||
.edit({
|
||||
embeds: [
|
||||
(
|
||||
await generateRollEmbed(
|
||||
rq.dd.message.authorId,
|
||||
<SolvedRoll>{
|
||||
error: true,
|
||||
errorCode: 'TooComplex',
|
||||
errorMsg: 'Error: Roll took too long to process, try breaking roll down into simpler parts',
|
||||
},
|
||||
<RollModifiers>{}
|
||||
)
|
||||
).embed,
|
||||
],
|
||||
})
|
||||
.catch((e) => utils.commonLoggers.messageEditError('rollQueue.ts:51', rq.dd.m, e));
|
||||
}
|
||||
}, config.limits.workerTimeout);
|
||||
|
||||
rollWorker.addEventListener('message', async (workerMessage) => {
|
||||
if (workerMessage.data === 'ready') {
|
||||
loggingEnabled && log(LT.LOG, `Sending roll to worker: ${rq.rollCmd}, ${JSON.stringify(rq.modifiers)}`);
|
||||
rollWorker.postMessage({
|
||||
rollCmd: rq.rollCmd,
|
||||
modifiers: rq.modifiers,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let apiErroredOut = false;
|
||||
try {
|
||||
currentWorkers--;
|
||||
clearTimeout(workerTimeout);
|
||||
const returnMsg = workerMessage.data;
|
||||
loggingEnabled && log(LT.LOG, `Roll came back from worker: ${returnMsg.line1.length} |&| ${returnMsg.line2.length} |&| ${returnMsg.line3.length} `);
|
||||
loggingEnabled && log(LT.LOG, `Roll came back from worker: ${returnMsg.line1} |&| ${returnMsg.line2} |&| ${returnMsg.line3} `);
|
||||
const pubEmbedDetails = await generateRollEmbed(rq.apiRoll ? rq.api.userId : rq.dd.message.authorId, returnMsg, rq.modifiers);
|
||||
const gmEmbedDetails = await generateRollEmbed(rq.apiRoll ? rq.api.userId : rq.dd.message.authorId, returnMsg, gmModifiers);
|
||||
const countEmbed = generateCountDetailsEmbed(returnMsg.counts);
|
||||
loggingEnabled && log(LT.LOG, `Embeds are generated: ${JSON.stringify(pubEmbedDetails)} |&| ${JSON.stringify(gmEmbedDetails)}`);
|
||||
|
||||
// If there was an error, report it to the user in hopes that they can determine what they did wrong
|
||||
if (returnMsg.error) {
|
||||
if (rq.apiRoll) {
|
||||
rq.api.resolve(stdResp.InternalServerError(returnMsg.errorMsg));
|
||||
} else {
|
||||
rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] });
|
||||
}
|
||||
|
||||
if (rq.apiRoll || (DEVMODE && config.logRolls)) {
|
||||
// If enabled, log rolls so we can see what went wrong
|
||||
dbClient
|
||||
.execute(queries.insertRollLogCmd(rq.apiRoll ? 1 : 0, 1), [rq.originalCommand, returnMsg.errorCode, rq.apiRoll ? null : rq.dd.m.id])
|
||||
.catch((e) => utils.commonLoggers.dbError('rollQueue.ts:82', 'insert into', e));
|
||||
}
|
||||
} else {
|
||||
let n: DiscordenoMessage | void = undefined;
|
||||
// Determine if we are to send a GM roll or a normal roll
|
||||
if (rq.modifiers.gmRoll) {
|
||||
if (rq.apiRoll) {
|
||||
n = await sendMessage(rq.api.channelId, {
|
||||
content: rq.modifiers.apiWarn,
|
||||
embeds: [pubEmbedDetails.embed],
|
||||
}).catch(() => {
|
||||
apiErroredOut = true;
|
||||
rq.api.resolve(stdResp.InternalServerError('Message failed to send - location 0.'));
|
||||
});
|
||||
} else {
|
||||
// Send the public embed to correct channel
|
||||
rq.dd.m.edit({ embeds: [pubEmbedDetails.embed] });
|
||||
}
|
||||
|
||||
if (!apiErroredOut) {
|
||||
// And message the full details to each of the GMs, alerting roller of every GM that could not be messaged
|
||||
rq.modifiers.gms.forEach(async (gm) => {
|
||||
log(LT.LOG, `Messaging GM ${gm}`);
|
||||
// Attempt to DM the GM and send a warning if it could not DM a GM
|
||||
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
|
||||
embeds: rq.modifiers.count ? [gmEmbedDetails.embed, countEmbed] : [gmEmbedDetails.embed],
|
||||
})
|
||||
.then(async () => {
|
||||
// Check if we need to attach a file and send it after the initial details sent
|
||||
if (gmEmbedDetails.hasAttachment) {
|
||||
await sendDirectMessage(BigInt(gm.substring(2, gm.length - 1)), {
|
||||
file: gmEmbedDetails.attachment,
|
||||
}).catch(() => {
|
||||
if (n && rq.apiRoll) {
|
||||
n.reply(generateDMFailed(gm));
|
||||
} else if (!rq.apiRoll) {
|
||||
rq.dd.message.reply(generateDMFailed(gm));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (rq.apiRoll && n) {
|
||||
n.reply(generateDMFailed(gm));
|
||||
} else if (!rq.apiRoll) {
|
||||
rq.dd.message.reply(generateDMFailed(gm));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Not a gm roll, so just send normal embed to correct channel
|
||||
if (rq.apiRoll) {
|
||||
n = await sendMessage(rq.api.channelId, {
|
||||
content: rq.modifiers.apiWarn,
|
||||
embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed],
|
||||
}).catch(() => {
|
||||
apiErroredOut = true;
|
||||
rq.api.resolve(stdResp.InternalServerError('Message failed to send - location 1.'));
|
||||
});
|
||||
} else {
|
||||
n = await rq.dd.m.edit({
|
||||
embeds: rq.modifiers.count ? [pubEmbedDetails.embed, countEmbed] : [pubEmbedDetails.embed],
|
||||
});
|
||||
}
|
||||
|
||||
if (pubEmbedDetails.hasAttachment && n) {
|
||||
// Attachment requires you to send a new message
|
||||
n.reply({
|
||||
file: pubEmbedDetails.attachment,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (rq.apiRoll && !apiErroredOut) {
|
||||
dbClient
|
||||
.execute(queries.insertRollLogCmd(1, 0), [rq.originalCommand, returnMsg.errorCode, n ? n.id : null])
|
||||
.catch((e) => utils.commonLoggers.dbError('rollQueue.ts:155', 'insert into', e));
|
||||
|
||||
rq.api.resolve(
|
||||
stdResp.OK(
|
||||
JSON.stringify(
|
||||
rq.modifiers.count
|
||||
? {
|
||||
counts: countEmbed,
|
||||
details: pubEmbedDetails,
|
||||
}
|
||||
: {
|
||||
details: pubEmbedDetails,
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log(LT.ERROR, `Unhandled RQ Error: ${JSON.stringify(e)}`);
|
||||
if (rq.apiRoll && !apiErroredOut) {
|
||||
rq.api.resolve(stdResp.InternalServerError(JSON.stringify(e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
401
src/artigen/parser.ts
Normal file
401
src/artigen/parser.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import config from '/config.ts';
|
||||
|
||||
import { RollModifiers } from 'src/mod.d.ts';
|
||||
import { CountDetails, ReturnData, SolvedRoll, SolvedStep } from 'artigen/solver.d.ts';
|
||||
import { compareTotalRolls, compareTotalRollsReverse, escapeCharacters, legalMathOperators, loggingEnabled } from 'artigen/rollUtils.ts';
|
||||
import { formatRoll } from 'artigen/rollFormatter.ts';
|
||||
import { fullSolver } from 'artigen/solver.ts';
|
||||
|
||||
// parseRoll(fullCmd, modifiers)
|
||||
// parseRoll handles converting fullCmd into a computer readable format for processing, and finally executes the solving
|
||||
export const parseRoll = (fullCmd: string, modifiers: RollModifiers): SolvedRoll => {
|
||||
const operators = ['(', ')', '^', '*', '/', '%', '+', '-'];
|
||||
const returnMsg = <SolvedRoll>{
|
||||
error: false,
|
||||
errorCode: '',
|
||||
errorMsg: '',
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
counts: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Whole function lives in a try-catch to allow safe throwing of errors on purpose
|
||||
try {
|
||||
// Split the fullCmd by the command prefix to allow every roll/math op to be handled individually
|
||||
const sepRolls = fullCmd.split(config.prefix);
|
||||
// TODO: HERE for the [[ ]] nesting stuff
|
||||
|
||||
const tempReturnData: ReturnData[] = [];
|
||||
const tempCountDetails: CountDetails[] = [
|
||||
{
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// Loop thru all roll/math ops
|
||||
for (const sepRoll of sepRolls) {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Working ${sepRoll}`);
|
||||
// Split the current iteration on the command postfix to separate the operation to be parsed and the text formatting after the operation
|
||||
const [tempConf, tempFormat] = sepRoll.split(config.postfix);
|
||||
|
||||
// Remove all spaces from the operation config and split it by any operator (keeping the operator in mathConf for fullSolver to do math on)
|
||||
const mathConf: (string | number | SolvedStep)[] = <(string | number | SolvedStep)[]>tempConf.replace(/ /g, '').split(/([-+()*/^]|(?<![d%])%)/g);
|
||||
|
||||
// Verify there are equal numbers of opening and closing parenthesis by adding 1 for opening parens and subtracting 1 for closing parens
|
||||
let parenCnt = 0;
|
||||
mathConf.forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Checking parenthesis balance ${e}`);
|
||||
if (e === '(') {
|
||||
parenCnt++;
|
||||
} else if (e === ')') {
|
||||
parenCnt--;
|
||||
}
|
||||
|
||||
// If parenCnt ever goes below 0, that means too many closing paren appeared before opening parens
|
||||
if (parenCnt < 0) {
|
||||
throw new Error('UnbalancedParens');
|
||||
}
|
||||
});
|
||||
|
||||
// If the parenCnt is not 0, then we do not have balanced parens and need to error out now
|
||||
if (parenCnt !== 0) {
|
||||
throw new Error('UnbalancedParens');
|
||||
}
|
||||
|
||||
// Evaluate all rolls into stepSolve format and all numbers into floats
|
||||
for (let i = 0; i < mathConf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Evaluating rolls into math-able items ${JSON.stringify(mathConf[i])}`);
|
||||
|
||||
const strMathConfI = mathConf[i].toString();
|
||||
|
||||
if (strMathConfI.length === 0) {
|
||||
// If its an empty string, get it out of here
|
||||
mathConf.splice(i, 1);
|
||||
i--;
|
||||
} else if (mathConf[i] == parseFloat(strMathConfI)) {
|
||||
// If its a number, parse the number out
|
||||
mathConf[i] = parseFloat(strMathConfI);
|
||||
} else if (strMathConfI.toLowerCase() === 'e') {
|
||||
// If the operand is the constant e, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Math.E,
|
||||
details: '*e*',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'fart' || strMathConfI.toLowerCase() === '💩') {
|
||||
mathConf[i] = {
|
||||
total: 7,
|
||||
details: '💩',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'sex') {
|
||||
mathConf[i] = {
|
||||
total: 69,
|
||||
details: '( ͡° ͜ʖ ͡°)',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'inf' || strMathConfI.toLowerCase() === 'infinity' || strMathConfI.toLowerCase() === '∞') {
|
||||
// If the operand is the constant Infinity, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Infinity,
|
||||
details: '∞',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'pi' || strMathConfI.toLowerCase() === '𝜋') {
|
||||
// If the operand is the constant pi, create a SolvedStep for it
|
||||
mathConf[i] = {
|
||||
total: Math.PI,
|
||||
details: '𝜋',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
} else if (strMathConfI.toLowerCase() === 'pie') {
|
||||
// If the operand is pie, pi*e, create a SolvedStep for e and pi (and the multiplication symbol between them)
|
||||
mathConf[i] = {
|
||||
total: Math.PI,
|
||||
details: '𝜋',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
mathConf.splice(
|
||||
i + 1,
|
||||
0,
|
||||
...[
|
||||
'*',
|
||||
{
|
||||
total: Math.E,
|
||||
details: '*e*',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
i += 2;
|
||||
} else if (!legalMathOperators.includes(strMathConfI) && legalMathOperators.some((mathOp) => strMathConfI.endsWith(mathOp))) {
|
||||
// Identify when someone does something weird like 4floor(2.5) and split 4 and floor
|
||||
const matchedMathOp = legalMathOperators.filter((mathOp) => strMathConfI.endsWith(mathOp))[0];
|
||||
mathConf[i] = parseFloat(strMathConfI.replace(matchedMathOp, ''));
|
||||
|
||||
mathConf.splice(i + 1, 0, ...['*', matchedMathOp]);
|
||||
i += 2;
|
||||
} else if (![...operators, ...legalMathOperators].includes(strMathConfI)) {
|
||||
// If nothing else has handled it by now, try it as a roll
|
||||
const formattedRoll = formatRoll(strMathConfI, modifiers);
|
||||
mathConf[i] = formattedRoll.solvedStep;
|
||||
tempCountDetails.push(formattedRoll.countDetails);
|
||||
}
|
||||
|
||||
// Identify if we are in a state where the current number is a negative number
|
||||
if (mathConf[i - 1] === '-' && ((!mathConf[i - 2] && mathConf[i - 2] !== 0) || mathConf[i - 2] === '(')) {
|
||||
if (typeof mathConf[i] === 'string') {
|
||||
// Current item is a mathOp, need to insert a "-1 *" before it
|
||||
mathConf.splice(i - 1, 1, ...[parseFloat('-1'), '*']);
|
||||
i += 2;
|
||||
} else {
|
||||
// Handle normally, just set current item to negative
|
||||
if (typeof mathConf[i] === 'number') {
|
||||
mathConf[i] = <number>mathConf[i] * -1;
|
||||
} else {
|
||||
(<SolvedStep>mathConf[i]).total = (<SolvedStep>mathConf[i]).total * -1;
|
||||
(<SolvedStep>mathConf[i]).details = `-${(<SolvedStep>mathConf[i]).details}`;
|
||||
}
|
||||
mathConf.splice(i - 1, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that mathConf is parsed, send it into the solver
|
||||
const tempSolved = fullSolver(mathConf, false);
|
||||
|
||||
// Push all of this step's solved data into the temp array
|
||||
tempReturnData.push({
|
||||
rollTotal: tempSolved.total,
|
||||
rollPostFormat: tempFormat,
|
||||
rollDetails: tempSolved.details,
|
||||
containsCrit: tempSolved.containsCrit,
|
||||
containsFail: tempSolved.containsFail,
|
||||
initConfig: tempConf,
|
||||
});
|
||||
}
|
||||
|
||||
// Parsing/Solving done, time to format the output for Discord
|
||||
|
||||
// Remove any floating spaces from fullCmd
|
||||
if (fullCmd[fullCmd.length - 1] === ' ') {
|
||||
fullCmd = fullCmd.substring(0, fullCmd.length - 1);
|
||||
}
|
||||
|
||||
// Escape any | and ` chars in fullCmd to prevent spoilers and code blocks from acting up
|
||||
fullCmd = escapeCharacters(fullCmd, '|');
|
||||
fullCmd = fullCmd.replace(/`/g, '');
|
||||
|
||||
let line1 = '';
|
||||
let line2 = '';
|
||||
let line3 = '';
|
||||
|
||||
// The ': ' is used by generateRollEmbed to split line 2 up
|
||||
const resultStr = tempReturnData.length > 1 ? 'Results: ' : 'Result: ';
|
||||
|
||||
// If a theoretical roll is requested, mark the output as such, else use default formatting
|
||||
if (modifiers.maxRoll || modifiers.minRoll || modifiers.nominalRoll) {
|
||||
const theoreticalTexts = ['Maximum', 'Minimum', 'Nominal'];
|
||||
const theoreticalBools = [modifiers.maxRoll, modifiers.minRoll, modifiers.nominalRoll];
|
||||
const theoreticalText = theoreticalTexts[theoreticalBools.indexOf(true)];
|
||||
|
||||
line1 = ` requested the Theoretical ${theoreticalText} of:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = `Theoretical ${theoreticalText} ${resultStr}`;
|
||||
} else if (modifiers.order === 'a') {
|
||||
line1 = ` requested the following rolls to be ordered from least to greatest:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = resultStr;
|
||||
tempReturnData.sort(compareTotalRolls);
|
||||
} else if (modifiers.order === 'd') {
|
||||
line1 = ` requested the following rolls to be ordered from greatest to least:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = resultStr;
|
||||
tempReturnData.sort(compareTotalRollsReverse);
|
||||
} else {
|
||||
line1 = ` rolled:\n\`${config.prefix}${fullCmd}\``;
|
||||
line2 = resultStr;
|
||||
}
|
||||
|
||||
// Fill out all of the details and results now
|
||||
tempReturnData.forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Parsing roll ${fullCmd} | Making return text ${JSON.stringify(e)}`);
|
||||
let preFormat = '';
|
||||
let postFormat = '';
|
||||
|
||||
// If the roll contained a crit success or fail, set the formatting around it
|
||||
if (e.containsCrit) {
|
||||
preFormat = `**${preFormat}`;
|
||||
postFormat = `${postFormat}**`;
|
||||
}
|
||||
if (e.containsFail) {
|
||||
preFormat = `__${preFormat}`;
|
||||
postFormat = `${postFormat}__`;
|
||||
}
|
||||
|
||||
// Populate line2 (the results) and line3 (the details) with their data
|
||||
if (modifiers.order === '') {
|
||||
line2 += `${preFormat}${modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}${escapeCharacters(
|
||||
e.rollPostFormat,
|
||||
'|*_~`'
|
||||
)} `;
|
||||
} else {
|
||||
// If order is on, turn rolls into csv without formatting
|
||||
line2 += `${preFormat}${modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}, `;
|
||||
}
|
||||
|
||||
line3 += `\`${e.initConfig}\` = ${e.rollDetails} = ${preFormat}${modifiers.commaTotals ? e.rollTotal.toLocaleString() : e.rollTotal}${postFormat}\n`;
|
||||
});
|
||||
|
||||
// If order is on, remove trailing ", "
|
||||
if (modifiers.order !== '') {
|
||||
line2 = line2.substring(0, line2.length - 2);
|
||||
}
|
||||
|
||||
// Fill in the return block
|
||||
returnMsg.line1 = line1;
|
||||
returnMsg.line2 = line2;
|
||||
returnMsg.line3 = line3;
|
||||
|
||||
// Reduce counts to a single object
|
||||
returnMsg.counts = tempCountDetails.reduce((acc, cnt) => ({
|
||||
total: acc.total + cnt.total,
|
||||
successful: acc.successful + cnt.successful,
|
||||
failed: acc.failed + cnt.failed,
|
||||
rerolled: acc.rerolled + cnt.rerolled,
|
||||
dropped: acc.dropped + cnt.dropped,
|
||||
exploded: acc.exploded + cnt.exploded,
|
||||
}));
|
||||
} catch (e) {
|
||||
const solverError = e as Error;
|
||||
// Welp, the unthinkable happened, we hit an error
|
||||
|
||||
// Split on _ for the error messages that have more info than just their name
|
||||
const errorSplits = solverError.message.split('_');
|
||||
const errorName = errorSplits.shift();
|
||||
const errorDetails = errorSplits.join('_');
|
||||
|
||||
let errorMsg = '';
|
||||
|
||||
// Translate the errorName to a specific errorMsg
|
||||
switch (errorName) {
|
||||
case 'WholeDieCountSizeOnly':
|
||||
errorMsg = 'Error: Die Size and Die Count must be whole numbers';
|
||||
break;
|
||||
case 'YouNeedAD':
|
||||
errorMsg = 'Formatting Error: Missing die size and count config';
|
||||
break;
|
||||
case 'CannotParseDieCount':
|
||||
errorMsg = `Formatting Error: Cannot parse \`${errorDetails}\` as a number`;
|
||||
break;
|
||||
case 'DoubleSeparator':
|
||||
errorMsg = `Formatting Error: \`${errorDetails}\` should only be specified once per roll, remove all but one and repeat roll`;
|
||||
break;
|
||||
case 'FormattingError':
|
||||
errorMsg = 'Formatting Error: Cannot use Keep and Drop at the same time, remove all but one and repeat roll';
|
||||
break;
|
||||
case 'NoMaxWithDash':
|
||||
errorMsg = 'Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct';
|
||||
break;
|
||||
case 'UnknownOperation':
|
||||
errorMsg = `Error: Unknown Operation ${errorDetails}`;
|
||||
if (errorDetails === '-') {
|
||||
errorMsg += '\nNote: Negative numbers are not supported';
|
||||
} else if (errorDetails === ' ') {
|
||||
errorMsg += `\nNote: Every roll must be closed by ${config.postfix}`;
|
||||
}
|
||||
break;
|
||||
case 'NoZerosAllowed':
|
||||
errorMsg = 'Formatting Error: ';
|
||||
switch (errorDetails) {
|
||||
case 'base':
|
||||
errorMsg += 'Die Size and Die Count';
|
||||
break;
|
||||
case 'drop':
|
||||
errorMsg += 'Drop (`d` or `dl`)';
|
||||
break;
|
||||
case 'keep':
|
||||
errorMsg += 'Keep (`k` or `kh`)';
|
||||
break;
|
||||
case 'dropHigh':
|
||||
errorMsg += 'Drop Highest (`dh`)';
|
||||
break;
|
||||
case 'keepLow':
|
||||
errorMsg += 'Keep Lowest (`kl`)';
|
||||
break;
|
||||
case 'reroll':
|
||||
errorMsg += 'Reroll (`r`)';
|
||||
break;
|
||||
case 'critScore':
|
||||
errorMsg += 'Crit Score (`cs`)';
|
||||
break;
|
||||
case 'critFail':
|
||||
errorMsg += 'Crit Fail (`cf`)';
|
||||
break;
|
||||
default:
|
||||
errorMsg += `Unhandled - ${errorDetails}`;
|
||||
break;
|
||||
}
|
||||
errorMsg += ' cannot be zero';
|
||||
break;
|
||||
case 'NoRerollOnAllSides':
|
||||
errorMsg = 'Error: Cannot reroll all sides of a die, must have at least one side that does not get rerolled';
|
||||
break;
|
||||
case 'CritScoreMinGtrMax':
|
||||
errorMsg = 'Formatting Error: CritScore maximum cannot be greater than minimum, check formatting and flip min/max';
|
||||
break;
|
||||
case 'MaxLoopsExceeded':
|
||||
errorMsg = 'Error: Roll is too complex or reaches infinity';
|
||||
break;
|
||||
case 'UnbalancedParens':
|
||||
errorMsg = 'Formatting Error: At least one of the equations contains unbalanced parenthesis';
|
||||
break;
|
||||
case 'EMDASNotNumber':
|
||||
errorMsg = 'Error: One or more operands is not a number';
|
||||
break;
|
||||
case 'ConfWhat':
|
||||
errorMsg = 'Error: Not all values got processed, please report the command used';
|
||||
break;
|
||||
case 'OperatorWhat':
|
||||
errorMsg = 'Error: Something really broke with the Operator, try again';
|
||||
break;
|
||||
case 'OperandNaN':
|
||||
errorMsg = 'Error: One or more operands reached NaN, check input';
|
||||
break;
|
||||
case 'UndefinedStep':
|
||||
errorMsg = 'Error: Roll became undefined, one or more operands are not a roll or a number, check input';
|
||||
break;
|
||||
default:
|
||||
log(LT.ERROR, `Unhandled Parser Error: ${errorName}, ${errorDetails}`);
|
||||
errorMsg = `Unhandled Error: ${solverError.message}\nCheck input and try again, if issue persists, please use \`${config.prefix}report\` to alert the devs of the issue`;
|
||||
break;
|
||||
}
|
||||
|
||||
// Fill in the return block
|
||||
returnMsg.error = true;
|
||||
returnMsg.errorCode = solverError.message;
|
||||
returnMsg.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
return returnMsg;
|
||||
};
|
||||
85
src/artigen/rollFormatter.ts
Normal file
85
src/artigen/rollFormatter.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { roll } from 'artigen/roller.ts';
|
||||
import { rollCounter } from 'artigen/counter.ts';
|
||||
import { RollFormat } from 'artigen/solver.d.ts';
|
||||
import { loggingEnabled } from 'artigen/rollUtils.ts';
|
||||
import { RollModifiers } from 'src/mod.d.ts';
|
||||
|
||||
// formatRoll(rollConf, modifiers) returns one SolvedStep
|
||||
// formatRoll handles creating and formatting the completed rolls into the SolvedStep format
|
||||
export const formatRoll = (rollConf: string, modifiers: RollModifiers): RollFormat => {
|
||||
let tempTotal = 0;
|
||||
let tempDetails = '[';
|
||||
let tempCrit = false;
|
||||
let tempFail = false;
|
||||
|
||||
// Generate the roll, passing flags thru
|
||||
const tempRollSet = roll(rollConf, modifiers);
|
||||
|
||||
// Loop thru all parts of the roll to document everything that was done to create the total roll
|
||||
tempRollSet.forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Formatting roll ${rollConf} | ${JSON.stringify(e)}`);
|
||||
let preFormat = '';
|
||||
let postFormat = '';
|
||||
|
||||
if (!e.dropped && !e.rerolled) {
|
||||
// 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;
|
||||
break;
|
||||
case 'cwod':
|
||||
tempTotal += e.critHit ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
if (e.critHit) {
|
||||
tempCrit = true;
|
||||
}
|
||||
if (e.critFail) {
|
||||
tempFail = true;
|
||||
}
|
||||
}
|
||||
// If the roll was a crit hit or fail, or dropped/rerolled, add the formatting needed
|
||||
if (e.critHit) {
|
||||
// Bold for crit success
|
||||
preFormat = `**${preFormat}`;
|
||||
postFormat = `${postFormat}**`;
|
||||
}
|
||||
if (e.critFail) {
|
||||
// Underline for crit fail
|
||||
preFormat = `__${preFormat}`;
|
||||
postFormat = `${postFormat}__`;
|
||||
}
|
||||
if (e.dropped || e.rerolled) {
|
||||
// Strikethrough for dropped/rerolled rolls
|
||||
preFormat = `~~${preFormat}`;
|
||||
postFormat = `${postFormat}~~`;
|
||||
}
|
||||
if (e.exploding) {
|
||||
// Add ! to indicate the roll came from an explosion
|
||||
postFormat = `!${postFormat}`;
|
||||
}
|
||||
|
||||
// Finally add this to the roll's details
|
||||
tempDetails += `${preFormat}${e.roll}${postFormat} + `;
|
||||
});
|
||||
// After the looping is done, remove the extra " + " from the details and cap it with the closing ]
|
||||
tempDetails = tempDetails.substring(0, tempDetails.length - 3);
|
||||
if (tempRollSet[0]?.type === 'cwod') {
|
||||
tempDetails += `, ${tempRollSet.filter((e) => e.critHit).length} Successes, ${tempRollSet.filter((e) => e.critFail).length} Fails`;
|
||||
}
|
||||
tempDetails += ']';
|
||||
|
||||
return {
|
||||
solvedStep: {
|
||||
total: tempTotal,
|
||||
details: tempDetails,
|
||||
containsCrit: tempCrit,
|
||||
containsFail: tempFail,
|
||||
},
|
||||
countDetails: rollCounter(tempRollSet),
|
||||
};
|
||||
};
|
||||
96
src/artigen/rollUtils.ts
Normal file
96
src/artigen/rollUtils.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
import { RollModifiers } from 'src/mod.d.ts';
|
||||
|
||||
import { DPercentConf, ReturnData, RollSet } from 'artigen/solver.d.ts';
|
||||
|
||||
type MathFunction = (arg: number) => number;
|
||||
export const loggingEnabled = false;
|
||||
export const legalMath: MathFunction[] = [];
|
||||
(Object.getOwnPropertyNames(Math) as (keyof Math)[]).forEach((propName) => {
|
||||
const mathProp = Math[propName];
|
||||
if (typeof mathProp === 'function' && mathProp.length === 1) {
|
||||
legalMath.push(mathProp as MathFunction);
|
||||
}
|
||||
});
|
||||
export const legalMathOperators = legalMath.map((oper) => oper.name);
|
||||
|
||||
// genRoll(size) returns number
|
||||
// genRoll rolls a die of size size and returns the result
|
||||
export const genRoll = (size: number, modifiers: RollModifiers, dPercent: DPercentConf): number => {
|
||||
let result;
|
||||
if (modifiers.maxRoll) {
|
||||
result = size;
|
||||
} else if (modifiers.minRoll) {
|
||||
result = 1;
|
||||
} else {
|
||||
// Math.random * size will return a decimal number between 0 and size (excluding size), so add 1 and floor the result to not get 0 as a result
|
||||
result = modifiers.nominalRoll ? size / 2 + 0.5 : Math.floor(Math.random() * size + 1);
|
||||
}
|
||||
return dPercent.on ? (result - 1) * dPercent.sizeAdjustment : result;
|
||||
};
|
||||
|
||||
// genFateRoll returns -1|0|1
|
||||
// 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) {
|
||||
return 0;
|
||||
} else {
|
||||
const sides = [-1, -1, 0, 0, 1, 1];
|
||||
return sides[genRoll(6, modifiers, <DPercentConf>{ on: false }) - 1];
|
||||
}
|
||||
};
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSets by RollSet.roll
|
||||
export const compareRolls = (a: RollSet, b: RollSet): number => {
|
||||
if (a.roll < b.roll) {
|
||||
return -1;
|
||||
}
|
||||
if (a.roll > b.roll) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const internalCompareTotalRolls = (a: ReturnData, b: ReturnData, dir: 1 | -1): number => {
|
||||
if (a.rollTotal < b.rollTotal) {
|
||||
return -1 * dir;
|
||||
}
|
||||
if (a.rollTotal > b.rollTotal) {
|
||||
return 1 * dir;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// compareTotalRolls(a, b) returns -1|0|1
|
||||
// compareTotalRolls is used to order an array of RollSets by RollSet.roll
|
||||
export const compareTotalRolls = (a: ReturnData, b: ReturnData): number => internalCompareTotalRolls(a, b, 1);
|
||||
|
||||
// compareTotalRollsReverse(a, b) returns 1|0|-1
|
||||
// compareTotalRollsReverse is used to order an array of RollSets by RollSet.roll reversed
|
||||
export const compareTotalRollsReverse = (a: ReturnData, b: ReturnData): number => internalCompareTotalRolls(a, b, -1);
|
||||
|
||||
// compareRolls(a, b) returns -1|0|1
|
||||
// compareRolls is used to order an array of RollSets by RollSet.origIdx
|
||||
export const compareOrigIdx = (a: RollSet, b: RollSet): number => {
|
||||
if (a.origIdx < b.origIdx) {
|
||||
return -1;
|
||||
}
|
||||
if (a.origIdx > b.origIdx) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// escapeCharacters(str, esc) returns str
|
||||
// escapeCharacters escapes all characters listed in esc
|
||||
export const escapeCharacters = (str: string, esc: string): string => {
|
||||
// Loop thru each esc char one at a time
|
||||
for (const e of esc) {
|
||||
loggingEnabled && log(LT.LOG, `Escaping character ${e} | ${str}, ${esc}`);
|
||||
// Create a new regex to look for that char that needs replaced and escape it
|
||||
const tempRgx = new RegExp(`[${e}]`, 'g');
|
||||
str = str.replace(tempRgx, `\\${e}`);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
33
src/artigen/rollWorker.ts
Normal file
33
src/artigen/rollWorker.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { closeLog, initLog } from '@Log4Deno';
|
||||
import { DEBUG } from '/flags.ts';
|
||||
import { parseRoll } from 'artigen/parser.ts';
|
||||
import { loggingEnabled } from 'artigen/rollUtils.ts';
|
||||
|
||||
loggingEnabled && initLog('logs/worker', DEBUG);
|
||||
|
||||
// Alert rollQueue that this worker is ready
|
||||
self.postMessage('ready');
|
||||
|
||||
// Handle the roll
|
||||
self.onmessage = async (e) => {
|
||||
const payload = e.data;
|
||||
const returnMsg = parseRoll(payload.rollCmd, payload.modifiers) || {
|
||||
error: true,
|
||||
errorCode: 'EmptyMessage',
|
||||
errorMsg: 'Error: Empty message',
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
counts: {
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
rerolled: 0,
|
||||
dropped: 0,
|
||||
exploded: 0,
|
||||
},
|
||||
};
|
||||
self.postMessage(returnMsg);
|
||||
loggingEnabled && (await closeLog());
|
||||
self.close();
|
||||
};
|
||||
766
src/artigen/roller.ts
Normal file
766
src/artigen/roller.ts
Normal file
@@ -0,0 +1,766 @@
|
||||
import config from '/config.ts';
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { RollConf, RollSet, RollType } from 'artigen/solver.d.ts';
|
||||
import { compareOrigIdx, compareRolls, genFateRoll, genRoll, loggingEnabled } from 'artigen/rollUtils.ts';
|
||||
import { RollModifiers } from 'src/mod.d.ts';
|
||||
|
||||
// Call with loopCountCheck(++loopCount);
|
||||
// Will ensure if maxLoops is 10, 10 loops will be allowed, 11 will not.
|
||||
const loopCountCheck = (loopCount: number): void => {
|
||||
if (loopCount > config.limits.maxLoops) {
|
||||
throw new Error('MaxLoopsExceeded');
|
||||
}
|
||||
};
|
||||
|
||||
const throwDoubleSepError = (sep: string): void => {
|
||||
throw new Error(`DoubleSeparator_${sep}`);
|
||||
};
|
||||
|
||||
// roll(rollStr, modifiers) returns RollSet
|
||||
// roll parses and executes the rollStr
|
||||
export const roll = (rollStr: string, modifiers: RollModifiers): RollSet[] => {
|
||||
/* Roll Capabilities
|
||||
* Deciphers and rolls a single dice roll set
|
||||
*
|
||||
* Check the README.md of this project for details on the roll options. I gave up trying to keep three places updated at once.
|
||||
*/
|
||||
|
||||
// Begin counting the number of loops to prevent from getting into an infinite loop
|
||||
let loopCount = 0;
|
||||
|
||||
// Make entire roll lowercase for ease of parsing
|
||||
rollStr = rollStr.toLowerCase();
|
||||
|
||||
// Split the roll on the die size (and the drop if its there)
|
||||
const dPts = rollStr.split('d');
|
||||
|
||||
// Initialize the configuration to store the parsed data
|
||||
let rollType: RollType = '';
|
||||
const rollConf: RollConf = {
|
||||
dieCount: 0,
|
||||
dieSize: 0,
|
||||
dPercent: {
|
||||
on: false,
|
||||
sizeAdjustment: 0,
|
||||
critVal: 0,
|
||||
},
|
||||
drop: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
keep: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
dropHigh: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
keepLow: {
|
||||
on: false,
|
||||
count: 0,
|
||||
},
|
||||
reroll: {
|
||||
on: false,
|
||||
once: false,
|
||||
nums: <number[]>[],
|
||||
},
|
||||
critScore: {
|
||||
on: false,
|
||||
range: <number[]>[],
|
||||
},
|
||||
critFail: {
|
||||
on: false,
|
||||
range: <number[]>[],
|
||||
},
|
||||
exploding: {
|
||||
on: false,
|
||||
once: false,
|
||||
compounding: false,
|
||||
penetrating: false,
|
||||
nums: <number[]>[],
|
||||
},
|
||||
};
|
||||
|
||||
// If the dPts is not long enough, throw error
|
||||
if (dPts.length < 2) {
|
||||
throw new Error('YouNeedAD');
|
||||
}
|
||||
|
||||
// Fill out the die count, first item will either be an int or empty string, short circuit execution will take care of replacing the empty string with a 1
|
||||
const rawDC = dPts.shift() || '1';
|
||||
if (rawDC.includes('.')) {
|
||||
throw new Error('WholeDieCountSizeOnly');
|
||||
} else if (rawDC.match(/\D/)) {
|
||||
throw new Error(`CannotParseDieCount_${rawDC}`);
|
||||
}
|
||||
const tempDC = rawDC.replace(/\D/g, '');
|
||||
// Rejoin all remaining parts
|
||||
let remains = dPts.join('d');
|
||||
|
||||
// Manual Parsing for custom roll types
|
||||
let manualParse = false;
|
||||
if (rawDC.endsWith('cwo')) {
|
||||
// CWOD dice parsing
|
||||
rollType = 'cwod';
|
||||
manualParse = true;
|
||||
|
||||
// Get CWOD parts, setting count and getting difficulty
|
||||
const cwodParts = rollStr.split('cwod');
|
||||
rollConf.dieCount = parseInt(cwodParts[0] || '1');
|
||||
rollConf.dieSize = 10;
|
||||
|
||||
// Use critScore to set the difficulty
|
||||
rollConf.critScore.on = true;
|
||||
const difficulty = parseInt(cwodParts[1] || '10');
|
||||
for (let i = difficulty; i <= rollConf.dieSize; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling cwod ${rollStr} | Parsing difficulty ${i}`);
|
||||
rollConf.critScore.range.push(i);
|
||||
}
|
||||
} else if (rawDC.endsWith('ova')) {
|
||||
// OVA dice parsing
|
||||
rollType = 'ova';
|
||||
manualParse = true;
|
||||
|
||||
// Get OVA parts, setting count and getting difficulty
|
||||
const ovaParts = rollStr.split('ovad');
|
||||
const ovaPart1 = ovaParts[1] || '6';
|
||||
if (ovaPart1.search(/\d+\.\d/) === 0) {
|
||||
throw new Error('WholeDieCountSizeOnly');
|
||||
}
|
||||
rollConf.dieCount = parseInt(ovaParts[0] || '1');
|
||||
rollConf.dieSize = parseInt(ovaPart1);
|
||||
} else if (remains.startsWith('f')) {
|
||||
// fate dice setup
|
||||
rollType = 'fate';
|
||||
rollConf.dieCount = parseInt(tempDC);
|
||||
// dieSize set to 1 as 1 is max face value, a six sided die is used internally
|
||||
rollConf.dieSize = 1;
|
||||
|
||||
// remove F from the remains
|
||||
remains = remains.slice(1);
|
||||
} else {
|
||||
// roll20 dice setup
|
||||
rollType = 'roll20';
|
||||
rollConf.dieCount = parseInt(tempDC);
|
||||
|
||||
// Finds the end of the die size/beginning of the additional options
|
||||
let afterDieIdx = dPts[0].search(/[^%\d]/);
|
||||
if (afterDieIdx === -1) {
|
||||
afterDieIdx = dPts[0].length;
|
||||
}
|
||||
|
||||
// Get the die size out of the remains and into the rollConf
|
||||
const rawDS = remains.slice(0, afterDieIdx);
|
||||
remains = remains.slice(afterDieIdx);
|
||||
|
||||
if (rawDS.startsWith('%')) {
|
||||
rollConf.dieSize = 10;
|
||||
rollConf.dPercent.on = true;
|
||||
const percentCount = rawDS.match(/%/g)?.length ?? 1;
|
||||
rollConf.dPercent.sizeAdjustment = Math.pow(10, percentCount - 1);
|
||||
rollConf.dPercent.critVal = Math.pow(10, percentCount) - rollConf.dPercent.sizeAdjustment;
|
||||
} else {
|
||||
rollConf.dieSize = parseInt(rawDS);
|
||||
}
|
||||
|
||||
if (remains.search(/\.\d/) === 0) {
|
||||
throw new Error('WholeDieCountSizeOnly');
|
||||
}
|
||||
}
|
||||
|
||||
if (!rollConf.dieCount || !rollConf.dieSize) {
|
||||
throw new Error('YouNeedAD');
|
||||
}
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`);
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`);
|
||||
|
||||
// Finish parsing the roll
|
||||
if (!manualParse && remains.length > 0) {
|
||||
// Determine if the first item is a drop, and if it is, add the d back in
|
||||
if (remains.search(/\D/) !== 0 || remains.indexOf('l') === 0 || remains.indexOf('h') === 0) {
|
||||
remains = `d${remains}`;
|
||||
}
|
||||
|
||||
// Loop until all remaining args are parsed
|
||||
while (remains.length > 0) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing remains ${remains}`);
|
||||
// Find the next number in the remains to be able to cut out the rule name
|
||||
let afterSepIdx = remains.search(/\d/);
|
||||
if (afterSepIdx < 0) {
|
||||
afterSepIdx = remains.length;
|
||||
}
|
||||
// Save the rule name to tSep and remove it from remains
|
||||
const tSep = remains.slice(0, afterSepIdx);
|
||||
remains = remains.slice(afterSepIdx);
|
||||
// Find the next non-number in the remains to be able to cut out the count/num
|
||||
let afterNumIdx = remains.search(/\D/);
|
||||
if (afterNumIdx < 0) {
|
||||
afterNumIdx = remains.length;
|
||||
}
|
||||
// Save the count/num to tNum leaving it in remains for the time being
|
||||
const tNum = parseInt(remains.slice(0, afterNumIdx));
|
||||
|
||||
// Switch on rule name
|
||||
switch (tSep) {
|
||||
case 'dl':
|
||||
case 'd':
|
||||
if (rollConf.drop.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Drop (Lowest)
|
||||
rollConf.drop.on = true;
|
||||
rollConf.drop.count = tNum;
|
||||
break;
|
||||
case 'kh':
|
||||
case 'k':
|
||||
if (rollConf.keep.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Keep (Highest)
|
||||
rollConf.keep.on = true;
|
||||
rollConf.keep.count = tNum;
|
||||
break;
|
||||
case 'dh':
|
||||
if (rollConf.dropHigh.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Drop (Highest)
|
||||
rollConf.dropHigh.on = true;
|
||||
rollConf.dropHigh.count = tNum;
|
||||
break;
|
||||
case 'kl':
|
||||
if (rollConf.keepLow.on) {
|
||||
// Ensure we do not override existing settings
|
||||
throwDoubleSepError(tSep);
|
||||
}
|
||||
// Configure Keep (Lowest)
|
||||
rollConf.keepLow.on = true;
|
||||
rollConf.keepLow.count = tNum;
|
||||
break;
|
||||
case 'ro':
|
||||
case 'ro=':
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro/ro= functions the same as r/r= in this context
|
||||
case 'r':
|
||||
case 'r=':
|
||||
// Configure Reroll (this can happen multiple times)
|
||||
rollConf.reroll.on = true;
|
||||
!rollConf.reroll.nums.includes(tNum) && rollConf.reroll.nums.push(tNum);
|
||||
break;
|
||||
case 'ro>':
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro> functions the same as r> in this context
|
||||
case 'r>':
|
||||
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.reroll.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing r> ${i}`);
|
||||
!rollConf.reroll.nums.includes(i) && rollConf.reroll.nums.push(i);
|
||||
}
|
||||
break;
|
||||
case 'ro<':
|
||||
rollConf.reroll.once = true;
|
||||
// falls through as ro< functions the same as r< in this context
|
||||
case 'r<':
|
||||
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.reroll.on = true;
|
||||
for (let i = 1; i <= tNum; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing r< ${i}`);
|
||||
!rollConf.reroll.nums.includes(i) && rollConf.reroll.nums.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cs':
|
||||
case 'cs=':
|
||||
// Configure CritScore for one number (this can happen multiple times)
|
||||
rollConf.critScore.on = true;
|
||||
!rollConf.critScore.range.includes(tNum) && rollConf.critScore.range.push(tNum);
|
||||
break;
|
||||
case 'cs>':
|
||||
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critScore.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing cs> ${i}`);
|
||||
!rollConf.critScore.range.includes(i) && rollConf.critScore.range.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cs<':
|
||||
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critScore.on = true;
|
||||
for (let i = 0; i <= tNum; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing cs< ${i}`);
|
||||
!rollConf.critScore.range.includes(i) && rollConf.critScore.range.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cf':
|
||||
case 'cf=':
|
||||
// Configure CritFail for one number (this can happen multiple times)
|
||||
rollConf.critFail.on = true;
|
||||
!rollConf.critFail.range.includes(tNum) && rollConf.critFail.range.push(tNum);
|
||||
break;
|
||||
case 'cf>':
|
||||
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critFail.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing cf> ${i}`);
|
||||
!rollConf.critFail.range.includes(i) && rollConf.critFail.range.push(i);
|
||||
}
|
||||
break;
|
||||
case 'cf<':
|
||||
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.critFail.on = true;
|
||||
for (let i = 0; i <= tNum; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing cf< ${i}`);
|
||||
!rollConf.critFail.range.includes(i) && rollConf.critFail.range.push(i);
|
||||
}
|
||||
break;
|
||||
case '!':
|
||||
case '!o':
|
||||
case '!p':
|
||||
case '!!':
|
||||
// Configure Exploding
|
||||
rollConf.exploding.on = true;
|
||||
if (afterNumIdx > 0) {
|
||||
// User gave a number to explode on, save it
|
||||
!rollConf.exploding.nums.includes(tNum) && rollConf.exploding.nums.push(tNum);
|
||||
} else {
|
||||
// User did not give number, use cs
|
||||
afterNumIdx = 1;
|
||||
}
|
||||
break;
|
||||
case '!=':
|
||||
case '!o=':
|
||||
case '!p=':
|
||||
case '!!=':
|
||||
// Configure Exploding (this can happen multiple times)
|
||||
rollConf.exploding.on = true;
|
||||
!rollConf.exploding.nums.includes(tNum) && rollConf.exploding.nums.push(tNum);
|
||||
break;
|
||||
case '!>':
|
||||
case '!o>':
|
||||
case '!p>':
|
||||
case '!!>':
|
||||
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.exploding.on = true;
|
||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing !> ${i}`);
|
||||
!rollConf.exploding.nums.includes(i) && rollConf.exploding.nums.push(i);
|
||||
}
|
||||
break;
|
||||
case '!<':
|
||||
case '!o<':
|
||||
case '!p<':
|
||||
case '!!<':
|
||||
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||
rollConf.exploding.on = true;
|
||||
for (let i = 1; i <= tNum; i++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Parsing !< ${i}`);
|
||||
!rollConf.exploding.nums.includes(i) && rollConf.exploding.nums.push(i);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Throw error immediately if unknown op is encountered
|
||||
throw new Error(`UnknownOperation_${tSep}`);
|
||||
}
|
||||
|
||||
// Exploding flags get set in their own switch statement to avoid weird duplicated code
|
||||
switch (tSep) {
|
||||
case '!o':
|
||||
case '!o=':
|
||||
case '!o>':
|
||||
case '!o<':
|
||||
rollConf.exploding.once = true;
|
||||
break;
|
||||
case '!p':
|
||||
case '!p=':
|
||||
case '!p>':
|
||||
case '!p<':
|
||||
rollConf.exploding.penetrating = true;
|
||||
break;
|
||||
case '!!':
|
||||
case '!!=':
|
||||
case '!!>':
|
||||
case '!!<':
|
||||
rollConf.exploding.compounding = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Finally slice off everything else parsed this loop
|
||||
remains = remains.slice(afterNumIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the parse, throwing errors for every invalid config
|
||||
if (rollConf.dieCount < 0) {
|
||||
throw new Error('NoZerosAllowed_base');
|
||||
}
|
||||
if (rollConf.dieCount === 0 || rollConf.dieSize === 0) {
|
||||
throw new Error('NoZerosAllowed_base');
|
||||
}
|
||||
|
||||
// Since only one drop or keep option can be active, count how many are active to throw the right error
|
||||
let dkdkCnt = 0;
|
||||
[rollConf.drop.on, rollConf.keep.on, rollConf.dropHigh.on, rollConf.keepLow.on].forEach((e) => {
|
||||
loggingEnabled && log(LT.LOG, `Handling ${rollType} ${rollStr} | Checking if drop/keep is on ${e}`);
|
||||
if (e) {
|
||||
dkdkCnt++;
|
||||
}
|
||||
});
|
||||
if (dkdkCnt > 1) {
|
||||
throw new Error('FormattingError_dk');
|
||||
}
|
||||
|
||||
if (rollConf.drop.on && rollConf.drop.count === 0) {
|
||||
throw new Error('NoZerosAllowed_drop');
|
||||
}
|
||||
if (rollConf.keep.on && rollConf.keep.count === 0) {
|
||||
throw new Error('NoZerosAllowed_keep');
|
||||
}
|
||||
if (rollConf.dropHigh.on && rollConf.dropHigh.count === 0) {
|
||||
throw new Error('NoZerosAllowed_dropHigh');
|
||||
}
|
||||
if (rollConf.keepLow.on && rollConf.keepLow.count === 0) {
|
||||
throw new Error('NoZerosAllowed_keepLow');
|
||||
}
|
||||
if (rollConf.reroll.on && !rollConf.dPercent.on && rollConf.reroll.nums.includes(0)) {
|
||||
throw new Error('NoZerosAllowed_reroll');
|
||||
}
|
||||
|
||||
// Filter rollConf num lists to only include valid numbers
|
||||
const validNumFilter = (curNum: number) => curNum <= rollConf.dieSize && curNum > (rollConf.dPercent.on ? -1 : 0);
|
||||
rollConf.reroll.nums = rollConf.reroll.nums.filter(validNumFilter);
|
||||
rollConf.critScore.range = rollConf.critScore.range.filter(validNumFilter);
|
||||
rollConf.critFail.range = rollConf.critFail.range.filter(validNumFilter);
|
||||
rollConf.exploding.nums = rollConf.exploding.nums.filter(validNumFilter);
|
||||
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.length === rollConf.dieSize) {
|
||||
throw new Error('NoRerollOnAllSides');
|
||||
}
|
||||
|
||||
// Roll the roll
|
||||
const rollSet = [];
|
||||
/* Roll will contain objects of the following format:
|
||||
* {
|
||||
* origIdx: 0,
|
||||
* roll: 0,
|
||||
* dropped: false,
|
||||
* rerolled: false,
|
||||
* exploding: false,
|
||||
* critHit: false,
|
||||
* critFail: false
|
||||
* }
|
||||
*
|
||||
* Each of these is defined as following:
|
||||
* {
|
||||
* origIdx: The original index of the roll
|
||||
* roll: The resulting roll on this die in the set
|
||||
* dropped: This die is to be dropped as it was one of the dy lowest dice
|
||||
* rerolled: This die has been rerolled as it matched rz, it is replaced by the very next die in the set
|
||||
* exploding: This die was rolled as the previous die exploded (was a crit hit)
|
||||
* critHit: This die matched csq[-u], max die value used if cs not used
|
||||
* critFail: This die rolled a nat 1, a critical failure
|
||||
* }
|
||||
*/
|
||||
|
||||
// Initialize a template rollSet to copy multiple times
|
||||
const getTemplateRoll = (): RollSet => ({
|
||||
type: rollType,
|
||||
origIdx: 0,
|
||||
roll: 0,
|
||||
dropped: false,
|
||||
rerolled: false,
|
||||
exploding: false,
|
||||
critHit: false,
|
||||
critFail: false,
|
||||
});
|
||||
|
||||
// Initial rolling, not handling reroll or exploding here
|
||||
for (let i = 0; i < rollConf.dieCount; i++) {
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Initial rolling ${i} of ${JSON.stringify(rollConf)}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
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
|
||||
rolling.roll = rollType === 'fate' ? genFateRoll(modifiers) : genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
|
||||
// Set origIdx of roll
|
||||
rolling.origIdx = i;
|
||||
|
||||
// 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(rolling.roll)) {
|
||||
rolling.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
rolling.critHit = rolling.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : rollConf.dieSize);
|
||||
}
|
||||
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.includes(rolling.roll)) {
|
||||
rolling.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
if (rollType === 'fate') {
|
||||
rolling.critFail = rolling.roll === -1;
|
||||
} else {
|
||||
rolling.critFail = rolling.roll === (rollConf.dPercent.on ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Push the newly created roll and loop again
|
||||
rollSet.push(rolling);
|
||||
}
|
||||
|
||||
// If needed, handle rerolling and exploding dice now
|
||||
if (rollConf.reroll.on || rollConf.exploding.on) {
|
||||
let minMaxOverride = 0;
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Handling rerolling and exploding ${JSON.stringify(rollSet[i])}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
// This big boolean statement first checks if reroll is on, if the roll is within the reroll range, and finally if ro is ON, make sure we haven't already rerolled the roll
|
||||
if (rollConf.reroll.on && rollConf.reroll.nums.includes(rollSet[i].roll) && (!rollConf.reroll.once || !rollSet[i ? i - 1 : i].rerolled)) {
|
||||
// If we need to reroll this roll, flag its been replaced and...
|
||||
rollSet[i].rerolled = true;
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
const newReroll = getTemplateRoll();
|
||||
if (modifiers.maxRoll && !minMaxOverride) {
|
||||
// If maximizeRoll is on and we've entered the reroll code, dieSize is not allowed, determine the next best option and always return that
|
||||
mmMaxLoop: for (let m = rollConf.dieSize - 1; m > 0; m--) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
if (!rollConf.reroll.nums.includes(m)) {
|
||||
minMaxOverride = m;
|
||||
break mmMaxLoop;
|
||||
}
|
||||
}
|
||||
} else if (modifiers.minRoll && !minMaxOverride) {
|
||||
// If minimizeRoll is on and we've entered the reroll code, 1 is not allowed, determine the next best option and always return that
|
||||
mmMinLoop: for (let m = rollConf.dPercent.on ? 1 : 2; m <= rollConf.dieSize; m++) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
if (!rollConf.reroll.nums.includes(m)) {
|
||||
minMaxOverride = m;
|
||||
break mmMinLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers.maxRoll || modifiers.minRoll) {
|
||||
newReroll.roll = minMaxOverride;
|
||||
} else {
|
||||
// If nominalRoll is on, set the roll to the average roll of dieSize, otherwise generate a new random roll
|
||||
newReroll.roll = genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
|
||||
}
|
||||
|
||||
// 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(newReroll.roll)) {
|
||||
newReroll.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
newReroll.critHit = newReroll.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : rollConf.dieSize);
|
||||
}
|
||||
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.includes(newReroll.roll)) {
|
||||
newReroll.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
newReroll.critFail = newReroll.roll === (rollConf.dPercent.on ? 0 : 1);
|
||||
}
|
||||
|
||||
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||
rollSet.splice(i + 1, 0, newReroll);
|
||||
} else if (
|
||||
rollConf.exploding.on &&
|
||||
!rollSet[i].rerolled &&
|
||||
(rollConf.exploding.nums.length ? rollConf.exploding.nums.includes(rollSet[i].roll) : rollSet[i].critHit) &&
|
||||
(!rollConf.exploding.once || !rollSet[i].exploding)
|
||||
) {
|
||||
// If we have exploding.nums set, use those to determine the exploding range, and make sure if !o is on, make sure we don't repeatedly explode
|
||||
// If it exploded, we keep both, so no flags need to be set
|
||||
|
||||
// Copy the template to fill out for this iteration
|
||||
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
|
||||
newExplodingRoll.roll = genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
|
||||
// Always mark this roll as exploding
|
||||
newExplodingRoll.exploding = true;
|
||||
|
||||
// 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(newExplodingRoll.roll)) {
|
||||
newExplodingRoll.critHit = true;
|
||||
} else if (!rollConf.critScore.on) {
|
||||
newExplodingRoll.critHit = newExplodingRoll.roll === (rollConf.dPercent.on ? rollConf.dPercent.critVal : rollConf.dieSize);
|
||||
}
|
||||
// If critFail arg is on, check if the roll should be a fail, if its off, check if the roll matches 1
|
||||
if (rollConf.critFail.on && rollConf.critFail.range.includes(newExplodingRoll.roll)) {
|
||||
newExplodingRoll.critFail = true;
|
||||
} else if (!rollConf.critFail.on) {
|
||||
newExplodingRoll.critFail = newExplodingRoll.roll === (rollConf.dPercent.on ? 0 : 1);
|
||||
}
|
||||
|
||||
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||
rollSet.splice(i + 1, 0, newExplodingRoll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If penetrating is on, do the decrements
|
||||
if (rollConf.exploding.penetrating) {
|
||||
for (const penRoll of rollSet) {
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Handling penetrating explosions ${JSON.stringify(penRoll)}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
// If the die was from an explosion, decrement it by one
|
||||
if (penRoll.exploding) {
|
||||
penRoll.roll--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle compounding explosions
|
||||
if (rollConf.exploding.compounding) {
|
||||
for (let i = 0; i < rollSet.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Handling compounding explosions ${JSON.stringify(rollSet[i])}`);
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
// Compound the exploding rolls, including the exploding flag and
|
||||
if (rollSet[i].exploding) {
|
||||
rollSet[i - 1].roll = rollSet[i - 1].roll + rollSet[i].roll;
|
||||
rollSet[i - 1].exploding = true;
|
||||
rollSet[i - 1].critFail = rollSet[i - 1].critFail || rollSet[i].critFail;
|
||||
rollSet[i - 1].critHit = rollSet[i - 1].critHit || rollSet[i].critHit;
|
||||
rollSet.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we need to handle the drop/keep flags
|
||||
if (dkdkCnt > 0) {
|
||||
// Count how many rerolled dice there are if the reroll flag was on
|
||||
let rerollCount = 0;
|
||||
if (rollConf.reroll.on) {
|
||||
for (let j = 0; j < rollSet.length; j++) {
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Setting originalIdx on ${JSON.stringify(rollSet[j])}`);
|
||||
rollSet[j].origIdx = j;
|
||||
|
||||
if (rollSet[j].rerolled) {
|
||||
rerollCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Order the rolls from least to greatest (by RollSet.roll)
|
||||
rollSet.sort(compareRolls);
|
||||
|
||||
// Determine how many valid rolls there are to drop from (may not be equal to dieCount due to exploding)
|
||||
const validRolls = rollSet.length - rerollCount;
|
||||
let dropCount = 0;
|
||||
|
||||
// For normal drop and keep, simple subtraction is enough to determine how many to drop
|
||||
// Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop
|
||||
if (rollConf.drop.on) {
|
||||
dropCount = rollConf.drop.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
} else if (rollConf.keep.on) {
|
||||
dropCount = validRolls - rollConf.keep.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
} // For inverted drop and keep, order must be flipped to greatest to least before the simple subtraction can determine how many to drop
|
||||
// Protections are in to prevent the dropCount from going below 0 or more than the valid rolls to drop
|
||||
else if (rollConf.dropHigh.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = rollConf.dropHigh.count;
|
||||
if (dropCount > validRolls) {
|
||||
dropCount = validRolls;
|
||||
}
|
||||
} else if (rollConf.keepLow.on) {
|
||||
rollSet.reverse();
|
||||
dropCount = validRolls - rollConf.keepLow.count;
|
||||
if (dropCount < 0) {
|
||||
dropCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Now its time to drop all dice needed
|
||||
let i = 0;
|
||||
while (dropCount > 0 && i < rollSet.length) {
|
||||
// If loopCount gets too high, stop trying to calculate infinity
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | Dropping dice ${dropCount} ${JSON.stringify(rollSet[i])}`);
|
||||
// Skip all rolls that were rerolled
|
||||
if (!rollSet[i].rerolled) {
|
||||
rollSet[i].dropped = true;
|
||||
dropCount--;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// Finally, return the rollSet to its original order
|
||||
rollSet.sort(compareOrigIdx);
|
||||
}
|
||||
|
||||
// Handle OVA dropping/keeping
|
||||
if (rollType === 'ova') {
|
||||
// Make "empty" vals array to easily sum up which die value is the greatest
|
||||
const rollVals: Array<number> = new Array(rollConf.dieSize).fill(0);
|
||||
|
||||
// Sum up all rolls
|
||||
for (const ovaRoll of rollSet) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled && log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | incrementing rollVals for ${ovaRoll}`);
|
||||
rollVals[ovaRoll.roll - 1] += ovaRoll.roll;
|
||||
}
|
||||
|
||||
// Find max value, using lastIndexOf to use the greatest die size max in case of duplicate maximums
|
||||
const maxRoll = rollVals.lastIndexOf(Math.max(...rollVals)) + 1;
|
||||
|
||||
// Drop all dice that are not a part of the max
|
||||
for (const ovaRoll of rollSet) {
|
||||
loopCountCheck(++loopCount);
|
||||
|
||||
loggingEnabled &&
|
||||
log(LT.LOG, `${loopCount} Handling ${rollType} ${rollStr} | checking if this roll should be dropped ${ovaRoll.roll} | to keep: ${maxRoll}`);
|
||||
if (ovaRoll.roll !== maxRoll) {
|
||||
ovaRoll.dropped = true;
|
||||
ovaRoll.critFail = false;
|
||||
ovaRoll.critHit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rollSet;
|
||||
};
|
||||
108
src/artigen/solver.d.ts
vendored
Normal file
108
src/artigen/solver.d.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
// Available Roll Types
|
||||
export type RollType = '' | 'roll20' | 'fate' | 'cwod' | 'ova';
|
||||
|
||||
// RollSet is used to preserve all information about a calculated roll
|
||||
export type RollSet = {
|
||||
type: RollType;
|
||||
origIdx: number;
|
||||
roll: number;
|
||||
dropped: boolean;
|
||||
rerolled: boolean;
|
||||
exploding: boolean;
|
||||
critHit: boolean;
|
||||
critFail: boolean;
|
||||
};
|
||||
|
||||
// SolvedStep is used to preserve information while math is being performed on the roll
|
||||
export type SolvedStep = {
|
||||
total: number;
|
||||
details: string;
|
||||
containsCrit: boolean;
|
||||
containsFail: boolean;
|
||||
};
|
||||
|
||||
// ReturnData is the temporary internal type used before getting turned into SolvedRoll
|
||||
export type ReturnData = {
|
||||
rollTotal: number;
|
||||
rollPostFormat: string;
|
||||
rollDetails: string;
|
||||
containsCrit: boolean;
|
||||
containsFail: boolean;
|
||||
initConfig: string;
|
||||
};
|
||||
|
||||
// CountDetails is the object holding the count data for creating the Count Embed
|
||||
export type CountDetails = {
|
||||
total: number;
|
||||
successful: number;
|
||||
failed: number;
|
||||
rerolled: number;
|
||||
dropped: number;
|
||||
exploded: number;
|
||||
};
|
||||
|
||||
// RollFormat is the return structure for the rollFormatter
|
||||
export type RollFormat = {
|
||||
solvedStep: SolvedStep;
|
||||
countDetails: CountDetails;
|
||||
};
|
||||
|
||||
// SolvedRoll is the complete solved and formatted roll, or the error said roll created
|
||||
export type SolvedRoll = {
|
||||
error: boolean;
|
||||
errorMsg: string;
|
||||
errorCode: string;
|
||||
line1: string;
|
||||
line2: string;
|
||||
line3: string;
|
||||
counts: CountDetails;
|
||||
};
|
||||
|
||||
export type DPercentConf = {
|
||||
on: boolean;
|
||||
sizeAdjustment: number;
|
||||
critVal: number;
|
||||
};
|
||||
|
||||
// RollConf is used by the roll20 setup
|
||||
export type RollConf = {
|
||||
dieCount: number;
|
||||
dieSize: number;
|
||||
dPercent: DPercentConf;
|
||||
drop: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
keep: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
dropHigh: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
keepLow: {
|
||||
on: boolean;
|
||||
count: number;
|
||||
};
|
||||
reroll: {
|
||||
on: boolean;
|
||||
once: boolean;
|
||||
nums: number[];
|
||||
};
|
||||
critScore: {
|
||||
on: boolean;
|
||||
range: number[];
|
||||
};
|
||||
critFail: {
|
||||
on: boolean;
|
||||
range: number[];
|
||||
};
|
||||
exploding: {
|
||||
on: boolean;
|
||||
once: boolean;
|
||||
compounding: boolean;
|
||||
penetrating: boolean;
|
||||
nums: number[];
|
||||
};
|
||||
};
|
||||
206
src/artigen/solver.ts
Normal file
206
src/artigen/solver.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/* The Artificer was built in memory of Babka
|
||||
* With love, Ean
|
||||
*
|
||||
* December 21, 2020
|
||||
*/
|
||||
import { log, LogTypes as LT } from '@Log4Deno';
|
||||
|
||||
import { SolvedStep } from 'artigen/solver.d.ts';
|
||||
import { legalMath, legalMathOperators, loggingEnabled } from 'artigen/rollUtils.ts';
|
||||
|
||||
// fullSolver(conf, wrapDetails) returns one condensed SolvedStep
|
||||
// fullSolver is a function that recursively solves the full roll and math
|
||||
export const fullSolver = (conf: (string | number | SolvedStep)[], wrapDetails: boolean): SolvedStep => {
|
||||
// Initialize PEMDAS
|
||||
const signs = ['^', '*', '/', '%', '+', '-'];
|
||||
const stepSolve = {
|
||||
total: 0,
|
||||
details: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
|
||||
// If entering with a single number, note it now
|
||||
let singleNum = false;
|
||||
if (conf.length === 1) {
|
||||
singleNum = true;
|
||||
}
|
||||
|
||||
// Evaluate all parenthesis
|
||||
while (conf.includes('(')) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for (`);
|
||||
// Get first open parenthesis
|
||||
let openParenIdx = conf.indexOf('(');
|
||||
let closeParenIdx = -1;
|
||||
let nextParenIdx = 0;
|
||||
|
||||
// Using nextParenIdx to count the opening/closing parens, find the matching paren to openParenIdx above
|
||||
closingParenLocator: for (let i = openParenIdx; i < conf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Looking for matching ) openIdx: ${openParenIdx} checking: ${i}`);
|
||||
// If we hit an open, add one (this includes the openParenIdx we start with), if we hit a close, subtract one
|
||||
if (conf[i] === '(') {
|
||||
nextParenIdx++;
|
||||
} else if (conf[i] === ')') {
|
||||
nextParenIdx--;
|
||||
}
|
||||
|
||||
// When nextParenIdx reaches 0 again, we will have found the matching closing parenthesis and can safely exit the for loop
|
||||
if (nextParenIdx === 0) {
|
||||
closeParenIdx = i;
|
||||
break closingParenLocator;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we did find the correct closing paren, if not, error out now
|
||||
if (closeParenIdx === -1 || closeParenIdx < openParenIdx) {
|
||||
throw new Error('UnbalancedParens');
|
||||
}
|
||||
|
||||
// Call the solver on the items between openParenIdx and closeParenIdx (excluding the parens)
|
||||
const parenSolve = fullSolver(conf.slice(openParenIdx + 1, closeParenIdx), true);
|
||||
// Replace the items between openParenIdx and closeParenIdx (including the parens) with its solved equivalent
|
||||
conf.splice(openParenIdx, closeParenIdx - openParenIdx + 1, parenSolve);
|
||||
|
||||
// Determine if previous idx is a Math operator and execute it
|
||||
if (openParenIdx - 1 > -1 && legalMathOperators.includes(conf[openParenIdx - 1].toString())) {
|
||||
// Update total and details of parenSolve
|
||||
parenSolve.total = legalMath[legalMathOperators.indexOf(conf[openParenIdx - 1].toString())](parenSolve.total);
|
||||
parenSolve.details = `${conf[openParenIdx - 1]}${parenSolve.details}`;
|
||||
|
||||
conf.splice(openParenIdx - 1, 2, parenSolve);
|
||||
// shift openParenIdx as we have just removed something before it
|
||||
openParenIdx--;
|
||||
}
|
||||
|
||||
// Determining if we need to add in a multiplication sign to handle implicit multiplication (like "(4)2" = 8)
|
||||
// Check if a number was directly before openParenIdx and slip in the "*" if needed
|
||||
if (openParenIdx - 1 > -1 && !signs.includes(conf[openParenIdx - 1].toString())) {
|
||||
conf.splice(openParenIdx, 0, '*');
|
||||
// shift openParenIdx as we have just added something before it
|
||||
openParenIdx++;
|
||||
}
|
||||
// Check if a number is directly after the closing paren and slip in the "*" if needed
|
||||
// openParenIdx is used here as the conf array has already been collapsed down
|
||||
if (openParenIdx + 1 < conf.length && !signs.includes(conf[openParenIdx + 1].toString())) {
|
||||
conf.splice(openParenIdx + 1, 0, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate all EMDAS by looping thru each tier of operators (exponential is the highest tier, addition/subtraction the lowest)
|
||||
const allCurOps = [['^'], ['*', '/', '%'], ['+', '-']];
|
||||
allCurOps.forEach((curOps) => {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)}`);
|
||||
// Iterate thru all operators/operands in the conf
|
||||
for (let i = 0; i < conf.length; i++) {
|
||||
loggingEnabled && log(LT.LOG, `Evaluating roll ${JSON.stringify(conf)} | Evaluating ${JSON.stringify(curOps)} | Checking ${JSON.stringify(conf[i])}`);
|
||||
// Check if the current index is in the active tier of operators
|
||||
if (curOps.includes(conf[i].toString())) {
|
||||
// Grab the operands from before and after the operator
|
||||
const operand1 = conf[i - 1];
|
||||
const operand2 = conf[i + 1];
|
||||
// Init temp math to NaN to catch bad parsing
|
||||
let oper1 = NaN;
|
||||
let oper2 = NaN;
|
||||
const subStepSolve = {
|
||||
total: NaN,
|
||||
details: '',
|
||||
containsCrit: false,
|
||||
containsFail: false,
|
||||
};
|
||||
|
||||
// If operand1 is a SolvedStep, populate our subStepSolve with its details and crit/fail flags
|
||||
if (typeof operand1 === 'object') {
|
||||
oper1 = operand1.total;
|
||||
subStepSolve.details = `${operand1.details}\\${conf[i]}`;
|
||||
subStepSolve.containsCrit = operand1.containsCrit;
|
||||
subStepSolve.containsFail = operand1.containsFail;
|
||||
} else {
|
||||
// else parse it as a number and add it to the subStep details
|
||||
if (operand1 || operand1 == 0) {
|
||||
oper1 = parseFloat(operand1.toString());
|
||||
subStepSolve.details = `${oper1.toString()}\\${conf[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If operand2 is a SolvedStep, populate our subStepSolve with its details without overriding what operand1 filled in
|
||||
if (typeof operand2 === 'object') {
|
||||
oper2 = operand2.total;
|
||||
subStepSolve.details += operand2.details;
|
||||
subStepSolve.containsCrit = subStepSolve.containsCrit || operand2.containsCrit;
|
||||
subStepSolve.containsFail = subStepSolve.containsFail || operand2.containsFail;
|
||||
} else {
|
||||
// else parse it as a number and add it to the subStep details
|
||||
oper2 = parseFloat(operand2.toString());
|
||||
subStepSolve.details += oper2;
|
||||
}
|
||||
|
||||
// Make sure neither operand is NaN before continuing
|
||||
if (isNaN(oper1) || isNaN(oper2)) {
|
||||
throw new Error('OperandNaN');
|
||||
}
|
||||
|
||||
// Verify a second time that both are numbers before doing math, throwing an error if necessary
|
||||
if (typeof oper1 === 'number' && typeof oper2 === 'number') {
|
||||
// Finally do the operator on the operands, throw an error if the operator is not found
|
||||
switch (conf[i]) {
|
||||
case '^':
|
||||
subStepSolve.total = Math.pow(oper1, oper2);
|
||||
break;
|
||||
case '*':
|
||||
subStepSolve.total = oper1 * oper2;
|
||||
break;
|
||||
case '/':
|
||||
subStepSolve.total = oper1 / oper2;
|
||||
break;
|
||||
case '%':
|
||||
subStepSolve.total = oper1 % oper2;
|
||||
break;
|
||||
case '+':
|
||||
subStepSolve.total = oper1 + oper2;
|
||||
break;
|
||||
case '-':
|
||||
subStepSolve.total = oper1 - oper2;
|
||||
break;
|
||||
default:
|
||||
throw new Error('OperatorWhat');
|
||||
}
|
||||
} else {
|
||||
throw new Error('EMDASNotNumber');
|
||||
}
|
||||
|
||||
// Replace the two operands and their operator with our subStepSolve
|
||||
conf.splice(i - 1, 3, subStepSolve);
|
||||
// Because we are messing around with the array we are iterating thru, we need to back up one idx to make sure every operator gets processed
|
||||
i--;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If we somehow have more than one item left in conf at this point, something broke, throw an error
|
||||
if (conf.length > 1) {
|
||||
loggingEnabled && log(LT.LOG, `ConfWHAT? ${JSON.stringify(conf)}`);
|
||||
throw new Error('ConfWhat');
|
||||
} else if (singleNum && typeof conf[0] === 'number') {
|
||||
// If we are only left with a number, populate the stepSolve with it
|
||||
stepSolve.total = conf[0];
|
||||
stepSolve.details = conf[0].toString();
|
||||
} else {
|
||||
// Else fully populate the stepSolve with what was computed
|
||||
stepSolve.total = (<SolvedStep>conf[0]).total;
|
||||
stepSolve.details = (<SolvedStep>conf[0]).details;
|
||||
stepSolve.containsCrit = (<SolvedStep>conf[0]).containsCrit;
|
||||
stepSolve.containsFail = (<SolvedStep>conf[0]).containsFail;
|
||||
}
|
||||
|
||||
// If this was a nested call, add on parens around the details to show what math we've done
|
||||
if (wrapDetails) {
|
||||
stepSolve.details = `(${stepSolve.details})`;
|
||||
}
|
||||
|
||||
// If our total has reached undefined for some reason, error out now
|
||||
if (stepSolve.total === undefined) {
|
||||
throw new Error('UndefinedStep');
|
||||
}
|
||||
|
||||
return stepSolve;
|
||||
};
|
||||
Reference in New Issue
Block a user