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
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.24';
36
+ const engineVersion = '1.15.2';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -134,7 +134,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
134
134
 
135
135
  /**
136
136
  * @callback GameInitCallback - Called after the engine starts, can be async
137
- * @returns {void|Promise<void>}
137
+ * @return {void|Promise<void>}
138
138
  * @memberof Engine
139
139
  */
140
140
  /**
@@ -164,7 +164,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
164
164
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
165
  {
166
166
  ASSERT(!mainContext, 'engine already initialized');
167
- ASSERT(Array.isArray(imageSources), 'pass in images as array');
167
+ ASSERT(isArray(imageSources), 'pass in images as array');
168
168
 
169
169
  // allow passing in empty functions
170
170
  gameInit ||= ()=>{};
@@ -456,21 +456,24 @@ function engineObjectsUpdate()
456
456
  // recursive object update
457
457
  function updateObject(o)
458
458
  {
459
- if (!o.destroyed)
460
- {
461
- o.update();
462
- for (const child of o.children)
463
- updateObject(child);
464
- }
459
+ if (o.destroyed)
460
+ return;
461
+
462
+ o.update();
463
+ for (const child of o.children)
464
+ updateObject(child);
465
465
  }
466
466
  for (const o of engineObjects)
467
467
  {
468
+ if (o.parent)
469
+ continue;
470
+
468
471
  // update top level objects
469
- if (!o.parent)
470
- {
471
- updateObject(o);
472
- o.updateTransforms();
473
- }
472
+ o.update();
473
+ o.updatePhysics();
474
+ for (const child of o.children)
475
+ updateObject(child);
476
+ o.updateTransforms();
474
477
  }
475
478
 
476
479
  // remove destroyed objects
@@ -487,10 +490,10 @@ function engineObjectsDestroy()
487
490
  }
488
491
 
489
492
  /** Collects all object within a given area
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
493
+ * @param {Vector2} [pos] - Center of test area, or undefined for all objects
494
+ * @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
492
495
  * @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
493
- * @return {Array<EngineObject>} - List of collected objects
496
+ * @return {Array<EngineObject>} - List of collected objects
494
497
  * @memberof Engine */
495
498
  function engineObjectsCollect(pos, size, objects=engineObjects)
496
499
  {
@@ -503,7 +506,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
503
506
  else if (size instanceof Vector2) // bounding box test
504
507
  {
505
508
  for (const o of objects)
506
- isOverlapping(pos, size, o.pos, o.size) && collectedObjects.push(o);
509
+ o.isOverlapping(pos, size) && collectedObjects.push(o);
507
510
  }
508
511
  else // circle test
509
512
  {
@@ -905,7 +908,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
905
908
  * @param {number} valueA
906
909
  * @param {number} valueB
907
910
  * @param {number} [wrapSize]
908
- * @returns {number}
911
+ * @return {number}
909
912
  * @memberof Utilities */
910
913
  function distanceWrap(valueA, valueB, wrapSize=1)
911
914
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
@@ -915,7 +918,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
915
918
  * @param {number} valueB
916
919
  * @param {number} percent
917
920
  * @param {number} [wrapSize]
918
- * @returns {number}
921
+ * @return {number}
919
922
  * @memberof Utilities */
920
923
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
921
924
  {
@@ -927,7 +930,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
927
930
  /** Returns signed wrapped distance between the two angles passed in
928
931
  * @param {number} angleA
929
932
  * @param {number} angleB
930
- * @returns {number}
933
+ * @return {number}
931
934
  * @memberof Utilities */
932
935
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
933
936
 
@@ -935,7 +938,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
935
938
  * @param {number} angleA
936
939
  * @param {number} angleB
937
940
  * @param {number} percent
938
- * @returns {number}
941
+ * @return {number}
939
942
  * @memberof Utilities */
940
943
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
941
944
 
@@ -1062,6 +1065,13 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1062
1065
  * @memberof Utilities */
1063
1066
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1064
1067
 
1068
+ /**
1069
+ * Check if object is an array
1070
+ * @param {any} a
1071
+ * @return {boolean}
1072
+ * @memberof Utilities */
1073
+ function isArray(a) { return Array.isArray(a); }
1074
+
1065
1075
  /**
1066
1076
  * @callback LineTestFunction - Checks if a position is colliding
1067
1077
  * @param {Vector2} pos
@@ -1761,7 +1771,6 @@ class Color
1761
1771
  * @return {string} */
1762
1772
  toString(useAlpha = true)
1763
1773
  {
1764
- ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1765
1774
  if (debug && !this.isValid())
1766
1775
  return `#000`;
1767
1776
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
@@ -1898,11 +1907,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1898
1907
  class Timer
1899
1908
  {
1900
1909
  /** Create a timer object set time passed in
1901
- * @param {number} [timeLeft] - How much time left before the timer elapses in seconds */
1902
- constructor(timeLeft)
1910
+ * @param {number} [timeLeft] - How much time left before the timer
1911
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1912
+ constructor(timeLeft, useRealTime=false)
1903
1913
  {
1904
1914
  ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1905
- this.time = timeLeft === undefined ? undefined : time + timeLeft;
1915
+ this.useRealTime = useRealTime;
1916
+ const globalTime = this.getGlobalTime();
1917
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1906
1918
  this.setTime = timeLeft;
1907
1919
  }
1908
1920
 
@@ -1911,10 +1923,19 @@ class Timer
1911
1923
  set(timeLeft=0)
1912
1924
  {
1913
1925
  ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
1914
- this.time = time + timeLeft;
1926
+ const globalTime = this.getGlobalTime();
1927
+ this.time = globalTime + timeLeft;
1915
1928
  this.setTime = timeLeft;
1916
1929
  }
1917
1930
 
1931
+ /** Set if the timer should keep running even when the game is paused
1932
+ * @param {boolean} [useRealTime] */
1933
+ setUseRealTime(useRealTime=true)
1934
+ {
1935
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
1936
+ this.useRealTime = useRealTime;
1937
+ }
1938
+
1918
1939
  /** Unset the timer */
1919
1940
  unset() { this.time = undefined; }
1920
1941
 
@@ -1924,24 +1945,28 @@ class Timer
1924
1945
 
1925
1946
  /** Returns true if set and has not elapsed
1926
1947
  * @return {boolean} */
1927
- active() { return time < this.time; }
1948
+ active() { return this.getGlobalTime() < this.time; }
1928
1949
 
1929
1950
  /** Returns true if set and elapsed
1930
1951
  * @return {boolean} */
1931
- elapsed() { return time >= this.time; }
1952
+ elapsed() { return this.getGlobalTime() >= this.time; }
1932
1953
 
1933
1954
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1934
1955
  * @return {number} */
1935
- get() { return this.isSet()? time - this.time : 0; }
1956
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
1936
1957
 
1937
1958
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
1938
1959
  * @return {number} */
1939
- getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
1960
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
1940
1961
 
1941
1962
  /** Get the time this timer was set to, returns 0 if not set
1942
1963
  * @return {number} */
1943
1964
  getSetTime() { return this.isSet() ? this.setTime : 0; }
1944
1965
 
1966
+ /** Get the current global time this timer is based on
1967
+ * @return {number} */
1968
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
1969
+
1945
1970
  /** Returns this timer expressed as a string
1946
1971
  * @return {string} */
1947
1972
  toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
@@ -2659,11 +2684,10 @@ class EngineObject
2659
2684
  }
2660
2685
 
2661
2686
  /** Update the object physics, called automatically by engine once each frame */
2662
- update()
2687
+ updatePhysics()
2663
2688
  {
2664
2689
  // child objects do not have physics
2665
- if (this.parent)
2666
- return;
2690
+ ASSERT(!this.parent);
2667
2691
 
2668
2692
  if (this.clampSpeed)
2669
2693
  {
@@ -2713,7 +2737,7 @@ class EngineObject
2713
2737
  continue;
2714
2738
 
2715
2739
  // check collision
2716
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
2740
+ if (!this.isOverlappingObject(o))
2717
2741
  continue;
2718
2742
 
2719
2743
  // notify objects of collision and check if should be resolved
@@ -2861,6 +2885,9 @@ class EngineObject
2861
2885
  }
2862
2886
  }
2863
2887
 
2888
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
2889
+ update() {}
2890
+
2864
2891
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
2865
2892
  render()
2866
2893
  {
@@ -2926,6 +2953,10 @@ class EngineObject
2926
2953
  * @return {number} */
2927
2954
  getAliveTime() { return time - this.spawnTime; }
2928
2955
 
2956
+ /** Get the speed of this object
2957
+ * @return {number} */
2958
+ getSpeed() { return this.velocity.length(); }
2959
+
2929
2960
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
2930
2961
  * @param {Vector2} acceleration */
2931
2962
  applyAcceleration(acceleration)
@@ -2970,6 +3001,20 @@ class EngineObject
2970
3001
  child.parent = 0;
2971
3002
  }
2972
3003
 
3004
+ /** Check if overlapping another engine object
3005
+ * Collisions are resoloved to prevent overlaps
3006
+ * @param {EngineObject} object
3007
+ * @return {boolean} */
3008
+ isOverlappingObject(object)
3009
+ { return this.isOverlapping(object.pos, object.size); }
3010
+
3011
+ /** Check if overlapping a point or aligned bounding box
3012
+ * @param {Vector2} pos - Center of box
3013
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3014
+ * @return {boolean} */
3015
+ isOverlapping(pos, size=vec2())
3016
+ { return isOverlapping(this.pos, this.size, pos, size); }
3017
+
2973
3018
  /** Set how this object collides
2974
3019
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
2975
3020
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -3315,7 +3360,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3315
3360
  {
3316
3361
  // normal canvas 2D rendering method (slower)
3317
3362
  ++drawCount;
3318
- size = new Vector2(size.x, -size.y); // fix upside down sprites
3363
+ size = new Vector2(size.x, -size.y); // flip upside down sprites
3319
3364
  drawCanvas2D(pos, size, angle, mirror, (context)=>
3320
3365
  {
3321
3366
  if (textureInfo)
@@ -3424,7 +3469,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3424
3469
  * @memberof Draw */
3425
3470
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3426
3471
  {
3427
- ASSERT(Array.isArray(points), 'points must be an array');
3472
+ ASSERT(isArray(points), 'points must be an array');
3428
3473
  ASSERT(isNumber(width), 'width must be a number');
3429
3474
  ASSERT(isColor(color), 'color is invalid');
3430
3475
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3530,7 +3575,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3530
3575
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3531
3576
  {
3532
3577
  ASSERT(isVector2(pos), 'pos must be a vec2');
3533
- ASSERT(Array.isArray(points), 'points must be an array');
3578
+ ASSERT(isArray(points), 'points must be an array');
3534
3579
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3535
3580
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3536
3581
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -3682,11 +3727,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3682
3727
  * @param {string} [font=fontDefault]
3683
3728
  * @param {string} [fontStyle]
3684
3729
  * @param {number} [maxWidth]
3730
+ * @param {number} [angle]
3685
3731
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3686
3732
  * @memberof Draw */
3687
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3733
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
3688
3734
  {
3689
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3735
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3690
3736
  }
3691
3737
 
3692
3738
  /** Draw text on overlay canvas in world space
@@ -3701,10 +3747,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3701
3747
  * @param {string} [font=fontDefault]
3702
3748
  * @param {string} [fontStyle]
3703
3749
  * @param {number} [maxWidth]
3750
+ * @param {number} [angle]
3704
3751
  * @memberof Draw */
3705
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3752
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
3706
3753
  {
3707
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3754
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
3708
3755
  }
3709
3756
 
3710
3757
  /** Draw text on overlay canvas in screen space
@@ -3719,9 +3766,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3719
3766
  * @param {string} [font=fontDefault]
3720
3767
  * @param {string} [fontStyle]
3721
3768
  * @param {number} [maxWidth]
3769
+ * @param {number} [angle]
3722
3770
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3723
3771
  * @memberof Draw */
3724
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
3772
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
3725
3773
  {
3726
3774
  ASSERT(isString(text), 'text must be a string');
3727
3775
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3732,6 +3780,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3732
3780
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3733
3781
  ASSERT(isString(font), 'font must be a string');
3734
3782
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
3783
+ ASSERT(isNumber(angle), 'angle must be a number');
3735
3784
 
3736
3785
  context.fillStyle = color.toString();
3737
3786
  context.strokeStyle = lineColor.toString();
@@ -3741,14 +3790,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3741
3790
  context.textBaseline = 'middle';
3742
3791
 
3743
3792
  const lines = (text+'').split('\n');
3744
- let posY = pos.y;
3745
- posY -= (lines.length-1) * size/2; // center text vertically
3793
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
3794
+ context.save();
3795
+ context.translate(pos.x, posY);
3796
+ context.rotate(-angle);
3797
+ let yOffset = 0;
3746
3798
  lines.forEach(line=>
3747
3799
  {
3748
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
3749
- context.fillText(line, pos.x, posY, maxWidth);
3750
- posY += size;
3800
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
3801
+ context.fillText(line, 0, yOffset, maxWidth);
3802
+ yOffset += size;
3751
3803
  });
3804
+ context.restore();
3752
3805
  }
3753
3806
 
3754
3807
  ///////////////////////////////////////////////////////////////////////////////
@@ -4180,6 +4233,11 @@ let mouseDeltaScreen = vec2();
4180
4233
  * @memberof Input */
4181
4234
  let mouseWheel = 0;
4182
4235
 
4236
+ /** True if mouse was inside the document window, set to false when mouse leaves
4237
+ * @type {boolean}
4238
+ * @memberof Input */
4239
+ let mouseInWindow = true;
4240
+
4183
4241
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
4184
4242
  * @type {boolean}
4185
4243
  * @memberof Input */
@@ -4274,6 +4332,7 @@ function inputInit()
4274
4332
  document.addEventListener('mousedown', onMouseDown);
4275
4333
  document.addEventListener('mouseup', onMouseUp);
4276
4334
  document.addEventListener('mousemove', onMouseMove);
4335
+ document.addEventListener('mouseleave', onMouseLeave);
4277
4336
  document.addEventListener('wheel', onMouseWheel);
4278
4337
  document.addEventListener('contextmenu', onContextMenu);
4279
4338
  document.addEventListener('blur', onBlur);
@@ -4298,14 +4357,14 @@ function inputInit()
4298
4357
  if (inputWASDEmulateDirection)
4299
4358
  inputData[0][remapKey(e.code)] = 4;
4300
4359
  }
4301
- function remapKey(c)
4360
+ function remapKey(k)
4302
4361
  {
4303
4362
  // handle remapping wasd keys to directions
4304
4363
  return inputWASDEmulateDirection ?
4305
- c === 'KeyW' ? 'ArrowUp' :
4306
- c === 'KeyS' ? 'ArrowDown' :
4307
- c === 'KeyA' ? 'ArrowLeft' :
4308
- c === 'KeyD' ? 'ArrowRight' : c : c;
4364
+ k === 'KeyW' ? 'ArrowUp' :
4365
+ k === 'KeyS' ? 'ArrowDown' :
4366
+ k === 'KeyA' ? 'ArrowLeft' :
4367
+ k === 'KeyD' ? 'ArrowRight' : k : k;
4309
4368
  }
4310
4369
  function onMouseDown(e)
4311
4370
  {
@@ -4319,7 +4378,7 @@ function inputInit()
4319
4378
  isUsingGamepad = false;
4320
4379
  inputData[0][e.button] = 3;
4321
4380
 
4322
- let mousePosScreenLast = mousePosScreen;
4381
+ const mousePosScreenLast = mousePosScreen;
4323
4382
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4324
4383
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4325
4384
  inputPreventDefault && e.button && e.preventDefault();
@@ -4332,10 +4391,17 @@ function inputInit()
4332
4391
  }
4333
4392
  function onMouseMove(e)
4334
4393
  {
4335
- let mousePosScreenLast = mousePosScreen;
4394
+ mouseInWindow = true;
4395
+ const mousePosScreenLast = mousePosScreen;
4336
4396
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4337
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4397
+
4398
+ // when pointer is locked use movementX/Y for delta
4399
+ const movement = pointerLockIsActive() ?
4400
+ vec2(e.movementX, e.movementY) :
4401
+ mousePosScreen.subtract(mousePosScreenLast);
4402
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
4338
4403
  }
4404
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
4339
4405
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4340
4406
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4341
4407
  function onBlur() { inputClear(); } // reset input when focus is lost
@@ -4474,7 +4540,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
4474
4540
  function touchGamepadButtonCenter()
4475
4541
  {
4476
4542
  // draw right face buttons
4477
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4543
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4478
4544
  if (touchGamepadButtonCount <= 2)
4479
4545
  center.x += touchGamepadSize/2;
4480
4546
  return center;
@@ -4484,14 +4550,13 @@ function touchGamepadButtonCenter()
4484
4550
  function touchInputInit()
4485
4551
  {
4486
4552
  // add non passive touch event listeners
4487
- let handleTouch = handleTouchDefault;
4488
4553
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4489
4554
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4490
4555
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4491
4556
 
4492
4557
  // handle all touch events the same way
4493
4558
  let wasTouching;
4494
- function handleTouchDefault(e)
4559
+ function handleTouch(e)
4495
4560
  {
4496
4561
  if (!touchInputEnable)
4497
4562
  return;
@@ -4642,7 +4707,7 @@ function touchGamepadRender()
4642
4707
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
4643
4708
  for (let i=0; i<touchGamepadButtonCount; i++)
4644
4709
  {
4645
- let j = mod(i-1, 4);
4710
+ const j = mod(i-1, 4);
4646
4711
  let button = touchGamepadButtonCount > 2 ?
4647
4712
  j : min(j, touchGamepadButtonCount-1);
4648
4713
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -4744,6 +4809,10 @@ class Sound
4744
4809
  {
4745
4810
  if (!soundEnable || headlessMode) return;
4746
4811
 
4812
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
4813
+ ASSERT(isNumber(range), 'range must be a number');
4814
+ ASSERT(isNumber(taper), 'taper must be a number');
4815
+
4747
4816
  /** @property {number} - World space max range of sound */
4748
4817
  this.range = range;
4749
4818
  /** @property {number} - At what percentage of range should it start tapering */
@@ -4782,6 +4851,12 @@ class Sound
4782
4851
  */
4783
4852
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4784
4853
  {
4854
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
4855
+ ASSERT(isNumber(volume), 'volume must be a number');
4856
+ ASSERT(isNumber(pitch), 'pitch must be a number');
4857
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
4858
+
4859
+
4785
4860
  if (!soundEnable || headlessMode) return;
4786
4861
  if (!this.sampleChannels) return;
4787
4862
 
@@ -4827,6 +4902,7 @@ class Sound
4827
4902
  */
4828
4903
  playNote(semitoneOffset=0, pos, volume)
4829
4904
  {
4905
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
4830
4906
  const pitch = getNoteFrequency(semitoneOffset, 1);
4831
4907
  return this.play(pos, volume, pitch, 0);
4832
4908
  }
@@ -4835,7 +4911,7 @@ class Sound
4835
4911
  * @return {number} - How long the sound is in seconds (undefined if loading)
4836
4912
  */
4837
4913
  getDuration()
4838
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
4914
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
4839
4915
 
4840
4916
  /** Check if sound is loaded, for sounds fetched from a url
4841
4917
  * @return {boolean} - True if sound is loaded and ready to play
@@ -4877,6 +4953,7 @@ class SoundWave extends Sound
4877
4953
  super(undefined, range, taper);
4878
4954
  if (!soundEnable || headlessMode) return;
4879
4955
  ASSERT(!filename || isString(filename), 'filename must be a string');
4956
+ ASSERT(isNumber(randomness), 'randomness must be a number');
4880
4957
 
4881
4958
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
4882
4959
  this.onloadCallback = onloadCallback;
@@ -5093,7 +5170,7 @@ class SoundInstance
5093
5170
  /** Get the total duration of this sound
5094
5171
  * @return {number} - Total duration in seconds
5095
5172
  */
5096
- getDuration() { return this.sound.getDuration() / this.rate; }
5173
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
5097
5174
 
5098
5175
  /** Get source of this sound instance
5099
5176
  * @return {AudioBufferSourceNode}
@@ -5407,6 +5484,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5407
5484
  }
5408
5485
 
5409
5486
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
5487
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
5410
5488
  * @param {Vector2} posStart
5411
5489
  * @param {Vector2} posEnd
5412
5490
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -5980,6 +6058,7 @@ class TileCollisionLayer extends TileLayer
5980
6058
  }
5981
6059
 
5982
6060
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6061
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
5983
6062
  * @param {Vector2} posStart
5984
6063
  * @param {Vector2} posEnd
5985
6064
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -6172,12 +6251,12 @@ class ParticleEmitter extends EngineObject
6172
6251
  this.previousPos = this.pos.copy();
6173
6252
  }
6174
6253
 
6254
+ /** Emitters do not have physics */
6255
+ updatePhysics() {}
6256
+
6175
6257
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6176
6258
  update()
6177
6259
  {
6178
- // only do default update to apply parent transforms
6179
- this.parent && super.update();
6180
-
6181
6260
  if (this.velocityInheritance)
6182
6261
  {
6183
6262
  // pass emitter velocity to particles
@@ -6194,7 +6273,7 @@ class ParticleEmitter extends EngineObject
6194
6273
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6195
6274
  {
6196
6275
  // emit particles
6197
- if (this.emitRate * particleEmitRateScale)
6276
+ if (this.emitRate && particleEmitRateScale)
6198
6277
  {
6199
6278
  const rate = 1/this.emitRate/particleEmitRateScale;
6200
6279
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -6332,8 +6411,6 @@ class Particle extends EngineObject
6332
6411
  /** Update the object physics, called automatically by engine once each frame */
6333
6412
  update()
6334
6413
  {
6335
- super.update();
6336
-
6337
6414
  if (this.collideTiles || this.collideSolidObjects)
6338
6415
  {
6339
6416
  // only apply max circular speed if particle can collide
@@ -6577,11 +6654,11 @@ class Medal
6577
6654
  const descriptionSize = height*.3;
6578
6655
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
6579
6656
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
6580
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6657
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6581
6658
 
6582
6659
  // draw the description
6583
6660
  pos.y = y + height - gap.y*2 - descriptionSize/2;
6584
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6661
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6585
6662
  context.restore();
6586
6663
  }
6587
6664
 
@@ -8030,11 +8107,16 @@ class UISystemPlugin
8030
8107
  // reset hover object at start of update
8031
8108
  uiSystem.lastHoverObject = uiSystem.hoverObject;
8032
8109
  uiSystem.hoverObject = undefined;
8110
+
8111
+ // update in reverse order so topmost objects get priority
8033
8112
  for (let i = uiSystem.uiObjects.length; i--;)
8034
8113
  {
8035
8114
  const o = uiSystem.uiObjects[i];
8036
8115
  o.parent || updateObject(o)
8037
8116
  }
8117
+
8118
+ // remove destroyed objects
8119
+ uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
8038
8120
  }
8039
8121
  function uiRender()
8040
8122
  {
@@ -8067,12 +8149,12 @@ class UISystemPlugin
8067
8149
  /** Draw a rectangle to the UI context
8068
8150
  * @param {Vector2} pos
8069
8151
  * @param {Vector2} size
8070
- * @param {Color} [color=uiSystem.defaultColor]
8071
- * @param {number} [lineWidth=uiSystem.defaultLineWidth]
8072
- * @param {Color} [lineColor=uiSystem.defaultLineColor]
8073
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
8074
- * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
8075
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
8152
+ * @param {Color} [color]
8153
+ * @param {number} [lineWidth]
8154
+ * @param {Color} [lineColor]
8155
+ * @param {number} [cornerRadius]
8156
+ * @param {Color} [gradientColor] */
8157
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
8076
8158
  {
8077
8159
  ASSERT(isVector2(pos), 'pos must be a vec2');
8078
8160
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8151,10 +8233,14 @@ class UISystemPlugin
8151
8233
  * @param {string} [align]
8152
8234
  * @param {string} [font=uiSystem.defaultFont]
8153
8235
  * @param {string} [fontStyle]
8154
- * @param {boolean} [applyMaxWidth=true] */
8155
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
8236
+ * @param {boolean} [applyMaxWidth=true]
8237
+ * @param {Vector2} [textShadow]
8238
+ */
8239
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
8156
8240
  {
8157
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8241
+ if (textShadow)
8242
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8243
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8158
8244
  }
8159
8245
 
8160
8246
  /**
@@ -8181,6 +8267,34 @@ class UISystemPlugin
8181
8267
  setCallback(onDragLeave, 'dragleave');
8182
8268
  setCallback(onDragOver, 'dragover');
8183
8269
  }
8270
+
8271
+ /** Convert a screen space position to native UI position
8272
+ * @param {Vector2} pos
8273
+ * @return {Vector2}
8274
+ */
8275
+ screenToNative(pos)
8276
+ {
8277
+ if (!uiSystem.nativeHeight)
8278
+ return pos;
8279
+
8280
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8281
+ const sInv = 1/s;
8282
+ const p = pos.copy();
8283
+ p.x += s*mainCanvasSize.x/2;
8284
+ p.x *= sInv;
8285
+ p.y *= sInv;
8286
+ p.x -= sInv*mainCanvasSize.x/2;
8287
+ return p;
8288
+ }
8289
+
8290
+ /** Destroy and remove all objects
8291
+ * @memberof Engine */
8292
+ destroyObjects()
8293
+ {
8294
+ for (const o of this.uiObjects)
8295
+ o.parent || o.destroy();
8296
+ this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
8297
+ }
8184
8298
  }
8185
8299
 
8186
8300
  ///////////////////////////////////////////////////////////////////////////////
@@ -8257,6 +8371,9 @@ class UIObject
8257
8371
  /** @property {boolean} - True if this can be a hover object */
8258
8372
  this.canBeHover = true;
8259
8373
  uiSystem.uiObjects.push(this);
8374
+
8375
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
8376
+ this.textShadow = undefined;
8260
8377
  }
8261
8378
 
8262
8379
  /** Add a child UIObject to this object
@@ -8279,29 +8396,41 @@ class UIObject
8279
8396
  child.parent = undefined;
8280
8397
  }
8281
8398
 
8399
+
8400
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
8401
+ destroy()
8402
+ {
8403
+ if (this.destroyed)
8404
+ return;
8405
+
8406
+ // disconnect from parent and destroy children
8407
+ this.destroyed = 1;
8408
+ this.parent && this.parent.removeChild(this);
8409
+ for (const child of this.children)
8410
+ {
8411
+ child.parent = 0;
8412
+ child.destroy();
8413
+ }
8414
+ }
8282
8415
  /** Check if the mouse is overlapping a box in screen space
8283
8416
  * @return {boolean} - True if overlapping
8284
8417
  */
8285
8418
  isMouseOverlapping()
8286
8419
  {
8420
+ if (!mouseInWindow) return false;
8421
+
8287
8422
  const size = !isTouchDevice ? this.size :
8288
8423
  this.size.add(vec2(this.extraTouchSize || 0));
8289
- if (!uiSystem.nativeHeight)
8290
- return isOverlapping(this.pos, size, mousePosScreen);
8291
-
8292
- const s = mainCanvasSize.y / uiSystem.nativeHeight;
8293
- const sInv = 1/s;
8294
- let pos = mousePosScreen.copy();
8295
- pos.x += s*mainCanvasSize.x/2;
8296
- pos.x *= sInv;
8297
- pos.y *= sInv;
8298
- pos.x -= sInv*mainCanvasSize.x/2;
8424
+ const pos = uiSystem.screenToNative(mousePosScreen);
8299
8425
  return isOverlapping(this.pos, size, pos);
8300
8426
  }
8301
8427
 
8302
8428
  /** Update the object, called automatically by plugin once each frame */
8303
8429
  update()
8304
8430
  {
8431
+ // call the custom update callback
8432
+ this.onUpdate();
8433
+
8305
8434
  const wasHover = uiSystem.lastHoverObject === this;
8306
8435
  const isActive = this.isActiveObject();
8307
8436
  const mouseDown = mouseIsDown(0);
@@ -8358,7 +8487,7 @@ class UIObject
8358
8487
 
8359
8488
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8360
8489
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8361
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8490
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
8362
8491
  }
8363
8492
 
8364
8493
  /** Special update when object is not visible */
@@ -8385,6 +8514,9 @@ class UIObject
8385
8514
  /** @return {boolean} - Is the mouse held onto this element */
8386
8515
  isActiveObject() { return uiSystem.activeObject === this; }
8387
8516
 
8517
+ /** Called each frame when object updates */
8518
+ onUpdate() {}
8519
+
8388
8520
  /** Called when the mouse enters the object */
8389
8521
  onEnter() {}
8390
8522
 
@@ -8439,8 +8571,9 @@ class UIText extends UIObject
8439
8571
  }
8440
8572
  render()
8441
8573
  {
8574
+ // only render the text
8442
8575
  const textSize = this.getTextSize();
8443
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8576
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
8444
8577
  }
8445
8578
  }
8446
8579
 
@@ -8516,7 +8649,7 @@ class UIButton extends UIObject
8516
8649
  // draw the text scaled to fit
8517
8650
  const textSize = this.getTextSize();
8518
8651
  uiSystem.drawText(this.text, this.pos, textSize,
8519
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8652
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8520
8653
  }
8521
8654
  }
8522
8655
 
@@ -8570,7 +8703,7 @@ class UICheckbox extends UIObject
8570
8703
  const textSize = this.getTextSize();
8571
8704
  const pos = this.pos.add(vec2(this.size.x,0));
8572
8705
  uiSystem.drawText(this.text, pos, textSize,
8573
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8706
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
8574
8707
  }
8575
8708
  }
8576
8709
 
@@ -8625,9 +8758,11 @@ class UIScrollbar extends UIObject
8625
8758
  const p1 = centerPos - handleWidth/2;
8626
8759
  const p2 = centerPos + handleWidth/2;
8627
8760
  const oldValue = this.value;
8761
+
8762
+ const p = uiSystem.screenToNative(mousePosScreen);
8628
8763
  this.value = isHorizontal ?
8629
- percent(mousePosScreen.x, p1, p2) :
8630
- percent(mousePosScreen.y, p2, p1);
8764
+ percent(p.x, p1, p2) :
8765
+ percent(p.y, p2, p1);
8631
8766
  this.value === oldValue || this.onChange();
8632
8767
  }
8633
8768
  }
@@ -8649,20 +8784,164 @@ class UIScrollbar extends UIObject
8649
8784
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8650
8785
  vec2(this.pos.x, lerp(p2, p1, this.value))
8651
8786
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8652
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8787
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
8653
8788
 
8654
8789
  // draw the text scaled to fit on the scrollbar
8655
8790
  const textSize = this.getTextSize();
8656
8791
  uiSystem.drawText(this.text, this.pos, textSize,
8657
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8792
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8793
+ }
8794
+ }
8795
+
8796
+ ///////////////////////////////////////////////////////////////////////////////
8797
+ /**
8798
+ * VideoPlayerUIObject - A UI object that plays video
8799
+ * @extends UIObject
8800
+ * @example
8801
+ * // Create a video player UI object
8802
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
8803
+ * video.play();
8804
+ * @memberof UISystem
8805
+ */
8806
+ class UIVideo extends UIObject
8807
+ {
8808
+ /** Create a video player UI object
8809
+ * @param {Vector2} [pos]
8810
+ * @param {Vector2} [size]
8811
+ * @param {string} src - Video file path or URL
8812
+ * @param {boolean} [autoplay=false] - Start playing immediately?
8813
+ * @param {boolean} [loop=false] - Loop the video?
8814
+ * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
8815
+ */
8816
+ constructor(pos, size, src, autoplay=false, loop=false, volume=1)
8817
+ {
8818
+ super(pos, size || vec2());
8819
+
8820
+ ASSERT(isString(src), 'video src must be a string');
8821
+ ASSERT(isNumber(volume), 'video volume must be a number');
8822
+
8823
+ this.color = BLACK; // default to black background
8824
+ this.cornerRadius = 0; // default to no corner radius
8825
+
8826
+ /** @property {float} - The video volume */
8827
+ this.volume = volume;
8828
+
8829
+ // create video element
8830
+ /** @property {HTMLVideoElement} - The video player */
8831
+ this.video = document.createElement('video');
8832
+ this.video.loop = loop;
8833
+ this.video.volume = clamp(volume * soundVolume);
8834
+ this.video.muted = !soundEnable;
8835
+ this.video.style.display = 'none';
8836
+ this.video.src = src;
8837
+ document.body.appendChild(this.video);
8838
+ autoplay && this.play();
8839
+ }
8840
+
8841
+ /** Play or resume the video
8842
+ * @return {Promise} Promise that resolves when playback starts */
8843
+ play()
8844
+ {
8845
+ // try to play the video, catch any errors (autoplay may be blocked)
8846
+ const promise = this.video.play();
8847
+ promise?.catch(()=>{});
8848
+ return promise;
8849
+ }
8850
+
8851
+ /** Pause the video */
8852
+ pause() { this.video.pause(); }
8853
+
8854
+ /** Stop and reset the video */
8855
+ stop() { this.video.pause(); this.video.currentTime = 0; }
8856
+
8857
+ /** Check if video is currently loading
8858
+ * @return {boolean} */
8859
+ isLoadng()
8860
+ { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
8861
+
8862
+ /** Check if video is currently paused
8863
+ * @return {boolean} */
8864
+ isPaused() { return this.video.paused; }
8865
+
8866
+ /** Check if video is currently playing
8867
+ * @return {boolean} */
8868
+ isPlaying()
8869
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
8870
+
8871
+ /** Check if video has ended playing
8872
+ * @return {boolean} */
8873
+ hasEnded() { return this.video.ended; }
8874
+
8875
+ /** Set volume (0-1)
8876
+ * @param {number} volume - Volume level (0-1) */
8877
+ setVolume(volume)
8878
+ {
8879
+ this.volume = volume;
8880
+ this.video.volume = clamp(volume * soundVolume);
8881
+ }
8882
+
8883
+ /** Set playback speed
8884
+ * @param {number} rate - Playback rate multiplier */
8885
+ setPlaybackRate(rate) { this.video.playbackRate = rate; }
8886
+
8887
+ /** Get current time in seconds
8888
+ * @return {number} Current playback time */
8889
+ getCurrentTime() { return this.video.currentTime || 0; }
8890
+
8891
+ /** Get duration in seconds
8892
+ * @return {number} Total video duration */
8893
+ getDuration() { return this.video.duration || 0; }
8894
+
8895
+ /** Get the native video dimensions
8896
+ * @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
8897
+ getVideoSize()
8898
+ { return vec2(this.video.videoWidth, this.video.videoHeight); }
8899
+
8900
+ /** Seek to time in seconds
8901
+ * @param {number} time - Time in seconds to seek to */
8902
+ setTime(time)
8903
+ { this.video.currentTime = clamp(time, 0, this.getDuration()); }
8904
+
8905
+ update()
8906
+ {
8907
+ super.update();
8908
+
8909
+ // update volume based on global sound volume
8910
+ this.video.volume = clamp(this.volume * soundVolume);
8911
+ }
8912
+
8913
+ /** Render video to UI canvas */
8914
+ render()
8915
+ {
8916
+ super.render();
8917
+
8918
+ if (this.isLoadng())
8919
+ return;
8920
+ const context = uiSystem.uiContext;
8921
+ const s = this.size;
8922
+ context.save();
8923
+ context.translate(this.pos.x, this.pos.y);
8924
+ context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
8925
+ context.restore();
8926
+ }
8927
+
8928
+ /** Clean up video on destroy */
8929
+ destroy()
8930
+ {
8931
+ if (this.destroyed)
8932
+ return;
8933
+
8934
+ this.video.pause();
8935
+ this.video.remove();
8936
+ super.destroy();
8658
8937
  }
8659
8938
  }
8660
8939
  /**
8661
8940
  * LittleJS Box2D Physics Plugin
8662
8941
  * - Box2dObject extends EngineObject with Box2D physics
8663
- * - Call box2dInit() before engineInit() to enable
8942
+ * - Call box2dInit() to enable
8664
8943
  * - You will also need to include box2d.wasm.js
8665
- * - Uses a super fast web assembly port of Box2D
8944
+ * - Uses a super fast web assembly port of Box2D v2.3.1
8666
8945
  * - More info: https://github.com/kripken/box2d.js
8667
8946
  * - Functions to create polygon, circle, and edge shapes
8668
8947
  * - Contact begin and end callbacks
@@ -8692,9 +8971,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8692
8971
  ///////////////////////////////////////////////////////////////////////////////
8693
8972
  /**
8694
8973
  * Box2D Object - extend with your own custom physics objects
8695
- * - A LittleJS object with Box2D physics
8696
- * - Each object has a Box2D body which can have multiple fixtures and joints
8974
+ * - A LittleJS object with Box2D physics, dynamic by default
8697
8975
  * - Provides interface for Box2D body and fixture functions
8976
+ * - Each object can have multiple fixtures and joints
8698
8977
  * @extends EngineObject
8699
8978
  * @memberof Box2D
8700
8979
  */
@@ -8708,7 +8987,7 @@ class Box2dObject extends EngineObject
8708
8987
  * @param {Color} [color]
8709
8988
  * @param {number} [bodyType]
8710
8989
  * @param {number} [renderOrder] */
8711
- constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8990
+ constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8712
8991
  {
8713
8992
  super(pos, size, tileInfo, angle, color, renderOrder);
8714
8993
 
@@ -8720,24 +8999,27 @@ class Box2dObject extends EngineObject
8720
8999
  this.body = box2d.world.CreateBody(bodyDef);
8721
9000
  this.body.object = this;
8722
9001
  this.lineColor = BLACK;
9002
+ box2d.objects.push(this);
9003
+
9004
+ // edge lists and loops for drawing
9005
+ this.edgeLists = [];
9006
+ this.edgeLoops = [];
8723
9007
  }
8724
9008
 
8725
9009
  /** Destroy this object and its physics body */
8726
9010
  destroy()
8727
9011
  {
9012
+ if (this.destroyed)
9013
+ return;
9014
+
8728
9015
  // destroy physics body, fixtures, and joints
8729
- this.body && box2d.world.DestroyBody(this.body);
8730
- this.body = 0;
9016
+ ASSERT(this.body, 'Box2dObject has no body to destroy');
9017
+ box2d.world.DestroyBody(this.body);
8731
9018
  super.destroy();
8732
9019
  }
8733
9020
 
8734
- /** Copy box2d update sim data */
8735
- update()
8736
- {
8737
- // use box2d physics update instead of normal engine update
8738
- this.pos = box2d.vec2From(this.body.GetPosition());
8739
- this.angle = -this.body.GetAngle();
8740
- }
9021
+ /** Box2d objects updated with Box2d world step */
9022
+ updatePhysics() {}
8741
9023
 
8742
9024
  /** Render the object, uses box2d drawing if no tile info exists */
8743
9025
  render()
@@ -8763,10 +9045,23 @@ class Box2dObject extends EngineObject
8763
9045
  * @param {Color} [lineColor]
8764
9046
  * @param {number} [lineWidth]
8765
9047
  * @param {CanvasRenderingContext2D} [context] */
8766
- drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
9048
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
8767
9049
  {
8768
- this.getFixtureList().forEach(fixture=>
8769
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
9050
+ // draw non-edge fixtures
9051
+ this.getFixtureList().forEach((fixture)=>
9052
+ {
9053
+ const shape = box2d.castObjectType(fixture.GetShape());
9054
+ if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
9055
+ {
9056
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
9057
+ }
9058
+ });
9059
+
9060
+ // draw edges using a single draw line for better connections
9061
+ this.edgeLists.forEach(points=>
9062
+ drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
9063
+ this.edgeLoops.forEach(points=>
9064
+ drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
8770
9065
  }
8771
9066
 
8772
9067
  ///////////////////////////////////////////////////////////////////////////////
@@ -8791,6 +9086,10 @@ class Box2dObject extends EngineObject
8791
9086
  * @param {boolean} [isSensor] */
8792
9087
  addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
8793
9088
  {
9089
+ ASSERT(isNumber(density), 'density must be a number');
9090
+ ASSERT(isNumber(friction), 'friction must be a number');
9091
+ ASSERT(isNumber(restitution), 'restitution must be a number');
9092
+
8794
9093
  const fd = new box2d.instance.b2FixtureDef();
8795
9094
  fd.set_shape(shape);
8796
9095
  fd.set_density(density);
@@ -8810,6 +9109,11 @@ class Box2dObject extends EngineObject
8810
9109
  * @param {boolean} [isSensor] */
8811
9110
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
8812
9111
  {
9112
+ ASSERT(isVector2(size), 'size must be a Vector2');
9113
+ ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
9114
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9115
+ ASSERT(isNumber(angle), 'angle must be a number');
9116
+
8813
9117
  const shape = new box2d.instance.b2PolygonShape();
8814
9118
  shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
8815
9119
  return this.addShape(shape, density, friction, restitution, isSensor);
@@ -8823,6 +9127,8 @@ class Box2dObject extends EngineObject
8823
9127
  * @param {boolean} [isSensor] */
8824
9128
  addPoly(points, density, friction, restitution, isSensor)
8825
9129
  {
9130
+ ASSERT(isArray(points), 'points must be an array');
9131
+
8826
9132
  function box2dCreatePolygonShape(points)
8827
9133
  {
8828
9134
  function box2dCreatePointList(points)
@@ -8858,6 +9164,9 @@ class Box2dObject extends EngineObject
8858
9164
  * @param {boolean} [isSensor] */
8859
9165
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
8860
9166
  {
9167
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9168
+ ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
9169
+
8861
9170
  const points = [];
8862
9171
  const radius = diameter/2;
8863
9172
  for (let i=sides; i--;)
@@ -8873,6 +9182,8 @@ class Box2dObject extends EngineObject
8873
9182
  * @param {boolean} [isSensor] */
8874
9183
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
8875
9184
  {
9185
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9186
+
8876
9187
  const sides = randInt(3, 9);
8877
9188
  const points = [];
8878
9189
  const radius = diameter/2;
@@ -8890,6 +9201,9 @@ class Box2dObject extends EngineObject
8890
9201
  * @param {boolean} [isSensor] */
8891
9202
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
8892
9203
  {
9204
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9205
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9206
+
8893
9207
  const shape = new box2d.instance.b2CircleShape();
8894
9208
  shape.set_m_p(box2d.vec2dTo(offset));
8895
9209
  shape.set_m_radius(diameter/2);
@@ -8905,53 +9219,63 @@ class Box2dObject extends EngineObject
8905
9219
  * @param {boolean} [isSensor] */
8906
9220
  addEdge(point1, point2, density, friction, restitution, isSensor)
8907
9221
  {
9222
+ ASSERT(isVector2(point1), 'point1 must be a Vector2');
9223
+ ASSERT(isVector2(point2), 'point2 must be a Vector2');
9224
+
8908
9225
  const shape = new box2d.instance.b2EdgeShape();
8909
9226
  shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
8910
9227
  return this.addShape(shape, density, friction, restitution, isSensor);
8911
9228
  }
8912
9229
 
8913
- /** Add an edge loop to the body, an edge loop connects the end points
9230
+ /** Add an edge list to the body
8914
9231
  * @param {Array<Vector2>} points
8915
9232
  * @param {number} [density]
8916
9233
  * @param {number} [friction]
8917
9234
  * @param {number} [restitution]
8918
9235
  * @param {boolean} [isSensor] */
8919
- addEdgeLoop(points, density, friction, restitution, isSensor)
9236
+ addEdgeList(points, density, friction, restitution, isSensor)
8920
9237
  {
8921
- const fixtures = [];
8922
- const getPoint = i=> points[mod(i,points.length)];
8923
- for (let i=0; i<points.length; ++i)
9238
+ ASSERT(isArray(points), 'points must be an array');
9239
+ const fixtures = [], edgePoints = [];
9240
+ for (let i=0; i<points.length-1; ++i)
8924
9241
  {
8925
9242
  const shape = new box2d.instance.b2EdgeShape();
8926
- shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
8927
- shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
8928
- shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
8929
- shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
9243
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9244
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9245
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9246
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
8930
9247
  const f = this.addShape(shape, density, friction, restitution, isSensor);
8931
9248
  fixtures.push(f);
9249
+ edgePoints.push(points[i].copy());
8932
9250
  }
9251
+ edgePoints.push(points[points.length-1].copy());
9252
+ this.edgeLists.push(edgePoints);
8933
9253
  return fixtures;
8934
9254
  }
8935
9255
 
8936
- /** Add an edge list to the body
9256
+ /** Add an edge loop to the body, an edge loop connects the end points
8937
9257
  * @param {Array<Vector2>} points
8938
9258
  * @param {number} [density]
8939
9259
  * @param {number} [friction]
8940
9260
  * @param {number} [restitution]
8941
9261
  * @param {boolean} [isSensor] */
8942
- addEdgeList(points, density, friction, restitution, isSensor)
9262
+ addEdgeLoop(points, density, friction, restitution, isSensor)
8943
9263
  {
8944
- const fixtures = [];
8945
- for (let i=0; i<points.length-1; ++i)
9264
+ ASSERT(isArray(points), 'points must be an array');
9265
+ const fixtures = [], edgePoints = [];
9266
+ const getPoint = i=> points[mod(i,points.length)];
9267
+ for (let i=0; i<points.length; ++i)
8946
9268
  {
8947
9269
  const shape = new box2d.instance.b2EdgeShape();
8948
- points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
8949
- points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
8950
- points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
8951
- points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
9270
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9271
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9272
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9273
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
8952
9274
  const f = this.addShape(shape, density, friction, restitution, isSensor);
8953
9275
  fixtures.push(f);
9276
+ i < points.length && edgePoints.push(points[i].copy());
8954
9277
  }
9278
+ this.edgeLoops.push(edgePoints);
8955
9279
  return fixtures;
8956
9280
  }
8957
9281
 
@@ -8986,6 +9310,10 @@ class Box2dObject extends EngineObject
8986
9310
  * @return {number} */
8987
9311
  getBodyType() { return this.body.GetType(); }
8988
9312
 
9313
+ /** Get the speed of this object
9314
+ * @return {number} */
9315
+ getSpeed() { return this.getLinearVelocity().length(); }
9316
+
8989
9317
  ///////////////////////////////////////////////////////////////////////////////
8990
9318
  // physics set functions
8991
9319
 
@@ -9001,33 +9329,40 @@ class Box2dObject extends EngineObject
9001
9329
 
9002
9330
  /** Sets the position
9003
9331
  * @param {Vector2} pos */
9004
- setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
9332
+ setPosition(pos)
9333
+ { this.setTransform(pos, this.body.GetAngle()); }
9005
9334
 
9006
9335
  /** Sets the angle
9007
9336
  * @param {number} angle */
9008
- setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9337
+ setAngle(angle)
9338
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9009
9339
 
9010
9340
  /** Sets the linear velocity
9011
9341
  * @param {Vector2} velocity */
9012
- setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9342
+ setLinearVelocity(velocity)
9343
+ { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9013
9344
 
9014
9345
  /** Sets the angular velocity
9015
9346
  * @param {number} angularVelocity */
9016
- setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
9347
+ setAngularVelocity(angularVelocity)
9348
+ { this.body.SetAngularVelocity(angularVelocity); }
9017
9349
 
9018
9350
  /** Sets the linear damping
9019
9351
  * @param {number} damping */
9020
- setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
9352
+ setLinearDamping(damping)
9353
+ { this.body.SetLinearDamping(damping); }
9021
9354
 
9022
9355
  /** Sets the angular damping
9023
9356
  * @param {number} damping */
9024
- setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
9357
+ setAngularDamping(damping)
9358
+ { this.body.SetAngularDamping(damping); }
9025
9359
 
9026
9360
  /** Sets the gravity scale
9027
9361
  * @param {number} [scale] */
9028
- setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
9362
+ setGravityScale(scale=1)
9363
+ { this.body.SetGravityScale(this.gravityScale = scale); }
9029
9364
 
9030
- /** Should this body be treated like a bullet for continuous collision detection?
9365
+ /** Should be like a bullet for continuous collision detection?
9031
9366
  * @param {boolean} [isBullet] */
9032
9367
  setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
9033
9368
 
@@ -9041,11 +9376,13 @@ class Box2dObject extends EngineObject
9041
9376
 
9042
9377
  /** Set whether the body is allowed to sleep
9043
9378
  * @param {boolean} [isAllowed] */
9044
- setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
9379
+ setSleepingAllowed(isAllowed=true)
9380
+ { this.body.SetSleepingAllowed(isAllowed); }
9045
9381
 
9046
9382
  /** Set whether the body can rotate
9047
9383
  * @param {boolean} [isFixed] */
9048
- setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
9384
+ setFixedRotation(isFixed=true)
9385
+ { this.body.SetFixedRotation(isFixed); }
9049
9386
 
9050
9387
  /** Set the center of mass of the body
9051
9388
  * @param {Vector2} center */
@@ -9057,10 +9394,11 @@ class Box2dObject extends EngineObject
9057
9394
 
9058
9395
  /** Set the moment of inertia of the body
9059
9396
  * @param {number} momentOfInertia */
9060
- setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
9397
+ setMomentOfInertia(momentOfInertia)
9398
+ { this.setMassData(undefined, undefined, momentOfInertia) }
9061
9399
 
9062
9400
  /** Reset the mass, center of mass, and moment */
9063
- resetMassData() { this.body.ResetMassData(); }
9401
+ resetMassData() { this.body.ResetMassData(); }
9064
9402
 
9065
9403
  /** Set the mass data of the body
9066
9404
  * @param {Vector2} [localCenter]
@@ -9702,6 +10040,50 @@ class Box2dPrismaticJoint extends Box2dJoint
9702
10040
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
9703
10041
  }
9704
10042
 
10043
+ ///////////////////////////////////////////////////////////////////////////////
10044
+ /**
10045
+ * Box2D Static Object - Box2d with a static physics body
10046
+ * @extends Box2dObject
10047
+ * @memberof Box2D
10048
+ */
10049
+ class Box2dStaticObject extends Box2dObject
10050
+ {
10051
+ /** Create a LittleJS object with Box2d physics
10052
+ * @param {Vector2} [pos]
10053
+ * @param {Vector2} [size]
10054
+ * @param {TileInfo} [tileInfo]
10055
+ * @param {number} [angle]
10056
+ * @param {Color} [color]
10057
+ * @param {number} [renderOrder] */
10058
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10059
+ {
10060
+ const bodyType = box2d.bodyTypeStatic;
10061
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10062
+ }
10063
+ }
10064
+
10065
+ ///////////////////////////////////////////////////////////////////////////////
10066
+ /**
10067
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
10068
+ * @extends Box2dObject
10069
+ * @memberof Box2D
10070
+ */
10071
+ class Box2dKiematicObject extends Box2dObject
10072
+ {
10073
+ /** Create a LittleJS object with Box2d physics
10074
+ * @param {Vector2} [pos]
10075
+ * @param {Vector2} [size]
10076
+ * @param {TileInfo} [tileInfo]
10077
+ * @param {number} [angle]
10078
+ * @param {Color} [color]
10079
+ * @param {number} [renderOrder] */
10080
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10081
+ {
10082
+ const bodyType = box2d.bodyTypeKinematic;
10083
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10084
+ }
10085
+ }
10086
+
9705
10087
  ///////////////////////////////////////////////////////////////////////////////
9706
10088
  /**
9707
10089
  * Box2D Wheel Joint
@@ -10064,6 +10446,7 @@ class Box2dPlugin
10064
10446
  box2d = this;
10065
10447
  this.instance = instance;
10066
10448
  this.world = new box2d.instance.b2World();
10449
+ this.objects = [];
10067
10450
 
10068
10451
  /** @property {number} - Velocity iterations per update*/
10069
10452
  this.velocityIterations = 8;
@@ -10306,9 +10689,10 @@ class Box2dPlugin
10306
10689
 
10307
10690
  /** converts a box2d vec2 pointer to a Vector2
10308
10691
  * @param {Object} v */
10309
- vec2FromPointer(v)
10692
+ vec2FromPointer(vp)
10310
10693
  {
10311
- return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
10694
+ const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
10695
+ return box2d.vec2From(v);
10312
10696
  }
10313
10697
 
10314
10698
  /** converts a Vector2 to a box2 vec2
@@ -10382,8 +10766,23 @@ async function box2dInit()
10382
10766
  // add the box2d plugin to the engine
10383
10767
  function box2dUpdate()
10384
10768
  {
10385
- if (!paused)
10386
- box2d.step();
10769
+ if (paused)
10770
+ return;
10771
+
10772
+ box2d.step();
10773
+
10774
+ // remove destroyed objects
10775
+ box2d.objects = box2d.objects.filter(o=>!o.destroyed);
10776
+
10777
+ // copy box2d physics results to engine objects
10778
+ for (const o of box2d.objects)
10779
+ {
10780
+ if (o.body)
10781
+ {
10782
+ o.pos = box2d.vec2From(o.body.GetPosition());
10783
+ o.angle = -o.body.GetAngle();
10784
+ }
10785
+ }
10387
10786
  }
10388
10787
  function box2dRender()
10389
10788
  {