littlejsengine 1.15.9 → 1.16.2

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 (54) hide show
  1. package/dist/littlejs.d.ts +154 -158
  2. package/dist/littlejs.esm.js +573 -570
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +553 -553
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +392 -396
  7. package/examples/box2d/game.js +1 -1
  8. package/examples/box2d/gameObjects.js +5 -5
  9. package/examples/breakout/game.js +3 -3
  10. package/examples/electron/game.js +1 -2
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +0 -1
  13. package/examples/index.html +1 -1
  14. package/examples/logo.png +0 -0
  15. package/examples/logo2.png +0 -0
  16. package/examples/module/game.js +1 -2
  17. package/examples/particles/index.html +1 -1
  18. package/examples/platformer/game.js +4 -4
  19. package/examples/platformer/gameEffects.js +1 -1
  20. package/examples/puzzle/game.js +2 -2
  21. package/examples/shorts/base.html +4 -3
  22. package/examples/shorts/box2dPool.js +2 -2
  23. package/examples/shorts/empty.js +1 -1
  24. package/examples/shorts/{systemFont.js → fontImage.js} +3 -3
  25. package/examples/shorts/fps.js +1 -1
  26. package/examples/shorts/helloWorld.js +2 -2
  27. package/examples/shorts/nineSlice.js +2 -2
  28. package/examples/shorts/slidingPuzzle.js +1 -1
  29. package/examples/starter/game.js +1 -2
  30. package/examples/starter/index.html +3 -2
  31. package/examples/stress/index.html +1 -1
  32. package/examples/typescript/game.js +1 -2
  33. package/examples/typescript/game.ts +1 -2
  34. package/package.json +1 -1
  35. package/plugins/box2d.js +8 -8
  36. package/plugins/drawUtilities.js +6 -6
  37. package/plugins/postProcess.js +13 -20
  38. package/plugins/uiSystem.js +16 -4
  39. package/reference.md +9 -11
  40. package/src/engine.js +40 -54
  41. package/src/engineAudio.js +5 -7
  42. package/src/engineBuild.js +1 -0
  43. package/src/engineDebug.js +163 -161
  44. package/src/engineDraw.js +108 -130
  45. package/src/engineExport.js +20 -17
  46. package/src/engineInput.js +4 -7
  47. package/src/engineMath.js +1201 -0
  48. package/src/engineMedals.js +7 -7
  49. package/src/engineObject.js +5 -4
  50. package/src/engineRelease.js +2 -4
  51. package/src/engineSettings.js +22 -32
  52. package/src/engineTileLayer.js +9 -10
  53. package/src/engineUtilities.js +34 -1187
  54. package/src/engineWebGL.js +13 -15
@@ -0,0 +1,1201 @@
1
+ /**
2
+ * LittleJS Utility Classes and Functions
3
+ * - General purpose math library
4
+ * - Vector2 - fast, simple, easy 2D vector class
5
+ * - Color - holds a rgba color with some math functions
6
+ * - Timer - tracks time automatically
7
+ * - RandomGenerator - seeded random number generator
8
+ * @namespace Math
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ /** The value of PI
14
+ * @type {number}
15
+ * @default Math.PI
16
+ * @memberof Math */
17
+ const PI = Math.PI;
18
+
19
+ /** Returns absolute value of value passed in
20
+ * @param {number} value
21
+ * @return {number}
22
+ * @memberof Math */
23
+ const abs = Math.abs;
24
+
25
+ /** Returns floored value of value passed in
26
+ * @param {number} value
27
+ * @return {number}
28
+ * @memberof Math */
29
+ const floor = Math.floor;
30
+
31
+ /** Returns ceiled value of value passed in
32
+ * @param {number} value
33
+ * @return {number}
34
+ * @memberof Math */
35
+ const ceil = Math.ceil;
36
+
37
+ /** Returns rounded value passed in
38
+ * @param {number} value
39
+ * @return {number}
40
+ * @memberof Math */
41
+ const round = Math.round;
42
+
43
+ /** Returns lowest value passed in
44
+ * @param {...number} values
45
+ * @return {number}
46
+ * @memberof Math */
47
+ const min = Math.min;
48
+
49
+ /** Returns highest value passed in
50
+ * @param {...number} values
51
+ * @return {number}
52
+ * @memberof Math */
53
+ const max = Math.max;
54
+
55
+ /** Returns the sign of value passed in
56
+ * @param {number} value
57
+ * @return {number}
58
+ * @memberof Math */
59
+ const sign = Math.sign;
60
+
61
+ /** Returns hypotenuse of values passed in
62
+ * @param {...number} values
63
+ * @return {number}
64
+ * @memberof Math */
65
+ const hypot = Math.hypot;
66
+
67
+ /** Returns log2 of value passed in
68
+ * @param {number} value
69
+ * @return {number}
70
+ * @memberof Math */
71
+ const log2 = Math.log2;
72
+
73
+ /** Returns sin of value passed in
74
+ * @param {number} value
75
+ * @return {number}
76
+ * @memberof Math */
77
+ const sin = Math.sin;
78
+
79
+ /** Returns cos of value passed in
80
+ * @param {number} value
81
+ * @return {number}
82
+ * @memberof Math */
83
+ const cos = Math.cos;
84
+
85
+ /** Returns tan of value passed in
86
+ * @param {number} value
87
+ * @return {number}
88
+ * @memberof Math */
89
+ const tan = Math.tan;
90
+
91
+ /** Returns atan2 of values passed in
92
+ * @param {number} y
93
+ * @param {number} x
94
+ * @return {number}
95
+ * @memberof Math */
96
+ const atan2 = Math.atan2;
97
+
98
+ /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
99
+ * @param {number} dividend
100
+ * @param {number} [divisor]
101
+ * @return {number}
102
+ * @memberof Math */
103
+ function mod(dividend, divisor=1) { return ((dividend % divisor) + divisor) % divisor; }
104
+
105
+ /** Clamps the value between max and min
106
+ * @param {number} value
107
+ * @param {number} [min]
108
+ * @param {number} [max]
109
+ * @return {number}
110
+ * @memberof Math */
111
+ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? max : value; }
112
+
113
+ /** Returns what percentage the value is between valueA and valueB
114
+ * @param {number} value
115
+ * @param {number} valueA
116
+ * @param {number} valueB
117
+ * @return {number}
118
+ * @memberof Math */
119
+ function percent(value, valueA, valueB)
120
+ { return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
121
+
122
+ /** Linearly interpolates between values passed in using percent
123
+ * @param {number} valueA
124
+ * @param {number} valueB
125
+ * @param {number} percent
126
+ * @return {number}
127
+ * @memberof Math */
128
+ function lerp(valueA, valueB, percent)
129
+ { return valueA + clamp(percent) * (valueB-valueA); }
130
+
131
+ /** Gets percent between percentA and percentB and linearly interpolates between lerpA and lerpB
132
+ * A shortcut for lerp(lerpA, lerpB, percent(value, percentA, percentB))
133
+ * @param {number} value
134
+ * @param {number} percentA
135
+ * @param {number} percentB
136
+ * @param {number} lerpA
137
+ * @param {number} lerpB
138
+ * @return {number}
139
+ * @memberof Math */
140
+ function percentLerp(value, percentA, percentB, lerpA, lerpB)
141
+ { return lerp(lerpA, lerpB, percent(value, percentA, percentB)); }
142
+
143
+ /** Returns signed wrapped distance between the two values passed in
144
+ * @param {number} valueA
145
+ * @param {number} valueB
146
+ * @param {number} [wrapSize]
147
+ * @return {number}
148
+ * @memberof Math */
149
+ function distanceWrap(valueA, valueB, wrapSize=1)
150
+ { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
151
+
152
+ /** Linearly interpolates between values passed in with wrapping
153
+ * @param {number} valueA
154
+ * @param {number} valueB
155
+ * @param {number} percent
156
+ * @param {number} [wrapSize]
157
+ * @return {number}
158
+ * @memberof Math */
159
+ function lerpWrap(valueA, valueB, percent, wrapSize=1)
160
+ { return valueA + clamp(percent) * distanceWrap(valueB, valueA, wrapSize); }
161
+
162
+ /** Returns signed wrapped distance between the two angles passed in
163
+ * @param {number} angleA
164
+ * @param {number} angleB
165
+ * @return {number}
166
+ * @memberof Math */
167
+ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
168
+
169
+ /** Linearly interpolates between the angles passed in with wrapping
170
+ * @param {number} angleA
171
+ * @param {number} angleB
172
+ * @param {number} percent
173
+ * @return {number}
174
+ * @memberof Math */
175
+ function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
176
+
177
+ /** Applies smoothstep function to the percentage value
178
+ * @param {number} percent
179
+ * @return {number}
180
+ * @memberof Math */
181
+ function smoothStep(percent) { return percent * percent * (3 - 2 * percent); }
182
+
183
+ /** Checks if the value passed in is a power of two
184
+ * @param {number} value
185
+ * @return {boolean}
186
+ * @memberof Math */
187
+ function isPowerOfTwo(value) { return !(value & (value - 1)); }
188
+
189
+ /** Returns the nearest power of two not less than the value
190
+ * @param {number} value
191
+ * @return {number}
192
+ * @memberof Math */
193
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
194
+
195
+ /** Returns true if two axis aligned bounding boxes are overlapping
196
+ * this can be used for simple collision detection between objects
197
+ * @param {Vector2} posA - Center of box A
198
+ * @param {Vector2} sizeA - Size of box A
199
+ * @param {Vector2} posB - Center of box B
200
+ * @param {Vector2} [sizeB=(0,0)] - Size of box B, uses a point if undefined
201
+ * @return {boolean} - True if overlapping
202
+ * @memberof Math */
203
+ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
204
+ {
205
+ const dx = (posA.x - posB.x)*2;
206
+ const dy = (posA.y - posB.y)*2;
207
+ const sx = sizeA.x + sizeB.x;
208
+ const sy = sizeA.y + sizeB.y;
209
+ return dx >= -sx && dx < sx && dy >= -sy && dy < sy;
210
+ }
211
+
212
+ /** Returns true if a line segment is intersecting an axis aligned box
213
+ * @param {Vector2} start - Start of raycast
214
+ * @param {Vector2} end - End of raycast
215
+ * @param {Vector2} pos - Center of box
216
+ * @param {Vector2} size - Size of box
217
+ * @return {boolean} - True if intersecting
218
+ * @memberof Math */
219
+ function isIntersecting(start, end, pos, size)
220
+ {
221
+ // Liang-Barsky algorithm
222
+ const boxMin = pos.subtract(size.scale(.5));
223
+ const boxMax = boxMin.add(size);
224
+ const delta = end.subtract(start);
225
+ const a = start.subtract(boxMin);
226
+ const b = start.subtract(boxMax);
227
+ const p = [-delta.x, delta.x, -delta.y, delta.y];
228
+ const q = [a.x, -b.x, a.y, -b.y];
229
+ let tMin = 0, tMax = 1;
230
+ for (let i = 4; i--;)
231
+ {
232
+ if (p[i])
233
+ {
234
+ const t = q[i] / p[i];
235
+ if (p[i] < 0)
236
+ {
237
+ if (t > tMax) return false;
238
+ tMin = max(t, tMin);
239
+ }
240
+ else
241
+ {
242
+ if (t < tMin) return false;
243
+ tMax = min(t, tMax);
244
+ }
245
+ }
246
+ else if (q[i] < 0)
247
+ return false;
248
+ }
249
+
250
+ return true;
251
+ }
252
+
253
+ /** Returns an oscillating wave between 0 and amplitude with frequency of 1 Hz by default
254
+ * @param {number} [frequency] - Frequency of the wave in Hz
255
+ * @param {number} [amplitude] - Amplitude (max height) of the wave
256
+ * @param {number} [t=time] - Value to use for time of the wave
257
+ * @param {number} [offset] - Value to use for time offset of the wave
258
+ * @return {number} - Value waving between 0 and amplitude
259
+ * @memberof Math */
260
+ function wave(frequency=1, amplitude=1, t=time, offset=0)
261
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
262
+
263
+ /**
264
+ * Check if object is a valid number, not NaN or undefined, but it may be infinite
265
+ * @param {any} n
266
+ * @return {boolean}
267
+ * @memberof Math */
268
+ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
269
+
270
+ /**
271
+ * Check if object is a valid string or can be converted to one
272
+ * @param {any} s
273
+ * @return {boolean}
274
+ * @memberof Math */
275
+ function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
276
+
277
+ /**
278
+ * Check if object is an array
279
+ * @param {any} a
280
+ * @return {boolean}
281
+ * @memberof Math */
282
+ function isArray(a) { return Array.isArray(a); }
283
+
284
+ /**
285
+ * @callback LineTestFunction - Checks if a position is colliding
286
+ * @param {Vector2} pos
287
+ * @memberof Draw
288
+ */
289
+
290
+ /**
291
+ * Casts a ray and returns position of the first collision found, or undefined if none are found
292
+ * @param {Vector2} posStart
293
+ * @param {Vector2} posEnd
294
+ * @param {LineTestFunction} testFunction - Check if colliding
295
+ * @param {Vector2} [normal] - Optional vector to store the normal
296
+ * @return {Vector2|undefined} - Position of the collision or undefined if none found
297
+ * @memberof Math */
298
+ function lineTest(posStart, posEnd, testFunction, normal)
299
+ {
300
+ ASSERT(isVector2(posStart), 'posStart must be a vec2');
301
+ ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
302
+ ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
303
+ ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
304
+
305
+ // get ray direction and length
306
+ const dx = posEnd.x - posStart.x;
307
+ const dy = posEnd.y - posStart.y;
308
+ const totalLength = hypot(dx, dy);
309
+ if (!totalLength) return;
310
+
311
+ // current integer cell we are in
312
+ const pos = posStart.floor();
313
+
314
+ // normalize ray direction
315
+ const dirX = dx / totalLength;
316
+ const dirY = dy / totalLength;
317
+
318
+ // step direction in grid
319
+ const stepX = sign(dirX);
320
+ const stepY = sign(dirY);
321
+
322
+ // distance along the ray to cross one full cell in X or Y
323
+ const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
324
+ const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
325
+
326
+ // distance along the ray from start to the first grid boundary
327
+ const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
328
+ const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
329
+ const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
330
+ const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
331
+
332
+ // use line drawing algorithm to test for collisions
333
+ let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
334
+ while (t < totalLength)
335
+ {
336
+ if (testFunction(pos))
337
+ {
338
+ // set hit point
339
+ const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
340
+
341
+ // move inside of tile if on positive edge
342
+ const e = 1e-9;
343
+ if (wasX)
344
+ {
345
+ if (stepX < 0)
346
+ hitPos.x -= e;
347
+ }
348
+ if (stepY < 0)
349
+ hitPos.y -= e;
350
+
351
+ // set normal
352
+ if (normal)
353
+ wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
354
+ return hitPos;
355
+ }
356
+
357
+ // advance to the next grid boundary
358
+ if (wasX = tX < tY)
359
+ {
360
+ pos.x += stepX;
361
+ t = tX;
362
+ tX += tDeltaX;
363
+ }
364
+ else
365
+ {
366
+ pos.y += stepY;
367
+ t = tY;
368
+ tY += tDeltaY;
369
+ }
370
+ }
371
+ }
372
+
373
+ ///////////////////////////////////////////////////////////////////////////////
374
+
375
+ /** Random global functions
376
+ * @namespace Random */
377
+
378
+ /** Returns a random value between the two values passed in
379
+ * @param {number} [valueA]
380
+ * @param {number} [valueB]
381
+ * @return {number}
382
+ * @memberof Random */
383
+ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
384
+
385
+ /** Returns a floored random value between the two values passed in
386
+ * The upper bound is exclusive. (If 2 is passed in, result will be 0 or 1)
387
+ * @param {number} valueA
388
+ * @param {number} [valueB]
389
+ * @return {number}
390
+ * @memberof Random */
391
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
392
+
393
+ /** Randomly returns true or false given the chance of true passed in
394
+ * @param {number} [chance]
395
+ * @return {boolean}
396
+ * @memberof Random */
397
+ function randBool(chance=.5) { return rand() < chance; }
398
+
399
+ /** Randomly returns either -1 or 1
400
+ * @return {number}
401
+ * @memberof Random */
402
+ function randSign() { return randInt(2) * 2 - 1; }
403
+
404
+ /** Returns a random Vector2 with the passed in length
405
+ * @param {number} [length]
406
+ * @return {Vector2}
407
+ * @memberof Random */
408
+ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length); }
409
+
410
+ /** Returns a random Vector2 within a circular shape
411
+ * @param {number} [radius]
412
+ * @param {number} [minRadius]
413
+ * @return {Vector2}
414
+ * @memberof Random */
415
+ function randInCircle(radius=1, minRadius=0)
416
+ { return radius > 0 ? randVec2(radius * rand(minRadius / radius, 1)**.5) : new Vector2; }
417
+
418
+ /** Returns a random color between the two passed in colors, combine components if linear
419
+ * @param {Color} [colorA=(1,1,1,1)]
420
+ * @param {Color} [colorB=(0,0,0,1)]
421
+ * @param {boolean} [linear]
422
+ * @return {Color}
423
+ * @memberof Random */
424
+ function randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
425
+ {
426
+ return linear ? colorA.lerp(colorB, rand()) :
427
+ new Color(rand(colorA.r,colorB.r), rand(colorA.g,colorB.g), rand(colorA.b,colorB.b), rand(colorA.a,colorB.a));
428
+ }
429
+
430
+ ///////////////////////////////////////////////////////////////////////////////
431
+
432
+ /**
433
+ * Seeded random number generator
434
+ * - Can be used to create a deterministic random number sequence
435
+ * @memberof Engine
436
+ * @example
437
+ * let r = new RandomGenerator(123); // random number generator with seed 123
438
+ * let a = r.float(); // random value between 0 and 1
439
+ * let b = r.int(10); // random integer between 0 and 9
440
+ * r.seed = 123; // reset the seed
441
+ * let c = r.float(); // the same value as a
442
+ */
443
+ class RandomGenerator
444
+ {
445
+ /** Create a random number generator with the seed passed in
446
+ * @param {number} [seed] - Starting seed or engine default seed */
447
+ constructor(seed = 123456789)
448
+ {
449
+ /** @property {number} - random seed */
450
+ this.seed = seed;
451
+ }
452
+
453
+ /** Returns a seeded random value between the two values passed in
454
+ * @param {number} [valueA]
455
+ * @param {number} [valueB]
456
+ * @return {number} */
457
+ float(valueA=1, valueB=0)
458
+ {
459
+ // xorshift algorithm
460
+ this.seed ^= this.seed << 13;
461
+ this.seed ^= this.seed >>> 17;
462
+ this.seed ^= this.seed << 5;
463
+ return valueB + (valueA - valueB) * ((this.seed >>> 0) / 2**32);
464
+ }
465
+
466
+ /** Returns a floored seeded random value the two values passed in
467
+ * @param {number} valueA
468
+ * @param {number} [valueB]
469
+ * @return {number} */
470
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
471
+
472
+ /** Randomly returns true or false given the chance of true passed in
473
+ * @param {number} [chance]
474
+ * @return {boolean} */
475
+ bool(chance=.5) { return this.float() < chance; }
476
+
477
+ /** Randomly returns either -1 or 1 deterministically
478
+ * @return {number} */
479
+ sign() { return this.float() > .5 ? 1 : -1; }
480
+
481
+ /** Returns a seeded random value between the two values passed in with a random sign
482
+ * @param {number} [valueA]
483
+ * @param {number} [valueB]
484
+ * @return {number} */
485
+ floatSign(valueA=1, valueB=0) { return this.float(valueA, valueB) * this.sign(); }
486
+
487
+ /** Returns a random angle between -PI and PI
488
+ * @return {number} */
489
+ angle() { return this.float(-PI, PI); }
490
+
491
+ /** Returns a seeded vec2 with size between the two values passed in
492
+ * @param {number} valueA
493
+ * @param {number} [valueB]
494
+ * @return {Vector2} */
495
+ vec2(valueA=1, valueB=0)
496
+ { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
497
+
498
+ /** Returns a random color between the two passed in colors, combine components if linear
499
+ * @param {Color} [colorA=(1,1,1,1)]
500
+ * @param {Color} [colorB=(0,0,0,1)]
501
+ * @param {boolean} [linear]
502
+ * @return {Color} */
503
+ randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
504
+ {
505
+ return linear ? colorA.lerp(colorB, this.float()) :
506
+ new Color(
507
+ this.float(colorA.r,colorB.r),
508
+ this.float(colorA.g,colorB.g),
509
+ this.float(colorA.b,colorB.b),
510
+ this.float(colorA.a,colorB.a));
511
+ }
512
+
513
+ /** Returns a new color that has each component randomly adjusted
514
+ * @param {Color} color
515
+ * @param {number} [amount]
516
+ * @param {number} [alphaAmount]
517
+ * @return {Color} */
518
+ mutateColor(color, amount=.05, alphaAmount=0)
519
+ {
520
+ ASSERT_NUMBER_VALID(amount);
521
+ ASSERT_NUMBER_VALID(alphaAmount);
522
+ return new Color
523
+ (
524
+ color.r + this.float(amount, -amount),
525
+ color.g + this.float(amount, -amount),
526
+ color.b + this.float(amount, -amount),
527
+ color.a + this.float(alphaAmount, -alphaAmount)
528
+ ).clamp();
529
+ }
530
+ }
531
+
532
+ ///////////////////////////////////////////////////////////////////////////////
533
+
534
+ /**
535
+ * Create a 2d vector, can take 1 or 2 scalar values
536
+ * @param {number} [x]
537
+ * @param {number} [y] - if y is undefined, x is used for both
538
+ * @return {Vector2}
539
+ * @example
540
+ * let a = vec2(0, 1); // vector with coordinates (0, 1)
541
+ * a = vec2(5); // set a to (5, 5)
542
+ * b = vec2(); // set b to (0, 0)
543
+ * @memberof Math */
544
+ function vec2(x=0, y) { return new Vector2(x, y === undefined ? x : y); }
545
+
546
+ /**
547
+ * Check if object is a valid Vector2
548
+ * @param {any} v
549
+ * @return {boolean}
550
+ * @memberof Math */
551
+ function isVector2(v) { return v instanceof Vector2 && v.isValid(); }
552
+
553
+ // vector2 asserts
554
+ function ASSERT_VECTOR2_VALID(v) { ASSERT(isVector2(v), 'Vector2 is invalid.', v); }
555
+ function ASSERT_NUMBER_VALID(n) { ASSERT(isNumber(n), 'Number is invalid.', n); }
556
+ function ASSERT_VECTOR2_NORMAL(v)
557
+ {
558
+ ASSERT_VECTOR2_VALID(v);
559
+ ASSERT(abs(v.lengthSquared()-1) < .01, 'Vector2 is not normal.', v);
560
+ }
561
+
562
+ /**
563
+ * 2D Vector object with vector math library
564
+ * - Functions do not change this so they can be chained together
565
+ * @memberof Engine
566
+ * @example
567
+ * let a = new Vector2(2, 3); // vector with coordinates (2, 3)
568
+ * let b = new Vector2; // vector with coordinates (0, 0)
569
+ * let c = vec2(4, 2); // use the vec2 function to make a Vector2
570
+ * let d = a.add(b).scale(5); // operators can be chained
571
+ */
572
+ class Vector2
573
+ {
574
+ /** Create a 2D vector with the x and y passed in, can also be created with vec2()
575
+ * @param {number} [x] - X axis location
576
+ * @param {number} [y] - Y axis location */
577
+ constructor(x=0, y=0)
578
+ {
579
+ /** @property {number} - X axis location */
580
+ this.x = x;
581
+ /** @property {number} - Y axis location */
582
+ this.y = y;
583
+ ASSERT(this.isValid(), 'Constructed Vector2 is invalid.', this);
584
+ }
585
+
586
+ /** Sets values of this vector and returns self
587
+ * @param {number} [x] - X axis location
588
+ * @param {number} [y] - Y axis location
589
+ * @return {Vector2} */
590
+ set(x=0, y=0)
591
+ {
592
+ this.x = x;
593
+ this.y = y;
594
+ ASSERT_VECTOR2_VALID(this);
595
+ return this;
596
+ }
597
+
598
+ /** Sets this vector from another vector and returns self
599
+ * @param {Vector2} v - other vector
600
+ * @return {Vector2} */
601
+ setFrom(v) { return this.set(v.x, v.y); }
602
+
603
+ /** Returns a new vector that is a copy of this
604
+ * @return {Vector2} */
605
+ copy() { return new Vector2(this.x, this.y); }
606
+
607
+ /** Returns a copy of this vector plus the vector passed in
608
+ * @param {Vector2} v - other vector
609
+ * @return {Vector2} */
610
+ add(v) { return new Vector2(this.x + v.x, this.y + v.y);}
611
+
612
+ /** Returns a copy of this vector minus the vector passed in
613
+ * @param {Vector2} v - other vector
614
+ * @return {Vector2} */
615
+ subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); }
616
+
617
+ /** Returns a copy of this vector times the vector passed in
618
+ * @param {Vector2} v - other vector
619
+ * @return {Vector2} */
620
+ multiply(v) { return new Vector2(this.x * v.x, this.y * v.y); }
621
+
622
+ /** Returns a copy of this vector divided by the vector passed in
623
+ * @param {Vector2} v - other vector
624
+ * @return {Vector2} */
625
+ divide(v) { return new Vector2(this.x / v.x, this.y / v.y); }
626
+
627
+ /** Returns a copy of this vector scaled by the vector passed in
628
+ * @param {number} s - scale
629
+ * @return {Vector2} */
630
+ scale(s) { return new Vector2(this.x * s, this.y * s); }
631
+
632
+ /** Returns the length of this vector
633
+ * @return {number} */
634
+ length() { return this.lengthSquared()**.5; }
635
+
636
+ /** Returns the length of this vector squared
637
+ * @return {number} */
638
+ lengthSquared() { return this.x**2 + this.y**2; }
639
+
640
+ /** Returns the distance from this vector to vector passed in
641
+ * @param {Vector2} v - other vector
642
+ * @return {number} */
643
+ distance(v) { return this.distanceSquared(v)**.5; }
644
+
645
+ /** Returns the distance squared from this vector to vector passed in
646
+ * @param {Vector2} v - other vector
647
+ * @return {number} */
648
+ distanceSquared(v) { return (this.x - v.x)**2 + (this.y - v.y)**2; }
649
+
650
+ /** Returns a new vector in same direction as this one with the length passed in
651
+ * @param {number} [length]
652
+ * @return {Vector2} */
653
+ normalize(length=1)
654
+ {
655
+ const l = this.length();
656
+ return l ? this.scale(length/l) : new Vector2(0, length);
657
+ }
658
+
659
+ /** Returns a new vector clamped to length passed in
660
+ * @param {number} [length]
661
+ * @return {Vector2} */
662
+ clampLength(length=1)
663
+ {
664
+ const l = this.length();
665
+ return l > length ? this.scale(length/l) : this.copy();
666
+ }
667
+
668
+ /** Returns the dot product of this and the vector passed in
669
+ * @param {Vector2} v - other vector
670
+ * @return {number} */
671
+ dot(v) { return this.x*v.x + this.y*v.y; }
672
+
673
+ /** Returns the cross product of this and the vector passed in
674
+ * @param {Vector2} v - other vector
675
+ * @return {number} */
676
+ cross(v) { return this.x*v.y - this.y*v.x; }
677
+
678
+ /** Returns a copy this vector reflected by the surface normal
679
+ * @param {Vector2} normal - surface normal (should be normalized)
680
+ * @param {number} restitution - how much to bounce, 1 is perfect bounce, 0 is no bounce
681
+ * @return {Vector2} */
682
+ reflect(normal, restitution=1)
683
+ { return this.subtract(normal.scale((1+restitution)*this.dot(normal))); }
684
+
685
+ /** Returns the clockwise angle of this vector, up is angle 0
686
+ * @return {number} */
687
+ angle() { return atan2(this.x, this.y); }
688
+
689
+ /** Sets this vector with clockwise angle and length passed in
690
+ * @param {number} [angle]
691
+ * @param {number} [length]
692
+ * @return {Vector2} */
693
+ setAngle(angle=0, length=1)
694
+ {
695
+ ASSERT_NUMBER_VALID(angle);
696
+ ASSERT_NUMBER_VALID(length);
697
+ this.x = length*sin(angle);
698
+ this.y = length*cos(angle);
699
+ return this;
700
+ }
701
+
702
+ /** Returns copy of this vector rotated by the clockwise angle passed in
703
+ * @param {number} angle
704
+ * @return {Vector2} */
705
+ rotate(angle)
706
+ {
707
+ ASSERT_NUMBER_VALID(angle);
708
+ const c = cos(-angle), s = sin(-angle);
709
+ return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
710
+ }
711
+
712
+ /** Sets this this vector to point in the specified integer direction (0-3), corresponding to multiples of 90 degree rotation
713
+ * @param {number} [direction]
714
+ * @param {number} [length]
715
+ * @return {Vector2} */
716
+ setDirection(direction, length=1)
717
+ {
718
+ ASSERT_NUMBER_VALID(direction);
719
+ ASSERT_NUMBER_VALID(length);
720
+ direction = mod(direction, 4);
721
+ ASSERT(direction===0 || direction===1 || direction===2 || direction===3,
722
+ 'Vector2.setDirection() direction must be an integer between 0 and 3.');
723
+
724
+ this.x = direction%2 ? direction-1 ? -length : length : 0;
725
+ this.y = direction%2 ? 0 : direction ? -length : length;
726
+ return this;
727
+ }
728
+
729
+ /** Returns the integer direction of this vector, corresponding to multiples of 90 degree rotation (0-3)
730
+ * @return {number} */
731
+ direction()
732
+ { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; }
733
+
734
+ /** Returns a copy of this vector with absolute values
735
+ * @return {Vector2} */
736
+ abs() { return new Vector2(abs(this.x), abs(this.y)); }
737
+
738
+ /** Returns a copy of this vector with each axis floored
739
+ * @return {Vector2} */
740
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
741
+
742
+ /** Returns new vec2 with modded values
743
+ * @param {number} [divisor]
744
+ * @return {Vector2} */
745
+ mod(divisor=1)
746
+ { return new Vector2(mod(this.x, divisor), mod(this.y, divisor)); }
747
+
748
+ /** Returns the area this vector covers as a rectangle
749
+ * @return {number} */
750
+ area() { return abs(this.x * this.y); }
751
+
752
+ /** Returns true if this vector is (0,0)
753
+ * @return {boolean} */
754
+ isZero() { return !this.x && !this.y; }
755
+
756
+ /** Returns a new vector that is p percent between this and the vector passed in
757
+ * @param {Vector2} v - other vector
758
+ * @param {number} percent
759
+ * @return {Vector2} */
760
+ lerp(v, percent)
761
+ {
762
+ ASSERT_VECTOR2_VALID(v);
763
+ ASSERT_NUMBER_VALID(percent);
764
+ const p = clamp(percent);
765
+ return new Vector2(v.x*p + this.x*(1-p), v.y*p + this.y*(1-p));
766
+ }
767
+
768
+ /** Returns true if this vector is within the bounds of an array size passed in
769
+ * @param {Vector2} arraySize
770
+ * @return {boolean} */
771
+ arrayCheck(arraySize)
772
+ { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; }
773
+
774
+ /** Returns this vector expressed as a string
775
+ * @param {number} digits - precision to display
776
+ * @return {string} */
777
+ toString(digits=3)
778
+ {
779
+ ASSERT_NUMBER_VALID(digits);
780
+ if (this.isValid())
781
+ return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
782
+ else
783
+ return `(${this.x}, ${this.y})`;
784
+ }
785
+
786
+ /** Checks if this is a valid vector
787
+ * @return {boolean} */
788
+ isValid() { return isNumber(this.x) && isNumber(this.y); }
789
+ }
790
+
791
+ ///////////////////////////////////////////////////////////////////////////////
792
+
793
+ /**
794
+ * Create a color object with RGBA values, white by default
795
+ * @param {number} [r=1] - red
796
+ * @param {number} [g=1] - green
797
+ * @param {number} [b=1] - blue
798
+ * @param {number} [a=1] - alpha
799
+ * @return {Color}
800
+ * @memberof Math
801
+ */
802
+ function rgb(r, g, b, a) { return new Color(r, g, b, a); }
803
+
804
+ /**
805
+ * Create a color object with HSLA values, white by default
806
+ * @param {number} [h=0] - hue
807
+ * @param {number} [s=0] - saturation
808
+ * @param {number} [l=1] - lightness
809
+ * @param {number} [a=1] - alpha
810
+ * @return {Color}
811
+ * @memberof Math */
812
+ function hsl(h, s, l, a) { return new Color().setHSLA(h, s, l, a); }
813
+
814
+ /**
815
+ * Check if object is a valid Color
816
+ * @param {any} c
817
+ * @return {boolean}
818
+ * @memberof Math */
819
+ function isColor(c) { return c instanceof Color && c.isValid(); }
820
+
821
+ // color asserts
822
+ function ASSERT_COLOR_VALID(c) { ASSERT(isColor(c), 'Color is invalid.', c); }
823
+
824
+ /**
825
+ * Color object (red, green, blue, alpha) with some helpful functions
826
+ * @memberof Engine
827
+ * @example
828
+ * let a = new Color; // white
829
+ * let b = new Color(1, 0, 0); // red
830
+ * let c = new Color(0, 0, 0, 0); // transparent black
831
+ * let d = rgb(0, 0, 1); // blue using rgb color
832
+ * let e = hsl(.3, 1, .5); // green using hsl color
833
+ */
834
+ class Color
835
+ {
836
+ /** Create a color with the rgba components passed in, white by default
837
+ * @param {number} [r] - red
838
+ * @param {number} [g] - green
839
+ * @param {number} [b] - blue
840
+ * @param {number} [a] - alpha*/
841
+ constructor(r=1, g=1, b=1, a=1)
842
+ {
843
+ /** @property {number} - Red */
844
+ this.r = r;
845
+ /** @property {number} - Green */
846
+ this.g = g;
847
+ /** @property {number} - Blue */
848
+ this.b = b;
849
+ /** @property {number} - Alpha */
850
+ this.a = a;
851
+ ASSERT(this.isValid(), 'Constructed Color is invalid.', this);
852
+ }
853
+
854
+ /** Sets values of this color and returns self
855
+ * @param {number} [r] - red
856
+ * @param {number} [g] - green
857
+ * @param {number} [b] - blue
858
+ * @param {number} [a] - alpha
859
+ * @return {Color} */
860
+ set(r=1, g=1, b=1, a=1)
861
+ {
862
+ this.r = r;
863
+ this.g = g;
864
+ this.b = b;
865
+ this.a = a;
866
+ ASSERT_COLOR_VALID(this);
867
+ return this;
868
+ }
869
+
870
+ /** Sets this color from another color and returns self
871
+ * @param {Color} c - other color
872
+ * @return {Color} */
873
+ setFrom(c) { return this.set(c.r, c.g, c.b, c.a); }
874
+
875
+ /** Returns a new color that is a copy of this
876
+ * @return {Color} */
877
+ copy() { return new Color(this.r, this.g, this.b, this.a); }
878
+
879
+ /** Returns a copy of this color plus the color passed in
880
+ * @param {Color} c - other color
881
+ * @return {Color} */
882
+ add(c) { return new Color(this.r+c.r, this.g+c.g, this.b+c.b, this.a+c.a); }
883
+
884
+ /** Returns a copy of this color minus the color passed in
885
+ * @param {Color} c - other color
886
+ * @return {Color} */
887
+ subtract(c) { return new Color(this.r-c.r, this.g-c.g, this.b-c.b, this.a-c.a); }
888
+
889
+ /** Returns a copy of this color times the color passed in
890
+ * @param {Color} c - other color
891
+ * @return {Color} */
892
+ multiply(c) { return new Color(this.r*c.r, this.g*c.g, this.b*c.b, this.a*c.a); }
893
+
894
+ /** Returns a copy of this color divided by the color passed in
895
+ * @param {Color} c - other color
896
+ * @return {Color} */
897
+ divide(c) { return new Color(this.r/c.r, this.g/c.g, this.b/c.b, this.a/c.a); }
898
+
899
+ /** Returns a copy of this color scaled by the value passed in, alpha can be scaled separately
900
+ * @param {number} scale
901
+ * @param {number} [alphaScale=scale]
902
+ * @return {Color} */
903
+ scale(scale, alphaScale=scale)
904
+ { return new Color(this.r*scale, this.g*scale, this.b*scale, this.a*alphaScale); }
905
+
906
+ /** Returns a copy of this color clamped to the valid range between 0 and 1
907
+ * @return {Color} */
908
+ clamp() { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); }
909
+
910
+ /** Returns a new color that is p percent between this and the color passed in
911
+ * @param {Color} c - other color
912
+ * @param {number} percent
913
+ * @return {Color} */
914
+ lerp(c, percent)
915
+ {
916
+ ASSERT_COLOR_VALID(c);
917
+ ASSERT_NUMBER_VALID(percent);
918
+ const p = clamp(percent);
919
+ return new Color(
920
+ c.r*p + this.r*(1-p),
921
+ c.g*p + this.g*(1-p),
922
+ c.b*p + this.b*(1-p),
923
+ c.a*p + this.a*(1-p));
924
+ }
925
+
926
+ /** Sets this color given a hue, saturation, lightness, and alpha
927
+ * @param {number} [h] - hue
928
+ * @param {number} [s] - saturation
929
+ * @param {number} [l] - lightness
930
+ * @param {number} [a] - alpha
931
+ * @return {Color} */
932
+ setHSLA(h=0, s=0, l=1, a=1)
933
+ {
934
+ h = mod(h,1);
935
+ s = clamp(s);
936
+ l = clamp(l);
937
+ const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
938
+ f = (p, q, t)=>
939
+ (t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
940
+ t*2 < 1 ? q :
941
+ t*3 < 2 ? p+(q-p)*(4-t*6) : p;
942
+ this.r = f(p, q, h + 1/3);
943
+ this.g = f(p, q, h);
944
+ this.b = f(p, q, h - 1/3);
945
+ this.a = a;
946
+ ASSERT_COLOR_VALID(this);
947
+ return this;
948
+ }
949
+
950
+ /** Returns this color expressed in hsla format
951
+ * @return {Array<number>} */
952
+ HSLA()
953
+ {
954
+ const r = clamp(this.r);
955
+ const g = clamp(this.g);
956
+ const b = clamp(this.b);
957
+ const a = clamp(this.a);
958
+ const maxC = max(r, g, b);
959
+ const minC = min(r, g, b);
960
+ const l = (maxC + minC) / 2;
961
+ let h = 0, s = 0;
962
+ if (maxC !== minC)
963
+ {
964
+ let d = maxC - minC;
965
+ s = l > .5 ? d / (2 - maxC - minC) : d / (maxC + minC);
966
+ if (r === maxC)
967
+ h = (g - b) / d + (g < b ? 6 : 0);
968
+ else if (g === maxC)
969
+ h = (b - r) / d + 2;
970
+ else if (b === maxC)
971
+ h = (r - g) / d + 4;
972
+ }
973
+ return [h / 6, s, l, a];
974
+ }
975
+
976
+ /** Returns a new color that has each component randomly adjusted
977
+ * @param {number} [amount]
978
+ * @param {number} [alphaAmount]
979
+ * @return {Color} */
980
+ mutate(amount=.05, alphaAmount=0)
981
+ {
982
+ ASSERT_NUMBER_VALID(amount);
983
+ ASSERT_NUMBER_VALID(alphaAmount);
984
+ return new Color
985
+ (
986
+ this.r + rand(amount, -amount),
987
+ this.g + rand(amount, -amount),
988
+ this.b + rand(amount, -amount),
989
+ this.a + rand(alphaAmount, -alphaAmount)
990
+ ).clamp();
991
+ }
992
+
993
+ /** Returns this color expressed as a hex color code
994
+ * @param {boolean} [useAlpha] - if alpha should be included in result
995
+ * @return {string} */
996
+ toString(useAlpha = true)
997
+ {
998
+ if (debug && !this.isValid())
999
+ return '#000';
1000
+ const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
1001
+ return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
1002
+ }
1003
+
1004
+ /** Set this color from a hex code
1005
+ * @param {string} hex - html hex code
1006
+ * @return {Color} */
1007
+ setHex(hex)
1008
+ {
1009
+ ASSERT(isString(hex), 'Color hex code must be a string');
1010
+ ASSERT(hex[0] === '#', 'Color hex code must start with #');
1011
+ ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1012
+
1013
+ if (hex.length < 6)
1014
+ {
1015
+ const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
1016
+ this.r = fromHex(1);
1017
+ this.g = fromHex(2);
1018
+ this.b = fromHex(3);
1019
+ this.a = hex.length === 5 ? fromHex(4) : 1;
1020
+ }
1021
+ else
1022
+ {
1023
+ const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1024
+ this.r = fromHex(1);
1025
+ this.g = fromHex(3);
1026
+ this.b = fromHex(5);
1027
+ this.a = hex.length === 9 ? fromHex(7) : 1;
1028
+ }
1029
+
1030
+ ASSERT_COLOR_VALID(this);
1031
+ return this;
1032
+ }
1033
+
1034
+ /** Returns this color expressed as 32 bit RGBA value
1035
+ * @return {number} */
1036
+ rgbaInt()
1037
+ {
1038
+ const r = clamp(this.r)*255|0;
1039
+ const g = clamp(this.g)*255<<8;
1040
+ const b = clamp(this.b)*255<<16;
1041
+ const a = clamp(this.a)*255<<24;
1042
+ return r + g + b + a;
1043
+ }
1044
+
1045
+ /** Checks if this is a valid color
1046
+ * @return {boolean} */
1047
+ isValid()
1048
+ { return isNumber(this.r) && isNumber(this.g) && isNumber(this.b) && isNumber(this.a); }
1049
+ }
1050
+
1051
+ ///////////////////////////////////////////////////////////////////////////////
1052
+ // Default Colors
1053
+
1054
+ /** Color - White #ffffff
1055
+ * @type {Color}
1056
+ * @memberof Math */
1057
+ const WHITE = debugProtectConstant(rgb());
1058
+
1059
+ /** Color - Clear White #757474ff with 0 alpha
1060
+ * @type {Color}
1061
+ * @memberof Math */
1062
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1063
+
1064
+ /** Color - Black #000000
1065
+ * @type {Color}
1066
+ * @memberof Math */
1067
+ const BLACK = debugProtectConstant(rgb(0,0,0));
1068
+
1069
+ /** Color - Clear Black #000000 with 0 alpha
1070
+ * @type {Color}
1071
+ * @memberof Math */
1072
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1073
+
1074
+ /** Color - Gray #808080
1075
+ * @type {Color}
1076
+ * @memberof Math */
1077
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1078
+
1079
+ /** Color - Red #ff0000
1080
+ * @type {Color}
1081
+ * @memberof Math */
1082
+ const RED = debugProtectConstant(rgb(1,0,0));
1083
+
1084
+ /** Color - Orange #ff8000
1085
+ * @type {Color}
1086
+ * @memberof Math */
1087
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
1088
+
1089
+ /** Color - Yellow #ffff00
1090
+ * @type {Color}
1091
+ * @memberof Math */
1092
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
1093
+
1094
+ /** Color - Green #00ff00
1095
+ * @type {Color}
1096
+ * @memberof Math */
1097
+ const GREEN = debugProtectConstant(rgb(0,1,0));
1098
+
1099
+ /** Color - Cyan #00ffff
1100
+ * @type {Color}
1101
+ * @memberof Math */
1102
+ const CYAN = debugProtectConstant(rgb(0,1,1));
1103
+
1104
+ /** Color - Blue #0000ff
1105
+ * @type {Color}
1106
+ * @memberof Math */
1107
+ const BLUE = debugProtectConstant(rgb(0,0,1));
1108
+
1109
+ /** Color - Purple #8000ff
1110
+ * @type {Color}
1111
+ * @memberof Math */
1112
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
1113
+
1114
+ /** Color - Magenta #ff00ff
1115
+ * @type {Color}
1116
+ * @memberof Math */
1117
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1118
+
1119
+ ///////////////////////////////////////////////////////////////////////////////
1120
+
1121
+ /**
1122
+ * Timer object tracks how long has passed since it was set
1123
+ * @memberof Engine
1124
+ * @example
1125
+ * let a = new Timer; // creates a timer that is not set
1126
+ * a.set(3); // sets the timer to 3 seconds
1127
+ *
1128
+ * let b = new Timer(1); // creates a timer with 1 second left
1129
+ * b.unset(); // unset the timer
1130
+ */
1131
+ class Timer
1132
+ {
1133
+ /** Create a timer object set time passed in
1134
+ * @param {number} [timeLeft] - How much time left before the timer
1135
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1136
+ constructor(timeLeft, useRealTime=false)
1137
+ {
1138
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1139
+ this.useRealTime = useRealTime;
1140
+ const globalTime = this.getGlobalTime();
1141
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1142
+ this.setTime = timeLeft;
1143
+ }
1144
+
1145
+ /** Set the timer with seconds passed in
1146
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
1147
+ set(timeLeft=0)
1148
+ {
1149
+ ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
1150
+ const globalTime = this.getGlobalTime();
1151
+ this.time = globalTime + timeLeft;
1152
+ this.setTime = timeLeft;
1153
+ }
1154
+
1155
+ /** Set if the timer should keep running even when the game is paused
1156
+ * @param {boolean} [useRealTime] */
1157
+ setUseRealTime(useRealTime=true)
1158
+ {
1159
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
1160
+ this.useRealTime = useRealTime;
1161
+ }
1162
+
1163
+ /** Unset the timer */
1164
+ unset() { this.time = undefined; }
1165
+
1166
+ /** Returns true if set
1167
+ * @return {boolean} */
1168
+ isSet() { return this.time !== undefined; }
1169
+
1170
+ /** Returns true if set and has not elapsed
1171
+ * @return {boolean} */
1172
+ active() { return this.getGlobalTime() < this.time; }
1173
+
1174
+ /** Returns true if set and elapsed
1175
+ * @return {boolean} */
1176
+ elapsed() { return this.getGlobalTime() >= this.time; }
1177
+
1178
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1179
+ * @return {number} */
1180
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
1181
+
1182
+ /** Get percentage elapsed based on time it was set to, returns 0 if not set
1183
+ * @return {number} */
1184
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
1185
+
1186
+ /** Get the time this timer was set to, returns 0 if not set
1187
+ * @return {number} */
1188
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
1189
+
1190
+ /** Get the current global time this timer is based on
1191
+ * @return {number} */
1192
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
1193
+
1194
+ /** Returns this timer expressed as a string
1195
+ * @return {string} */
1196
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1197
+
1198
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1199
+ * @return {number} */
1200
+ valueOf() { return this.get(); }
1201
+ }