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
@@ -108,11 +108,16 @@ class UISystemPlugin
108
108
  // reset hover object at start of update
109
109
  uiSystem.lastHoverObject = uiSystem.hoverObject;
110
110
  uiSystem.hoverObject = undefined;
111
+
112
+ // update in reverse order so topmost objects get priority
111
113
  for (let i = uiSystem.uiObjects.length; i--;)
112
114
  {
113
115
  const o = uiSystem.uiObjects[i];
114
116
  o.parent || updateObject(o)
115
117
  }
118
+
119
+ // remove destroyed objects
120
+ uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
116
121
  }
117
122
  function uiRender()
118
123
  {
@@ -145,12 +150,12 @@ class UISystemPlugin
145
150
  /** Draw a rectangle to the UI context
146
151
  * @param {Vector2} pos
147
152
  * @param {Vector2} size
148
- * @param {Color} [color=uiSystem.defaultColor]
149
- * @param {number} [lineWidth=uiSystem.defaultLineWidth]
150
- * @param {Color} [lineColor=uiSystem.defaultLineColor]
151
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
152
- * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
153
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
153
+ * @param {Color} [color]
154
+ * @param {number} [lineWidth]
155
+ * @param {Color} [lineColor]
156
+ * @param {number} [cornerRadius]
157
+ * @param {Color} [gradientColor] */
158
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
154
159
  {
155
160
  ASSERT(isVector2(pos), 'pos must be a vec2');
156
161
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -229,10 +234,14 @@ class UISystemPlugin
229
234
  * @param {string} [align]
230
235
  * @param {string} [font=uiSystem.defaultFont]
231
236
  * @param {string} [fontStyle]
232
- * @param {boolean} [applyMaxWidth=true] */
233
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
237
+ * @param {boolean} [applyMaxWidth=true]
238
+ * @param {Vector2} [textShadow]
239
+ */
240
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
234
241
  {
235
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
242
+ if (textShadow)
243
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
244
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
236
245
  }
237
246
 
238
247
  /**
@@ -259,6 +268,34 @@ class UISystemPlugin
259
268
  setCallback(onDragLeave, 'dragleave');
260
269
  setCallback(onDragOver, 'dragover');
261
270
  }
271
+
272
+ /** Convert a screen space position to native UI position
273
+ * @param {Vector2} pos
274
+ * @return {Vector2}
275
+ */
276
+ screenToNative(pos)
277
+ {
278
+ if (!uiSystem.nativeHeight)
279
+ return pos;
280
+
281
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
282
+ const sInv = 1/s;
283
+ const p = pos.copy();
284
+ p.x += s*mainCanvasSize.x/2;
285
+ p.x *= sInv;
286
+ p.y *= sInv;
287
+ p.x -= sInv*mainCanvasSize.x/2;
288
+ return p;
289
+ }
290
+
291
+ /** Destroy and remove all objects
292
+ * @memberof Engine */
293
+ destroyObjects()
294
+ {
295
+ for (const o of this.uiObjects)
296
+ o.parent || o.destroy();
297
+ this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
298
+ }
262
299
  }
263
300
 
264
301
  ///////////////////////////////////////////////////////////////////////////////
@@ -335,6 +372,9 @@ class UIObject
335
372
  /** @property {boolean} - True if this can be a hover object */
336
373
  this.canBeHover = true;
337
374
  uiSystem.uiObjects.push(this);
375
+
376
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
377
+ this.textShadow = undefined;
338
378
  }
339
379
 
340
380
  /** Add a child UIObject to this object
@@ -357,29 +397,41 @@ class UIObject
357
397
  child.parent = undefined;
358
398
  }
359
399
 
400
+
401
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
402
+ destroy()
403
+ {
404
+ if (this.destroyed)
405
+ return;
406
+
407
+ // disconnect from parent and destroy children
408
+ this.destroyed = 1;
409
+ this.parent && this.parent.removeChild(this);
410
+ for (const child of this.children)
411
+ {
412
+ child.parent = 0;
413
+ child.destroy();
414
+ }
415
+ }
360
416
  /** Check if the mouse is overlapping a box in screen space
361
417
  * @return {boolean} - True if overlapping
362
418
  */
363
419
  isMouseOverlapping()
364
420
  {
421
+ if (!mouseInWindow) return false;
422
+
365
423
  const size = !isTouchDevice ? this.size :
366
424
  this.size.add(vec2(this.extraTouchSize || 0));
367
- if (!uiSystem.nativeHeight)
368
- return isOverlapping(this.pos, size, mousePosScreen);
369
-
370
- const s = mainCanvasSize.y / uiSystem.nativeHeight;
371
- const sInv = 1/s;
372
- let pos = mousePosScreen.copy();
373
- pos.x += s*mainCanvasSize.x/2;
374
- pos.x *= sInv;
375
- pos.y *= sInv;
376
- pos.x -= sInv*mainCanvasSize.x/2;
425
+ const pos = uiSystem.screenToNative(mousePosScreen);
377
426
  return isOverlapping(this.pos, size, pos);
378
427
  }
379
428
 
380
429
  /** Update the object, called automatically by plugin once each frame */
381
430
  update()
382
431
  {
432
+ // call the custom update callback
433
+ this.onUpdate();
434
+
383
435
  const wasHover = uiSystem.lastHoverObject === this;
384
436
  const isActive = this.isActiveObject();
385
437
  const mouseDown = mouseIsDown(0);
@@ -436,7 +488,7 @@ class UIObject
436
488
 
437
489
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
438
490
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
439
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
491
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
440
492
  }
441
493
 
442
494
  /** Special update when object is not visible */
@@ -463,6 +515,9 @@ class UIObject
463
515
  /** @return {boolean} - Is the mouse held onto this element */
464
516
  isActiveObject() { return uiSystem.activeObject === this; }
465
517
 
518
+ /** Called each frame when object updates */
519
+ onUpdate() {}
520
+
466
521
  /** Called when the mouse enters the object */
467
522
  onEnter() {}
468
523
 
@@ -517,8 +572,9 @@ class UIText extends UIObject
517
572
  }
518
573
  render()
519
574
  {
575
+ // only render the text
520
576
  const textSize = this.getTextSize();
521
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
577
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
522
578
  }
523
579
  }
524
580
 
@@ -594,7 +650,7 @@ class UIButton extends UIObject
594
650
  // draw the text scaled to fit
595
651
  const textSize = this.getTextSize();
596
652
  uiSystem.drawText(this.text, this.pos, textSize,
597
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
653
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
598
654
  }
599
655
  }
600
656
 
@@ -648,7 +704,7 @@ class UICheckbox extends UIObject
648
704
  const textSize = this.getTextSize();
649
705
  const pos = this.pos.add(vec2(this.size.x,0));
650
706
  uiSystem.drawText(this.text, pos, textSize,
651
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
707
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
652
708
  }
653
709
  }
654
710
 
@@ -703,9 +759,11 @@ class UIScrollbar extends UIObject
703
759
  const p1 = centerPos - handleWidth/2;
704
760
  const p2 = centerPos + handleWidth/2;
705
761
  const oldValue = this.value;
762
+
763
+ const p = uiSystem.screenToNative(mousePosScreen);
706
764
  this.value = isHorizontal ?
707
- percent(mousePosScreen.x, p1, p2) :
708
- percent(mousePosScreen.y, p2, p1);
765
+ percent(p.x, p1, p2) :
766
+ percent(p.y, p2, p1);
709
767
  this.value === oldValue || this.onChange();
710
768
  }
711
769
  }
@@ -727,11 +785,155 @@ class UIScrollbar extends UIObject
727
785
  vec2(lerp(p1, p2, this.value), this.pos.y) :
728
786
  vec2(this.pos.x, lerp(p2, p1, this.value))
729
787
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
730
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
788
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
731
789
 
732
790
  // draw the text scaled to fit on the scrollbar
733
791
  const textSize = this.getTextSize();
734
792
  uiSystem.drawText(this.text, this.pos, textSize,
735
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
793
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
794
+ }
795
+ }
796
+
797
+ ///////////////////////////////////////////////////////////////////////////////
798
+ /**
799
+ * VideoPlayerUIObject - A UI object that plays video
800
+ * @extends UIObject
801
+ * @example
802
+ * // Create a video player UI object
803
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
804
+ * video.play();
805
+ * @memberof UISystem
806
+ */
807
+ class UIVideo extends UIObject
808
+ {
809
+ /** Create a video player UI object
810
+ * @param {Vector2} [pos]
811
+ * @param {Vector2} [size]
812
+ * @param {string} src - Video file path or URL
813
+ * @param {boolean} [autoplay=false] - Start playing immediately?
814
+ * @param {boolean} [loop=false] - Loop the video?
815
+ * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
816
+ */
817
+ constructor(pos, size, src, autoplay=false, loop=false, volume=1)
818
+ {
819
+ super(pos, size || vec2());
820
+
821
+ ASSERT(isString(src), 'video src must be a string');
822
+ ASSERT(isNumber(volume), 'video volume must be a number');
823
+
824
+ this.color = BLACK; // default to black background
825
+ this.cornerRadius = 0; // default to no corner radius
826
+
827
+ /** @property {float} - The video volume */
828
+ this.volume = volume;
829
+
830
+ // create video element
831
+ /** @property {HTMLVideoElement} - The video player */
832
+ this.video = document.createElement('video');
833
+ this.video.loop = loop;
834
+ this.video.volume = clamp(volume * soundVolume);
835
+ this.video.muted = !soundEnable;
836
+ this.video.style.display = 'none';
837
+ this.video.src = src;
838
+ document.body.appendChild(this.video);
839
+ autoplay && this.play();
840
+ }
841
+
842
+ /** Play or resume the video
843
+ * @return {Promise} Promise that resolves when playback starts */
844
+ play()
845
+ {
846
+ // try to play the video, catch any errors (autoplay may be blocked)
847
+ const promise = this.video.play();
848
+ promise?.catch(()=>{});
849
+ return promise;
850
+ }
851
+
852
+ /** Pause the video */
853
+ pause() { this.video.pause(); }
854
+
855
+ /** Stop and reset the video */
856
+ stop() { this.video.pause(); this.video.currentTime = 0; }
857
+
858
+ /** Check if video is currently loading
859
+ * @return {boolean} */
860
+ isLoadng()
861
+ { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
862
+
863
+ /** Check if video is currently paused
864
+ * @return {boolean} */
865
+ isPaused() { return this.video.paused; }
866
+
867
+ /** Check if video is currently playing
868
+ * @return {boolean} */
869
+ isPlaying()
870
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
871
+
872
+ /** Check if video has ended playing
873
+ * @return {boolean} */
874
+ hasEnded() { return this.video.ended; }
875
+
876
+ /** Set volume (0-1)
877
+ * @param {number} volume - Volume level (0-1) */
878
+ setVolume(volume)
879
+ {
880
+ this.volume = volume;
881
+ this.video.volume = clamp(volume * soundVolume);
882
+ }
883
+
884
+ /** Set playback speed
885
+ * @param {number} rate - Playback rate multiplier */
886
+ setPlaybackRate(rate) { this.video.playbackRate = rate; }
887
+
888
+ /** Get current time in seconds
889
+ * @return {number} Current playback time */
890
+ getCurrentTime() { return this.video.currentTime || 0; }
891
+
892
+ /** Get duration in seconds
893
+ * @return {number} Total video duration */
894
+ getDuration() { return this.video.duration || 0; }
895
+
896
+ /** Get the native video dimensions
897
+ * @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
898
+ getVideoSize()
899
+ { return vec2(this.video.videoWidth, this.video.videoHeight); }
900
+
901
+ /** Seek to time in seconds
902
+ * @param {number} time - Time in seconds to seek to */
903
+ setTime(time)
904
+ { this.video.currentTime = clamp(time, 0, this.getDuration()); }
905
+
906
+ update()
907
+ {
908
+ super.update();
909
+
910
+ // update volume based on global sound volume
911
+ this.video.volume = clamp(this.volume * soundVolume);
912
+ }
913
+
914
+ /** Render video to UI canvas */
915
+ render()
916
+ {
917
+ super.render();
918
+
919
+ if (this.isLoadng())
920
+ return;
921
+ const context = uiSystem.uiContext;
922
+ const s = this.size;
923
+ context.save();
924
+ context.translate(this.pos.x, this.pos.y);
925
+ context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
926
+ context.restore();
927
+ }
928
+
929
+ /** Clean up video on destroy */
930
+ destroy()
931
+ {
932
+ if (this.destroyed)
933
+ return;
934
+
935
+ this.video.pause();
936
+ this.video.remove();
937
+ super.destroy();
736
938
  }
737
939
  }
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.14.24';
33
+ const engineVersion = '1.15.2';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -131,7 +131,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
131
131
 
132
132
  /**
133
133
  * @callback GameInitCallback - Called after the engine starts, can be async
134
- * @returns {void|Promise<void>}
134
+ * @return {void|Promise<void>}
135
135
  * @memberof Engine
136
136
  */
137
137
  /**
@@ -161,7 +161,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
161
161
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
162
162
  {
163
163
  ASSERT(!mainContext, 'engine already initialized');
164
- ASSERT(Array.isArray(imageSources), 'pass in images as array');
164
+ ASSERT(isArray(imageSources), 'pass in images as array');
165
165
 
166
166
  // allow passing in empty functions
167
167
  gameInit ||= ()=>{};
@@ -453,21 +453,24 @@ function engineObjectsUpdate()
453
453
  // recursive object update
454
454
  function updateObject(o)
455
455
  {
456
- if (!o.destroyed)
457
- {
458
- o.update();
459
- for (const child of o.children)
460
- updateObject(child);
461
- }
456
+ if (o.destroyed)
457
+ return;
458
+
459
+ o.update();
460
+ for (const child of o.children)
461
+ updateObject(child);
462
462
  }
463
463
  for (const o of engineObjects)
464
464
  {
465
+ if (o.parent)
466
+ continue;
467
+
465
468
  // update top level objects
466
- if (!o.parent)
467
- {
468
- updateObject(o);
469
- o.updateTransforms();
470
- }
469
+ o.update();
470
+ o.updatePhysics();
471
+ for (const child of o.children)
472
+ updateObject(child);
473
+ o.updateTransforms();
471
474
  }
472
475
 
473
476
  // remove destroyed objects
@@ -484,10 +487,10 @@ function engineObjectsDestroy()
484
487
  }
485
488
 
486
489
  /** Collects all object within a given area
487
- * @param {Vector2} [pos] - Center of test area, or undefined for all objects
488
- * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
490
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
491
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
489
492
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
490
- * @return {Array<EngineObject>} - List of collected objects
493
+ * @return {Array<EngineObject>} - List of collected objects
491
494
  * @memberof Engine */
492
495
  function engineObjectsCollect(pos, size, objects=engineObjects)
493
496
  {
@@ -500,7 +503,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
500
503
  else if (size instanceof Vector2) // bounding box test
501
504
  {
502
505
  for (const o of objects)
503
- isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
506
+ o.isOverlapping(pos, size) && collectedObjects.push(o);
504
507
  }
505
508
  else // circle test
506
509
  {
@@ -66,6 +66,10 @@ class Sound
66
66
  {
67
67
  if (!soundEnable || headlessMode) return;
68
68
 
69
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
70
+ ASSERT(isNumber(range), 'range must be a number');
71
+ ASSERT(isNumber(taper), 'taper must be a number');
72
+
69
73
  /** @property {number} - World space max range of sound */
70
74
  this.range = range;
71
75
  /** @property {number} - At what percentage of range should it start tapering */
@@ -104,6 +108,12 @@ class Sound
104
108
  */
105
109
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
106
110
  {
111
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
112
+ ASSERT(isNumber(volume), 'volume must be a number');
113
+ ASSERT(isNumber(pitch), 'pitch must be a number');
114
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
115
+
116
+
107
117
  if (!soundEnable || headlessMode) return;
108
118
  if (!this.sampleChannels) return;
109
119
 
@@ -149,6 +159,7 @@ class Sound
149
159
  */
150
160
  playNote(semitoneOffset=0, pos, volume)
151
161
  {
162
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
152
163
  const pitch = getNoteFrequency(semitoneOffset, 1);
153
164
  return this.play(pos, volume, pitch, 0);
154
165
  }
@@ -157,7 +168,7 @@ class Sound
157
168
  * @return {number} - How long the sound is in seconds (undefined if loading)
158
169
  */
159
170
  getDuration()
160
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
171
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
161
172
 
162
173
  /** Check if sound is loaded, for sounds fetched from a url
163
174
  * @return {boolean} - True if sound is loaded and ready to play
@@ -199,6 +210,7 @@ class SoundWave extends Sound
199
210
  super(undefined, range, taper);
200
211
  if (!soundEnable || headlessMode) return;
201
212
  ASSERT(!filename || isString(filename), 'filename must be a string');
213
+ ASSERT(isNumber(randomness), 'randomness must be a number');
202
214
 
203
215
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
204
216
  this.onloadCallback = onloadCallback;
@@ -415,7 +427,7 @@ class SoundInstance
415
427
  /** Get the total duration of this sound
416
428
  * @return {number} - Total duration in seconds
417
429
  */
418
- getDuration() { return this.sound.getDuration() / this.rate; }
430
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
419
431
 
420
432
  /** Get source of this sound instance
421
433
  * @return {AudioBufferSourceNode}
@@ -73,6 +73,12 @@ function LOG(...output) { console.log(...output); }
73
73
  * @memberof Debug */
74
74
  function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
75
75
  {
76
+ ASSERT(isVector2(pos), 'pos must be a vec2');
77
+ ASSERT(isVector2(size), 'size must be a vec2');
78
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
79
+ ASSERT(isNumber(time), 'time must be a number');
80
+ ASSERT(isNumber(angle), 'angle must be a number');
81
+
76
82
  if (typeof size === 'number')
77
83
  size = vec2(size); // allow passing in floats
78
84
  if (isColor(color))
@@ -93,6 +99,12 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
93
99
  * @memberof Debug */
94
100
  function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
95
101
  {
102
+ ASSERT(isVector2(pos), 'pos must be a vec2');
103
+ ASSERT(isArray(points), 'points must be an array');
104
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
105
+ ASSERT(isNumber(time), 'time must be a number');
106
+ ASSERT(isNumber(angle), 'angle must be a number');
107
+
96
108
  if (isColor(color))
97
109
  color = color.toString();
98
110
  pos = pos.copy();
@@ -110,6 +122,11 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
110
122
  * @memberof Debug */
111
123
  function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
112
124
  {
125
+ ASSERT(isVector2(pos), 'pos must be a vec2');
126
+ ASSERT(isNumber(size), 'size must be a number');
127
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
128
+ ASSERT(isNumber(time), 'time must be a number');
129
+
113
130
  if (isColor(color))
114
131
  color = color.toString();
115
132
  pos = pos.copy();
@@ -135,6 +152,10 @@ function debugPoint(pos, color, time, angle)
135
152
  * @memberof Debug */
136
153
  function debugLine(posA, posB, color, width=.1, time)
137
154
  {
155
+ ASSERT(isVector2(posA), 'posA must be a vec2');
156
+ ASSERT(isVector2(posB), 'posB must be sa vec2');
157
+ ASSERT(isNumber(width), 'width must be a number');
158
+
138
159
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
139
160
  const size = vec2(width, halfDelta.length()*2);
140
161
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
@@ -149,6 +170,11 @@ function debugLine(posA, posB, color, width=.1, time)
149
170
  * @memberof Debug */
150
171
  function debugOverlap(posA, sizeA, posB, sizeB, color)
151
172
  {
173
+ ASSERT(isVector2(posA), 'posA must be a vec2');
174
+ ASSERT(isVector2(posB), 'posB must be a vec2');
175
+ ASSERT(isVector2(sizeA), 'sizeA must be a vec2');
176
+ ASSERT(isVector2(sizeB), 'sizeB must be a vec2');
177
+
152
178
  const minPos = vec2(
153
179
  min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
154
180
  min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
@@ -171,6 +197,14 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
171
197
  * @memberof Debug */
172
198
  function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
173
199
  {
200
+ ASSERT(isString(text), 'text must be a string');
201
+ ASSERT(isVector2(pos), 'pos must be a vec2');
202
+ ASSERT(isNumber(size), 'size must be a number');
203
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
204
+ ASSERT(isNumber(time), 'time must be a number');
205
+ ASSERT(isNumber(angle), 'angle must be a number');
206
+ ASSERT(isString(font), 'font must be a string');
207
+
174
208
  if (isColor(color))
175
209
  color = color.toString();
176
210
  pos = pos.copy();
@@ -624,6 +658,9 @@ function debugVideoCaptureUpdate()
624
658
  debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
625
659
  }
626
660
 
661
+ ///////////////////////////////////////////////////////////////////////////////
662
+ // debug utility functions
663
+
627
664
  // make color constants immutable with debug assertions
628
665
  function debugProtectConstant(obj)
629
666
  {