littlejsengine 1.14.23 → 1.14.28

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 (49) hide show
  1. package/dist/littlejs.d.ts +99 -20
  2. package/dist/littlejs.esm.js +378 -130
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +373 -130
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +336 -130
  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 +10 -7
  12. package/examples/platformer/gameCharacter.js +3 -2
  13. package/examples/platformer/gameObjects.js +3 -8
  14. package/examples/shorts/base.html +1 -1
  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/spaceGame.js +0 -2
  30. package/examples/shorts/spriteAtlas.js +0 -2
  31. package/examples/shorts/tiltedView.js +0 -3
  32. package/examples/shorts/timers.js +1 -2
  33. package/examples/shorts/topDown.js +0 -2
  34. package/examples/starter/index.html +2 -2
  35. package/package.json +1 -1
  36. package/plugins/box2d.js +170 -50
  37. package/plugins/pluginExport.js +2 -0
  38. package/plugins/uiSystem.js +55 -27
  39. package/src/engine.js +17 -14
  40. package/src/engineAudio.js +14 -2
  41. package/src/engineDebug.js +37 -0
  42. package/src/engineDraw.js +20 -12
  43. package/src/engineExport.js +3 -0
  44. package/src/engineInput.js +24 -12
  45. package/src/engineMedals.js +2 -2
  46. package/src/engineObject.js +24 -4
  47. package/src/engineParticles.js +1 -6
  48. package/src/engineTileLayer.js +2 -0
  49. package/src/engineUtilities.js +7 -1
@@ -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.0';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -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
@@ -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
  {
@@ -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);
@@ -2659,11 +2668,10 @@ class EngineObject
2659
2668
  }
2660
2669
 
2661
2670
  /** Update the object physics, called automatically by engine once each frame */
2662
- update()
2671
+ updatePhysics()
2663
2672
  {
2664
2673
  // child objects do not have physics
2665
- if (this.parent)
2666
- return;
2674
+ ASSERT(!this.parent);
2667
2675
 
2668
2676
  if (this.clampSpeed)
2669
2677
  {
@@ -2713,7 +2721,7 @@ class EngineObject
2713
2721
  continue;
2714
2722
 
2715
2723
  // check collision
2716
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
2724
+ if (!this.isOverlappingObject(o))
2717
2725
  continue;
2718
2726
 
2719
2727
  // notify objects of collision and check if should be resolved
@@ -2861,6 +2869,9 @@ class EngineObject
2861
2869
  }
2862
2870
  }
2863
2871
 
2872
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
2873
+ update() {}
2874
+
2864
2875
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
2865
2876
  render()
2866
2877
  {
@@ -2926,6 +2937,10 @@ class EngineObject
2926
2937
  * @return {number} */
2927
2938
  getAliveTime() { return time - this.spawnTime; }
2928
2939
 
2940
+ /** Get the speed of this object
2941
+ * @return {number} */
2942
+ getSpeed() { return this.velocity.length(); }
2943
+
2929
2944
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
2930
2945
  * @param {Vector2} acceleration */
2931
2946
  applyAcceleration(acceleration)
@@ -2970,6 +2985,20 @@ class EngineObject
2970
2985
  child.parent = 0;
2971
2986
  }
2972
2987
 
2988
+ /** Check if overlapping another engine object
2989
+ * Collisions are resoloved to prevent overlaps
2990
+ * @param {EngineObject} object
2991
+ * @return {boolean} */
2992
+ isOverlappingObject(object)
2993
+ { return this.isOverlapping(object.pos, object.size); }
2994
+
2995
+ /** Check if overlapping a point or aligned bounding box
2996
+ * @param {Vector2} pos - Center of box
2997
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
2998
+ * @return {boolean} */
2999
+ isOverlapping(pos, size=vec2())
3000
+ { return isOverlapping(this.pos, this.size, pos, size); }
3001
+
2973
3002
  /** Set how this object collides
2974
3003
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
2975
3004
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -3424,7 +3453,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3424
3453
  * @memberof Draw */
3425
3454
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3426
3455
  {
3427
- ASSERT(Array.isArray(points), 'points must be an array');
3456
+ ASSERT(isArray(points), 'points must be an array');
3428
3457
  ASSERT(isNumber(width), 'width must be a number');
3429
3458
  ASSERT(isColor(color), 'color is invalid');
3430
3459
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3530,7 +3559,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3530
3559
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3531
3560
  {
3532
3561
  ASSERT(isVector2(pos), 'pos must be a vec2');
3533
- ASSERT(Array.isArray(points), 'points must be an array');
3562
+ ASSERT(isArray(points), 'points must be an array');
3534
3563
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3535
3564
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3536
3565
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -3682,11 +3711,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3682
3711
  * @param {string} [font=fontDefault]
3683
3712
  * @param {string} [fontStyle]
3684
3713
  * @param {number} [maxWidth]
3714
+ * @param {number} [angle]
3685
3715
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3686
3716
  * @memberof Draw */
3687
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3717
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
3688
3718
  {
3689
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3719
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3690
3720
  }
3691
3721
 
3692
3722
  /** Draw text on overlay canvas in world space
@@ -3701,10 +3731,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3701
3731
  * @param {string} [font=fontDefault]
3702
3732
  * @param {string} [fontStyle]
3703
3733
  * @param {number} [maxWidth]
3734
+ * @param {number} [angle]
3704
3735
  * @memberof Draw */
3705
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3736
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
3706
3737
  {
3707
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3738
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
3708
3739
  }
3709
3740
 
3710
3741
  /** Draw text on overlay canvas in screen space
@@ -3719,9 +3750,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3719
3750
  * @param {string} [font=fontDefault]
3720
3751
  * @param {string} [fontStyle]
3721
3752
  * @param {number} [maxWidth]
3753
+ * @param {number} [angle]
3722
3754
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3723
3755
  * @memberof Draw */
3724
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
3756
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
3725
3757
  {
3726
3758
  ASSERT(isString(text), 'text must be a string');
3727
3759
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3732,6 +3764,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3732
3764
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3733
3765
  ASSERT(isString(font), 'font must be a string');
3734
3766
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
3767
+ ASSERT(isNumber(angle), 'angle must be a number');
3735
3768
 
3736
3769
  context.fillStyle = color.toString();
3737
3770
  context.strokeStyle = lineColor.toString();
@@ -3741,14 +3774,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3741
3774
  context.textBaseline = 'middle';
3742
3775
 
3743
3776
  const lines = (text+'').split('\n');
3744
- let posY = pos.y;
3745
- posY -= (lines.length-1) * size/2; // center text vertically
3777
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
3778
+ context.save();
3779
+ context.translate(pos.x, posY);
3780
+ context.rotate(-angle);
3781
+ let yOffset = 0;
3746
3782
  lines.forEach(line=>
3747
3783
  {
3748
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
3749
- context.fillText(line, pos.x, posY, maxWidth);
3750
- posY += size;
3784
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
3785
+ context.fillText(line, 0, yOffset, maxWidth);
3786
+ yOffset += size;
3751
3787
  });
3788
+ context.restore();
3752
3789
  }
3753
3790
 
3754
3791
  ///////////////////////////////////////////////////////////////////////////////
@@ -4180,6 +4217,11 @@ let mouseDeltaScreen = vec2();
4180
4217
  * @memberof Input */
4181
4218
  let mouseWheel = 0;
4182
4219
 
4220
+ /** True if mouse was inside the document window, set to false when mouse leaves
4221
+ * @type {boolean}
4222
+ * @memberof Input */
4223
+ let mouseInWindow = true;
4224
+
4183
4225
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
4184
4226
  * @type {boolean}
4185
4227
  * @memberof Input */
@@ -4274,6 +4316,7 @@ function inputInit()
4274
4316
  document.addEventListener('mousedown', onMouseDown);
4275
4317
  document.addEventListener('mouseup', onMouseUp);
4276
4318
  document.addEventListener('mousemove', onMouseMove);
4319
+ document.addEventListener('mouseleave', onMouseLeave);
4277
4320
  document.addEventListener('wheel', onMouseWheel);
4278
4321
  document.addEventListener('contextmenu', onContextMenu);
4279
4322
  document.addEventListener('blur', onBlur);
@@ -4298,14 +4341,14 @@ function inputInit()
4298
4341
  if (inputWASDEmulateDirection)
4299
4342
  inputData[0][remapKey(e.code)] = 4;
4300
4343
  }
4301
- function remapKey(c)
4344
+ function remapKey(k)
4302
4345
  {
4303
4346
  // handle remapping wasd keys to directions
4304
4347
  return inputWASDEmulateDirection ?
4305
- c === 'KeyW' ? 'ArrowUp' :
4306
- c === 'KeyS' ? 'ArrowDown' :
4307
- c === 'KeyA' ? 'ArrowLeft' :
4308
- c === 'KeyD' ? 'ArrowRight' : c : c;
4348
+ k === 'KeyW' ? 'ArrowUp' :
4349
+ k === 'KeyS' ? 'ArrowDown' :
4350
+ k === 'KeyA' ? 'ArrowLeft' :
4351
+ k === 'KeyD' ? 'ArrowRight' : k : k;
4309
4352
  }
4310
4353
  function onMouseDown(e)
4311
4354
  {
@@ -4319,7 +4362,7 @@ function inputInit()
4319
4362
  isUsingGamepad = false;
4320
4363
  inputData[0][e.button] = 3;
4321
4364
 
4322
- let mousePosScreenLast = mousePosScreen;
4365
+ const mousePosScreenLast = mousePosScreen;
4323
4366
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4324
4367
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4325
4368
  inputPreventDefault && e.button && e.preventDefault();
@@ -4332,10 +4375,17 @@ function inputInit()
4332
4375
  }
4333
4376
  function onMouseMove(e)
4334
4377
  {
4335
- let mousePosScreenLast = mousePosScreen;
4378
+ mouseInWindow = true;
4379
+ const mousePosScreenLast = mousePosScreen;
4336
4380
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4337
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4381
+
4382
+ // when pointer is locked use movementX/Y for delta
4383
+ const movement = pointerLockIsActive() ?
4384
+ vec2(e.movementX, e.movementY) :
4385
+ mousePosScreen.subtract(mousePosScreenLast);
4386
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
4338
4387
  }
4388
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
4339
4389
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4340
4390
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4341
4391
  function onBlur() { inputClear(); } // reset input when focus is lost
@@ -4474,7 +4524,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
4474
4524
  function touchGamepadButtonCenter()
4475
4525
  {
4476
4526
  // draw right face buttons
4477
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4527
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4478
4528
  if (touchGamepadButtonCount <= 2)
4479
4529
  center.x += touchGamepadSize/2;
4480
4530
  return center;
@@ -4484,14 +4534,13 @@ function touchGamepadButtonCenter()
4484
4534
  function touchInputInit()
4485
4535
  {
4486
4536
  // add non passive touch event listeners
4487
- let handleTouch = handleTouchDefault;
4488
4537
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4489
4538
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4490
4539
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4491
4540
 
4492
4541
  // handle all touch events the same way
4493
4542
  let wasTouching;
4494
- function handleTouchDefault(e)
4543
+ function handleTouch(e)
4495
4544
  {
4496
4545
  if (!touchInputEnable)
4497
4546
  return;
@@ -4642,7 +4691,7 @@ function touchGamepadRender()
4642
4691
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
4643
4692
  for (let i=0; i<touchGamepadButtonCount; i++)
4644
4693
  {
4645
- let j = mod(i-1, 4);
4694
+ const j = mod(i-1, 4);
4646
4695
  let button = touchGamepadButtonCount > 2 ?
4647
4696
  j : min(j, touchGamepadButtonCount-1);
4648
4697
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -4744,6 +4793,10 @@ class Sound
4744
4793
  {
4745
4794
  if (!soundEnable || headlessMode) return;
4746
4795
 
4796
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
4797
+ ASSERT(isNumber(range), 'range must be a number');
4798
+ ASSERT(isNumber(taper), 'taper must be a number');
4799
+
4747
4800
  /** @property {number} - World space max range of sound */
4748
4801
  this.range = range;
4749
4802
  /** @property {number} - At what percentage of range should it start tapering */
@@ -4782,6 +4835,12 @@ class Sound
4782
4835
  */
4783
4836
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4784
4837
  {
4838
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
4839
+ ASSERT(isNumber(volume), 'volume must be a number');
4840
+ ASSERT(isNumber(pitch), 'pitch must be a number');
4841
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
4842
+
4843
+
4785
4844
  if (!soundEnable || headlessMode) return;
4786
4845
  if (!this.sampleChannels) return;
4787
4846
 
@@ -4827,6 +4886,7 @@ class Sound
4827
4886
  */
4828
4887
  playNote(semitoneOffset=0, pos, volume)
4829
4888
  {
4889
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
4830
4890
  const pitch = getNoteFrequency(semitoneOffset, 1);
4831
4891
  return this.play(pos, volume, pitch, 0);
4832
4892
  }
@@ -4835,7 +4895,7 @@ class Sound
4835
4895
  * @return {number} - How long the sound is in seconds (undefined if loading)
4836
4896
  */
4837
4897
  getDuration()
4838
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
4898
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
4839
4899
 
4840
4900
  /** Check if sound is loaded, for sounds fetched from a url
4841
4901
  * @return {boolean} - True if sound is loaded and ready to play
@@ -4877,6 +4937,7 @@ class SoundWave extends Sound
4877
4937
  super(undefined, range, taper);
4878
4938
  if (!soundEnable || headlessMode) return;
4879
4939
  ASSERT(!filename || isString(filename), 'filename must be a string');
4940
+ ASSERT(isNumber(randomness), 'randomness must be a number');
4880
4941
 
4881
4942
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
4882
4943
  this.onloadCallback = onloadCallback;
@@ -5093,7 +5154,7 @@ class SoundInstance
5093
5154
  /** Get the total duration of this sound
5094
5155
  * @return {number} - Total duration in seconds
5095
5156
  */
5096
- getDuration() { return this.sound.getDuration() / this.rate; }
5157
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
5097
5158
 
5098
5159
  /** Get source of this sound instance
5099
5160
  * @return {AudioBufferSourceNode}
@@ -5407,6 +5468,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5407
5468
  }
5408
5469
 
5409
5470
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
5471
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
5410
5472
  * @param {Vector2} posStart
5411
5473
  * @param {Vector2} posEnd
5412
5474
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -5980,6 +6042,7 @@ class TileCollisionLayer extends TileLayer
5980
6042
  }
5981
6043
 
5982
6044
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6045
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
5983
6046
  * @param {Vector2} posStart
5984
6047
  * @param {Vector2} posEnd
5985
6048
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -6175,9 +6238,6 @@ class ParticleEmitter extends EngineObject
6175
6238
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6176
6239
  update()
6177
6240
  {
6178
- // only do default update to apply parent transforms
6179
- this.parent && super.update();
6180
-
6181
6241
  if (this.velocityInheritance)
6182
6242
  {
6183
6243
  // pass emitter velocity to particles
@@ -6194,7 +6254,7 @@ class ParticleEmitter extends EngineObject
6194
6254
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6195
6255
  {
6196
6256
  // emit particles
6197
- if (this.emitRate * particleEmitRateScale)
6257
+ if (this.emitRate && particleEmitRateScale)
6198
6258
  {
6199
6259
  const rate = 1/this.emitRate/particleEmitRateScale;
6200
6260
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -6332,8 +6392,6 @@ class Particle extends EngineObject
6332
6392
  /** Update the object physics, called automatically by engine once each frame */
6333
6393
  update()
6334
6394
  {
6335
- super.update();
6336
-
6337
6395
  if (this.collideTiles || this.collideSolidObjects)
6338
6396
  {
6339
6397
  // only apply max circular speed if particle can collide
@@ -6577,11 +6635,11 @@ class Medal
6577
6635
  const descriptionSize = height*.3;
6578
6636
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
6579
6637
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
6580
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6638
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6581
6639
 
6582
6640
  // draw the description
6583
6641
  pos.y = y + height - gap.y*2 - descriptionSize/2;
6584
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6642
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6585
6643
  context.restore();
6586
6644
  }
6587
6645
 
@@ -8067,12 +8125,12 @@ class UISystemPlugin
8067
8125
  /** Draw a rectangle to the UI context
8068
8126
  * @param {Vector2} pos
8069
8127
  * @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)
8128
+ * @param {Color} [color]
8129
+ * @param {number} [lineWidth]
8130
+ * @param {Color} [lineColor]
8131
+ * @param {number} [cornerRadius]
8132
+ * @param {Color} [gradientColor] */
8133
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
8076
8134
  {
8077
8135
  ASSERT(isVector2(pos), 'pos must be a vec2');
8078
8136
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8151,10 +8209,14 @@ class UISystemPlugin
8151
8209
  * @param {string} [align]
8152
8210
  * @param {string} [font=uiSystem.defaultFont]
8153
8211
  * @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)
8212
+ * @param {boolean} [applyMaxWidth=true]
8213
+ * @param {Vector2} [textShadow]
8214
+ */
8215
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
8156
8216
  {
8157
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8217
+ if (textShadow)
8218
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8219
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8158
8220
  }
8159
8221
 
8160
8222
  /**
@@ -8181,6 +8243,25 @@ class UISystemPlugin
8181
8243
  setCallback(onDragLeave, 'dragleave');
8182
8244
  setCallback(onDragOver, 'dragover');
8183
8245
  }
8246
+
8247
+ /** Convert a screen space position to native UI position
8248
+ * @param {Vector2} pos
8249
+ * @return {Vector2}
8250
+ */
8251
+ screenToNative(pos)
8252
+ {
8253
+ if (!uiSystem.nativeHeight)
8254
+ return pos;
8255
+
8256
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8257
+ const sInv = 1/s;
8258
+ const p = pos.copy();
8259
+ p.x += s*mainCanvasSize.x/2;
8260
+ p.x *= sInv;
8261
+ p.y *= sInv;
8262
+ p.x -= sInv*mainCanvasSize.x/2;
8263
+ return p;
8264
+ }
8184
8265
  }
8185
8266
 
8186
8267
  ///////////////////////////////////////////////////////////////////////////////
@@ -8257,6 +8338,9 @@ class UIObject
8257
8338
  /** @property {boolean} - True if this can be a hover object */
8258
8339
  this.canBeHover = true;
8259
8340
  uiSystem.uiObjects.push(this);
8341
+
8342
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
8343
+ this.textShadow = undefined;
8260
8344
  }
8261
8345
 
8262
8346
  /** Add a child UIObject to this object
@@ -8284,24 +8368,20 @@ class UIObject
8284
8368
  */
8285
8369
  isMouseOverlapping()
8286
8370
  {
8371
+ if (!mouseInWindow) return false;
8372
+
8287
8373
  const size = !isTouchDevice ? this.size :
8288
8374
  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;
8375
+ const pos = uiSystem.screenToNative(mousePosScreen);
8299
8376
  return isOverlapping(this.pos, size, pos);
8300
8377
  }
8301
8378
 
8302
8379
  /** Update the object, called automatically by plugin once each frame */
8303
8380
  update()
8304
8381
  {
8382
+ // call the custom update callback
8383
+ this.onUpdate();
8384
+
8305
8385
  const wasHover = uiSystem.lastHoverObject === this;
8306
8386
  const isActive = this.isActiveObject();
8307
8387
  const mouseDown = mouseIsDown(0);
@@ -8358,7 +8438,7 @@ class UIObject
8358
8438
 
8359
8439
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8360
8440
  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);
8441
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
8362
8442
  }
8363
8443
 
8364
8444
  /** Special update when object is not visible */
@@ -8385,6 +8465,9 @@ class UIObject
8385
8465
  /** @return {boolean} - Is the mouse held onto this element */
8386
8466
  isActiveObject() { return uiSystem.activeObject === this; }
8387
8467
 
8468
+ /** Called each frame when object updates */
8469
+ onUpdate() {}
8470
+
8388
8471
  /** Called when the mouse enters the object */
8389
8472
  onEnter() {}
8390
8473
 
@@ -8439,8 +8522,9 @@ class UIText extends UIObject
8439
8522
  }
8440
8523
  render()
8441
8524
  {
8525
+ // only render the text
8442
8526
  const textSize = this.getTextSize();
8443
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8527
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
8444
8528
  }
8445
8529
  }
8446
8530
 
@@ -8516,7 +8600,7 @@ class UIButton extends UIObject
8516
8600
  // draw the text scaled to fit
8517
8601
  const textSize = this.getTextSize();
8518
8602
  uiSystem.drawText(this.text, this.pos, textSize,
8519
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8603
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8520
8604
  }
8521
8605
  }
8522
8606
 
@@ -8570,7 +8654,7 @@ class UICheckbox extends UIObject
8570
8654
  const textSize = this.getTextSize();
8571
8655
  const pos = this.pos.add(vec2(this.size.x,0));
8572
8656
  uiSystem.drawText(this.text, pos, textSize,
8573
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8657
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
8574
8658
  }
8575
8659
  }
8576
8660
 
@@ -8625,9 +8709,11 @@ class UIScrollbar extends UIObject
8625
8709
  const p1 = centerPos - handleWidth/2;
8626
8710
  const p2 = centerPos + handleWidth/2;
8627
8711
  const oldValue = this.value;
8712
+
8713
+ const p = uiSystem.screenToNative(mousePosScreen);
8628
8714
  this.value = isHorizontal ?
8629
- percent(mousePosScreen.x, p1, p2) :
8630
- percent(mousePosScreen.y, p2, p1);
8715
+ percent(p.x, p1, p2) :
8716
+ percent(p.y, p2, p1);
8631
8717
  this.value === oldValue || this.onChange();
8632
8718
  }
8633
8719
  }
@@ -8649,20 +8735,20 @@ class UIScrollbar extends UIObject
8649
8735
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8650
8736
  vec2(this.pos.x, lerp(p2, p1, this.value))
8651
8737
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8652
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8738
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
8653
8739
 
8654
8740
  // draw the text scaled to fit on the scrollbar
8655
8741
  const textSize = this.getTextSize();
8656
8742
  uiSystem.drawText(this.text, this.pos, textSize,
8657
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8743
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8658
8744
  }
8659
8745
  }
8660
8746
  /**
8661
8747
  * LittleJS Box2D Physics Plugin
8662
8748
  * - Box2dObject extends EngineObject with Box2D physics
8663
- * - Call box2dInit() before engineInit() to enable
8749
+ * - Call box2dInit() to enable
8664
8750
  * - You will also need to include box2d.wasm.js
8665
- * - Uses a super fast web assembly port of Box2D
8751
+ * - Uses a super fast web assembly port of Box2D v2.3.1
8666
8752
  * - More info: https://github.com/kripken/box2d.js
8667
8753
  * - Functions to create polygon, circle, and edge shapes
8668
8754
  * - Contact begin and end callbacks
@@ -8692,9 +8778,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8692
8778
  ///////////////////////////////////////////////////////////////////////////////
8693
8779
  /**
8694
8780
  * 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
8781
+ * - A LittleJS object with Box2D physics, dynamic by default
8697
8782
  * - Provides interface for Box2D body and fixture functions
8783
+ * - Each object can have multiple fixtures and joints
8698
8784
  * @extends EngineObject
8699
8785
  * @memberof Box2D
8700
8786
  */
@@ -8708,7 +8794,7 @@ class Box2dObject extends EngineObject
8708
8794
  * @param {Color} [color]
8709
8795
  * @param {number} [bodyType]
8710
8796
  * @param {number} [renderOrder] */
8711
- constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8797
+ constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8712
8798
  {
8713
8799
  super(pos, size, tileInfo, angle, color, renderOrder);
8714
8800
 
@@ -8720,24 +8806,27 @@ class Box2dObject extends EngineObject
8720
8806
  this.body = box2d.world.CreateBody(bodyDef);
8721
8807
  this.body.object = this;
8722
8808
  this.lineColor = BLACK;
8809
+ box2d.objects.push(this);
8810
+
8811
+ // edge lists and loops for drawing
8812
+ this.edgeLists = [];
8813
+ this.edgeLoops = [];
8723
8814
  }
8724
8815
 
8725
8816
  /** Destroy this object and its physics body */
8726
8817
  destroy()
8727
8818
  {
8819
+ if (this.destroyed)
8820
+ return;
8821
+
8728
8822
  // destroy physics body, fixtures, and joints
8729
- this.body && box2d.world.DestroyBody(this.body);
8730
- this.body = 0;
8823
+ ASSERT(this.body, 'Box2dObject has no body to destroy');
8824
+ box2d.world.DestroyBody(this.body);
8731
8825
  super.destroy();
8732
8826
  }
8733
8827
 
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
- }
8828
+ /** Box2d objects updated with Box2d world step */
8829
+ updatePhysics() {}
8741
8830
 
8742
8831
  /** Render the object, uses box2d drawing if no tile info exists */
8743
8832
  render()
@@ -8763,10 +8852,23 @@ class Box2dObject extends EngineObject
8763
8852
  * @param {Color} [lineColor]
8764
8853
  * @param {number} [lineWidth]
8765
8854
  * @param {CanvasRenderingContext2D} [context] */
8766
- drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
8855
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
8767
8856
  {
8768
- this.getFixtureList().forEach(fixture=>
8769
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
8857
+ // draw non-edge fixtures
8858
+ this.getFixtureList().forEach((fixture)=>
8859
+ {
8860
+ const shape = box2d.castObjectType(fixture.GetShape());
8861
+ if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
8862
+ {
8863
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
8864
+ }
8865
+ });
8866
+
8867
+ // draw edges using a single draw line for better connections
8868
+ this.edgeLists.forEach(points=>
8869
+ drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
8870
+ this.edgeLoops.forEach(points=>
8871
+ drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
8770
8872
  }
8771
8873
 
8772
8874
  ///////////////////////////////////////////////////////////////////////////////
@@ -8791,6 +8893,10 @@ class Box2dObject extends EngineObject
8791
8893
  * @param {boolean} [isSensor] */
8792
8894
  addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
8793
8895
  {
8896
+ ASSERT(isNumber(density), 'density must be a number');
8897
+ ASSERT(isNumber(friction), 'friction must be a number');
8898
+ ASSERT(isNumber(restitution), 'restitution must be a number');
8899
+
8794
8900
  const fd = new box2d.instance.b2FixtureDef();
8795
8901
  fd.set_shape(shape);
8796
8902
  fd.set_density(density);
@@ -8810,6 +8916,11 @@ class Box2dObject extends EngineObject
8810
8916
  * @param {boolean} [isSensor] */
8811
8917
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
8812
8918
  {
8919
+ ASSERT(isVector2(size), 'size must be a Vector2');
8920
+ ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
8921
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
8922
+ ASSERT(isNumber(angle), 'angle must be a number');
8923
+
8813
8924
  const shape = new box2d.instance.b2PolygonShape();
8814
8925
  shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
8815
8926
  return this.addShape(shape, density, friction, restitution, isSensor);
@@ -8823,6 +8934,8 @@ class Box2dObject extends EngineObject
8823
8934
  * @param {boolean} [isSensor] */
8824
8935
  addPoly(points, density, friction, restitution, isSensor)
8825
8936
  {
8937
+ ASSERT(isArray(points), 'points must be an array');
8938
+
8826
8939
  function box2dCreatePolygonShape(points)
8827
8940
  {
8828
8941
  function box2dCreatePointList(points)
@@ -8858,6 +8971,9 @@ class Box2dObject extends EngineObject
8858
8971
  * @param {boolean} [isSensor] */
8859
8972
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
8860
8973
  {
8974
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
8975
+ ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
8976
+
8861
8977
  const points = [];
8862
8978
  const radius = diameter/2;
8863
8979
  for (let i=sides; i--;)
@@ -8873,6 +8989,8 @@ class Box2dObject extends EngineObject
8873
8989
  * @param {boolean} [isSensor] */
8874
8990
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
8875
8991
  {
8992
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
8993
+
8876
8994
  const sides = randInt(3, 9);
8877
8995
  const points = [];
8878
8996
  const radius = diameter/2;
@@ -8890,6 +9008,9 @@ class Box2dObject extends EngineObject
8890
9008
  * @param {boolean} [isSensor] */
8891
9009
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
8892
9010
  {
9011
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9012
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9013
+
8893
9014
  const shape = new box2d.instance.b2CircleShape();
8894
9015
  shape.set_m_p(box2d.vec2dTo(offset));
8895
9016
  shape.set_m_radius(diameter/2);
@@ -8905,53 +9026,63 @@ class Box2dObject extends EngineObject
8905
9026
  * @param {boolean} [isSensor] */
8906
9027
  addEdge(point1, point2, density, friction, restitution, isSensor)
8907
9028
  {
9029
+ ASSERT(isVector2(point1), 'point1 must be a Vector2');
9030
+ ASSERT(isVector2(point2), 'point2 must be a Vector2');
9031
+
8908
9032
  const shape = new box2d.instance.b2EdgeShape();
8909
9033
  shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
8910
9034
  return this.addShape(shape, density, friction, restitution, isSensor);
8911
9035
  }
8912
9036
 
8913
- /** Add an edge loop to the body, an edge loop connects the end points
9037
+ /** Add an edge list to the body
8914
9038
  * @param {Array<Vector2>} points
8915
9039
  * @param {number} [density]
8916
9040
  * @param {number} [friction]
8917
9041
  * @param {number} [restitution]
8918
9042
  * @param {boolean} [isSensor] */
8919
- addEdgeLoop(points, density, friction, restitution, isSensor)
9043
+ addEdgeList(points, density, friction, restitution, isSensor)
8920
9044
  {
8921
- const fixtures = [];
8922
- const getPoint = i=> points[mod(i,points.length)];
8923
- for (let i=0; i<points.length; ++i)
9045
+ ASSERT(isArray(points), 'points must be an array');
9046
+ const fixtures = [], edgePoints = [];
9047
+ for (let i=0; i<points.length-1; ++i)
8924
9048
  {
8925
9049
  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)));
9050
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9051
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9052
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9053
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
8930
9054
  const f = this.addShape(shape, density, friction, restitution, isSensor);
8931
9055
  fixtures.push(f);
9056
+ edgePoints.push(points[i].copy());
8932
9057
  }
9058
+ edgePoints.push(points[points.length-1].copy());
9059
+ this.edgeLists.push(edgePoints);
8933
9060
  return fixtures;
8934
9061
  }
8935
9062
 
8936
- /** Add an edge list to the body
9063
+ /** Add an edge loop to the body, an edge loop connects the end points
8937
9064
  * @param {Array<Vector2>} points
8938
9065
  * @param {number} [density]
8939
9066
  * @param {number} [friction]
8940
9067
  * @param {number} [restitution]
8941
9068
  * @param {boolean} [isSensor] */
8942
- addEdgeList(points, density, friction, restitution, isSensor)
9069
+ addEdgeLoop(points, density, friction, restitution, isSensor)
8943
9070
  {
8944
- const fixtures = [];
8945
- for (let i=0; i<points.length-1; ++i)
9071
+ ASSERT(isArray(points), 'points must be an array');
9072
+ const fixtures = [], edgePoints = [];
9073
+ const getPoint = i=> points[mod(i,points.length)];
9074
+ for (let i=0; i<points.length; ++i)
8946
9075
  {
8947
9076
  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]));
9077
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9078
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9079
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9080
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
8952
9081
  const f = this.addShape(shape, density, friction, restitution, isSensor);
8953
9082
  fixtures.push(f);
9083
+ i < points.length && edgePoints.push(points[i].copy());
8954
9084
  }
9085
+ this.edgeLoops.push(edgePoints);
8955
9086
  return fixtures;
8956
9087
  }
8957
9088
 
@@ -8986,6 +9117,10 @@ class Box2dObject extends EngineObject
8986
9117
  * @return {number} */
8987
9118
  getBodyType() { return this.body.GetType(); }
8988
9119
 
9120
+ /** Get the speed of this object
9121
+ * @return {number} */
9122
+ getSpeed() { return this.getLinearVelocity().length(); }
9123
+
8989
9124
  ///////////////////////////////////////////////////////////////////////////////
8990
9125
  // physics set functions
8991
9126
 
@@ -9001,33 +9136,40 @@ class Box2dObject extends EngineObject
9001
9136
 
9002
9137
  /** Sets the position
9003
9138
  * @param {Vector2} pos */
9004
- setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
9139
+ setPosition(pos)
9140
+ { this.setTransform(pos, this.body.GetAngle()); }
9005
9141
 
9006
9142
  /** Sets the angle
9007
9143
  * @param {number} angle */
9008
- setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9144
+ setAngle(angle)
9145
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9009
9146
 
9010
9147
  /** Sets the linear velocity
9011
9148
  * @param {Vector2} velocity */
9012
- setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9149
+ setLinearVelocity(velocity)
9150
+ { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9013
9151
 
9014
9152
  /** Sets the angular velocity
9015
9153
  * @param {number} angularVelocity */
9016
- setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
9154
+ setAngularVelocity(angularVelocity)
9155
+ { this.body.SetAngularVelocity(angularVelocity); }
9017
9156
 
9018
9157
  /** Sets the linear damping
9019
9158
  * @param {number} damping */
9020
- setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
9159
+ setLinearDamping(damping)
9160
+ { this.body.SetLinearDamping(damping); }
9021
9161
 
9022
9162
  /** Sets the angular damping
9023
9163
  * @param {number} damping */
9024
- setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
9164
+ setAngularDamping(damping)
9165
+ { this.body.SetAngularDamping(damping); }
9025
9166
 
9026
9167
  /** Sets the gravity scale
9027
9168
  * @param {number} [scale] */
9028
- setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
9169
+ setGravityScale(scale=1)
9170
+ { this.body.SetGravityScale(this.gravityScale = scale); }
9029
9171
 
9030
- /** Should this body be treated like a bullet for continuous collision detection?
9172
+ /** Should be like a bullet for continuous collision detection?
9031
9173
  * @param {boolean} [isBullet] */
9032
9174
  setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
9033
9175
 
@@ -9041,11 +9183,13 @@ class Box2dObject extends EngineObject
9041
9183
 
9042
9184
  /** Set whether the body is allowed to sleep
9043
9185
  * @param {boolean} [isAllowed] */
9044
- setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
9186
+ setSleepingAllowed(isAllowed=true)
9187
+ { this.body.SetSleepingAllowed(isAllowed); }
9045
9188
 
9046
9189
  /** Set whether the body can rotate
9047
9190
  * @param {boolean} [isFixed] */
9048
- setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
9191
+ setFixedRotation(isFixed=true)
9192
+ { this.body.SetFixedRotation(isFixed); }
9049
9193
 
9050
9194
  /** Set the center of mass of the body
9051
9195
  * @param {Vector2} center */
@@ -9057,10 +9201,11 @@ class Box2dObject extends EngineObject
9057
9201
 
9058
9202
  /** Set the moment of inertia of the body
9059
9203
  * @param {number} momentOfInertia */
9060
- setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
9204
+ setMomentOfInertia(momentOfInertia)
9205
+ { this.setMassData(undefined, undefined, momentOfInertia) }
9061
9206
 
9062
9207
  /** Reset the mass, center of mass, and moment */
9063
- resetMassData() { this.body.ResetMassData(); }
9208
+ resetMassData() { this.body.ResetMassData(); }
9064
9209
 
9065
9210
  /** Set the mass data of the body
9066
9211
  * @param {Vector2} [localCenter]
@@ -9702,6 +9847,50 @@ class Box2dPrismaticJoint extends Box2dJoint
9702
9847
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
9703
9848
  }
9704
9849
 
9850
+ ///////////////////////////////////////////////////////////////////////////////
9851
+ /**
9852
+ * Box2D Static Object - Box2d with a static physics body
9853
+ * @extends Box2dObject
9854
+ * @memberof Box2D
9855
+ */
9856
+ class Box2dStaticObject extends Box2dObject
9857
+ {
9858
+ /** Create a LittleJS object with Box2d physics
9859
+ * @param {Vector2} [pos]
9860
+ * @param {Vector2} [size]
9861
+ * @param {TileInfo} [tileInfo]
9862
+ * @param {number} [angle]
9863
+ * @param {Color} [color]
9864
+ * @param {number} [renderOrder] */
9865
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
9866
+ {
9867
+ const bodyType = box2d.bodyTypeStatic;
9868
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
9869
+ }
9870
+ }
9871
+
9872
+ ///////////////////////////////////////////////////////////////////////////////
9873
+ /**
9874
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
9875
+ * @extends Box2dObject
9876
+ * @memberof Box2D
9877
+ */
9878
+ class Box2dKiematicObject extends Box2dObject
9879
+ {
9880
+ /** Create a LittleJS object with Box2d physics
9881
+ * @param {Vector2} [pos]
9882
+ * @param {Vector2} [size]
9883
+ * @param {TileInfo} [tileInfo]
9884
+ * @param {number} [angle]
9885
+ * @param {Color} [color]
9886
+ * @param {number} [renderOrder] */
9887
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
9888
+ {
9889
+ const bodyType = box2d.bodyTypeKinematic;
9890
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
9891
+ }
9892
+ }
9893
+
9705
9894
  ///////////////////////////////////////////////////////////////////////////////
9706
9895
  /**
9707
9896
  * Box2D Wheel Joint
@@ -10064,6 +10253,7 @@ class Box2dPlugin
10064
10253
  box2d = this;
10065
10254
  this.instance = instance;
10066
10255
  this.world = new box2d.instance.b2World();
10256
+ this.objects = [];
10067
10257
 
10068
10258
  /** @property {number} - Velocity iterations per update*/
10069
10259
  this.velocityIterations = 8;
@@ -10306,9 +10496,10 @@ class Box2dPlugin
10306
10496
 
10307
10497
  /** converts a box2d vec2 pointer to a Vector2
10308
10498
  * @param {Object} v */
10309
- vec2FromPointer(v)
10499
+ vec2FromPointer(vp)
10310
10500
  {
10311
- return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
10501
+ const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
10502
+ return box2d.vec2From(v);
10312
10503
  }
10313
10504
 
10314
10505
  /** converts a Vector2 to a box2 vec2
@@ -10382,8 +10573,23 @@ async function box2dInit()
10382
10573
  // add the box2d plugin to the engine
10383
10574
  function box2dUpdate()
10384
10575
  {
10385
- if (!paused)
10386
- box2d.step();
10576
+ if (paused)
10577
+ return;
10578
+
10579
+ box2d.step();
10580
+
10581
+ // remove destroyed objects
10582
+ box2d.objects = box2d.objects.filter(o=>!o.destroyed);
10583
+
10584
+ // copy box2d physics results to engine objects
10585
+ for (const o of box2d.objects)
10586
+ {
10587
+ if (o.body)
10588
+ {
10589
+ o.pos = box2d.vec2From(o.body.GetPosition());
10590
+ o.angle = -o.body.GetAngle();
10591
+ }
10592
+ }
10387
10593
  }
10388
10594
  function box2dRender()
10389
10595
  {