@dicelette/core 1.28.2 → 1.28.4

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 CHANGED
@@ -123,9 +123,6 @@ Note: when a parameter `engine` is shown it usually defaults to the `NumberGener
123
123
 
124
124
  ### Utility functions (`src/utils.ts`)
125
125
 
126
- #### Function: escapeRegex(string: string): string
127
- - Escape input string to be used in a RegExp.
128
-
129
126
  #### Function: standardizeDice(dice: string): string
130
127
  - Standardizes dice notation while preserving bracketed text.
131
128
 
package/dist/index.d.mts CHANGED
@@ -312,9 +312,10 @@ declare function includeDiceType(dice: string, diceType?: string, userStats?: bo
312
312
  * @param {Engine|null} engine The random engine to use, default to nodeCrypto
313
313
  * @param {boolean} pity Whether to enable pity system (reroll on failure) or not
314
314
  * @param {boolean} sort Whether to sort the dice results or not
315
+ * @param {string} comment Optional comment to attach to the result. If provided, skips extracting the comment from the dice string (assumes dice is already clean).
315
316
  * @returns {Resultat|undefined} The result of the roll
316
317
  */
317
- declare function roll(dice: string, engine?: Engine | null, pity?: boolean, sort?: SortOrder): Resultat | undefined;
318
+ declare function roll(dice: string, engine?: Engine | null, pity?: boolean, sort?: SortOrder, comment?: string): Resultat | undefined;
318
319
  declare function replaceInFormula(element: string, diceResult: Resultat, compareResult: {
319
320
  dice: string;
320
321
  compare: Compare | undefined;
@@ -354,6 +355,7 @@ declare function resolveFormulaHint(formula: string, allAttributes: Record<strin
354
355
  declare function calculateSimilarity(str1: string, str2: string): number;
355
356
  /**
356
357
  * Calculates the Levenshtein distance between two strings.
358
+ * Uses two rolling rows instead of a full matrix: O(min(m,n)) space instead of O(m×n).
357
359
  */
358
360
  declare function levenshteinDistance(str1: string, str2: string): number;
359
361
  declare function findBestStatMatch<T>(searchTerm: string, normalizedStats: Map<string, T>, similarityThreshold?: number): T | undefined;
@@ -368,10 +370,17 @@ declare function replaceUnknown(dice: string, replacer: string): string;
368
370
  declare function verifyStatMatcherPattern(dice: string, replaceUnknow?: string): string;
369
371
 
370
372
  /**
371
- * Escape regex string
372
- * @param string {string}
373
+ * Splits a dice string into the dice expression and its trailing comment.
374
+ * Comments are preceded by whitespace and start with #, //, [, or /*.
375
+ * The returned comment does NOT include the marker prefix.
376
+ * @example
377
+ * splitDiceComment("1d6 # attack") // => { dice: "1d6", comment: "attack" }
378
+ * splitDiceComment("2d8+3") // => { dice: "2d8+3", comment: undefined }
373
379
  */
374
- declare function escapeRegex(string: string): string;
380
+ declare function splitDiceComment(dice: string): {
381
+ dice: string;
382
+ comment: string | undefined;
383
+ };
375
384
  /**
376
385
  * Allow to keep the text as if in brackets
377
386
  * @param dice {string}
@@ -450,7 +459,7 @@ declare function evalOneCombinaison(combinaison: string, stats: Record<string, n
450
459
  /**
451
460
  * Parse the provided JSON and verify each field to check if everything could work when rolling
452
461
  * @param {unknown} template
453
- * @param {boolean} verify - If true, will roll the dices to check if everything is valid
462
+ * @param verify - If true, will roll the dices to check if everything is valid
454
463
  * @param engine
455
464
  * @returns {StatisticalTemplate}
456
465
  */
@@ -477,4 +486,4 @@ declare function testStatCombinaison(template: StatisticalTemplate, engine?: Eng
477
486
  */
478
487
  declare function generateRandomStat(total?: number | undefined, max?: number, min?: number, engine?: Engine | null): number;
479
488
 
480
- export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, escapeRegex, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
489
+ export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, splitDiceComment, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
package/dist/index.d.ts CHANGED
@@ -312,9 +312,10 @@ declare function includeDiceType(dice: string, diceType?: string, userStats?: bo
312
312
  * @param {Engine|null} engine The random engine to use, default to nodeCrypto
313
313
  * @param {boolean} pity Whether to enable pity system (reroll on failure) or not
314
314
  * @param {boolean} sort Whether to sort the dice results or not
315
+ * @param {string} comment Optional comment to attach to the result. If provided, skips extracting the comment from the dice string (assumes dice is already clean).
315
316
  * @returns {Resultat|undefined} The result of the roll
316
317
  */
317
- declare function roll(dice: string, engine?: Engine | null, pity?: boolean, sort?: SortOrder): Resultat | undefined;
318
+ declare function roll(dice: string, engine?: Engine | null, pity?: boolean, sort?: SortOrder, comment?: string): Resultat | undefined;
318
319
  declare function replaceInFormula(element: string, diceResult: Resultat, compareResult: {
319
320
  dice: string;
320
321
  compare: Compare | undefined;
@@ -354,6 +355,7 @@ declare function resolveFormulaHint(formula: string, allAttributes: Record<strin
354
355
  declare function calculateSimilarity(str1: string, str2: string): number;
355
356
  /**
356
357
  * Calculates the Levenshtein distance between two strings.
358
+ * Uses two rolling rows instead of a full matrix: O(min(m,n)) space instead of O(m×n).
357
359
  */
358
360
  declare function levenshteinDistance(str1: string, str2: string): number;
359
361
  declare function findBestStatMatch<T>(searchTerm: string, normalizedStats: Map<string, T>, similarityThreshold?: number): T | undefined;
@@ -368,10 +370,17 @@ declare function replaceUnknown(dice: string, replacer: string): string;
368
370
  declare function verifyStatMatcherPattern(dice: string, replaceUnknow?: string): string;
369
371
 
370
372
  /**
371
- * Escape regex string
372
- * @param string {string}
373
+ * Splits a dice string into the dice expression and its trailing comment.
374
+ * Comments are preceded by whitespace and start with #, //, [, or /*.
375
+ * The returned comment does NOT include the marker prefix.
376
+ * @example
377
+ * splitDiceComment("1d6 # attack") // => { dice: "1d6", comment: "attack" }
378
+ * splitDiceComment("2d8+3") // => { dice: "2d8+3", comment: undefined }
373
379
  */
374
- declare function escapeRegex(string: string): string;
380
+ declare function splitDiceComment(dice: string): {
381
+ dice: string;
382
+ comment: string | undefined;
383
+ };
375
384
  /**
376
385
  * Allow to keep the text as if in brackets
377
386
  * @param dice {string}
@@ -450,7 +459,7 @@ declare function evalOneCombinaison(combinaison: string, stats: Record<string, n
450
459
  /**
451
460
  * Parse the provided JSON and verify each field to check if everything could work when rolling
452
461
  * @param {unknown} template
453
- * @param {boolean} verify - If true, will roll the dices to check if everything is valid
462
+ * @param verify - If true, will roll the dices to check if everything is valid
454
463
  * @param engine
455
464
  * @returns {StatisticalTemplate}
456
465
  */
@@ -477,4 +486,4 @@ declare function testStatCombinaison(template: StatisticalTemplate, engine?: Eng
477
486
  */
478
487
  declare function generateRandomStat(total?: number | undefined, max?: number, min?: number, engine?: Engine | null): number;
479
488
 
480
- export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, escapeRegex, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
489
+ export { COMMENT_REGEX, type Compare, type ComparedValue, type Critical, type CustomCritical, type CustomCriticalMap, DETECT_CRITICAL, DiceTypeError, EmptyObjectError, FormulaError, type FormulaHintResult, MIN_THRESHOLD_MATCH, MaxGreater, type Modifier, NORMALIZE_SINGLE_DICE, NoStatisticsError, OPTIONAL_COMMENT, REMOVER_PATTERN, type Resultat, SIGN_REGEX, SIGN_REGEX_SPACE, SYMBOL_DICE, type Sign, SortOrder, type Statistic, type StatisticalSchema, type StatisticalTemplate, TooManyDice, TooManyStats, calculateSimilarity, createCriticalCustom, diceRandomParse, diceTypeRandomParse, evalCombinaison, evalOneCombinaison, evalStatsDice, findBestRecord, findBestStatMatch, generateRandomStat, generateStatsDice, getCachedRegex, getEngine, getEngineId, includeDiceType, isNumber, levenshteinDistance, randomInt, replaceExpByRandom, replaceFormulaInDice, replaceInFormula, replaceUnknown, resolveFormulaHint, roll, splitDiceComment, standardizeDice, templateSchema, testDiceRegistered, testStatCombinaison, verifyStatMatcherPattern, verifyTemplateValue };
package/dist/index.js CHANGED
@@ -41,7 +41,6 @@ __export(index_exports, {
41
41
  createCriticalCustom: () => createCriticalCustom,
42
42
  diceRandomParse: () => diceRandomParse,
43
43
  diceTypeRandomParse: () => diceTypeRandomParse,
44
- escapeRegex: () => escapeRegex,
45
44
  evalCombinaison: () => evalCombinaison,
46
45
  evalOneCombinaison: () => evalOneCombinaison,
47
46
  evalStatsDice: () => evalStatsDice,
@@ -62,6 +61,7 @@ __export(index_exports, {
62
61
  replaceUnknown: () => replaceUnknown,
63
62
  resolveFormulaHint: () => resolveFormulaHint,
64
63
  roll: () => roll,
64
+ splitDiceComment: () => splitDiceComment,
65
65
  standardizeDice: () => standardizeDice,
66
66
  templateSchema: () => templateSchema,
67
67
  testDiceRegistered: () => testDiceRegistered,
@@ -239,6 +239,7 @@ var templateSchema = import_zod.z.object({
239
239
  });
240
240
 
241
241
  // src/regex.ts
242
+ var REGEX_CACHE_MAX = 500;
242
243
  var regexCache = /* @__PURE__ */ new Map();
243
244
  var NORMALIZE_SINGLE_DICE = (str) => str.replace(/\b1d(\d+)/gi, "d$1");
244
245
  var REMOVER_PATTERN = {
@@ -253,6 +254,10 @@ function getCachedRegex(pattern, flags = "") {
253
254
  const key = `${pattern}|${flags}`;
254
255
  let regex = regexCache.get(key);
255
256
  if (!regex) {
257
+ if (regexCache.size >= REGEX_CACHE_MAX) {
258
+ const oldest = regexCache.keys().next().value;
259
+ if (oldest !== void 0) regexCache.delete(oldest);
260
+ }
256
261
  regex = new RegExp(pattern, flags);
257
262
  regexCache.set(key, regex);
258
263
  }
@@ -285,10 +290,6 @@ var import_mathjs10 = require("mathjs");
285
290
  var import_rpg_dice_roller7 = require("@dice-roller/rpg-dice-roller");
286
291
  var import_mathjs8 = require("mathjs");
287
292
 
288
- // src/dice/compare.ts
289
- var import_rpg_dice_roller4 = require("@dice-roller/rpg-dice-roller");
290
- var import_mathjs2 = require("mathjs");
291
-
292
293
  // src/utils.ts
293
294
  var import_uniformize2 = require("uniformize");
294
295
  var import_rpg_dice_roller3 = require("@dice-roller/rpg-dice-roller");
@@ -302,12 +303,12 @@ var import_rpg_dice_roller2 = require("@dice-roller/rpg-dice-roller");
302
303
  function evalStatsDice(testDice, allStats, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto, pity) {
303
304
  let dice = testDice.trimEnd();
304
305
  if (allStats && Object.keys(allStats).length > 0) {
306
+ dice = dice.standardize();
305
307
  const names = Object.keys(allStats);
306
308
  for (const name of names) {
307
- const regex = new RegExp(escapeRegex(name.standardize()), "gi");
308
- if (dice.standardize().match(regex)) {
309
- const statValue = allStats[name];
310
- dice = dice.standardize().replace(regex, statValue.toString()).trimEnd();
309
+ const regex = getCachedRegex(name.standardize().escapeRegex(), "gi");
310
+ if (dice.match(regex)) {
311
+ dice = dice.replace(regex, allStats[name].toString()).trimEnd();
311
312
  }
312
313
  }
313
314
  }
@@ -325,8 +326,8 @@ function diceRandomParse(value, template, engine = import_rpg_dice_roller2.Numbe
325
326
  const statNames = Object.keys(template.statistics);
326
327
  let newDice = value;
327
328
  for (const name of statNames) {
328
- const regex = new RegExp(escapeRegex(name.standardize()), "gi");
329
- if (value.match(regex)) {
329
+ const regex = getCachedRegex(name.standardize().escapeRegex(), "gi");
330
+ if (newDice.match(regex)) {
330
331
  let max;
331
332
  let min;
332
333
  const foundStat = template.statistics?.[name];
@@ -336,7 +337,7 @@ function diceRandomParse(value, template, engine = import_rpg_dice_roller2.Numbe
336
337
  }
337
338
  const total = template.total || 100;
338
339
  const randomStatValue = generateRandomStat(total, max, min, engine);
339
- newDice = value.replace(regex, randomStatValue.toString());
340
+ newDice = newDice.replace(regex, randomStatValue.toString());
340
341
  }
341
342
  }
342
343
  return replaceFormulaInDice(newDice);
@@ -359,7 +360,7 @@ function evalCombinaison(combinaison, stats) {
359
360
  for (const [stat, combin] of Object.entries(combinaison)) {
360
361
  let formula = combin.standardize();
361
362
  for (const [statName, value] of Object.entries(stats)) {
362
- const regex = new RegExp(statName.standardize(), "gi");
363
+ const regex = getCachedRegex(statName.standardize().escapeRegex(), "gi");
363
364
  formula = formula.replace(regex, value.toString());
364
365
  }
365
366
  try {
@@ -373,7 +374,7 @@ function evalCombinaison(combinaison, stats) {
373
374
  function evalOneCombinaison(combinaison, stats) {
374
375
  let formula = combinaison.standardize();
375
376
  for (const [statName, value] of Object.entries(stats)) {
376
- const regex = new RegExp(statName.standardize(), "gi");
377
+ const regex = getCachedRegex(statName.standardize().escapeRegex(), "gi");
377
378
  formula = formula.replace(regex, value.toString());
378
379
  }
379
380
  try {
@@ -421,7 +422,7 @@ function verifyTemplateValue(template, verify = true, engine = import_rpg_dice_r
421
422
  engine
422
423
  );
423
424
  const rolled = roll(cleanedDice2, engine);
424
- if (!rolled) throw new DiceTypeError(cleanedDice2, "no_roll_result", "no roll result");
425
+ if (!rolled) throw new DiceTypeError(cleanedDice2, "roll");
425
426
  }
426
427
  if (statistiqueTemplate.customCritical) {
427
428
  if (!statistiqueTemplate.diceType) {
@@ -446,15 +447,16 @@ function verifyTemplateValue(template, verify = true, engine = import_rpg_dice_r
446
447
  }
447
448
  function testDiceRegistered(template, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto) {
448
449
  if (!template.damage) return;
449
- if (Object.keys(template.damage).length === 0) throw new EmptyObjectError();
450
- if (Object.keys(template.damage).length > 25) throw new TooManyDice();
451
- for (const [name, dice] of Object.entries(template.damage)) {
450
+ const damageEntries = Object.entries(template.damage);
451
+ if (damageEntries.length === 0) throw new EmptyObjectError();
452
+ if (damageEntries.length > 25) throw new TooManyDice();
453
+ for (const [name, dice] of damageEntries) {
452
454
  if (!dice) continue;
453
455
  const diceReplaced = replaceExpByRandom(dice);
454
456
  const randomDiceParsed = diceRandomParse(diceReplaced, template, engine);
455
457
  try {
456
458
  const rolled = roll(randomDiceParsed, engine);
457
- if (!rolled) throw new DiceTypeError(name, "no_roll_result", dice);
459
+ if (!rolled) throw new DiceTypeError(name, "testDiceRegistered", dice);
458
460
  } catch (error) {
459
461
  throw new DiceTypeError(name, "testDiceRegistered", error);
460
462
  }
@@ -462,19 +464,14 @@ function testDiceRegistered(template, engine = import_rpg_dice_roller2.NumberGen
462
464
  }
463
465
  function testStatCombinaison(template, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto) {
464
466
  if (!template.statistics) return;
465
- const onlycombinaisonStats = Object.fromEntries(
466
- Object.entries(template.statistics).filter(
467
- ([_, value]) => value.combinaison !== void 0
468
- )
469
- );
470
- const allOtherStats = Object.fromEntries(
471
- Object.entries(template.statistics).filter(([_, value]) => !value.combinaison)
472
- );
467
+ const onlycombinaisonStats = {};
468
+ const allOtherStats = {};
469
+ for (const [k, v] of Object.entries(template.statistics)) {
470
+ if (v.combinaison !== void 0) onlycombinaisonStats[k] = v;
471
+ else allOtherStats[k] = v;
472
+ }
473
473
  if (Object.keys(onlycombinaisonStats).length === 0) return;
474
- const allStats = Object.keys(template.statistics).filter(
475
- (stat) => !template.statistics[stat].combinaison
476
- );
477
- if (allStats.length === 0) throw new NoStatisticsError();
474
+ if (Object.keys(allOtherStats).length === 0) throw new NoStatisticsError();
478
475
  const error = [];
479
476
  for (const [stat, value] of Object.entries(onlycombinaisonStats)) {
480
477
  let formula = value.combinaison;
@@ -482,7 +479,7 @@ function testStatCombinaison(template, engine = import_rpg_dice_roller2.NumberGe
482
479
  const { max, min } = data;
483
480
  const total = template.total || 100;
484
481
  const randomStatValue = generateRandomStat(total, max, min, engine);
485
- const regex = new RegExp(other, "gi");
482
+ const regex = getCachedRegex(other.escapeRegex(), "gi");
486
483
  formula = formula.replace(regex, randomStatValue.toString());
487
484
  }
488
485
  try {
@@ -495,20 +492,20 @@ function testStatCombinaison(template, engine = import_rpg_dice_roller2.NumberGe
495
492
  return;
496
493
  }
497
494
  function generateRandomStat(total = 100, max, min, engine = import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto) {
498
- let randomStatValue = total + 1;
495
+ if (total <= 1) throw new RangeError(`total must be greater than 1, got ${total}`);
499
496
  const random = new import_random_js.Random(engine || import_rpg_dice_roller2.NumberGenerator.engines.nodeCrypto);
500
- while (randomStatValue >= total || randomStatValue === 0) {
501
- if (max && min) randomStatValue = randomInt(min, max, engine, random);
502
- else if (max) randomStatValue = randomInt(1, max, engine, random);
503
- else if (min) randomStatValue = randomInt(min, total, engine, random);
504
- else randomStatValue = randomInt(1, total, engine, random);
505
- }
506
- return randomStatValue;
497
+ const effectiveMin = Math.max(min ?? 1, 1);
498
+ const effectiveMax = Math.min(max ?? total - 1, total - 1);
499
+ if (effectiveMin > effectiveMax) throw new MaxGreater(effectiveMin, effectiveMax);
500
+ return randomInt(effectiveMin, effectiveMax, engine, random);
507
501
  }
508
502
 
509
503
  // src/utils.ts
510
- function escapeRegex(string) {
511
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
504
+ function splitDiceComment(dice) {
505
+ const match = /\s(#|\/{2}|\[|\/\*)(?<comment>.*)/i.exec(dice);
506
+ if (!match?.groups) return { dice: dice.trimEnd(), comment: void 0 };
507
+ const comment = match.groups.comment.trim() || void 0;
508
+ return { dice: dice.slice(0, match.index).trimEnd(), comment };
512
509
  }
513
510
  function standardizeDice(dice) {
514
511
  return dice.replace(
@@ -543,6 +540,8 @@ function createCriticalCustom(dice, customCritical, template, engine = import_rp
543
540
  }
544
541
 
545
542
  // src/dice/compare.ts
543
+ var import_rpg_dice_roller4 = require("@dice-roller/rpg-dice-roller");
544
+ var import_mathjs2 = require("mathjs");
546
545
  function isTrivialComparison(maxValue, minValue, compare) {
547
546
  const canSucceed = canComparisonSucceed(maxValue, compare, minValue);
548
547
  const canFail = canComparisonFail(maxValue, compare, minValue);
@@ -652,32 +651,31 @@ var import_rpg_dice_roller5 = require("@dice-roller/rpg-dice-roller");
652
651
  var import_mathjs3 = require("mathjs");
653
652
 
654
653
  // src/dice/replace.ts
654
+ var PARENTHESIS_REGEX = /d\((\d+)\)/g;
655
+ var BRACKET_COMMENT_REGEX = /\[(?<comments>.*?)\]/;
656
+ var OPTIONAL_COMMENT_REGEX = /\s+(#|\/\/)(?<comment>.*)/;
655
657
  function replaceUnwantedText(dice, sortOrder) {
656
- const d = dice.replaceAll(/[{}]/g, "").replaceAll(/s[ad]/gi, "");
657
- if (sortOrder) return sortDice(d, sortOrder);
658
+ let d = dice.replaceAll(/[{}]/g, "").replaceAll(/s[ad]/gi, "");
659
+ if (sortOrder) d = sortDice(d, sortOrder);
660
+ if (!d.length) throw new DiceTypeError(dice, "empty_dice");
658
661
  return d;
659
662
  }
660
663
  function sortDice(dice, sortOrder) {
661
664
  if (sortOrder === "none" /* None */) return dice;
662
665
  const dices = dice.split(/; ?/);
666
+ const decorated = dices.map((d) => ({
667
+ d,
668
+ v: Number.parseInt(d.split("= ")[1], 10) || 0
669
+ }));
663
670
  if (sortOrder === "sa" /* Ascending */) {
664
- dices.sort((a, b) => {
665
- const totalA = Number.parseInt(a.split("= ")[1], 10) || 0;
666
- const totalB = Number.parseInt(b.split("= ")[1], 10) || 0;
667
- return totalB - totalA;
668
- });
671
+ decorated.sort((a, b) => b.v - a.v);
669
672
  } else if (sortOrder === "sd" /* Descending */) {
670
- dices.sort((a, b) => {
671
- const totalA = Number.parseInt(a.split("= ")[1], 10) || 0;
672
- const totalB = Number.parseInt(b.split("= ")[1], 10) || 0;
673
- return totalA - totalB;
674
- });
673
+ decorated.sort((a, b) => a.v - b.v);
675
674
  }
676
- return dices.join("; ");
675
+ return decorated.map((x) => x.d).join("; ");
677
676
  }
678
677
  function fixParenthesis(dice) {
679
- const parenthesisRegex = /d\((\d+)\)/g;
680
- return dice.replaceAll(parenthesisRegex, (_match, p1) => `d${p1}`);
678
+ return dice.replaceAll(PARENTHESIS_REGEX, (_match, p1) => `d${p1}`);
681
679
  }
682
680
  function replaceText(element, total, dice) {
683
681
  return {
@@ -686,12 +684,10 @@ function replaceText(element, total, dice) {
686
684
  };
687
685
  }
688
686
  function formatComment(dice) {
689
- const commentsRegex = /\[(?<comments>.*?)\]/;
690
- const commentsMatch = commentsRegex.exec(dice);
687
+ const commentsMatch = BRACKET_COMMENT_REGEX.exec(dice);
691
688
  const comments = commentsMatch?.groups?.comments ? `${commentsMatch.groups.comments}` : "";
692
- const diceWithoutBrackets = dice.replace(commentsRegex, "");
693
- const optionalCommentsRegex = /\s+(#|\/\/)(?<comment>.*)/;
694
- const optionalComments = optionalCommentsRegex.exec(diceWithoutBrackets);
689
+ const diceWithoutBrackets = dice.replace(BRACKET_COMMENT_REGEX, "");
690
+ const optionalComments = OPTIONAL_COMMENT_REGEX.exec(diceWithoutBrackets);
695
691
  const optional = optionalComments?.groups?.comment ? `${optionalComments.groups.comment.trim()}` : "";
696
692
  let finalComment = "";
697
693
  if (comments && optional) finalComment = `__${comments} ${optional}__ \u2014 `;
@@ -843,23 +839,27 @@ function calculateSimilarity(str1, str2) {
843
839
  return (longer.length - distance) / longer.length;
844
840
  }
845
841
  function levenshteinDistance(str1, str2) {
846
- const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
847
- for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
848
- for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
842
+ if (str1.length > str2.length) {
843
+ [str1, str2] = [str2, str1];
844
+ }
845
+ let prev = Array.from({ length: str1.length + 1 }, (_, i) => i);
846
+ let curr = new Array(str1.length + 1);
849
847
  for (let j = 1; j <= str2.length; j++) {
848
+ curr[0] = j;
850
849
  for (let i = 1; i <= str1.length; i++) {
851
850
  const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
852
- matrix[j][i] = Math.min(
853
- matrix[j][i - 1] + 1,
851
+ curr[i] = Math.min(
852
+ curr[i - 1] + 1,
854
853
  // insertion
855
- matrix[j - 1][i] + 1,
854
+ prev[i] + 1,
856
855
  // deletion
857
- matrix[j - 1][i - 1] + cost
856
+ prev[i - 1] + cost
858
857
  // substitution
859
858
  );
860
859
  }
860
+ [prev, curr] = [curr, prev];
861
861
  }
862
- return matrix[str2.length][str1.length];
862
+ return prev[str1.length];
863
863
  }
864
864
  function findBestStatMatch(searchTerm, normalizedStats, similarityThreshold = MIN_THRESHOLD_MATCH) {
865
865
  const exact = normalizedStats.get(searchTerm);
@@ -1133,28 +1133,23 @@ function setSortOrder(toRoll, sort) {
1133
1133
  }
1134
1134
  function prepareDice(diceInput) {
1135
1135
  let dice = standardizeDice(replaceFormulaInDice(diceInput)).replace(/^\+/, "").replaceAll("=>", ">=").replaceAll("=<", "<=").trimStart();
1136
- dice = dice.replaceAll(DETECT_CRITICAL, "").trimEnd();
1136
+ dice = dice.replaceAll(REMOVER_PATTERN.CRITICAL_BLOCK, "").trimEnd();
1137
1137
  const explodingSuccess = normalizeExplodingSuccess(dice);
1138
1138
  if (explodingSuccess) dice = explodingSuccess.dice;
1139
- let diceDisplay;
1140
- if (dice.includes(";")) {
1141
- const mainDice = dice.split(";")[0];
1142
- diceDisplay = explodingSuccess?.originalDice ?? mainDice;
1143
- } else {
1144
- diceDisplay = explodingSuccess?.originalDice ?? dice;
1145
- }
1139
+ const sharedSeparatorIndex = dice.indexOf(";");
1140
+ const hasSharedSeparator = sharedSeparatorIndex !== -1;
1141
+ let diceDisplay = explodingSuccess?.originalDice ?? (hasSharedSeparator ? dice.slice(0, sharedSeparatorIndex) : dice);
1146
1142
  const curlyBulkMatch = dice.match(/^\{(\d+#.*)\}$/);
1147
1143
  const isCurlyBulk = !!curlyBulkMatch;
1148
1144
  const bulkContent = isCurlyBulk ? curlyBulkMatch[1] : "";
1149
- const isSharedRoll = dice.includes(";");
1150
1145
  let isSharedCurly = false;
1151
- if (isSharedRoll && dice.match(/^\{.*;\s*.*\}$/)) {
1146
+ if (hasSharedSeparator && dice.match(/^\{.*;\s*.*\}$/)) {
1152
1147
  dice = dice.slice(1, -1);
1153
1148
  isSharedCurly = true;
1154
1149
  diceDisplay = diceDisplay.slice(1);
1155
1150
  }
1156
1151
  let isSimpleCurly = false;
1157
- if (!isCurlyBulk && !isSharedRoll && dice.match(/^\{.*\}$/)) {
1152
+ if (!isCurlyBulk && !hasSharedSeparator && dice.match(/^\{.*\}$/)) {
1158
1153
  const innerContent = dice.slice(1, -1);
1159
1154
  const hasModifiers = innerContent.match(/[+\-*/%^]/);
1160
1155
  const hasComparison = innerContent.match(/(([><=!]+\d+f)|([><=]|!=)+\d+)/);
@@ -1167,7 +1162,7 @@ function prepareDice(diceInput) {
1167
1162
  dice,
1168
1163
  diceDisplay,
1169
1164
  explodingSuccess,
1170
- isSharedRoll,
1165
+ isSharedRoll: hasSharedSeparator,
1171
1166
  isSharedCurly,
1172
1167
  isCurlyBulk,
1173
1168
  bulkContent,
@@ -1184,10 +1179,15 @@ function getSortOrder(dice) {
1184
1179
  function handleBulkRolls(dice, isCurlyBulk, bulkContent, compare, explodingSuccess, diceDisplay, engine, sort) {
1185
1180
  const bulkProcessContent = isCurlyBulk ? bulkContent : dice;
1186
1181
  const diceArray = bulkProcessContent.split("#");
1182
+ if (!isNumber(diceArray[0])) {
1183
+ throw new DiceTypeError(dice, "bulk_number");
1184
+ }
1187
1185
  const numberOfDice = Number.parseInt(diceArray[0], 10);
1188
- let diceToRoll = diceArray[1].replace(COMMENT_REGEX, "");
1189
- const commentsMatch = diceArray[1].match(COMMENT_REGEX);
1190
- const comments = commentsMatch ? commentsMatch[2] : void 0;
1186
+ if (numberOfDice <= 0) {
1187
+ throw new DiceTypeError(dice, "bulk_zero");
1188
+ }
1189
+ const { dice: diceToRollBase, comment: comments } = splitDiceComment(diceArray[1]);
1190
+ let diceToRoll = diceToRollBase;
1191
1191
  let curlyCompare;
1192
1192
  if (isCurlyBulk) {
1193
1193
  const curlyCompareRegex = diceToRoll.match(SIGN_REGEX_SPACE);
@@ -1368,11 +1368,12 @@ function handlePitySystem(dice, compare, diceRoll, roller, engine) {
1368
1368
  isFail = (0, import_mathjs9.evaluate)(`${res.total}${compare.sign}${compare.value}`);
1369
1369
  }
1370
1370
  }
1371
+ if (!res?.result.length) throw new DiceTypeError(dice, "empty_dice");
1371
1372
  return { rerollCount, result: res };
1372
1373
  }
1373
1374
 
1374
1375
  // src/roll.ts
1375
- function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nodeCrypto, pity, sort) {
1376
+ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nodeCrypto, pity, sort, comment) {
1376
1377
  if (sort === "none" /* None */) sort = void 0;
1377
1378
  const prepared = prepareDice(dice);
1378
1379
  if (!prepared.dice.includes("d")) return void 0;
@@ -1415,8 +1416,10 @@ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nod
1415
1416
  }
1416
1417
  const roller = new import_rpg_dice_roller8.DiceRoller();
1417
1418
  import_rpg_dice_roller8.NumberGenerator.generator.engine = engine;
1418
- let diceWithoutComment = processedDice.replace(COMMENT_REGEX, "").trimEnd();
1419
- diceWithoutComment = setSortOrder(diceWithoutComment, sort);
1419
+ const splitResult = splitDiceComment(processedDice);
1420
+ const diceBase = comment !== void 0 ? processedDice.trimEnd() : splitResult.dice;
1421
+ const resolvedComment = comment ?? splitResult.comment;
1422
+ const diceWithoutComment = setSortOrder(diceBase, sort);
1420
1423
  let diceRoll;
1421
1424
  try {
1422
1425
  diceRoll = roller.roll(diceWithoutComment);
@@ -1432,8 +1435,6 @@ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nod
1432
1435
  );
1433
1436
  compare.trivial = trivial ? true : void 0;
1434
1437
  }
1435
- const commentMatch = processedDice.match(COMMENT_REGEX);
1436
- const comment = commentMatch ? commentMatch[2] : void 0;
1437
1438
  let rerollCount = 0;
1438
1439
  let pityResult;
1439
1440
  if (pity && compare) {
@@ -1450,7 +1451,7 @@ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nod
1450
1451
  return {
1451
1452
  ...pityResult,
1452
1453
  dice: prepared.isSimpleCurly ? finalDiceDisplay : processedDice,
1453
- comment,
1454
+ comment: resolvedComment,
1454
1455
  compare,
1455
1456
  modifier: modificator,
1456
1457
  pityLogs: rerollCount,
@@ -1469,10 +1470,11 @@ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nod
1469
1470
  prepared.explodingSuccess.normalizedSegment,
1470
1471
  prepared.explodingSuccess.originalSegment
1471
1472
  );
1473
+ if (!resultOutput.length) throw new DiceTypeError(dice, "empty_dice");
1472
1474
  return {
1473
1475
  dice: prepared.isSimpleCurly ? finalDiceDisplay : prepared.diceDisplay,
1474
1476
  result: resultOutput,
1475
- comment,
1477
+ comment: resolvedComment,
1476
1478
  compare: compare ? compare : void 0,
1477
1479
  modifier: modificator,
1478
1480
  total: successes,
@@ -1483,7 +1485,7 @@ function roll(dice, engine = import_rpg_dice_roller8.NumberGenerator.engines.nod
1483
1485
  return {
1484
1486
  dice: prepared.isSimpleCurly ? finalDiceDisplay : processedDice,
1485
1487
  result: resultOutput,
1486
- comment,
1488
+ comment: resolvedComment,
1487
1489
  compare: compare ? compare : void 0,
1488
1490
  modifier: modificator,
1489
1491
  total: roller.total,
@@ -1679,7 +1681,6 @@ function replaceInFormula(element, diceResult, compareResult, res, engine = impo
1679
1681
  createCriticalCustom,
1680
1682
  diceRandomParse,
1681
1683
  diceTypeRandomParse,
1682
- escapeRegex,
1683
1684
  evalCombinaison,
1684
1685
  evalOneCombinaison,
1685
1686
  evalStatsDice,
@@ -1700,6 +1701,7 @@ function replaceInFormula(element, diceResult, compareResult, res, engine = impo
1700
1701
  replaceUnknown,
1701
1702
  resolveFormulaHint,
1702
1703
  roll,
1704
+ splitDiceComment,
1703
1705
  standardizeDice,
1704
1706
  templateSchema,
1705
1707
  testDiceRegistered,