@foxystar/molang 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.
Files changed (44) hide show
  1. package/README.md +82 -0
  2. package/esm/LRUCache.d.ts +6 -0
  3. package/esm/LRUCache.js +30 -0
  4. package/esm/diagnostics/error.d.ts +12 -0
  5. package/esm/diagnostics/error.js +37 -0
  6. package/esm/diagnostics/suggestions.d.ts +2 -0
  7. package/esm/diagnostics/suggestions.js +35 -0
  8. package/esm/index.d.ts +8 -0
  9. package/esm/index.js +6 -0
  10. package/esm/molang.d.ts +2 -0
  11. package/esm/molang.js +22 -0
  12. package/esm/package.json +3 -0
  13. package/esm/parser/expression.d.ts +87 -0
  14. package/esm/parser/expression.js +19 -0
  15. package/esm/parser/parser.d.ts +20 -0
  16. package/esm/parser/parser.js +490 -0
  17. package/esm/runtime/context.d.ts +19 -0
  18. package/esm/runtime/context.js +51 -0
  19. package/esm/runtime/math.d.ts +32 -0
  20. package/esm/runtime/math.js +89 -0
  21. package/esm/runtime/runtime.d.ts +34 -0
  22. package/esm/runtime/runtime.js +537 -0
  23. package/package.json +39 -0
  24. package/script/LRUCache.d.ts +6 -0
  25. package/script/LRUCache.js +33 -0
  26. package/script/diagnostics/error.d.ts +12 -0
  27. package/script/diagnostics/error.js +42 -0
  28. package/script/diagnostics/suggestions.d.ts +2 -0
  29. package/script/diagnostics/suggestions.js +40 -0
  30. package/script/index.d.ts +8 -0
  31. package/script/index.js +27 -0
  32. package/script/molang.d.ts +2 -0
  33. package/script/molang.js +29 -0
  34. package/script/package.json +3 -0
  35. package/script/parser/expression.d.ts +87 -0
  36. package/script/parser/expression.js +22 -0
  37. package/script/parser/parser.d.ts +20 -0
  38. package/script/parser/parser.js +494 -0
  39. package/script/runtime/context.d.ts +19 -0
  40. package/script/runtime/context.js +57 -0
  41. package/script/runtime/math.d.ts +32 -0
  42. package/script/runtime/math.js +92 -0
  43. package/script/runtime/runtime.d.ts +34 -0
  44. package/script/runtime/runtime.js +567 -0
@@ -0,0 +1,490 @@
1
+ import { MolangParseError } from "../diagnostics/error.js";
2
+ import { MolangRuntime } from "../runtime/runtime.js";
3
+ import { TOKEN_REGEX, } from "./expression.js";
4
+ export class MolangParser {
5
+ constructor(input) {
6
+ Object.defineProperty(this, "input", {
7
+ enumerable: true,
8
+ configurable: true,
9
+ writable: true,
10
+ value: input
11
+ });
12
+ Object.defineProperty(this, "tokens", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: []
17
+ });
18
+ Object.defineProperty(this, "position", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: 0
23
+ });
24
+ this.tokenize();
25
+ }
26
+ tokenize() {
27
+ let str = this.input;
28
+ let offset = 0;
29
+ while (str.length > 0) {
30
+ const match = TOKEN_REGEX.exec(str);
31
+ if (match === null || match.index !== 0) {
32
+ throw new MolangParseError(`Unexpected token '${str[0]}'`, offset, this.input);
33
+ }
34
+ const raw = match[0];
35
+ for (const type of Object.keys(match.groups)) {
36
+ if (match.groups[type]) {
37
+ if (type !== "whitespace") {
38
+ this.tokens.push({
39
+ type: type, value: raw,
40
+ start: offset,
41
+ end: offset + raw.length
42
+ });
43
+ }
44
+ offset += raw.length;
45
+ str = str.slice(raw.length);
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ this.tokens.push({
51
+ type: "eof", value: "",
52
+ start: offset, end: offset
53
+ });
54
+ }
55
+ peek() {
56
+ return this.tokens[this.position];
57
+ }
58
+ consume(expectedType) {
59
+ const token = this.tokens[this.position++];
60
+ if (expectedType && token.type !== expectedType) {
61
+ throw new Error(`Expected token type ${expectedType}, but got ${token.type}`);
62
+ }
63
+ return token;
64
+ }
65
+ parseProgram() {
66
+ const body = [];
67
+ const start = this.peek().start;
68
+ while (this.peek().type !== "eof") {
69
+ body.push(this.parseStatement());
70
+ if (this.peek().type === "semicolon") {
71
+ this.consume("semicolon");
72
+ }
73
+ else if (this.peek().type !== "eof") {
74
+ throw new MolangParseError("Expected ';' between statements", this.peek().start, this.input);
75
+ }
76
+ }
77
+ const end = this.peek().end;
78
+ return { type: "Program", body, start, end };
79
+ }
80
+ parseStatement() {
81
+ const token = this.peek();
82
+ // loop(...)
83
+ if (token.type === "identifier" && token.value === "loop") {
84
+ const start = token.start;
85
+ this.consume("identifier"); // loop
86
+ this.consume("parenthesis"); // (
87
+ const count = this.parseExpression();
88
+ if (this.peek().type !== "comma") {
89
+ throw new MolangParseError("Expected ',' after loop count", this.peek().start, this.input);
90
+ }
91
+ this.consume("comma");
92
+ // body is a STATEMENT, not an expression
93
+ const body = this.parseStatement();
94
+ this.consume("parenthesis"); // )
95
+ return {
96
+ type: "LoopStatement",
97
+ count,
98
+ body,
99
+ start,
100
+ end: body.end
101
+ };
102
+ }
103
+ // for_each(...)
104
+ if (token.type === "identifier" && token.value === "for_each") {
105
+ const start = token.start;
106
+ this.consume("identifier"); // for_each
107
+ this.consume("parenthesis"); // (
108
+ // iterator (must be assignable)
109
+ const iterator = this.parseExpression();
110
+ if (iterator.type !== "Identifier" &&
111
+ iterator.type !== "IndexExpression") {
112
+ throw new MolangParseError("'for_each' iterator must be an assignable variable", iterator.start, this.input);
113
+ }
114
+ if (this.peek().type !== "comma") {
115
+ throw new MolangParseError("Expected ',' after for_each iterator", this.peek().start, this.input);
116
+ }
117
+ this.consume("comma");
118
+ const iterable = this.parseExpression();
119
+ if (this.peek().type !== "comma") {
120
+ throw new MolangParseError("Expected ',' after for_each iterable", this.peek().start, this.input);
121
+ }
122
+ this.consume("comma");
123
+ // body is a STATEMENT
124
+ const body = this.parseStatement();
125
+ this.consume("parenthesis"); // )
126
+ return {
127
+ type: "ForEachStatement",
128
+ iterator,
129
+ iterable,
130
+ body,
131
+ start,
132
+ end: body.end
133
+ };
134
+ }
135
+ if (token.type === "brace" && token.value === "{") {
136
+ const start = token.start;
137
+ this.consume("brace"); // {
138
+ const body = [];
139
+ while (!(this.peek().type === "brace" && this.peek().value === "}")) {
140
+ body.push(this.parseStatement());
141
+ if (this.peek().type === "semicolon") {
142
+ this.consume("semicolon");
143
+ }
144
+ }
145
+ const end = this.consume("brace").end; // }
146
+ return { type: "BlockStatement", body, start, end };
147
+ }
148
+ if (token.type === "identifier" && token.value === "return") {
149
+ const start = token.start;
150
+ this.consume("identifier");
151
+ // return;
152
+ if (this.peek().type === "semicolon" || this.peek().type === "eof") {
153
+ return {
154
+ type: "ReturnStatement",
155
+ start,
156
+ end: token.end
157
+ };
158
+ }
159
+ // return <expr>
160
+ const value = this.parseExpression();
161
+ return {
162
+ type: "ReturnStatement",
163
+ value,
164
+ start,
165
+ end: value.end
166
+ };
167
+ }
168
+ // break
169
+ if (token.type === "identifier" && token.value === "break") {
170
+ const t = this.consume("identifier");
171
+ return { type: "BreakStatement", start: t.start, end: t.end };
172
+ }
173
+ // continue
174
+ if (token.type === "identifier" && token.value === "continue") {
175
+ const t = this.consume("identifier");
176
+ return { type: "ContinueStatement", start: t.start, end: t.end };
177
+ }
178
+ const expr = this.parseBinaryExpression();
179
+ if (this.peek().type === "operator" && this.peek().value === "?") {
180
+ this.consume("operator");
181
+ // Only allow a STATEMENT here
182
+ const thenStmt = this.parseStatement();
183
+ let elseStmt;
184
+ if (this.peek().type === "operator" && this.peek().value === ":") {
185
+ this.consume("operator"); // :
186
+ elseStmt = this.parseStatement();
187
+ }
188
+ return {
189
+ type: "ConditionalStatement",
190
+ condition: expr,
191
+ then: thenStmt,
192
+ else: elseStmt,
193
+ start: expr.start,
194
+ end: (elseStmt ?? thenStmt).end
195
+ };
196
+ }
197
+ // Assignment
198
+ if (this.peek().type === "assignment") {
199
+ this.consume("assignment");
200
+ const value = this.parseExpression();
201
+ return {
202
+ type: "AssignStatement",
203
+ target: expr,
204
+ value,
205
+ start: expr.start,
206
+ end: value.end
207
+ };
208
+ }
209
+ // Expression statement
210
+ return {
211
+ type: "ExprStatement",
212
+ expr,
213
+ start: expr.start,
214
+ end: expr.end
215
+ };
216
+ }
217
+ parseExpression() {
218
+ return this.parseConditionalExpression();
219
+ }
220
+ parseBinaryExpression(precedence = 0) {
221
+ let left = this.parsePrimaryExpression();
222
+ while (true) {
223
+ const token = this.peek();
224
+ if (token.type === "comma" || token.type === "parenthesis" || token.type === "eof") {
225
+ break;
226
+ }
227
+ const tokenPrecedence = this.getOperatorPrecedence(token.value);
228
+ if (tokenPrecedence < precedence) {
229
+ break;
230
+ }
231
+ this.consume("operator");
232
+ const right = this.parseBinaryExpression(tokenPrecedence + 1);
233
+ if (token.value === "->") {
234
+ left = {
235
+ type: "ArrowExpression",
236
+ left, right,
237
+ start: left.start,
238
+ end: right.end
239
+ };
240
+ continue; // Skip
241
+ }
242
+ if (left.type === "Literal" && right.type === "Literal") {
243
+ const folded = this.tryFold(token.value, left.value, right.value);
244
+ if (folded !== undefined) {
245
+ left = {
246
+ type: "Literal",
247
+ value: folded,
248
+ start: left.start,
249
+ end: right.end
250
+ };
251
+ continue; // keep parsing higher-precedence ops
252
+ }
253
+ }
254
+ left = {
255
+ type: "BinaryExpression",
256
+ operator: token.value,
257
+ left, right,
258
+ start: left.start,
259
+ end: right.end
260
+ };
261
+ }
262
+ return left;
263
+ }
264
+ tryFold(op, left, right) {
265
+ if (typeof left !== "number" || typeof right !== "number") {
266
+ return;
267
+ }
268
+ switch (op) {
269
+ case "+": return left + right;
270
+ case "-": return left - right;
271
+ case "*": return left * right;
272
+ case "/": return left / right;
273
+ case "<": return left < right;
274
+ case ">": return left > right;
275
+ case "<=": return left <= right;
276
+ case ">=": return left >= right;
277
+ }
278
+ return;
279
+ }
280
+ getOperatorPrecedence(operator) {
281
+ switch (operator) {
282
+ case "?": return -1; // handled outside binary parsing
283
+ case ":": return -1;
284
+ case "||": return 1;
285
+ case "&&": return 2;
286
+ case "??": return 0;
287
+ case "|": return 2;
288
+ case "^": return 3;
289
+ case "&": return 4;
290
+ case "==":
291
+ case "!=":
292
+ case "<":
293
+ case ">":
294
+ case "<=":
295
+ case ">=":
296
+ return 5;
297
+ case "<<":
298
+ case ">>":
299
+ case ">>>":
300
+ return 6;
301
+ case "+":
302
+ case "-":
303
+ return 7;
304
+ case "*":
305
+ case "/":
306
+ return 8;
307
+ case "->":
308
+ return 9;
309
+ default:
310
+ return -1;
311
+ }
312
+ }
313
+ parseConditionalExpression() {
314
+ const condition = this.parseBinaryExpression();
315
+ if (this.peek().type === "operator" &&
316
+ this.peek().value === "?") {
317
+ this.consume("operator"); // ?
318
+ const then = this.parseExpression();
319
+ // A ? B : C
320
+ if (this.peek().type === "operator" &&
321
+ this.peek().value === ":") {
322
+ this.consume("operator"); // :
323
+ const elseStmt = this.parseExpression();
324
+ return {
325
+ type: "ConditionalExpression",
326
+ condition,
327
+ then,
328
+ else: elseStmt,
329
+ start: condition.start,
330
+ end: elseStmt.end
331
+ };
332
+ }
333
+ // Binary condition (A ? B)
334
+ return {
335
+ type: "ConditionalExpression",
336
+ condition,
337
+ then,
338
+ start: condition.start,
339
+ end: then.end
340
+ };
341
+ }
342
+ return condition;
343
+ }
344
+ parsePrimaryExpression() {
345
+ return this.parseUnary();
346
+ }
347
+ parseUnary() {
348
+ const token = this.peek();
349
+ if (token.type === "operator" && (token.value === "-" || token.value === "!")) {
350
+ this.consume("operator");
351
+ const expr = this.parseUnary();
352
+ return {
353
+ type: "UnaryExpression",
354
+ operator: token.value,
355
+ expression: expr,
356
+ start: token.start,
357
+ end: expr.end
358
+ };
359
+ }
360
+ return this.parseAtom();
361
+ }
362
+ parseAtom() {
363
+ const token = this.peek();
364
+ if (token.type === "number") {
365
+ this.consume("number");
366
+ return {
367
+ type: "Literal",
368
+ value: Number(token.value),
369
+ start: token.start,
370
+ end: token.end
371
+ };
372
+ }
373
+ if (token.type === "string") {
374
+ this.consume("string");
375
+ return {
376
+ type: "Literal",
377
+ value: token.value.slice(1, -1),
378
+ start: token.start,
379
+ end: token.end
380
+ };
381
+ }
382
+ if (token.type === "bracket" && token.value === "[") {
383
+ const start = token.start;
384
+ this.consume("bracket");
385
+ const elements = [];
386
+ while (!(this.peek().type === "bracket" && this.peek().value === "]")) {
387
+ elements.push(this.parseExpression());
388
+ if (this.peek().type !== "comma") {
389
+ break;
390
+ }
391
+ this.consume("comma");
392
+ }
393
+ const close = this.consume("bracket");
394
+ return {
395
+ type: "ArrayLiteral",
396
+ elements,
397
+ start, end: close.end
398
+ };
399
+ }
400
+ if (token.type === "identifier" && (token.value === "true" || token.value === "false")) {
401
+ this.consume("identifier");
402
+ return {
403
+ type: "Literal",
404
+ value: token.value === "true",
405
+ start: token.start,
406
+ end: token.end
407
+ };
408
+ }
409
+ if (token.type === "identifier") {
410
+ this.consume("identifier");
411
+ const path = MolangRuntime.normalizePath(token.value);
412
+ let expr = {
413
+ type: "Identifier",
414
+ name: token.value,
415
+ path,
416
+ start: token.start,
417
+ end: token.end
418
+ };
419
+ // Check if this is a function call
420
+ if (this.peek().type === "parenthesis" && this.peek().value === "(") {
421
+ this.consume("parenthesis");
422
+ const args = [];
423
+ while (true) {
424
+ const next = this.peek();
425
+ if (next.type === "eof") {
426
+ throw new MolangParseError("Unclosed function call", token.start, this.input);
427
+ }
428
+ if (next.type === "parenthesis" && next.value === ")") {
429
+ break;
430
+ }
431
+ if (next.type === "comma") {
432
+ throw new MolangParseError(`Unexpected token ','`, next.start, this.input);
433
+ }
434
+ args.push(this.parseExpression());
435
+ const afterArg = this.peek();
436
+ if (afterArg.type === "comma") {
437
+ this.consume("comma");
438
+ const lookahead = this.peek();
439
+ if (lookahead.type === "parenthesis" && lookahead.value === ")") {
440
+ throw new MolangParseError(`Unexpected token ')'`, lookahead.start, this.input);
441
+ }
442
+ }
443
+ else if (!(afterArg.type === "parenthesis" && afterArg.value === ")")) {
444
+ throw new MolangParseError(`Unexpected token '${afterArg.value}'`, afterArg.start, this.input);
445
+ }
446
+ }
447
+ this.consume("parenthesis");
448
+ expr = {
449
+ type: "CallExpression",
450
+ callee: expr,
451
+ arguments: args,
452
+ start: expr.start,
453
+ end: this.tokens[this.position - 1].end
454
+ };
455
+ }
456
+ while (this.peek().type === "bracket" && this.peek().value === "[") {
457
+ this.consume("bracket");
458
+ const index = this.parseExpression();
459
+ const close = this.consume("bracket");
460
+ expr = {
461
+ type: "IndexExpression",
462
+ array: expr,
463
+ index,
464
+ start: expr.start,
465
+ end: close.end
466
+ };
467
+ }
468
+ return expr;
469
+ }
470
+ if (token.type === "parenthesis" && token.value === "(") {
471
+ this.consume("parenthesis");
472
+ let expr = this.parseExpression();
473
+ this.consume("parenthesis");
474
+ while (this.peek().type === "bracket" && this.peek().value === "[") {
475
+ this.consume("bracket");
476
+ const index = this.parseExpression();
477
+ const close = this.consume("bracket");
478
+ expr = {
479
+ type: "IndexExpression",
480
+ array: expr,
481
+ index,
482
+ start: expr.start,
483
+ end: close.end
484
+ };
485
+ }
486
+ return expr;
487
+ }
488
+ throw new Error(`Unexpected token: ${token.value}`);
489
+ }
490
+ }
@@ -0,0 +1,19 @@
1
+ export type MolangPrimitive = number | string | boolean;
2
+ export type MolangArray = MolangPrimitive[];
3
+ export type MolangValue = MolangPrimitive | MolangArray;
4
+ export type MolangFunction = (...args: never[]) => unknown;
5
+ export type MolangBinding = MolangValue | MolangFunction | MolangNamespace | undefined;
6
+ export type MolangNamespace = {
7
+ [key: string]: MolangBinding;
8
+ };
9
+ export interface MolangContext extends MolangNamespace {
10
+ math?: MolangNamespace;
11
+ variable?: MolangNamespace;
12
+ temp?: MolangNamespace;
13
+ query?: MolangNamespace;
14
+ context?: MolangNamespace;
15
+ }
16
+ export declare const DEFAULT_CONTEXT: MolangContext;
17
+ export declare function createMolangContext(initial?: MolangContext): MolangContext;
18
+ export declare function isNamespace(value: unknown): value is MolangNamespace;
19
+ export declare function mergeContext(base: MolangNamespace, user: MolangNamespace): MolangNamespace;
@@ -0,0 +1,51 @@
1
+ import { MolangMath } from "./math.js";
2
+ import { PerlinNoise } from "@foxystar/math";
3
+ ;
4
+ const PERLIN = new PerlinNoise();
5
+ export const DEFAULT_CONTEXT = {
6
+ math: MolangMath,
7
+ variable: {},
8
+ temp: {},
9
+ context: {},
10
+ query: {
11
+ block_state: (_name) => {
12
+ throw new Error("This function call can only be used in block context");
13
+ },
14
+ noise: (x, y) => {
15
+ return PERLIN.noise({ x, y });
16
+ },
17
+ },
18
+ };
19
+ export function createMolangContext(initial) {
20
+ const ctx = mergeContext(DEFAULT_CONTEXT, initial ?? {});
21
+ ctx.variable = {
22
+ ...(initial?.variable ?? {}),
23
+ };
24
+ ctx.temp = {
25
+ ...(initial?.temp ?? {}),
26
+ };
27
+ return ctx;
28
+ }
29
+ const LOCKED_KEYS = new Set(["math"]);
30
+ export function isNamespace(value) {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+ export function mergeContext(base, user) {
34
+ const out = Object.create(base);
35
+ for (const key in user) {
36
+ if (base === DEFAULT_CONTEXT && LOCKED_KEYS.has(key)) {
37
+ throw new Error(`Cannot override core Molang namespace '${key}'`);
38
+ }
39
+ const userValue = user[key];
40
+ const baseValue = base[key];
41
+ const baseIsNamespace = isNamespace(baseValue);
42
+ const userIsNamespace = isNamespace(userValue);
43
+ if (baseIsNamespace && userIsNamespace) {
44
+ out[key] = mergeContext(baseValue, userValue);
45
+ }
46
+ else {
47
+ out[key] = userValue;
48
+ }
49
+ }
50
+ return out;
51
+ }
@@ -0,0 +1,32 @@
1
+ export declare const MolangMath: {
2
+ pi: number;
3
+ abs: (x: number) => number;
4
+ ceil: (x: number) => number;
5
+ exp: (x: number) => number;
6
+ ln: (x: number) => number;
7
+ floor: (x: number) => number;
8
+ pow: (x: number, y: number) => number;
9
+ round: (x: number) => number;
10
+ sign: (x: number) => number;
11
+ sqrt: (x: number) => number;
12
+ trunc: (x: number) => number;
13
+ clamp: (value: number, min: number, max: number) => number;
14
+ max: (...values: number[]) => number;
15
+ min: (...values: number[]) => number;
16
+ cos: (v: number) => number;
17
+ acos: (v: number) => number;
18
+ sin: (v: number) => number;
19
+ asin: (v: number) => number;
20
+ atan: (v: number) => number;
21
+ atan2: (y: number, x: number) => number;
22
+ copy_sign: (x: number, y: number) => number;
23
+ random: (low: number, high: number) => number;
24
+ random_integer: (low: number, high: number) => number;
25
+ die_roll: (num: number, low: number, high: number) => number;
26
+ die_roll_integer: (num: number, low: number, high: number) => number;
27
+ hermite_blend: (t: number) => number;
28
+ lerp: (start: number, end: number, t: number) => number;
29
+ lerprotate: (start: number, end: number, t: number) => number;
30
+ min_angle: (value: number) => number;
31
+ mod: (value: number, denom: number) => number;
32
+ };
@@ -0,0 +1,89 @@
1
+ import { XoroshiroRandom } from "@foxystar/math";
2
+ const rng = new XoroshiroRandom();
3
+ const DEG_TO_RAD = Math.PI / 180;
4
+ const RAD_TO_DEG = 180 / Math.PI;
5
+ function randomRange(low, high) {
6
+ return low + rng.nextDouble() * (high - low);
7
+ }
8
+ function randomIntRange(low, high) {
9
+ if (high < low) {
10
+ return low;
11
+ }
12
+ return low + rng.nextInt(high - low + 1);
13
+ }
14
+ export const MolangMath = {
15
+ pi: Math.PI,
16
+ abs: Math.abs,
17
+ ceil: Math.ceil,
18
+ exp: Math.exp,
19
+ ln: Math.log,
20
+ floor: Math.floor,
21
+ pow: Math.pow,
22
+ round: Math.round,
23
+ sign: Math.sign,
24
+ sqrt: Math.sqrt,
25
+ trunc: Math.trunc,
26
+ clamp: (value, min, max) => {
27
+ return Math.min(Math.max(value, min), max);
28
+ },
29
+ max: Math.max,
30
+ min: Math.min,
31
+ cos: (v) => {
32
+ return Math.cos(v * DEG_TO_RAD);
33
+ },
34
+ acos: (v) => {
35
+ return Math.acos(v) * RAD_TO_DEG;
36
+ },
37
+ sin: (v) => {
38
+ return Math.sin(v * DEG_TO_RAD);
39
+ },
40
+ asin: (v) => {
41
+ return Math.asin(v) * RAD_TO_DEG;
42
+ },
43
+ atan: (v) => {
44
+ return Math.atan(v) * RAD_TO_DEG;
45
+ },
46
+ atan2: (y, x) => {
47
+ return Math.atan2(y, x) * RAD_TO_DEG;
48
+ },
49
+ copy_sign: (x, y) => {
50
+ return Math.abs(x) * Math.sign(y);
51
+ },
52
+ random: (low, high) => {
53
+ return randomRange(low, high);
54
+ },
55
+ random_integer: (low, high) => {
56
+ return randomIntRange(low, high);
57
+ },
58
+ die_roll: (num, low, high) => {
59
+ let sum = 0;
60
+ for (let i = 0; i < num; i++) {
61
+ sum += randomRange(low, high);
62
+ }
63
+ return sum;
64
+ },
65
+ die_roll_integer: (num, low, high) => {
66
+ let sum = 0;
67
+ for (let i = 0; i < num; i++) {
68
+ sum += randomIntRange(low, high);
69
+ }
70
+ return sum;
71
+ },
72
+ hermite_blend: (t) => {
73
+ return 3 * t * t - 2 * t * t * t;
74
+ },
75
+ lerp: (start, end, t) => {
76
+ return start + (end - start) * t;
77
+ },
78
+ lerprotate: (start, end, t) => {
79
+ const diff = ((end - start + 540) % 360) - 180;
80
+ return start + diff * t;
81
+ },
82
+ min_angle: (value) => {
83
+ const angle = ((value + 180) % 360 + 360) % 360 - 180;
84
+ return angle;
85
+ },
86
+ mod: (value, denom) => {
87
+ return ((value % denom) + denom) % denom;
88
+ },
89
+ };