@yipe/dice 0.2.17 → 0.2.20

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.
@@ -2405,6 +2405,822 @@ var Mixture = class _Mixture {
2405
2405
  }
2406
2406
  };
2407
2407
 
2408
+ // src/parser/dice.ts
2409
+ var Dice = class _Dice {
2410
+ constructor(x = 0) {
2411
+ __publicField(this, "faces", {});
2412
+ __publicField(this, "privateData", {});
2413
+ __publicField(this, "outcomeData", {});
2414
+ __publicField(this, "hasHitDistributionCalculated", false);
2415
+ __publicField(this, "identifier");
2416
+ if (x <= 0) return;
2417
+ for (let i = 1; i <= x; i++) {
2418
+ this.faces[i] = 1;
2419
+ }
2420
+ }
2421
+ getOutcomeDistribution(key) {
2422
+ if (key === "hit") {
2423
+ this.ensureHitDistribution();
2424
+ }
2425
+ return this.outcomeData[key];
2426
+ }
2427
+ getFullOutcomeDistribution() {
2428
+ return { ...this.outcomeData };
2429
+ }
2430
+ setOutcomeDistribution(key, data) {
2431
+ if (data) {
2432
+ this.outcomeData[key] = data;
2433
+ } else {
2434
+ delete this.outcomeData[key];
2435
+ }
2436
+ }
2437
+ hasOutcomeData(key) {
2438
+ if (key === "hit") {
2439
+ this.ensureHitDistribution();
2440
+ }
2441
+ const data = this.outcomeData[key];
2442
+ return data !== void 0 && Object.keys(data).length > 0;
2443
+ }
2444
+ getOutcomeCount(key, face) {
2445
+ return this.outcomeData[key]?.[face] ?? 0;
2446
+ }
2447
+ getAverage(key) {
2448
+ const distribution = this.getOutcomeDistribution(key);
2449
+ if (!distribution) return 0;
2450
+ const totalCount = Object.values(distribution).reduce(
2451
+ (sum, count) => sum + count,
2452
+ 0
2453
+ );
2454
+ const expectedDamage = Object.entries(distribution).reduce(
2455
+ (sum, [damage, count]) => sum + Number(damage) * count,
2456
+ 0
2457
+ );
2458
+ if (totalCount === 0) return 0;
2459
+ return expectedDamage / totalCount;
2460
+ }
2461
+ // TODO this can be private later if we change how testing works
2462
+ calculateHitDistribution() {
2463
+ const hitValues = {};
2464
+ for (const [face, totalCount] of Object.entries(this.faces)) {
2465
+ const numFace = Number(face);
2466
+ let hitCount = totalCount;
2467
+ for (const outcomeType of [
2468
+ "crit",
2469
+ "missNone",
2470
+ "missDamage",
2471
+ "saveHalf",
2472
+ "saveFail",
2473
+ "pc"
2474
+ ]) {
2475
+ const distribution = this.getOutcomeDistribution(outcomeType);
2476
+ if (distribution && distribution[numFace]) {
2477
+ hitCount -= distribution[numFace];
2478
+ }
2479
+ }
2480
+ if (numFace === 0) {
2481
+ hitCount = 0;
2482
+ }
2483
+ if (hitCount < 0) {
2484
+ console.error("hitCount is <=0?", face, totalCount, hitCount);
2485
+ hitCount = 0;
2486
+ }
2487
+ hitValues[numFace] = hitCount;
2488
+ }
2489
+ return hitValues;
2490
+ }
2491
+ ensureHitDistribution() {
2492
+ if (!this.hasHitDistributionCalculated) {
2493
+ const hitValues = this.calculateHitDistribution();
2494
+ this.setOutcomeDistribution("hit", hitValues);
2495
+ this.hasHitDistributionCalculated = true;
2496
+ }
2497
+ }
2498
+ // PRIVATE FUNCTIONS
2499
+ binaryOp(other, op, diceConstructor) {
2500
+ const result = diceConstructor ? diceConstructor() : new _Dice();
2501
+ const isScalar = typeof other === "number";
2502
+ const keys1 = this.keys();
2503
+ for (const key1 of keys1) {
2504
+ const value1 = this.faces[key1];
2505
+ if (isScalar) {
2506
+ const resultKey = op(key1, other);
2507
+ result.increment(resultKey, value1);
2508
+ } else {
2509
+ const keys2 = other.keys();
2510
+ for (const key2 of keys2) {
2511
+ const value2 = other.faces[key2];
2512
+ const resultKey = op(key1, key2);
2513
+ result.increment(resultKey, value1 * value2);
2514
+ }
2515
+ }
2516
+ }
2517
+ return result;
2518
+ }
2519
+ removeFaces(facesToRemove) {
2520
+ const result = new _Dice();
2521
+ for (const [key, value] of Object.entries(this.faces)) {
2522
+ const numKey = Number(key);
2523
+ if (!facesToRemove.includes(numKey)) {
2524
+ result.faces[numKey] = value;
2525
+ }
2526
+ }
2527
+ result.privateData = { ...this.privateData };
2528
+ result.outcomeData = { ...this.outcomeData };
2529
+ return result;
2530
+ }
2531
+ // PUBLIC FUNTIONS
2532
+ getFaceEntries() {
2533
+ return Object.entries(this.faces).map(([k, v]) => [Number(k), v]);
2534
+ }
2535
+ getFaceMap() {
2536
+ return { ...this.faces };
2537
+ }
2538
+ get(face) {
2539
+ return this.faces[face] ?? 0;
2540
+ }
2541
+ keys() {
2542
+ return Object.keys(this.faces).map(Number);
2543
+ }
2544
+ values() {
2545
+ return Object.values(this.faces);
2546
+ }
2547
+ total() {
2548
+ return Object.values(this.faces).reduce((sum, value) => sum + value, 0);
2549
+ }
2550
+ setFace(key, value) {
2551
+ this.faces[key] = value;
2552
+ }
2553
+ static scalar(value) {
2554
+ const result = new _Dice();
2555
+ result.increment(value, 1);
2556
+ return result;
2557
+ }
2558
+ maxFace() {
2559
+ const numericKeys = this.keys();
2560
+ if (numericKeys.length === 0) {
2561
+ throw new Error("No numeric faces found");
2562
+ }
2563
+ return Math.max(...numericKeys);
2564
+ }
2565
+ minFace() {
2566
+ const numericKeys = this.keys();
2567
+ if (numericKeys.length === 0) {
2568
+ throw new Error("No numeric faces found");
2569
+ }
2570
+ return Math.min(...numericKeys);
2571
+ }
2572
+ increment(face, count) {
2573
+ const current = this.faces[face] || 0;
2574
+ this.faces[face] = current + count;
2575
+ }
2576
+ normalize(scalar) {
2577
+ const result = new _Dice();
2578
+ for (const [face, count] of Object.entries(this.faces)) {
2579
+ result.faces[Number(face)] = count * scalar;
2580
+ }
2581
+ result.privateData = { ...this.privateData };
2582
+ result.outcomeData = { ...this.outcomeData };
2583
+ return result;
2584
+ }
2585
+ // OPERATIONS
2586
+ add(other) {
2587
+ return this.binaryOp(other, (a, b) => a + b);
2588
+ }
2589
+ subtract(other) {
2590
+ return this.binaryOp(other, (a, b) => a - b);
2591
+ }
2592
+ conditionalApply(other) {
2593
+ return this.binaryOp(other, (a, b) => (a === 0 ? 0 : 1) * b);
2594
+ }
2595
+ multiply(other) {
2596
+ return this.binaryOp(other, (a, b) => a * b);
2597
+ }
2598
+ addNonZero(other) {
2599
+ return this.binaryOp(other, (a, b) => a !== 0 ? a + b : a);
2600
+ }
2601
+ eq(other) {
2602
+ return this.binaryOp(other, (a, b) => a === b ? 1 : 0);
2603
+ }
2604
+ max(other) {
2605
+ return this.binaryOp(other, (a, b) => Math.max(a, b));
2606
+ }
2607
+ min(other) {
2608
+ return this.binaryOp(other, (a, b) => Math.min(a, b));
2609
+ }
2610
+ advantage() {
2611
+ return this.max(this);
2612
+ }
2613
+ ge(other) {
2614
+ return this.binaryOp(other, (a, b) => a >= b ? 0 : 1);
2615
+ }
2616
+ divide(other) {
2617
+ return this.binaryOp(other, (a, b) => a / b);
2618
+ }
2619
+ divideRoundUp(other) {
2620
+ return this.binaryOp(other, (a, b) => Math.ceil(a / b));
2621
+ }
2622
+ divideRoundDown(other) {
2623
+ return this.binaryOp(other, (a, b) => Math.floor(a / b));
2624
+ }
2625
+ and(other) {
2626
+ return this.binaryOp(other, (a, b) => a && b ? 1 : 0);
2627
+ }
2628
+ checkTarget(other, comparisonLogic) {
2629
+ const createResult = () => {
2630
+ const result = new _Dice();
2631
+ result.increment(0, 0);
2632
+ result.increment(1, 0);
2633
+ return result;
2634
+ };
2635
+ return this.binaryOp(other, comparisonLogic, createResult);
2636
+ }
2637
+ dc(other) {
2638
+ const dcCheck = (a, b) => a >= b ? 0 : 1;
2639
+ const result = this.checkTarget(other, dcCheck);
2640
+ result.privateData.isDCCheck = true;
2641
+ return result;
2642
+ }
2643
+ ac(other) {
2644
+ const acCheck = (a, b) => a >= b ? a : 0;
2645
+ return this.checkTarget(other, acCheck);
2646
+ }
2647
+ deleteFace(face) {
2648
+ const result = new _Dice();
2649
+ for (const [key, value] of Object.entries(this.faces)) {
2650
+ const numKey = Number(key);
2651
+ if (numKey !== face) {
2652
+ result.increment(numKey, value);
2653
+ }
2654
+ }
2655
+ result.privateData = { ...this.privateData };
2656
+ result.outcomeData = { ...this.outcomeData };
2657
+ return result;
2658
+ }
2659
+ reroll(toReroll) {
2660
+ const rerollDice = typeof toReroll === "number" ? _Dice.scalar(toReroll) : toReroll;
2661
+ const removed = this.removeFaces(rerollDice.keys());
2662
+ let result = new _Dice();
2663
+ for (const face of this.keys()) {
2664
+ const wasRerolled = rerollDice.keys().includes(face);
2665
+ result = result.combine(removed);
2666
+ if (wasRerolled) {
2667
+ result = result.combine(this);
2668
+ }
2669
+ }
2670
+ return result;
2671
+ }
2672
+ // This is not addition and not rolling two dice at once.
2673
+ // Instead, it’s mixing two distributions into a single weighted die.
2674
+ combine(other) {
2675
+ if (typeof other === "number") {
2676
+ other = _Dice.scalar(other);
2677
+ }
2678
+ const result = new _Dice();
2679
+ for (const [key, value] of Object.entries(other.faces)) {
2680
+ result.faces[Number(key)] = value;
2681
+ }
2682
+ const except = new _Dice();
2683
+ for (const [key, value] of Object.entries(this.faces)) {
2684
+ const numKey = Number(key);
2685
+ result.increment(numKey, value);
2686
+ if (!(numKey in other.faces)) {
2687
+ except.increment(numKey, value);
2688
+ }
2689
+ }
2690
+ result.privateData = { ...this.privateData, except: other };
2691
+ result.outcomeData = { ...this.outcomeData };
2692
+ return result;
2693
+ }
2694
+ combineInPlace(other) {
2695
+ for (const [key, value] of Object.entries(other.faces)) {
2696
+ const numKey = Number(key);
2697
+ const current = this.faces[numKey] || 0;
2698
+ this.faces[numKey] = current + value;
2699
+ }
2700
+ }
2701
+ percent() {
2702
+ const total = this.total();
2703
+ const result = {};
2704
+ for (const [face, count] of Object.entries(this.faces)) {
2705
+ result[Number(face)] = count / total;
2706
+ }
2707
+ return result;
2708
+ }
2709
+ average() {
2710
+ const total = this.total();
2711
+ if (total === 0) return 0;
2712
+ let sum = 0;
2713
+ for (const [key, value] of Object.entries(this.faces)) {
2714
+ sum += Number(key) * value;
2715
+ }
2716
+ return sum / total;
2717
+ }
2718
+ /*
2719
+ * Convert dice to PMF using OutcomeType labels directly from damage distribution.
2720
+ * This is much cleaner than the original complex distribution conversion.
2721
+ */
2722
+ toPMF(numEpsilon = EPS) {
2723
+ const total = this.total();
2724
+ if (total === 0) return PMF.empty(numEpsilon);
2725
+ this.ensureHitDistribution();
2726
+ const map = /* @__PURE__ */ new Map();
2727
+ const hitDistro = this.getOutcomeDistribution("hit") || {};
2728
+ const critDistro = this.getOutcomeDistribution("crit") || {};
2729
+ const missDistro = this.getOutcomeDistribution("missDamage") || {};
2730
+ const saveDistro = this.getOutcomeDistribution("saveHalf") || {};
2731
+ const pcDistro = this.getOutcomeDistribution("pc") || {};
2732
+ let isSaveHalf = false;
2733
+ for (const halfDamage of Object.keys(saveDistro).map(Number)) {
2734
+ const fullDamage = halfDamage * 2;
2735
+ if (fullDamage > 0 && hitDistro[fullDamage]) {
2736
+ isSaveHalf = true;
2737
+ break;
2738
+ }
2739
+ }
2740
+ const isDCCheck = this.privateData.isDCCheck === true;
2741
+ const clampNonNeg = (x) => x < 0 && x > -1e-15 ? 0 : x;
2742
+ for (const [faceStr, faceCountRaw] of Object.entries(this.getFaceMap())) {
2743
+ const face = Number(faceStr);
2744
+ const faceCount = Number(faceCountRaw);
2745
+ if (faceCount <= 0) continue;
2746
+ let p = faceCount / total;
2747
+ p = clampNonNeg(p);
2748
+ if (!(p > 0)) continue;
2749
+ if (numEpsilon >= 0 && p < numEpsilon) continue;
2750
+ const count = {};
2751
+ const attr = {};
2752
+ if (hitDistro[face]) {
2753
+ const c = clampNonNeg(hitDistro[face] / total);
2754
+ if (c > 0) {
2755
+ if (isSaveHalf || isDCCheck) {
2756
+ count.saveFail = c;
2757
+ attr.saveFail = clampNonNeg(face * hitDistro[face] / total);
2758
+ } else {
2759
+ count.hit = c;
2760
+ attr.hit = clampNonNeg(face * hitDistro[face] / total);
2761
+ }
2762
+ }
2763
+ }
2764
+ if (critDistro[face]) {
2765
+ const c = clampNonNeg(critDistro[face] / total);
2766
+ if (c > 0) {
2767
+ count.crit = c;
2768
+ attr.crit = clampNonNeg(face * critDistro[face] / total);
2769
+ }
2770
+ }
2771
+ if (missDistro[face]) {
2772
+ const c = clampNonNeg(missDistro[face] / total);
2773
+ if (c > 0) {
2774
+ count.missDamage = c;
2775
+ attr.missDamage = clampNonNeg(face * missDistro[face] / total);
2776
+ }
2777
+ }
2778
+ if (saveDistro[face]) {
2779
+ const c = clampNonNeg(saveDistro[face] / total);
2780
+ if (c > 0) {
2781
+ if (isSaveHalf) {
2782
+ count.saveHalf = c;
2783
+ attr.saveHalf = clampNonNeg(face * saveDistro[face] / total);
2784
+ } else {
2785
+ count.saveFail = (count.saveFail ?? 0) + c;
2786
+ attr.saveFail = clampNonNeg(
2787
+ (attr.saveFail ?? 0) + face * saveDistro[face] / total
2788
+ );
2789
+ }
2790
+ }
2791
+ }
2792
+ if (pcDistro[face]) {
2793
+ const c = clampNonNeg(pcDistro[face] / total);
2794
+ if (c > 0) {
2795
+ count.pc = c;
2796
+ attr.pc = clampNonNeg(face * pcDistro[face] / total);
2797
+ }
2798
+ }
2799
+ if (!isSaveHalf && !isDCCheck) {
2800
+ const distroCountRaw = (hitDistro[face] || 0) + (critDistro[face] || 0) + (missDistro[face] || 0) + (saveDistro[face] || 0) + (pcDistro[face] || 0);
2801
+ const unaccountedCount = clampNonNeg(faceCount - distroCountRaw);
2802
+ if (unaccountedCount > 0) {
2803
+ const frac = clampNonNeg(unaccountedCount / total);
2804
+ if (frac > 0) {
2805
+ count.missNone = (count.missNone ?? 0) + frac;
2806
+ }
2807
+ }
2808
+ }
2809
+ const bin = { p, count };
2810
+ if (Object.keys(attr).length > 0) {
2811
+ bin.attr = attr;
2812
+ }
2813
+ map.set(face, bin);
2814
+ }
2815
+ const identifier = this.identifier || "ERROR";
2816
+ if (identifier === "ERROR") {
2817
+ console.error("Dice identifier is undefined", this);
2818
+ }
2819
+ return new PMF(map, numEpsilon, true, identifier).compact(numEpsilon, true);
2820
+ }
2821
+ };
2822
+
2823
+ // src/parser/parser.ts
2824
+ var parseCache = new LRUCache(1e3);
2825
+ function parse(expression, n = 0) {
2826
+ const cleaned = expression.replace(/ /g, "").toLowerCase();
2827
+ {
2828
+ const cacheKey = `${cleaned}:${n}`;
2829
+ const cached = parseCache.get(cacheKey);
2830
+ if (cached) return cached;
2831
+ }
2832
+ const chars = [...cleaned];
2833
+ let result = void 0;
2834
+ try {
2835
+ result = parseExpression(chars, n);
2836
+ } catch (error) {
2837
+ throw new Error(`Cannot parse dice expression [${expression}]: ${error}`);
2838
+ }
2839
+ try {
2840
+ result.privateData = result.privateData || {};
2841
+ result.identifier = cleaned;
2842
+ } catch {
2843
+ }
2844
+ if (chars.length > 0) {
2845
+ throw new Error(
2846
+ `Unexpected token: '${chars[0]}' from expression: '${expression}'`
2847
+ );
2848
+ }
2849
+ const resultPMF = result.toPMF(-1);
2850
+ {
2851
+ const cacheKey = `${cleaned}:${n}`;
2852
+ parseCache.set(cacheKey, resultPMF);
2853
+ }
2854
+ return resultPMF;
2855
+ }
2856
+ function combineDiceWithNormalization(dice, normValue, outcomeType, currentNorm, finalResult) {
2857
+ dice = dice.normalize(currentNorm);
2858
+ finalResult = finalResult.normalize(normValue);
2859
+ finalResult.setOutcomeDistribution(outcomeType, dice.getFaceMap());
2860
+ finalResult = finalResult.combine(dice);
2861
+ return { newNorm: currentNorm * normValue, updatedResult: finalResult };
2862
+ }
2863
+ function parseExpression(arr, n) {
2864
+ const result = (() => {
2865
+ const res = parseArgument(arr, n);
2866
+ return typeof res === "number" ? Dice.scalar(res) : res;
2867
+ })();
2868
+ let op = parseOperation(arr);
2869
+ let finalResult = result;
2870
+ while (op != null) {
2871
+ const arg = !op.unary ? parseArgument(arr, n) : finalResult;
2872
+ let crit;
2873
+ let critNorm = 1;
2874
+ if (arr[0] === "x" || arr[0] === "c") {
2875
+ const isXcrit = arr[0] === "x";
2876
+ if (isXcrit) assertToken(arr, "x");
2877
+ assertToken(arr, "c");
2878
+ assertToken(arr, "r");
2879
+ assertToken(arr, "i");
2880
+ assertToken(arr, "t");
2881
+ const count = isXcrit ? parseNumber(arr, n) : 1;
2882
+ crit = new Dice();
2883
+ for (let i = 0; i < count; i++) {
2884
+ const max = finalResult.maxFace();
2885
+ crit.setFace(max, finalResult.get(max));
2886
+ finalResult = finalResult.deleteFace(max);
2887
+ }
2888
+ critNorm = crit.total();
2889
+ crit = op.call(crit, parseBinaryArgument(arg, arr, n));
2890
+ critNorm = crit && critNorm ? crit.total() / critNorm : 1;
2891
+ }
2892
+ let save;
2893
+ let saveNorm = 1;
2894
+ if (arr[0] === "s") {
2895
+ assertToken(arr, "s");
2896
+ assertToken(arr, "a");
2897
+ assertToken(arr, "v");
2898
+ assertToken(arr, "e");
2899
+ save = new Dice();
2900
+ const min = finalResult.minFace();
2901
+ save.increment(min > 0 ? min : 1, finalResult.get(min));
2902
+ saveNorm = save.total();
2903
+ finalResult = finalResult.deleteFace(min);
2904
+ save = op.call(save, parseBinaryArgument(arg, arr, n));
2905
+ saveNorm = save && saveNorm ? save.total() / saveNorm : 1;
2906
+ }
2907
+ let pc;
2908
+ let pcNorm = 1;
2909
+ if (arr.length >= 2 && arr[0] === "p" && arr[1] === "c") {
2910
+ assertToken(arr, "p");
2911
+ assertToken(arr, "c");
2912
+ pc = new Dice();
2913
+ const min = finalResult.minFace();
2914
+ pc.increment(min > 0 ? min : 1, finalResult.get(min));
2915
+ const missBefore = pc.total();
2916
+ finalResult = finalResult.deleteFace(min);
2917
+ pc = op.call(pc, parseBinaryArgument(arg, arr, n)).divideRoundDown(2);
2918
+ const missAfter = pc ? pc.total() : 0;
2919
+ pcNorm = missBefore ? missAfter / missBefore : 1;
2920
+ }
2921
+ let miss;
2922
+ let missNorm = 1;
2923
+ if (arr[0] === "m") {
2924
+ assertToken(arr, "m");
2925
+ assertToken(arr, "i");
2926
+ assertToken(arr, "s");
2927
+ assertToken(arr, "s");
2928
+ miss = new Dice();
2929
+ const min = finalResult.minFace();
2930
+ miss.increment(min > 0 ? min : 1, finalResult.get(min));
2931
+ missNorm = miss.total();
2932
+ finalResult = finalResult.deleteFace(min);
2933
+ miss = op.call(miss, parseBinaryArgument(arg, arr, n));
2934
+ missNorm = miss && missNorm ? miss.total() / missNorm : 1;
2935
+ }
2936
+ let norm = finalResult.total();
2937
+ finalResult = op.call(finalResult, arg);
2938
+ norm = norm ? finalResult.total() / norm : 1;
2939
+ if (crit) {
2940
+ const result2 = combineDiceWithNormalization(
2941
+ crit,
2942
+ critNorm,
2943
+ "crit",
2944
+ norm,
2945
+ finalResult
2946
+ );
2947
+ norm = result2.newNorm;
2948
+ finalResult = result2.updatedResult;
2949
+ }
2950
+ if (save) {
2951
+ const result2 = combineDiceWithNormalization(
2952
+ save,
2953
+ saveNorm,
2954
+ "saveHalf",
2955
+ norm,
2956
+ finalResult
2957
+ );
2958
+ norm = result2.newNorm;
2959
+ finalResult = result2.updatedResult;
2960
+ }
2961
+ if (miss) {
2962
+ const result2 = combineDiceWithNormalization(
2963
+ miss,
2964
+ missNorm,
2965
+ "missDamage",
2966
+ norm,
2967
+ finalResult
2968
+ );
2969
+ norm = result2.newNorm;
2970
+ finalResult = result2.updatedResult;
2971
+ }
2972
+ if (pc) {
2973
+ const result2 = combineDiceWithNormalization(
2974
+ pc,
2975
+ pcNorm,
2976
+ "pc",
2977
+ norm,
2978
+ finalResult
2979
+ );
2980
+ norm = result2.newNorm;
2981
+ finalResult = result2.updatedResult;
2982
+ }
2983
+ op = parseOperation(arr);
2984
+ }
2985
+ return finalResult;
2986
+ }
2987
+ function parseArgument(s, n) {
2988
+ let result = parseArgumentInternal(s, n);
2989
+ while (true) {
2990
+ const next = parseArgumentInternal(s, n);
2991
+ if (next === void 0) break;
2992
+ result = multiplyDiceByDice(result, next);
2993
+ }
2994
+ return result;
2995
+ }
2996
+ function multiplyDiceByDice(d1, d2) {
2997
+ if (typeof d1 === "number") d1 = Dice.scalar(d1);
2998
+ if (typeof d2 === "number") d2 = Dice.scalar(d2);
2999
+ const result = new Dice();
3000
+ const faces = {};
3001
+ let normalizationFactor = 1;
3002
+ for (const key of d1.keys()) {
3003
+ let face;
3004
+ if (typeof key !== "number") {
3005
+ continue;
3006
+ }
3007
+ if (d2.privateData.keep) {
3008
+ const repeat = Array(key).fill(d2);
3009
+ face = opDice(repeat, d2.privateData.keep);
3010
+ } else {
3011
+ face = multiplyDice(key, d2);
3012
+ }
3013
+ normalizationFactor *= face.total();
3014
+ faces[key] = face;
3015
+ }
3016
+ for (const key of Object.keys(faces)) {
3017
+ const k = parseFloat(key);
3018
+ const face = faces[k];
3019
+ const count = d1.get(k);
3020
+ result.combineInPlace(
3021
+ face.normalize(count * normalizationFactor / face.total())
3022
+ );
3023
+ }
3024
+ result.privateData.except = {};
3025
+ return result;
3026
+ }
3027
+ function multiplyDice(n, d2) {
3028
+ if (n === 0) return new Dice(0);
3029
+ if (n === 1) return d2;
3030
+ const half = Math.floor(n / 2);
3031
+ let result = multiplyDice(half, d2);
3032
+ result = result.add(result);
3033
+ if (n % 2 === 1) {
3034
+ result = result.add(d2);
3035
+ }
3036
+ return result;
3037
+ }
3038
+ function opDice(diceList, keepFn) {
3039
+ return opDiceInternal(diceList, new Dice(), 0, [], 1, keepFn);
3040
+ }
3041
+ function opDiceInternal(diceList, result, index, values, weight, combineFn) {
3042
+ if (index === diceList.length) {
3043
+ return result.combine(Dice.scalar(combineFn(values)).normalize(weight));
3044
+ }
3045
+ const currentDice = diceList[index];
3046
+ for (const face of currentDice.keys()) {
3047
+ values.push(face);
3048
+ result = opDiceInternal(
3049
+ diceList,
3050
+ result,
3051
+ index + 1,
3052
+ values,
3053
+ weight * currentDice.get(face),
3054
+ combineFn
3055
+ );
3056
+ values.pop();
3057
+ }
3058
+ return result;
3059
+ }
3060
+ function parseArgumentInternal(s, n) {
3061
+ if (s.length === 0) return;
3062
+ const c = s[0];
3063
+ switch (c) {
3064
+ case "(":
3065
+ s.shift();
3066
+ return assertToken(s, ")", parseExpression(s, n));
3067
+ case "h":
3068
+ case "d":
3069
+ return parseDice(s, n);
3070
+ case "k":
3071
+ assertToken(s, "k");
3072
+ return parseKeep(s, n);
3073
+ case "n":
3074
+ return parseNumber(s, n);
3075
+ default:
3076
+ if (isDigit(c)) return parseNumber(s, n);
3077
+ return;
3078
+ }
3079
+ }
3080
+ function parseBinaryArgument(arg, arr, n) {
3081
+ if (arr.length >= 4 && arr[0] === "h" && peek(arr, "half")) {
3082
+ assertToken(arr, "half");
3083
+ const diceArg = typeof arg === "number" ? Dice.scalar(arg) : arg;
3084
+ return diceArg.divideRoundDown(2);
3085
+ }
3086
+ const parsed = parseArgument(arr, n);
3087
+ return typeof parsed === "number" ? Dice.scalar(parsed) : parsed;
3088
+ }
3089
+ function assertToken(s, expected, ret) {
3090
+ for (const ch of expected) {
3091
+ const found = s.shift();
3092
+ if (found !== ch) {
3093
+ throw new Error(`Expected character '${ch}', found '${found}'`);
3094
+ }
3095
+ }
3096
+ return ret;
3097
+ }
3098
+ function parseDice(s, n) {
3099
+ let rerollOne = false;
3100
+ if (peek(s, "hd") && peekIsNumber(s, 2)) {
3101
+ assertToken(s, "h");
3102
+ assertToken(s, "d");
3103
+ rerollOne = true;
3104
+ } else if (peek(s, "d") && peekIsNumber(s, 1)) {
3105
+ assertToken(s, "d");
3106
+ } else {
3107
+ return;
3108
+ }
3109
+ const sides = parseNumber(s, n);
3110
+ let result = new Dice(sides);
3111
+ if (rerollOne) {
3112
+ result = result.deleteFace(1).combine(result);
3113
+ }
3114
+ return result;
3115
+ }
3116
+ function peek(arr, expected) {
3117
+ if (expected.length > arr.length) return false;
3118
+ for (let i = 0; i < expected.length; i++) {
3119
+ if (arr[i] !== expected.charAt(i)) return false;
3120
+ }
3121
+ return true;
3122
+ }
3123
+ function peekIsNumber(arr, index) {
3124
+ if (index >= arr.length) return false;
3125
+ return isDigit(arr[index]) || arr[index] === "n";
3126
+ }
3127
+ function parseNumber(s, n) {
3128
+ let ret = "";
3129
+ while (s.length > 0 && (isDigit(s[0]) || s[0] === "n")) {
3130
+ const ch = s.shift();
3131
+ ret += ch === "n" ? n.toString() : ch;
3132
+ }
3133
+ if (ret.length === 0) {
3134
+ throw new Error(`Expected number, found: '${s[0]}'`);
3135
+ }
3136
+ return parseInt(ret, 10);
3137
+ }
3138
+ function isDigit(c) {
3139
+ return c >= "0" && c <= "9";
3140
+ }
3141
+ function parseKeep(s, n) {
3142
+ let keepLowest = false;
3143
+ if (peek(s, "l")) {
3144
+ assertToken(s, "l");
3145
+ keepLowest = true;
3146
+ } else if (peek(s, "h")) {
3147
+ assertToken(s, "h");
3148
+ keepLowest = false;
3149
+ } else {
3150
+ return;
3151
+ }
3152
+ const keepCount = parseNumber(s, n);
3153
+ const result = parseArgumentInternal(s, n);
3154
+ if (result instanceof Dice) {
3155
+ result.privateData.keep = keepN(keepCount, keepLowest);
3156
+ return result;
3157
+ }
3158
+ throw new Error("Expected Dice after keep modifier");
3159
+ }
3160
+ function keepN(n, low) {
3161
+ return (values) => {
3162
+ const sorted = [...values].sort((a, b) => low ? a - b : b - a);
3163
+ return sorted.slice(0, n).reduce((sum, val) => sum + val, 0);
3164
+ };
3165
+ }
3166
+ function parseOperation(s) {
3167
+ switch (s[0]) {
3168
+ case ")":
3169
+ return;
3170
+ case "a":
3171
+ assertToken(s, "ac");
3172
+ return Dice.prototype.ac;
3173
+ case "d":
3174
+ assertToken(s, "dc");
3175
+ return Dice.prototype.dc;
3176
+ case "!":
3177
+ assertToken(s, "!");
3178
+ const adv = Dice.prototype.advantage;
3179
+ adv.unary = true;
3180
+ return adv;
3181
+ case ">":
3182
+ assertToken(s, ">");
3183
+ return Dice.prototype.max;
3184
+ case "<":
3185
+ assertToken(s, "<");
3186
+ return Dice.prototype.min;
3187
+ case "+":
3188
+ assertToken(s, "+");
3189
+ return Dice.prototype.addNonZero;
3190
+ case "~":
3191
+ assertToken(s, "~");
3192
+ assertToken(s, "+");
3193
+ return Dice.prototype.add;
3194
+ case "-":
3195
+ assertToken(s, "-");
3196
+ return Dice.prototype.subtract;
3197
+ case "&":
3198
+ assertToken(s, "&");
3199
+ return Dice.prototype.combine;
3200
+ case "r":
3201
+ assertToken(s, "reroll");
3202
+ return Dice.prototype.reroll;
3203
+ case "*":
3204
+ assertToken(s, "*");
3205
+ if (peek(s, "*")) {
3206
+ assertToken(s, "*");
3207
+ return Dice.prototype.multiply;
3208
+ }
3209
+ return Dice.prototype.conditionalApply;
3210
+ case "/":
3211
+ assertToken(s, "/");
3212
+ if (s[0] === "/") {
3213
+ assertToken(s, "/");
3214
+ return Dice.prototype.divideRoundDown;
3215
+ }
3216
+ return Dice.prototype.divideRoundUp;
3217
+ case "=":
3218
+ assertToken(s, "=");
3219
+ return Dice.prototype.eq;
3220
+ }
3221
+ return;
3222
+ }
3223
+
2408
3224
  // src/builder/prob.ts
2409
3225
  function d20PmfFromCdf(cdfPow, eps = EPS) {
2410
3226
  const out = /* @__PURE__ */ new Map();
@@ -2546,6 +3362,9 @@ var RollBuilder = class _RollBuilder {
2546
3362
  if (isNaN(arg)) throw new Error("Invalid NaN value for argument");
2547
3363
  return new _RollBuilder(0).plus(arg);
2548
3364
  }
3365
+ if (typeof arg === "string") {
3366
+ return new ParsedRollBuilder(arg);
3367
+ }
2549
3368
  if (arg instanceof _RollBuilder) {
2550
3369
  return arg;
2551
3370
  }
@@ -3128,6 +3947,34 @@ var AlwaysCritBuilder = class _AlwaysCritBuilder extends RollBuilder {
3128
3947
  return new _AlwaysCritBuilder(baseCopy, newConfig, this.fromAlwaysHit);
3129
3948
  }
3130
3949
  };
3950
+ var ParsedRollBuilder = class _ParsedRollBuilder extends RollBuilder {
3951
+ constructor(expression) {
3952
+ super([]);
3953
+ __publicField(this, "cachedPMF");
3954
+ __publicField(this, "originalExpression");
3955
+ this.originalExpression = expression;
3956
+ this.cachedPMF = parse(expression, 0);
3957
+ }
3958
+ toPMF(eps = 0) {
3959
+ return this.cachedPMF;
3960
+ }
3961
+ toExpression() {
3962
+ return this.originalExpression;
3963
+ }
3964
+ toAST() {
3965
+ throw new Error(
3966
+ "ParsedRollBuilder does not support AST conversion. Use the builder API instead."
3967
+ );
3968
+ }
3969
+ copy() {
3970
+ return new _ParsedRollBuilder(this.originalExpression);
3971
+ }
3972
+ doubleDice() {
3973
+ throw new Error(
3974
+ "ParsedRollBuilder does not support doubleDice(). Use explicit onCrit() with the crit damage expression instead."
3975
+ );
3976
+ }
3977
+ };
3131
3978
 
3132
3979
  // src/builder/factory.ts
3133
3980
  var rollFn = (count, sidesOrDie, modifier) => {
@@ -3658,7 +4505,11 @@ var AttackBuilder = class _AttackBuilder {
3658
4505
  if (this.critEffect) {
3659
4506
  crit = this.critEffect;
3660
4507
  } else {
3661
- crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
4508
+ if (this.hitEffect instanceof ParsedRollBuilder) {
4509
+ crit = RollBuilder.fromArgs(0);
4510
+ } else {
4511
+ crit = this.hitEffect?.copy().doubleDice() ?? RollBuilder.fromArgs(0);
4512
+ }
3662
4513
  }
3663
4514
  const critThreshold = this.check.critThreshold;
3664
4515
  if (critThreshold < 1 || critThreshold > 20) {
@@ -3666,11 +4517,14 @@ var AttackBuilder = class _AttackBuilder {
3666
4517
  `Invalid crit threshold: ${critThreshold}. Must be between 1 and 20.`
3667
4518
  );
3668
4519
  }
3669
- if (critThreshold === 20) {
3670
- effectPart += ` crit (${crit.toExpression()})`;
3671
- } else {
3672
- const xcritNumber = 21 - critThreshold;
3673
- effectPart += ` xcrit${xcritNumber} (${crit.toExpression()})`;
4520
+ const critExpression = crit.toExpression();
4521
+ if (critExpression !== "0") {
4522
+ if (critThreshold === 20) {
4523
+ effectPart += ` crit (${critExpression})`;
4524
+ } else {
4525
+ const xcritNumber = 21 - critThreshold;
4526
+ effectPart += ` xcrit${xcritNumber} (${critExpression})`;
4527
+ }
3674
4528
  }
3675
4529
  }
3676
4530
  if (this.missEffect) {
@@ -3751,7 +4605,7 @@ var AttackBuilder = class _AttackBuilder {
3751
4605
  pCrit,
3752
4606
  pMiss: pmiss
3753
4607
  } = this.resolveProbabilities(this.check, eps);
3754
- const hitPMF = this.hitEffect ? pmfFromRollBuilder(this.hitEffect, eps) : PMF.delta(0, eps);
4608
+ const hitPMF = this.hitEffect ? this.hitEffect instanceof ParsedRollBuilder ? this.hitEffect.toPMF(eps) : pmfFromRollBuilder(this.hitEffect, eps) : PMF.delta(0, eps);
3755
4609
  let critPMF = null;
3756
4610
  let phit = pHit;
3757
4611
  let pcrit = pCrit;
@@ -3760,10 +4614,22 @@ var AttackBuilder = class _AttackBuilder {
3760
4614
  phit += pcrit;
3761
4615
  pcrit = 0;
3762
4616
  } else {
3763
- const critBuilder = this.critEffect ?? this.hitEffect?.copy().doubleDice();
3764
- critPMF = critBuilder ? pmfFromRollBuilder(critBuilder, eps) : PMF.delta(0, eps);
4617
+ let critBuilder;
4618
+ if (this.critEffect) {
4619
+ critBuilder = this.critEffect;
4620
+ } else if (this.hitEffect instanceof ParsedRollBuilder) {
4621
+ critPMF = null;
4622
+ phit += pcrit;
4623
+ pcrit = 0;
4624
+ critBuilder = void 0;
4625
+ } else {
4626
+ critBuilder = this.hitEffect?.copy().doubleDice();
4627
+ }
4628
+ if (critBuilder) {
4629
+ critPMF = critBuilder instanceof ParsedRollBuilder ? critBuilder.toPMF(eps) : pmfFromRollBuilder(critBuilder, eps);
4630
+ }
3765
4631
  }
3766
- const missPMF = this.missEffect ? pmfFromRollBuilder(this.missEffect, eps) : PMF.delta(0, eps);
4632
+ const missPMF = this.missEffect ? this.missEffect instanceof ParsedRollBuilder ? this.missEffect.toPMF(eps) : pmfFromRollBuilder(this.missEffect, eps) : PMF.delta(0, eps);
3767
4633
  const mix = new Mixture(eps);
3768
4634
  if (phit > 0) mix.add("hit", hitPMF, phit);
3769
4635
  if (critPMF && pcrit > 0) mix.add("crit", critPMF, pcrit);
@@ -3889,7 +4755,7 @@ var SaveBuilder = class _SaveBuilder {
3889
4755
  const { pSuccess: psuccess = 0, pFail: pfail = 1 } = resolveProbabilities(
3890
4756
  this.check
3891
4757
  );
3892
- const failPMF = this.failureEffect ? pmfFromRollBuilder(this.failureEffect) : PMF.delta(0);
4758
+ const failPMF = this.failureEffect ? this.failureEffect instanceof ParsedRollBuilder ? this.failureEffect.toPMF(eps) : pmfFromRollBuilder(this.failureEffect) : PMF.delta(0);
3893
4759
  const onSuccess = this.saveOutcome ?? "half";
3894
4760
  let successPMF = PMF.delta(0, eps);
3895
4761
  if (onSuccess === "half") successPMF = failPMF.scaleDamage(0.5, "floor");
@@ -4020,6 +4886,7 @@ exports.AlwaysHitBuilder = AlwaysHitBuilder;
4020
4886
  exports.DCBuilder = DCBuilder;
4021
4887
  exports.HalfRollBuilder = HalfRollBuilder;
4022
4888
  exports.MaxOfRollBuilder = MaxOfRollBuilder;
4889
+ exports.ParsedRollBuilder = ParsedRollBuilder;
4023
4890
  exports.RollBuilder = RollBuilder;
4024
4891
  exports.SaveBuilder = SaveBuilder;
4025
4892
  exports.builderPMFCache = builderPMFCache;