@yipe/dice 0.2.16 → 0.2.19
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 +256 -35
- package/dist/builder/index.cjs +947 -12
- package/dist/builder/index.cjs.map +1 -1
- package/dist/builder/index.d.cts +23 -2
- package/dist/builder/index.d.ts +23 -2
- package/dist/builder/index.js +947 -13
- package/dist/builder/index.js.map +1 -1
- package/dist/index.cjs +70 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +70 -2
- package/dist/index.js.map +1 -1
- package/dist/{pmf-CYaEbY7m.d.cts → pmf-BC1poIqF.d.cts} +31 -0
- package/dist/{pmf-CYaEbY7m.d.ts → pmf-BC1poIqF.d.ts} +31 -0
- package/package.json +1 -1
package/dist/builder/index.js
CHANGED
|
@@ -52,6 +52,7 @@ var _DiceQuery = class _DiceQuery {
|
|
|
52
52
|
constructor(singles, combined, eps = EPS) {
|
|
53
53
|
__publicField(this, "singles");
|
|
54
54
|
__publicField(this, "combined");
|
|
55
|
+
__publicField(this, "_combinedWithAttr");
|
|
55
56
|
this.singles = Array.isArray(singles) ? singles : [singles];
|
|
56
57
|
if (this.singles.some((s) => s === void 0)) {
|
|
57
58
|
throw new Error("DiceQuery contains undefined singles");
|
|
@@ -59,6 +60,35 @@ var _DiceQuery = class _DiceQuery {
|
|
|
59
60
|
const c = combined ?? PMF.convolveMany(this.singles);
|
|
60
61
|
this.combined = Math.abs(c.mass() - 1) <= eps ? c : c.normalize();
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns a new PMF with damage attribution metadata populated.
|
|
65
|
+
*
|
|
66
|
+
* This method computes attribution on-demand for builder-generated PMFs,
|
|
67
|
+
* enabling them to work with damage attribution charts. The `attr` field
|
|
68
|
+
* tracks how much damage each outcome type contributes at each damage value.
|
|
69
|
+
*
|
|
70
|
+
* For each bin at damage D: sum(attr.values()) ≈ D × P(damage = D)
|
|
71
|
+
*
|
|
72
|
+
* Performance: Cached after first call. Adds minimal overhead vs `combined`.
|
|
73
|
+
*
|
|
74
|
+
* @returns PMF with attr field populated for damage attribution charts
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const attack = d20.plus(5).ac(15).onHit(d(2,6).plus(3)).onCrit(d(2,6))
|
|
78
|
+
* const query = attack.toQuery()
|
|
79
|
+
* const pmf = query.combinedWithAttribution()
|
|
80
|
+
* // Now pmf can be used with toDamageAttributionChartSeries()
|
|
81
|
+
*/
|
|
82
|
+
combinedWithAttribution() {
|
|
83
|
+
if (this._combinedWithAttr) {
|
|
84
|
+
return this._combinedWithAttr;
|
|
85
|
+
}
|
|
86
|
+
const singlesWithAttr = this.singles.map((pmf) => pmf.withAttribution());
|
|
87
|
+
const combined = PMF.convolveMany(singlesWithAttr, this.combined.epsilon);
|
|
88
|
+
const normalized = Math.abs(combined.mass() - 1) <= this.combined.epsilon ? combined : combined.normalize();
|
|
89
|
+
this._combinedWithAttr = normalized;
|
|
90
|
+
return normalized;
|
|
91
|
+
}
|
|
62
92
|
/**
|
|
63
93
|
* Returns the expected damage across all possible outcomes.
|
|
64
94
|
*
|
|
@@ -1403,9 +1433,9 @@ var _PMF = class _PMF {
|
|
|
1403
1433
|
let totalWeight = items.reduce((s, { weight }) => s + weight, 0);
|
|
1404
1434
|
if (Math.abs(totalWeight) <= eps) totalWeight = 0;
|
|
1405
1435
|
if (Math.abs(1 - totalWeight) <= eps) totalWeight = 1;
|
|
1406
|
-
if (totalWeight > 1 +
|
|
1436
|
+
if (totalWeight > 1 + EPS) {
|
|
1407
1437
|
throw new Error(
|
|
1408
|
-
`PMF.exclusive: total weight ${totalWeight
|
|
1438
|
+
`PMF.exclusive: total weight ${totalWeight} exceeds 1. (epsilon: ${eps})`
|
|
1409
1439
|
);
|
|
1410
1440
|
}
|
|
1411
1441
|
let out = _PMF.empty(eps);
|
|
@@ -1447,6 +1477,44 @@ var _PMF = class _PMF {
|
|
|
1447
1477
|
}
|
|
1448
1478
|
return out;
|
|
1449
1479
|
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Adds damage attribution metadata to this PMF based on existing count metadata.
|
|
1482
|
+
* For each bin, sets attr[outcome] = damage × count[outcome].
|
|
1483
|
+
*
|
|
1484
|
+
* This enables damage attribution charts to work with builder-generated PMFs.
|
|
1485
|
+
* The parser generates attr automatically, but builder PMFs only have count.
|
|
1486
|
+
*
|
|
1487
|
+
* @returns New PMF with attr field populated in each bin
|
|
1488
|
+
*/
|
|
1489
|
+
withAttribution() {
|
|
1490
|
+
for (const [damage, bin] of this.map) {
|
|
1491
|
+
if (damage !== 0 && bin.attr && Object.keys(bin.attr).length > 0) {
|
|
1492
|
+
return this;
|
|
1493
|
+
}
|
|
1494
|
+
if (damage > 0) break;
|
|
1495
|
+
}
|
|
1496
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
1497
|
+
for (const [damage, bin] of this.map) {
|
|
1498
|
+
const attr = {};
|
|
1499
|
+
for (const outcome in bin.count) {
|
|
1500
|
+
const probability = bin.count[outcome];
|
|
1501
|
+
if (probability > 0) {
|
|
1502
|
+
attr[outcome] = damage * probability;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
newMap.set(damage, {
|
|
1506
|
+
p: bin.p,
|
|
1507
|
+
count: { ...bin.count },
|
|
1508
|
+
attr: Object.keys(attr).length > 0 ? attr : void 0
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
return new _PMF(
|
|
1512
|
+
newMap,
|
|
1513
|
+
this.epsilon,
|
|
1514
|
+
this.normalized,
|
|
1515
|
+
`${this.identifier}~attr`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1450
1518
|
/**
|
|
1451
1519
|
* General-purpose N-way mixture.
|
|
1452
1520
|
* weights: Array of [weight, PMF].
|
|
@@ -2335,6 +2403,822 @@ var Mixture = class _Mixture {
|
|
|
2335
2403
|
}
|
|
2336
2404
|
};
|
|
2337
2405
|
|
|
2406
|
+
// src/parser/dice.ts
|
|
2407
|
+
var Dice = class _Dice {
|
|
2408
|
+
constructor(x = 0) {
|
|
2409
|
+
__publicField(this, "faces", {});
|
|
2410
|
+
__publicField(this, "privateData", {});
|
|
2411
|
+
__publicField(this, "outcomeData", {});
|
|
2412
|
+
__publicField(this, "hasHitDistributionCalculated", false);
|
|
2413
|
+
__publicField(this, "identifier");
|
|
2414
|
+
if (x <= 0) return;
|
|
2415
|
+
for (let i = 1; i <= x; i++) {
|
|
2416
|
+
this.faces[i] = 1;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
getOutcomeDistribution(key) {
|
|
2420
|
+
if (key === "hit") {
|
|
2421
|
+
this.ensureHitDistribution();
|
|
2422
|
+
}
|
|
2423
|
+
return this.outcomeData[key];
|
|
2424
|
+
}
|
|
2425
|
+
getFullOutcomeDistribution() {
|
|
2426
|
+
return { ...this.outcomeData };
|
|
2427
|
+
}
|
|
2428
|
+
setOutcomeDistribution(key, data) {
|
|
2429
|
+
if (data) {
|
|
2430
|
+
this.outcomeData[key] = data;
|
|
2431
|
+
} else {
|
|
2432
|
+
delete this.outcomeData[key];
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
hasOutcomeData(key) {
|
|
2436
|
+
if (key === "hit") {
|
|
2437
|
+
this.ensureHitDistribution();
|
|
2438
|
+
}
|
|
2439
|
+
const data = this.outcomeData[key];
|
|
2440
|
+
return data !== void 0 && Object.keys(data).length > 0;
|
|
2441
|
+
}
|
|
2442
|
+
getOutcomeCount(key, face) {
|
|
2443
|
+
return this.outcomeData[key]?.[face] ?? 0;
|
|
2444
|
+
}
|
|
2445
|
+
getAverage(key) {
|
|
2446
|
+
const distribution = this.getOutcomeDistribution(key);
|
|
2447
|
+
if (!distribution) return 0;
|
|
2448
|
+
const totalCount = Object.values(distribution).reduce(
|
|
2449
|
+
(sum, count) => sum + count,
|
|
2450
|
+
0
|
|
2451
|
+
);
|
|
2452
|
+
const expectedDamage = Object.entries(distribution).reduce(
|
|
2453
|
+
(sum, [damage, count]) => sum + Number(damage) * count,
|
|
2454
|
+
0
|
|
2455
|
+
);
|
|
2456
|
+
if (totalCount === 0) return 0;
|
|
2457
|
+
return expectedDamage / totalCount;
|
|
2458
|
+
}
|
|
2459
|
+
// TODO this can be private later if we change how testing works
|
|
2460
|
+
calculateHitDistribution() {
|
|
2461
|
+
const hitValues = {};
|
|
2462
|
+
for (const [face, totalCount] of Object.entries(this.faces)) {
|
|
2463
|
+
const numFace = Number(face);
|
|
2464
|
+
let hitCount = totalCount;
|
|
2465
|
+
for (const outcomeType of [
|
|
2466
|
+
"crit",
|
|
2467
|
+
"missNone",
|
|
2468
|
+
"missDamage",
|
|
2469
|
+
"saveHalf",
|
|
2470
|
+
"saveFail",
|
|
2471
|
+
"pc"
|
|
2472
|
+
]) {
|
|
2473
|
+
const distribution = this.getOutcomeDistribution(outcomeType);
|
|
2474
|
+
if (distribution && distribution[numFace]) {
|
|
2475
|
+
hitCount -= distribution[numFace];
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (numFace === 0) {
|
|
2479
|
+
hitCount = 0;
|
|
2480
|
+
}
|
|
2481
|
+
if (hitCount < 0) {
|
|
2482
|
+
console.error("hitCount is <=0?", face, totalCount, hitCount);
|
|
2483
|
+
hitCount = 0;
|
|
2484
|
+
}
|
|
2485
|
+
hitValues[numFace] = hitCount;
|
|
2486
|
+
}
|
|
2487
|
+
return hitValues;
|
|
2488
|
+
}
|
|
2489
|
+
ensureHitDistribution() {
|
|
2490
|
+
if (!this.hasHitDistributionCalculated) {
|
|
2491
|
+
const hitValues = this.calculateHitDistribution();
|
|
2492
|
+
this.setOutcomeDistribution("hit", hitValues);
|
|
2493
|
+
this.hasHitDistributionCalculated = true;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
// PRIVATE FUNCTIONS
|
|
2497
|
+
binaryOp(other, op, diceConstructor) {
|
|
2498
|
+
const result = diceConstructor ? diceConstructor() : new _Dice();
|
|
2499
|
+
const isScalar = typeof other === "number";
|
|
2500
|
+
const keys1 = this.keys();
|
|
2501
|
+
for (const key1 of keys1) {
|
|
2502
|
+
const value1 = this.faces[key1];
|
|
2503
|
+
if (isScalar) {
|
|
2504
|
+
const resultKey = op(key1, other);
|
|
2505
|
+
result.increment(resultKey, value1);
|
|
2506
|
+
} else {
|
|
2507
|
+
const keys2 = other.keys();
|
|
2508
|
+
for (const key2 of keys2) {
|
|
2509
|
+
const value2 = other.faces[key2];
|
|
2510
|
+
const resultKey = op(key1, key2);
|
|
2511
|
+
result.increment(resultKey, value1 * value2);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return result;
|
|
2516
|
+
}
|
|
2517
|
+
removeFaces(facesToRemove) {
|
|
2518
|
+
const result = new _Dice();
|
|
2519
|
+
for (const [key, value] of Object.entries(this.faces)) {
|
|
2520
|
+
const numKey = Number(key);
|
|
2521
|
+
if (!facesToRemove.includes(numKey)) {
|
|
2522
|
+
result.faces[numKey] = value;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
result.privateData = { ...this.privateData };
|
|
2526
|
+
result.outcomeData = { ...this.outcomeData };
|
|
2527
|
+
return result;
|
|
2528
|
+
}
|
|
2529
|
+
// PUBLIC FUNTIONS
|
|
2530
|
+
getFaceEntries() {
|
|
2531
|
+
return Object.entries(this.faces).map(([k, v]) => [Number(k), v]);
|
|
2532
|
+
}
|
|
2533
|
+
getFaceMap() {
|
|
2534
|
+
return { ...this.faces };
|
|
2535
|
+
}
|
|
2536
|
+
get(face) {
|
|
2537
|
+
return this.faces[face] ?? 0;
|
|
2538
|
+
}
|
|
2539
|
+
keys() {
|
|
2540
|
+
return Object.keys(this.faces).map(Number);
|
|
2541
|
+
}
|
|
2542
|
+
values() {
|
|
2543
|
+
return Object.values(this.faces);
|
|
2544
|
+
}
|
|
2545
|
+
total() {
|
|
2546
|
+
return Object.values(this.faces).reduce((sum, value) => sum + value, 0);
|
|
2547
|
+
}
|
|
2548
|
+
setFace(key, value) {
|
|
2549
|
+
this.faces[key] = value;
|
|
2550
|
+
}
|
|
2551
|
+
static scalar(value) {
|
|
2552
|
+
const result = new _Dice();
|
|
2553
|
+
result.increment(value, 1);
|
|
2554
|
+
return result;
|
|
2555
|
+
}
|
|
2556
|
+
maxFace() {
|
|
2557
|
+
const numericKeys = this.keys();
|
|
2558
|
+
if (numericKeys.length === 0) {
|
|
2559
|
+
throw new Error("No numeric faces found");
|
|
2560
|
+
}
|
|
2561
|
+
return Math.max(...numericKeys);
|
|
2562
|
+
}
|
|
2563
|
+
minFace() {
|
|
2564
|
+
const numericKeys = this.keys();
|
|
2565
|
+
if (numericKeys.length === 0) {
|
|
2566
|
+
throw new Error("No numeric faces found");
|
|
2567
|
+
}
|
|
2568
|
+
return Math.min(...numericKeys);
|
|
2569
|
+
}
|
|
2570
|
+
increment(face, count) {
|
|
2571
|
+
const current = this.faces[face] || 0;
|
|
2572
|
+
this.faces[face] = current + count;
|
|
2573
|
+
}
|
|
2574
|
+
normalize(scalar) {
|
|
2575
|
+
const result = new _Dice();
|
|
2576
|
+
for (const [face, count] of Object.entries(this.faces)) {
|
|
2577
|
+
result.faces[Number(face)] = count * scalar;
|
|
2578
|
+
}
|
|
2579
|
+
result.privateData = { ...this.privateData };
|
|
2580
|
+
result.outcomeData = { ...this.outcomeData };
|
|
2581
|
+
return result;
|
|
2582
|
+
}
|
|
2583
|
+
// OPERATIONS
|
|
2584
|
+
add(other) {
|
|
2585
|
+
return this.binaryOp(other, (a, b) => a + b);
|
|
2586
|
+
}
|
|
2587
|
+
subtract(other) {
|
|
2588
|
+
return this.binaryOp(other, (a, b) => a - b);
|
|
2589
|
+
}
|
|
2590
|
+
conditionalApply(other) {
|
|
2591
|
+
return this.binaryOp(other, (a, b) => (a === 0 ? 0 : 1) * b);
|
|
2592
|
+
}
|
|
2593
|
+
multiply(other) {
|
|
2594
|
+
return this.binaryOp(other, (a, b) => a * b);
|
|
2595
|
+
}
|
|
2596
|
+
addNonZero(other) {
|
|
2597
|
+
return this.binaryOp(other, (a, b) => a !== 0 ? a + b : a);
|
|
2598
|
+
}
|
|
2599
|
+
eq(other) {
|
|
2600
|
+
return this.binaryOp(other, (a, b) => a === b ? 1 : 0);
|
|
2601
|
+
}
|
|
2602
|
+
max(other) {
|
|
2603
|
+
return this.binaryOp(other, (a, b) => Math.max(a, b));
|
|
2604
|
+
}
|
|
2605
|
+
min(other) {
|
|
2606
|
+
return this.binaryOp(other, (a, b) => Math.min(a, b));
|
|
2607
|
+
}
|
|
2608
|
+
advantage() {
|
|
2609
|
+
return this.max(this);
|
|
2610
|
+
}
|
|
2611
|
+
ge(other) {
|
|
2612
|
+
return this.binaryOp(other, (a, b) => a >= b ? 0 : 1);
|
|
2613
|
+
}
|
|
2614
|
+
divide(other) {
|
|
2615
|
+
return this.binaryOp(other, (a, b) => a / b);
|
|
2616
|
+
}
|
|
2617
|
+
divideRoundUp(other) {
|
|
2618
|
+
return this.binaryOp(other, (a, b) => Math.ceil(a / b));
|
|
2619
|
+
}
|
|
2620
|
+
divideRoundDown(other) {
|
|
2621
|
+
return this.binaryOp(other, (a, b) => Math.floor(a / b));
|
|
2622
|
+
}
|
|
2623
|
+
and(other) {
|
|
2624
|
+
return this.binaryOp(other, (a, b) => a && b ? 1 : 0);
|
|
2625
|
+
}
|
|
2626
|
+
checkTarget(other, comparisonLogic) {
|
|
2627
|
+
const createResult = () => {
|
|
2628
|
+
const result = new _Dice();
|
|
2629
|
+
result.increment(0, 0);
|
|
2630
|
+
result.increment(1, 0);
|
|
2631
|
+
return result;
|
|
2632
|
+
};
|
|
2633
|
+
return this.binaryOp(other, comparisonLogic, createResult);
|
|
2634
|
+
}
|
|
2635
|
+
dc(other) {
|
|
2636
|
+
const dcCheck = (a, b) => a >= b ? 0 : 1;
|
|
2637
|
+
const result = this.checkTarget(other, dcCheck);
|
|
2638
|
+
result.privateData.isDCCheck = true;
|
|
2639
|
+
return result;
|
|
2640
|
+
}
|
|
2641
|
+
ac(other) {
|
|
2642
|
+
const acCheck = (a, b) => a >= b ? a : 0;
|
|
2643
|
+
return this.checkTarget(other, acCheck);
|
|
2644
|
+
}
|
|
2645
|
+
deleteFace(face) {
|
|
2646
|
+
const result = new _Dice();
|
|
2647
|
+
for (const [key, value] of Object.entries(this.faces)) {
|
|
2648
|
+
const numKey = Number(key);
|
|
2649
|
+
if (numKey !== face) {
|
|
2650
|
+
result.increment(numKey, value);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
result.privateData = { ...this.privateData };
|
|
2654
|
+
result.outcomeData = { ...this.outcomeData };
|
|
2655
|
+
return result;
|
|
2656
|
+
}
|
|
2657
|
+
reroll(toReroll) {
|
|
2658
|
+
const rerollDice = typeof toReroll === "number" ? _Dice.scalar(toReroll) : toReroll;
|
|
2659
|
+
const removed = this.removeFaces(rerollDice.keys());
|
|
2660
|
+
let result = new _Dice();
|
|
2661
|
+
for (const face of this.keys()) {
|
|
2662
|
+
const wasRerolled = rerollDice.keys().includes(face);
|
|
2663
|
+
result = result.combine(removed);
|
|
2664
|
+
if (wasRerolled) {
|
|
2665
|
+
result = result.combine(this);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
return result;
|
|
2669
|
+
}
|
|
2670
|
+
// This is not addition and not rolling two dice at once.
|
|
2671
|
+
// Instead, it’s mixing two distributions into a single weighted die.
|
|
2672
|
+
combine(other) {
|
|
2673
|
+
if (typeof other === "number") {
|
|
2674
|
+
other = _Dice.scalar(other);
|
|
2675
|
+
}
|
|
2676
|
+
const result = new _Dice();
|
|
2677
|
+
for (const [key, value] of Object.entries(other.faces)) {
|
|
2678
|
+
result.faces[Number(key)] = value;
|
|
2679
|
+
}
|
|
2680
|
+
const except = new _Dice();
|
|
2681
|
+
for (const [key, value] of Object.entries(this.faces)) {
|
|
2682
|
+
const numKey = Number(key);
|
|
2683
|
+
result.increment(numKey, value);
|
|
2684
|
+
if (!(numKey in other.faces)) {
|
|
2685
|
+
except.increment(numKey, value);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
result.privateData = { ...this.privateData, except: other };
|
|
2689
|
+
result.outcomeData = { ...this.outcomeData };
|
|
2690
|
+
return result;
|
|
2691
|
+
}
|
|
2692
|
+
combineInPlace(other) {
|
|
2693
|
+
for (const [key, value] of Object.entries(other.faces)) {
|
|
2694
|
+
const numKey = Number(key);
|
|
2695
|
+
const current = this.faces[numKey] || 0;
|
|
2696
|
+
this.faces[numKey] = current + value;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
percent() {
|
|
2700
|
+
const total = this.total();
|
|
2701
|
+
const result = {};
|
|
2702
|
+
for (const [face, count] of Object.entries(this.faces)) {
|
|
2703
|
+
result[Number(face)] = count / total;
|
|
2704
|
+
}
|
|
2705
|
+
return result;
|
|
2706
|
+
}
|
|
2707
|
+
average() {
|
|
2708
|
+
const total = this.total();
|
|
2709
|
+
if (total === 0) return 0;
|
|
2710
|
+
let sum = 0;
|
|
2711
|
+
for (const [key, value] of Object.entries(this.faces)) {
|
|
2712
|
+
sum += Number(key) * value;
|
|
2713
|
+
}
|
|
2714
|
+
return sum / total;
|
|
2715
|
+
}
|
|
2716
|
+
/*
|
|
2717
|
+
* Convert dice to PMF using OutcomeType labels directly from damage distribution.
|
|
2718
|
+
* This is much cleaner than the original complex distribution conversion.
|
|
2719
|
+
*/
|
|
2720
|
+
toPMF(numEpsilon = EPS) {
|
|
2721
|
+
const total = this.total();
|
|
2722
|
+
if (total === 0) return PMF.empty(numEpsilon);
|
|
2723
|
+
this.ensureHitDistribution();
|
|
2724
|
+
const map = /* @__PURE__ */ new Map();
|
|
2725
|
+
const hitDistro = this.getOutcomeDistribution("hit") || {};
|
|
2726
|
+
const critDistro = this.getOutcomeDistribution("crit") || {};
|
|
2727
|
+
const missDistro = this.getOutcomeDistribution("missDamage") || {};
|
|
2728
|
+
const saveDistro = this.getOutcomeDistribution("saveHalf") || {};
|
|
2729
|
+
const pcDistro = this.getOutcomeDistribution("pc") || {};
|
|
2730
|
+
let isSaveHalf = false;
|
|
2731
|
+
for (const halfDamage of Object.keys(saveDistro).map(Number)) {
|
|
2732
|
+
const fullDamage = halfDamage * 2;
|
|
2733
|
+
if (fullDamage > 0 && hitDistro[fullDamage]) {
|
|
2734
|
+
isSaveHalf = true;
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
const isDCCheck = this.privateData.isDCCheck === true;
|
|
2739
|
+
const clampNonNeg = (x) => x < 0 && x > -1e-15 ? 0 : x;
|
|
2740
|
+
for (const [faceStr, faceCountRaw] of Object.entries(this.getFaceMap())) {
|
|
2741
|
+
const face = Number(faceStr);
|
|
2742
|
+
const faceCount = Number(faceCountRaw);
|
|
2743
|
+
if (faceCount <= 0) continue;
|
|
2744
|
+
let p = faceCount / total;
|
|
2745
|
+
p = clampNonNeg(p);
|
|
2746
|
+
if (!(p > 0)) continue;
|
|
2747
|
+
if (numEpsilon >= 0 && p < numEpsilon) continue;
|
|
2748
|
+
const count = {};
|
|
2749
|
+
const attr = {};
|
|
2750
|
+
if (hitDistro[face]) {
|
|
2751
|
+
const c = clampNonNeg(hitDistro[face] / total);
|
|
2752
|
+
if (c > 0) {
|
|
2753
|
+
if (isSaveHalf || isDCCheck) {
|
|
2754
|
+
count.saveFail = c;
|
|
2755
|
+
attr.saveFail = clampNonNeg(face * hitDistro[face] / total);
|
|
2756
|
+
} else {
|
|
2757
|
+
count.hit = c;
|
|
2758
|
+
attr.hit = clampNonNeg(face * hitDistro[face] / total);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
if (critDistro[face]) {
|
|
2763
|
+
const c = clampNonNeg(critDistro[face] / total);
|
|
2764
|
+
if (c > 0) {
|
|
2765
|
+
count.crit = c;
|
|
2766
|
+
attr.crit = clampNonNeg(face * critDistro[face] / total);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
if (missDistro[face]) {
|
|
2770
|
+
const c = clampNonNeg(missDistro[face] / total);
|
|
2771
|
+
if (c > 0) {
|
|
2772
|
+
count.missDamage = c;
|
|
2773
|
+
attr.missDamage = clampNonNeg(face * missDistro[face] / total);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (saveDistro[face]) {
|
|
2777
|
+
const c = clampNonNeg(saveDistro[face] / total);
|
|
2778
|
+
if (c > 0) {
|
|
2779
|
+
if (isSaveHalf) {
|
|
2780
|
+
count.saveHalf = c;
|
|
2781
|
+
attr.saveHalf = clampNonNeg(face * saveDistro[face] / total);
|
|
2782
|
+
} else {
|
|
2783
|
+
count.saveFail = (count.saveFail ?? 0) + c;
|
|
2784
|
+
attr.saveFail = clampNonNeg(
|
|
2785
|
+
(attr.saveFail ?? 0) + face * saveDistro[face] / total
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
if (pcDistro[face]) {
|
|
2791
|
+
const c = clampNonNeg(pcDistro[face] / total);
|
|
2792
|
+
if (c > 0) {
|
|
2793
|
+
count.pc = c;
|
|
2794
|
+
attr.pc = clampNonNeg(face * pcDistro[face] / total);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
if (!isSaveHalf && !isDCCheck) {
|
|
2798
|
+
const distroCountRaw = (hitDistro[face] || 0) + (critDistro[face] || 0) + (missDistro[face] || 0) + (saveDistro[face] || 0) + (pcDistro[face] || 0);
|
|
2799
|
+
const unaccountedCount = clampNonNeg(faceCount - distroCountRaw);
|
|
2800
|
+
if (unaccountedCount > 0) {
|
|
2801
|
+
const frac = clampNonNeg(unaccountedCount / total);
|
|
2802
|
+
if (frac > 0) {
|
|
2803
|
+
count.missNone = (count.missNone ?? 0) + frac;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
const bin = { p, count };
|
|
2808
|
+
if (Object.keys(attr).length > 0) {
|
|
2809
|
+
bin.attr = attr;
|
|
2810
|
+
}
|
|
2811
|
+
map.set(face, bin);
|
|
2812
|
+
}
|
|
2813
|
+
const identifier = this.identifier || "ERROR";
|
|
2814
|
+
if (identifier === "ERROR") {
|
|
2815
|
+
console.error("Dice identifier is undefined", this);
|
|
2816
|
+
}
|
|
2817
|
+
return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true);
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
|
|
2821
|
+
// src/parser/parser.ts
|
|
2822
|
+
var parseCache = new LRUCache(1e3);
|
|
2823
|
+
function parse(expression, n = 0) {
|
|
2824
|
+
const cleaned = expression.replace(/ /g, "").toLowerCase();
|
|
2825
|
+
{
|
|
2826
|
+
const cacheKey = `${cleaned}:${n}`;
|
|
2827
|
+
const cached = parseCache.get(cacheKey);
|
|
2828
|
+
if (cached) return cached;
|
|
2829
|
+
}
|
|
2830
|
+
const chars = [...cleaned];
|
|
2831
|
+
let result = void 0;
|
|
2832
|
+
try {
|
|
2833
|
+
result = parseExpression(chars, n);
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
throw new Error(`Cannot parse dice expression [${expression}]: ${error}`);
|
|
2836
|
+
}
|
|
2837
|
+
try {
|
|
2838
|
+
result.privateData = result.privateData || {};
|
|
2839
|
+
result.identifier = cleaned;
|
|
2840
|
+
} catch {
|
|
2841
|
+
}
|
|
2842
|
+
if (chars.length > 0) {
|
|
2843
|
+
throw new Error(
|
|
2844
|
+
`Unexpected token: '${chars[0]}' from expression: '${expression}'`
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
const resultPMF = result.toPMF(-1);
|
|
2848
|
+
{
|
|
2849
|
+
const cacheKey = `${cleaned}:${n}`;
|
|
2850
|
+
parseCache.set(cacheKey, resultPMF);
|
|
2851
|
+
}
|
|
2852
|
+
return resultPMF;
|
|
2853
|
+
}
|
|
2854
|
+
function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm, finalResult) {
|
|
2855
|
+
dice = dice.normalize(currentNorm);
|
|
2856
|
+
finalResult = finalResult.normalize(normValue);
|
|
2857
|
+
finalResult.setOutcomeDistribution(outcomeType, dice.getFaceMap());
|
|
2858
|
+
finalResult = finalResult.combine(dice);
|
|
2859
|
+
return { newNorm: currentNorm * normValue, updatedResult: finalResult };
|
|
2860
|
+
}
|
|
2861
|
+
function parseExpression(arr, n) {
|
|
2862
|
+
const result = (() => {
|
|
2863
|
+
const res = parseArgument(arr, n);
|
|
2864
|
+
return typeof res === "number" ? Dice.scalar(res) : res;
|
|
2865
|
+
})();
|
|
2866
|
+
let op = parseOperation(arr);
|
|
2867
|
+
let finalResult = result;
|
|
2868
|
+
while (op != null) {
|
|
2869
|
+
const arg = !op.unary ? parseArgument(arr, n) : finalResult;
|
|
2870
|
+
let crit;
|
|
2871
|
+
let critNorm = 1;
|
|
2872
|
+
if (arr[0] === "x" || arr[0] === "c") {
|
|
2873
|
+
const isXcrit = arr[0] === "x";
|
|
2874
|
+
if (isXcrit) assertToken(arr, "x");
|
|
2875
|
+
assertToken(arr, "c");
|
|
2876
|
+
assertToken(arr, "r");
|
|
2877
|
+
assertToken(arr, "i");
|
|
2878
|
+
assertToken(arr, "t");
|
|
2879
|
+
const count = isXcrit ? parseNumber(arr, n) : 1;
|
|
2880
|
+
crit = new Dice();
|
|
2881
|
+
for (let i = 0; i < count; i++) {
|
|
2882
|
+
const max = finalResult.maxFace();
|
|
2883
|
+
crit.setFace(max, finalResult.get(max));
|
|
2884
|
+
finalResult = finalResult.deleteFace(max);
|
|
2885
|
+
}
|
|
2886
|
+
critNorm = crit.total();
|
|
2887
|
+
crit = op.call(crit, parseBinaryArgument(arg, arr, n));
|
|
2888
|
+
critNorm = crit && critNorm ? crit.total() / critNorm : 1;
|
|
2889
|
+
}
|
|
2890
|
+
let save;
|
|
2891
|
+
let saveNorm = 1;
|
|
2892
|
+
if (arr[0] === "s") {
|
|
2893
|
+
assertToken(arr, "s");
|
|
2894
|
+
assertToken(arr, "a");
|
|
2895
|
+
assertToken(arr, "v");
|
|
2896
|
+
assertToken(arr, "e");
|
|
2897
|
+
save = new Dice();
|
|
2898
|
+
const min = finalResult.minFace();
|
|
2899
|
+
save.increment(min > 0 ? min : 1, finalResult.get(min));
|
|
2900
|
+
saveNorm = save.total();
|
|
2901
|
+
finalResult = finalResult.deleteFace(min);
|
|
2902
|
+
save = op.call(save, parseBinaryArgument(arg, arr, n));
|
|
2903
|
+
saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
|
|
2904
|
+
}
|
|
2905
|
+
let pc;
|
|
2906
|
+
let pcNorm = 1;
|
|
2907
|
+
if (arr.length >= 2 && arr[0] === "p" && arr[1] === "c") {
|
|
2908
|
+
assertToken(arr, "p");
|
|
2909
|
+
assertToken(arr, "c");
|
|
2910
|
+
pc = new Dice();
|
|
2911
|
+
const min = finalResult.minFace();
|
|
2912
|
+
pc.increment(min > 0 ? min : 1, finalResult.get(min));
|
|
2913
|
+
const missBefore = pc.total();
|
|
2914
|
+
finalResult = finalResult.deleteFace(min);
|
|
2915
|
+
pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
|
|
2916
|
+
const missAfter = pc ? pc.total() : 0;
|
|
2917
|
+
pcNorm = missBefore ? missAfter / missBefore : 1;
|
|
2918
|
+
}
|
|
2919
|
+
let miss;
|
|
2920
|
+
let missNorm = 1;
|
|
2921
|
+
if (arr[0] === "m") {
|
|
2922
|
+
assertToken(arr, "m");
|
|
2923
|
+
assertToken(arr, "i");
|
|
2924
|
+
assertToken(arr, "s");
|
|
2925
|
+
assertToken(arr, "s");
|
|
2926
|
+
miss = new Dice();
|
|
2927
|
+
const min = finalResult.minFace();
|
|
2928
|
+
miss.increment(min > 0 ? min : 1, finalResult.get(min));
|
|
2929
|
+
missNorm = miss.total();
|
|
2930
|
+
finalResult = finalResult.deleteFace(min);
|
|
2931
|
+
miss = op.call(miss, parseBinaryArgument(arg, arr, n));
|
|
2932
|
+
missNorm = miss && missNorm ? miss.total() / missNorm : 1;
|
|
2933
|
+
}
|
|
2934
|
+
let norm = finalResult.total();
|
|
2935
|
+
finalResult = op.call(finalResult, arg);
|
|
2936
|
+
norm = norm ? finalResult.total() / norm : 1;
|
|
2937
|
+
if (crit) {
|
|
2938
|
+
const result2 = combineDiceWithNormalization(
|
|
2939
|
+
crit,
|
|
2940
|
+
critNorm,
|
|
2941
|
+
"crit",
|
|
2942
|
+
norm,
|
|
2943
|
+
finalResult
|
|
2944
|
+
);
|
|
2945
|
+
norm = result2.newNorm;
|
|
2946
|
+
finalResult = result2.updatedResult;
|
|
2947
|
+
}
|
|
2948
|
+
if (save) {
|
|
2949
|
+
const result2 = combineDiceWithNormalization(
|
|
2950
|
+
save,
|
|
2951
|
+
saveNorm,
|
|
2952
|
+
"saveHalf",
|
|
2953
|
+
norm,
|
|
2954
|
+
finalResult
|
|
2955
|
+
);
|
|
2956
|
+
norm = result2.newNorm;
|
|
2957
|
+
finalResult = result2.updatedResult;
|
|
2958
|
+
}
|
|
2959
|
+
if (miss) {
|
|
2960
|
+
const result2 = combineDiceWithNormalization(
|
|
2961
|
+
miss,
|
|
2962
|
+
missNorm,
|
|
2963
|
+
"missDamage",
|
|
2964
|
+
norm,
|
|
2965
|
+
finalResult
|
|
2966
|
+
);
|
|
2967
|
+
norm = result2.newNorm;
|
|
2968
|
+
finalResult = result2.updatedResult;
|
|
2969
|
+
}
|
|
2970
|
+
if (pc) {
|
|
2971
|
+
const result2 = combineDiceWithNormalization(
|
|
2972
|
+
pc,
|
|
2973
|
+
pcNorm,
|
|
2974
|
+
"pc",
|
|
2975
|
+
norm,
|
|
2976
|
+
finalResult
|
|
2977
|
+
);
|
|
2978
|
+
norm = result2.newNorm;
|
|
2979
|
+
finalResult = result2.updatedResult;
|
|
2980
|
+
}
|
|
2981
|
+
op = parseOperation(arr);
|
|
2982
|
+
}
|
|
2983
|
+
return finalResult;
|
|
2984
|
+
}
|
|
2985
|
+
function parseArgument(s, n) {
|
|
2986
|
+
let result = parseArgumentInternal(s, n);
|
|
2987
|
+
while (true) {
|
|
2988
|
+
const next = parseArgumentInternal(s, n);
|
|
2989
|
+
if (next === void 0) break;
|
|
2990
|
+
result = multiplyDiceByDice(result, next);
|
|
2991
|
+
}
|
|
2992
|
+
return result;
|
|
2993
|
+
}
|
|
2994
|
+
function multiplyDiceByDice(d1, d2) {
|
|
2995
|
+
if (typeof d1 === "number") d1 = Dice.scalar(d1);
|
|
2996
|
+
if (typeof d2 === "number") d2 = Dice.scalar(d2);
|
|
2997
|
+
const result = new Dice();
|
|
2998
|
+
const faces = {};
|
|
2999
|
+
let normalizationFactor = 1;
|
|
3000
|
+
for (const key of d1.keys()) {
|
|
3001
|
+
let face;
|
|
3002
|
+
if (typeof key !== "number") {
|
|
3003
|
+
continue;
|
|
3004
|
+
}
|
|
3005
|
+
if (d2.privateData.keep) {
|
|
3006
|
+
const repeat = Array(key).fill(d2);
|
|
3007
|
+
face = opDice(repeat, d2.privateData.keep);
|
|
3008
|
+
} else {
|
|
3009
|
+
face = multiplyDice(key, d2);
|
|
3010
|
+
}
|
|
3011
|
+
normalizationFactor *= face.total();
|
|
3012
|
+
faces[key] = face;
|
|
3013
|
+
}
|
|
3014
|
+
for (const key of Object.keys(faces)) {
|
|
3015
|
+
const k = parseFloat(key);
|
|
3016
|
+
const face = faces[k];
|
|
3017
|
+
const count = d1.get(k);
|
|
3018
|
+
result.combineInPlace(
|
|
3019
|
+
face.normalize(count * normalizationFactor / face.total())
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
result.privateData.except = {};
|
|
3023
|
+
return result;
|
|
3024
|
+
}
|
|
3025
|
+
function multiplyDice(n, d2) {
|
|
3026
|
+
if (n === 0) return new Dice(0);
|
|
3027
|
+
if (n === 1) return d2;
|
|
3028
|
+
const half = Math.floor(n / 2);
|
|
3029
|
+
let result = multiplyDice(half, d2);
|
|
3030
|
+
result = result.add(result);
|
|
3031
|
+
if (n % 2 === 1) {
|
|
3032
|
+
result = result.add(d2);
|
|
3033
|
+
}
|
|
3034
|
+
return result;
|
|
3035
|
+
}
|
|
3036
|
+
function opDice(diceList, keepFn) {
|
|
3037
|
+
return opDiceInternal(diceList, new Dice(), 0, [], 1, keepFn);
|
|
3038
|
+
}
|
|
3039
|
+
function opDiceInternal(diceList, result, index, values, weight, combineFn) {
|
|
3040
|
+
if (index === diceList.length) {
|
|
3041
|
+
return result.combine(Dice.scalar(combineFn(values)).normalize(weight));
|
|
3042
|
+
}
|
|
3043
|
+
const currentDice = diceList[index];
|
|
3044
|
+
for (const face of currentDice.keys()) {
|
|
3045
|
+
values.push(face);
|
|
3046
|
+
result = opDiceInternal(
|
|
3047
|
+
diceList,
|
|
3048
|
+
result,
|
|
3049
|
+
index + 1,
|
|
3050
|
+
values,
|
|
3051
|
+
weight * currentDice.get(face),
|
|
3052
|
+
combineFn
|
|
3053
|
+
);
|
|
3054
|
+
values.pop();
|
|
3055
|
+
}
|
|
3056
|
+
return result;
|
|
3057
|
+
}
|
|
3058
|
+
function parseArgumentInternal(s, n) {
|
|
3059
|
+
if (s.length === 0) return;
|
|
3060
|
+
const c = s[0];
|
|
3061
|
+
switch (c) {
|
|
3062
|
+
case "(":
|
|
3063
|
+
s.shift();
|
|
3064
|
+
return assertToken(s, ")", parseExpression(s, n));
|
|
3065
|
+
case "h":
|
|
3066
|
+
case "d":
|
|
3067
|
+
return parseDice(s, n);
|
|
3068
|
+
case "k":
|
|
3069
|
+
assertToken(s, "k");
|
|
3070
|
+
return parseKeep(s, n);
|
|
3071
|
+
case "n":
|
|
3072
|
+
return parseNumber(s, n);
|
|
3073
|
+
default:
|
|
3074
|
+
if (isDigit(c)) return parseNumber(s, n);
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
function parseBinaryArgument(arg, arr, n) {
|
|
3079
|
+
if (arr.length >= 4 && arr[0] === "h" && peek(arr, "half")) {
|
|
3080
|
+
assertToken(arr, "half");
|
|
3081
|
+
const diceArg = typeof arg === "number" ? Dice.scalar(arg) : arg;
|
|
3082
|
+
return diceArg.divideRoundDown(2);
|
|
3083
|
+
}
|
|
3084
|
+
const parsed = parseArgument(arr, n);
|
|
3085
|
+
return typeof parsed === "number" ? Dice.scalar(parsed) : parsed;
|
|
3086
|
+
}
|
|
3087
|
+
function assertToken(s, expected, ret) {
|
|
3088
|
+
for (const ch of expected) {
|
|
3089
|
+
const found = s.shift();
|
|
3090
|
+
if (found !== ch) {
|
|
3091
|
+
throw new Error(`Expected character '${ch}', found '${found}'`);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
return ret;
|
|
3095
|
+
}
|
|
3096
|
+
function parseDice(s, n) {
|
|
3097
|
+
let rerollOne = false;
|
|
3098
|
+
if (peek(s, "hd") && peekIsNumber(s, 2)) {
|
|
3099
|
+
assertToken(s, "h");
|
|
3100
|
+
assertToken(s, "d");
|
|
3101
|
+
rerollOne = true;
|
|
3102
|
+
} else if (peek(s, "d") && peekIsNumber(s, 1)) {
|
|
3103
|
+
assertToken(s, "d");
|
|
3104
|
+
} else {
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
const sides = parseNumber(s, n);
|
|
3108
|
+
let result = new Dice(sides);
|
|
3109
|
+
if (rerollOne) {
|
|
3110
|
+
result = result.deleteFace(1).combine(result);
|
|
3111
|
+
}
|
|
3112
|
+
return result;
|
|
3113
|
+
}
|
|
3114
|
+
function peek(arr, expected) {
|
|
3115
|
+
if (expected.length > arr.length) return false;
|
|
3116
|
+
for (let i = 0; i < expected.length; i++) {
|
|
3117
|
+
if (arr[i] !== expected.charAt(i)) return false;
|
|
3118
|
+
}
|
|
3119
|
+
return true;
|
|
3120
|
+
}
|
|
3121
|
+
function peekIsNumber(arr, index) {
|
|
3122
|
+
if (index >= arr.length) return false;
|
|
3123
|
+
return isDigit(arr[index]) || arr[index] === "n";
|
|
3124
|
+
}
|
|
3125
|
+
function parseNumber(s, n) {
|
|
3126
|
+
let ret = "";
|
|
3127
|
+
while (s.length > 0 && (isDigit(s[0]) || s[0] === "n")) {
|
|
3128
|
+
const ch = s.shift();
|
|
3129
|
+
ret += ch === "n" ? n.toString() : ch;
|
|
3130
|
+
}
|
|
3131
|
+
if (ret.length === 0) {
|
|
3132
|
+
throw new Error(`Expected number, found: '${s[0]}'`);
|
|
3133
|
+
}
|
|
3134
|
+
return parseInt(ret, 10);
|
|
3135
|
+
}
|
|
3136
|
+
function isDigit(c) {
|
|
3137
|
+
return c >= "0" && c <= "9";
|
|
3138
|
+
}
|
|
3139
|
+
function parseKeep(s, n) {
|
|
3140
|
+
let keepLowest = false;
|
|
3141
|
+
if (peek(s, "l")) {
|
|
3142
|
+
assertToken(s, "l");
|
|
3143
|
+
keepLowest = true;
|
|
3144
|
+
} else if (peek(s, "h")) {
|
|
3145
|
+
assertToken(s, "h");
|
|
3146
|
+
keepLowest = false;
|
|
3147
|
+
} else {
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
const keepCount = parseNumber(s, n);
|
|
3151
|
+
const result = parseArgumentInternal(s, n);
|
|
3152
|
+
if (result instanceof Dice) {
|
|
3153
|
+
result.privateData.keep = keepN(keepCount, keepLowest);
|
|
3154
|
+
return result;
|
|
3155
|
+
}
|
|
3156
|
+
throw new Error("Expected Dice after keep modifier");
|
|
3157
|
+
}
|
|
3158
|
+
function keepN(n, low) {
|
|
3159
|
+
return (values) => {
|
|
3160
|
+
const sorted = [...values].sort((a, b) => low ? a - b : b - a);
|
|
3161
|
+
return sorted.slice(0, n).reduce((sum, val) => sum + val, 0);
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
function parseOperation(s) {
|
|
3165
|
+
switch (s[0]) {
|
|
3166
|
+
case ")":
|
|
3167
|
+
return;
|
|
3168
|
+
case "a":
|
|
3169
|
+
assertToken(s, "ac");
|
|
3170
|
+
return Dice.prototype.ac;
|
|
3171
|
+
case "d":
|
|
3172
|
+
assertToken(s, "dc");
|
|
3173
|
+
return Dice.prototype.dc;
|
|
3174
|
+
case "!":
|
|
3175
|
+
assertToken(s, "!");
|
|
3176
|
+
const adv = Dice.prototype.advantage;
|
|
3177
|
+
adv.unary = true;
|
|
3178
|
+
return adv;
|
|
3179
|
+
case ">":
|
|
3180
|
+
assertToken(s, ">");
|
|
3181
|
+
return Dice.prototype.max;
|
|
3182
|
+
case "<":
|
|
3183
|
+
assertToken(s, "<");
|
|
3184
|
+
return Dice.prototype.min;
|
|
3185
|
+
case "+":
|
|
3186
|
+
assertToken(s, "+");
|
|
3187
|
+
return Dice.prototype.addNonZero;
|
|
3188
|
+
case "~":
|
|
3189
|
+
assertToken(s, "~");
|
|
3190
|
+
assertToken(s, "+");
|
|
3191
|
+
return Dice.prototype.add;
|
|
3192
|
+
case "-":
|
|
3193
|
+
assertToken(s, "-");
|
|
3194
|
+
return Dice.prototype.subtract;
|
|
3195
|
+
case "&":
|
|
3196
|
+
assertToken(s, "&");
|
|
3197
|
+
return Dice.prototype.combine;
|
|
3198
|
+
case "r":
|
|
3199
|
+
assertToken(s, "reroll");
|
|
3200
|
+
return Dice.prototype.reroll;
|
|
3201
|
+
case "*":
|
|
3202
|
+
assertToken(s, "*");
|
|
3203
|
+
if (peek(s, "*")) {
|
|
3204
|
+
assertToken(s, "*");
|
|
3205
|
+
return Dice.prototype.multiply;
|
|
3206
|
+
}
|
|
3207
|
+
return Dice.prototype.conditionalApply;
|
|
3208
|
+
case "/":
|
|
3209
|
+
assertToken(s, "/");
|
|
3210
|
+
if (s[0] === "/") {
|
|
3211
|
+
assertToken(s, "/");
|
|
3212
|
+
return Dice.prototype.divideRoundDown;
|
|
3213
|
+
}
|
|
3214
|
+
return Dice.prototype.divideRoundUp;
|
|
3215
|
+
case "=":
|
|
3216
|
+
assertToken(s, "=");
|
|
3217
|
+
return Dice.prototype.eq;
|
|
3218
|
+
}
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
2338
3222
|
// src/builder/prob.ts
|
|
2339
3223
|
function d20PmfFromCdf(cdfPow, eps = EPS) {
|
|
2340
3224
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -2476,6 +3360,9 @@ var RollBuilder = class _RollBuilder {
|
|
|
2476
3360
|
if (isNaN(arg)) throw new Error("Invalid NaN value for argument");
|
|
2477
3361
|
return new _RollBuilder(0).plus(arg);
|
|
2478
3362
|
}
|
|
3363
|
+
if (typeof arg === "string") {
|
|
3364
|
+
return new ParsedRollBuilder(arg);
|
|
3365
|
+
}
|
|
2479
3366
|
if (arg instanceof _RollBuilder) {
|
|
2480
3367
|
return arg;
|
|
2481
3368
|
}
|
|
@@ -3058,6 +3945,34 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
|
|
|
3058
3945
|
return new _AlwaysCritBuilder(baseCopy, newConfig, this.fromAlwaysHit);
|
|
3059
3946
|
}
|
|
3060
3947
|
};
|
|
3948
|
+
var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
|
|
3949
|
+
constructor(expression) {
|
|
3950
|
+
super([]);
|
|
3951
|
+
__publicField(this, "cachedPMF");
|
|
3952
|
+
__publicField(this, "originalExpression");
|
|
3953
|
+
this.originalExpression = expression;
|
|
3954
|
+
this.cachedPMF = parse(expression, 0);
|
|
3955
|
+
}
|
|
3956
|
+
toPMF(eps = 0) {
|
|
3957
|
+
return this.cachedPMF;
|
|
3958
|
+
}
|
|
3959
|
+
toExpression() {
|
|
3960
|
+
return this.originalExpression;
|
|
3961
|
+
}
|
|
3962
|
+
toAST() {
|
|
3963
|
+
throw new Error(
|
|
3964
|
+
"ParsedRollBuilder does not support AST conversion. Use the builder API instead."
|
|
3965
|
+
);
|
|
3966
|
+
}
|
|
3967
|
+
copy() {
|
|
3968
|
+
return new _ParsedRollBuilder(this.originalExpression);
|
|
3969
|
+
}
|
|
3970
|
+
doubleDice() {
|
|
3971
|
+
throw new Error(
|
|
3972
|
+
"ParsedRollBuilder does not support doubleDice(). Use explicit onCrit() with the crit damage expression instead."
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
};
|
|
3061
3976
|
|
|
3062
3977
|
// src/builder/factory.ts
|
|
3063
3978
|
var rollFn = (count, sidesOrDie, modifier) => {
|
|
@@ -3588,7 +4503,11 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3588
4503
|
if (this.critEffect) {
|
|
3589
4504
|
crit = this.critEffect;
|
|
3590
4505
|
} else {
|
|
3591
|
-
|
|
4506
|
+
if (this.hitEffect instanceof ParsedRollBuilder) {
|
|
4507
|
+
crit = RollBuilder.fromArgs(0);
|
|
4508
|
+
} else {
|
|
4509
|
+
crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
|
|
4510
|
+
}
|
|
3592
4511
|
}
|
|
3593
4512
|
const critThreshold = this.check.critThreshold;
|
|
3594
4513
|
if (critThreshold < 1 || critThreshold > 20) {
|
|
@@ -3596,11 +4515,14 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3596
4515
|
`Invalid crit threshold: ${critThreshold}. Must be between 1 and 20.`
|
|
3597
4516
|
);
|
|
3598
4517
|
}
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
4518
|
+
const critExpression = crit.toExpression();
|
|
4519
|
+
if (critExpression !== "0") {
|
|
4520
|
+
if (critThreshold === 20) {
|
|
4521
|
+
effectPart += ` crit (${critExpression})`;
|
|
4522
|
+
} else {
|
|
4523
|
+
const xcritNumber = 21 - critThreshold;
|
|
4524
|
+
effectPart += ` xcrit${xcritNumber} (${critExpression})`;
|
|
4525
|
+
}
|
|
3604
4526
|
}
|
|
3605
4527
|
}
|
|
3606
4528
|
if (this.missEffect) {
|
|
@@ -3681,7 +4603,7 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3681
4603
|
pCrit,
|
|
3682
4604
|
pMiss: pmiss
|
|
3683
4605
|
} = this.resolveProbabilities(this.check, eps);
|
|
3684
|
-
const hitPMF = this.hitEffect ? pmfFromRollBuilder(this.hitEffect, eps) : PMF.delta(0, eps);
|
|
4606
|
+
const hitPMF = this.hitEffect ? this.hitEffect instanceof ParsedRollBuilder ? this.hitEffect.toPMF(eps) : pmfFromRollBuilder(this.hitEffect, eps) : PMF.delta(0, eps);
|
|
3685
4607
|
let critPMF = null;
|
|
3686
4608
|
let phit = pHit;
|
|
3687
4609
|
let pcrit = pCrit;
|
|
@@ -3690,10 +4612,22 @@ var AttackBuilder = class _AttackBuilder {
|
|
|
3690
4612
|
phit += pcrit;
|
|
3691
4613
|
pcrit = 0;
|
|
3692
4614
|
} else {
|
|
3693
|
-
|
|
3694
|
-
|
|
4615
|
+
let critBuilder;
|
|
4616
|
+
if (this.critEffect) {
|
|
4617
|
+
critBuilder = this.critEffect;
|
|
4618
|
+
} else if (this.hitEffect instanceof ParsedRollBuilder) {
|
|
4619
|
+
critPMF = null;
|
|
4620
|
+
phit += pcrit;
|
|
4621
|
+
pcrit = 0;
|
|
4622
|
+
critBuilder = void 0;
|
|
4623
|
+
} else {
|
|
4624
|
+
critBuilder = this.hitEffect?.copy().doubleDice();
|
|
4625
|
+
}
|
|
4626
|
+
if (critBuilder) {
|
|
4627
|
+
critPMF = critBuilder instanceof ParsedRollBuilder ? critBuilder.toPMF(eps) : pmfFromRollBuilder(critBuilder, eps);
|
|
4628
|
+
}
|
|
3695
4629
|
}
|
|
3696
|
-
const missPMF = this.missEffect ? pmfFromRollBuilder(this.missEffect, eps) : PMF.delta(0, eps);
|
|
4630
|
+
const missPMF = this.missEffect ? this.missEffect instanceof ParsedRollBuilder ? this.missEffect.toPMF(eps) : pmfFromRollBuilder(this.missEffect, eps) : PMF.delta(0, eps);
|
|
3697
4631
|
const mix = new Mixture(eps);
|
|
3698
4632
|
if (phit > 0) mix.add("hit", hitPMF, phit);
|
|
3699
4633
|
if (critPMF && pcrit > 0) mix.add("crit", critPMF, pcrit);
|
|
@@ -3944,6 +4878,6 @@ RollBuilder.prototype.dc = function(saveDC) {
|
|
|
3944
4878
|
return new DCBuilder(this).dc(saveDC);
|
|
3945
4879
|
};
|
|
3946
4880
|
|
|
3947
|
-
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, RollBuilder, SaveBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
|
|
4881
|
+
export { ACBuilder, AlwaysCritBuilder, AlwaysHitBuilder, DCBuilder, HalfRollBuilder, MaxOfRollBuilder, ParsedRollBuilder, RollBuilder, SaveBuilder, builderPMFCache, d, d10, d100, d12, d20, d4, d6, d8, defaultConfig, flat, hd20, roll };
|
|
3948
4882
|
//# sourceMappingURL=index.js.map
|
|
3949
4883
|
//# sourceMappingURL=index.js.map
|