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/dist/littlejs.js CHANGED
@@ -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
  {
@@ -790,6 +793,12 @@ function LOG(...output) { console.log(...output); }
790
793
  * @memberof Debug */
791
794
  function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
792
795
  {
796
+ ASSERT(isVector2(pos), 'pos must be a vec2');
797
+ ASSERT(isVector2(size), 'size must be a vec2');
798
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
799
+ ASSERT(isNumber(time), 'time must be a number');
800
+ ASSERT(isNumber(angle), 'angle must be a number');
801
+
793
802
  if (typeof size === 'number')
794
803
  size = vec2(size); // allow passing in floats
795
804
  if (isColor(color))
@@ -810,6 +819,12 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
810
819
  * @memberof Debug */
811
820
  function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
812
821
  {
822
+ ASSERT(isVector2(pos), 'pos must be a vec2');
823
+ ASSERT(isArray(points), 'points must be an array');
824
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
825
+ ASSERT(isNumber(time), 'time must be a number');
826
+ ASSERT(isNumber(angle), 'angle must be a number');
827
+
813
828
  if (isColor(color))
814
829
  color = color.toString();
815
830
  pos = pos.copy();
@@ -827,6 +842,11 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
827
842
  * @memberof Debug */
828
843
  function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
829
844
  {
845
+ ASSERT(isVector2(pos), 'pos must be a vec2');
846
+ ASSERT(isNumber(size), 'size must be a number');
847
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
848
+ ASSERT(isNumber(time), 'time must be a number');
849
+
830
850
  if (isColor(color))
831
851
  color = color.toString();
832
852
  pos = pos.copy();
@@ -852,6 +872,10 @@ function debugPoint(pos, color, time, angle)
852
872
  * @memberof Debug */
853
873
  function debugLine(posA, posB, color, width=.1, time)
854
874
  {
875
+ ASSERT(isVector2(posA), 'posA must be a vec2');
876
+ ASSERT(isVector2(posB), 'posB must be sa vec2');
877
+ ASSERT(isNumber(width), 'width must be a number');
878
+
855
879
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
856
880
  const size = vec2(width, halfDelta.length()*2);
857
881
  debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
@@ -866,6 +890,11 @@ function debugLine(posA, posB, color, width=.1, time)
866
890
  * @memberof Debug */
867
891
  function debugOverlap(posA, sizeA, posB, sizeB, color)
868
892
  {
893
+ ASSERT(isVector2(posA), 'posA must be a vec2');
894
+ ASSERT(isVector2(posB), 'posB must be a vec2');
895
+ ASSERT(isVector2(sizeA), 'sizeA must be a vec2');
896
+ ASSERT(isVector2(sizeB), 'sizeB must be a vec2');
897
+
869
898
  const minPos = vec2(
870
899
  min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
871
900
  min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
@@ -888,6 +917,14 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
888
917
  * @memberof Debug */
889
918
  function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
890
919
  {
920
+ ASSERT(isString(text), 'text must be a string');
921
+ ASSERT(isVector2(pos), 'pos must be a vec2');
922
+ ASSERT(isNumber(size), 'size must be a number');
923
+ ASSERT(isString(color) || isColor(color), 'color is invalid');
924
+ ASSERT(isNumber(time), 'time must be a number');
925
+ ASSERT(isNumber(angle), 'angle must be a number');
926
+ ASSERT(isString(font), 'font must be a string');
927
+
891
928
  if (isColor(color))
892
929
  color = color.toString();
893
930
  pos = pos.copy();
@@ -1341,6 +1378,9 @@ function debugVideoCaptureUpdate()
1341
1378
  debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
1342
1379
  }
1343
1380
 
1381
+ ///////////////////////////////////////////////////////////////////////////////
1382
+ // debug utility functions
1383
+
1344
1384
  // make color constants immutable with debug assertions
1345
1385
  function debugProtectConstant(obj)
1346
1386
  {
@@ -1515,7 +1555,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
1515
1555
  * @param {number} valueA
1516
1556
  * @param {number} valueB
1517
1557
  * @param {number} [wrapSize]
1518
- * @returns {number}
1558
+ * @return {number}
1519
1559
  * @memberof Utilities */
1520
1560
  function distanceWrap(valueA, valueB, wrapSize=1)
1521
1561
  { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
@@ -1525,7 +1565,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
1525
1565
  * @param {number} valueB
1526
1566
  * @param {number} percent
1527
1567
  * @param {number} [wrapSize]
1528
- * @returns {number}
1568
+ * @return {number}
1529
1569
  * @memberof Utilities */
1530
1570
  function lerpWrap(valueA, valueB, percent, wrapSize=1)
1531
1571
  {
@@ -1537,7 +1577,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
1537
1577
  /** Returns signed wrapped distance between the two angles passed in
1538
1578
  * @param {number} angleA
1539
1579
  * @param {number} angleB
1540
- * @returns {number}
1580
+ * @return {number}
1541
1581
  * @memberof Utilities */
1542
1582
  function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
1543
1583
 
@@ -1545,7 +1585,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
1545
1585
  * @param {number} angleA
1546
1586
  * @param {number} angleB
1547
1587
  * @param {number} percent
1548
- * @returns {number}
1588
+ * @return {number}
1549
1589
  * @memberof Utilities */
1550
1590
  function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
1551
1591
 
@@ -1672,6 +1712,13 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1672
1712
  * @memberof Utilities */
1673
1713
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1674
1714
 
1715
+ /**
1716
+ * Check if object is an array
1717
+ * @param {any} a
1718
+ * @return {boolean}
1719
+ * @memberof Utilities */
1720
+ function isArray(a) { return Array.isArray(a); }
1721
+
1675
1722
  /**
1676
1723
  * @callback LineTestFunction - Checks if a position is colliding
1677
1724
  * @param {Vector2} pos
@@ -2371,7 +2418,6 @@ class Color
2371
2418
  * @return {string} */
2372
2419
  toString(useAlpha = true)
2373
2420
  {
2374
- ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
2375
2421
  if (debug && !this.isValid())
2376
2422
  return `#000`;
2377
2423
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
@@ -2508,11 +2554,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2508
2554
  class Timer
2509
2555
  {
2510
2556
  /** Create a timer object set time passed in
2511
- * @param {number} [timeLeft] - How much time left before the timer elapses in seconds */
2512
- constructor(timeLeft)
2557
+ * @param {number} [timeLeft] - How much time left before the timer
2558
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
2559
+ constructor(timeLeft, useRealTime=false)
2513
2560
  {
2514
2561
  ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
2515
- this.time = timeLeft === undefined ? undefined : time + timeLeft;
2562
+ this.useRealTime = useRealTime;
2563
+ const globalTime = this.getGlobalTime();
2564
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
2516
2565
  this.setTime = timeLeft;
2517
2566
  }
2518
2567
 
@@ -2521,10 +2570,19 @@ class Timer
2521
2570
  set(timeLeft=0)
2522
2571
  {
2523
2572
  ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2524
- this.time = time + timeLeft;
2573
+ const globalTime = this.getGlobalTime();
2574
+ this.time = globalTime + timeLeft;
2525
2575
  this.setTime = timeLeft;
2526
2576
  }
2527
2577
 
2578
+ /** Set if the timer should keep running even when the game is paused
2579
+ * @param {boolean} [useRealTime] */
2580
+ setUseRealTime(useRealTime=true)
2581
+ {
2582
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2583
+ this.useRealTime = useRealTime;
2584
+ }
2585
+
2528
2586
  /** Unset the timer */
2529
2587
  unset() { this.time = undefined; }
2530
2588
 
@@ -2534,24 +2592,28 @@ class Timer
2534
2592
 
2535
2593
  /** Returns true if set and has not elapsed
2536
2594
  * @return {boolean} */
2537
- active() { return time < this.time; }
2595
+ active() { return this.getGlobalTime() < this.time; }
2538
2596
 
2539
2597
  /** Returns true if set and elapsed
2540
2598
  * @return {boolean} */
2541
- elapsed() { return time >= this.time; }
2599
+ elapsed() { return this.getGlobalTime() >= this.time; }
2542
2600
 
2543
2601
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2544
2602
  * @return {number} */
2545
- get() { return this.isSet()? time - this.time : 0; }
2603
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2546
2604
 
2547
2605
  /** Get percentage elapsed based on time it was set to, returns 0 if not set
2548
2606
  * @return {number} */
2549
- getPercent() { return this.isSet()? 1-percent(this.time - time, 0, this.setTime) : 0; }
2607
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2550
2608
 
2551
2609
  /** Get the time this timer was set to, returns 0 if not set
2552
2610
  * @return {number} */
2553
2611
  getSetTime() { return this.isSet() ? this.setTime : 0; }
2554
2612
 
2613
+ /** Get the current global time this timer is based on
2614
+ * @return {number} */
2615
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
2616
+
2555
2617
  /** Returns this timer expressed as a string
2556
2618
  * @return {string} */
2557
2619
  toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
@@ -3269,11 +3331,10 @@ class EngineObject
3269
3331
  }
3270
3332
 
3271
3333
  /** Update the object physics, called automatically by engine once each frame */
3272
- update()
3334
+ updatePhysics()
3273
3335
  {
3274
3336
  // child objects do not have physics
3275
- if (this.parent)
3276
- return;
3337
+ ASSERT(!this.parent);
3277
3338
 
3278
3339
  if (this.clampSpeed)
3279
3340
  {
@@ -3323,7 +3384,7 @@ class EngineObject
3323
3384
  continue;
3324
3385
 
3325
3386
  // check collision
3326
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
3387
+ if (!this.isOverlappingObject(o))
3327
3388
  continue;
3328
3389
 
3329
3390
  // notify objects of collision and check if should be resolved
@@ -3471,6 +3532,9 @@ class EngineObject
3471
3532
  }
3472
3533
  }
3473
3534
 
3535
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
3536
+ update() {}
3537
+
3474
3538
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
3475
3539
  render()
3476
3540
  {
@@ -3536,6 +3600,10 @@ class EngineObject
3536
3600
  * @return {number} */
3537
3601
  getAliveTime() { return time - this.spawnTime; }
3538
3602
 
3603
+ /** Get the speed of this object
3604
+ * @return {number} */
3605
+ getSpeed() { return this.velocity.length(); }
3606
+
3539
3607
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
3540
3608
  * @param {Vector2} acceleration */
3541
3609
  applyAcceleration(acceleration)
@@ -3580,6 +3648,20 @@ class EngineObject
3580
3648
  child.parent = 0;
3581
3649
  }
3582
3650
 
3651
+ /** Check if overlapping another engine object
3652
+ * Collisions are resoloved to prevent overlaps
3653
+ * @param {EngineObject} object
3654
+ * @return {boolean} */
3655
+ isOverlappingObject(object)
3656
+ { return this.isOverlapping(object.pos, object.size); }
3657
+
3658
+ /** Check if overlapping a point or aligned bounding box
3659
+ * @param {Vector2} pos - Center of box
3660
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3661
+ * @return {boolean} */
3662
+ isOverlapping(pos, size=vec2())
3663
+ { return isOverlapping(this.pos, this.size, pos, size); }
3664
+
3583
3665
  /** Set how this object collides
3584
3666
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
3585
3667
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -3925,7 +4007,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3925
4007
  {
3926
4008
  // normal canvas 2D rendering method (slower)
3927
4009
  ++drawCount;
3928
- size = new Vector2(size.x, -size.y); // fix upside down sprites
4010
+ size = new Vector2(size.x, -size.y); // flip upside down sprites
3929
4011
  drawCanvas2D(pos, size, angle, mirror, (context)=>
3930
4012
  {
3931
4013
  if (textureInfo)
@@ -4034,7 +4116,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4034
4116
  * @memberof Draw */
4035
4117
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
4036
4118
  {
4037
- ASSERT(Array.isArray(points), 'points must be an array');
4119
+ ASSERT(isArray(points), 'points must be an array');
4038
4120
  ASSERT(isNumber(width), 'width must be a number');
4039
4121
  ASSERT(isColor(color), 'color is invalid');
4040
4122
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4140,7 +4222,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
4140
4222
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
4141
4223
  {
4142
4224
  ASSERT(isVector2(pos), 'pos must be a vec2');
4143
- ASSERT(Array.isArray(points), 'points must be an array');
4225
+ ASSERT(isArray(points), 'points must be an array');
4144
4226
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
4145
4227
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4146
4228
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -4292,11 +4374,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4292
4374
  * @param {string} [font=fontDefault]
4293
4375
  * @param {string} [fontStyle]
4294
4376
  * @param {number} [maxWidth]
4377
+ * @param {number} [angle]
4295
4378
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4296
4379
  * @memberof Draw */
4297
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
4380
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
4298
4381
  {
4299
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
4382
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
4300
4383
  }
4301
4384
 
4302
4385
  /** Draw text on overlay canvas in world space
@@ -4311,10 +4394,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4311
4394
  * @param {string} [font=fontDefault]
4312
4395
  * @param {string} [fontStyle]
4313
4396
  * @param {number} [maxWidth]
4397
+ * @param {number} [angle]
4314
4398
  * @memberof Draw */
4315
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
4399
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
4316
4400
  {
4317
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
4401
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
4318
4402
  }
4319
4403
 
4320
4404
  /** Draw text on overlay canvas in screen space
@@ -4329,9 +4413,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4329
4413
  * @param {string} [font=fontDefault]
4330
4414
  * @param {string} [fontStyle]
4331
4415
  * @param {number} [maxWidth]
4416
+ * @param {number} [angle]
4332
4417
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4333
4418
  * @memberof Draw */
4334
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
4419
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
4335
4420
  {
4336
4421
  ASSERT(isString(text), 'text must be a string');
4337
4422
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4342,6 +4427,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4342
4427
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4343
4428
  ASSERT(isString(font), 'font must be a string');
4344
4429
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
4430
+ ASSERT(isNumber(angle), 'angle must be a number');
4345
4431
 
4346
4432
  context.fillStyle = color.toString();
4347
4433
  context.strokeStyle = lineColor.toString();
@@ -4351,14 +4437,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4351
4437
  context.textBaseline = 'middle';
4352
4438
 
4353
4439
  const lines = (text+'').split('\n');
4354
- let posY = pos.y;
4355
- posY -= (lines.length-1) * size/2; // center text vertically
4440
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
4441
+ context.save();
4442
+ context.translate(pos.x, posY);
4443
+ context.rotate(-angle);
4444
+ let yOffset = 0;
4356
4445
  lines.forEach(line=>
4357
4446
  {
4358
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
4359
- context.fillText(line, pos.x, posY, maxWidth);
4360
- posY += size;
4447
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
4448
+ context.fillText(line, 0, yOffset, maxWidth);
4449
+ yOffset += size;
4361
4450
  });
4451
+ context.restore();
4362
4452
  }
4363
4453
 
4364
4454
  ///////////////////////////////////////////////////////////////////////////////
@@ -4790,6 +4880,11 @@ let mouseDeltaScreen = vec2();
4790
4880
  * @memberof Input */
4791
4881
  let mouseWheel = 0;
4792
4882
 
4883
+ /** True if mouse was inside the document window, set to false when mouse leaves
4884
+ * @type {boolean}
4885
+ * @memberof Input */
4886
+ let mouseInWindow = true;
4887
+
4793
4888
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
4794
4889
  * @type {boolean}
4795
4890
  * @memberof Input */
@@ -4884,6 +4979,7 @@ function inputInit()
4884
4979
  document.addEventListener('mousedown', onMouseDown);
4885
4980
  document.addEventListener('mouseup', onMouseUp);
4886
4981
  document.addEventListener('mousemove', onMouseMove);
4982
+ document.addEventListener('mouseleave', onMouseLeave);
4887
4983
  document.addEventListener('wheel', onMouseWheel);
4888
4984
  document.addEventListener('contextmenu', onContextMenu);
4889
4985
  document.addEventListener('blur', onBlur);
@@ -4908,14 +5004,14 @@ function inputInit()
4908
5004
  if (inputWASDEmulateDirection)
4909
5005
  inputData[0][remapKey(e.code)] = 4;
4910
5006
  }
4911
- function remapKey(c)
5007
+ function remapKey(k)
4912
5008
  {
4913
5009
  // handle remapping wasd keys to directions
4914
5010
  return inputWASDEmulateDirection ?
4915
- c === 'KeyW' ? 'ArrowUp' :
4916
- c === 'KeyS' ? 'ArrowDown' :
4917
- c === 'KeyA' ? 'ArrowLeft' :
4918
- c === 'KeyD' ? 'ArrowRight' : c : c;
5011
+ k === 'KeyW' ? 'ArrowUp' :
5012
+ k === 'KeyS' ? 'ArrowDown' :
5013
+ k === 'KeyA' ? 'ArrowLeft' :
5014
+ k === 'KeyD' ? 'ArrowRight' : k : k;
4919
5015
  }
4920
5016
  function onMouseDown(e)
4921
5017
  {
@@ -4929,7 +5025,7 @@ function inputInit()
4929
5025
  isUsingGamepad = false;
4930
5026
  inputData[0][e.button] = 3;
4931
5027
 
4932
- let mousePosScreenLast = mousePosScreen;
5028
+ const mousePosScreenLast = mousePosScreen;
4933
5029
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4934
5030
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4935
5031
  inputPreventDefault && e.button && e.preventDefault();
@@ -4942,10 +5038,17 @@ function inputInit()
4942
5038
  }
4943
5039
  function onMouseMove(e)
4944
5040
  {
4945
- let mousePosScreenLast = mousePosScreen;
5041
+ mouseInWindow = true;
5042
+ const mousePosScreenLast = mousePosScreen;
4946
5043
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4947
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5044
+
5045
+ // when pointer is locked use movementX/Y for delta
5046
+ const movement = pointerLockIsActive() ?
5047
+ vec2(e.movementX, e.movementY) :
5048
+ mousePosScreen.subtract(mousePosScreenLast);
5049
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
4948
5050
  }
5051
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
4949
5052
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4950
5053
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4951
5054
  function onBlur() { inputClear(); } // reset input when focus is lost
@@ -5084,7 +5187,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
5084
5187
  function touchGamepadButtonCenter()
5085
5188
  {
5086
5189
  // draw right face buttons
5087
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5190
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5088
5191
  if (touchGamepadButtonCount <= 2)
5089
5192
  center.x += touchGamepadSize/2;
5090
5193
  return center;
@@ -5094,14 +5197,13 @@ function touchGamepadButtonCenter()
5094
5197
  function touchInputInit()
5095
5198
  {
5096
5199
  // add non passive touch event listeners
5097
- let handleTouch = handleTouchDefault;
5098
5200
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
5099
5201
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
5100
5202
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
5101
5203
 
5102
5204
  // handle all touch events the same way
5103
5205
  let wasTouching;
5104
- function handleTouchDefault(e)
5206
+ function handleTouch(e)
5105
5207
  {
5106
5208
  if (!touchInputEnable)
5107
5209
  return;
@@ -5252,7 +5354,7 @@ function touchGamepadRender()
5252
5354
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
5253
5355
  for (let i=0; i<touchGamepadButtonCount; i++)
5254
5356
  {
5255
- let j = mod(i-1, 4);
5357
+ const j = mod(i-1, 4);
5256
5358
  let button = touchGamepadButtonCount > 2 ?
5257
5359
  j : min(j, touchGamepadButtonCount-1);
5258
5360
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -5354,6 +5456,10 @@ class Sound
5354
5456
  {
5355
5457
  if (!soundEnable || headlessMode) return;
5356
5458
 
5459
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5460
+ ASSERT(isNumber(range), 'range must be a number');
5461
+ ASSERT(isNumber(taper), 'taper must be a number');
5462
+
5357
5463
  /** @property {number} - World space max range of sound */
5358
5464
  this.range = range;
5359
5465
  /** @property {number} - At what percentage of range should it start tapering */
@@ -5392,6 +5498,12 @@ class Sound
5392
5498
  */
5393
5499
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
5394
5500
  {
5501
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
5502
+ ASSERT(isNumber(volume), 'volume must be a number');
5503
+ ASSERT(isNumber(pitch), 'pitch must be a number');
5504
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
5505
+
5506
+
5395
5507
  if (!soundEnable || headlessMode) return;
5396
5508
  if (!this.sampleChannels) return;
5397
5509
 
@@ -5437,6 +5549,7 @@ class Sound
5437
5549
  */
5438
5550
  playNote(semitoneOffset=0, pos, volume)
5439
5551
  {
5552
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5440
5553
  const pitch = getNoteFrequency(semitoneOffset, 1);
5441
5554
  return this.play(pos, volume, pitch, 0);
5442
5555
  }
@@ -5445,7 +5558,7 @@ class Sound
5445
5558
  * @return {number} - How long the sound is in seconds (undefined if loading)
5446
5559
  */
5447
5560
  getDuration()
5448
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
5561
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5449
5562
 
5450
5563
  /** Check if sound is loaded, for sounds fetched from a url
5451
5564
  * @return {boolean} - True if sound is loaded and ready to play
@@ -5487,6 +5600,7 @@ class SoundWave extends Sound
5487
5600
  super(undefined, range, taper);
5488
5601
  if (!soundEnable || headlessMode) return;
5489
5602
  ASSERT(!filename || isString(filename), 'filename must be a string');
5603
+ ASSERT(isNumber(randomness), 'randomness must be a number');
5490
5604
 
5491
5605
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5492
5606
  this.onloadCallback = onloadCallback;
@@ -5703,7 +5817,7 @@ class SoundInstance
5703
5817
  /** Get the total duration of this sound
5704
5818
  * @return {number} - Total duration in seconds
5705
5819
  */
5706
- getDuration() { return this.sound.getDuration() / this.rate; }
5820
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
5707
5821
 
5708
5822
  /** Get source of this sound instance
5709
5823
  * @return {AudioBufferSourceNode}
@@ -6017,6 +6131,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
6017
6131
  }
6018
6132
 
6019
6133
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6134
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
6020
6135
  * @param {Vector2} posStart
6021
6136
  * @param {Vector2} posEnd
6022
6137
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -6590,6 +6705,7 @@ class TileCollisionLayer extends TileLayer
6590
6705
  }
6591
6706
 
6592
6707
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6708
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
6593
6709
  * @param {Vector2} posStart
6594
6710
  * @param {Vector2} posEnd
6595
6711
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -6782,12 +6898,12 @@ class ParticleEmitter extends EngineObject
6782
6898
  this.previousPos = this.pos.copy();
6783
6899
  }
6784
6900
 
6901
+ /** Emitters do not have physics */
6902
+ updatePhysics() {}
6903
+
6785
6904
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6786
6905
  update()
6787
6906
  {
6788
- // only do default update to apply parent transforms
6789
- this.parent && super.update();
6790
-
6791
6907
  if (this.velocityInheritance)
6792
6908
  {
6793
6909
  // pass emitter velocity to particles
@@ -6804,7 +6920,7 @@ class ParticleEmitter extends EngineObject
6804
6920
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6805
6921
  {
6806
6922
  // emit particles
6807
- if (this.emitRate * particleEmitRateScale)
6923
+ if (this.emitRate && particleEmitRateScale)
6808
6924
  {
6809
6925
  const rate = 1/this.emitRate/particleEmitRateScale;
6810
6926
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -6942,8 +7058,6 @@ class Particle extends EngineObject
6942
7058
  /** Update the object physics, called automatically by engine once each frame */
6943
7059
  update()
6944
7060
  {
6945
- super.update();
6946
-
6947
7061
  if (this.collideTiles || this.collideSolidObjects)
6948
7062
  {
6949
7063
  // only apply max circular speed if particle can collide
@@ -7187,11 +7301,11 @@ class Medal
7187
7301
  const descriptionSize = height*.3;
7188
7302
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
7189
7303
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
7190
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
7304
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
7191
7305
 
7192
7306
  // draw the description
7193
7307
  pos.y = y + height - gap.y*2 - descriptionSize/2;
7194
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
7308
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
7195
7309
  context.restore();
7196
7310
  }
7197
7311
 
@@ -8640,11 +8754,16 @@ class UISystemPlugin
8640
8754
  // reset hover object at start of update
8641
8755
  uiSystem.lastHoverObject = uiSystem.hoverObject;
8642
8756
  uiSystem.hoverObject = undefined;
8757
+
8758
+ // update in reverse order so topmost objects get priority
8643
8759
  for (let i = uiSystem.uiObjects.length; i--;)
8644
8760
  {
8645
8761
  const o = uiSystem.uiObjects[i];
8646
8762
  o.parent || updateObject(o)
8647
8763
  }
8764
+
8765
+ // remove destroyed objects
8766
+ uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
8648
8767
  }
8649
8768
  function uiRender()
8650
8769
  {
@@ -8677,12 +8796,12 @@ class UISystemPlugin
8677
8796
  /** Draw a rectangle to the UI context
8678
8797
  * @param {Vector2} pos
8679
8798
  * @param {Vector2} size
8680
- * @param {Color} [color=uiSystem.defaultColor]
8681
- * @param {number} [lineWidth=uiSystem.defaultLineWidth]
8682
- * @param {Color} [lineColor=uiSystem.defaultLineColor]
8683
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
8684
- * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
8685
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
8799
+ * @param {Color} [color]
8800
+ * @param {number} [lineWidth]
8801
+ * @param {Color} [lineColor]
8802
+ * @param {number} [cornerRadius]
8803
+ * @param {Color} [gradientColor] */
8804
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
8686
8805
  {
8687
8806
  ASSERT(isVector2(pos), 'pos must be a vec2');
8688
8807
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8761,10 +8880,14 @@ class UISystemPlugin
8761
8880
  * @param {string} [align]
8762
8881
  * @param {string} [font=uiSystem.defaultFont]
8763
8882
  * @param {string} [fontStyle]
8764
- * @param {boolean} [applyMaxWidth=true] */
8765
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
8883
+ * @param {boolean} [applyMaxWidth=true]
8884
+ * @param {Vector2} [textShadow]
8885
+ */
8886
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
8766
8887
  {
8767
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8888
+ if (textShadow)
8889
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8890
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8768
8891
  }
8769
8892
 
8770
8893
  /**
@@ -8791,6 +8914,34 @@ class UISystemPlugin
8791
8914
  setCallback(onDragLeave, 'dragleave');
8792
8915
  setCallback(onDragOver, 'dragover');
8793
8916
  }
8917
+
8918
+ /** Convert a screen space position to native UI position
8919
+ * @param {Vector2} pos
8920
+ * @return {Vector2}
8921
+ */
8922
+ screenToNative(pos)
8923
+ {
8924
+ if (!uiSystem.nativeHeight)
8925
+ return pos;
8926
+
8927
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8928
+ const sInv = 1/s;
8929
+ const p = pos.copy();
8930
+ p.x += s*mainCanvasSize.x/2;
8931
+ p.x *= sInv;
8932
+ p.y *= sInv;
8933
+ p.x -= sInv*mainCanvasSize.x/2;
8934
+ return p;
8935
+ }
8936
+
8937
+ /** Destroy and remove all objects
8938
+ * @memberof Engine */
8939
+ destroyObjects()
8940
+ {
8941
+ for (const o of this.uiObjects)
8942
+ o.parent || o.destroy();
8943
+ this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
8944
+ }
8794
8945
  }
8795
8946
 
8796
8947
  ///////////////////////////////////////////////////////////////////////////////
@@ -8867,6 +9018,9 @@ class UIObject
8867
9018
  /** @property {boolean} - True if this can be a hover object */
8868
9019
  this.canBeHover = true;
8869
9020
  uiSystem.uiObjects.push(this);
9021
+
9022
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
9023
+ this.textShadow = undefined;
8870
9024
  }
8871
9025
 
8872
9026
  /** Add a child UIObject to this object
@@ -8889,29 +9043,41 @@ class UIObject
8889
9043
  child.parent = undefined;
8890
9044
  }
8891
9045
 
9046
+
9047
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
9048
+ destroy()
9049
+ {
9050
+ if (this.destroyed)
9051
+ return;
9052
+
9053
+ // disconnect from parent and destroy children
9054
+ this.destroyed = 1;
9055
+ this.parent && this.parent.removeChild(this);
9056
+ for (const child of this.children)
9057
+ {
9058
+ child.parent = 0;
9059
+ child.destroy();
9060
+ }
9061
+ }
8892
9062
  /** Check if the mouse is overlapping a box in screen space
8893
9063
  * @return {boolean} - True if overlapping
8894
9064
  */
8895
9065
  isMouseOverlapping()
8896
9066
  {
9067
+ if (!mouseInWindow) return false;
9068
+
8897
9069
  const size = !isTouchDevice ? this.size :
8898
9070
  this.size.add(vec2(this.extraTouchSize || 0));
8899
- if (!uiSystem.nativeHeight)
8900
- return isOverlapping(this.pos, size, mousePosScreen);
8901
-
8902
- const s = mainCanvasSize.y / uiSystem.nativeHeight;
8903
- const sInv = 1/s;
8904
- let pos = mousePosScreen.copy();
8905
- pos.x += s*mainCanvasSize.x/2;
8906
- pos.x *= sInv;
8907
- pos.y *= sInv;
8908
- pos.x -= sInv*mainCanvasSize.x/2;
9071
+ const pos = uiSystem.screenToNative(mousePosScreen);
8909
9072
  return isOverlapping(this.pos, size, pos);
8910
9073
  }
8911
9074
 
8912
9075
  /** Update the object, called automatically by plugin once each frame */
8913
9076
  update()
8914
9077
  {
9078
+ // call the custom update callback
9079
+ this.onUpdate();
9080
+
8915
9081
  const wasHover = uiSystem.lastHoverObject === this;
8916
9082
  const isActive = this.isActiveObject();
8917
9083
  const mouseDown = mouseIsDown(0);
@@ -8968,7 +9134,7 @@ class UIObject
8968
9134
 
8969
9135
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8970
9136
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8971
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
9137
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
8972
9138
  }
8973
9139
 
8974
9140
  /** Special update when object is not visible */
@@ -8995,6 +9161,9 @@ class UIObject
8995
9161
  /** @return {boolean} - Is the mouse held onto this element */
8996
9162
  isActiveObject() { return uiSystem.activeObject === this; }
8997
9163
 
9164
+ /** Called each frame when object updates */
9165
+ onUpdate() {}
9166
+
8998
9167
  /** Called when the mouse enters the object */
8999
9168
  onEnter() {}
9000
9169
 
@@ -9049,8 +9218,9 @@ class UIText extends UIObject
9049
9218
  }
9050
9219
  render()
9051
9220
  {
9221
+ // only render the text
9052
9222
  const textSize = this.getTextSize();
9053
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
9223
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
9054
9224
  }
9055
9225
  }
9056
9226
 
@@ -9126,7 +9296,7 @@ class UIButton extends UIObject
9126
9296
  // draw the text scaled to fit
9127
9297
  const textSize = this.getTextSize();
9128
9298
  uiSystem.drawText(this.text, this.pos, textSize,
9129
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
9299
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
9130
9300
  }
9131
9301
  }
9132
9302
 
@@ -9180,7 +9350,7 @@ class UICheckbox extends UIObject
9180
9350
  const textSize = this.getTextSize();
9181
9351
  const pos = this.pos.add(vec2(this.size.x,0));
9182
9352
  uiSystem.drawText(this.text, pos, textSize,
9183
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
9353
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
9184
9354
  }
9185
9355
  }
9186
9356
 
@@ -9235,9 +9405,11 @@ class UIScrollbar extends UIObject
9235
9405
  const p1 = centerPos - handleWidth/2;
9236
9406
  const p2 = centerPos + handleWidth/2;
9237
9407
  const oldValue = this.value;
9408
+
9409
+ const p = uiSystem.screenToNative(mousePosScreen);
9238
9410
  this.value = isHorizontal ?
9239
- percent(mousePosScreen.x, p1, p2) :
9240
- percent(mousePosScreen.y, p2, p1);
9411
+ percent(p.x, p1, p2) :
9412
+ percent(p.y, p2, p1);
9241
9413
  this.value === oldValue || this.onChange();
9242
9414
  }
9243
9415
  }
@@ -9259,20 +9431,164 @@ class UIScrollbar extends UIObject
9259
9431
  vec2(lerp(p1, p2, this.value), this.pos.y) :
9260
9432
  vec2(this.pos.x, lerp(p2, p1, this.value))
9261
9433
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
9262
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
9434
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
9263
9435
 
9264
9436
  // draw the text scaled to fit on the scrollbar
9265
9437
  const textSize = this.getTextSize();
9266
9438
  uiSystem.drawText(this.text, this.pos, textSize,
9267
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
9439
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
9440
+ }
9441
+ }
9442
+
9443
+ ///////////////////////////////////////////////////////////////////////////////
9444
+ /**
9445
+ * VideoPlayerUIObject - A UI object that plays video
9446
+ * @extends UIObject
9447
+ * @example
9448
+ * // Create a video player UI object
9449
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
9450
+ * video.play();
9451
+ * @memberof UISystem
9452
+ */
9453
+ class UIVideo extends UIObject
9454
+ {
9455
+ /** Create a video player UI object
9456
+ * @param {Vector2} [pos]
9457
+ * @param {Vector2} [size]
9458
+ * @param {string} src - Video file path or URL
9459
+ * @param {boolean} [autoplay=false] - Start playing immediately?
9460
+ * @param {boolean} [loop=false] - Loop the video?
9461
+ * @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
9462
+ */
9463
+ constructor(pos, size, src, autoplay=false, loop=false, volume=1)
9464
+ {
9465
+ super(pos, size || vec2());
9466
+
9467
+ ASSERT(isString(src), 'video src must be a string');
9468
+ ASSERT(isNumber(volume), 'video volume must be a number');
9469
+
9470
+ this.color = BLACK; // default to black background
9471
+ this.cornerRadius = 0; // default to no corner radius
9472
+
9473
+ /** @property {float} - The video volume */
9474
+ this.volume = volume;
9475
+
9476
+ // create video element
9477
+ /** @property {HTMLVideoElement} - The video player */
9478
+ this.video = document.createElement('video');
9479
+ this.video.loop = loop;
9480
+ this.video.volume = clamp(volume * soundVolume);
9481
+ this.video.muted = !soundEnable;
9482
+ this.video.style.display = 'none';
9483
+ this.video.src = src;
9484
+ document.body.appendChild(this.video);
9485
+ autoplay && this.play();
9486
+ }
9487
+
9488
+ /** Play or resume the video
9489
+ * @return {Promise} Promise that resolves when playback starts */
9490
+ play()
9491
+ {
9492
+ // try to play the video, catch any errors (autoplay may be blocked)
9493
+ const promise = this.video.play();
9494
+ promise?.catch(()=>{});
9495
+ return promise;
9496
+ }
9497
+
9498
+ /** Pause the video */
9499
+ pause() { this.video.pause(); }
9500
+
9501
+ /** Stop and reset the video */
9502
+ stop() { this.video.pause(); this.video.currentTime = 0; }
9503
+
9504
+ /** Check if video is currently loading
9505
+ * @return {boolean} */
9506
+ isLoadng()
9507
+ { return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
9508
+
9509
+ /** Check if video is currently paused
9510
+ * @return {boolean} */
9511
+ isPaused() { return this.video.paused; }
9512
+
9513
+ /** Check if video is currently playing
9514
+ * @return {boolean} */
9515
+ isPlaying()
9516
+ { return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
9517
+
9518
+ /** Check if video has ended playing
9519
+ * @return {boolean} */
9520
+ hasEnded() { return this.video.ended; }
9521
+
9522
+ /** Set volume (0-1)
9523
+ * @param {number} volume - Volume level (0-1) */
9524
+ setVolume(volume)
9525
+ {
9526
+ this.volume = volume;
9527
+ this.video.volume = clamp(volume * soundVolume);
9528
+ }
9529
+
9530
+ /** Set playback speed
9531
+ * @param {number} rate - Playback rate multiplier */
9532
+ setPlaybackRate(rate) { this.video.playbackRate = rate; }
9533
+
9534
+ /** Get current time in seconds
9535
+ * @return {number} Current playback time */
9536
+ getCurrentTime() { return this.video.currentTime || 0; }
9537
+
9538
+ /** Get duration in seconds
9539
+ * @return {number} Total video duration */
9540
+ getDuration() { return this.video.duration || 0; }
9541
+
9542
+ /** Get the native video dimensions
9543
+ * @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
9544
+ getVideoSize()
9545
+ { return vec2(this.video.videoWidth, this.video.videoHeight); }
9546
+
9547
+ /** Seek to time in seconds
9548
+ * @param {number} time - Time in seconds to seek to */
9549
+ setTime(time)
9550
+ { this.video.currentTime = clamp(time, 0, this.getDuration()); }
9551
+
9552
+ update()
9553
+ {
9554
+ super.update();
9555
+
9556
+ // update volume based on global sound volume
9557
+ this.video.volume = clamp(this.volume * soundVolume);
9558
+ }
9559
+
9560
+ /** Render video to UI canvas */
9561
+ render()
9562
+ {
9563
+ super.render();
9564
+
9565
+ if (this.isLoadng())
9566
+ return;
9567
+ const context = uiSystem.uiContext;
9568
+ const s = this.size;
9569
+ context.save();
9570
+ context.translate(this.pos.x, this.pos.y);
9571
+ context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
9572
+ context.restore();
9573
+ }
9574
+
9575
+ /** Clean up video on destroy */
9576
+ destroy()
9577
+ {
9578
+ if (this.destroyed)
9579
+ return;
9580
+
9581
+ this.video.pause();
9582
+ this.video.remove();
9583
+ super.destroy();
9268
9584
  }
9269
9585
  }
9270
9586
  /**
9271
9587
  * LittleJS Box2D Physics Plugin
9272
9588
  * - Box2dObject extends EngineObject with Box2D physics
9273
- * - Call box2dInit() before engineInit() to enable
9589
+ * - Call box2dInit() to enable
9274
9590
  * - You will also need to include box2d.wasm.js
9275
- * - Uses a super fast web assembly port of Box2D
9591
+ * - Uses a super fast web assembly port of Box2D v2.3.1
9276
9592
  * - More info: https://github.com/kripken/box2d.js
9277
9593
  * - Functions to create polygon, circle, and edge shapes
9278
9594
  * - Contact begin and end callbacks
@@ -9302,9 +9618,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
9302
9618
  ///////////////////////////////////////////////////////////////////////////////
9303
9619
  /**
9304
9620
  * Box2D Object - extend with your own custom physics objects
9305
- * - A LittleJS object with Box2D physics
9306
- * - Each object has a Box2D body which can have multiple fixtures and joints
9621
+ * - A LittleJS object with Box2D physics, dynamic by default
9307
9622
  * - Provides interface for Box2D body and fixture functions
9623
+ * - Each object can have multiple fixtures and joints
9308
9624
  * @extends EngineObject
9309
9625
  * @memberof Box2D
9310
9626
  */
@@ -9318,7 +9634,7 @@ class Box2dObject extends EngineObject
9318
9634
  * @param {Color} [color]
9319
9635
  * @param {number} [bodyType]
9320
9636
  * @param {number} [renderOrder] */
9321
- constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
9637
+ constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
9322
9638
  {
9323
9639
  super(pos, size, tileInfo, angle, color, renderOrder);
9324
9640
 
@@ -9330,24 +9646,27 @@ class Box2dObject extends EngineObject
9330
9646
  this.body = box2d.world.CreateBody(bodyDef);
9331
9647
  this.body.object = this;
9332
9648
  this.lineColor = BLACK;
9649
+ box2d.objects.push(this);
9650
+
9651
+ // edge lists and loops for drawing
9652
+ this.edgeLists = [];
9653
+ this.edgeLoops = [];
9333
9654
  }
9334
9655
 
9335
9656
  /** Destroy this object and its physics body */
9336
9657
  destroy()
9337
9658
  {
9659
+ if (this.destroyed)
9660
+ return;
9661
+
9338
9662
  // destroy physics body, fixtures, and joints
9339
- this.body && box2d.world.DestroyBody(this.body);
9340
- this.body = 0;
9663
+ ASSERT(this.body, 'Box2dObject has no body to destroy');
9664
+ box2d.world.DestroyBody(this.body);
9341
9665
  super.destroy();
9342
9666
  }
9343
9667
 
9344
- /** Copy box2d update sim data */
9345
- update()
9346
- {
9347
- // use box2d physics update instead of normal engine update
9348
- this.pos = box2d.vec2From(this.body.GetPosition());
9349
- this.angle = -this.body.GetAngle();
9350
- }
9668
+ /** Box2d objects updated with Box2d world step */
9669
+ updatePhysics() {}
9351
9670
 
9352
9671
  /** Render the object, uses box2d drawing if no tile info exists */
9353
9672
  render()
@@ -9373,10 +9692,23 @@ class Box2dObject extends EngineObject
9373
9692
  * @param {Color} [lineColor]
9374
9693
  * @param {number} [lineWidth]
9375
9694
  * @param {CanvasRenderingContext2D} [context] */
9376
- drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
9695
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9377
9696
  {
9378
- this.getFixtureList().forEach(fixture=>
9379
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
9697
+ // draw non-edge fixtures
9698
+ this.getFixtureList().forEach((fixture)=>
9699
+ {
9700
+ const shape = box2d.castObjectType(fixture.GetShape());
9701
+ if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
9702
+ {
9703
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
9704
+ }
9705
+ });
9706
+
9707
+ // draw edges using a single draw line for better connections
9708
+ this.edgeLists.forEach(points=>
9709
+ drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
9710
+ this.edgeLoops.forEach(points=>
9711
+ drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
9380
9712
  }
9381
9713
 
9382
9714
  ///////////////////////////////////////////////////////////////////////////////
@@ -9401,6 +9733,10 @@ class Box2dObject extends EngineObject
9401
9733
  * @param {boolean} [isSensor] */
9402
9734
  addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
9403
9735
  {
9736
+ ASSERT(isNumber(density), 'density must be a number');
9737
+ ASSERT(isNumber(friction), 'friction must be a number');
9738
+ ASSERT(isNumber(restitution), 'restitution must be a number');
9739
+
9404
9740
  const fd = new box2d.instance.b2FixtureDef();
9405
9741
  fd.set_shape(shape);
9406
9742
  fd.set_density(density);
@@ -9420,6 +9756,11 @@ class Box2dObject extends EngineObject
9420
9756
  * @param {boolean} [isSensor] */
9421
9757
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
9422
9758
  {
9759
+ ASSERT(isVector2(size), 'size must be a Vector2');
9760
+ ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
9761
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9762
+ ASSERT(isNumber(angle), 'angle must be a number');
9763
+
9423
9764
  const shape = new box2d.instance.b2PolygonShape();
9424
9765
  shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
9425
9766
  return this.addShape(shape, density, friction, restitution, isSensor);
@@ -9433,6 +9774,8 @@ class Box2dObject extends EngineObject
9433
9774
  * @param {boolean} [isSensor] */
9434
9775
  addPoly(points, density, friction, restitution, isSensor)
9435
9776
  {
9777
+ ASSERT(isArray(points), 'points must be an array');
9778
+
9436
9779
  function box2dCreatePolygonShape(points)
9437
9780
  {
9438
9781
  function box2dCreatePointList(points)
@@ -9468,6 +9811,9 @@ class Box2dObject extends EngineObject
9468
9811
  * @param {boolean} [isSensor] */
9469
9812
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
9470
9813
  {
9814
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9815
+ ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
9816
+
9471
9817
  const points = [];
9472
9818
  const radius = diameter/2;
9473
9819
  for (let i=sides; i--;)
@@ -9483,6 +9829,8 @@ class Box2dObject extends EngineObject
9483
9829
  * @param {boolean} [isSensor] */
9484
9830
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
9485
9831
  {
9832
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9833
+
9486
9834
  const sides = randInt(3, 9);
9487
9835
  const points = [];
9488
9836
  const radius = diameter/2;
@@ -9500,6 +9848,9 @@ class Box2dObject extends EngineObject
9500
9848
  * @param {boolean} [isSensor] */
9501
9849
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
9502
9850
  {
9851
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9852
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9853
+
9503
9854
  const shape = new box2d.instance.b2CircleShape();
9504
9855
  shape.set_m_p(box2d.vec2dTo(offset));
9505
9856
  shape.set_m_radius(diameter/2);
@@ -9515,53 +9866,63 @@ class Box2dObject extends EngineObject
9515
9866
  * @param {boolean} [isSensor] */
9516
9867
  addEdge(point1, point2, density, friction, restitution, isSensor)
9517
9868
  {
9869
+ ASSERT(isVector2(point1), 'point1 must be a Vector2');
9870
+ ASSERT(isVector2(point2), 'point2 must be a Vector2');
9871
+
9518
9872
  const shape = new box2d.instance.b2EdgeShape();
9519
9873
  shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
9520
9874
  return this.addShape(shape, density, friction, restitution, isSensor);
9521
9875
  }
9522
9876
 
9523
- /** Add an edge loop to the body, an edge loop connects the end points
9877
+ /** Add an edge list to the body
9524
9878
  * @param {Array<Vector2>} points
9525
9879
  * @param {number} [density]
9526
9880
  * @param {number} [friction]
9527
9881
  * @param {number} [restitution]
9528
9882
  * @param {boolean} [isSensor] */
9529
- addEdgeLoop(points, density, friction, restitution, isSensor)
9883
+ addEdgeList(points, density, friction, restitution, isSensor)
9530
9884
  {
9531
- const fixtures = [];
9532
- const getPoint = i=> points[mod(i,points.length)];
9533
- for (let i=0; i<points.length; ++i)
9885
+ ASSERT(isArray(points), 'points must be an array');
9886
+ const fixtures = [], edgePoints = [];
9887
+ for (let i=0; i<points.length-1; ++i)
9534
9888
  {
9535
9889
  const shape = new box2d.instance.b2EdgeShape();
9536
- shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9537
- shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9538
- shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9539
- shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
9890
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9891
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9892
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9893
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
9540
9894
  const f = this.addShape(shape, density, friction, restitution, isSensor);
9541
9895
  fixtures.push(f);
9896
+ edgePoints.push(points[i].copy());
9542
9897
  }
9898
+ edgePoints.push(points[points.length-1].copy());
9899
+ this.edgeLists.push(edgePoints);
9543
9900
  return fixtures;
9544
9901
  }
9545
9902
 
9546
- /** Add an edge list to the body
9903
+ /** Add an edge loop to the body, an edge loop connects the end points
9547
9904
  * @param {Array<Vector2>} points
9548
9905
  * @param {number} [density]
9549
9906
  * @param {number} [friction]
9550
9907
  * @param {number} [restitution]
9551
9908
  * @param {boolean} [isSensor] */
9552
- addEdgeList(points, density, friction, restitution, isSensor)
9909
+ addEdgeLoop(points, density, friction, restitution, isSensor)
9553
9910
  {
9554
- const fixtures = [];
9555
- for (let i=0; i<points.length-1; ++i)
9911
+ ASSERT(isArray(points), 'points must be an array');
9912
+ const fixtures = [], edgePoints = [];
9913
+ const getPoint = i=> points[mod(i,points.length)];
9914
+ for (let i=0; i<points.length; ++i)
9556
9915
  {
9557
9916
  const shape = new box2d.instance.b2EdgeShape();
9558
- points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9559
- points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9560
- points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9561
- points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
9917
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9918
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9919
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9920
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
9562
9921
  const f = this.addShape(shape, density, friction, restitution, isSensor);
9563
9922
  fixtures.push(f);
9923
+ i < points.length && edgePoints.push(points[i].copy());
9564
9924
  }
9925
+ this.edgeLoops.push(edgePoints);
9565
9926
  return fixtures;
9566
9927
  }
9567
9928
 
@@ -9596,6 +9957,10 @@ class Box2dObject extends EngineObject
9596
9957
  * @return {number} */
9597
9958
  getBodyType() { return this.body.GetType(); }
9598
9959
 
9960
+ /** Get the speed of this object
9961
+ * @return {number} */
9962
+ getSpeed() { return this.getLinearVelocity().length(); }
9963
+
9599
9964
  ///////////////////////////////////////////////////////////////////////////////
9600
9965
  // physics set functions
9601
9966
 
@@ -9611,33 +9976,40 @@ class Box2dObject extends EngineObject
9611
9976
 
9612
9977
  /** Sets the position
9613
9978
  * @param {Vector2} pos */
9614
- setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
9979
+ setPosition(pos)
9980
+ { this.setTransform(pos, this.body.GetAngle()); }
9615
9981
 
9616
9982
  /** Sets the angle
9617
9983
  * @param {number} angle */
9618
- setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9984
+ setAngle(angle)
9985
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9619
9986
 
9620
9987
  /** Sets the linear velocity
9621
9988
  * @param {Vector2} velocity */
9622
- setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9989
+ setLinearVelocity(velocity)
9990
+ { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9623
9991
 
9624
9992
  /** Sets the angular velocity
9625
9993
  * @param {number} angularVelocity */
9626
- setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
9994
+ setAngularVelocity(angularVelocity)
9995
+ { this.body.SetAngularVelocity(angularVelocity); }
9627
9996
 
9628
9997
  /** Sets the linear damping
9629
9998
  * @param {number} damping */
9630
- setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
9999
+ setLinearDamping(damping)
10000
+ { this.body.SetLinearDamping(damping); }
9631
10001
 
9632
10002
  /** Sets the angular damping
9633
10003
  * @param {number} damping */
9634
- setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
10004
+ setAngularDamping(damping)
10005
+ { this.body.SetAngularDamping(damping); }
9635
10006
 
9636
10007
  /** Sets the gravity scale
9637
10008
  * @param {number} [scale] */
9638
- setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
10009
+ setGravityScale(scale=1)
10010
+ { this.body.SetGravityScale(this.gravityScale = scale); }
9639
10011
 
9640
- /** Should this body be treated like a bullet for continuous collision detection?
10012
+ /** Should be like a bullet for continuous collision detection?
9641
10013
  * @param {boolean} [isBullet] */
9642
10014
  setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
9643
10015
 
@@ -9651,11 +10023,13 @@ class Box2dObject extends EngineObject
9651
10023
 
9652
10024
  /** Set whether the body is allowed to sleep
9653
10025
  * @param {boolean} [isAllowed] */
9654
- setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
10026
+ setSleepingAllowed(isAllowed=true)
10027
+ { this.body.SetSleepingAllowed(isAllowed); }
9655
10028
 
9656
10029
  /** Set whether the body can rotate
9657
10030
  * @param {boolean} [isFixed] */
9658
- setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
10031
+ setFixedRotation(isFixed=true)
10032
+ { this.body.SetFixedRotation(isFixed); }
9659
10033
 
9660
10034
  /** Set the center of mass of the body
9661
10035
  * @param {Vector2} center */
@@ -9667,10 +10041,11 @@ class Box2dObject extends EngineObject
9667
10041
 
9668
10042
  /** Set the moment of inertia of the body
9669
10043
  * @param {number} momentOfInertia */
9670
- setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
10044
+ setMomentOfInertia(momentOfInertia)
10045
+ { this.setMassData(undefined, undefined, momentOfInertia) }
9671
10046
 
9672
10047
  /** Reset the mass, center of mass, and moment */
9673
- resetMassData() { this.body.ResetMassData(); }
10048
+ resetMassData() { this.body.ResetMassData(); }
9674
10049
 
9675
10050
  /** Set the mass data of the body
9676
10051
  * @param {Vector2} [localCenter]
@@ -10312,6 +10687,50 @@ class Box2dPrismaticJoint extends Box2dJoint
10312
10687
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
10313
10688
  }
10314
10689
 
10690
+ ///////////////////////////////////////////////////////////////////////////////
10691
+ /**
10692
+ * Box2D Static Object - Box2d with a static physics body
10693
+ * @extends Box2dObject
10694
+ * @memberof Box2D
10695
+ */
10696
+ class Box2dStaticObject extends Box2dObject
10697
+ {
10698
+ /** Create a LittleJS object with Box2d physics
10699
+ * @param {Vector2} [pos]
10700
+ * @param {Vector2} [size]
10701
+ * @param {TileInfo} [tileInfo]
10702
+ * @param {number} [angle]
10703
+ * @param {Color} [color]
10704
+ * @param {number} [renderOrder] */
10705
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10706
+ {
10707
+ const bodyType = box2d.bodyTypeStatic;
10708
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10709
+ }
10710
+ }
10711
+
10712
+ ///////////////////////////////////////////////////////////////////////////////
10713
+ /**
10714
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
10715
+ * @extends Box2dObject
10716
+ * @memberof Box2D
10717
+ */
10718
+ class Box2dKiematicObject extends Box2dObject
10719
+ {
10720
+ /** Create a LittleJS object with Box2d physics
10721
+ * @param {Vector2} [pos]
10722
+ * @param {Vector2} [size]
10723
+ * @param {TileInfo} [tileInfo]
10724
+ * @param {number} [angle]
10725
+ * @param {Color} [color]
10726
+ * @param {number} [renderOrder] */
10727
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10728
+ {
10729
+ const bodyType = box2d.bodyTypeKinematic;
10730
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10731
+ }
10732
+ }
10733
+
10315
10734
  ///////////////////////////////////////////////////////////////////////////////
10316
10735
  /**
10317
10736
  * Box2D Wheel Joint
@@ -10674,6 +11093,7 @@ class Box2dPlugin
10674
11093
  box2d = this;
10675
11094
  this.instance = instance;
10676
11095
  this.world = new box2d.instance.b2World();
11096
+ this.objects = [];
10677
11097
 
10678
11098
  /** @property {number} - Velocity iterations per update*/
10679
11099
  this.velocityIterations = 8;
@@ -10916,9 +11336,10 @@ class Box2dPlugin
10916
11336
 
10917
11337
  /** converts a box2d vec2 pointer to a Vector2
10918
11338
  * @param {Object} v */
10919
- vec2FromPointer(v)
11339
+ vec2FromPointer(vp)
10920
11340
  {
10921
- return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
11341
+ const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
11342
+ return box2d.vec2From(v);
10922
11343
  }
10923
11344
 
10924
11345
  /** converts a Vector2 to a box2 vec2
@@ -10992,8 +11413,23 @@ async function box2dInit()
10992
11413
  // add the box2d plugin to the engine
10993
11414
  function box2dUpdate()
10994
11415
  {
10995
- if (!paused)
10996
- box2d.step();
11416
+ if (paused)
11417
+ return;
11418
+
11419
+ box2d.step();
11420
+
11421
+ // remove destroyed objects
11422
+ box2d.objects = box2d.objects.filter(o=>!o.destroyed);
11423
+
11424
+ // copy box2d physics results to engine objects
11425
+ for (const o of box2d.objects)
11426
+ {
11427
+ if (o.body)
11428
+ {
11429
+ o.pos = box2d.vec2From(o.body.GetPosition());
11430
+ o.angle = -o.body.GetAngle();
11431
+ }
11432
+ }
10997
11433
  }
10998
11434
  function box2dRender()
10999
11435
  {