tiny-essentials 1.24.3 → 1.24.5
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 +1 -0
- package/changelog/1/24/4.md +71 -0
- package/changelog/1/24/5.md +18 -0
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinySimpleDice.min.js +1 -1
- package/dist/v1/libs/TinyHtml/index.cjs +33 -0
- package/dist/v1/libs/TinyHtml/index.d.mts +14 -0
- package/dist/v1/libs/TinyHtml/index.mjs +30 -0
- package/dist/v1/libs/TinySimpleDice.cjs +428 -0
- package/dist/v1/libs/TinySimpleDice.d.mts +207 -0
- package/dist/v1/libs/TinySimpleDice.mjs +369 -0
- package/docs/v1/Exports.md +71 -1
- package/docs/v1/libs/TinySimpleDice.md +287 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{"use strict";var e={d:(r,
|
|
1
|
+
(()=>{"use strict";var e={d:(r,t)=>{for(var o in t)e.o(t,o)&&!e.o(r,o)&&Object.defineProperty(r,o,{enumerable:!0,get:t[o]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r)},r={};function t(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)&&"[object Object]"===Object.prototype.toString.call(e)}e.d(r,{TinySimpleDice:()=>n});class o{static _safeEvaluate(e){if(!/^[\d+\-*/%.()\s^]+$/.test(e))throw new Error(`Invalid characters in expression: "${e}"`);const r=e.replace(/\^/g,"**");try{return Function(`"use strict"; return (${r})`)()}catch(r){if(!(r instanceof Error))throw new Error("Unknown Error");throw new Error(`Invalid expression "${e}": ${r.message}`)}}static replaceValues(e,r){let t=0;return e.replace(/(\d*)d(\d+)/g,(e,o)=>{const n=o?Number(o):1;let s=0;for(let e=0;e<n;e++){const e=r[t++];s+=void 0!==e?e:0}return String(s)})}static tokenizeValues(e,r){const t=o.replaceValues(e,r),n=[],s=/\d+|[()+\-*/]/g;let a;for(;null!==(a=s.exec(t));)/^\d+$/.test(a[0])?n.push(Number(a[0])):n.push(a[0]);return{tokens:n,text:t}}static parseString(e){if("string"!=typeof e)throw new TypeError("Input must be a string.");const r=e.split(",").map(e=>e.trim()).filter(Boolean),t=[],o=[];if(r.forEach((e,r)=>{const n=/\b(?:(\d+)?d(\d+))\b/g;let s;const a=[],i=e.replace(/\(([^()]+?\|[^()]+?)\)/g,(e,r)=>{const t="string"==typeof r?r.split("|").map(e=>e.trim()).filter(Boolean):[];if(0===t.length)throw new Error(`Invalid random-choice group: "${e}"`);return t[Math.floor(Math.random()*t.length)]});for(;null!==(s=n.exec(i));){const e=parseInt(s[1]||"1",10),t=parseInt(s[2],10);if(isNaN(t))throw new Error(`Invalid dice sides in expression "${s[0]}" at position ${r+1}.`);a.push({count:e,sides:t})}if(0===a.length&&Number.isNaN(parseFloat(i)))throw new Error(`Invalid dice expression at position ${r+1}: "${e}"`);t.push(...a),o.push({index:r,original:e,expression:i})}),0===t.length)throw new Error("Invalid dice amount.");return{sides:t,modifiers:o}}static applyModifiers(e,r){if(!Array.isArray(e)||!e.every(e=>"number"==typeof e&&!Number.isNaN(e)||t(e)&&"number"==typeof e.value&&!Number.isNaN(e.value)&&"number"==typeof e.sides&&!Number.isNaN(e.sides)))throw new TypeError("Bases must be a valid numbers.");if(!Array.isArray(r))throw new TypeError("Modifiers must be an array of modifier objects.");let n=0;const s=[],a=[...e];for(const e in r){const i=r[e];if("string"!=typeof i.expression)throw new Error("Each modifier must include an expression string.");const l=i.original,u=i.expression,c=[],p=[],f=[],h=e=>e.match(/\(\s*[^()]+\|\s*[^()]+\s*\)|\b\d*d\d+\b|[-+]?\d+(?:\.\d+)?|[+\-*/%^()]/g)||[],d=h(u),m=h(l),w=[...d],b=[],y=u.replace(/\b\d*d\d+\b/g,e=>{const r=e.split("d"),o=()=>{for(const r in d)if(d[r]===e&&b.indexOf(r)<0){b.push(r),f.push(Number(r));break}},n=e=>{if(e.value>e.sides)throw new Error(`Invalid dice roll: value (${e.value}) must be between 1 and ${e.sides}.`)};if(0===r[0].trim().length){const r=a.shift(),s="number"==typeof r?r:t(r)?r.value:0;t(r)&&n(r),c.push([s]);for(const r in w)if(w[r]===e){w[r]=String(s),p.push(Number(r));break}return o(),s}const s=[],i=Number(r[0]),l=["("];let u="(";for(let e=0;e<i;e++){const r=a.shift(),o="number"==typeof r?r:t(r)?r.value:0;t(r)&&n(r),l.push(String(o));const c=e<i-1?" + ":")";l.push(c),u+=`${o}${c}`,s.push(o)}for(const r in w){const t=Number(r);if(w[t]===e){w.splice(t,1,...l);let e=1;for(let r=0;r<i;r++)p.push(t+r+e),e++;break}}return c.push(s),o(),u});let g;try{g=o._safeEvaluate(y)}catch(e){if(!(e instanceof Error))throw new Error("Unknown Error");throw new Error(`Error evaluating expression "${y}" (from "${u}"): ${e.message}`)}s.push({rawTokensP:d,rawTokens:m,tokens:w,rawDiceTokenSlots:f,diceTokenSlots:p,dicesResult:c,total:g}),n+=g}return{final:n,steps:s}}static rollArrayIndex(e){const r=Array.isArray(e),t=e instanceof Set;if(!r&&!t)throw new TypeError("rollArrayIndex expects an array or Set.");return Math.floor(Math.random()*(r?e.length:e.size))}#e;#r;get maxValue(){return this.#e}set maxValue(e){if(!Number.isInteger(e)||e<0)throw new TypeError("maxValue must be an integer greater than -1.");this.#e=e}get allowZero(){return this.#r}set allowZero(e){if("boolean"!=typeof e)throw new TypeError("allowZero must be a boolean.");this.#r=e}constructor({maxValue:e,allowZero:r=!0}){if("boolean"!=typeof r)throw new TypeError("allowZero must be an boolean.");if(!Number.isInteger(e)||e<0)throw new TypeError("maxValue must be an integer greater than -1.");this.#e=e,this.#r=r}roll(){const e=this.#r?0:1;return Math.floor(Math.random()*(this.#e-e+1))+e}}const n=o;window.TinySimpleDice=r.TinySimpleDice})();
|
|
@@ -5,6 +5,13 @@ var TinyHtmlResetInput = require('./input/button/TinyHtmlResetInput.cjs');
|
|
|
5
5
|
var TinyHtmlSubmitInput = require('./input/button/TinyHtmlSubmitInput.cjs');
|
|
6
6
|
var TinyHtmlNumberInput = require('./input/number/TinyHtmlNumberInput.cjs');
|
|
7
7
|
var TinyHtmlRangeInput = require('./input/number/TinyHtmlRangeInput.cjs');
|
|
8
|
+
var TinyHtmlCheckboxInput = require('./input/check/TinyHtmlCheckboxInput.cjs');
|
|
9
|
+
var TinyHtmlRadioInput = require('./input/check/TinyHtmlRadioInput.cjs');
|
|
10
|
+
var TinyHtmlDateInput = require('./input/date/TinyHtmlDateInput.cjs');
|
|
11
|
+
var TinyHtmlDateTimeInput = require('./input/date/TinyHtmlDateTimeInput.cjs');
|
|
12
|
+
var TinyHtmlMonthInput = require('./input/date/TinyHtmlMonthInput.cjs');
|
|
13
|
+
var TinyHtmlTimeInput = require('./input/date/TinyHtmlTimeInput.cjs');
|
|
14
|
+
var TinyHtmlWeekInput = require('./input/date/TinyHtmlWeekInput.cjs');
|
|
8
15
|
var TinyHtmlEmailInput = require('./input/text/TinyHtmlEmailInput.cjs');
|
|
9
16
|
var TinyHtmlPasswordInput = require('./input/text/TinyHtmlPasswordInput.cjs');
|
|
10
17
|
var TinyHtmlSearchInput = require('./input/text/TinyHtmlSearchInput.cjs');
|
|
@@ -100,11 +107,25 @@ class TinyHtmlElems {
|
|
|
100
107
|
static SubmitInput = TinyHtmlSubmitInput;
|
|
101
108
|
|
|
102
109
|
/////////////////////////////////////////////////////
|
|
110
|
+
|
|
103
111
|
static NumberInput = TinyHtmlNumberInput;
|
|
104
112
|
static RangerInput = TinyHtmlRangeInput;
|
|
105
113
|
|
|
106
114
|
//////////////////////////////////////////////////
|
|
107
115
|
|
|
116
|
+
static CheckboxInput = TinyHtmlCheckboxInput;
|
|
117
|
+
static RadioInput = TinyHtmlRadioInput;
|
|
118
|
+
|
|
119
|
+
//////////////////////////////////////////////////
|
|
120
|
+
|
|
121
|
+
static DateInput = TinyHtmlDateInput;
|
|
122
|
+
static DateTimeInput = TinyHtmlDateTimeInput;
|
|
123
|
+
static MonthInput = TinyHtmlMonthInput;
|
|
124
|
+
static TimeInput = TinyHtmlTimeInput;
|
|
125
|
+
static WeekInput = TinyHtmlWeekInput;
|
|
126
|
+
|
|
127
|
+
//////////////////////////////////////////////////
|
|
128
|
+
|
|
108
129
|
static EmailInput = TinyHtmlEmailInput;
|
|
109
130
|
static PasswordInput = TinyHtmlPasswordInput;
|
|
110
131
|
static SearchInput = TinyHtmlSearchInput;
|
|
@@ -163,6 +184,18 @@ export {
|
|
|
163
184
|
TinyHtmlNumberInput,
|
|
164
185
|
TinyHtmlRangeInput,
|
|
165
186
|
//////////////////////////////////////////////////
|
|
187
|
+
|
|
188
|
+
TinyHtmlCheckboxInput;
|
|
189
|
+
TinyHtmlRadioInput;
|
|
190
|
+
|
|
191
|
+
//////////////////////////////////////////////////
|
|
192
|
+
|
|
193
|
+
TinyHtmlDateInput;
|
|
194
|
+
TinyHtmlDateTimeInput;
|
|
195
|
+
TinyHtmlMonthInput;
|
|
196
|
+
TinyHtmlTimeInput;
|
|
197
|
+
TinyHtmlWeekInput;
|
|
198
|
+
|
|
166
199
|
TinyHtmlEmailInput,
|
|
167
200
|
TinyHtmlPasswordInput,
|
|
168
201
|
TinyHtmlSearchInput,
|
|
@@ -11,6 +11,13 @@ declare class TinyHtmlElems {
|
|
|
11
11
|
static SubmitInput: typeof TinyHtmlSubmitInput;
|
|
12
12
|
static NumberInput: typeof TinyHtmlNumberInput;
|
|
13
13
|
static RangerInput: typeof TinyHtmlRangeInput;
|
|
14
|
+
static CheckboxInput: typeof TinyHtmlCheckboxInput;
|
|
15
|
+
static RadioInput: typeof TinyHtmlRadioInput;
|
|
16
|
+
static DateInput: typeof TinyHtmlDateInput;
|
|
17
|
+
static DateTimeInput: typeof TinyHtmlDateTimeInput;
|
|
18
|
+
static MonthInput: typeof TinyHtmlMonthInput;
|
|
19
|
+
static TimeInput: typeof TinyHtmlTimeInput;
|
|
20
|
+
static WeekInput: typeof TinyHtmlWeekInput;
|
|
14
21
|
static EmailInput: typeof TinyHtmlEmailInput;
|
|
15
22
|
static PasswordInput: typeof TinyHtmlPasswordInput;
|
|
16
23
|
static SearchInput: typeof TinyHtmlSearchInput;
|
|
@@ -71,6 +78,13 @@ import TinyHtmlSource from './media/TinyHtmlSource.mjs';
|
|
|
71
78
|
import TinyHtmlButtonInput from './input/button/TinyHtmlButtonInput.mjs';
|
|
72
79
|
import TinyHtmlSubmitInput from './input/button/TinyHtmlSubmitInput.mjs';
|
|
73
80
|
import TinyHtmlRangeInput from './input/number/TinyHtmlRangeInput.mjs';
|
|
81
|
+
import TinyHtmlCheckboxInput from './input/check/TinyHtmlCheckboxInput.mjs';
|
|
82
|
+
import TinyHtmlRadioInput from './input/check/TinyHtmlRadioInput.mjs';
|
|
83
|
+
import TinyHtmlDateInput from './input/date/TinyHtmlDateInput.mjs';
|
|
84
|
+
import TinyHtmlDateTimeInput from './input/date/TinyHtmlDateTimeInput.mjs';
|
|
85
|
+
import TinyHtmlMonthInput from './input/date/TinyHtmlMonthInput.mjs';
|
|
86
|
+
import TinyHtmlTimeInput from './input/date/TinyHtmlTimeInput.mjs';
|
|
87
|
+
import TinyHtmlWeekInput from './input/date/TinyHtmlWeekInput.mjs';
|
|
74
88
|
import TinyHtmlEmailInput from './input/text/TinyHtmlEmailInput.mjs';
|
|
75
89
|
import TinyHtmlPasswordInput from './input/text/TinyHtmlPasswordInput.mjs';
|
|
76
90
|
import TinyHtmlSearchInput from './input/text/TinyHtmlSearchInput.mjs';
|
|
@@ -6,6 +6,15 @@ import TinyHtmlSubmitInput from './input/button/TinyHtmlSubmitInput.mjs';
|
|
|
6
6
|
import TinyHtmlNumberInput from './input/number/TinyHtmlNumberInput.mjs';
|
|
7
7
|
import TinyHtmlRangeInput from './input/number/TinyHtmlRangeInput.mjs';
|
|
8
8
|
//////////////////////////////////////////////////
|
|
9
|
+
import TinyHtmlCheckboxInput from './input/check/TinyHtmlCheckboxInput.mjs';
|
|
10
|
+
import TinyHtmlRadioInput from './input/check/TinyHtmlRadioInput.mjs';
|
|
11
|
+
//////////////////////////////////////////////////
|
|
12
|
+
import TinyHtmlDateInput from './input/date/TinyHtmlDateInput.mjs';
|
|
13
|
+
import TinyHtmlDateTimeInput from './input/date/TinyHtmlDateTimeInput.mjs';
|
|
14
|
+
import TinyHtmlMonthInput from './input/date/TinyHtmlMonthInput.mjs';
|
|
15
|
+
import TinyHtmlTimeInput from './input/date/TinyHtmlTimeInput.mjs';
|
|
16
|
+
import TinyHtmlWeekInput from './input/date/TinyHtmlWeekInput.mjs';
|
|
17
|
+
//////////////////////////////////////////////////
|
|
9
18
|
import TinyHtmlEmailInput from './input/text/TinyHtmlEmailInput.mjs';
|
|
10
19
|
import TinyHtmlPasswordInput from './input/text/TinyHtmlPasswordInput.mjs';
|
|
11
20
|
import TinyHtmlSearchInput from './input/text/TinyHtmlSearchInput.mjs';
|
|
@@ -99,6 +108,15 @@ class TinyHtmlElems {
|
|
|
99
108
|
static NumberInput = TinyHtmlNumberInput;
|
|
100
109
|
static RangerInput = TinyHtmlRangeInput;
|
|
101
110
|
//////////////////////////////////////////////////
|
|
111
|
+
static CheckboxInput = TinyHtmlCheckboxInput;
|
|
112
|
+
static RadioInput = TinyHtmlRadioInput;
|
|
113
|
+
//////////////////////////////////////////////////
|
|
114
|
+
static DateInput = TinyHtmlDateInput;
|
|
115
|
+
static DateTimeInput = TinyHtmlDateTimeInput;
|
|
116
|
+
static MonthInput = TinyHtmlMonthInput;
|
|
117
|
+
static TimeInput = TinyHtmlTimeInput;
|
|
118
|
+
static WeekInput = TinyHtmlWeekInput;
|
|
119
|
+
//////////////////////////////////////////////////
|
|
102
120
|
static EmailInput = TinyHtmlEmailInput;
|
|
103
121
|
static PasswordInput = TinyHtmlPasswordInput;
|
|
104
122
|
static SearchInput = TinyHtmlSearchInput;
|
|
@@ -148,6 +166,18 @@ export {
|
|
|
148
166
|
TinyHtmlNumberInput,
|
|
149
167
|
TinyHtmlRangeInput,
|
|
150
168
|
//////////////////////////////////////////////////
|
|
169
|
+
|
|
170
|
+
TinyHtmlCheckboxInput;
|
|
171
|
+
TinyHtmlRadioInput;
|
|
172
|
+
|
|
173
|
+
//////////////////////////////////////////////////
|
|
174
|
+
|
|
175
|
+
TinyHtmlDateInput;
|
|
176
|
+
TinyHtmlDateTimeInput;
|
|
177
|
+
TinyHtmlMonthInput;
|
|
178
|
+
TinyHtmlTimeInput;
|
|
179
|
+
TinyHtmlWeekInput;
|
|
180
|
+
|
|
151
181
|
TinyHtmlEmailInput,
|
|
152
182
|
TinyHtmlPasswordInput,
|
|
153
183
|
TinyHtmlSearchInput,
|
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var objChecker = require('../basics/objChecker.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a list of values used in dice modifier operations.
|
|
7
|
+
*
|
|
8
|
+
* Each entry can be either:
|
|
9
|
+
* - A number (representing a fixed numeric value), or
|
|
10
|
+
* - An object describing a dice roll, containing:
|
|
11
|
+
* - `value`: The rolled result.
|
|
12
|
+
* - `sides`: The number of sides of the die (e.g., 6 for a d6).
|
|
13
|
+
*
|
|
14
|
+
* Examples of valid entries:
|
|
15
|
+
* - `5` → a direct numeric value
|
|
16
|
+
* - `{ value: 3, sides: 6 }` → result of rolling a d6
|
|
17
|
+
*
|
|
18
|
+
* @typedef {(number | { value: number; sides: number })[]} DiceModifiersValues
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents the result after applying all dice modifiers to a roll.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {Object} ApplyDiceModifiersResult
|
|
25
|
+
* @property {number} final - The final computed value after all modifiers and dice results are processed.
|
|
26
|
+
* @property {ApplyDiceModifiersStep[]} steps - A detailed, ordered list of steps describing how the final value was obtained.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Represents a single step in the dice-modification process.
|
|
31
|
+
*
|
|
32
|
+
* @typedef {Object} ApplyDiceModifiersStep
|
|
33
|
+
* @property {string[]} tokens - The parsed tokens used in this step after normalization.
|
|
34
|
+
* @property {string[]} rawTokensP - The raw tokens preserved in their partially processed state.
|
|
35
|
+
* @property {string[]} rawTokens - The original unprocessed tokens extracted from the expression.
|
|
36
|
+
* @property {number[]} rawDiceTokenSlots - Index references pointing to where each raw dice token appears in the original expression.
|
|
37
|
+
* @property {number[]} diceTokenSlots - Index references for processed/normalized dice tokens within the final evaluation sequence.
|
|
38
|
+
* @property {number} total - The computed subtotal for this step, before aggregation in later steps.
|
|
39
|
+
* @property {Array<number[]>} dicesResult - A list containing the result set of each dice roll.
|
|
40
|
+
* Each entry represents a single dice token and contains an array with the rolled numbers.
|
|
41
|
+
*/
|
|
42
|
+
|
|
3
43
|
/**
|
|
4
44
|
* TinySimpleDice
|
|
5
45
|
*
|
|
@@ -8,6 +48,394 @@
|
|
|
8
48
|
* values suitable for indexing arrays or Sets.
|
|
9
49
|
*/
|
|
10
50
|
class TinySimpleDice {
|
|
51
|
+
/**
|
|
52
|
+
* Safely evaluates a mathematical expression (supports +, -, *, /, %, **, parentheses, fractions and decimals).
|
|
53
|
+
* This function ensures only valid math characters are processed.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} expression - Mathematical expression to evaluate.
|
|
56
|
+
* @returns {number} The calculated numeric result.
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
static _safeEvaluate(expression) {
|
|
60
|
+
// Sanitize and validate only allowed math-safe characters
|
|
61
|
+
if (!/^[\d+\-*/%.()\s^]+$/.test(expression)) {
|
|
62
|
+
throw new Error(`Invalid characters in expression: "${expression}"`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Normalize power operator: allow both ** and ^ for exponentiation
|
|
66
|
+
const normalized = expression.replace(/\^/g, '**');
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Create isolated, safe Function (no variables, only math ops)
|
|
70
|
+
return Function(`"use strict"; return (${normalized})`)();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (!(err instanceof Error)) throw new Error('Unknown Error');
|
|
73
|
+
throw new Error(`Invalid expression "${expression}": ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Replaces dice patterns such as `d6`, `d32`, or multi-dice patterns like `3d6`
|
|
79
|
+
* using sequential values from the provided array.
|
|
80
|
+
*
|
|
81
|
+
* - `d6` consumes 1 value from the array.
|
|
82
|
+
* - `3d6` consumes 3 values from the array and replaces the pattern with their sum.
|
|
83
|
+
*
|
|
84
|
+
* If there are not enough values available, missing rolls default to 0.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} input - The input string containing dice patterns.
|
|
87
|
+
* @param {number[]} values - Sequential numeric values used to replace each dice roll.
|
|
88
|
+
* @returns {string} The resulting string where each dice expression is replaced by its computed value.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* replaceValues("You deal d6 damage", [4]);
|
|
92
|
+
* // → "You deal 4 damage"
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* replaceValues("Roll 3d6 for strength", [3, 5, 2]);
|
|
96
|
+
* // → "Roll 10 for strength"
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* replaceValues("Attack: 2d4 + d8", [1, 3, 7]);
|
|
100
|
+
* // → "Attack: 4 + 7"
|
|
101
|
+
*/
|
|
102
|
+
static replaceValues(input, values) {
|
|
103
|
+
let index = 0;
|
|
104
|
+
|
|
105
|
+
return input.replace(/(\d*)d(\d+)/g, (match, qty) => {
|
|
106
|
+
const count = qty ? Number(qty) : 1; // default is 1dX
|
|
107
|
+
let total = 0;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < count; i++) {
|
|
110
|
+
const value = values[index++];
|
|
111
|
+
total += value !== undefined ? value : 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return String(total);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Tokenizes an expression string while replacing dice patterns (`d6`, `3d6`, etc.)
|
|
120
|
+
* with numeric values taken sequentially from the provided array.
|
|
121
|
+
*
|
|
122
|
+
* This function performs two operations:
|
|
123
|
+
* 1. Dice replacement:
|
|
124
|
+
* - `d6` consumes 1 value.
|
|
125
|
+
* - `3d6` consumes 3 values and returns their sum.
|
|
126
|
+
* - Missing values default to 0.
|
|
127
|
+
*
|
|
128
|
+
* 2. Tokenization:
|
|
129
|
+
* - Numbers become numeric tokens.
|
|
130
|
+
* - Operators (`+`, `-`, `*`, `/`, `(`, `)`) become string tokens.
|
|
131
|
+
*
|
|
132
|
+
* Example:
|
|
133
|
+
* Input: "2d6 + 4 - d8"
|
|
134
|
+
* Values: [3, 4, 5]
|
|
135
|
+
* Output: [7, "+", 4, "-", 5]
|
|
136
|
+
*
|
|
137
|
+
* @param {string} input - The input string containing dice expressions.
|
|
138
|
+
* @param {number[]} values - Sequential numbers used to replace each dice roll.
|
|
139
|
+
* @returns {{ tokens: (string|number)[], text: string }} A tokenized list where dice expressions become numbers.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* tokenizeValues("You deal d6 + 2", [4]);
|
|
143
|
+
* // → [4, "+", 2]
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* tokenizeValues("3d6 + d4", [3, 5, 2, 1]);
|
|
147
|
+
* // → [10, "+", 1]
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* tokenizeValues("2d4 - 1d8 + 7", [1, 3, 7]);
|
|
151
|
+
* // → [4, "-", 7, "+", 7]
|
|
152
|
+
*/
|
|
153
|
+
static tokenizeValues(input, values) {
|
|
154
|
+
// Tokenizer: numbers become numbers, operators become strings.
|
|
155
|
+
const replaced = TinySimpleDice.replaceValues(input, values);
|
|
156
|
+
const tokens = [];
|
|
157
|
+
const regex = /\d+|[()+\-*/]/g;
|
|
158
|
+
let match;
|
|
159
|
+
|
|
160
|
+
while ((match = regex.exec(replaced)) !== null) {
|
|
161
|
+
if (/^\d+$/.test(match[0])) {
|
|
162
|
+
tokens.push(Number(match[0]));
|
|
163
|
+
} else {
|
|
164
|
+
tokens.push(match[0]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { tokens, text: replaced };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parses a dice configuration string supporting notations like "6d" (one d6) or "3d6" (three d6).
|
|
173
|
+
* Extracts all valid dice expressions and keeps their full context as modifiers.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} input - Comma-separated dice expressions.
|
|
176
|
+
* @returns {{
|
|
177
|
+
* sides: { count: number, sides: number }[],
|
|
178
|
+
* modifiers: { index: number, original: string, expression: string }[]
|
|
179
|
+
* }}
|
|
180
|
+
*/
|
|
181
|
+
static parseString(input) {
|
|
182
|
+
if (typeof input !== 'string') {
|
|
183
|
+
throw new TypeError('Input must be a string.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const parts = input
|
|
187
|
+
.split(',')
|
|
188
|
+
.map((p) => p.trim())
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
|
|
191
|
+
/** @type {{ count: number, sides: number }[]} */
|
|
192
|
+
const sides = [];
|
|
193
|
+
|
|
194
|
+
/** @type {{ index: number, original: string, expression: string }[]} */
|
|
195
|
+
const modifiers = [];
|
|
196
|
+
|
|
197
|
+
parts.forEach((part, i) => {
|
|
198
|
+
// ✅ Match dice patterns:
|
|
199
|
+
// - 6d → one d6
|
|
200
|
+
// - 3d6 → three d6
|
|
201
|
+
// - 12d100 → twelve d100
|
|
202
|
+
const regex = /\b(?:(\d+)?d(\d+))\b/g;
|
|
203
|
+
let match;
|
|
204
|
+
const foundDice = [];
|
|
205
|
+
|
|
206
|
+
// --- 🔸 Resolve random choice groups like (0 | 1 | d1)
|
|
207
|
+
const finalPart = part.replace(/\(([^()]+?\|[^()]+?)\)/g, (match, inner) => {
|
|
208
|
+
const options =
|
|
209
|
+
typeof inner === 'string'
|
|
210
|
+
? inner
|
|
211
|
+
.split('|')
|
|
212
|
+
.map((s) => s.trim())
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
: [];
|
|
215
|
+
|
|
216
|
+
if (options.length === 0) throw new Error(`Invalid random-choice group: "${match}"`);
|
|
217
|
+
|
|
218
|
+
const chosen = options[Math.floor(Math.random() * options.length)];
|
|
219
|
+
return chosen;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
while ((match = regex.exec(finalPart)) !== null) {
|
|
223
|
+
const count = parseInt(match[1] || '1', 10); // Default to 1 if not specified (e.g. "d6" or "6d")
|
|
224
|
+
const sidesCount = parseInt(match[2], 10);
|
|
225
|
+
|
|
226
|
+
if (isNaN(sidesCount)) {
|
|
227
|
+
throw new Error(`Invalid dice sides in expression "${match[0]}" at position ${i + 1}.`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
foundDice.push({ count, sides: sidesCount });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (foundDice.length === 0 && Number.isNaN(parseFloat(finalPart))) {
|
|
234
|
+
throw new Error(`Invalid dice expression at position ${i + 1}: "${part}"`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Add all found dice
|
|
238
|
+
sides.push(...foundDice);
|
|
239
|
+
|
|
240
|
+
// Store full expression
|
|
241
|
+
modifiers.push({
|
|
242
|
+
index: i,
|
|
243
|
+
original: part,
|
|
244
|
+
expression: finalPart,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (sides.length === 0) throw new Error(`Invalid dice amount.`);
|
|
249
|
+
|
|
250
|
+
return { sides, modifiers };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Applies parsed modifiers (expressions) to a base number.
|
|
255
|
+
* Replaces only the first number in the expression with the current result
|
|
256
|
+
* before evaluation. Returns an object containing a step-by-step history.
|
|
257
|
+
*
|
|
258
|
+
* @param {DiceModifiersValues} values - Starting number (e.g., dice base value).
|
|
259
|
+
* @param {{ expression: string, original: string }[]} modifiers - Parsed modifiers from TinySimpleDice.parseString.
|
|
260
|
+
* @returns {ApplyDiceModifiersResult}
|
|
261
|
+
*/
|
|
262
|
+
static applyModifiers(values, modifiers) {
|
|
263
|
+
if (
|
|
264
|
+
!Array.isArray(values) ||
|
|
265
|
+
!values.every(
|
|
266
|
+
(n) =>
|
|
267
|
+
(typeof n === 'number' && !Number.isNaN(n)) ||
|
|
268
|
+
(objChecker.isJsonObject(n) &&
|
|
269
|
+
typeof n.value === 'number' &&
|
|
270
|
+
!Number.isNaN(n.value) &&
|
|
271
|
+
typeof n.sides === 'number' &&
|
|
272
|
+
!Number.isNaN(n.sides)),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
throw new TypeError('Bases must be a valid numbers.');
|
|
276
|
+
|
|
277
|
+
if (!Array.isArray(modifiers))
|
|
278
|
+
throw new TypeError('Modifiers must be an array of modifier objects.');
|
|
279
|
+
|
|
280
|
+
let result = 0;
|
|
281
|
+
|
|
282
|
+
/** @type {ApplyDiceModifiersStep[]} */
|
|
283
|
+
const steps = [];
|
|
284
|
+
/** @type {DiceModifiersValues} */
|
|
285
|
+
const iv = [...values];
|
|
286
|
+
|
|
287
|
+
for (const index in modifiers) {
|
|
288
|
+
const mod = modifiers[index];
|
|
289
|
+
if (typeof mod.expression !== 'string') {
|
|
290
|
+
throw new Error('Each modifier must include an expression string.');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const originalExp = mod.original;
|
|
294
|
+
const expression = mod.expression;
|
|
295
|
+
|
|
296
|
+
/** @type {Array<number[]>} */
|
|
297
|
+
const dices = [];
|
|
298
|
+
|
|
299
|
+
/** @type {number[]} */
|
|
300
|
+
const diceTokenSlots = [];
|
|
301
|
+
/** @type {number[]} */
|
|
302
|
+
const rawDiceTokenSlots = [];
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Tokenize expression for manipulation or display.
|
|
306
|
+
* Supports dice, numbers, parentheses, math ops, and choice groups.
|
|
307
|
+
* Ensures (0 | 1 | d1) is treated as ONE token.
|
|
308
|
+
* @param {string} value
|
|
309
|
+
* @returns {string[]}
|
|
310
|
+
*/
|
|
311
|
+
const matchTokens = (value) =>
|
|
312
|
+
value.match(/\(\s*[^()]+\|\s*[^()]+\s*\)|\b\d*d\d+\b|[-+]?\d+(?:\.\d+)?|[+\-*/%^()]/g) ||
|
|
313
|
+
[];
|
|
314
|
+
|
|
315
|
+
const rawTokens = matchTokens(expression);
|
|
316
|
+
const rawTokensOriginal = matchTokens(originalExp);
|
|
317
|
+
const tokens = [...rawTokens];
|
|
318
|
+
/** @type {string[]} */
|
|
319
|
+
const rawSlotsUsed = [];
|
|
320
|
+
|
|
321
|
+
// ✅ Replace the first numeric literal (integer/decimal) that may be inside parentheses
|
|
322
|
+
// @ts-ignore
|
|
323
|
+
const replacedExpr = expression.replace(/\b\d*d\d+\b/g, (m0) => {
|
|
324
|
+
// Parse dice numbers
|
|
325
|
+
const diceParsed = m0.split('d');
|
|
326
|
+
const getRawTokenSlot = () => {
|
|
327
|
+
for (const index in rawTokens) {
|
|
328
|
+
if (rawTokens[index] === m0 && rawSlotsUsed.indexOf(index) < 0) {
|
|
329
|
+
rawSlotsUsed.push(index);
|
|
330
|
+
rawDiceTokenSlots.push(Number(index));
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validates that the dice value does not exceed the number of sides.
|
|
338
|
+
* @param {{ value: number; sides: number }} r - The dice roll result and the number of sides.
|
|
339
|
+
* @throws {Error} If the value is greater than the number of sides.
|
|
340
|
+
*/
|
|
341
|
+
const diceValidator = (r) => {
|
|
342
|
+
if (r.value > r.sides)
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Invalid dice roll: value (${r.value}) must be between 1 and ${r.sides}.`,
|
|
345
|
+
);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// 1dn
|
|
349
|
+
if (diceParsed[0].trim().length === 0) {
|
|
350
|
+
const r = iv.shift();
|
|
351
|
+
const rv = typeof r === 'number' ? r : objChecker.isJsonObject(r) ? r.value : 0;
|
|
352
|
+
if (objChecker.isJsonObject(r)) diceValidator(r);
|
|
353
|
+
|
|
354
|
+
dices.push([rv]);
|
|
355
|
+
|
|
356
|
+
for (const index in tokens) {
|
|
357
|
+
if (tokens[index] === m0) {
|
|
358
|
+
tokens[index] = String(rv);
|
|
359
|
+
diceTokenSlots.push(Number(index));
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getRawTokenSlot();
|
|
365
|
+
return rv;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ndn
|
|
369
|
+
/** @type {number[]} */
|
|
370
|
+
const dices2 = [];
|
|
371
|
+
const diceAmount = Number(diceParsed[0]);
|
|
372
|
+
|
|
373
|
+
const newTokensInsert = ['('];
|
|
374
|
+
let total = '(';
|
|
375
|
+
for (let i = 0; i < diceAmount; i++) {
|
|
376
|
+
const r = iv.shift();
|
|
377
|
+
const rv = typeof r === 'number' ? r : objChecker.isJsonObject(r) ? r.value : 0;
|
|
378
|
+
if (objChecker.isJsonObject(r)) diceValidator(r);
|
|
379
|
+
|
|
380
|
+
newTokensInsert.push(String(rv));
|
|
381
|
+
|
|
382
|
+
const finishSpace = i < diceAmount - 1 ? ' + ' : ')';
|
|
383
|
+
newTokensInsert.push(finishSpace);
|
|
384
|
+
|
|
385
|
+
total += `${rv}${finishSpace}`;
|
|
386
|
+
dices2.push(rv);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const index in tokens) {
|
|
390
|
+
const i = Number(index);
|
|
391
|
+
if (tokens[i] === m0) {
|
|
392
|
+
tokens.splice(i, 1, ...newTokensInsert);
|
|
393
|
+
// Each new item a new string is added
|
|
394
|
+
let amount = 1;
|
|
395
|
+
for (let i2 = 0; i2 < diceAmount; i2++) {
|
|
396
|
+
diceTokenSlots.push(i + i2 + amount);
|
|
397
|
+
amount++;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
dices.push(dices2);
|
|
403
|
+
|
|
404
|
+
getRawTokenSlot();
|
|
405
|
+
return total;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
/** @type {number} */
|
|
409
|
+
let evaluated;
|
|
410
|
+
try {
|
|
411
|
+
evaluated = TinySimpleDice._safeEvaluate(replacedExpr);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
if (!(err instanceof Error)) throw new Error('Unknown Error');
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Error evaluating expression "${replacedExpr}" (from "${expression}"): ${err.message}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
steps.push({
|
|
420
|
+
rawTokensP: rawTokens,
|
|
421
|
+
rawTokens: rawTokensOriginal,
|
|
422
|
+
tokens,
|
|
423
|
+
rawDiceTokenSlots,
|
|
424
|
+
diceTokenSlots,
|
|
425
|
+
dicesResult: dices,
|
|
426
|
+
total: evaluated,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
result += evaluated;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Complete
|
|
433
|
+
return {
|
|
434
|
+
final: result,
|
|
435
|
+
steps,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
11
439
|
/**
|
|
12
440
|
* Rolls a dice specifically for choosing an array or Set index.
|
|
13
441
|
* @param {any[]|Set<any>} arr - The array or Set to get a random index from.
|