@yipe/dice 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +282 -8
- package/dist/builder/ac.d.ts +44 -1
- package/dist/builder/ac.d.ts.map +1 -1
- package/dist/builder/arguments.d.ts +6 -0
- package/dist/builder/arguments.d.ts.map +1 -0
- package/dist/builder/ast.d.ts +36 -7
- package/dist/builder/ast.d.ts.map +1 -1
- package/dist/builder/attack.d.ts +88 -5
- package/dist/builder/attack.d.ts.map +1 -1
- package/dist/builder/dc.d.ts +13 -0
- package/dist/builder/dc.d.ts.map +1 -1
- package/dist/builder/example.d.ts +11 -15
- package/dist/builder/example.d.ts.map +1 -1
- package/dist/builder/expression.d.ts +72 -0
- package/dist/builder/expression.d.ts.map +1 -0
- package/dist/builder/factory.d.ts +2 -3
- package/dist/builder/factory.d.ts.map +1 -1
- package/dist/builder/index.cjs +3657 -1373
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.js +3650 -1374
- package/dist/builder/index.js.map +1 -1
- package/dist/builder/nodes.d.ts +7 -1
- package/dist/builder/nodes.d.ts.map +1 -1
- package/dist/builder/prob.d.ts +9 -0
- package/dist/builder/prob.d.ts.map +1 -1
- package/dist/builder/roll.d.ts +151 -21
- package/dist/builder/roll.d.ts.map +1 -1
- package/dist/builder/save.d.ts +6 -1
- package/dist/builder/save.d.ts.map +1 -1
- package/dist/builder/types.d.ts +18 -0
- package/dist/builder/types.d.ts.map +1 -1
- package/dist/common/bounce.d.ts +24 -11
- package/dist/common/bounce.d.ts.map +1 -1
- package/dist/common/lru-cache.d.ts +29 -1
- package/dist/common/lru-cache.d.ts.map +1 -1
- package/dist/index.cjs +1219 -421
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1219 -421
- package/dist/index.js.map +1 -1
- package/dist/parser/dice.d.ts +59 -16
- package/dist/parser/dice.d.ts.map +1 -1
- package/dist/parser/parser.d.ts +1 -5
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/rollType.d.ts +4 -4
- package/dist/parser/scaleDice.d.ts +14 -0
- package/dist/parser/scaleDice.d.ts.map +1 -0
- package/dist/pmf/mixture.d.ts +17 -3
- package/dist/pmf/mixture.d.ts.map +1 -1
- package/dist/pmf/pmf.d.ts +118 -33
- package/dist/pmf/pmf.d.ts.map +1 -1
- package/dist/pmf/query.d.ts +25 -12
- package/dist/pmf/query.d.ts.map +1 -1
- package/dist/turn/effects.d.ts +114 -0
- package/dist/turn/effects.d.ts.map +1 -0
- package/dist/turn/index.d.ts +3 -1
- package/dist/turn/index.d.ts.map +1 -1
- package/dist/turn/plan.d.ts +101 -21
- package/dist/turn/plan.d.ts.map +1 -1
- package/dist/turn/state.d.ts +14 -8
- package/dist/turn/state.d.ts.map +1 -1
- package/dist/turn/turn.d.ts +127 -26
- package/dist/turn/turn.d.ts.map +1 -1
- package/dist/turn/types.d.ts +154 -17
- package/dist/turn/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,6 +179,121 @@ RollBuilder (d20, d6, roll(), etc.)
|
|
|
179
179
|
└─ .toPMF() ──► PMF
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
**Crits double the damage dice, never the flats.** An `onHit` payload with no `onCrit` override
|
|
183
|
+
crits with its dice doubled: `roll(2, d6).plus(5)` and the string `"2d6+5"` both crit as `4d6 + 5`.
|
|
184
|
+
A pool doubles inside, then pools — `roll(2, d6).plus(3).keepHighestAll(2, 1)` crits as
|
|
185
|
+
`roll(4, d6).plus(3).keepHighestAll(2, 1)` (mean 18.93), not as the whole pool rolled twice (22.74).
|
|
186
|
+
Rider damage doubles the same way; a bare `PMF` has no dice, so it is added as-is. `onCrit(...)` and
|
|
187
|
+
`noCrit()` stay explicit, and a damage string containing an attack or save check (`AC`/`DC`) throws
|
|
188
|
+
when used as a payload that doubles (`onHit`, `doubleDice()`); as a rider it is added as-is on a
|
|
189
|
+
crit, like the attack or save builder it stands for.
|
|
190
|
+
An attack string with no crit clause crits too: `"(d20 + 8 AC 16) * (2d6)"` rolls its natural 20 as
|
|
191
|
+
`4d6`, like `d20.plus(8).ac(16).onHit(roll(2, d6))`; a `crit (…)` clause still wins. A term after
|
|
192
|
+
the payload joined by `+`, `*`, `**`, `/` or `//` is part of the payload and never applies to a
|
|
193
|
+
miss (the grammar reads left to right). `+` adds to every landed hit and crit, and to every
|
|
194
|
+
other outcome that carries a payload (miss damage, a potent-cantrip half, a save's failure or half),
|
|
195
|
+
one whose payload rolled 0 included, like the builder's `plus`: `(d20 + 5 AC 12) * (1d4 - 1) + 1d6` is
|
|
196
|
+
`onHit(roll(1, d4).minus(1).plus(d6))` (mean 3.8). `*`, `**`, `/` and `//` act on the payload's
|
|
197
|
+
value, so a hit that deals 0 still deals 0. `(d20 + 5 AC 15) * (1d8) + 1d6` crits as `2d8 + 2d6`,
|
|
198
|
+
like `onHit(roll(1, d8).plus(roll(1, d6)))`, and keeps its hit, crit and miss labels. After a
|
|
199
|
+
`crit (…)` clause the term is added to the crit as written, and after a `miss (…)` clause to the
|
|
200
|
+
miss damage too. The crit rate reads the check's natural die, its one d20 wherever it sits in the
|
|
201
|
+
sum (with no d20, its largest die), through bonus to-hit dice, advantage (`d20 > d20`, `d20!`,
|
|
202
|
+
`2kh1d20`), disadvantage (`2kl1(1d20)`), elven accuracy (`3kh1(1d20)`) and halfling luck (`hd20`,
|
|
203
|
+
`d20 reroll 1`), for `crit` and `xcrit N` alike: `(d20 > d20 + 5 + 1d4 AC 15) * (2d6)` crits at
|
|
204
|
+
39/400, like `d20.withAdvantage().plus(5).plus(d4)`, `(1d4 + d20 + 5 AC 15)` crits on the d20, not
|
|
205
|
+
the d4, and `(d20 + d100 AC 60)` on the d20, not the d100 (61/2000). A max or min against another
|
|
206
|
+
die or a number crits where the natural 20 is the value kept: `(d20 > d4 + 5 AC 10)` on every
|
|
207
|
+
natural 20, `(d20 < 15 + 5 AC 10)` never. An `&` mix crits where one of its sides rolls its natural
|
|
208
|
+
20, at that side's share of the mix, in either order and with the AC gate on either side:
|
|
209
|
+
`(d4 & d20 AC 5) * (1d6) crit (2d6)` and `(d4 & (d20 AC 5)) * (1d6)` both crit at 1/24. A die on
|
|
210
|
+
the AC side is the target's roll, never the natural roll. A check with no single natural die
|
|
211
|
+
(`2d20`, `d20 + d20`, `d20 + d20 + d100`, `2kh2(1d20)`, advantage over a total like `(d20 + 1d4)!`,
|
|
212
|
+
`d20 + 5 > d20`, a reroll, repeat, keep or double advantage of a mix with a smaller die in it, like
|
|
213
|
+
`(d20 & d4) reroll 1`, `1(d20 & d4)`, `2kh1(d20 & d4)` or `(d20 & d4)!!`) throws `crit rate cannot
|
|
214
|
+
be computed exactly …` when it would crit, and so does an `xcrit N` wider than the die. A check with
|
|
215
|
+
no die at all (`(15 AC 12) * (1d6)`, `(25 AC d20) * (1d6)`) has no natural roll, so it never crits,
|
|
216
|
+
and a `crit (…)` or `xcrit N` clause on it is inert: `(15 AC 12) * (1d6) crit (2d6)` means 3.5, like
|
|
217
|
+
`roll.flat(15).ac(12).onHit(d6)`, which emits that string.
|
|
218
|
+
|
|
219
|
+
**`&` shapes that are refused.** An `&` mix weights each side by its count of outcomes, so these
|
|
220
|
+
shapes have no single reading and throw rather than return a number that depends on how they are
|
|
221
|
+
spelled:
|
|
222
|
+
|
|
223
|
+
- `&` with dice on either side inside a payload that doubles on a crit: an attack string with no
|
|
224
|
+
crit clause (`(d20 + 5 AC 15) * (1d6 & 3)`, or a trailing `+ (1d4 & 2)`), `onHit("1d6 & 3")`, a
|
|
225
|
+
rider's auto-crit and `doubleDice()`. Doubling the dice also changes each side's share of the
|
|
226
|
+
mix (`1d6 & 3` → `2d6 & 3` moves the flat's share from 1/7 to 1/37), so this throws
|
|
227
|
+
`AmbiguousCritDoublingError`. Give the crit explicitly: `… * (1d6 & 3) crit (2d6 & 3)` parses.
|
|
228
|
+
- `&` with an attack already split into crit, miss or save outcomes (`((d20 AC 5) * (1d6)) & d4`,
|
|
229
|
+
`(d20 + 5 AC 15) * (1d6) & 3`), `&` of a saving throw with anything but another saving throw
|
|
230
|
+
(`d4 & (d20 DC 12)`), and a crit, save, pc or miss clause right after an `&` mix
|
|
231
|
+
(`(d20 + 5 AC 15) & (1d6) crit (2d6)`). Mix the checks before the payload instead:
|
|
232
|
+
`((d20 AC 10) & (d20 AC 15)) * (1d6)`.
|
|
233
|
+
- A reroll, repeat, keep or double advantage of a mix with a smaller die in it, when it would crit
|
|
234
|
+
(listed above). `(1(d20 & d4) AC 5) * (1d6) crit (2d6)` and `(2kh1(d20 & d4) AC 15) * (1d6) crit
|
|
235
|
+
(2d6)` were exact in 0.11.0 (119/48 and 2093/1152) and now throw. Spell the second as
|
|
236
|
+
`((d20 & d4)! AC 15)`, which gives the same exact value.
|
|
237
|
+
|
|
238
|
+
A parsed attack still differs from the builder in these cases:
|
|
239
|
+
|
|
240
|
+
- `parse()` has no natural-1 miss and no natural-20 hit: `(d20+30 AC 5) * (1d8)` means 4.725, the
|
|
241
|
+
builder 4.5.
|
|
242
|
+
- A payload the doubling rewrite cannot read (`d4d6`, a nested check) is added to the crit as-is;
|
|
243
|
+
the builder's `onHit` throws.
|
|
244
|
+
- A term joined by an op that also changes a miss (`~+`, `-`, `>`, `<`, `=`, `reroll`, `!`) loses
|
|
245
|
+
the attack's crit and miss labels, so riders never see its crit. So do a clause after a trailing
|
|
246
|
+
term (`… * (2d6) + 3 miss (1d6)`) and a repeat wrapper around an attack string
|
|
247
|
+
(`2((d20 + 5 AC 15) * (2d6))`). An `&` there throws (see above).
|
|
248
|
+
|
|
249
|
+
**Crits on keeps.** A keep-highest-of-1 payload, meaning "roll it N times, keep the best", doubles
|
|
250
|
+
its dice inside each trial on a crit. For example, `roll(2,d6).keepHighest(2,1).plus(3)` crits as
|
|
251
|
+
`2kh1(4d6) + 3` (18.9334, the same as the pooled `keepHighestAll(2,1)`), and the parsed
|
|
252
|
+
`2kh1(2d6)+3` crits as `2kh1(4d6)+3`. Other shapes have more than one reasonable doubled meaning,
|
|
253
|
+
so `doubleDice()`/`scaleDice()` throws an Error rather than guessing. The throw names the shape and
|
|
254
|
+
asks for an explicit crit. The shapes that throw are: a per-die keep with K >= 2
|
|
255
|
+
(`roll(4,d6).keepHighest(4,3)`, which used to crit at 88.98); any keepLowest (`3kl1`, `2kl1`); a
|
|
256
|
+
`bestOf()` that is a keep, or becomes one when doubled (`roll(4,d6).bestOf(3)`,
|
|
257
|
+
`roll(2,d6).bestOf(3)`); a non-d20 die rolled with advantage, disadvantage or elven accuracy
|
|
258
|
+
(`d6.withAdvantage()`, which used to crit at the hit's mean); a parsed `NkhK(...)` with K >= 2 or
|
|
259
|
+
any `NklK(...)`, including when nested (`4kh3d6`, `3kh2(2d6+1)`, `2kh1(4kh3(1d6))`); a parsed
|
|
260
|
+
min of two dice terms (`d6 < d6`); and a parsed `&` mix with dice on either side (`1d6 & 3`). This
|
|
261
|
+
covers an attack's auto-crit (`resolve()` and `toExpression()` throw), a rider's auto-crit, and an
|
|
262
|
+
attack string with no crit clause
|
|
263
|
+
(`(d20+5 AC 12) * (4kh3(1d6))` now fails to parse). You can fix any of these with `onCrit(...)`, a
|
|
264
|
+
rider's `critDamage`, a `crit (...)` clause, or `noCrit()`. The attack check's own d20 advantage is
|
|
265
|
+
never doubled and is unaffected. Three things still do not throw: `keepHighestAll`/`keepLowestAll`
|
|
266
|
+
pools, which double inside and then pool as before; a flat cap or floor like `2d6 < 9` or `3>d6`;
|
|
267
|
+
and `diceMatchInfo()`, whose crit descriptor is `null` for such an attack (its hit side is unchanged).
|
|
268
|
+
|
|
269
|
+
**Builder semantics.** `roll(N, X)` is N independent copies of X, so `roll(2, d6.keepHighest(2, 1))`
|
|
270
|
+
is two best-of-two d6 (161/18), and a group of zero dice rolls nothing (`roll(0, d6).plus(3)` is 3).
|
|
271
|
+
A roll type applies to each die of its group: `roll(2, d6).withAdvantage()` is two advantaged d6.
|
|
272
|
+
`explode(k)` needs a finite cap k, sides must be finite and not negative (`roll(2, 0)` is no die),
|
|
273
|
+
`scaleResult()` needs a finite numerator and a non-zero finite denominator, and `ac()`, `dc()`,
|
|
274
|
+
`critOn()`, `minimumDamageDie()`, `rerollDamage()` and a `withCheck()` result need finite numbers;
|
|
275
|
+
each throws, naming the argument. Per-die keeps have one reading each: `roll(1, d).keepHighest(T, K)`
|
|
276
|
+
keeps K of T dice, `roll(N, d).keepHighest(N, K)` keeps K of the N dice, and
|
|
277
|
+
`roll(N, d).keepHighest(T, 1)` is the best of T rolls of the whole group (`keepLowest(T, 1)` with
|
|
278
|
+
T ≠ N the worst); any other keep on several dice throws `AmbiguousKeepError`, so use
|
|
279
|
+
`keepHighestAll`/`keepLowestAll` or a keep on one die. `minus(X)` subtracts X's flat along with its
|
|
280
|
+
dice. `half()`, `scaleResult()` and `maxOf()` keep their transform under `plus()`/`minus()`.
|
|
281
|
+
A check's natural roll is its d20 wherever it sits in the sum (with no d20, its largest die);
|
|
282
|
+
`withAdvantage()`/`withDisadvantage()`/`withElvenAccuracy()` apply to that d20 whatever the call
|
|
283
|
+
order; a natural roll of more than one die (`roll(2, d20).ac(15)`) throws; a check with no die
|
|
284
|
+
(`flat(15).ac(12)`) compares its total with the target and never crits; a natural 20 always crits,
|
|
285
|
+
including under `alwaysHits()` and `alwaysCrits()`. A parsed string cannot be a check
|
|
286
|
+
(`d("d20+5").ac(15)` throws `ParsedCheckError`); spell it with the builder or as a full attack string.
|
|
287
|
+
`rerollDamage(k)` rerolls a face when its kept value, after any `minimumDamageDie` floor, is below
|
|
288
|
+
a fresh die's expected value, and never lowers a payload's own `reroll` or `minimum`.
|
|
289
|
+
|
|
290
|
+
**Strings from builders.** `toExpression()` prints a string that `parse()` reads back to the
|
|
291
|
+
builder's own distribution: a term after the first is parenthesised when it is an expression of its
|
|
292
|
+
own, `~+` joins a term to a running total that can be 0, and an attack always carries its crit clause
|
|
293
|
+
(`noCrit()` prints `xcrit0 (<hit payload>)`, which crits on no natural face). Explode, pool-wide explode, `scaleResult(…, "round")`,
|
|
294
|
+
fractional scale factors, `plusSeparateDamage()` and `halfOnMiss()` have no spelling and throw.
|
|
295
|
+
Strings have no natural-1 miss or natural-20 hit.
|
|
296
|
+
|
|
182
297
|
### Core Class Flow
|
|
183
298
|
|
|
184
299
|
```
|
|
@@ -317,6 +432,49 @@ const query = new DiceQuery(pmf);
|
|
|
317
432
|
console.log("DPR:", query.mean());
|
|
318
433
|
```
|
|
319
434
|
|
|
435
|
+
#### Grammar
|
|
436
|
+
|
|
437
|
+
Spaces are ignored and letters may be any case. Every binary operator has the **same precedence and
|
|
438
|
+
associates left to right**: `1d6 + 2 * 3` is `(1d6 + 2) * 3`. Parenthesise to group.
|
|
439
|
+
|
|
440
|
+
| Syntax | Meaning |
|
|
441
|
+
| --- | --- |
|
|
442
|
+
| `7`, `d6`, `3d6`, `hd20` | A number, a die, a sum of dice, a die whose 1 is rerolled once (halfling luck, `d20 reroll 1`). |
|
|
443
|
+
| `N(X)`, `(X)d6` | N independent copies of X, summed. The count may be rolled: `(1d4)d6`. A count of 0 is 0 (`0d6`, `(1d4 - 1)d6` has P(0) = 1/4); a count that can be negative throws. |
|
|
444
|
+
| `NkhK(X)`, `NklK(X)` | The sum of the K highest (lowest) of N independent copies of X, exact for any X: `4kh3d6`, `2kl1(1d20)`, `4kh1(2d20)`. `2kl1(2d6)` keeps the lower of two 2d6 *sums*. |
|
|
445
|
+
| `X + Y` | Adds Y where the total so far is not 0, so a miss (0) stays 0. After an attack's or a save's payload it adds to every outcome that carries a payload (a landed hit or crit, a miss clause's damage, a potent-cantrip half, a save's failure or half), one that deals 0 included: `(d20 + 5 AC 15) * (1d4 - 1) + 1d6`. Inside a check total (left of `AC`/`DC`) `+` always adds, so `d20 - 5 + 1d4 AC 1` adds the d4 on a natural 5 too. |
|
|
446
|
+
| `X ~+ Y` | Always adds. |
|
|
447
|
+
| `X - Y`, `-X` | Subtracts. A leading `-` negates the argument after it, repeat included: `-2d6` is `-(2d6)`, `1d6 + -3` is `1d6 - 3`, `-1d8 + 1d6` has mean -1. There is no unary `+`. |
|
|
448
|
+
| `X * Y` | Y where X is not 0, else 0: the hit gate of `(check) * (damage)`. |
|
|
449
|
+
| `X ** Y` | The product. |
|
|
450
|
+
| `X / Y`, `X // Y` | Division rounding up, rounding down (toward -∞). A divisor that can be 0 throws. |
|
|
451
|
+
| `X > Y`, `X < Y` | The max, the min: `d20 > d20` is advantage, `3>d6` a floor of 3. |
|
|
452
|
+
| `X!` | The max of two independent copies of X. |
|
|
453
|
+
| `X = Y` | 1 where X equals Y, else 0. |
|
|
454
|
+
| `X & Y` | A mix weighted by each side's count of outcomes (see the refused shapes above). |
|
|
455
|
+
| `X reroll R` | Rolls X, and on a result in the face set R rolls X again and keeps the second roll. |
|
|
456
|
+
| `X AC T`, `X DC T` | An attack check (X where X ≥ T, else 0) and a saving throw (0 on a save, 1 on a failure). A total of exactly 0 that meets a T of 0 or less lands, though it reads 0 like a miss: `(d20 - 5 AC 0) * (1d6)` hits on a natural 5 and crits on a 20, like `d20.minus(5).ac(0).onHit(roll(1, d6))`. |
|
|
457
|
+
| `… crit (Y)`, `… xcritN (Y)`, `… miss (Y)`, `… save half`, `… pc` | Outcome clauses after an attack's or a save's payload. `xcrit0 (Y)` never crits: every landing is a hit. |
|
|
458
|
+
|
|
459
|
+
**Rerolls.** `reroll N` rerolls the face N only; `reroll dN` rerolls every face from 1 to N; the
|
|
460
|
+
builder's `.reroll(N)` rerolls every face up to N, like `reroll dN`. On a d6 they differ from N = 2
|
|
461
|
+
on: `d6 reroll 2` is 15/4, `d6 reroll d2` and `d6.reroll(2)` are 25/6. `reroll d0` rerolls
|
|
462
|
+
nothing. A reroll applies to the whole value on its left, so `2d6 reroll 1` rerolls the *total*,
|
|
463
|
+
which is never 1 (mean 7); spell a per-die reroll as `2(d6 reroll 1)` (47/6), and several of them as
|
|
464
|
+
`1(d8 reroll 1) + 2(d6 reroll 1)` (613/48). Each result keeps its own probability, so a reroll of a
|
|
465
|
+
sum, a max or a floor is exact: `2d6 reroll 2` is 257/36, `(d20 > d20) reroll 1` is 221713/16000.
|
|
466
|
+
The reroll decides on the raw face and a minimum applies after: `3>(d6 reroll 1)` (what
|
|
467
|
+
`roll(1, d6).reroll(1).minimum(3)` prints) is 25/6.
|
|
468
|
+
|
|
469
|
+
**Labels.** An attack labels its outcomes `hit`, `crit`, `missNone` and `missDamage`. A save labels a
|
|
470
|
+
failed save `saveFail` (whatever the payload rolls, 0 included), a halved success `saveHalf`, and a
|
|
471
|
+
success with no `save half` `missNone`, like the builder's `onSaveFailure()`. A landed hit whose
|
|
472
|
+
payload deals 0 is still `hit` (a 0-damage crit is `crit`), like the builder, and so is an AC
|
|
473
|
+
check's landed total of exactly 0; only a miss is `missNone`.
|
|
474
|
+
|
|
475
|
+
A `d0` has no faces: it is only a face set (`reroll d0`); rolled on its own it throws, as does a
|
|
476
|
+
string the grammar cannot read, always as a `DiceParseError`.
|
|
477
|
+
|
|
320
478
|
### Error Handling
|
|
321
479
|
|
|
322
480
|
`parse()` throws a `DiceParseError` (a subclass of `Error`) for invalid input.
|
|
@@ -335,14 +493,15 @@ try {
|
|
|
335
493
|
```
|
|
336
494
|
|
|
337
495
|
For UI code that parses on every keystroke, `tryParse()` returns an empty PMF
|
|
338
|
-
instead of throwing, and
|
|
339
|
-
a half-typed damage field is one for a keystroke or two:
|
|
496
|
+
instead of throwing, and also reads an integer with a leading `+`, which the
|
|
497
|
+
grammar rejects — a half-typed damage field is one for a keystroke or two:
|
|
340
498
|
|
|
341
499
|
```ts
|
|
342
500
|
import { tryParse } from "@yipe/dice";
|
|
343
501
|
|
|
344
502
|
tryParse("1d6 + 2").mean(); // 5.5
|
|
345
|
-
tryParse("
|
|
503
|
+
tryParse("+7").mean(); // 7 — a leading `+`, which the grammar rejects
|
|
504
|
+
tryParse("-3").mean(); // -3 — the same as parse("-3")
|
|
346
505
|
tryParse("0x10").mass(); // 0 — decimal only
|
|
347
506
|
tryParse("1d").mass(); // 0 — empty PMF
|
|
348
507
|
```
|
|
@@ -400,7 +559,8 @@ chance of 0.015 instead of 0.1225.
|
|
|
400
559
|
|---|---|---|
|
|
401
560
|
| `onFirstHit` | once, on the first attack that lands — doubled if it crit | Sneak Attack |
|
|
402
561
|
| `onAnyCrit` | once, if any attack crit | Divine Smite |
|
|
403
|
-
| `onAnyMiss` | once, if any attack missed |
|
|
562
|
+
| `onAnyMiss` | once, if any attack missed; runs after every attack | a reroll no grant reaches |
|
|
563
|
+
| `onFirstMiss` | once, on the first attack that missed; runs right after it | Unerring Accuracy, Lucky |
|
|
404
564
|
| `onEveryHit` | once per attack that lands | Hunter's Mark, Hex, Rage |
|
|
405
565
|
| `otherwise` | when the rider before it did *not* | flurry of blows if you didn't smite |
|
|
406
566
|
|
|
@@ -434,6 +594,10 @@ turn([sword, sword]).onEveryHit(flat(2)); // Rage
|
|
|
434
594
|
turn([dagger, dagger]).onAnyCrit(roll(4, d8)).otherwise([unarmed, unarmed]); // smite, or flurry
|
|
435
595
|
```
|
|
436
596
|
|
|
597
|
+
A list of attacks deals their exact summed damage, but it is one payload, not attacks that each
|
|
598
|
+
land: it is not a watchable attack. Naming it in `of` throws `not-an-attack`, and it never joins a
|
|
599
|
+
later rider's default `of`. To watch each strike, give each strike its own rider.
|
|
600
|
+
|
|
437
601
|
#### The hard build
|
|
438
602
|
|
|
439
603
|
A goliath rogue/monk/paladin, every trigger at once:
|
|
@@ -444,7 +608,7 @@ const goliath = turn([dagger, dagger])
|
|
|
444
608
|
.onFirstHit(d10) // fire's burn
|
|
445
609
|
.onAnyCrit(roll(2, d8)) // divine smite
|
|
446
610
|
.otherwise([unarmed, unarmed]) // flurry of blows, if the smite didn't happen
|
|
447
|
-
.onEveryHit(d6); // hunter's mark
|
|
611
|
+
.onEveryHit(d6); // hunter's mark, on the daggers
|
|
448
612
|
|
|
449
613
|
goliath.mean(); // 39.5903
|
|
450
614
|
goliath.toQuery().damageAttributionChartModel();
|
|
@@ -453,7 +617,8 @@ goliath.toQuery().damageAttributionChartModel();
|
|
|
453
617
|
Two things that would be easy to get wrong are handled for you. Riders sharing a trigger resolve
|
|
454
618
|
**jointly** — sneak attack and fire's burn fire together or not at all, which shows up in the spread
|
|
455
619
|
even though it never moves the mean. And `otherwise()` binds to the rider immediately before it, so
|
|
456
|
-
the smite and the flurry are two branches of one decision and can never both land.
|
|
620
|
+
the smite and the flurry are two branches of one decision and can never both land. The mark
|
|
621
|
+
watches the two daggers: the flurry is a list rider, which no rider watches.
|
|
457
622
|
|
|
458
623
|
#### Asking questions
|
|
459
624
|
|
|
@@ -467,6 +632,104 @@ t.toQuery().probTotalAtLeast(20); // 0.5062 — P(20+ damage)
|
|
|
467
632
|
t.toQuery().percentiles([0.25, 0.5, 0.75]); // [15, 20, 24]
|
|
468
633
|
```
|
|
469
634
|
|
|
635
|
+
#### Once per turn: keep the better damage roll
|
|
636
|
+
|
|
637
|
+
`onFirstHit` also takes a **transform** instead of damage. `keepBestDamage()` rolls the attack's own
|
|
638
|
+
base payload a second time and keeps the better total — once per turn, on the first attack that
|
|
639
|
+
lands. No dice are restated, and one call covers every attack in the turn:
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import { keepBestDamage } from "@yipe/dice/builder";
|
|
643
|
+
|
|
644
|
+
const attack = d20.plus(5).ac(12).onHit(roll(2, d6).plus(3));
|
|
645
|
+
|
|
646
|
+
turn([attack, attack]).mean(); // 14.7000
|
|
647
|
+
turn([attack, attack]).onFirstHit(keepBestDamage()).mean(); // 15.9849
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
What it rerolls is the **base payload**: a crit transforms the doubled crit dice, while
|
|
651
|
+
`plusSeparateDamage` channels, `every-hit` riders and the miss branch are never rerolled. Spending
|
|
652
|
+
on the first landing is a policy, and not the best one — a player who sees a high roll holds the
|
|
653
|
+
reroll — so the number is a lower bound. `keepBestDamage().ifBelow({ hit, crit })` spends only when
|
|
654
|
+
the base payload total (dice plus the payload's own flat bonus, excluding separate-damage channels)
|
|
655
|
+
is below the threshold for that mode. When no later attack it watches can still land — the last
|
|
656
|
+
one, or an earlier one whose watched reroll can no longer fire — it spends on any landing.
|
|
657
|
+
`ifBelow({ hit: 10, crit: 18 })` reaches the optimum on the turn above, and since the threshold
|
|
658
|
+
reads the payload's own values it works on a parsed string or a bare `PMF` too.
|
|
659
|
+
On a tie the original roll is kept; the total is the same, so that only matters to a dice-match
|
|
660
|
+
trigger reading the same attack.
|
|
661
|
+
`fireProbability(id)` reports P(spent). A second transform over any of the same attacks throws
|
|
662
|
+
`duplicate-substitute`, and a transform passed to any other verb throws `unsupported-trigger`.
|
|
663
|
+
|
|
664
|
+
#### Conditions: advantage, disadvantage and crits granted by earlier attacks
|
|
665
|
+
|
|
666
|
+
The same verbs take a **grant** — a modifier with a lifetime — for the attack rolls after it:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import { advantage, disadvantage, turn } from "@yipe/dice/builder";
|
|
670
|
+
|
|
671
|
+
const sword = d20.plus(5).ac(12).onHit(roll(1, d8).plus(3));
|
|
672
|
+
|
|
673
|
+
// A hit gives the next attack advantage. The next attack uses it up even on a miss, and passes it
|
|
674
|
+
// on if it lands.
|
|
675
|
+
turn([sword, sword]).onEveryHit(advantage().untilNextAttack()).mean(); // 12.202125
|
|
676
|
+
turn([sword, sword, sword]).onEveryHit(advantage().untilNextAttack()).mean(); // 19.192196
|
|
677
|
+
|
|
678
|
+
// A hit gives advantage for the rest of the turn if the target fails a save; each landing tries again.
|
|
679
|
+
turn([axe, axe]).onEveryHit(advantage().untilEndOfTurn(), { save: d20.plus(2).dc(15) });
|
|
680
|
+
|
|
681
|
+
// Once per turn, on the first hit, 40% of the time: advantage, and every later hit is a crit.
|
|
682
|
+
turn([fist, fist, fist]).onFirstHit(advantage().critOnHit().untilEndOfTurn(), { chance: 0.4 });
|
|
683
|
+
|
|
684
|
+
// One save, two grants: advantage for the melee attacks, disadvantage for the ranged ones.
|
|
685
|
+
turn().attack(axe, { tag: "melee" }).attack(bow, { tag: "ranged" })
|
|
686
|
+
.onEveryHit([advantage().untilEndOfTurn().to("melee"),
|
|
687
|
+
disadvantage().untilEndOfTurn().to("ranged")], { of: ["melee"], save: dc });
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
`advantage()`, `disadvantage()` and `critOnHit()` are not grants until they get a lifetime, so
|
|
691
|
+
`onEveryHit(advantage())` does not compile. A grant combines with the reading attack's own roll
|
|
692
|
+
type by cancellation — advantage and disadvantage together roll flat — and a net advantage rolls
|
|
693
|
+
three dice for an attack built with `threeDiceAdvantage()`. `critOnHit` makes every landing a crit;
|
|
694
|
+
a natural 1 still misses. `.to(…)` takes ids or tags; left out, every later attack roll reads the
|
|
695
|
+
grant, rerolls and bonus attacks included. An `onAnyMiss` reroll resolves after every attack, so one
|
|
696
|
+
that would read a grant, or apply one, throws `unsupported-trigger`: use `onFirstMiss`, which
|
|
697
|
+
resolves right after the miss and reads the grants in force there.
|
|
698
|
+
|
|
699
|
+
`chance` or `save` gates the grants only: damage in the same call (`[d8, advantage()…]`) lands
|
|
700
|
+
whatever the save does. `onEveryHit` rolls the save again on each landing until it takes, and
|
|
701
|
+
`onFirstHit` rolls it once. An application whose grants are all `untilEndOfTurn` and all already
|
|
702
|
+
in force rolls no further save; one with any `untilNextAttack` grant always rolls.
|
|
703
|
+
`onSave` applies other grants on the success branch of the same roll:
|
|
704
|
+
`{ chance: 0.4, onSave: advantage().untilNextAttack() }`. `fireProbability(id)` reports P(the grants
|
|
705
|
+
were applied where a later attack reads them). In plain data it is `TurnSpec.conditions`, with the
|
|
706
|
+
`save` already turned into its `chance`.
|
|
707
|
+
|
|
708
|
+
#### Rerolls, sweeping AC, and naming attacks
|
|
709
|
+
|
|
710
|
+
A reroll is an attack-shaped rider. `onFirstMiss(attack)` resolves right after the attack that
|
|
711
|
+
missed, so anything that reads order — a `first-hit` rider's crit mode, a granted advantage — sees
|
|
712
|
+
it where it happened. A rider or transform added *after* an `onAnyMiss` / `onFirstMiss` reroll
|
|
713
|
+
watches it without being told:
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
turn([sword, sword])
|
|
717
|
+
.onFirstMiss(sword) // reroll the first miss
|
|
718
|
+
.onFirstHit(roll(1, d10)) // watches both attacks and the reroll
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
`vsAC(ac)` rebuilds every attack with an AC — including rerolls and bonus attacks carried by
|
|
722
|
+
riders — for a DPR-by-AC sweep, leaving saves untouched:
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
const base = turn([sword, sword]).onFirstHit(roll(3, d6));
|
|
726
|
+
[12, 14, 16, 18].map((ac) => base.vsAC(ac).mean());
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
`attack(source, { tag })` and `attacks(n, source, { tag })` name a group of attacks, and an `of`
|
|
730
|
+
entry that is not an id expands to every attack with that tag — so reordering a turn cannot
|
|
731
|
+
silently retarget a rider the way a positional `attack 2` can.
|
|
732
|
+
|
|
470
733
|
#### Ids, errors, and plain data
|
|
471
734
|
|
|
472
735
|
Nothing above needs an `id`: attacks and riders get `attack 1`, `rider 2`, … in declaration order,
|
|
@@ -482,12 +745,17 @@ paladin.riderIds; // ["smite"]
|
|
|
482
745
|
|
|
483
746
|
Every construction path validates immediately and throws a `TurnSpecError` whose `code` —
|
|
484
747
|
`unknown-id`, `cycle`, `not-an-attack`, `duplicate-id`, `self-reference`, `unused-crit-damage`,
|
|
485
|
-
`too-many-groups`
|
|
748
|
+
`too-many-groups`, `no-dice-descriptor`, `duplicate-substitute`, `attack-after-rider`,
|
|
749
|
+
`no-rebindable-source`, `unsupported-trigger`, `unsupported-policy`, `too-many-flags` — maps
|
|
486
750
|
straight onto a UI field state. A bad `of` fails at the call that introduced it, not later at
|
|
487
751
|
`.mean()`.
|
|
488
752
|
|
|
489
753
|
Each `onX` method takes an optional `{ id, of, critDamage }`, where `of` picks which attacks the
|
|
490
|
-
rider watches
|
|
754
|
+
rider watches. Left out, it is filled in at that call: the attacks declared so far, plus any reroll
|
|
755
|
+
declared so far. So declare attacks first — `.attack()` after such a rider throws
|
|
756
|
+
`attack-after-rider` rather than silently leaving the new attack out. `Turn.from` fills an omitted
|
|
757
|
+
`of` the same way — every attack plus the rerolls, for a rider the rerolls listed before it — so
|
|
758
|
+
both spellings of a turn watch the same sources. All of them are sugar over `rider()`, which takes the
|
|
491
759
|
trigger as plain data — and `Trigger` is JSON-safe, so a UI can persist one and hand it straight
|
|
492
760
|
back:
|
|
493
761
|
|
|
@@ -529,6 +797,12 @@ console.table(query.toChartSeries());
|
|
|
529
797
|
└─────────┴────┴──────────┘
|
|
530
798
|
```
|
|
531
799
|
|
|
800
|
+
Percentiles and `quantile()` land on the exact bin, so a d20's median is 10. `missChance()` is the
|
|
801
|
+
probability that at least one attack misses; for "every attack misses" use
|
|
802
|
+
`probExactlyK(["missNone", "missDamage"], n)`. `turn()`, `Turn.from()` and `resolve()` keep every
|
|
803
|
+
reachable damage value (their `eps` defaults to 0). `setCachingEnabled(false)` turns off and
|
|
804
|
+
empties every internal cache; PMFs returned from a cache have frozen bins.
|
|
805
|
+
|
|
532
806
|
## 🧪 Running Examples
|
|
533
807
|
|
|
534
808
|
This repository includes example scripts:
|
package/dist/builder/ac.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { PMF } from "../pmf/pmf.js";
|
|
2
2
|
import { AttackBuilder } from "./attack.js";
|
|
3
3
|
import { AlwaysCritBuilder, RollBuilder } from "./roll.js";
|
|
4
|
+
import type { RollConfig, RollType } from "./types.js";
|
|
4
5
|
export interface AttackConfig {
|
|
5
6
|
ac: number;
|
|
6
7
|
critThreshold: number;
|
|
8
|
+
advantageDice: 2 | 3;
|
|
7
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Combines a source's own roll type with granted advantage/disadvantage flags by cancellation —
|
|
12
|
+
* `{advantage, disadvantage}` from any combination of source and grants resolve to `advantage` /
|
|
13
|
+
* `disadvantage` / `flat`. `advantageDice` governs how many dice a NET advantage rolls, whether
|
|
14
|
+
* the advantage came from `rollType` itself or from `flags`; an own `elven accuracy` roll type
|
|
15
|
+
* always rolls three. Pure; independently testable against the full 4×2×4 table.
|
|
16
|
+
*/
|
|
17
|
+
export declare function combine(rollType: RollType, advantageDice: 2 | 3, flags: {
|
|
18
|
+
advantage: boolean;
|
|
19
|
+
disadvantage: boolean;
|
|
20
|
+
}): {
|
|
21
|
+
rollType: RollType;
|
|
22
|
+
dice: number;
|
|
23
|
+
};
|
|
8
24
|
export declare class ACBuilder extends RollBuilder {
|
|
9
25
|
readonly attackConfig: AttackConfig;
|
|
10
26
|
constructor(baseRoll: RollBuilder, ac: number, attackConfig?: AttackConfig);
|
|
@@ -16,11 +32,38 @@ export declare class ACBuilder extends RollBuilder {
|
|
|
16
32
|
onHit(count: number, die: RollBuilder, modifier: number): AttackBuilder;
|
|
17
33
|
onHit(count: number, sides: number, modifier: number): AttackBuilder;
|
|
18
34
|
get critThreshold(): number;
|
|
35
|
+
/**
|
|
36
|
+
* Three-dice advantage is an attacker property, not a roll type. Setting it now applies
|
|
37
|
+
* whenever the NET result later resolves to advantage — whether this source already has
|
|
38
|
+
* advantage baked in, or advantage is granted afterward (via `withCheck`).
|
|
39
|
+
*/
|
|
40
|
+
threeDiceAdvantage(): ACBuilder;
|
|
41
|
+
/**
|
|
42
|
+
* The dice configs with `advantageDice` mechanically folded in (`elven accuracy` in place of
|
|
43
|
+
* `advantage` when the net roll should use 3 dice). Shared by every consumer that needs the
|
|
44
|
+
* REAL resolvable dice rather than the raw stored config: {@link getRootDieConfig} (read by
|
|
45
|
+
* `rollType`/`resolveRootD20`) and {@link alwaysCrits} (whose target class has no override).
|
|
46
|
+
*/
|
|
47
|
+
private resolvedConfigs;
|
|
48
|
+
getRootDieConfig(): RollConfig | undefined;
|
|
19
49
|
cacheKey(): string | null;
|
|
50
|
+
/** Sets the crit threshold: a natural roll at or above it is a crit. */
|
|
20
51
|
critOn(threshold: number): ACBuilder;
|
|
21
52
|
alwaysCrits(): AlwaysCritBuilder;
|
|
53
|
+
/**
|
|
54
|
+
* `(<to-hit total> AC <ac>)`: the natural roll as {@link toPMF} reads it (one root die, rolled with
|
|
55
|
+
* the resolved roll type, so three-dice advantage prints `d20 > d20 > d20`), its flats and every
|
|
56
|
+
* bonus die.
|
|
57
|
+
*/
|
|
22
58
|
toExpression(): string;
|
|
59
|
+
/**
|
|
60
|
+
* The raw to-hit PMF: the check's total where it reaches the AC and 0 where it does not, with no
|
|
61
|
+
* natural-1 miss or natural-20 hit (`d20.plus(20).ac(15).toPMF()` never shows 0, although the
|
|
62
|
+
* attack misses on a natural 1). The attack outcome — natural rules, crits — comes from
|
|
63
|
+
* `onHit(...).resolve()`.
|
|
64
|
+
*/
|
|
23
65
|
toPMF(eps?: number): PMF;
|
|
24
|
-
|
|
66
|
+
/** Accepts a replacement AC, so `vsAC` can rebind the check without rebuilding the roll. */
|
|
67
|
+
copy(ac?: number): ACBuilder;
|
|
25
68
|
}
|
|
26
69
|
//# sourceMappingURL=ac.d.ts.map
|
package/dist/builder/ac.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ac.d.ts","sourceRoot":"","sources":["../../src/builder/ac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"ac.d.ts","sourceRoot":"","sources":["../../src/builder/ac.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,OAAO,EAAE,iBAAiB,EAAoB,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IAItB,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CACrB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,CAAC,GAAG,CAAC,EACpB,KAAK,EAAE;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GACnD;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAUtC;AAED,qBAAa,SAAU,SAAQ,WAAW;IACxC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IAEpC,YAAY,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,YAAY,EAQzE;IAED,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACvC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAAC;IACtD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC;IACnD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAC;IAMrE,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;;;OAIG;IACH,kBAAkB,IAAI,SAAS,CAM9B;IAED;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAiBd,gBAAgB,IAAI,UAAU,GAAG,SAAS,CAIlD;IAEQ,QAAQ,IAAI,MAAM,GAAG,IAAI,CAKjC;IAED,wEAAwE;IACxE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAOnC;IAED,WAAW,IAAI,iBAAiB,CAU/B;IAED;;;;OAIG;IACM,YAAY,IAAI,MAAM,CAE9B;IAED;;;;;OAKG;IACM,KAAK,CAAC,GAAG,GAAE,MAAU,GAAG,GAAG,CAsBnC;IAED,4FAA4F;IACnF,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAQpC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throws for a non-finite builder argument, naming it. NaN reads "got NaN" unless the call site has
|
|
3
|
+
* its own, earlier NaN message. Internal: not re-exported from the package.
|
|
4
|
+
*/
|
|
5
|
+
export declare function requireFinite(value: number, what: string): void;
|
|
6
|
+
//# sourceMappingURL=arguments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arguments.d.ts","sourceRoot":"","sources":["../../src/builder/arguments.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/D"}
|
package/dist/builder/ast.d.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { PMF } from "../.js";
|
|
2
2
|
import type { DieNode, ExpressionNode } from "./nodes.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type RollBuilder } from "./roll.js";
|
|
4
4
|
import type { RollConfig, RollType } from "./types.js";
|
|
5
|
+
/** Clears the single-die and d20-lift PMF caches (reached through `clearRollCache`). */
|
|
6
|
+
export declare function clearDieCaches(): void;
|
|
5
7
|
export declare function dieNodeFromConfig(cfg: RollConfig): DieNode;
|
|
8
|
+
/**
|
|
9
|
+
* Thrown for a per-die `keepHighest(T, K)`/`keepLowest(T, K)` on a group of N > 1 dice whose
|
|
10
|
+
* reading is not unique: "keep K of T rolls of the whole N-dice sum" and "keep K of T single dice"
|
|
11
|
+
* give different numbers, and nothing in the call says which is meant.
|
|
12
|
+
*/
|
|
13
|
+
export declare class AmbiguousKeepError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
type KeepSpec = NonNullable<RollConfig["keep"]>;
|
|
17
|
+
/**
|
|
18
|
+
* How a per-die keep on a group of `count` d`sides` reads:
|
|
19
|
+
*
|
|
20
|
+
* - `"dice"`: keep K of T single dice. Used for one die (`d6.keepHighest(3, 2)`), for K >= 2 of
|
|
21
|
+
* the group's own N dice (`roll(4, d6).keepHighest(4, 3)`), and for `bestOf(k)`.
|
|
22
|
+
* - `"trials"`: roll the whole N-dice group T times and keep K of those sums. Used for K = 1:
|
|
23
|
+
* `roll(2, d6).keepHighest(2, 1)` is the better of two 2d6, and `keepLowest(T, 1)` with T != N
|
|
24
|
+
* is the worse of T rolls of the group.
|
|
25
|
+
*
|
|
26
|
+
* Every other shape on N > 1 dice (K >= 2 with T != N, and `keepLowest(N, 1)`, whose single-die
|
|
27
|
+
* and whole-group readings differ) throws an {@link AmbiguousKeepError} naming both spellings.
|
|
28
|
+
*/
|
|
29
|
+
export declare function perDieKeepReading(count: number, sides: number, keep: KeepSpec, synthesizedBestOf?: boolean): "dice" | "trials";
|
|
6
30
|
export declare function astFromRollConfigs(configs: readonly RollConfig[]): ExpressionNode | undefined;
|
|
7
31
|
export declare function resolve(node: ExpressionNode, eps?: number): PMF;
|
|
8
32
|
export declare function pmfFromRollBuilder(rb: RollBuilder, eps?: number): PMF;
|
|
@@ -26,14 +50,19 @@ export declare function pmfFromRollBuilder(rb: RollBuilder, eps?: number): PMF;
|
|
|
26
50
|
*/
|
|
27
51
|
export declare function resolveD20Roll(die: DieNode, rollType: RollType | undefined): PMF;
|
|
28
52
|
/**
|
|
29
|
-
* Resolve a check builder's
|
|
30
|
-
* that die's reroll/minimum/explode, then lift by its
|
|
31
|
-
* attack/save check builder ({@link ACBuilder},
|
|
32
|
-
* {@link AlwaysCritBuilder}, `DCBuilder`, save
|
|
33
|
-
*
|
|
34
|
-
*
|
|
53
|
+
* Resolve a check builder's natural roll (its d20, or with no d20 its largest die: see
|
|
54
|
+
* {@link naturalRollIndex}), honoring that die's reroll/minimum/explode, then lift by its
|
|
55
|
+
* `rollType`. The single entry point every attack/save check builder ({@link ACBuilder},
|
|
56
|
+
* {@link AttackBuilder}, {@link AlwaysHitBuilder}, {@link AlwaysCritBuilder}, `DCBuilder`, save
|
|
57
|
+
* `resolveProbabilities`) uses for the natural roll; the other dice are bonus dice.
|
|
58
|
+
*
|
|
59
|
+
* A check with no die has no natural roll: this returns a certain 0, so its total is its flat
|
|
60
|
+
* modifier plus any bonus dice and no natural-1/natural-20 rule can apply. A natural roll of more
|
|
61
|
+
* than one die (`roll(2, d20)`, or two equal top dice like `d20.plus(d20)`) has no single natural
|
|
62
|
+
* 1 or 20 and throws.
|
|
35
63
|
*/
|
|
36
64
|
export declare function resolveRootD20(check: RollBuilder): PMF;
|
|
37
65
|
export declare function resolveSingleDie(die: DieNode, eps?: number): PMF;
|
|
38
66
|
export declare function getASTSignature(node: ExpressionNode): string;
|
|
67
|
+
export {};
|
|
39
68
|
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/builder/ast.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/builder/ast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,KAAK,EAIV,OAAO,EACP,cAAc,EAIf,MAAM,SAAS,CAAC;AACjB,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOpD,wFAAwF;AACxF,wBAAgB,cAAc,IAAI,IAAI,CAGrC;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAa1D;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAI1B;CACF;AAED,KAAK,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAEhD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,QAAQ,EACd,iBAAiB,UAAQ,GACxB,MAAM,GAAG,QAAQ,CAgBnB;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,cAAc,GAAG,SAAS,CAsF5B;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,GAAE,MAAmB,GAAG,GAAG,CAmG3E;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,WAAW,EACf,GAAG,GAAE,MAAmB,GACvB,GAAG,CAGL;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,GAAG,CA8BhF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,CAgBtD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,GAAE,MAAmB,GAAG,GAAG,CA2E5E;AA6UD,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAwD5D"}
|