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
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.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
  {
@@ -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
  {
@@ -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);
@@ -3269,11 +3315,10 @@ class EngineObject
3269
3315
  }
3270
3316
 
3271
3317
  /** Update the object physics, called automatically by engine once each frame */
3272
- update()
3318
+ updatePhysics()
3273
3319
  {
3274
3320
  // child objects do not have physics
3275
- if (this.parent)
3276
- return;
3321
+ ASSERT(!this.parent);
3277
3322
 
3278
3323
  if (this.clampSpeed)
3279
3324
  {
@@ -3323,7 +3368,7 @@ class EngineObject
3323
3368
  continue;
3324
3369
 
3325
3370
  // check collision
3326
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
3371
+ if (!this.isOverlappingObject(o))
3327
3372
  continue;
3328
3373
 
3329
3374
  // notify objects of collision and check if should be resolved
@@ -3471,6 +3516,9 @@ class EngineObject
3471
3516
  }
3472
3517
  }
3473
3518
 
3519
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
3520
+ update() {}
3521
+
3474
3522
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
3475
3523
  render()
3476
3524
  {
@@ -3536,6 +3584,10 @@ class EngineObject
3536
3584
  * @return {number} */
3537
3585
  getAliveTime() { return time - this.spawnTime; }
3538
3586
 
3587
+ /** Get the speed of this object
3588
+ * @return {number} */
3589
+ getSpeed() { return this.velocity.length(); }
3590
+
3539
3591
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
3540
3592
  * @param {Vector2} acceleration */
3541
3593
  applyAcceleration(acceleration)
@@ -3580,6 +3632,20 @@ class EngineObject
3580
3632
  child.parent = 0;
3581
3633
  }
3582
3634
 
3635
+ /** Check if overlapping another engine object
3636
+ * Collisions are resoloved to prevent overlaps
3637
+ * @param {EngineObject} object
3638
+ * @return {boolean} */
3639
+ isOverlappingObject(object)
3640
+ { return this.isOverlapping(object.pos, object.size); }
3641
+
3642
+ /** Check if overlapping a point or aligned bounding box
3643
+ * @param {Vector2} pos - Center of box
3644
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
3645
+ * @return {boolean} */
3646
+ isOverlapping(pos, size=vec2())
3647
+ { return isOverlapping(this.pos, this.size, pos, size); }
3648
+
3583
3649
  /** Set how this object collides
3584
3650
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
3585
3651
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -4034,7 +4100,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
4034
4100
  * @memberof Draw */
4035
4101
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
4036
4102
  {
4037
- ASSERT(Array.isArray(points), 'points must be an array');
4103
+ ASSERT(isArray(points), 'points must be an array');
4038
4104
  ASSERT(isNumber(width), 'width must be a number');
4039
4105
  ASSERT(isColor(color), 'color is invalid');
4040
4106
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4140,7 +4206,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
4140
4206
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
4141
4207
  {
4142
4208
  ASSERT(isVector2(pos), 'pos must be a vec2');
4143
- ASSERT(Array.isArray(points), 'points must be an array');
4209
+ ASSERT(isArray(points), 'points must be an array');
4144
4210
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
4145
4211
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4146
4212
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -4292,11 +4358,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4292
4358
  * @param {string} [font=fontDefault]
4293
4359
  * @param {string} [fontStyle]
4294
4360
  * @param {number} [maxWidth]
4361
+ * @param {number} [angle]
4295
4362
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4296
4363
  * @memberof Draw */
4297
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
4364
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
4298
4365
  {
4299
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
4366
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
4300
4367
  }
4301
4368
 
4302
4369
  /** Draw text on overlay canvas in world space
@@ -4311,10 +4378,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4311
4378
  * @param {string} [font=fontDefault]
4312
4379
  * @param {string} [fontStyle]
4313
4380
  * @param {number} [maxWidth]
4381
+ * @param {number} [angle]
4314
4382
  * @memberof Draw */
4315
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
4383
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
4316
4384
  {
4317
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
4385
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
4318
4386
  }
4319
4387
 
4320
4388
  /** Draw text on overlay canvas in screen space
@@ -4329,9 +4397,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4329
4397
  * @param {string} [font=fontDefault]
4330
4398
  * @param {string} [fontStyle]
4331
4399
  * @param {number} [maxWidth]
4400
+ * @param {number} [angle]
4332
4401
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4333
4402
  * @memberof Draw */
4334
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
4403
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
4335
4404
  {
4336
4405
  ASSERT(isString(text), 'text must be a string');
4337
4406
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4342,6 +4411,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4342
4411
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4343
4412
  ASSERT(isString(font), 'font must be a string');
4344
4413
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
4414
+ ASSERT(isNumber(angle), 'angle must be a number');
4345
4415
 
4346
4416
  context.fillStyle = color.toString();
4347
4417
  context.strokeStyle = lineColor.toString();
@@ -4351,14 +4421,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4351
4421
  context.textBaseline = 'middle';
4352
4422
 
4353
4423
  const lines = (text+'').split('\n');
4354
- let posY = pos.y;
4355
- posY -= (lines.length-1) * size/2; // center text vertically
4424
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
4425
+ context.save();
4426
+ context.translate(pos.x, posY);
4427
+ context.rotate(-angle);
4428
+ let yOffset = 0;
4356
4429
  lines.forEach(line=>
4357
4430
  {
4358
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
4359
- context.fillText(line, pos.x, posY, maxWidth);
4360
- posY += size;
4431
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
4432
+ context.fillText(line, 0, yOffset, maxWidth);
4433
+ yOffset += size;
4361
4434
  });
4435
+ context.restore();
4362
4436
  }
4363
4437
 
4364
4438
  ///////////////////////////////////////////////////////////////////////////////
@@ -4790,6 +4864,11 @@ let mouseDeltaScreen = vec2();
4790
4864
  * @memberof Input */
4791
4865
  let mouseWheel = 0;
4792
4866
 
4867
+ /** True if mouse was inside the document window, set to false when mouse leaves
4868
+ * @type {boolean}
4869
+ * @memberof Input */
4870
+ let mouseInWindow = true;
4871
+
4793
4872
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
4794
4873
  * @type {boolean}
4795
4874
  * @memberof Input */
@@ -4884,6 +4963,7 @@ function inputInit()
4884
4963
  document.addEventListener('mousedown', onMouseDown);
4885
4964
  document.addEventListener('mouseup', onMouseUp);
4886
4965
  document.addEventListener('mousemove', onMouseMove);
4966
+ document.addEventListener('mouseleave', onMouseLeave);
4887
4967
  document.addEventListener('wheel', onMouseWheel);
4888
4968
  document.addEventListener('contextmenu', onContextMenu);
4889
4969
  document.addEventListener('blur', onBlur);
@@ -4908,14 +4988,14 @@ function inputInit()
4908
4988
  if (inputWASDEmulateDirection)
4909
4989
  inputData[0][remapKey(e.code)] = 4;
4910
4990
  }
4911
- function remapKey(c)
4991
+ function remapKey(k)
4912
4992
  {
4913
4993
  // handle remapping wasd keys to directions
4914
4994
  return inputWASDEmulateDirection ?
4915
- c === 'KeyW' ? 'ArrowUp' :
4916
- c === 'KeyS' ? 'ArrowDown' :
4917
- c === 'KeyA' ? 'ArrowLeft' :
4918
- c === 'KeyD' ? 'ArrowRight' : c : c;
4995
+ k === 'KeyW' ? 'ArrowUp' :
4996
+ k === 'KeyS' ? 'ArrowDown' :
4997
+ k === 'KeyA' ? 'ArrowLeft' :
4998
+ k === 'KeyD' ? 'ArrowRight' : k : k;
4919
4999
  }
4920
5000
  function onMouseDown(e)
4921
5001
  {
@@ -4929,7 +5009,7 @@ function inputInit()
4929
5009
  isUsingGamepad = false;
4930
5010
  inputData[0][e.button] = 3;
4931
5011
 
4932
- let mousePosScreenLast = mousePosScreen;
5012
+ const mousePosScreenLast = mousePosScreen;
4933
5013
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4934
5014
  mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4935
5015
  inputPreventDefault && e.button && e.preventDefault();
@@ -4942,10 +5022,17 @@ function inputInit()
4942
5022
  }
4943
5023
  function onMouseMove(e)
4944
5024
  {
4945
- let mousePosScreenLast = mousePosScreen;
5025
+ mouseInWindow = true;
5026
+ const mousePosScreenLast = mousePosScreen;
4946
5027
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4947
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
5028
+
5029
+ // when pointer is locked use movementX/Y for delta
5030
+ const movement = pointerLockIsActive() ?
5031
+ vec2(e.movementX, e.movementY) :
5032
+ mousePosScreen.subtract(mousePosScreenLast);
5033
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
4948
5034
  }
5035
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
4949
5036
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4950
5037
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4951
5038
  function onBlur() { inputClear(); } // reset input when focus is lost
@@ -5084,7 +5171,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
5084
5171
  function touchGamepadButtonCenter()
5085
5172
  {
5086
5173
  // draw right face buttons
5087
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5174
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5088
5175
  if (touchGamepadButtonCount <= 2)
5089
5176
  center.x += touchGamepadSize/2;
5090
5177
  return center;
@@ -5094,14 +5181,13 @@ function touchGamepadButtonCenter()
5094
5181
  function touchInputInit()
5095
5182
  {
5096
5183
  // add non passive touch event listeners
5097
- let handleTouch = handleTouchDefault;
5098
5184
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
5099
5185
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
5100
5186
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
5101
5187
 
5102
5188
  // handle all touch events the same way
5103
5189
  let wasTouching;
5104
- function handleTouchDefault(e)
5190
+ function handleTouch(e)
5105
5191
  {
5106
5192
  if (!touchInputEnable)
5107
5193
  return;
@@ -5252,7 +5338,7 @@ function touchGamepadRender()
5252
5338
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
5253
5339
  for (let i=0; i<touchGamepadButtonCount; i++)
5254
5340
  {
5255
- let j = mod(i-1, 4);
5341
+ const j = mod(i-1, 4);
5256
5342
  let button = touchGamepadButtonCount > 2 ?
5257
5343
  j : min(j, touchGamepadButtonCount-1);
5258
5344
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -5354,6 +5440,10 @@ class Sound
5354
5440
  {
5355
5441
  if (!soundEnable || headlessMode) return;
5356
5442
 
5443
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5444
+ ASSERT(isNumber(range), 'range must be a number');
5445
+ ASSERT(isNumber(taper), 'taper must be a number');
5446
+
5357
5447
  /** @property {number} - World space max range of sound */
5358
5448
  this.range = range;
5359
5449
  /** @property {number} - At what percentage of range should it start tapering */
@@ -5392,6 +5482,12 @@ class Sound
5392
5482
  */
5393
5483
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
5394
5484
  {
5485
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
5486
+ ASSERT(isNumber(volume), 'volume must be a number');
5487
+ ASSERT(isNumber(pitch), 'pitch must be a number');
5488
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
5489
+
5490
+
5395
5491
  if (!soundEnable || headlessMode) return;
5396
5492
  if (!this.sampleChannels) return;
5397
5493
 
@@ -5437,6 +5533,7 @@ class Sound
5437
5533
  */
5438
5534
  playNote(semitoneOffset=0, pos, volume)
5439
5535
  {
5536
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5440
5537
  const pitch = getNoteFrequency(semitoneOffset, 1);
5441
5538
  return this.play(pos, volume, pitch, 0);
5442
5539
  }
@@ -5445,7 +5542,7 @@ class Sound
5445
5542
  * @return {number} - How long the sound is in seconds (undefined if loading)
5446
5543
  */
5447
5544
  getDuration()
5448
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
5545
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5449
5546
 
5450
5547
  /** Check if sound is loaded, for sounds fetched from a url
5451
5548
  * @return {boolean} - True if sound is loaded and ready to play
@@ -5487,6 +5584,7 @@ class SoundWave extends Sound
5487
5584
  super(undefined, range, taper);
5488
5585
  if (!soundEnable || headlessMode) return;
5489
5586
  ASSERT(!filename || isString(filename), 'filename must be a string');
5587
+ ASSERT(isNumber(randomness), 'randomness must be a number');
5490
5588
 
5491
5589
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5492
5590
  this.onloadCallback = onloadCallback;
@@ -5703,7 +5801,7 @@ class SoundInstance
5703
5801
  /** Get the total duration of this sound
5704
5802
  * @return {number} - Total duration in seconds
5705
5803
  */
5706
- getDuration() { return this.sound.getDuration() / this.rate; }
5804
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
5707
5805
 
5708
5806
  /** Get source of this sound instance
5709
5807
  * @return {AudioBufferSourceNode}
@@ -6017,6 +6115,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
6017
6115
  }
6018
6116
 
6019
6117
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6118
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
6020
6119
  * @param {Vector2} posStart
6021
6120
  * @param {Vector2} posEnd
6022
6121
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -6590,6 +6689,7 @@ class TileCollisionLayer extends TileLayer
6590
6689
  }
6591
6690
 
6592
6691
  /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6692
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
6593
6693
  * @param {Vector2} posStart
6594
6694
  * @param {Vector2} posEnd
6595
6695
  * @param {EngineObject} [object] - An object or undefined for generic test
@@ -6785,9 +6885,6 @@ class ParticleEmitter extends EngineObject
6785
6885
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6786
6886
  update()
6787
6887
  {
6788
- // only do default update to apply parent transforms
6789
- this.parent && super.update();
6790
-
6791
6888
  if (this.velocityInheritance)
6792
6889
  {
6793
6890
  // pass emitter velocity to particles
@@ -6804,7 +6901,7 @@ class ParticleEmitter extends EngineObject
6804
6901
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6805
6902
  {
6806
6903
  // emit particles
6807
- if (this.emitRate * particleEmitRateScale)
6904
+ if (this.emitRate && particleEmitRateScale)
6808
6905
  {
6809
6906
  const rate = 1/this.emitRate/particleEmitRateScale;
6810
6907
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -6942,8 +7039,6 @@ class Particle extends EngineObject
6942
7039
  /** Update the object physics, called automatically by engine once each frame */
6943
7040
  update()
6944
7041
  {
6945
- super.update();
6946
-
6947
7042
  if (this.collideTiles || this.collideSolidObjects)
6948
7043
  {
6949
7044
  // only apply max circular speed if particle can collide
@@ -7187,11 +7282,11 @@ class Medal
7187
7282
  const descriptionSize = height*.3;
7188
7283
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
7189
7284
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
7190
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
7285
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
7191
7286
 
7192
7287
  // draw the description
7193
7288
  pos.y = y + height - gap.y*2 - descriptionSize/2;
7194
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
7289
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
7195
7290
  context.restore();
7196
7291
  }
7197
7292
 
@@ -8677,12 +8772,12 @@ class UISystemPlugin
8677
8772
  /** Draw a rectangle to the UI context
8678
8773
  * @param {Vector2} pos
8679
8774
  * @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)
8775
+ * @param {Color} [color]
8776
+ * @param {number} [lineWidth]
8777
+ * @param {Color} [lineColor]
8778
+ * @param {number} [cornerRadius]
8779
+ * @param {Color} [gradientColor] */
8780
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
8686
8781
  {
8687
8782
  ASSERT(isVector2(pos), 'pos must be a vec2');
8688
8783
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8761,10 +8856,14 @@ class UISystemPlugin
8761
8856
  * @param {string} [align]
8762
8857
  * @param {string} [font=uiSystem.defaultFont]
8763
8858
  * @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)
8859
+ * @param {boolean} [applyMaxWidth=true]
8860
+ * @param {Vector2} [textShadow]
8861
+ */
8862
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
8766
8863
  {
8767
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8864
+ if (textShadow)
8865
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8866
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8768
8867
  }
8769
8868
 
8770
8869
  /**
@@ -8791,6 +8890,25 @@ class UISystemPlugin
8791
8890
  setCallback(onDragLeave, 'dragleave');
8792
8891
  setCallback(onDragOver, 'dragover');
8793
8892
  }
8893
+
8894
+ /** Convert a screen space position to native UI position
8895
+ * @param {Vector2} pos
8896
+ * @return {Vector2}
8897
+ */
8898
+ screenToNative(pos)
8899
+ {
8900
+ if (!uiSystem.nativeHeight)
8901
+ return pos;
8902
+
8903
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8904
+ const sInv = 1/s;
8905
+ const p = pos.copy();
8906
+ p.x += s*mainCanvasSize.x/2;
8907
+ p.x *= sInv;
8908
+ p.y *= sInv;
8909
+ p.x -= sInv*mainCanvasSize.x/2;
8910
+ return p;
8911
+ }
8794
8912
  }
8795
8913
 
8796
8914
  ///////////////////////////////////////////////////////////////////////////////
@@ -8867,6 +8985,9 @@ class UIObject
8867
8985
  /** @property {boolean} - True if this can be a hover object */
8868
8986
  this.canBeHover = true;
8869
8987
  uiSystem.uiObjects.push(this);
8988
+
8989
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
8990
+ this.textShadow = undefined;
8870
8991
  }
8871
8992
 
8872
8993
  /** Add a child UIObject to this object
@@ -8894,24 +9015,20 @@ class UIObject
8894
9015
  */
8895
9016
  isMouseOverlapping()
8896
9017
  {
9018
+ if (!mouseInWindow) return false;
9019
+
8897
9020
  const size = !isTouchDevice ? this.size :
8898
9021
  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;
9022
+ const pos = uiSystem.screenToNative(mousePosScreen);
8909
9023
  return isOverlapping(this.pos, size, pos);
8910
9024
  }
8911
9025
 
8912
9026
  /** Update the object, called automatically by plugin once each frame */
8913
9027
  update()
8914
9028
  {
9029
+ // call the custom update callback
9030
+ this.onUpdate();
9031
+
8915
9032
  const wasHover = uiSystem.lastHoverObject === this;
8916
9033
  const isActive = this.isActiveObject();
8917
9034
  const mouseDown = mouseIsDown(0);
@@ -8968,7 +9085,7 @@ class UIObject
8968
9085
 
8969
9086
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8970
9087
  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);
9088
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
8972
9089
  }
8973
9090
 
8974
9091
  /** Special update when object is not visible */
@@ -8995,6 +9112,9 @@ class UIObject
8995
9112
  /** @return {boolean} - Is the mouse held onto this element */
8996
9113
  isActiveObject() { return uiSystem.activeObject === this; }
8997
9114
 
9115
+ /** Called each frame when object updates */
9116
+ onUpdate() {}
9117
+
8998
9118
  /** Called when the mouse enters the object */
8999
9119
  onEnter() {}
9000
9120
 
@@ -9049,8 +9169,9 @@ class UIText extends UIObject
9049
9169
  }
9050
9170
  render()
9051
9171
  {
9172
+ // only render the text
9052
9173
  const textSize = this.getTextSize();
9053
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
9174
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
9054
9175
  }
9055
9176
  }
9056
9177
 
@@ -9126,7 +9247,7 @@ class UIButton extends UIObject
9126
9247
  // draw the text scaled to fit
9127
9248
  const textSize = this.getTextSize();
9128
9249
  uiSystem.drawText(this.text, this.pos, textSize,
9129
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
9250
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
9130
9251
  }
9131
9252
  }
9132
9253
 
@@ -9180,7 +9301,7 @@ class UICheckbox extends UIObject
9180
9301
  const textSize = this.getTextSize();
9181
9302
  const pos = this.pos.add(vec2(this.size.x,0));
9182
9303
  uiSystem.drawText(this.text, pos, textSize,
9183
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
9304
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
9184
9305
  }
9185
9306
  }
9186
9307
 
@@ -9235,9 +9356,11 @@ class UIScrollbar extends UIObject
9235
9356
  const p1 = centerPos - handleWidth/2;
9236
9357
  const p2 = centerPos + handleWidth/2;
9237
9358
  const oldValue = this.value;
9359
+
9360
+ const p = uiSystem.screenToNative(mousePosScreen);
9238
9361
  this.value = isHorizontal ?
9239
- percent(mousePosScreen.x, p1, p2) :
9240
- percent(mousePosScreen.y, p2, p1);
9362
+ percent(p.x, p1, p2) :
9363
+ percent(p.y, p2, p1);
9241
9364
  this.value === oldValue || this.onChange();
9242
9365
  }
9243
9366
  }
@@ -9259,20 +9382,20 @@ class UIScrollbar extends UIObject
9259
9382
  vec2(lerp(p1, p2, this.value), this.pos.y) :
9260
9383
  vec2(this.pos.x, lerp(p2, p1, this.value))
9261
9384
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
9262
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
9385
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
9263
9386
 
9264
9387
  // draw the text scaled to fit on the scrollbar
9265
9388
  const textSize = this.getTextSize();
9266
9389
  uiSystem.drawText(this.text, this.pos, textSize,
9267
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
9390
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
9268
9391
  }
9269
9392
  }
9270
9393
  /**
9271
9394
  * LittleJS Box2D Physics Plugin
9272
9395
  * - Box2dObject extends EngineObject with Box2D physics
9273
- * - Call box2dInit() before engineInit() to enable
9396
+ * - Call box2dInit() to enable
9274
9397
  * - You will also need to include box2d.wasm.js
9275
- * - Uses a super fast web assembly port of Box2D
9398
+ * - Uses a super fast web assembly port of Box2D v2.3.1
9276
9399
  * - More info: https://github.com/kripken/box2d.js
9277
9400
  * - Functions to create polygon, circle, and edge shapes
9278
9401
  * - Contact begin and end callbacks
@@ -9302,9 +9425,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
9302
9425
  ///////////////////////////////////////////////////////////////////////////////
9303
9426
  /**
9304
9427
  * 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
9428
+ * - A LittleJS object with Box2D physics, dynamic by default
9307
9429
  * - Provides interface for Box2D body and fixture functions
9430
+ * - Each object can have multiple fixtures and joints
9308
9431
  * @extends EngineObject
9309
9432
  * @memberof Box2D
9310
9433
  */
@@ -9318,7 +9441,7 @@ class Box2dObject extends EngineObject
9318
9441
  * @param {Color} [color]
9319
9442
  * @param {number} [bodyType]
9320
9443
  * @param {number} [renderOrder] */
9321
- constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
9444
+ constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
9322
9445
  {
9323
9446
  super(pos, size, tileInfo, angle, color, renderOrder);
9324
9447
 
@@ -9330,24 +9453,27 @@ class Box2dObject extends EngineObject
9330
9453
  this.body = box2d.world.CreateBody(bodyDef);
9331
9454
  this.body.object = this;
9332
9455
  this.lineColor = BLACK;
9456
+ box2d.objects.push(this);
9457
+
9458
+ // edge lists and loops for drawing
9459
+ this.edgeLists = [];
9460
+ this.edgeLoops = [];
9333
9461
  }
9334
9462
 
9335
9463
  /** Destroy this object and its physics body */
9336
9464
  destroy()
9337
9465
  {
9466
+ if (this.destroyed)
9467
+ return;
9468
+
9338
9469
  // destroy physics body, fixtures, and joints
9339
- this.body && box2d.world.DestroyBody(this.body);
9340
- this.body = 0;
9470
+ ASSERT(this.body, 'Box2dObject has no body to destroy');
9471
+ box2d.world.DestroyBody(this.body);
9341
9472
  super.destroy();
9342
9473
  }
9343
9474
 
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
- }
9475
+ /** Box2d objects updated with Box2d world step */
9476
+ updatePhysics() {}
9351
9477
 
9352
9478
  /** Render the object, uses box2d drawing if no tile info exists */
9353
9479
  render()
@@ -9373,10 +9499,23 @@ class Box2dObject extends EngineObject
9373
9499
  * @param {Color} [lineColor]
9374
9500
  * @param {number} [lineWidth]
9375
9501
  * @param {CanvasRenderingContext2D} [context] */
9376
- drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
9502
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9377
9503
  {
9378
- this.getFixtureList().forEach(fixture=>
9379
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
9504
+ // draw non-edge fixtures
9505
+ this.getFixtureList().forEach((fixture)=>
9506
+ {
9507
+ const shape = box2d.castObjectType(fixture.GetShape());
9508
+ if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
9509
+ {
9510
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
9511
+ }
9512
+ });
9513
+
9514
+ // draw edges using a single draw line for better connections
9515
+ this.edgeLists.forEach(points=>
9516
+ drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
9517
+ this.edgeLoops.forEach(points=>
9518
+ drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
9380
9519
  }
9381
9520
 
9382
9521
  ///////////////////////////////////////////////////////////////////////////////
@@ -9401,6 +9540,10 @@ class Box2dObject extends EngineObject
9401
9540
  * @param {boolean} [isSensor] */
9402
9541
  addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
9403
9542
  {
9543
+ ASSERT(isNumber(density), 'density must be a number');
9544
+ ASSERT(isNumber(friction), 'friction must be a number');
9545
+ ASSERT(isNumber(restitution), 'restitution must be a number');
9546
+
9404
9547
  const fd = new box2d.instance.b2FixtureDef();
9405
9548
  fd.set_shape(shape);
9406
9549
  fd.set_density(density);
@@ -9420,6 +9563,11 @@ class Box2dObject extends EngineObject
9420
9563
  * @param {boolean} [isSensor] */
9421
9564
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
9422
9565
  {
9566
+ ASSERT(isVector2(size), 'size must be a Vector2');
9567
+ ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
9568
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9569
+ ASSERT(isNumber(angle), 'angle must be a number');
9570
+
9423
9571
  const shape = new box2d.instance.b2PolygonShape();
9424
9572
  shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
9425
9573
  return this.addShape(shape, density, friction, restitution, isSensor);
@@ -9433,6 +9581,8 @@ class Box2dObject extends EngineObject
9433
9581
  * @param {boolean} [isSensor] */
9434
9582
  addPoly(points, density, friction, restitution, isSensor)
9435
9583
  {
9584
+ ASSERT(isArray(points), 'points must be an array');
9585
+
9436
9586
  function box2dCreatePolygonShape(points)
9437
9587
  {
9438
9588
  function box2dCreatePointList(points)
@@ -9468,6 +9618,9 @@ class Box2dObject extends EngineObject
9468
9618
  * @param {boolean} [isSensor] */
9469
9619
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
9470
9620
  {
9621
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9622
+ ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
9623
+
9471
9624
  const points = [];
9472
9625
  const radius = diameter/2;
9473
9626
  for (let i=sides; i--;)
@@ -9483,6 +9636,8 @@ class Box2dObject extends EngineObject
9483
9636
  * @param {boolean} [isSensor] */
9484
9637
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
9485
9638
  {
9639
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9640
+
9486
9641
  const sides = randInt(3, 9);
9487
9642
  const points = [];
9488
9643
  const radius = diameter/2;
@@ -9500,6 +9655,9 @@ class Box2dObject extends EngineObject
9500
9655
  * @param {boolean} [isSensor] */
9501
9656
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
9502
9657
  {
9658
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9659
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9660
+
9503
9661
  const shape = new box2d.instance.b2CircleShape();
9504
9662
  shape.set_m_p(box2d.vec2dTo(offset));
9505
9663
  shape.set_m_radius(diameter/2);
@@ -9515,53 +9673,63 @@ class Box2dObject extends EngineObject
9515
9673
  * @param {boolean} [isSensor] */
9516
9674
  addEdge(point1, point2, density, friction, restitution, isSensor)
9517
9675
  {
9676
+ ASSERT(isVector2(point1), 'point1 must be a Vector2');
9677
+ ASSERT(isVector2(point2), 'point2 must be a Vector2');
9678
+
9518
9679
  const shape = new box2d.instance.b2EdgeShape();
9519
9680
  shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
9520
9681
  return this.addShape(shape, density, friction, restitution, isSensor);
9521
9682
  }
9522
9683
 
9523
- /** Add an edge loop to the body, an edge loop connects the end points
9684
+ /** Add an edge list to the body
9524
9685
  * @param {Array<Vector2>} points
9525
9686
  * @param {number} [density]
9526
9687
  * @param {number} [friction]
9527
9688
  * @param {number} [restitution]
9528
9689
  * @param {boolean} [isSensor] */
9529
- addEdgeLoop(points, density, friction, restitution, isSensor)
9690
+ addEdgeList(points, density, friction, restitution, isSensor)
9530
9691
  {
9531
- const fixtures = [];
9532
- const getPoint = i=> points[mod(i,points.length)];
9533
- for (let i=0; i<points.length; ++i)
9692
+ ASSERT(isArray(points), 'points must be an array');
9693
+ const fixtures = [], edgePoints = [];
9694
+ for (let i=0; i<points.length-1; ++i)
9534
9695
  {
9535
9696
  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)));
9697
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9698
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9699
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9700
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
9540
9701
  const f = this.addShape(shape, density, friction, restitution, isSensor);
9541
9702
  fixtures.push(f);
9703
+ edgePoints.push(points[i].copy());
9542
9704
  }
9705
+ edgePoints.push(points[points.length-1].copy());
9706
+ this.edgeLists.push(edgePoints);
9543
9707
  return fixtures;
9544
9708
  }
9545
9709
 
9546
- /** Add an edge list to the body
9710
+ /** Add an edge loop to the body, an edge loop connects the end points
9547
9711
  * @param {Array<Vector2>} points
9548
9712
  * @param {number} [density]
9549
9713
  * @param {number} [friction]
9550
9714
  * @param {number} [restitution]
9551
9715
  * @param {boolean} [isSensor] */
9552
- addEdgeList(points, density, friction, restitution, isSensor)
9716
+ addEdgeLoop(points, density, friction, restitution, isSensor)
9553
9717
  {
9554
- const fixtures = [];
9555
- for (let i=0; i<points.length-1; ++i)
9718
+ ASSERT(isArray(points), 'points must be an array');
9719
+ const fixtures = [], edgePoints = [];
9720
+ const getPoint = i=> points[mod(i,points.length)];
9721
+ for (let i=0; i<points.length; ++i)
9556
9722
  {
9557
9723
  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]));
9724
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9725
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9726
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9727
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
9562
9728
  const f = this.addShape(shape, density, friction, restitution, isSensor);
9563
9729
  fixtures.push(f);
9730
+ i < points.length && edgePoints.push(points[i].copy());
9564
9731
  }
9732
+ this.edgeLoops.push(edgePoints);
9565
9733
  return fixtures;
9566
9734
  }
9567
9735
 
@@ -9596,6 +9764,10 @@ class Box2dObject extends EngineObject
9596
9764
  * @return {number} */
9597
9765
  getBodyType() { return this.body.GetType(); }
9598
9766
 
9767
+ /** Get the speed of this object
9768
+ * @return {number} */
9769
+ getSpeed() { return this.getLinearVelocity().length(); }
9770
+
9599
9771
  ///////////////////////////////////////////////////////////////////////////////
9600
9772
  // physics set functions
9601
9773
 
@@ -9611,33 +9783,40 @@ class Box2dObject extends EngineObject
9611
9783
 
9612
9784
  /** Sets the position
9613
9785
  * @param {Vector2} pos */
9614
- setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
9786
+ setPosition(pos)
9787
+ { this.setTransform(pos, this.body.GetAngle()); }
9615
9788
 
9616
9789
  /** Sets the angle
9617
9790
  * @param {number} angle */
9618
- setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9791
+ setAngle(angle)
9792
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9619
9793
 
9620
9794
  /** Sets the linear velocity
9621
9795
  * @param {Vector2} velocity */
9622
- setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9796
+ setLinearVelocity(velocity)
9797
+ { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9623
9798
 
9624
9799
  /** Sets the angular velocity
9625
9800
  * @param {number} angularVelocity */
9626
- setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
9801
+ setAngularVelocity(angularVelocity)
9802
+ { this.body.SetAngularVelocity(angularVelocity); }
9627
9803
 
9628
9804
  /** Sets the linear damping
9629
9805
  * @param {number} damping */
9630
- setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
9806
+ setLinearDamping(damping)
9807
+ { this.body.SetLinearDamping(damping); }
9631
9808
 
9632
9809
  /** Sets the angular damping
9633
9810
  * @param {number} damping */
9634
- setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
9811
+ setAngularDamping(damping)
9812
+ { this.body.SetAngularDamping(damping); }
9635
9813
 
9636
9814
  /** Sets the gravity scale
9637
9815
  * @param {number} [scale] */
9638
- setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
9816
+ setGravityScale(scale=1)
9817
+ { this.body.SetGravityScale(this.gravityScale = scale); }
9639
9818
 
9640
- /** Should this body be treated like a bullet for continuous collision detection?
9819
+ /** Should be like a bullet for continuous collision detection?
9641
9820
  * @param {boolean} [isBullet] */
9642
9821
  setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
9643
9822
 
@@ -9651,11 +9830,13 @@ class Box2dObject extends EngineObject
9651
9830
 
9652
9831
  /** Set whether the body is allowed to sleep
9653
9832
  * @param {boolean} [isAllowed] */
9654
- setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
9833
+ setSleepingAllowed(isAllowed=true)
9834
+ { this.body.SetSleepingAllowed(isAllowed); }
9655
9835
 
9656
9836
  /** Set whether the body can rotate
9657
9837
  * @param {boolean} [isFixed] */
9658
- setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
9838
+ setFixedRotation(isFixed=true)
9839
+ { this.body.SetFixedRotation(isFixed); }
9659
9840
 
9660
9841
  /** Set the center of mass of the body
9661
9842
  * @param {Vector2} center */
@@ -9667,10 +9848,11 @@ class Box2dObject extends EngineObject
9667
9848
 
9668
9849
  /** Set the moment of inertia of the body
9669
9850
  * @param {number} momentOfInertia */
9670
- setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
9851
+ setMomentOfInertia(momentOfInertia)
9852
+ { this.setMassData(undefined, undefined, momentOfInertia) }
9671
9853
 
9672
9854
  /** Reset the mass, center of mass, and moment */
9673
- resetMassData() { this.body.ResetMassData(); }
9855
+ resetMassData() { this.body.ResetMassData(); }
9674
9856
 
9675
9857
  /** Set the mass data of the body
9676
9858
  * @param {Vector2} [localCenter]
@@ -10312,6 +10494,50 @@ class Box2dPrismaticJoint extends Box2dJoint
10312
10494
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
10313
10495
  }
10314
10496
 
10497
+ ///////////////////////////////////////////////////////////////////////////////
10498
+ /**
10499
+ * Box2D Static Object - Box2d with a static physics body
10500
+ * @extends Box2dObject
10501
+ * @memberof Box2D
10502
+ */
10503
+ class Box2dStaticObject extends Box2dObject
10504
+ {
10505
+ /** Create a LittleJS object with Box2d physics
10506
+ * @param {Vector2} [pos]
10507
+ * @param {Vector2} [size]
10508
+ * @param {TileInfo} [tileInfo]
10509
+ * @param {number} [angle]
10510
+ * @param {Color} [color]
10511
+ * @param {number} [renderOrder] */
10512
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10513
+ {
10514
+ const bodyType = box2d.bodyTypeStatic;
10515
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10516
+ }
10517
+ }
10518
+
10519
+ ///////////////////////////////////////////////////////////////////////////////
10520
+ /**
10521
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
10522
+ * @extends Box2dObject
10523
+ * @memberof Box2D
10524
+ */
10525
+ class Box2dKiematicObject extends Box2dObject
10526
+ {
10527
+ /** Create a LittleJS object with Box2d physics
10528
+ * @param {Vector2} [pos]
10529
+ * @param {Vector2} [size]
10530
+ * @param {TileInfo} [tileInfo]
10531
+ * @param {number} [angle]
10532
+ * @param {Color} [color]
10533
+ * @param {number} [renderOrder] */
10534
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10535
+ {
10536
+ const bodyType = box2d.bodyTypeKinematic;
10537
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10538
+ }
10539
+ }
10540
+
10315
10541
  ///////////////////////////////////////////////////////////////////////////////
10316
10542
  /**
10317
10543
  * Box2D Wheel Joint
@@ -10674,6 +10900,7 @@ class Box2dPlugin
10674
10900
  box2d = this;
10675
10901
  this.instance = instance;
10676
10902
  this.world = new box2d.instance.b2World();
10903
+ this.objects = [];
10677
10904
 
10678
10905
  /** @property {number} - Velocity iterations per update*/
10679
10906
  this.velocityIterations = 8;
@@ -10916,9 +11143,10 @@ class Box2dPlugin
10916
11143
 
10917
11144
  /** converts a box2d vec2 pointer to a Vector2
10918
11145
  * @param {Object} v */
10919
- vec2FromPointer(v)
11146
+ vec2FromPointer(vp)
10920
11147
  {
10921
- return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
11148
+ const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
11149
+ return box2d.vec2From(v);
10922
11150
  }
10923
11151
 
10924
11152
  /** converts a Vector2 to a box2 vec2
@@ -10992,8 +11220,23 @@ async function box2dInit()
10992
11220
  // add the box2d plugin to the engine
10993
11221
  function box2dUpdate()
10994
11222
  {
10995
- if (!paused)
10996
- box2d.step();
11223
+ if (paused)
11224
+ return;
11225
+
11226
+ box2d.step();
11227
+
11228
+ // remove destroyed objects
11229
+ box2d.objects = box2d.objects.filter(o=>!o.destroyed);
11230
+
11231
+ // copy box2d physics results to engine objects
11232
+ for (const o of box2d.objects)
11233
+ {
11234
+ if (o.body)
11235
+ {
11236
+ o.pos = box2d.vec2From(o.body.GetPosition());
11237
+ o.angle = -o.body.GetAngle();
11238
+ }
11239
+ }
10997
11240
  }
10998
11241
  function box2dRender()
10999
11242
  {