Add target success/failures option
This commit is contained in:
parent
38bc021455
commit
15fd57ea18
|
@ -16,6 +16,8 @@ export interface RollSet {
|
||||||
critFail: boolean;
|
critFail: boolean;
|
||||||
isComplex: boolean;
|
isComplex: boolean;
|
||||||
matchLabel: string;
|
matchLabel: string;
|
||||||
|
success: boolean;
|
||||||
|
fail: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountDetails is the object holding the count data for creating the Count Embed
|
// CountDetails is the object holding the count data for creating the Count Embed
|
||||||
|
@ -119,9 +121,18 @@ export interface RollConf {
|
||||||
returnTotal: boolean;
|
returnTotal: boolean;
|
||||||
};
|
};
|
||||||
sort: SortDisabled | SortEnabled;
|
sort: SortDisabled | SortEnabled;
|
||||||
|
success: RangeConf;
|
||||||
|
fail: RangeConf;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SumOverride {
|
export interface SumOverride {
|
||||||
on: boolean;
|
on: boolean;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecutedRoll {
|
||||||
|
rollSet: RollSet[];
|
||||||
|
countSuccessOverride: boolean;
|
||||||
|
countFailOverride: boolean;
|
||||||
|
sumOverride: SumOverride;
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,13 @@ export const DiceOptions = Object.freeze({
|
||||||
Sort: 's',
|
Sort: 's',
|
||||||
SortAsc: 'sa',
|
SortAsc: 'sa',
|
||||||
SortDesc: 'sd',
|
SortDesc: 'sd',
|
||||||
|
SuccessLt: '<',
|
||||||
|
SuccessGtr: '>',
|
||||||
|
SuccessEqu: '=',
|
||||||
|
Fail: 'f',
|
||||||
|
FailLt: 'f<',
|
||||||
|
FailGtr: 'f>',
|
||||||
|
FailEqu: 'f=',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should be ordered such that 'mt' will be encountered before 'm'
|
// Should be ordered such that 'mt' will be encountered before 'm'
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { log, LogTypes as LT } from '@Log4Deno';
|
import { log, LogTypes as LT } from '@Log4Deno';
|
||||||
|
|
||||||
import { RollModifiers, RollSet, SumOverride } from 'artigen/dice/dice.d.ts';
|
import { ExecutedRoll, RollModifiers, RollSet, SumOverride } from 'artigen/dice/dice.d.ts';
|
||||||
import { genFateRoll, genRoll } from 'artigen/dice/randomRoll.ts';
|
import { genFateRoll, genRoll } from 'artigen/dice/randomRoll.ts';
|
||||||
import { getRollConf } from 'artigen/dice/getRollConf.ts';
|
import { getRollConf } from 'artigen/dice/getRollConf.ts';
|
||||||
|
|
||||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||||
import { compareOrigIdx, compareRolls, compareRollsReverse } from 'artigen/utils/sortFuncs.ts';
|
import { compareOrigIdx, compareRolls, compareRollsReverse } from 'artigen/utils/sortFuncs.ts';
|
||||||
|
|
||||||
|
import { flagRoll } from 'artigen/utils/diceFlagger.ts';
|
||||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||||
import { generateRollVals } from 'artigen/utils/rollValCounter.ts';
|
import { generateRollVals } from 'artigen/utils/rollValCounter.ts';
|
||||||
|
|
||||||
// roll(rollStr, modifiers) returns RollSet
|
// roll(rollStr, modifiers) returns RollSet
|
||||||
// roll parses and executes the rollStr
|
// roll parses and executes the rollStr
|
||||||
export const executeRoll = (rollStr: string, modifiers: RollModifiers): [RollSet[], SumOverride] => {
|
export const executeRoll = (rollStr: string, modifiers: RollModifiers): ExecutedRoll => {
|
||||||
/* Roll Capabilities
|
/* Roll Capabilities
|
||||||
* Deciphers and rolls a single dice roll set
|
* Deciphers and rolls a single dice roll set
|
||||||
*
|
*
|
||||||
|
@ -67,8 +68,12 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): [RollSet
|
||||||
rollConf.keepLow.on ||
|
rollConf.keepLow.on ||
|
||||||
rollConf.critScore.on ||
|
rollConf.critScore.on ||
|
||||||
rollConf.critFail.on ||
|
rollConf.critFail.on ||
|
||||||
rollConf.exploding.on,
|
rollConf.exploding.on ||
|
||||||
|
rollConf.success.on ||
|
||||||
|
rollConf.fail.on,
|
||||||
matchLabel: '',
|
matchLabel: '',
|
||||||
|
success: false,
|
||||||
|
fail: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial rolling, not handling reroll or exploding here
|
// Initial rolling, not handling reroll or exploding here
|
||||||
|
@ -85,22 +90,7 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): [RollSet
|
||||||
// Set origIdx of roll
|
// Set origIdx of roll
|
||||||
rolling.origIdx = i;
|
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
|
flagRoll(rollConf, rolling);
|
||||||
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 (rollConf.type === 'fate') {
|
|
||||||
rolling.critFail = rolling.roll === -1;
|
|
||||||
} else {
|
|
||||||
rolling.critFail = rolling.roll === (rollConf.dPercent.on ? 0 : 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the newly created roll and loop again
|
// Push the newly created roll and loop again
|
||||||
rollSet.push(rolling);
|
rollSet.push(rolling);
|
||||||
|
@ -151,18 +141,7 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): [RollSet
|
||||||
newReroll.roll = genRoll(rollConf.dieSize, modifiers, rollConf.dPercent);
|
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
|
flagRoll(rollConf, newReroll);
|
||||||
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
|
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||||
rollSet.splice(i + 1, 0, newReroll);
|
rollSet.splice(i + 1, 0, newReroll);
|
||||||
|
@ -183,18 +162,7 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): [RollSet
|
||||||
// Always mark this roll as exploding
|
// Always mark this roll as exploding
|
||||||
newExplodingRoll.exploding = true;
|
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
|
flagRoll(rollConf, newExplodingRoll);
|
||||||
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
|
// Slot this new roll in after the current iteration so it can be processed in the next loop
|
||||||
rollSet.splice(i + 1, 0, newExplodingRoll);
|
rollSet.splice(i + 1, 0, newExplodingRoll);
|
||||||
|
@ -374,5 +342,10 @@ export const executeRoll = (rollStr: string, modifiers: RollModifiers): [RollSet
|
||||||
rollSet.sort(rollConf.sort.direction === 'a' ? compareRolls : compareRollsReverse);
|
rollSet.sort(rollConf.sort.direction === 'a' ? compareRolls : compareRollsReverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [rollSet, sumOverride];
|
return {
|
||||||
|
rollSet,
|
||||||
|
sumOverride,
|
||||||
|
countSuccessOverride: rollConf.success.on,
|
||||||
|
countFailOverride: rollConf.fail.on,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,10 +19,10 @@ export const generateFormattedRoll = (rollConf: string, modifiers: RollModifiers
|
||||||
let tempComplex = false;
|
let tempComplex = false;
|
||||||
|
|
||||||
// Generate the roll, passing flags thru
|
// Generate the roll, passing flags thru
|
||||||
const [tempRollSet, sumOverride] = executeRoll(rollConf, modifiers);
|
const executedRoll = executeRoll(rollConf, modifiers);
|
||||||
|
|
||||||
// Loop thru all parts of the roll to document everything that was done to create the total roll
|
// Loop thru all parts of the roll to document everything that was done to create the total roll
|
||||||
tempRollSet.forEach((e) => {
|
executedRoll.rollSet.forEach((e) => {
|
||||||
loopCountCheck();
|
loopCountCheck();
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `Formatting roll ${rollConf} | ${JSON.stringify(e)}`);
|
loggingEnabled && log(LT.LOG, `Formatting roll ${rollConf} | ${JSON.stringify(e)}`);
|
||||||
|
@ -38,7 +38,7 @@ export const generateFormattedRoll = (rollConf: string, modifiers: RollModifiers
|
||||||
tempTotal += e.roll;
|
tempTotal += e.roll;
|
||||||
break;
|
break;
|
||||||
case 'cwod':
|
case 'cwod':
|
||||||
tempTotal += e.critHit ? 1 : 0;
|
tempTotal += e.success ? 1 : 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (e.critHit) {
|
if (e.critHit) {
|
||||||
|
@ -82,22 +82,33 @@ export const generateFormattedRoll = (rollConf: string, modifiers: RollModifiers
|
||||||
});
|
});
|
||||||
// After the looping is done, remove the extra " + " from the details and cap it with the closing ]
|
// After the looping is done, remove the extra " + " from the details and cap it with the closing ]
|
||||||
tempDetails = tempDetails.substring(0, tempDetails.length - 3);
|
tempDetails = tempDetails.substring(0, tempDetails.length - 3);
|
||||||
if (tempRollSet[0]?.type === 'cwod') {
|
if (executedRoll.countSuccessOverride) {
|
||||||
const successCnt = tempRollSet.filter((e) => !e.dropped && !e.rerolled && e.critHit).length;
|
const successCnt = executedRoll.rollSet.filter((e) => !e.dropped && !e.rerolled && e.success).length;
|
||||||
const failCnt = tempRollSet.filter((e) => !e.dropped && !e.rerolled && e.critFail).length;
|
tempDetails += `, ${successCnt} Success${successCnt !== 1 ? 'es' : ''}`;
|
||||||
tempDetails += `, ${successCnt} Success${successCnt !== 1 ? 'es' : ''}, ${failCnt} Fail${failCnt !== 1 ? 's' : ''}`;
|
|
||||||
|
executedRoll.sumOverride.on = true;
|
||||||
|
executedRoll.sumOverride.value += successCnt;
|
||||||
|
}
|
||||||
|
if (executedRoll.countFailOverride) {
|
||||||
|
const failCnt = executedRoll.rollSet.filter((e) => !e.dropped && !e.rerolled && e.fail).length;
|
||||||
|
tempDetails += `, ${failCnt} Fail${failCnt !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
executedRoll.sumOverride.on = true;
|
||||||
|
if (executedRoll.rollSet[0]?.type !== 'cwod') {
|
||||||
|
executedRoll.sumOverride.value -= failCnt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tempDetails += ']';
|
tempDetails += ']';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
solvedStep: {
|
solvedStep: {
|
||||||
total: sumOverride.on ? sumOverride.value : tempTotal,
|
total: executedRoll.sumOverride.on ? executedRoll.sumOverride.value : tempTotal,
|
||||||
details: tempDetails,
|
details: tempDetails,
|
||||||
containsCrit: tempCrit,
|
containsCrit: tempCrit,
|
||||||
containsFail: tempFail,
|
containsFail: tempFail,
|
||||||
isComplex: tempComplex,
|
isComplex: tempComplex,
|
||||||
},
|
},
|
||||||
countDetails: modifiers.count || modifiers.confirmCrit ? rollCounter(tempRollSet) : rollCounter([]),
|
countDetails: modifiers.count || modifiers.confirmCrit ? rollCounter(executedRoll.rollSet) : rollCounter([]),
|
||||||
rollDistributions: modifiers.rollDist ? createRollDistMap(tempRollSet) : new Map<string, number[]>(),
|
rollDistributions: modifiers.rollDist ? createRollDistMap(executedRoll.rollSet) : new Map<string, number[]>(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,9 +4,11 @@ import { RollConf } from 'artigen/dice/dice.d.ts';
|
||||||
|
|
||||||
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||||
|
|
||||||
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
|
||||||
import { DiceOptions, NumberlessDiceOptions } from 'artigen/dice/diceOptions.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 => {
|
const throwDoubleSepError = (sep: string): void => {
|
||||||
throw new Error(`DoubleSeparator_${sep}`);
|
throw new Error(`DoubleSeparator_${sep}`);
|
||||||
};
|
};
|
||||||
|
@ -71,6 +73,14 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
on: false,
|
on: false,
|
||||||
direction: '',
|
direction: '',
|
||||||
},
|
},
|
||||||
|
success: {
|
||||||
|
on: false,
|
||||||
|
range: [],
|
||||||
|
},
|
||||||
|
fail: {
|
||||||
|
on: false,
|
||||||
|
range: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the dPts is not long enough, throw error
|
// If the dPts is not long enough, throw error
|
||||||
|
@ -101,8 +111,10 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
rollConf.dieCount = parseInt(cwodParts[0] || '1');
|
rollConf.dieCount = parseInt(cwodParts[0] || '1');
|
||||||
rollConf.dieSize = 10;
|
rollConf.dieSize = 10;
|
||||||
|
|
||||||
// Use critScore to set the difficulty
|
// Use success to set the difficulty
|
||||||
rollConf.critScore.on = true;
|
rollConf.success.on = true;
|
||||||
|
rollConf.fail.on = true;
|
||||||
|
addToRange('cwod', rollConf.fail.range, 1);
|
||||||
const tempDifficulty = (cwodParts[1] ?? '').search(/\d/) === 0 ? cwodParts[1] : '';
|
const tempDifficulty = (cwodParts[1] ?? '').search(/\d/) === 0 ? cwodParts[1] : '';
|
||||||
let afterDifficultyIdx = tempDifficulty.search(/[^\d]/);
|
let afterDifficultyIdx = tempDifficulty.search(/[^\d]/);
|
||||||
if (afterDifficultyIdx === -1) {
|
if (afterDifficultyIdx === -1) {
|
||||||
|
@ -114,7 +126,7 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
loopCountCheck();
|
loopCountCheck();
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling cwod ${rollStr} | Parsing difficulty ${i}`);
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling cwod ${rollStr} | Parsing difficulty ${i}`);
|
||||||
rollConf.critScore.range.push(i);
|
rollConf.success.range.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any garbage from the remains
|
// Remove any garbage from the remains
|
||||||
|
@ -275,7 +287,7 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.RerollEqu:
|
case DiceOptions.RerollEqu:
|
||||||
// Configure Reroll (this can happen multiple times)
|
// Configure Reroll (this can happen multiple times)
|
||||||
rollConf.reroll.on = true;
|
rollConf.reroll.on = true;
|
||||||
!rollConf.reroll.nums.includes(tNum) && rollConf.reroll.nums.push(tNum);
|
addToRange(tSep, rollConf.reroll.nums, tNum);
|
||||||
break;
|
break;
|
||||||
case DiceOptions.RerollOnceGtr:
|
case DiceOptions.RerollOnceGtr:
|
||||||
rollConf.reroll.once = true;
|
rollConf.reroll.once = true;
|
||||||
|
@ -283,12 +295,7 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.RerollGtr:
|
case DiceOptions.RerollGtr:
|
||||||
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
// Configure reroll for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.reroll.on = true;
|
rollConf.reroll.on = true;
|
||||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
gtrAddToRange(tSep, rollConf.reroll.nums, tNum, rollConf.dieSize);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing r> ${i}`);
|
|
||||||
!rollConf.reroll.nums.includes(i) && rollConf.reroll.nums.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.RerollOnceLt:
|
case DiceOptions.RerollOnceLt:
|
||||||
rollConf.reroll.once = true;
|
rollConf.reroll.once = true;
|
||||||
|
@ -296,64 +303,39 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.RerollLt:
|
case DiceOptions.RerollLt:
|
||||||
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
// Configure reroll for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.reroll.on = true;
|
rollConf.reroll.on = true;
|
||||||
for (let i = 1; i <= tNum; i++) {
|
ltAddToRange(tSep, rollConf.reroll.nums, tNum);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing r< ${i}`);
|
|
||||||
!rollConf.reroll.nums.includes(i) && rollConf.reroll.nums.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.CritSuccess:
|
case DiceOptions.CritSuccess:
|
||||||
case DiceOptions.CritSuccessEqu:
|
case DiceOptions.CritSuccessEqu:
|
||||||
// Configure CritScore for one number (this can happen multiple times)
|
// Configure CritScore for one number (this can happen multiple times)
|
||||||
rollConf.critScore.on = true;
|
rollConf.critScore.on = true;
|
||||||
!rollConf.critScore.range.includes(tNum) && rollConf.critScore.range.push(tNum);
|
addToRange(tSep, rollConf.critScore.range, tNum);
|
||||||
break;
|
break;
|
||||||
case DiceOptions.CritSuccessGtr:
|
case DiceOptions.CritSuccessGtr:
|
||||||
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
// Configure CritScore for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.critScore.on = true;
|
rollConf.critScore.on = true;
|
||||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
gtrAddToRange(tSep, rollConf.critScore.range, tNum, rollConf.dieSize);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing cs> ${i}`);
|
|
||||||
!rollConf.critScore.range.includes(i) && rollConf.critScore.range.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.CritSuccessLt:
|
case DiceOptions.CritSuccessLt:
|
||||||
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
// Configure CritScore for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.critScore.on = true;
|
rollConf.critScore.on = true;
|
||||||
for (let i = 0; i <= tNum; i++) {
|
ltAddToRange(tSep, rollConf.critScore.range, tNum);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing cs< ${i}`);
|
|
||||||
!rollConf.critScore.range.includes(i) && rollConf.critScore.range.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.CritFail:
|
case DiceOptions.CritFail:
|
||||||
case DiceOptions.CritFailEqu:
|
case DiceOptions.CritFailEqu:
|
||||||
// Configure CritFail for one number (this can happen multiple times)
|
// Configure CritFail for one number (this can happen multiple times)
|
||||||
rollConf.critFail.on = true;
|
rollConf.critFail.on = true;
|
||||||
!rollConf.critFail.range.includes(tNum) && rollConf.critFail.range.push(tNum);
|
addToRange(tSep, rollConf.critFail.range, tNum);
|
||||||
break;
|
break;
|
||||||
case DiceOptions.CritFailGtr:
|
case DiceOptions.CritFailGtr:
|
||||||
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
// Configure CritFail for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.critFail.on = true;
|
rollConf.critFail.on = true;
|
||||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
gtrAddToRange(tSep, rollConf.critFail.range, tNum, rollConf.dieSize);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing cf> ${i}`);
|
|
||||||
!rollConf.critFail.range.includes(i) && rollConf.critFail.range.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.CritFailLt:
|
case DiceOptions.CritFailLt:
|
||||||
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
// Configure CritFail for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.critFail.on = true;
|
rollConf.critFail.on = true;
|
||||||
for (let i = 0; i <= tNum; i++) {
|
ltAddToRange(tSep, rollConf.critFail.range, tNum);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing cf< ${i}`);
|
|
||||||
!rollConf.critFail.range.includes(i) && rollConf.critFail.range.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.Exploding:
|
case DiceOptions.Exploding:
|
||||||
case DiceOptions.ExplodeOnce:
|
case DiceOptions.ExplodeOnce:
|
||||||
|
@ -363,7 +345,7 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
rollConf.exploding.on = true;
|
rollConf.exploding.on = true;
|
||||||
if (afterNumIdx > 0) {
|
if (afterNumIdx > 0) {
|
||||||
// User gave a number to explode on, save it
|
// User gave a number to explode on, save it
|
||||||
!rollConf.exploding.nums.includes(tNum) && rollConf.exploding.nums.push(tNum);
|
addToRange(tSep, rollConf.exploding.nums, tNum);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DiceOptions.ExplodingEqu:
|
case DiceOptions.ExplodingEqu:
|
||||||
|
@ -372,7 +354,7 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.CompoundingExplosionEqu:
|
case DiceOptions.CompoundingExplosionEqu:
|
||||||
// Configure Exploding (this can happen multiple times)
|
// Configure Exploding (this can happen multiple times)
|
||||||
rollConf.exploding.on = true;
|
rollConf.exploding.on = true;
|
||||||
!rollConf.exploding.nums.includes(tNum) && rollConf.exploding.nums.push(tNum);
|
addToRange(tSep, rollConf.exploding.nums, tNum);
|
||||||
break;
|
break;
|
||||||
case DiceOptions.ExplodingGtr:
|
case DiceOptions.ExplodingGtr:
|
||||||
case DiceOptions.ExplodeOnceGtr:
|
case DiceOptions.ExplodeOnceGtr:
|
||||||
|
@ -380,12 +362,7 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.CompoundingExplosionGtr:
|
case DiceOptions.CompoundingExplosionGtr:
|
||||||
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
// Configure Exploding for all numbers greater than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.exploding.on = true;
|
rollConf.exploding.on = true;
|
||||||
for (let i = tNum; i <= rollConf.dieSize; i++) {
|
gtrAddToRange(tSep, rollConf.exploding.nums, tNum, rollConf.dieSize);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing !> ${i}`);
|
|
||||||
!rollConf.exploding.nums.includes(i) && rollConf.exploding.nums.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.ExplodingLt:
|
case DiceOptions.ExplodingLt:
|
||||||
case DiceOptions.ExplodeOnceLt:
|
case DiceOptions.ExplodeOnceLt:
|
||||||
|
@ -393,15 +370,16 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.CompoundingExplosionLt:
|
case DiceOptions.CompoundingExplosionLt:
|
||||||
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
// Configure Exploding for all numbers less than or equal to tNum (this could happen multiple times, but why)
|
||||||
rollConf.exploding.on = true;
|
rollConf.exploding.on = true;
|
||||||
for (let i = 1; i <= tNum; i++) {
|
ltAddToRange(tSep, rollConf.exploding.nums, tNum);
|
||||||
loopCountCheck();
|
|
||||||
|
|
||||||
loggingEnabled && log(LT.LOG, `${getLoopCount()} Handling ${rollConf.type} ${rollStr} | Parsing !< ${i}`);
|
|
||||||
!rollConf.exploding.nums.includes(i) && rollConf.exploding.nums.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case DiceOptions.Matching:
|
|
||||||
case DiceOptions.MatchingTotal:
|
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;
|
rollConf.match.on = true;
|
||||||
if (afterNumIdx > 0) {
|
if (afterNumIdx > 0) {
|
||||||
// User gave a number to work with, save it
|
// User gave a number to work with, save it
|
||||||
|
@ -410,13 +388,52 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
break;
|
break;
|
||||||
case DiceOptions.Sort:
|
case DiceOptions.Sort:
|
||||||
case DiceOptions.SortAsc:
|
case DiceOptions.SortAsc:
|
||||||
|
if (rollConf.sort.on) {
|
||||||
|
// Ensure we do not override existing settings
|
||||||
|
throwDoubleSepError(tSep);
|
||||||
|
}
|
||||||
rollConf.sort.on = true;
|
rollConf.sort.on = true;
|
||||||
rollConf.sort.direction = 'a';
|
rollConf.sort.direction = 'a';
|
||||||
break;
|
break;
|
||||||
case DiceOptions.SortDesc:
|
case DiceOptions.SortDesc:
|
||||||
|
if (rollConf.sort.on) {
|
||||||
|
// Ensure we do not override existing settings
|
||||||
|
throwDoubleSepError(tSep);
|
||||||
|
}
|
||||||
rollConf.sort.on = true;
|
rollConf.sort.on = true;
|
||||||
rollConf.sort.direction = 'd';
|
rollConf.sort.direction = 'd';
|
||||||
break;
|
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);
|
||||||
|
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);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Throw error immediately if unknown op is encountered
|
// Throw error immediately if unknown op is encountered
|
||||||
throw new Error(`UnknownOperation_${tSep}`);
|
throw new Error(`UnknownOperation_${tSep}`);
|
||||||
|
@ -442,9 +459,6 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
case DiceOptions.CompoundingExplosionEqu:
|
case DiceOptions.CompoundingExplosionEqu:
|
||||||
rollConf.exploding.compounding = true;
|
rollConf.exploding.compounding = true;
|
||||||
break;
|
break;
|
||||||
case DiceOptions.MatchingTotal:
|
|
||||||
rollConf.match.returnTotal = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally slice off everything else parsed this loop
|
// Finally slice off everything else parsed this loop
|
||||||
|
@ -472,6 +486,10 @@ export const getRollConf = (rollStr: string): RollConf => {
|
||||||
throw new Error('FormattingError_dk');
|
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) {
|
if (rollConf.drop.on && rollConf.drop.count === 0) {
|
||||||
throw new Error('NoZerosAllowed_drop');
|
throw new Error('NoZerosAllowed_drop');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { RollConf, RollSet } from 'artigen/dice/dice.d.ts';
|
||||||
|
|
||||||
|
export const flagRoll = (rollConf: RollConf, rollSet: RollSet) => {
|
||||||
|
// If critScore arg is on, check if the roll should be a crit, if its off, check if the roll matches the die size
|
||||||
|
if (rollConf.critScore.on && rollConf.critScore.range.includes(rollSet.roll)) {
|
||||||
|
rollSet.critHit = true;
|
||||||
|
} else if (!rollConf.critScore.on) {
|
||||||
|
rollSet.critHit = rollSet.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(rollSet.roll)) {
|
||||||
|
rollSet.critFail = true;
|
||||||
|
} else if (!rollConf.critFail.on) {
|
||||||
|
if (rollConf.type === 'fate') {
|
||||||
|
rollSet.critFail = rollSet.roll === -1;
|
||||||
|
} else {
|
||||||
|
rollSet.critFail = rollSet.roll === (rollConf.dPercent.on ? 0 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If success arg is on, check if roll should be successful
|
||||||
|
if (rollConf.success.on && rollConf.success.range.includes(rollSet.roll)) {
|
||||||
|
rollSet.success = true;
|
||||||
|
rollSet.matchLabel = 'S';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If fail arg is on, check if roll should be failed
|
||||||
|
if (rollConf.fail.on && rollConf.fail.range.includes(rollSet.roll)) {
|
||||||
|
rollSet.fail = true;
|
||||||
|
rollSet.matchLabel = 'F';
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { log, LogTypes as LT } from '@Log4Deno';
|
||||||
|
|
||||||
|
import { getLoopCount, loopCountCheck } from 'artigen/managers/loopManager.ts';
|
||||||
|
|
||||||
|
import { loggingEnabled } from 'artigen/utils/logFlag.ts';
|
||||||
|
|
||||||
|
// Add tNum to range
|
||||||
|
export const addToRange = (tSep: string, range: Array<number>, tNum: number) => {
|
||||||
|
loggingEnabled && log(LT.LOG, `${getLoopCount()} addToRange on ${tSep} attempting to add: ${tNum}`);
|
||||||
|
!range.includes(tNum) && range.push(tNum);
|
||||||
|
};
|
||||||
|
|
||||||
|
const internalAddMultipleToRange = (tSep: string, range: Array<number>, start: number, end: number) => {
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
loopCountCheck();
|
||||||
|
addToRange(tSep, range, i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add numbers less than or equal to tNum to range
|
||||||
|
export const ltAddToRange = (tSep: string, range: Array<number>, tNum: number) => internalAddMultipleToRange(tSep, range, 0, tNum);
|
||||||
|
|
||||||
|
// Add numbers greater than or equal to tNum to range
|
||||||
|
export const gtrAddToRange = (tSep: string, range: Array<number>, tNum: number, dieSize: number) => internalAddMultipleToRange(tSep, range, tNum, dieSize);
|
|
@ -32,7 +32,18 @@ export const translateError = (solverError: Error): [string, string] => {
|
||||||
errorMsg = `Formatting Error: \`${errorDetails}\` should only be specified once per roll, remove all but one and repeat roll`;
|
errorMsg = `Formatting Error: \`${errorDetails}\` should only be specified once per roll, remove all but one and repeat roll`;
|
||||||
break;
|
break;
|
||||||
case 'FormattingError':
|
case 'FormattingError':
|
||||||
errorMsg = 'Formatting Error: Cannot use Keep and Drop at the same time, remove all but one and repeat roll';
|
errorMsg = 'Formatting Error: ';
|
||||||
|
switch (errorDetails) {
|
||||||
|
case 'dk':
|
||||||
|
errorMsg += 'Cannot use Keep and Drop at the same time, remove all but one and repeat roll';
|
||||||
|
break;
|
||||||
|
case 'mtsf':
|
||||||
|
errorMsg += 'Cannot use Match with CWOD Dice, or the Success or Fail options, remove all but one and repeat roll';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMsg += `Unhandled - ${errorDetails}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'NoMaxWithDash':
|
case 'NoMaxWithDash':
|
||||||
errorMsg = 'Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct';
|
errorMsg = 'Formatting Error: CritScore range specified without a maximum, remove - or add maximum to correct';
|
||||||
|
|
|
@ -237,7 +237,10 @@ Any time a Compounding Explosion happens, the formatting on the die will still s
|
||||||
description: `**Usage:** \`xdym\`, \`xdymz\`, \`xdymt\`, or \`xdymtz\`
|
description: `**Usage:** \`xdym\`, \`xdymz\`, \`xdymt\`, or \`xdymtz\`
|
||||||
\`z\` - Minimum count of matches for a label to be added
|
\`z\` - Minimum count of matches for a label to be added
|
||||||
|
|
||||||
The basic \`m\` option will only add labels without modifying the results, whereas the \`mt\` options will add labels and will change the result of the roll to be equal to the number of labels that have been added.`,
|
\`m\` will only add labels without modifying the results.
|
||||||
|
\`mt\` will add labels and will change the result of the roll to be equal to the number of labels that have been added.
|
||||||
|
|
||||||
|
**Notice:** Cannot be combined with Target Number/Successes or Target Failures`,
|
||||||
example: [
|
example: [
|
||||||
'`[[10d6m]]` => [**C:6** + B:2 + 4 + __C:1__ + __C:1__ + B:2 + **C:6** + B:2 + **C:6** + **C:6**] = 36',
|
'`[[10d6m]]` => [**C:6** + B:2 + 4 + __C:1__ + __C:1__ + B:2 + **C:6** + B:2 + **C:6** + **C:6**] = 36',
|
||||||
'`[[10d6m4]]` => [**A:6** + 2 + 4 + __1__ + __1__ + 2 + **A:6** + 2 + **A:6** + **A:6**] = 36',
|
'`[[10d6m4]]` => [**A:6** + 2 + 4 + __1__ + __1__ + 2 + **A:6** + 2 + **A:6** + **A:6**] = 36',
|
||||||
|
@ -260,6 +263,43 @@ The basic \`m\` option will only add labels without modifying the results, where
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'target-success',
|
||||||
|
{
|
||||||
|
name: 'Target Number/Successes',
|
||||||
|
description: `**Usage:** \`xdy=z\`, \`xdy<z\`, or \`xdy>z\`
|
||||||
|
\`z\` - Number to compare to for configuring the success target
|
||||||
|
|
||||||
|
Will set the result to be the number of successes. When combined with the Target Failures option, the failures will be subtracted from the result.
|
||||||
|
|
||||||
|
**Notice:** Cannot be combined with the Dice Matching option`,
|
||||||
|
example: [
|
||||||
|
'`[[10d6=5]]` => [2 + 3 + 3 + __1__ + 2 + S:5 + S:5 + 2 + **6** + __1__, 2 Successes] = **__2__**',
|
||||||
|
'`[[10d6>5]]` => [__1__ + S:5 + 4 + 3 + **S:6** + 2 + 3 + S:5 + S:5 + 3, 4 Successes] = 4',
|
||||||
|
'`[[10d6<5]]` => [S:5 + S:2 + S:2 + **6** + S:2 + **6** + S:4 + S:4 + **6** + S:3, 7 Successes] = **7**',
|
||||||
|
'`[[10d6>5f<2]]` => [__F:1__ + **S:6** + __F:1__ + S:5 + S:5 + S:5 + 4 + **S:6** + 4 + __F:1__, 5 Successes, 3 Fails] = **__2__**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'target-fail',
|
||||||
|
{
|
||||||
|
name: 'Target Failures',
|
||||||
|
description: `**Usage:** \`xdyfz\`, \`xdyf=z\`, \`xdyf<z\`, or \`xdyf>z\`
|
||||||
|
\`z\` - Number to compare to for configuring the fail target
|
||||||
|
|
||||||
|
Will set the result to be the number of fails (as a negative number). When combined with the Target Number/Successes option, the successes will be added to the result.
|
||||||
|
|
||||||
|
**Notice:** Cannot be combined with the Dice Matching option`,
|
||||||
|
example: [
|
||||||
|
'`[[10d6f5]]` => [**6** + 4 + **6** + 3 + F:5 + __1__ + **6** + F:5 + 4 + 2, 2 Fails] = **__-2__**',
|
||||||
|
'`[[10d6f=5]]` => [**6** + 4 + **6** + 3 + F:5 + __1__ + **6** + F:5 + 4 + 2, 2 Fails] = **__-2__**',
|
||||||
|
'`[[10d6f>5]]` => [2 + 3 + 3 + **F:6** + F:5 + 2 + __1__ + **F:6** + 2 + **F:6**, 4 Fails] = **__-4__**',
|
||||||
|
'`[[10d6f<5]]` => [F:4 + F:5 + **6** + F:4 + F:3 + F:5 + F:4 + F:2 + F:4 + **6**, 8 Fails] = **-8**',
|
||||||
|
'`[[10d6>5f<2]]` => [F:2 + 4 + __F:1__ + __F:1__ + __F:1__ + **S:6** + __F:1__ + **S:6** + __F:1__ + 3, 2 Successes, 6 Fails] = **__-4__**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DiceOptionsHelpPages: HelpPage = {
|
export const DiceOptionsHelpPages: HelpPage = {
|
||||||
|
|
|
@ -37,8 +37,8 @@ Rolls a Fate die, has 6 sides with values \`[-1, -1, 0, 0, 1, 1]\`.`,
|
||||||
Rolls a specified number of 10 sided dice and counts successful and failed rolls.`,
|
Rolls a specified number of 10 sided dice and counts successful and failed rolls.`,
|
||||||
example: [
|
example: [
|
||||||
'`[[cwod]]` => [3, 0 Successes, 0 Fails] = 0',
|
'`[[cwod]]` => [3, 0 Successes, 0 Fails] = 0',
|
||||||
'`[[4cwod]]` => [**10** + __1__ + 9 + __1__, 1 Success, 2 Fails] = **__1__**',
|
'`[[4cwod]]` => [8 + __F:1__ + **S:10** + **S:10**, 2 Successes, 1 Fail] = **__2__**',
|
||||||
'`[[5cwod8]]` => [**10** + 2 + 5 + **8** + 4, 2 Successes, 0 Fails] = **2**',
|
'`[[5cwod8]]` => [__F:1__ + S:8 + 6 + 7 + S:9, 2 Successes, 1 Fail] = __2__',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue