533 lines
20 KiB
TypeScript
533 lines
20 KiB
TypeScript
import { log, LogTypes as LT } from '@Log4Deno';
|
|
|
|
import { RollConf } from 'artigen/dice/dice.d.ts';
|
|
|
|
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
|
|
|
import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/diceOptions.ts';
|
|
|
|
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
|
import { addToRange, gtrAddToRange, ltAddToRange } from 'artigen/utils/rangeAdder.ts';
|
|
|
|
const throwDoubleSepError = (sep: string): void => {
|
|
throw new Error(`DoubleSeparator_${sep}`);
|
|
};
|
|
|
|
// Converts a rollStr into a machine readable rollConf
|
|
export const getRollConf = (rollStr: string): RollConf => {
|
|
// 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
|
|
const rollConf: RollConf = {
|
|
type: '',
|
|
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: [],
|
|
},
|
|
critScore: {
|
|
on: false,
|
|
range: [],
|
|
},
|
|
critFail: {
|
|
on: false,
|
|
range: [],
|
|
},
|
|
exploding: {
|
|
on: false,
|
|
once: false,
|
|
compounding: false,
|
|
penetrating: false,
|
|
nums: [],
|
|
},
|
|
match: {
|
|
on: false,
|
|
minCount: 2,
|
|
returnTotal: false,
|
|
},
|
|
sort: {
|
|
on: false,
|
|
direction: '',
|
|
},
|
|
success: {
|
|
on: false,
|
|
range: [],
|
|
},
|
|
fail: {
|
|
on: false,
|
|
range: [],
|
|
},
|
|
};
|
|
|
|
// If the dPts is not long enough, throw error
|
|
if (dPts.length < 2) {
|
|
throw new Error(`YouNeedAD_${rollStr}`);
|
|
}
|
|
|
|
// 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.endsWith('cwo') && !rawDC.endsWith('ova') && rawDC.match(/\D/)) {
|
|
throw new Error(`CannotParseDieCount_${rawDC}`);
|
|
}
|
|
const tempDC = rawDC.replace(/\D/g, '');
|
|
// Rejoin all remaining parts
|
|
let remains = dPts.join('d');
|
|
|
|
loggingEnabled && log(LT.LOG, `Initial breaking of rollStr ${rawDC} ${tempDC} ${dPts} ${remains}`);
|
|
|
|
// Manual Parsing for custom roll types
|
|
if (rawDC.endsWith('cwo')) {
|
|
// CWOD dice parsing
|
|
rollConf.type = 'cwod';
|
|
|
|
// Get CWOD parts, setting count and getting difficulty
|
|
const cwodParts = rollStr.split('cwod');
|
|
rollConf.dieCount = parseInt(cwodParts[0] || '1');
|
|
rollConf.dieSize = 10;
|
|
|
|
// Use success to set the difficulty
|
|
rollConf.success.on = true;
|
|
rollConf.fail.on = true;
|
|
addToRange('cwod', rollConf.fail.range, 1);
|
|
const tempDifficulty = (cwodParts[1] ?? '').search(/\d/) === 0 ? cwodParts[1] : '';
|
|
let afterDifficultyIdx = tempDifficulty.search(/[^\d]/);
|
|
if (afterDifficultyIdx === -1) {
|
|
afterDifficultyIdx = tempDifficulty.length;
|
|
}
|
|
const difficulty = parseInt(tempDifficulty.slice(0, afterDifficultyIdx) || '10');
|
|
|
|
for (let i = difficulty; i <= rollConf.dieSize; i++) {
|
|
loopCountCheck();
|
|
|
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling cwod ${rollStr} | Parsing difficulty ${i}`);
|
|
rollConf.success.range.push(i);
|
|
}
|
|
|
|
// Remove any garbage from the remains
|
|
remains = remains.slice(afterDifficultyIdx);
|
|
} else if (rawDC.endsWith('ova')) {
|
|
// OVA dice parsing
|
|
rollConf.type = 'ova';
|
|
|
|
// Get OVA parts, setting count and getting difficulty
|
|
const ovaParts = rollStr.split('ovad');
|
|
const tempOvaPart1 = (ovaParts[1] ?? '').search(/\d/) === 0 ? ovaParts[1] : '';
|
|
if (tempOvaPart1.search(/\d+\.\d/) === 0) {
|
|
throw new Error('WholeDieCountSizeOnly');
|
|
}
|
|
rollConf.dieCount = parseInt(ovaParts[0] || '1');
|
|
|
|
let afterOvaSizeIdx = tempOvaPart1.search(/[^\d]/);
|
|
if (afterOvaSizeIdx === -1) {
|
|
afterOvaSizeIdx = tempOvaPart1.length;
|
|
}
|
|
rollConf.dieSize = parseInt(tempOvaPart1.slice(0, afterOvaSizeIdx) || '6');
|
|
|
|
// Remove any garbage from the remains
|
|
remains = remains.slice(afterOvaSizeIdx);
|
|
} else if (remains.startsWith('f')) {
|
|
// fate dice setup
|
|
rollConf.type = '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
|
|
rollConf.type = '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');
|
|
}
|
|
}
|
|
|
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Count: ${rollConf.dieCount}`);
|
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsed Die Size: ${rollConf.dieSize}`);
|
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | remains: ${remains}`);
|
|
|
|
if (!rollConf.dieCount || !rollConf.dieSize) {
|
|
throw new Error(`YouNeedAD_${rollStr}`);
|
|
}
|
|
|
|
// Finish parsing the roll
|
|
if (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();
|
|
|
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${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;
|
|
}
|
|
|
|
// Determine if afterSepIdx needs to be moved up (cases like mt! or !mt)
|
|
const tempSep = remains.slice(0, afterSepIdx);
|
|
let noNumberAfter = false;
|
|
NumberlessDiceOptions.some((opt) => {
|
|
loopCountCheck();
|
|
if (tempSep.startsWith(opt) && tempSep !== opt) {
|
|
afterSepIdx = opt.length;
|
|
noNumberAfter = true;
|
|
return true;
|
|
}
|
|
return tempSep === opt;
|
|
});
|
|
|
|
// 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 = noNumberAfter ? 0 : 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));
|
|
|
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} tSep: ${tSep} ${afterSepIdx}, tNum: ${tNum} ${afterNumIdx}`);
|
|
|
|
// Switch on rule name
|
|
switch (tSep) {
|
|
case DiceOptions.Drop:
|
|
case DiceOptions.DropLow:
|
|
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 DiceOptions.Keep:
|
|
case DiceOptions.KeepHigh:
|
|
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 DiceOptions.DropHigh:
|
|
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 DiceOptions.KeepLow:
|
|
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 DiceOptions.RerollOnce:
|
|
case DiceOptions.RerollOnceEqu:
|
|
rollConf.reroll.once = true;
|
|
// falls through as ro/ro= functions the same as r/r= in this context
|
|
case DiceOptions.Reroll:
|
|
case DiceOptions.RerollEqu:
|
|
// Configure Reroll (this can happen multiple times)
|
|
rollConf.reroll.on = true;
|
|
addToRange(tSep, rollConf.reroll.nums, tNum);
|
|
break;
|
|
case DiceOptions.RerollOnceGtr:
|
|
rollConf.reroll.once = true;
|
|
// falls through as ro> functions the same as r> in this context
|
|
case DiceOptions.RerollGtr:
|
|
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.reroll.on = true;
|
|
gtrAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.dieSize);
|
|
break;
|
|
case DiceOptions.RerollOnceLt:
|
|
rollConf.reroll.once = true;
|
|
// falls through as ro< functions the same as r< in this context
|
|
case DiceOptions.RerollLt:
|
|
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.reroll.on = true;
|
|
ltAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.type);
|
|
break;
|
|
case DiceOptions.CritSuccess:
|
|
case DiceOptions.CritSuccessEqu:
|
|
// Configure CritScore for one number (this can happen multiple times)
|
|
rollConf.critScore.on = true;
|
|
addToRange(tSep, rollConf.critScore.range, tNum);
|
|
break;
|
|
case DiceOptions.CritSuccessGtr:
|
|
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.critScore.on = true;
|
|
gtrAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.dieSize);
|
|
break;
|
|
case DiceOptions.CritSuccessLt:
|
|
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.critScore.on = true;
|
|
ltAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.type);
|
|
break;
|
|
case DiceOptions.CritFail:
|
|
case DiceOptions.CritFailEqu:
|
|
// Configure CritFail for one number (this can happen multiple times)
|
|
rollConf.critFail.on = true;
|
|
addToRange(tSep, rollConf.critFail.range, tNum);
|
|
break;
|
|
case DiceOptions.CritFailGtr:
|
|
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.critFail.on = true;
|
|
gtrAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.dieSize);
|
|
break;
|
|
case DiceOptions.CritFailLt:
|
|
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.critFail.on = true;
|
|
ltAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.type);
|
|
break;
|
|
case DiceOptions.Exploding:
|
|
case DiceOptions.ExplodeOnce:
|
|
case DiceOptions.PenetratingExplosion:
|
|
case DiceOptions.CompoundingExplosion:
|
|
// Configure Exploding
|
|
rollConf.exploding.on = true;
|
|
if (afterNumIdx > 0) {
|
|
// User gave a number to explode on, save it
|
|
addToRange(tSep, rollConf.exploding.nums, tNum);
|
|
}
|
|
break;
|
|
case DiceOptions.ExplodingEqu:
|
|
case DiceOptions.ExplodeOnceEqu:
|
|
case DiceOptions.PenetratingExplosionEqu:
|
|
case DiceOptions.CompoundingExplosionEqu:
|
|
// Configure Exploding (this can happen multiple times)
|
|
rollConf.exploding.on = true;
|
|
addToRange(tSep, rollConf.exploding.nums, tNum);
|
|
break;
|
|
case DiceOptions.ExplodingGtr:
|
|
case DiceOptions.ExplodeOnceGtr:
|
|
case DiceOptions.PenetratingExplosionGtr:
|
|
case DiceOptions.CompoundingExplosionGtr:
|
|
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.exploding.on = true;
|
|
gtrAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.dieSize);
|
|
break;
|
|
case DiceOptions.ExplodingLt:
|
|
case DiceOptions.ExplodeOnceLt:
|
|
case DiceOptions.PenetratingExplosionLt:
|
|
case DiceOptions.CompoundingExplosionLt:
|
|
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.exploding.on = true;
|
|
ltAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.type);
|
|
break;
|
|
case DiceOptions.MatchingTotal:
|
|
rollConf.match.returnTotal = true;
|
|
// falls through as mt functions the same as m in this context
|
|
case DiceOptions.Matching:
|
|
if (rollConf.match.on) {
|
|
// Ensure we do not override existing settings
|
|
throwDoubleSepError(tSep);
|
|
}
|
|
rollConf.match.on = true;
|
|
if (afterNumIdx > 0) {
|
|
// User gave a number to work with, save it
|
|
rollConf.match.minCount = tNum;
|
|
}
|
|
break;
|
|
case DiceOptions.Sort:
|
|
case DiceOptions.SortAsc:
|
|
if (rollConf.sort.on) {
|
|
// Ensure we do not override existing settings
|
|
throwDoubleSepError(tSep);
|
|
}
|
|
rollConf.sort.on = true;
|
|
rollConf.sort.direction = 'a';
|
|
break;
|
|
case DiceOptions.SortDesc:
|
|
if (rollConf.sort.on) {
|
|
// Ensure we do not override existing settings
|
|
throwDoubleSepError(tSep);
|
|
}
|
|
rollConf.sort.on = true;
|
|
rollConf.sort.direction = 'd';
|
|
break;
|
|
case DiceOptions.SuccessEqu:
|
|
// Configure success (this can happen multiple times)
|
|
rollConf.success.on = true;
|
|
addToRange(tSep, rollConf.success.range, tNum);
|
|
break;
|
|
case DiceOptions.SuccessGtr:
|
|
// Configure success for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.success.on = true;
|
|
gtrAddToRange(tSep, rollConf.success.range, tNum, rollConf.dieSize);
|
|
break;
|
|
case DiceOptions.SuccessLt:
|
|
// Configure success for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.success.on = true;
|
|
ltAddToRange(tSep, rollConf.success.range, tNum, rollConf.type);
|
|
break;
|
|
case DiceOptions.Fail:
|
|
case DiceOptions.FailEqu:
|
|
// Configure fail (this can happen multiple times)
|
|
rollConf.fail.on = true;
|
|
addToRange(tSep, rollConf.fail.range, tNum);
|
|
break;
|
|
case DiceOptions.FailGtr:
|
|
// Configure fail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.fail.on = true;
|
|
gtrAddToRange(tSep, rollConf.fail.range, tNum, rollConf.dieSize);
|
|
break;
|
|
case DiceOptions.FailLt:
|
|
// Configure fail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
|
rollConf.fail.on = true;
|
|
ltAddToRange(tSep, rollConf.fail.range, tNum, rollConf.type);
|
|
break;
|
|
default:
|
|
// Throw error immediately if unknown op is encountered
|
|
throw new Error(`UnknownOperation_${tSep}`);
|
|
}
|
|
|
|
// Followup switch to avoid weird duplicated code
|
|
switch (tSep) {
|
|
case DiceOptions.ExplodeOnce:
|
|
case DiceOptions.ExplodeOnceLt:
|
|
case DiceOptions.ExplodeOnceGtr:
|
|
case DiceOptions.ExplodeOnceEqu:
|
|
rollConf.exploding.once = true;
|
|
break;
|
|
case DiceOptions.PenetratingExplosion:
|
|
case DiceOptions.PenetratingExplosionLt:
|
|
case DiceOptions.PenetratingExplosionGtr:
|
|
case DiceOptions.PenetratingExplosionEqu:
|
|
rollConf.exploding.penetrating = true;
|
|
break;
|
|
case DiceOptions.CompoundingExplosion:
|
|
case DiceOptions.CompoundingExplosionLt:
|
|
case DiceOptions.CompoundingExplosionGtr:
|
|
case DiceOptions.CompoundingExplosionEqu:
|
|
rollConf.exploding.compounding = true;
|
|
break;
|
|
}
|
|
|
|
// Finally slice off everything else parsed this loop
|
|
remains = remains.slice(afterNumIdx);
|
|
}
|
|
}
|
|
|
|
loggingEnabled && log(LT.LOG, `RollConf before cleanup: ${JSON.stringify(rollConf)}`);
|
|
|
|
// 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 ${rollConf.type} ${rollStr} | Checking if drop/keep is on ${e}`);
|
|
if (e) {
|
|
dkdkCnt++;
|
|
}
|
|
});
|
|
if (dkdkCnt > 1) {
|
|
throw new Error('FormattingError_dk');
|
|
}
|
|
|
|
if (rollConf.match.on && (rollConf.success.on || rollConf.fail.on)) {
|
|
throw new Error('FormattingError_mtsf');
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
// Filter rollConf num lists to only include valid numbers
|
|
const validNumFilter = (curNum: number) => {
|
|
if (rollConf.type === 'fate') {
|
|
return [-1, 0, 1].includes(curNum);
|
|
}
|
|
return 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);
|
|
rollConf.success.range = rollConf.success.range.filter(validNumFilter);
|
|
rollConf.fail.range = rollConf.fail.range.filter(validNumFilter);
|
|
|
|
if (rollConf.reroll.on && rollConf.reroll.nums.length === (rollConf.type === 'fate' ? 3 : rollConf.dieSize)) {
|
|
throw new Error('NoRerollOnAllSides');
|
|
}
|
|
|
|
loggingEnabled && log(LT.LOG, `RollConf after cleanup: ${JSON.stringify(rollConf)}`);
|
|
|
|
return rollConf;
|
|
};
|