littlejsengine 1.14.23 → 1.15.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 (52) hide show
  1. package/dist/littlejs.d.ts +184 -30
  2. package/dist/littlejs.esm.js +589 -147
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +583 -147
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +546 -147
  7. package/examples/box2d/gameObjects.js +6 -10
  8. package/examples/box2d/scenes.js +2 -2
  9. package/examples/breakout/gameObjects.js +1 -1
  10. package/examples/electron/index.html +2 -2
  11. package/examples/index.html +11 -7
  12. package/examples/platformer/gameCharacter.js +3 -2
  13. package/examples/platformer/gameObjects.js +3 -8
  14. package/examples/shorts/base.html +2 -2
  15. package/examples/shorts/box2d.js +2 -2
  16. package/examples/shorts/box2dCar.js +18 -7
  17. package/examples/shorts/box2dPool.js +108 -0
  18. package/examples/shorts/debugDraw.js +2 -3
  19. package/examples/shorts/flappyGame.js +1 -0
  20. package/examples/shorts/fps.js +1 -1
  21. package/examples/shorts/hillGlideGame.js +0 -2
  22. package/examples/shorts/input.js +59 -0
  23. package/examples/shorts/landerGame.js +1 -3
  24. package/examples/shorts/medals.js +15 -9
  25. package/examples/shorts/music.js +15 -17
  26. package/examples/shorts/musicPlayer.js +24 -24
  27. package/examples/shorts/platformer.js +0 -2
  28. package/examples/shorts/slidingPuzzle.js +1 -1
  29. package/examples/shorts/song.mp3 +0 -0
  30. package/examples/shorts/spaceGame.js +0 -2
  31. package/examples/shorts/spriteAtlas.js +0 -2
  32. package/examples/shorts/tiltedView.js +0 -3
  33. package/examples/shorts/timers.js +1 -2
  34. package/examples/shorts/topDown.js +0 -2
  35. package/examples/shorts/video.webm +0 -0
  36. package/examples/shorts/videoPlayer.js +33 -0
  37. package/examples/starter/index.html +3 -3
  38. package/package.json +1 -1
  39. package/plugins/box2d.js +170 -50
  40. package/plugins/pluginExport.js +3 -0
  41. package/plugins/uiSystem.js +229 -27
  42. package/src/engine.js +21 -18
  43. package/src/engineAudio.js +14 -2
  44. package/src/engineDebug.js +37 -0
  45. package/src/engineDraw.js +21 -13
  46. package/src/engineExport.js +3 -0
  47. package/src/engineInput.js +24 -12
  48. package/src/engineMedals.js +2 -2
  49. package/src/engineObject.js +24 -4
  50. package/src/engineParticles.js +4 -6
  51. package/src/engineTileLayer.js +2 -0
  52. package/src/engineUtilities.js +35 -13
package/src/engineDraw.js CHANGED
@@ -294,7 +294,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
294
294
  {
295
295
  // normal canvas 2D rendering method (slower)
296
296
  ++drawCount;
297
- size = new Vector2(size.x, -size.y); // fix upside down sprites
297
+ size = new Vector2(size.x, -size.y); // flip upside down sprites
298
298
  drawCanvas2D(pos, size, angle, mirror, (context)=>
299
299
  {
300
300
  if (textureInfo)
@@ -403,7 +403,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
403
403
  * @memberof Draw */
404
404
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
405
405
  {
406
- ASSERT(Array.isArray(points), 'points must be an array');
406
+ ASSERT(isArray(points), 'points must be an array');
407
407
  ASSERT(isNumber(width), 'width must be a number');
408
408
  ASSERT(isColor(color), 'color is invalid');
409
409
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -509,7 +509,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
509
509
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
510
510
  {
511
511
  ASSERT(isVector2(pos), 'pos must be a vec2');
512
- ASSERT(Array.isArray(points), 'points must be an array');
512
+ ASSERT(isArray(points), 'points must be an array');
513
513
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
514
514
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
515
515
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -661,11 +661,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
661
661
  * @param {string} [font=fontDefault]
662
662
  * @param {string} [fontStyle]
663
663
  * @param {number} [maxWidth]
664
+ * @param {number} [angle]
664
665
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
665
666
  * @memberof Draw */
666
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
667
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
667
668
  {
668
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
669
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
669
670
  }
670
671
 
671
672
  /** Draw text on overlay canvas in world space
@@ -680,10 +681,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
680
681
  * @param {string} [font=fontDefault]
681
682
  * @param {string} [fontStyle]
682
683
  * @param {number} [maxWidth]
684
+ * @param {number} [angle]
683
685
  * @memberof Draw */
684
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
686
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
685
687
  {
686
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
688
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
687
689
  }
688
690
 
689
691
  /** Draw text on overlay canvas in screen space
@@ -698,9 +700,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
698
700
  * @param {string} [font=fontDefault]
699
701
  * @param {string} [fontStyle]
700
702
  * @param {number} [maxWidth]
703
+ * @param {number} [angle]
701
704
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
702
705
  * @memberof Draw */
703
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
706
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
704
707
  {
705
708
  ASSERT(isString(text), 'text must be a string');
706
709
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -711,6 +714,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
711
714
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
712
715
  ASSERT(isString(font), 'font must be a string');
713
716
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
717
+ ASSERT(isNumber(angle), 'angle must be a number');
714
718
 
715
719
  context.fillStyle = color.toString();
716
720
  context.strokeStyle = lineColor.toString();
@@ -720,14 +724,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
720
724
  context.textBaseline = 'middle';
721
725
 
722
726
  const lines = (text+'').split('\n');
723
- let posY = pos.y;
724
- posY -= (lines.length-1) * size/2; // center text vertically
727
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
728
+ context.save();
729
+ context.translate(pos.x, posY);
730
+ context.rotate(-angle);
731
+ let yOffset = 0;
725
732
  lines.forEach(line=>
726
733
  {
727
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
728
- context.fillText(line, pos.x, posY, maxWidth);
729
- posY += size;
734
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
735
+ context.fillText(line, 0, yOffset, maxWidth);
736
+ yOffset += size;
730
737
  });
738
+ context.restore();
731
739
  }
732
740
 
733
741
  ///////////////////////////////////////////////////////////////////////////////
@@ -173,6 +173,7 @@ export
173
173
  // Random
174
174
  rand,
175
175
  randInt,
176
+ randBool,
176
177
  randSign,
177
178
  randInCircle,
178
179
  randVec2,
@@ -190,6 +191,7 @@ export
190
191
  isVector2,
191
192
  isNumber,
192
193
  isString,
194
+ isArray,
193
195
 
194
196
  // Default Colors
195
197
  WHITE,
@@ -290,6 +292,7 @@ export
290
292
  mouseDelta,
291
293
  mouseDeltaScreen,
292
294
  mouseWheel,
295
+ mouseInWindow,
293
296
  isUsingGamepad,
294
297
  inputPreventDefault,
295
298
  setInputPreventDefault,
@@ -119,6 +119,11 @@ let mouseDeltaScreen = vec2();
119
119
  * @memberof Input */
120
120
  let mouseWheel = 0;
121
121
 
122
+ /** True if mouse was inside the document window, set to false when mouse leaves
123
+ * @type {boolean}
124
+ * @memberof Input */
125
+ let mouseInWindow = true;
126
+
122
127
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
123
128
  * @type {boolean}
124
129
  * @memberof Input */
@@ -213,6 +218,7 @@ function inputInit()
213
218
  document.addEventListener('mousedown', onMouseDown);
214
219
  document.addEventListener('mouseup', onMouseUp);
215
220
  document.addEventListener('mousemove', onMouseMove);
221
+ document.addEventListener('mouseleave', onMouseLeave);
216
222
  document.addEventListener('wheel', onMouseWheel);
217
223
  document.addEventListener('contextmenu', onContextMenu);
218
224
  document.addEventListener('blur', onBlur);
@@ -237,14 +243,14 @@ function inputInit()
237
243
  if (inputWASDEmulateDirection)
238
244
  inputData[0][remapKey(e.code)] = 4;
239
245
  }
240
- function remapKey(c)
246
+ function remapKey(k)
241
247
  {
242
248
  // handle remapping wasd keys to directions
243
249
  return inputWASDEmulateDirection ?
244
- c === 'KeyW' ? 'ArrowUp' :
245
- c === 'KeyS' ? 'ArrowDown' :
246
- c === 'KeyA' ? 'ArrowLeft' :
247
- c === 'KeyD' ? 'ArrowRight' : c : c;
250
+ k === 'KeyW' ? 'ArrowUp' :
251
+ k === 'KeyS' ? 'ArrowDown' :
252
+ k === 'KeyA' ? 'ArrowLeft' :
253
+ k === 'KeyD' ? 'ArrowRight' : k : k;
248
254
  }
249
255
  function onMouseDown(e)
250
256
  {
@@ -258,7 +264,7 @@ function inputInit()
258
264
  isUsingGamepad = false;
259
265
  inputData[0][e.button] = 3;
260
266
 
261
- let mousePosScreenLast = mousePosScreen;
267
+ const mousePosScreenLast = mousePosScreen;
262
268
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
263
269
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
264
270
  inputPreventDefault && e.button && e.preventDefault();
@@ -271,10 +277,17 @@ function inputInit()
271
277
  }
272
278
  function onMouseMove(e)
273
279
  {
274
- let mousePosScreenLast = mousePosScreen;
280
+ mouseInWindow = true;
281
+ const mousePosScreenLast = mousePosScreen;
275
282
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
276
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
283
+
284
+ // when pointer is locked use movementX/Y for delta
285
+ const movement = pointerLockIsActive() ?
286
+ vec2(e.movementX, e.movementY) :
287
+ mousePosScreen.subtract(mousePosScreenLast);
288
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
277
289
  }
290
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
278
291
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
279
292
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
280
293
  function onBlur() { inputClear(); } // reset input when focus is lost
@@ -413,7 +426,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
413
426
  function touchGamepadButtonCenter()
414
427
  {
415
428
  // draw right face buttons
416
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
429
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
417
430
  if (touchGamepadButtonCount <= 2)
418
431
  center.x += touchGamepadSize/2;
419
432
  return center;
@@ -423,14 +436,13 @@ function touchGamepadButtonCenter()
423
436
  function touchInputInit()
424
437
  {
425
438
  // add non passive touch event listeners
426
- let handleTouch = handleTouchDefault;
427
439
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
428
440
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
429
441
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
430
442
 
431
443
  // handle all touch events the same way
432
444
  let wasTouching;
433
- function handleTouchDefault(e)
445
+ function handleTouch(e)
434
446
  {
435
447
  if (!touchInputEnable)
436
448
  return;
@@ -581,7 +593,7 @@ function touchGamepadRender()
581
593
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
582
594
  for (let i=0; i<touchGamepadButtonCount; i++)
583
595
  {
584
- let j = mod(i-1, 4);
596
+ const j = mod(i-1, 4);
585
597
  let button = touchGamepadButtonCount > 2 ?
586
598
  j : min(j, touchGamepadButtonCount-1);
587
599
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -167,11 +167,11 @@ class Medal
167
167
  const descriptionSize = height*.3;
168
168
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
169
169
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
170
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
170
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
171
171
 
172
172
  // draw the description
173
173
  pos.y = y + height - gap.y*2 - descriptionSize/2;
174
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
174
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
175
175
  context.restore();
176
176
  }
177
177
 
@@ -135,11 +135,10 @@ class EngineObject
135
135
  }
136
136
 
137
137
  /** Update the object physics, called automatically by engine once each frame */
138
- update()
138
+ updatePhysics()
139
139
  {
140
140
  // child objects do not have physics
141
- if (this.parent)
142
- return;
141
+ ASSERT(!this.parent);
143
142
 
144
143
  if (this.clampSpeed)
145
144
  {
@@ -189,7 +188,7 @@ class EngineObject
189
188
  continue;
190
189
 
191
190
  // check collision
192
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
191
+ if (!this.isOverlappingObject(o))
193
192
  continue;
194
193
 
195
194
  // notify objects of collision and check if should be resolved
@@ -337,6 +336,9 @@ class EngineObject
337
336
  }
338
337
  }
339
338
 
339
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
340
+ update() {}
341
+
340
342
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
341
343
  render()
342
344
  {
@@ -402,6 +404,10 @@ class EngineObject
402
404
  * @return {number} */
403
405
  getAliveTime() { return time - this.spawnTime; }
404
406
 
407
+ /** Get the speed of this object
408
+ * @return {number} */
409
+ getSpeed() { return this.velocity.length(); }
410
+
405
411
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
406
412
  * @param {Vector2} acceleration */
407
413
  applyAcceleration(acceleration)
@@ -446,6 +452,20 @@ class EngineObject
446
452
  child.parent = 0;
447
453
  }
448
454
 
455
+ /** Check if overlapping another engine object
456
+ * Collisions are resoloved to prevent overlaps
457
+ * @param {EngineObject} object
458
+ * @return {boolean} */
459
+ isOverlappingObject(object)
460
+ { return this.isOverlapping(object.pos, object.size); }
461
+
462
+ /** Check if overlapping a point or aligned bounding box
463
+ * @param {Vector2} pos - Center of box
464
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
465
+ * @return {boolean} */
466
+ isOverlapping(pos, size=vec2())
467
+ { return isOverlapping(this.pos, this.size, pos, size); }
468
+
449
469
  /** Set how this object collides
450
470
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
451
471
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -160,12 +160,12 @@ class ParticleEmitter extends EngineObject
160
160
  this.previousPos = this.pos.copy();
161
161
  }
162
162
 
163
+ /** Emitters do not have physics */
164
+ updatePhysics() {}
165
+
163
166
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
164
167
  update()
165
168
  {
166
- // only do default update to apply parent transforms
167
- this.parent && super.update();
168
-
169
169
  if (this.velocityInheritance)
170
170
  {
171
171
  // pass emitter velocity to particles
@@ -182,7 +182,7 @@ class ParticleEmitter extends EngineObject
182
182
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
183
183
  {
184
184
  // emit particles
185
- if (this.emitRate * particleEmitRateScale)
185
+ if (this.emitRate && particleEmitRateScale)
186
186
  {
187
187
  const rate = 1/this.emitRate/particleEmitRateScale;
188
188
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -320,8 +320,6 @@ class Particle extends EngineObject
320
320
  /** Update the object physics, called automatically by engine once each frame */
321
321
  update()
322
322
  {
323
- super.update();
324
-
325
323
  if (this.collideTiles || this.collideSolidObjects)
326
324
  {
327
325
  // only apply max circular speed if particle can collide
@@ -48,6 +48,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
48
48
  }
49
49
 
50
50
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
51
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
51
52
  * @param {Vector2} posStart
52
53
  * @param {Vector2} posEnd
53
54
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -621,6 +622,7 @@ class TileCollisionLayer extends TileLayer
621
622
  }
622
623
 
623
624
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
625
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
624
626
  * @param {Vector2} posStart
625
627
  * @param {Vector2} posEnd
626
628
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -148,7 +148,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
148
148
  * @param {number} valueA
149
149
  * @param {number} valueB
150
150
  * @param {number} [wrapSize]
151
- * @returns {number}
151
+ * @return {number}
152
152
  * @memberof Utilities */
153
153
  function distanceWrap(valueA, valueB, wrapSize=1)
154
154
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
@@ -158,7 +158,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
158
158
  * @param {number} valueB
159
159
  * @param {number} percent
160
160
  * @param {number} [wrapSize]
161
- * @returns {number}
161
+ * @return {number}
162
162
  * @memberof Utilities */
163
163
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
164
164
  {
@@ -170,7 +170,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
170
170
  /** Returns signed wrapped distance between the two angles passed in
171
171
  * @param {number} angleA
172
172
  * @param {number} angleB
173
- * @returns {number}
173
+ * @return {number}
174
174
  * @memberof Utilities */
175
175
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
176
176
 
@@ -178,7 +178,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
178
178
  * @param {number} angleA
179
179
  * @param {number} angleB
180
180
  * @param {number} percent
181
- * @returns {number}
181
+ * @return {number}
182
182
  * @memberof Utilities */
183
183
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
184
184
 
@@ -305,6 +305,13 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
305
305
  * @memberof Utilities */
306
306
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
307
307
 
308
+ /**
309
+ * Check if object is an array
310
+ * @param {any} a
311
+ * @return {boolean}
312
+ * @memberof Utilities */
313
+ function isArray(a) { return Array.isArray(a); }
314
+
308
315
  /**
309
316
  * @callback LineTestFunction - Checks if a position is colliding
310
317
  * @param {Vector2} pos
@@ -1004,7 +1011,6 @@ class Color
1004
1011
  * @return {string} */
1005
1012
  toString(useAlpha = true)
1006
1013
  {
1007
- ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1008
1014
  if (debug && !this.isValid())
1009
1015
  return `#000`;
1010
1016
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
@@ -1141,11 +1147,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1141
1147
  class Timer
1142
1148
  {
1143
1149
  /** Create a timer object set time passed in
1144
- * @param {number} [timeLeft] - How much time left before the timer elapses in seconds */
1145
- constructor(timeLeft)
1150
+ * @param {number} [timeLeft] - How much time left before the timer
1151
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1152
+ constructor(timeLeft, useRealTime=false)
1146
1153
  {
1147
1154
  ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1148
- this.time = timeLeft === undefined ? undefined : time + timeLeft;
1155
+ this.useRealTime = useRealTime;
1156
+ const globalTime = this.getGlobalTime();
1157
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1149
1158
  this.setTime = timeLeft;
1150
1159
  }
1151
1160
 
@@ -1154,10 +1163,19 @@ class Timer
1154
1163
  set(timeLeft=0)
1155
1164
  {
1156
1165
  ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
1157
- this.time = time + timeLeft;
1166
+ const globalTime = this.getGlobalTime();
1167
+ this.time = globalTime + timeLeft;
1158
1168
  this.setTime = timeLeft;
1159
1169
  }
1160
1170
 
1171
+ /** Set if the timer should keep running even when the game is paused
1172
+ * @param {boolean} [useRealTime] */
1173
+ setUseRealTime(useRealTime=true)
1174
+ {
1175
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
1176
+ this.useRealTime = useRealTime;
1177
+ }
1178
+
1161
1179
  /** Unset the timer */
1162
1180
  unset() { this.time = undefined; }
1163
1181
 
@@ -1167,24 +1185,28 @@ class Timer
1167
1185
 
1168
1186
  /** Returns true if set and has not elapsed
1169
1187
  * @return {boolean} */
1170
- active() { return time < this.time; }
1188
+ active() { return this.getGlobalTime() < this.time; }
1171
1189
 
1172
1190
  /** Returns true if set and elapsed
1173
1191
  * @return {boolean} */
1174
- elapsed() { return time >= this.time; }
1192
+ elapsed() { return this.getGlobalTime() >= this.time; }
1175
1193
 
1176
1194
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1177
1195
  * @return {number} */
1178
- get() { return this.isSet()? time - this.time : 0; }
1196
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
1179
1197
 
1180
1198
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1181
1199
  * @return {number} */
1182
- getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1200
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
1183
1201
 
1184
1202
  /** Get the time this timer was set to, returns 0 if not set
1185
1203
  * @return {number} */
1186
1204
  getSetTime() { return this.isSet() ? this.setTime : 0; }
1187
1205
 
1206
+ /** Get the current global time this timer is based on
1207
+ * @return {number} */
1208
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
1209
+
1188
1210
  /** Returns this timer expressed as a string
1189
1211
  * @return {string} */
1190
1212
  toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }