pollyroll 0.1.0
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/LICENSE +21 -0
- package/README.md +323 -0
- package/dist/chunk-JIYQHWUA.js +243 -0
- package/dist/chunk-OEMS7CZP.cjs +251 -0
- package/dist/index.cjs +155 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +140 -0
- package/dist/react.cjs +50 -0
- package/dist/react.d.cts +22 -0
- package/dist/react.d.ts +22 -0
- package/dist/react.js +47 -0
- package/dist/render.cjs +2515 -0
- package/dist/render.d.cts +75 -0
- package/dist/render.d.ts +75 -0
- package/dist/render.js +2498 -0
- package/dist/types-CnzvkHmJ.d.cts +88 -0
- package/dist/types-CnzvkHmJ.d.ts +88 -0
- package/package.json +120 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/notation.ts
|
|
4
|
+
var PollyrollSyntaxError = class extends Error {
|
|
5
|
+
index;
|
|
6
|
+
constructor(message, index) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "PollyrollSyntaxError";
|
|
9
|
+
this.index = index;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var MAX_LENGTH = 256;
|
|
13
|
+
var MAX_TERMS = 20;
|
|
14
|
+
var MAX_COUNT = 100;
|
|
15
|
+
var MAX_DICE = 200;
|
|
16
|
+
var MAX_CONSTANT = 999999;
|
|
17
|
+
var WHITESPACE = /\s/;
|
|
18
|
+
var SIZES = {
|
|
19
|
+
"4": "d4",
|
|
20
|
+
"6": "d6",
|
|
21
|
+
"8": "d8",
|
|
22
|
+
"10": "d10",
|
|
23
|
+
"12": "d12",
|
|
24
|
+
"20": "d20",
|
|
25
|
+
"100": "d100"
|
|
26
|
+
};
|
|
27
|
+
var isDigit = (c) => c >= "0" && c <= "9";
|
|
28
|
+
function parse(notation) {
|
|
29
|
+
if (typeof notation !== "string") {
|
|
30
|
+
throw new PollyrollSyntaxError("Notation must be a string", 0);
|
|
31
|
+
}
|
|
32
|
+
if (notation.length > MAX_LENGTH) {
|
|
33
|
+
throw new PollyrollSyntaxError("Notation is longer than 256 characters", MAX_LENGTH);
|
|
34
|
+
}
|
|
35
|
+
const chars = [];
|
|
36
|
+
const origin = [];
|
|
37
|
+
for (let i = 0; i < notation.length; i++) {
|
|
38
|
+
const c = notation.charAt(i);
|
|
39
|
+
if (WHITESPACE.test(c)) continue;
|
|
40
|
+
chars.push(c >= "A" && c <= "Z" ? c.toLowerCase() : c);
|
|
41
|
+
origin.push(i);
|
|
42
|
+
}
|
|
43
|
+
if (chars.length === 0) {
|
|
44
|
+
throw new PollyrollSyntaxError("Notation is empty", 0);
|
|
45
|
+
}
|
|
46
|
+
let pos = 0;
|
|
47
|
+
const peek = (offset = 0) => chars[pos + offset] ?? "";
|
|
48
|
+
const word2 = (w) => chars.slice(pos, pos + w.length).join("") === w;
|
|
49
|
+
const fail = (message, at) => new PollyrollSyntaxError(message, origin[at] ?? notation.length);
|
|
50
|
+
const unexpected = () => {
|
|
51
|
+
const at = origin[pos];
|
|
52
|
+
return at === void 0 ? fail("Unexpected end of notation", pos) : fail(`Unexpected character "${notation.charAt(at)}"`, pos);
|
|
53
|
+
};
|
|
54
|
+
const digits = () => {
|
|
55
|
+
let s = "";
|
|
56
|
+
while (isDigit(peek())) {
|
|
57
|
+
s += peek();
|
|
58
|
+
pos++;
|
|
59
|
+
}
|
|
60
|
+
return s;
|
|
61
|
+
};
|
|
62
|
+
const parseDie = () => {
|
|
63
|
+
const c = peek();
|
|
64
|
+
if (c === "%" || c === "f") {
|
|
65
|
+
pos++;
|
|
66
|
+
return c === "%" ? "d100" : "dF";
|
|
67
|
+
}
|
|
68
|
+
if (!isDigit(c)) throw unexpected();
|
|
69
|
+
const sizeAt = pos;
|
|
70
|
+
const size = digits();
|
|
71
|
+
const die = SIZES[size];
|
|
72
|
+
if (!die) throw fail(`Unsupported die size d${size}`, sizeAt);
|
|
73
|
+
return die;
|
|
74
|
+
};
|
|
75
|
+
const parseTerm = (sign2) => {
|
|
76
|
+
if (word2("adv") || word2("dis")) {
|
|
77
|
+
const mode = peek() === "a" ? "kh" : "kl";
|
|
78
|
+
pos += 3;
|
|
79
|
+
return { kind: "dice", sign: sign2, count: 2, die: "d20", keep: { mode, n: 1 }, explode: false };
|
|
80
|
+
}
|
|
81
|
+
let count = 1;
|
|
82
|
+
if (isDigit(peek())) {
|
|
83
|
+
const countAt = pos;
|
|
84
|
+
count = Number(digits());
|
|
85
|
+
if (word2("adv") || word2("dis")) throw fail("adv and dis take no dice count", pos);
|
|
86
|
+
if (peek() !== "d") {
|
|
87
|
+
if (count > MAX_CONSTANT) throw fail("Constant is larger than 999999", countAt);
|
|
88
|
+
return { kind: "constant", sign: sign2, value: count };
|
|
89
|
+
}
|
|
90
|
+
if (count < 1 || count > MAX_COUNT) {
|
|
91
|
+
throw fail("Dice count must be between 1 and 100", countAt);
|
|
92
|
+
}
|
|
93
|
+
} else if (peek() !== "d") {
|
|
94
|
+
throw unexpected();
|
|
95
|
+
}
|
|
96
|
+
pos++;
|
|
97
|
+
const die = parseDie();
|
|
98
|
+
let keep = null;
|
|
99
|
+
let explode = false;
|
|
100
|
+
for (; ; ) {
|
|
101
|
+
const c = peek();
|
|
102
|
+
const next = peek(1);
|
|
103
|
+
if (c === "!" || c === "x") {
|
|
104
|
+
if (explode) throw fail("A term can explode only once", pos);
|
|
105
|
+
if (die === "dF") throw fail("Fudge dice cannot explode", pos);
|
|
106
|
+
explode = true;
|
|
107
|
+
pos++;
|
|
108
|
+
} else if (c === "k" || c === "d") {
|
|
109
|
+
if (keep) throw fail("A term can have only one keep or drop", pos);
|
|
110
|
+
const side = next === "h" || next === "l" ? next : c === "k" ? "h" : "l";
|
|
111
|
+
const mode = c === "k" ? side === "h" ? "kh" : "kl" : side === "h" ? "dh" : "dl";
|
|
112
|
+
let nAt = pos;
|
|
113
|
+
pos += next === side ? 2 : 1;
|
|
114
|
+
let n = 1;
|
|
115
|
+
if (isDigit(peek())) {
|
|
116
|
+
nAt = pos;
|
|
117
|
+
n = Number(digits());
|
|
118
|
+
}
|
|
119
|
+
if (n < 1 || n > count)
|
|
120
|
+
throw fail(`Keep or drop count must be between 1 and ${count}`, nAt);
|
|
121
|
+
keep = { mode, n };
|
|
122
|
+
} else {
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { kind: "dice", sign: sign2, count, die, keep, explode };
|
|
127
|
+
};
|
|
128
|
+
const terms = [];
|
|
129
|
+
let total = 0;
|
|
130
|
+
let sign = 1;
|
|
131
|
+
if (peek() === "+" || peek() === "-") {
|
|
132
|
+
sign = peek() === "-" ? -1 : 1;
|
|
133
|
+
pos++;
|
|
134
|
+
}
|
|
135
|
+
for (; ; ) {
|
|
136
|
+
const start = pos;
|
|
137
|
+
const term = parseTerm(sign);
|
|
138
|
+
terms.push(term);
|
|
139
|
+
if (terms.length > MAX_TERMS) throw fail("Notation has more than 20 terms", start);
|
|
140
|
+
if (term.kind === "dice") {
|
|
141
|
+
total += term.count;
|
|
142
|
+
if (total > MAX_DICE) throw fail("Notation has more than 200 dice", start);
|
|
143
|
+
}
|
|
144
|
+
const c = peek();
|
|
145
|
+
if (c === "") break;
|
|
146
|
+
if (c !== "+" && c !== "-") throw unexpected();
|
|
147
|
+
sign = c === "-" ? -1 : 1;
|
|
148
|
+
pos++;
|
|
149
|
+
}
|
|
150
|
+
if (total === 0) {
|
|
151
|
+
throw new PollyrollSyntaxError("Notation has no dice", 0);
|
|
152
|
+
}
|
|
153
|
+
return { terms };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/core/evaluate.ts
|
|
157
|
+
var ascending = (list) => list.map(([, i]) => i).sort((a, b) => a - b);
|
|
158
|
+
function evaluate(event) {
|
|
159
|
+
const buckets = parse(event.notation).terms.filter((t) => t.kind === "dice").map((term) => ({ term, dice: [] }));
|
|
160
|
+
for (const die of event.dice) {
|
|
161
|
+
const bucket = buckets[die.group];
|
|
162
|
+
if (!bucket) throw new TypeError(`die group ${die.group} is out of range`);
|
|
163
|
+
bucket.dice.push(die.value);
|
|
164
|
+
}
|
|
165
|
+
let total = event.modifier;
|
|
166
|
+
const groups = buckets.map(({ term, dice }) => {
|
|
167
|
+
const pairs = [];
|
|
168
|
+
dice.forEach((v, i) => {
|
|
169
|
+
if (v !== null) pairs.push([v, i]);
|
|
170
|
+
});
|
|
171
|
+
if (pairs.length < dice.length) {
|
|
172
|
+
total = null;
|
|
173
|
+
return { die: term.die, dice, kept: [], dropped: [], subtotal: null };
|
|
174
|
+
}
|
|
175
|
+
let keep = pairs;
|
|
176
|
+
let drop = [];
|
|
177
|
+
if (term.keep) {
|
|
178
|
+
const { mode, n } = term.keep;
|
|
179
|
+
const dir = mode === "kh" || mode === "dh" ? -1 : 1;
|
|
180
|
+
const ranked = pairs.slice().sort(([va, a], [vb, b]) => dir * (va - vb) || a - b);
|
|
181
|
+
[keep, drop] = mode[0] === "k" ? [ranked.slice(0, n), ranked.slice(n)] : [ranked.slice(n), ranked.slice(0, n)];
|
|
182
|
+
}
|
|
183
|
+
let subtotal = 0;
|
|
184
|
+
for (const [v] of keep) subtotal += term.sign * v;
|
|
185
|
+
if (total !== null) total += subtotal;
|
|
186
|
+
return { die: term.die, dice, kept: ascending(keep), dropped: ascending(drop), subtotal };
|
|
187
|
+
});
|
|
188
|
+
return { total, modifier: event.modifier, groups };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/core/rng.ts
|
|
192
|
+
var TWO_32 = 2 ** 32;
|
|
193
|
+
var SEED_PATTERN = /^[0-9a-f]{32}$/;
|
|
194
|
+
var word = new Uint32Array(1);
|
|
195
|
+
function rejectionInt(max, draw) {
|
|
196
|
+
if (!Number.isInteger(max) || max < 1 || max > TWO_32) {
|
|
197
|
+
throw new RangeError(`maxExclusive must be an integer in [1, 2^32], got ${max}`);
|
|
198
|
+
}
|
|
199
|
+
const limit = TWO_32 - TWO_32 % max;
|
|
200
|
+
let u = draw();
|
|
201
|
+
while (u >= limit) u = draw();
|
|
202
|
+
return u % max;
|
|
203
|
+
}
|
|
204
|
+
function cryptoInt(maxExclusive) {
|
|
205
|
+
return rejectionInt(maxExclusive, () => {
|
|
206
|
+
let u = 0;
|
|
207
|
+
for (const w of globalThis.crypto.getRandomValues(word)) u = w;
|
|
208
|
+
return u;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function generateSeed() {
|
|
212
|
+
const words = globalThis.crypto.getRandomValues(new Uint32Array(4));
|
|
213
|
+
let seed = "";
|
|
214
|
+
for (const w of words) seed += w.toString(16).padStart(8, "0");
|
|
215
|
+
return seed;
|
|
216
|
+
}
|
|
217
|
+
function isSeed(value) {
|
|
218
|
+
return typeof value === "string" && SEED_PATTERN.test(value);
|
|
219
|
+
}
|
|
220
|
+
function createSeedRng(seed) {
|
|
221
|
+
if (!isSeed(seed)) throw new TypeError("seed must be 32 lowercase hex characters");
|
|
222
|
+
const part = (i) => parseInt(seed.slice(i * 8, i * 8 + 8), 16) | 0;
|
|
223
|
+
let a = part(0);
|
|
224
|
+
let b = part(1);
|
|
225
|
+
let c = part(2);
|
|
226
|
+
let d = part(3);
|
|
227
|
+
const u32 = () => {
|
|
228
|
+
let t = a + b | 0;
|
|
229
|
+
a = b ^ b >>> 9;
|
|
230
|
+
b = c + (c << 3) | 0;
|
|
231
|
+
c = c << 21 | c >>> 11;
|
|
232
|
+
d = d + 1 | 0;
|
|
233
|
+
t = t + d | 0;
|
|
234
|
+
c = c + t | 0;
|
|
235
|
+
return t >>> 0;
|
|
236
|
+
};
|
|
237
|
+
for (let i = 0; i < 12; i++) u32();
|
|
238
|
+
return {
|
|
239
|
+
u32,
|
|
240
|
+
float: () => u32() / TWO_32,
|
|
241
|
+
int: (maxExclusive) => rejectionInt(maxExclusive, u32)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
exports.PollyrollSyntaxError = PollyrollSyntaxError;
|
|
246
|
+
exports.createSeedRng = createSeedRng;
|
|
247
|
+
exports.cryptoInt = cryptoInt;
|
|
248
|
+
exports.evaluate = evaluate;
|
|
249
|
+
exports.generateSeed = generateSeed;
|
|
250
|
+
exports.isSeed = isSeed;
|
|
251
|
+
exports.parse = parse;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkOEMS7CZP_cjs = require('./chunk-OEMS7CZP.cjs');
|
|
4
|
+
|
|
5
|
+
// src/core/dice.ts
|
|
6
|
+
function dieRange(type) {
|
|
7
|
+
return type === "dF" ? [-1, 1] : [1, Number(type.slice(1))];
|
|
8
|
+
}
|
|
9
|
+
var MAX_WAVE = 10;
|
|
10
|
+
|
|
11
|
+
// src/core/roll.ts
|
|
12
|
+
function createRoll(notation, opts = {}) {
|
|
13
|
+
const ast = chunkOEMS7CZP_cjs.parse(notation);
|
|
14
|
+
const rng = opts.rng ?? chunkOEMS7CZP_cjs.cryptoInt;
|
|
15
|
+
const seed = opts.seed ?? chunkOEMS7CZP_cjs.generateSeed();
|
|
16
|
+
if (!chunkOEMS7CZP_cjs.isSeed(seed)) throw new TypeError("seed must be 32 lowercase hex characters");
|
|
17
|
+
const terms = [];
|
|
18
|
+
let modifier = 0;
|
|
19
|
+
for (const term of ast.terms) {
|
|
20
|
+
if (term.kind === "dice") terms.push(term);
|
|
21
|
+
else modifier += term.sign * term.value;
|
|
22
|
+
}
|
|
23
|
+
const dice = [];
|
|
24
|
+
const roll = (term, group, wave, next) => {
|
|
25
|
+
const [min, max] = dieRange(term.die);
|
|
26
|
+
const size = max - min + 1;
|
|
27
|
+
const r = rng(size);
|
|
28
|
+
if (!Number.isInteger(r) || r < 0 || r >= size) {
|
|
29
|
+
throw new RangeError(`rng must return an integer in [0, ${size}), got ${r}`);
|
|
30
|
+
}
|
|
31
|
+
const value = r + min;
|
|
32
|
+
dice.push({ type: term.die, value, group, wave });
|
|
33
|
+
if (term.explode && value === max) next.push([term, group]);
|
|
34
|
+
};
|
|
35
|
+
let pending = [];
|
|
36
|
+
terms.forEach((term, group) => {
|
|
37
|
+
for (let i = 0; i < term.count; i++) roll(term, group, 0, pending);
|
|
38
|
+
});
|
|
39
|
+
for (let wave = 1; wave <= MAX_WAVE && pending.length > 0; wave++) {
|
|
40
|
+
const next = [];
|
|
41
|
+
for (const [term, group] of pending) roll(term, group, wave, next);
|
|
42
|
+
pending = next;
|
|
43
|
+
}
|
|
44
|
+
const event = {
|
|
45
|
+
v: 1,
|
|
46
|
+
id: globalThis.crypto.randomUUID(),
|
|
47
|
+
notation,
|
|
48
|
+
dice,
|
|
49
|
+
modifier,
|
|
50
|
+
seed,
|
|
51
|
+
createdAt: Date.now()
|
|
52
|
+
};
|
|
53
|
+
if (opts.skin !== void 0) event.skin = opts.skin;
|
|
54
|
+
if (opts.rollerId !== void 0) event.rollerId = opts.rollerId;
|
|
55
|
+
if (opts.audience !== void 0) event.audience = opts.audience;
|
|
56
|
+
return event;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/core/event.ts
|
|
60
|
+
var MAX_EVENT_DICE = 2200;
|
|
61
|
+
var isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
62
|
+
var isStr = (x, min, max) => typeof x === "string" && x.length >= min && x.length <= max;
|
|
63
|
+
var isInt = (x, min, max) => typeof x === "number" && Number.isInteger(x) && x >= min && x <= max;
|
|
64
|
+
var isUnit = (x) => typeof x === "number" && x >= 0 && x <= 1;
|
|
65
|
+
var oneOf = (x, words) => typeof x === "string" && words.split(" ").includes(x);
|
|
66
|
+
var optional = (x, test) => x === void 0 || test(x);
|
|
67
|
+
var all = (list, test) => {
|
|
68
|
+
for (const x of list) if (!test(x)) return false;
|
|
69
|
+
return true;
|
|
70
|
+
};
|
|
71
|
+
var isColor = (x) => isStr(x, 0, 64);
|
|
72
|
+
var isMaterial = (m) => isObj(m) ? isUnit(m.metalness) && isUnit(m.roughness) && "clearcoat transmission tint sparkle".split(" ").every((k) => optional(m[k], isUnit)) : oneOf(m, "plastic metal wood glass stone gem");
|
|
73
|
+
var isSkin = (s) => isObj(s) ? isMaterial(s.material) && (Array.isArray(s.color) ? s.color.length === 2 && all(s.color, isColor) : isColor(s.color)) && isColor(s.labelColor) && optional(s.labelStyle, (x) => oneOf(x, "engraved printed embossed")) && optional(s.pattern, (x) => oneOf(x, "none gradient speckle marble wood swirl")) && optional(s.font, (x) => isStr(x, 1, 128)) : isStr(s, 1, 64);
|
|
74
|
+
var isId = (x) => isStr(x, 1, 128);
|
|
75
|
+
var isAudience = (a) => Array.isArray(a) ? a.length <= 100 && all(a, isId) : oneOf(a, "all dm");
|
|
76
|
+
function redact(event) {
|
|
77
|
+
const dice = event.dice.filter((die) => die.wave === 0).map((die) => ({ ...die, value: null }));
|
|
78
|
+
const copy = { ...event, dice };
|
|
79
|
+
const { audience, skin } = event;
|
|
80
|
+
if (Array.isArray(audience)) copy.audience = audience.slice();
|
|
81
|
+
if (typeof skin === "object") {
|
|
82
|
+
const { color } = skin;
|
|
83
|
+
copy.skin = { ...skin, color: typeof color === "string" ? color : [color[0], color[1]] };
|
|
84
|
+
}
|
|
85
|
+
return copy;
|
|
86
|
+
}
|
|
87
|
+
function check(e) {
|
|
88
|
+
if (!isObj(e) || e.v !== 1 || !isId(e.id) || typeof e.notation !== "string" || !chunkOEMS7CZP_cjs.isSeed(e.seed) || typeof e.createdAt !== "number" || !(e.createdAt >= 0 && e.createdAt < Infinity) || !optional(e.skin, isSkin) || !optional(e.rollerId, isId) || !optional(e.audience, isAudience)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const terms = [];
|
|
92
|
+
let modifier = 0;
|
|
93
|
+
for (const t of chunkOEMS7CZP_cjs.parse(e.notation).terms) {
|
|
94
|
+
if (t.kind === "dice") terms.push(t);
|
|
95
|
+
else modifier += t.sign * t.value;
|
|
96
|
+
}
|
|
97
|
+
const dice = e.dice;
|
|
98
|
+
if (e.modifier !== modifier || !Array.isArray(dice) || dice.length < 1 || dice.length > MAX_EVENT_DICE) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const W = MAX_WAVE + 1;
|
|
102
|
+
const count = new Array(terms.length * W).fill(0);
|
|
103
|
+
const maxed = count.slice();
|
|
104
|
+
const hidden = terms.map(() => false);
|
|
105
|
+
const get = (list, i) => list[i] ?? 0;
|
|
106
|
+
let lastWave = 0;
|
|
107
|
+
for (const die of dice) {
|
|
108
|
+
if (!isObj(die)) return false;
|
|
109
|
+
const { type, group, wave, value } = die;
|
|
110
|
+
if (!isInt(group, 0, terms.length - 1)) return false;
|
|
111
|
+
const term = terms[group];
|
|
112
|
+
if (!term || term.die !== type || !isInt(wave, lastWave, term.explode ? MAX_WAVE : 0)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
lastWave = wave;
|
|
116
|
+
const [min, max] = dieRange(term.die);
|
|
117
|
+
if (value === null) hidden[group] = true;
|
|
118
|
+
else if (!isInt(value, min, max)) return false;
|
|
119
|
+
const k = group * W + wave;
|
|
120
|
+
count[k] = get(count, k) + 1;
|
|
121
|
+
if (value === max) maxed[k] = get(maxed, k) + 1;
|
|
122
|
+
}
|
|
123
|
+
return terms.every((term, g) => {
|
|
124
|
+
const at = (list, w) => get(list, g * W + w);
|
|
125
|
+
if (at(count, 0) !== term.count) return false;
|
|
126
|
+
for (let w = 1; term.explode && w <= MAX_WAVE; w++) {
|
|
127
|
+
const limit = hidden[g] ? at(count, w - 1) : at(maxed, w - 1);
|
|
128
|
+
if (hidden[g] ? at(count, w) > limit : at(count, w) !== limit) return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function isRollEvent(input) {
|
|
134
|
+
try {
|
|
135
|
+
return check(input);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Object.defineProperty(exports, "PollyrollSyntaxError", {
|
|
142
|
+
enumerable: true,
|
|
143
|
+
get: function () { return chunkOEMS7CZP_cjs.PollyrollSyntaxError; }
|
|
144
|
+
});
|
|
145
|
+
Object.defineProperty(exports, "evaluate", {
|
|
146
|
+
enumerable: true,
|
|
147
|
+
get: function () { return chunkOEMS7CZP_cjs.evaluate; }
|
|
148
|
+
});
|
|
149
|
+
Object.defineProperty(exports, "parse", {
|
|
150
|
+
enumerable: true,
|
|
151
|
+
get: function () { return chunkOEMS7CZP_cjs.parse; }
|
|
152
|
+
});
|
|
153
|
+
exports.createRoll = createRoll;
|
|
154
|
+
exports.isRollEvent = isRollEvent;
|
|
155
|
+
exports.redact = redact;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { R as RollAst, C as CreateRollOptions, a as RollEvent, b as RollSummary } from './types-CnzvkHmJ.cjs';
|
|
2
|
+
export { A as Audience, c as ConstantTerm, D as DiceTerm, d as DieType, K as KeepMode, L as LabelStyle, M as MaterialParams, e as MaterialPreset, P as PatternName, f as RollGroupSummary, g as RollTerm, h as RolledDie, S as Skin, i as SkinRef } from './types-CnzvkHmJ.cjs';
|
|
3
|
+
|
|
4
|
+
declare class PollyrollSyntaxError extends Error {
|
|
5
|
+
readonly index: number;
|
|
6
|
+
constructor(message: string, index: number);
|
|
7
|
+
}
|
|
8
|
+
declare function parse(notation: string): RollAst;
|
|
9
|
+
|
|
10
|
+
declare function createRoll(notation: string, opts?: CreateRollOptions): RollEvent;
|
|
11
|
+
|
|
12
|
+
declare function evaluate(event: RollEvent): RollSummary;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Copy of `event` with every value set to null and explosion dice (wave ≥ 1) removed, so the
|
|
16
|
+
* number of dice reveals nothing about the hidden values.
|
|
17
|
+
*/
|
|
18
|
+
declare function redact(event: RollEvent): RollEvent;
|
|
19
|
+
/** True when `input` is a structurally valid v1 `RollEvent`. Never throws. */
|
|
20
|
+
declare function isRollEvent(input: unknown): input is RollEvent;
|
|
21
|
+
|
|
22
|
+
export { CreateRollOptions, PollyrollSyntaxError, RollAst, RollEvent, RollSummary, createRoll, evaluate, isRollEvent, parse, redact };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { R as RollAst, C as CreateRollOptions, a as RollEvent, b as RollSummary } from './types-CnzvkHmJ.js';
|
|
2
|
+
export { A as Audience, c as ConstantTerm, D as DiceTerm, d as DieType, K as KeepMode, L as LabelStyle, M as MaterialParams, e as MaterialPreset, P as PatternName, f as RollGroupSummary, g as RollTerm, h as RolledDie, S as Skin, i as SkinRef } from './types-CnzvkHmJ.js';
|
|
3
|
+
|
|
4
|
+
declare class PollyrollSyntaxError extends Error {
|
|
5
|
+
readonly index: number;
|
|
6
|
+
constructor(message: string, index: number);
|
|
7
|
+
}
|
|
8
|
+
declare function parse(notation: string): RollAst;
|
|
9
|
+
|
|
10
|
+
declare function createRoll(notation: string, opts?: CreateRollOptions): RollEvent;
|
|
11
|
+
|
|
12
|
+
declare function evaluate(event: RollEvent): RollSummary;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Copy of `event` with every value set to null and explosion dice (wave ≥ 1) removed, so the
|
|
16
|
+
* number of dice reveals nothing about the hidden values.
|
|
17
|
+
*/
|
|
18
|
+
declare function redact(event: RollEvent): RollEvent;
|
|
19
|
+
/** True when `input` is a structurally valid v1 `RollEvent`. Never throws. */
|
|
20
|
+
declare function isRollEvent(input: unknown): input is RollEvent;
|
|
21
|
+
|
|
22
|
+
export { CreateRollOptions, PollyrollSyntaxError, RollAst, RollEvent, RollSummary, createRoll, evaluate, isRollEvent, parse, redact };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { parse, cryptoInt, generateSeed, isSeed } from './chunk-JIYQHWUA.js';
|
|
2
|
+
export { PollyrollSyntaxError, evaluate, parse } from './chunk-JIYQHWUA.js';
|
|
3
|
+
|
|
4
|
+
// src/core/dice.ts
|
|
5
|
+
function dieRange(type) {
|
|
6
|
+
return type === "dF" ? [-1, 1] : [1, Number(type.slice(1))];
|
|
7
|
+
}
|
|
8
|
+
var MAX_WAVE = 10;
|
|
9
|
+
|
|
10
|
+
// src/core/roll.ts
|
|
11
|
+
function createRoll(notation, opts = {}) {
|
|
12
|
+
const ast = parse(notation);
|
|
13
|
+
const rng = opts.rng ?? cryptoInt;
|
|
14
|
+
const seed = opts.seed ?? generateSeed();
|
|
15
|
+
if (!isSeed(seed)) throw new TypeError("seed must be 32 lowercase hex characters");
|
|
16
|
+
const terms = [];
|
|
17
|
+
let modifier = 0;
|
|
18
|
+
for (const term of ast.terms) {
|
|
19
|
+
if (term.kind === "dice") terms.push(term);
|
|
20
|
+
else modifier += term.sign * term.value;
|
|
21
|
+
}
|
|
22
|
+
const dice = [];
|
|
23
|
+
const roll = (term, group, wave, next) => {
|
|
24
|
+
const [min, max] = dieRange(term.die);
|
|
25
|
+
const size = max - min + 1;
|
|
26
|
+
const r = rng(size);
|
|
27
|
+
if (!Number.isInteger(r) || r < 0 || r >= size) {
|
|
28
|
+
throw new RangeError(`rng must return an integer in [0, ${size}), got ${r}`);
|
|
29
|
+
}
|
|
30
|
+
const value = r + min;
|
|
31
|
+
dice.push({ type: term.die, value, group, wave });
|
|
32
|
+
if (term.explode && value === max) next.push([term, group]);
|
|
33
|
+
};
|
|
34
|
+
let pending = [];
|
|
35
|
+
terms.forEach((term, group) => {
|
|
36
|
+
for (let i = 0; i < term.count; i++) roll(term, group, 0, pending);
|
|
37
|
+
});
|
|
38
|
+
for (let wave = 1; wave <= MAX_WAVE && pending.length > 0; wave++) {
|
|
39
|
+
const next = [];
|
|
40
|
+
for (const [term, group] of pending) roll(term, group, wave, next);
|
|
41
|
+
pending = next;
|
|
42
|
+
}
|
|
43
|
+
const event = {
|
|
44
|
+
v: 1,
|
|
45
|
+
id: globalThis.crypto.randomUUID(),
|
|
46
|
+
notation,
|
|
47
|
+
dice,
|
|
48
|
+
modifier,
|
|
49
|
+
seed,
|
|
50
|
+
createdAt: Date.now()
|
|
51
|
+
};
|
|
52
|
+
if (opts.skin !== void 0) event.skin = opts.skin;
|
|
53
|
+
if (opts.rollerId !== void 0) event.rollerId = opts.rollerId;
|
|
54
|
+
if (opts.audience !== void 0) event.audience = opts.audience;
|
|
55
|
+
return event;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/core/event.ts
|
|
59
|
+
var MAX_EVENT_DICE = 2200;
|
|
60
|
+
var isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
61
|
+
var isStr = (x, min, max) => typeof x === "string" && x.length >= min && x.length <= max;
|
|
62
|
+
var isInt = (x, min, max) => typeof x === "number" && Number.isInteger(x) && x >= min && x <= max;
|
|
63
|
+
var isUnit = (x) => typeof x === "number" && x >= 0 && x <= 1;
|
|
64
|
+
var oneOf = (x, words) => typeof x === "string" && words.split(" ").includes(x);
|
|
65
|
+
var optional = (x, test) => x === void 0 || test(x);
|
|
66
|
+
var all = (list, test) => {
|
|
67
|
+
for (const x of list) if (!test(x)) return false;
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
var isColor = (x) => isStr(x, 0, 64);
|
|
71
|
+
var isMaterial = (m) => isObj(m) ? isUnit(m.metalness) && isUnit(m.roughness) && "clearcoat transmission tint sparkle".split(" ").every((k) => optional(m[k], isUnit)) : oneOf(m, "plastic metal wood glass stone gem");
|
|
72
|
+
var isSkin = (s) => isObj(s) ? isMaterial(s.material) && (Array.isArray(s.color) ? s.color.length === 2 && all(s.color, isColor) : isColor(s.color)) && isColor(s.labelColor) && optional(s.labelStyle, (x) => oneOf(x, "engraved printed embossed")) && optional(s.pattern, (x) => oneOf(x, "none gradient speckle marble wood swirl")) && optional(s.font, (x) => isStr(x, 1, 128)) : isStr(s, 1, 64);
|
|
73
|
+
var isId = (x) => isStr(x, 1, 128);
|
|
74
|
+
var isAudience = (a) => Array.isArray(a) ? a.length <= 100 && all(a, isId) : oneOf(a, "all dm");
|
|
75
|
+
function redact(event) {
|
|
76
|
+
const dice = event.dice.filter((die) => die.wave === 0).map((die) => ({ ...die, value: null }));
|
|
77
|
+
const copy = { ...event, dice };
|
|
78
|
+
const { audience, skin } = event;
|
|
79
|
+
if (Array.isArray(audience)) copy.audience = audience.slice();
|
|
80
|
+
if (typeof skin === "object") {
|
|
81
|
+
const { color } = skin;
|
|
82
|
+
copy.skin = { ...skin, color: typeof color === "string" ? color : [color[0], color[1]] };
|
|
83
|
+
}
|
|
84
|
+
return copy;
|
|
85
|
+
}
|
|
86
|
+
function check(e) {
|
|
87
|
+
if (!isObj(e) || e.v !== 1 || !isId(e.id) || typeof e.notation !== "string" || !isSeed(e.seed) || typeof e.createdAt !== "number" || !(e.createdAt >= 0 && e.createdAt < Infinity) || !optional(e.skin, isSkin) || !optional(e.rollerId, isId) || !optional(e.audience, isAudience)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const terms = [];
|
|
91
|
+
let modifier = 0;
|
|
92
|
+
for (const t of parse(e.notation).terms) {
|
|
93
|
+
if (t.kind === "dice") terms.push(t);
|
|
94
|
+
else modifier += t.sign * t.value;
|
|
95
|
+
}
|
|
96
|
+
const dice = e.dice;
|
|
97
|
+
if (e.modifier !== modifier || !Array.isArray(dice) || dice.length < 1 || dice.length > MAX_EVENT_DICE) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const W = MAX_WAVE + 1;
|
|
101
|
+
const count = new Array(terms.length * W).fill(0);
|
|
102
|
+
const maxed = count.slice();
|
|
103
|
+
const hidden = terms.map(() => false);
|
|
104
|
+
const get = (list, i) => list[i] ?? 0;
|
|
105
|
+
let lastWave = 0;
|
|
106
|
+
for (const die of dice) {
|
|
107
|
+
if (!isObj(die)) return false;
|
|
108
|
+
const { type, group, wave, value } = die;
|
|
109
|
+
if (!isInt(group, 0, terms.length - 1)) return false;
|
|
110
|
+
const term = terms[group];
|
|
111
|
+
if (!term || term.die !== type || !isInt(wave, lastWave, term.explode ? MAX_WAVE : 0)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
lastWave = wave;
|
|
115
|
+
const [min, max] = dieRange(term.die);
|
|
116
|
+
if (value === null) hidden[group] = true;
|
|
117
|
+
else if (!isInt(value, min, max)) return false;
|
|
118
|
+
const k = group * W + wave;
|
|
119
|
+
count[k] = get(count, k) + 1;
|
|
120
|
+
if (value === max) maxed[k] = get(maxed, k) + 1;
|
|
121
|
+
}
|
|
122
|
+
return terms.every((term, g) => {
|
|
123
|
+
const at = (list, w) => get(list, g * W + w);
|
|
124
|
+
if (at(count, 0) !== term.count) return false;
|
|
125
|
+
for (let w = 1; term.explode && w <= MAX_WAVE; w++) {
|
|
126
|
+
const limit = hidden[g] ? at(count, w - 1) : at(maxed, w - 1);
|
|
127
|
+
if (hidden[g] ? at(count, w) > limit : at(count, w) !== limit) return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function isRollEvent(input) {
|
|
133
|
+
try {
|
|
134
|
+
return check(input);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { createRoll, isRollEvent, redact };
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var render = require('pollyroll/render');
|
|
6
|
+
|
|
7
|
+
// src/react/index.ts
|
|
8
|
+
|
|
9
|
+
// src/react/skin.ts
|
|
10
|
+
var skinChanged = (next, last) => JSON.stringify(next) !== JSON.stringify(last);
|
|
11
|
+
|
|
12
|
+
// src/react/index.ts
|
|
13
|
+
function useDiceTray(ref, opts) {
|
|
14
|
+
const [tray, setTray] = react.useState(null);
|
|
15
|
+
const optsRef = react.useRef(opts);
|
|
16
|
+
optsRef.current = opts;
|
|
17
|
+
const skinRef = react.useRef(opts?.skin);
|
|
18
|
+
react.useEffect(() => {
|
|
19
|
+
const el = ref.current;
|
|
20
|
+
if (!el) return;
|
|
21
|
+
const created = render.createDiceTray(el, optsRef.current);
|
|
22
|
+
skinRef.current = optsRef.current?.skin;
|
|
23
|
+
setTray(created);
|
|
24
|
+
return () => {
|
|
25
|
+
created.dispose();
|
|
26
|
+
setTray(null);
|
|
27
|
+
};
|
|
28
|
+
}, [ref]);
|
|
29
|
+
const skin = opts?.skin;
|
|
30
|
+
react.useEffect(() => {
|
|
31
|
+
if (!tray || skin === void 0 || !skinChanged(skin, skinRef.current)) return;
|
|
32
|
+
skinRef.current = skin;
|
|
33
|
+
tray.setSkin(skin);
|
|
34
|
+
}, [tray, skin]);
|
|
35
|
+
return tray;
|
|
36
|
+
}
|
|
37
|
+
function DiceTray(props) {
|
|
38
|
+
const { trayRef, className, style, ...opts } = props;
|
|
39
|
+
const ref = react.useRef(null);
|
|
40
|
+
const tray = useDiceTray(ref, opts);
|
|
41
|
+
react.useImperativeHandle(trayRef, () => tray, [tray]);
|
|
42
|
+
return react.createElement("div", {
|
|
43
|
+
ref,
|
|
44
|
+
className,
|
|
45
|
+
style: { position: "absolute", inset: 0, pointerEvents: "none", ...style }
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exports.DiceTray = DiceTray;
|
|
50
|
+
exports.useDiceTray = useDiceTray;
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Ref, CSSProperties, ReactElement, RefObject } from 'react';
|
|
2
|
+
import { TrayOptions, DiceTray as DiceTray$1 } from 'pollyroll/render';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a tray on ref.current after mount and disposes it on unmount. Options are read once;
|
|
6
|
+
* only `skin` is reactive, applied when it changes by value (an undefined skin is ignored).
|
|
7
|
+
*/
|
|
8
|
+
declare function useDiceTray(ref: RefObject<HTMLElement | null>, opts?: TrayOptions): DiceTray$1 | null;
|
|
9
|
+
interface DiceTrayProps extends TrayOptions {
|
|
10
|
+
/** Receives the tray once created, and null after unmount. */
|
|
11
|
+
trayRef?: Ref<DiceTray$1 | null>;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Merged after the overlay defaults. */
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Overlay: a div styled position:absolute; inset:0; pointer-events:none, hosting the tray canvas.
|
|
18
|
+
* Tray options are read once on mount; only `skin` is reactive (applied when it changes by value).
|
|
19
|
+
*/
|
|
20
|
+
declare function DiceTray(props: DiceTrayProps): ReactElement;
|
|
21
|
+
|
|
22
|
+
export { DiceTray, type DiceTrayProps, useDiceTray };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Ref, CSSProperties, ReactElement, RefObject } from 'react';
|
|
2
|
+
import { TrayOptions, DiceTray as DiceTray$1 } from 'pollyroll/render';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a tray on ref.current after mount and disposes it on unmount. Options are read once;
|
|
6
|
+
* only `skin` is reactive, applied when it changes by value (an undefined skin is ignored).
|
|
7
|
+
*/
|
|
8
|
+
declare function useDiceTray(ref: RefObject<HTMLElement | null>, opts?: TrayOptions): DiceTray$1 | null;
|
|
9
|
+
interface DiceTrayProps extends TrayOptions {
|
|
10
|
+
/** Receives the tray once created, and null after unmount. */
|
|
11
|
+
trayRef?: Ref<DiceTray$1 | null>;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Merged after the overlay defaults. */
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Overlay: a div styled position:absolute; inset:0; pointer-events:none, hosting the tray canvas.
|
|
18
|
+
* Tray options are read once on mount; only `skin` is reactive (applied when it changes by value).
|
|
19
|
+
*/
|
|
20
|
+
declare function DiceTray(props: DiceTrayProps): ReactElement;
|
|
21
|
+
|
|
22
|
+
export { DiceTray, type DiceTrayProps, useDiceTray };
|