|
|
@@ -1,10 +1,13 @@
|
|
|
+const takeRegex = () => /\b(take|keep|drop|lose)\s+(?:the\s+)?(top|highest|high|best|bottom|lowest|low|worst)\s+(one|two|three|four|five|six|seven|eight|nine|ten|\d+)/i
|
|
|
const rollRegex = () => /\b(roll|flip)\b/i
|
|
|
const diceRegex = () => /\b(\d*)[dD](\d+)\b(?:\s?([+-])\s?(\d+))?/g
|
|
|
const rerollRegex = () => /\b(reroll|re-roll)\s+((?:(?:\S*|\d+s|\d+'s|\d+)\s*)+)/
|
|
|
-const numberRegex = () => /\b(one|two|three|four|five|six|seven|eight|nine|ten|ones|twos|threes|fours|fives|sixes|sevens|eights|nines|tens|one's|two's|three's|four's|five's|seven's|eight's|nine's|ten's|\d+)\b/gi
|
|
|
+const numberRegex = () => /\b(take|keep|drop|lose|one|two|three|four|five|six|seven|eight|nine|ten|ones|twos|threes|fours|fives|sixes|sevens|eights|nines|tens|one's|two's|three's|four's|five's|seven's|eight's|nine's|ten's|\d+)\b/gi
|
|
|
const advRegex = () => /\b(disadvantage|advantage)\b/i
|
|
|
const percentRegex = () => /\b(percentile|percentiles|percent)\b/i
|
|
|
const dicebotRegex = () => /\b(dicebot|dice bot)\b/i
|
|
|
+const multiplierRegex = () => /\bx(-?\d+)/i
|
|
|
+const helpRegex = () => /(dicebot|roll) help/i
|
|
|
const unicoder = require('./unicoder')
|
|
|
|
|
|
const d6 = {
|
|
|
@@ -77,14 +80,19 @@ class Roll {
|
|
|
} else if (this.bonus > 0) {
|
|
|
str += ` + ${this.bonus}`
|
|
|
}
|
|
|
- if (this.values.length > 1 || this.bonus) {
|
|
|
- str += ` = ${this.formatter && this.formatter.total ? this.formatter.total(this.values, this.bonus || 0, this) : this.value()}`
|
|
|
- }
|
|
|
if (this.rerolls === 1) {
|
|
|
str += ` with 1 reroll`
|
|
|
} else if (this.rerolls > 1) {
|
|
|
str += ` with ${this.rerolls} rerolls`
|
|
|
}
|
|
|
+ if (this.dropped && this.dropped.length) {
|
|
|
+ str += ` dropping ${this.dropped.map(v => this.formatter && this.formatter.stringify ? this.formatter.stringify(v) : `[${v}]`).join(', ')}`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.values.length > 1 || this.bonus) {
|
|
|
+ str += ` = ${this.formatter && this.formatter.total ? this.formatter.total(this.values, this.bonus || 0, this) : this.value()}`
|
|
|
+ }
|
|
|
+
|
|
|
return str
|
|
|
}
|
|
|
}
|
|
|
@@ -113,47 +121,66 @@ class Dice {
|
|
|
if (!this.rng) this.rng = () => Math.random()
|
|
|
}
|
|
|
roll() {
|
|
|
- if (this.count > 100) return new BadRoll(`I lost count rolling ${this}`)
|
|
|
- if (this.sides > 256) return new BadRoll(`The faces are too small to read on ${this}`)
|
|
|
- if (this.count === 0) return new BadRoll(`I finished before I started rolling ${this}`)
|
|
|
- if (this.sides === 0) return new BadRoll(`I lost my grip on ${this}`)
|
|
|
- if (this.sides === 1) return new BadRoll(`The mobius ${this} never stopped rolling`)
|
|
|
- if (this.bonusSign === '-' && this.bonus > this.sides) return new BadRoll(`It doesn't seem fair to roll ${this}`)
|
|
|
-
|
|
|
- let values = []
|
|
|
- for (let i = 0; i < this.count; i++) {
|
|
|
- if (this.modifier === 'advantage') {
|
|
|
- values.push(Math.max(random(this.rng, this.sides), random(this.rng, this.sides)))
|
|
|
- } else if (this.modifier === 'disadvantage') {
|
|
|
- values.push(Math.min(random(this.rng, this.sides), random(this.rng, this.sides)))
|
|
|
- } else {
|
|
|
- values.push(random(this.rng, this.sides))
|
|
|
+ if (this.multiplier === 0) return [ new BadRoll(`That was easy rolling ${this}`)]
|
|
|
+ if (this.multiplier > 32) return [new BadRoll(`I can't keep track of ${this}`)]
|
|
|
+ if (this.multiplier < 0) return [new BadRoll(`I finished before I started rolling ${this}`)]
|
|
|
+ return Array(this.multiplier).fill().map(() => {
|
|
|
+ if (this.count > 100) return new BadRoll(`I lost count rolling ${this}`)
|
|
|
+ if (this.sides > 256) return new BadRoll(`The faces are too small to read on ${this}`)
|
|
|
+ if (this.count === 0) return new BadRoll(`I finished before I started rolling ${this}`)
|
|
|
+ if (this.sides === 0) return new BadRoll(`I lost my grip on ${this}`)
|
|
|
+ if (this.sides === 1) return new BadRoll(`The mobius ${this} never stopped rolling`)
|
|
|
+ if (this.bonusSign === '-' && this.bonus > this.sides) return new BadRoll(`It doesn't seem fair to roll ${this}`)
|
|
|
+ let values = []
|
|
|
+ for (let i = 0; i < this.count; i++) {
|
|
|
+ if (this.modifier === 'advantage') {
|
|
|
+ values.push(Math.max(random(this.rng, this.sides), random(this.rng, this.sides)))
|
|
|
+ } else if (this.modifier === 'disadvantage') {
|
|
|
+ values.push(Math.min(random(this.rng, this.sides), random(this.rng, this.sides)))
|
|
|
+ } else {
|
|
|
+ values.push(random(this.rng, this.sides))
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- let bonus = 0
|
|
|
- switch (this.bonusSign) {
|
|
|
- case '-': bonus = -this.bonus; break
|
|
|
- case '+': bonus = this.bonus; break
|
|
|
- }
|
|
|
- let rerolls = 0
|
|
|
- for (let i = 0; i < 10; i++) {
|
|
|
- let foundAny = false
|
|
|
- for (let d = 0; d < values.length; d++) {
|
|
|
- const v = values[d]
|
|
|
- if (this.rerollNumbers && this.rerollNumbers.includes(v)) {
|
|
|
- values[d] = random(this.rng, this.sides)
|
|
|
- foundAny = true
|
|
|
- rerolls++
|
|
|
+ let bonus = 0
|
|
|
+ switch (this.bonusSign) {
|
|
|
+ case '-': bonus = -this.bonus; break
|
|
|
+ case '+': bonus = this.bonus; break
|
|
|
+ }
|
|
|
+ let rerolls = 0
|
|
|
+ for (let i = 0; i < 10; i++) {
|
|
|
+ let foundAny = false
|
|
|
+ for (let d = 0; d < values.length; d++) {
|
|
|
+ const v = values[d]
|
|
|
+ if (this.rerollNumbers && this.rerollNumbers.includes(v)) {
|
|
|
+ values[d] = random(this.rng, this.sides)
|
|
|
+ foundAny = true
|
|
|
+ rerolls++
|
|
|
+ }
|
|
|
}
|
|
|
+ if (!foundAny) break
|
|
|
}
|
|
|
- if (!foundAny) break
|
|
|
- }
|
|
|
- return new Roll({
|
|
|
- values,
|
|
|
- bonus,
|
|
|
- rerolls,
|
|
|
- percent: this.percent,
|
|
|
- formatter: this.formatter
|
|
|
+
|
|
|
+ let dropped = []
|
|
|
+ if (this.takeOrDrop) {
|
|
|
+ let c = this.takeOrDrop.count
|
|
|
+ if (this.takeOrDrop.takeDrop === 'drop') {
|
|
|
+ c = Math.max(0, values.length - c)
|
|
|
+ }
|
|
|
+ if (this.takeOrDrop.highLow === 'highest') {
|
|
|
+ values.sort((a,b) => b - a)
|
|
|
+ } else {
|
|
|
+ values.sort((a,b) => a - b)
|
|
|
+ }
|
|
|
+ dropped = values.splice(c)
|
|
|
+ }
|
|
|
+ return new Roll({
|
|
|
+ values,
|
|
|
+ dropped,
|
|
|
+ bonus,
|
|
|
+ rerolls,
|
|
|
+ percent: this.percent,
|
|
|
+ formatter: this.formatter
|
|
|
+ })
|
|
|
})
|
|
|
}
|
|
|
toString() {
|
|
|
@@ -178,21 +205,108 @@ class Dice {
|
|
|
str += ` and reroll ${this.rerollNumbers.slice(0, this.rerollNumbers.length - 1).join(`'s, `)}'s, and ${this.rerollNumbers[this.rerollNumbers.length - 1]}'s`
|
|
|
}
|
|
|
}
|
|
|
+ if (this.takeOrDrop) {
|
|
|
+ str += ` and ${this.takeOrDrop.takeDrop} the ${this.takeOrDrop.highLow} ${this.takeOrDrop.count}`
|
|
|
+ }
|
|
|
+ if (this.multiplier !== 1) {
|
|
|
+ str += ` x${this.multiplier}`
|
|
|
+ }
|
|
|
return str
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Dice.parse = (str, opts) => {
|
|
|
+ str = str.toLowerCase()
|
|
|
const rRegex = rollRegex()
|
|
|
const roll = rRegex.exec(str)
|
|
|
const rrRegex = rerollRegex()
|
|
|
const rerolls = []
|
|
|
const reroll = rrRegex.exec(str)
|
|
|
const rerollNumbers = []
|
|
|
+ const rtake = takeRegex()
|
|
|
+ const take = rtake.exec(str)
|
|
|
+ const mult = multiplierRegex().exec(str)
|
|
|
+
|
|
|
+ let multiplier = 1
|
|
|
+ if (mult) {
|
|
|
+ multiplier = parseFloat(mult[1])
|
|
|
+ }
|
|
|
+
|
|
|
+ let todTakeDrop = ''
|
|
|
+ let todHighLow = ''
|
|
|
+ let todCount = 0
|
|
|
+ if (take) {
|
|
|
+ switch (take[1]) {
|
|
|
+ case 'take':
|
|
|
+ case 'keep':
|
|
|
+ todTakeDrop = 'take'
|
|
|
+ break
|
|
|
+ case 'drop':
|
|
|
+ case 'lose':
|
|
|
+ todTakeDrop = 'drop'
|
|
|
+ break
|
|
|
+ }
|
|
|
+ switch (take[2]) {
|
|
|
+ case 'top':
|
|
|
+ case 'highest':
|
|
|
+ case 'high':
|
|
|
+ case 'best':
|
|
|
+ todHighLow = 'highest'
|
|
|
+ break
|
|
|
+ case 'bottom':
|
|
|
+ case 'lowest':
|
|
|
+ case 'low':
|
|
|
+ case 'worst':
|
|
|
+ todHighLow = 'lowest'
|
|
|
+ break
|
|
|
+ }
|
|
|
+ switch (take[3]) {
|
|
|
+ case 'one':
|
|
|
+ todCount = 1
|
|
|
+ break
|
|
|
+ case 'two':
|
|
|
+ todCount = 2
|
|
|
+ break
|
|
|
+ case 'three':
|
|
|
+ todCount = 3
|
|
|
+ break
|
|
|
+ case 'four':
|
|
|
+ todCount = 4
|
|
|
+ break
|
|
|
+ case 'five':
|
|
|
+ todCount = 5
|
|
|
+ break
|
|
|
+ case 'six':
|
|
|
+ todCount = 6
|
|
|
+ break
|
|
|
+ case 'seven':
|
|
|
+ todCount = 7
|
|
|
+ break
|
|
|
+ case 'eight':
|
|
|
+ todCount = 8
|
|
|
+ break
|
|
|
+ case 'nine':
|
|
|
+ todCount = 9
|
|
|
+ break
|
|
|
+ case 'ten':
|
|
|
+ todCount = 10
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ todCount = parseInt(take[3])
|
|
|
+ }
|
|
|
+ }
|
|
|
if (reroll) {
|
|
|
const nRegex = numberRegex()
|
|
|
- while (number = nRegex.exec(str)) {
|
|
|
+ const strRerollNumbers = reroll[2]
|
|
|
+ let broken = false
|
|
|
+ while (!broken && (number = nRegex.exec(strRerollNumbers))) {
|
|
|
switch (number[1]) {
|
|
|
+ case 'drop':
|
|
|
+ case 'keep':
|
|
|
+ case 'take':
|
|
|
+ case 'lose':
|
|
|
+ broken = true
|
|
|
+ break;
|
|
|
case 'ones':
|
|
|
case 'one':
|
|
|
rerollNumbers.push(1)
|
|
|
@@ -261,6 +375,7 @@ Dice.parse = (str, opts) => {
|
|
|
count: 2,
|
|
|
sides: 10,
|
|
|
percent: true,
|
|
|
+ multiplier,
|
|
|
rng: opts && opts.rng
|
|
|
}))
|
|
|
}
|
|
|
@@ -275,6 +390,14 @@ Dice.parse = (str, opts) => {
|
|
|
rerollNumbers,
|
|
|
modifier,
|
|
|
percent,
|
|
|
+ multiplier,
|
|
|
+ takeOrDrop: (todTakeDrop && todHighLow && todCount)
|
|
|
+ ? {
|
|
|
+ takeDrop: todTakeDrop,
|
|
|
+ highLow: todHighLow,
|
|
|
+ count: todCount
|
|
|
+ }
|
|
|
+ : null,
|
|
|
dicebot,
|
|
|
rng: opts && opts.rng
|
|
|
}))
|
|
|
@@ -283,23 +406,36 @@ Dice.parse = (str, opts) => {
|
|
|
}
|
|
|
|
|
|
Dice.chat = (chat, opts) => {
|
|
|
- const dice = Dice.parse(chat, opts).filter(x => x.enabled)
|
|
|
- if (dice.length) {
|
|
|
- const diceStrings = dice.map(x => x.toString())
|
|
|
- if (diceStrings.length > 1) {
|
|
|
- diceStrings[diceStrings.length - 1] = `and ${diceStrings[diceStrings.length - 1]}`
|
|
|
+ if (helpRegex().test(chat)) {
|
|
|
+ return `Tell me to roll dice by saying "Roll" followed by dice notation. For example, \`Roll a D20\`, or \`Roll 4d4+2\`. You can also \`Roll percentile\` to roll two D10's as 1-100, or \`Roll a D100\` (It's just as random). Also try \`Roll a D20 with advantage\`, \`Roll a D20 at disadvantage\`, or \`Roll 4d6 and reroll ones\`. You can roll a nice stat block with \`Roll 4d6, reroll ones, take the top 3 x6\`. If you mess up, you can edit your message without tainting the outcome.`
|
|
|
+ } else {
|
|
|
+ const dice = Dice.parse(chat, opts).filter(x => x.enabled)
|
|
|
+ if (dice.length) {
|
|
|
+ const diceStrings = dice.map(x => x.toString())
|
|
|
+ if (diceStrings.length > 1) {
|
|
|
+ diceStrings[diceStrings.length - 1] = `and ${diceStrings[diceStrings.length - 1]}`
|
|
|
+ }
|
|
|
+ const isCoin = dice.length === 1 && dice[0].sides === 2
|
|
|
+ const rollingString = `${isCoin ? 'Flipping' : 'Rolling'} ${diceStrings.join(', ')}...`
|
|
|
+ const rolls = dice.flatMap(die => die.roll())
|
|
|
+ const rollsSplits = rolls.map(roll => roll.toString().split(' = '))
|
|
|
+ const rollsStrings = rollsSplits.map(x => x[0])
|
|
|
+ const rollsResults = rollsSplits.map(x => x[1]).filter(x => x)
|
|
|
+ const results = rollsResults.length
|
|
|
+ ? ' = ' + rollsResults.join(', ')
|
|
|
+ : ''
|
|
|
+
|
|
|
+ const bonusString = rolls
|
|
|
+ .filter(x => x.formatter && x.formatter.bonusText)
|
|
|
+ .map(x => x.formatter.bonusText(x))
|
|
|
+ .filter(x => x)
|
|
|
+ .map(x => '\n' + x)
|
|
|
+ .join('')
|
|
|
+
|
|
|
+ let rollSummary = rollsStrings.join(', ')
|
|
|
+ if (rollSummary.length > 1000) rollSummary = '... '
|
|
|
+ return `${rollingString} ${rollSummary}${results}.${bonusString}`
|
|
|
}
|
|
|
- const isCoin = dice.length === 1 && dice[0].sides === 2
|
|
|
- const rollingString = `${isCoin ? 'Flipping' : 'Rolling'} ${diceStrings.join(', ')}...`
|
|
|
- const rolls = dice.map(die => die.roll())
|
|
|
- const rollsStrings = rolls.map(roll => roll.toString())
|
|
|
- const bonusString = rolls
|
|
|
- .filter(x => x.formatter && x.formatter.bonusText)
|
|
|
- .map(x => x.formatter.bonusText(x))
|
|
|
- .filter(x => x)
|
|
|
- .map(x => '\n' + x)
|
|
|
- .join('')
|
|
|
- return `${rollingString} ${rollsStrings.join(', ')}.${bonusString}`
|
|
|
}
|
|
|
}
|
|
|
|