littlejsengine 1.14.19 → 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 (70) hide show
  1. package/dist/littlejs.d.ts +217 -38
  2. package/dist/littlejs.esm.js +879 -324
  3. package/dist/littlejs.esm.min.js +1 -1
  4. package/dist/littlejs.js +861 -324
  5. package/dist/littlejs.min.js +1 -1
  6. package/dist/littlejs.release.js +799 -325
  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/game.js +1 -41
  11. package/examples/electron/index.html +2 -2
  12. package/examples/htmlMenu/game.js +1 -1
  13. package/examples/index.html +19 -10
  14. package/examples/module/game.js +5 -7
  15. package/examples/particles/index.html +6 -9
  16. package/examples/platformer/gameCharacter.js +4 -3
  17. package/examples/platformer/gameEffects.js +7 -2
  18. package/examples/platformer/gameObjects.js +6 -11
  19. package/examples/shorts/animation.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/box2d.js +2 -2
  22. package/examples/shorts/box2dCar.js +18 -7
  23. package/examples/shorts/box2dPool.js +108 -0
  24. package/examples/shorts/cameraDrag.js +22 -0
  25. package/examples/shorts/debugDraw.js +37 -0
  26. package/examples/shorts/flappyGame.js +1 -0
  27. package/examples/shorts/fps.js +90 -0
  28. package/examples/shorts/helloWorld.js +1 -1
  29. package/examples/shorts/hillGlideGame.js +1 -3
  30. package/examples/shorts/input.js +59 -0
  31. package/examples/shorts/landerGame.js +1 -3
  32. package/examples/shorts/medals.js +15 -9
  33. package/examples/shorts/music.js +15 -17
  34. package/examples/shorts/musicPlayer.js +24 -24
  35. package/examples/shorts/platformer.js +0 -2
  36. package/examples/shorts/postProcess.js +1 -1
  37. package/examples/shorts/sequencer.js +3 -3
  38. package/examples/shorts/slidingPuzzle.js +1 -1
  39. package/examples/shorts/spaceGame.js +1 -3
  40. package/examples/shorts/spriteAtlas.js +6 -8
  41. package/examples/shorts/starfield.js +1 -1
  42. package/examples/shorts/tileRaycast.js +39 -0
  43. package/examples/shorts/tiles.png +0 -0
  44. package/examples/shorts/tiltedView.js +1 -4
  45. package/examples/shorts/timers.js +1 -2
  46. package/examples/shorts/topDown.js +0 -2
  47. package/examples/starter/game.js +5 -7
  48. package/examples/starter/index.html +2 -2
  49. package/examples/typescript/game.js +3 -2
  50. package/examples/typescript/game.ts +5 -7
  51. package/examples/uiSystem/game.js +2 -2
  52. package/package.json +1 -1
  53. package/plugins/box2d.js +170 -50
  54. package/plugins/pluginExport.js +2 -0
  55. package/plugins/uiSystem.js +55 -27
  56. package/src/engine.js +19 -16
  57. package/src/engineAudio.js +27 -15
  58. package/src/engineDebug.js +64 -0
  59. package/src/engineDraw.js +88 -32
  60. package/src/engineExport.js +16 -0
  61. package/src/engineInput.js +35 -25
  62. package/src/engineMedals.js +2 -2
  63. package/src/engineObject.js +55 -10
  64. package/src/engineParticles.js +36 -20
  65. package/src/engineRelease.js +2 -1
  66. package/src/engineSettings.js +20 -1
  67. package/src/engineTileLayer.js +64 -59
  68. package/src/engineUtilities.js +213 -59
  69. package/src/engineWebGL.js +13 -8
  70. package/examples/shorts/raycasting.js +0 -79
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.14.19';
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
  {
@@ -516,7 +519,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
516
519
 
517
520
  /**
518
521
  * @callback ObjectCallbackFunction - Function that processes an object
519
- * @param {EngineObject} uiObjects
522
+ * @param {EngineObject} object
520
523
  * @memberof Engine
521
524
  */
522
525
 
@@ -564,7 +567,7 @@ function drawEngineSplashScreen(t)
564
567
  // background
565
568
  const p3 = percent(t, 1, .8);
566
569
  const p4 = percent(t, 0, .5);
567
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,Math.hypot(w,h)*.7);
570
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
568
571
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
569
572
  g.addColorStop(1,hsl(0,0,0,p3).toString());
570
573
  x.save();
@@ -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();
@@ -1339,6 +1376,36 @@ function debugVideoCaptureUpdate()
1339
1376
  combineCanvases();
1340
1377
  debugVideoCaptureTrack.requestFrame();
1341
1378
  debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
1379
+ }
1380
+
1381
+ ///////////////////////////////////////////////////////////////////////////////
1382
+ // debug utility functions
1383
+
1384
+ // make color constants immutable with debug assertions
1385
+ function debugProtectConstant(obj)
1386
+ {
1387
+ if (debug)
1388
+ {
1389
+ // get properties and store original values
1390
+ const props = Object.keys(obj), values = {};
1391
+ props.forEach(prop => values[prop] = obj[prop]);
1392
+
1393
+ // replace with getters/setters that assert
1394
+ props.forEach(prop =>
1395
+ {
1396
+ Object.defineProperty(obj, prop, {
1397
+ get: () => values[prop],
1398
+ set: (value) =>
1399
+ {
1400
+ ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1401
+ },
1402
+ enumerable: true
1403
+ });
1404
+ });
1405
+ }
1406
+
1407
+ // freeze the object to prevent adding new properties
1408
+ return Object.freeze(obj);
1342
1409
  }
1343
1410
  /**
1344
1411
  * LittleJS Utility Classes and Functions
@@ -1350,7 +1417,7 @@ function debugVideoCaptureUpdate()
1350
1417
  * @namespace Utilities
1351
1418
  */
1352
1419
 
1353
- /** A shortcut to get Math.PI
1420
+ /** The value of PI
1354
1421
  * @type {number}
1355
1422
  * @default Math.PI
1356
1423
  * @memberof Utilities */
@@ -1360,25 +1427,80 @@ const PI = Math.PI;
1360
1427
  * @param {number} value
1361
1428
  * @return {number}
1362
1429
  * @memberof Utilities */
1363
- function abs(value) { return Math.abs(value); }
1430
+ const abs = Math.abs;
1431
+
1432
+ /** Returns floored value of value passed in
1433
+ * @param {number} value
1434
+ * @return {number}
1435
+ * @memberof Utilities */
1436
+ const floor = Math.floor;
1437
+
1438
+ /** Returns ceiled value of value passed in
1439
+ * @param {number} value
1440
+ * @return {number}
1441
+ * @memberof Utilities */
1442
+ const ceil = Math.ceil;
1443
+
1444
+ /** Returns rounded value passed in
1445
+ * @param {number} value
1446
+ * @return {number}
1447
+ * @memberof Utilities */
1448
+ const round = Math.round;
1364
1449
 
1365
1450
  /** Returns lowest value passed in
1366
1451
  * @param {...number} values
1367
1452
  * @return {number}
1368
1453
  * @memberof Utilities */
1369
- function min(...values) { return Math.min(...values); }
1454
+ const min = Math.min;
1370
1455
 
1371
1456
  /** Returns highest value passed in
1372
1457
  * @param {...number} values
1373
1458
  * @return {number}
1374
1459
  * @memberof Utilities */
1375
- function max(...values) { return Math.max(...values); }
1460
+ const max = Math.max;
1376
1461
 
1377
1462
  /** Returns the sign of value passed in
1378
1463
  * @param {number} value
1379
1464
  * @return {number}
1380
1465
  * @memberof Utilities */
1381
- function sign(value) { return Math.sign(value); }
1466
+ const sign = Math.sign;
1467
+
1468
+ /** Returns hypotenuse of values passed in
1469
+ * @param {...number} values
1470
+ * @return {number}
1471
+ * @memberof Utilities */
1472
+ const hypot = Math.hypot;
1473
+
1474
+ /** Returns log2 of value passed in
1475
+ * @param {number} value
1476
+ * @return {number}
1477
+ * @memberof Utilities */
1478
+ const log2 = Math.log2;
1479
+
1480
+ /** Returns sin of value passed in
1481
+ * @param {number} value
1482
+ * @return {number}
1483
+ * @memberof Utilities */
1484
+ const sin = Math.sin;
1485
+
1486
+ /** Returns cos of value passed in
1487
+ * @param {number} value
1488
+ * @return {number}
1489
+ * @memberof Utilities */
1490
+ const cos = Math.cos;
1491
+
1492
+ /** Returns tan of value passed in
1493
+ * @param {number} value
1494
+ * @return {number}
1495
+ * @memberof Utilities */
1496
+ const tan = Math.tan;
1497
+
1498
+ /** Returns atan2 of values passed in
1499
+ * @param {number} y
1500
+ * @param {number} x
1501
+ * @return {number}
1502
+ * @memberof Utilities */
1503
+ const atan2 = Math.atan2;
1382
1504
 
1383
1505
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
1384
1506
  * @param {number} dividend
@@ -1483,7 +1605,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
1483
1605
  * @param {number} value
1484
1606
  * @return {number}
1485
1607
  * @memberof Utilities */
1486
- function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
1608
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
1487
1609
 
1488
1610
  /** Returns true if two axis aligned bounding boxes are overlapping
1489
1611
  * this can be used for simple collision detection between objects
@@ -1551,7 +1673,7 @@ function isIntersecting(start, end, pos, size)
1551
1673
  * @return {number} - Value waving between 0 and amplitude
1552
1674
  * @memberof Utilities */
1553
1675
  function wave(frequency=1, amplitude=1, t=time, offset=0)
1554
- { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1676
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
1555
1677
 
1556
1678
  /** Formats seconds to mm:ss style for display purposes
1557
1679
  * @param {number} t - time in seconds
@@ -1590,6 +1712,103 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1590
1712
  * @memberof Utilities */
1591
1713
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1592
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
+
1722
+ /**
1723
+ * @callback LineTestFunction - Checks if a position is colliding
1724
+ * @param {Vector2} pos
1725
+ * @memberof Draw
1726
+ */
1727
+
1728
+ /**
1729
+ * Casts a ray and returns position of the first collision found, or undefined if none are found
1730
+ * @param {Vector2} posStart
1731
+ * @param {Vector2} posEnd
1732
+ * @param {LineTestFunction} testFunction - Check if colliding
1733
+ * @param {Vector2} [normal] - Optional vector to store the normal
1734
+ * @return {Vector2|undefined} - Position of the collision or undefined if none found
1735
+ * @memberof Utilities */
1736
+ function lineTest(posStart, posEnd, testFunction, normal)
1737
+ {
1738
+ ASSERT(isVector2(posStart), 'posStart must be a vec2');
1739
+ ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
1740
+ ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
1741
+ ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
1742
+
1743
+ // get ray direction and length
1744
+ const dx = posEnd.x - posStart.x;
1745
+ const dy = posEnd.y - posStart.y;
1746
+ const totalLength = hypot(dx, dy);
1747
+ if (!totalLength)
1748
+ return;
1749
+
1750
+ // current integer cell we are in
1751
+ const pos = posStart.floor();
1752
+
1753
+ // normalize ray direction
1754
+ const dirX = dx / totalLength;
1755
+ const dirY = dy / totalLength;
1756
+
1757
+ // step direction in grid
1758
+ const stepX = sign(dirX);
1759
+ const stepY = sign(dirY);
1760
+
1761
+ // distance along the ray to cross one full cell in X or Y
1762
+ const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
1763
+ const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
1764
+
1765
+ // distance along the ray from start to the first grid boundary
1766
+ const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
1767
+ const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
1768
+ const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
1769
+ const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
1770
+
1771
+ // use line drawing algorithm to test for collisions
1772
+ let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
1773
+ while (t < totalLength)
1774
+ {
1775
+ if (testFunction(pos))
1776
+ {
1777
+ // set hit point
1778
+ const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
1779
+
1780
+ // move inside of tile if on positive edge
1781
+ const e = 1e-9;
1782
+ if (wasX)
1783
+ {
1784
+ if (stepX < 0)
1785
+ hitPos.x -= e;
1786
+ }
1787
+ if (stepY < 0)
1788
+ hitPos.y -= e;
1789
+
1790
+ // set normal
1791
+ if (normal)
1792
+ wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
1793
+ return hitPos;
1794
+ }
1795
+
1796
+ // advance to the next grid boundary
1797
+ if (wasX = tX < tY)
1798
+ {
1799
+ pos.x += stepX;
1800
+ t = tX;
1801
+ tX += tDeltaX;
1802
+ }
1803
+ else
1804
+ {
1805
+ pos.y += stepY;
1806
+ t = tY;
1807
+ tY += tDeltaY;
1808
+ }
1809
+ }
1810
+ }
1811
+
1593
1812
  ///////////////////////////////////////////////////////////////////////////////
1594
1813
 
1595
1814
  /** Random global functions
@@ -1608,7 +1827,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
1608
1827
  * @param {number} [valueB]
1609
1828
  * @return {number}
1610
1829
  * @memberof Random */
1611
- function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
1830
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
1612
1831
 
1613
1832
  /** Randomly returns true or false given the chance of true passed in
1614
1833
  * @param {number} [chance]
@@ -1687,7 +1906,7 @@ class RandomGenerator
1687
1906
  * @param {number} valueA
1688
1907
  * @param {number} [valueB]
1689
1908
  * @return {number} */
1690
- int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
1909
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
1691
1910
 
1692
1911
  /** Randomly returns true or false given the chance of true passed in
1693
1912
  * @param {number} [chance]
@@ -1714,6 +1933,39 @@ class RandomGenerator
1714
1933
  * @return {Vector2} */
1715
1934
  vec2(valueA=1, valueB=0)
1716
1935
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1936
+
1937
+ /** Returns a random color between the two passed in colors, combine components if linear
1938
+ * @param {Color} [colorA=(1,1,1,1)]
1939
+ * @param {Color} [colorB=(0,0,0,1)]
1940
+ * @param {boolean} [linear]
1941
+ * @return {Color} */
1942
+ randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1943
+ {
1944
+ return linear ? colorA.lerp(colorB, this.float()) :
1945
+ new Color(
1946
+ this.float(colorA.r,colorB.r),
1947
+ this.float(colorA.g,colorB.g),
1948
+ this.float(colorA.b,colorB.b),
1949
+ this.float(colorA.a,colorB.a));
1950
+ }
1951
+
1952
+ /** Returns a new color that has each component randomly adjusted
1953
+ * @param {Color} color
1954
+ * @param {number} [amount]
1955
+ * @param {number} [alphaAmount]
1956
+ * @return {Color} */
1957
+ mutateColor(color, amount=.05, alphaAmount=0)
1958
+ {
1959
+ ASSERT_NUMBER_VALID(amount);
1960
+ ASSERT_NUMBER_VALID(alphaAmount);
1961
+ return new Color
1962
+ (
1963
+ color.r + this.float(amount, -amount),
1964
+ color.g + this.float(amount, -amount),
1965
+ color.b + this.float(amount, -amount),
1966
+ color.a + this.float(alphaAmount, -alphaAmount)
1967
+ ).clamp();
1968
+ }
1717
1969
  }
1718
1970
 
1719
1971
  ///////////////////////////////////////////////////////////////////////////////
@@ -1865,7 +2117,7 @@ class Vector2
1865
2117
 
1866
2118
  /** Returns the clockwise angle of this vector, up is angle 0
1867
2119
  * @return {number} */
1868
- angle() { return Math.atan2(this.x, this.y); }
2120
+ angle() { return atan2(this.x, this.y); }
1869
2121
 
1870
2122
  /** Sets this vector with clockwise angle and length passed in
1871
2123
  * @param {number} [angle]
@@ -1875,8 +2127,8 @@ class Vector2
1875
2127
  {
1876
2128
  ASSERT_NUMBER_VALID(angle);
1877
2129
  ASSERT_NUMBER_VALID(length);
1878
- this.x = length*Math.sin(angle);
1879
- this.y = length*Math.cos(angle);
2130
+ this.x = length*sin(angle);
2131
+ this.y = length*cos(angle);
1880
2132
  return this;
1881
2133
  }
1882
2134
 
@@ -1886,7 +2138,7 @@ class Vector2
1886
2138
  rotate(angle)
1887
2139
  {
1888
2140
  ASSERT_NUMBER_VALID(angle);
1889
- const c = Math.cos(-angle), s = Math.sin(-angle);
2141
+ const c = cos(-angle), s = sin(-angle);
1890
2142
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1891
2143
  }
1892
2144
 
@@ -1918,7 +2170,7 @@ class Vector2
1918
2170
 
1919
2171
  /** Returns a copy of this vector with each axis floored
1920
2172
  * @return {Vector2} */
1921
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
2173
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
1922
2174
 
1923
2175
  /** Returns new vec2 with modded values
1924
2176
  * @param {number} [divisor]
@@ -2166,7 +2418,6 @@ class Color
2166
2418
  * @return {string} */
2167
2419
  toString(useAlpha = true)
2168
2420
  {
2169
- ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
2170
2421
  if (debug && !this.isValid())
2171
2422
  return `#000`;
2172
2423
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
@@ -2226,67 +2477,67 @@ class Color
2226
2477
  /** Color - White #ffffff
2227
2478
  * @type {Color}
2228
2479
  * @memberof Utilities */
2229
- const WHITE = protectEngineConstant(rgb());
2480
+ const WHITE = debugProtectConstant(rgb());
2230
2481
 
2231
2482
  /** Color - Clear White #757474ff with 0 alpha
2232
2483
  * @type {Color}
2233
2484
  * @memberof Utilities */
2234
- const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
2485
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
2235
2486
 
2236
2487
  /** Color - Black #000000
2237
2488
  * @type {Color}
2238
2489
  * @memberof Utilities */
2239
- const BLACK = protectEngineConstant(rgb(0,0,0));
2490
+ const BLACK = debugProtectConstant(rgb(0,0,0));
2240
2491
 
2241
2492
  /** Color - Clear Black #000000 with 0 alpha
2242
2493
  * @type {Color}
2243
2494
  * @memberof Utilities */
2244
- const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
2495
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
2245
2496
 
2246
2497
  /** Color - Gray #808080
2247
2498
  * @type {Color}
2248
2499
  * @memberof Utilities */
2249
- const GRAY = protectEngineConstant(rgb(.5,.5,.5));
2500
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
2250
2501
 
2251
2502
  /** Color - Red #ff0000
2252
2503
  * @type {Color}
2253
2504
  * @memberof Utilities */
2254
- const RED = protectEngineConstant(rgb(1,0,0));
2505
+ const RED = debugProtectConstant(rgb(1,0,0));
2255
2506
 
2256
2507
  /** Color - Orange #ff8000
2257
2508
  * @type {Color}
2258
2509
  * @memberof Utilities */
2259
- const ORANGE = protectEngineConstant(rgb(1,.5,0));
2510
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
2260
2511
 
2261
2512
  /** Color - Yellow #ffff00
2262
2513
  * @type {Color}
2263
2514
  * @memberof Utilities */
2264
- const YELLOW = protectEngineConstant(rgb(1,1,0));
2515
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
2265
2516
 
2266
2517
  /** Color - Green #00ff00
2267
2518
  * @type {Color}
2268
2519
  * @memberof Utilities */
2269
- const GREEN = protectEngineConstant(rgb(0,1,0));
2520
+ const GREEN = debugProtectConstant(rgb(0,1,0));
2270
2521
 
2271
2522
  /** Color - Cyan #00ffff
2272
2523
  * @type {Color}
2273
2524
  * @memberof Utilities */
2274
- const CYAN = protectEngineConstant(rgb(0,1,1));
2525
+ const CYAN = debugProtectConstant(rgb(0,1,1));
2275
2526
 
2276
2527
  /** Color - Blue #0000ff
2277
2528
  * @type {Color}
2278
2529
  * @memberof Utilities */
2279
- const BLUE = protectEngineConstant(rgb(0,0,1));
2530
+ const BLUE = debugProtectConstant(rgb(0,0,1));
2280
2531
 
2281
2532
  /** Color - Purple #8000ff
2282
2533
  * @type {Color}
2283
2534
  * @memberof Utilities */
2284
- const PURPLE = protectEngineConstant(rgb(.5,0,1));
2535
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
2285
2536
 
2286
2537
  /** Color - Magenta #ff00ff
2287
2538
  * @type {Color}
2288
2539
  * @memberof Utilities */
2289
- const MAGENTA = protectEngineConstant(rgb(1,0,1));
2540
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2290
2541
 
2291
2542
  ///////////////////////////////////////////////////////////////////////////////
2292
2543
 
@@ -2349,41 +2600,11 @@ class Timer
2349
2600
 
2350
2601
  /** Returns this timer expressed as a string
2351
2602
  * @return {string} */
2352
- toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2603
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2353
2604
 
2354
2605
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2355
2606
  * @return {number} */
2356
2607
  valueOf() { return this.get(); }
2357
- }
2358
-
2359
- ///////////////////////////////////////////////////////////////////////////////
2360
- // Helper functions used by the engine
2361
-
2362
- // make color constants immutable with debug assertions
2363
- function protectEngineConstant(obj)
2364
- {
2365
- if (debug)
2366
- {
2367
- // get properties and store original values
2368
- const props = Object.keys(obj), values = {};
2369
- props.forEach(prop => values[prop] = obj[prop]);
2370
-
2371
- // replace with getters/setters that assert
2372
- props.forEach(prop =>
2373
- {
2374
- Object.defineProperty(obj, prop, {
2375
- get: () => values[prop],
2376
- set: (value) =>
2377
- {
2378
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
2379
- },
2380
- enumerable: true
2381
- });
2382
- });
2383
- }
2384
-
2385
- // freeze the object to prevent adding new properties
2386
- return Object.freeze(obj);
2387
2608
  }
2388
2609
  /**
2389
2610
  * LittleJS Engine Settings
@@ -2601,6 +2822,14 @@ let touchInputEnable = true;
2601
2822
  * @memberof Settings */
2602
2823
  let touchGamepadEnable = false;
2603
2824
 
2825
+ /** True if touch gamepad should have start button in the center
2826
+ * - When the game is paused, any touch will press the button
2827
+ * - This can function as a way to pause/unpause the game
2828
+ * @type {boolean}
2829
+ * @default
2830
+ * @memberof Settings */
2831
+ let touchGamepadCenterButton = true;
2832
+
2604
2833
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
2605
2834
  * @type {boolean}
2606
2835
  * @default
@@ -2776,9 +3005,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2776
3005
  * @memberof Settings */
2777
3006
  function setGLEnable(enable)
2778
3007
  {
3008
+ if (enable && !glCanBeEnabled)
3009
+ {
3010
+ console.warn('Can not enable WebGL if it was disabled on start.');
3011
+ return;
3012
+ }
2779
3013
  glEnable = enable;
2780
3014
  if (glCanvas) // hide glCanvas if WebGL is disabled
2781
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
3015
+ glCanvas.style.display = enable ? '' : 'none';
2782
3016
  }
2783
3017
 
2784
3018
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -2866,6 +3100,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
2866
3100
  * @memberof Settings */
2867
3101
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
2868
3102
 
3103
+ /** True if touch gamepad should have start button in the center
3104
+ * - This can function as a way to pause/unpause the game
3105
+ * @param {boolean} enable
3106
+ * @memberof Settings */
3107
+ function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
3108
+
2869
3109
  /** Set if touch gamepad should be analog stick or 8 way dpad
2870
3110
  * @param {boolean} analog
2871
3111
  * @memberof Settings */
@@ -3075,11 +3315,10 @@ class EngineObject
3075
3315
  }
3076
3316
 
3077
3317
  /** Update the object physics, called automatically by engine once each frame */
3078
- update()
3318
+ updatePhysics()
3079
3319
  {
3080
3320
  // child objects do not have physics
3081
- if (this.parent)
3082
- return;
3321
+ ASSERT(!this.parent);
3083
3322
 
3084
3323
  if (this.clampSpeed)
3085
3324
  {
@@ -3129,7 +3368,7 @@ class EngineObject
3129
3368
  continue;
3130
3369
 
3131
3370
  // check collision
3132
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
3371
+ if (!this.isOverlappingObject(o))
3133
3372
  continue;
3134
3373
 
3135
3374
  // notify objects of collision and check if should be resolved
@@ -3227,6 +3466,27 @@ class EngineObject
3227
3466
  // test which side we bounced off (or both if a corner)
3228
3467
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3229
3468
  const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
3469
+
3470
+ if (blockedLayerX)
3471
+ {
3472
+ // try to move up a tiny bit
3473
+ const epsilon = 1e-3;
3474
+ const maxMoveUp = .1;
3475
+ const y = floor(oldPos.y-this.size.y/2+1) +
3476
+ this.size.y/2 + epsilon;
3477
+ const delta = y - this.pos.y;
3478
+ if (delta < maxMoveUp)
3479
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
3480
+ {
3481
+ this.pos.y = y;
3482
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
3483
+ return;
3484
+ }
3485
+
3486
+ // move to previous position and bounce
3487
+ this.pos.x = oldPos.x;
3488
+ this.velocity.x *= -this.restitution;
3489
+ }
3230
3490
  if (blockedLayerY || !blockedLayerX)
3231
3491
  {
3232
3492
  // bounce velocity
@@ -3250,18 +3510,15 @@ class EngineObject
3250
3510
  this.groundObject = undefined;
3251
3511
  }
3252
3512
  }
3253
- if (blockedLayerX)
3254
- {
3255
- // move to previous position and bounce
3256
- this.pos.x = oldPos.x;
3257
- this.velocity.x *= -this.restitution;
3258
- }
3259
3513
  debugPhysics && debugRect(this.pos, this.size, '#f00');
3260
3514
  }
3261
3515
  }
3262
3516
  }
3263
3517
  }
3264
3518
 
3519
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
3520
+ update() {}
3521
+
3265
3522
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
3266
3523
  render()
3267
3524
  {
@@ -3313,10 +3570,24 @@ class EngineObject
3313
3570
  */
3314
3571
  collideWithObject(object) { return true; }
3315
3572
 
3573
+ /** Get this object's up vector
3574
+ * @param {number} [scale] - length of the vector
3575
+ * @return {Vector2} */
3576
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
3577
+
3578
+ /** Get this object's right vector
3579
+ * @param {number} [scale] - length of the vector
3580
+ * @return {Vector2} */
3581
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
3582
+
3316
3583
  /** How long since the object was created
3317
3584
  * @return {number} */
3318
3585
  getAliveTime() { return time - this.spawnTime; }
3319
3586
 
3587
+ /** Get the speed of this object
3588
+ * @return {number} */
3589
+ getSpeed() { return this.velocity.length(); }
3590
+
3320
3591
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
3321
3592
  * @param {Vector2} acceleration */
3322
3593
  applyAcceleration(acceleration)
@@ -3361,6 +3632,20 @@ class EngineObject
3361
3632
  child.parent = 0;
3362
3633
  }
3363
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
+
3364
3649
  /** Set how this object collides
3365
3650
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
3366
3651
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -3664,6 +3949,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3664
3949
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3665
3950
  if (useWebGL)
3666
3951
  {
3952
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3667
3953
  if (screenSpace)
3668
3954
  {
3669
3955
  // convert to world space
@@ -3759,6 +4045,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3759
4045
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3760
4046
  if (useWebGL)
3761
4047
  {
4048
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3762
4049
  if (screenSpace)
3763
4050
  {
3764
4051
  // convert to world space
@@ -3770,7 +4057,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3770
4057
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
3771
4058
  const colorTopInt = colorTop.rgbaInt();
3772
4059
  const colorBottomInt = colorBottom.rgbaInt();
3773
- const c = Math.cos(-angle), s = Math.sin(-angle);
4060
+ const c = cos(-angle), s = sin(-angle);
3774
4061
  for (let i=4; i--;)
3775
4062
  {
3776
4063
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -3813,7 +4100,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3813
4100
  * @memberof Draw */
3814
4101
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3815
4102
  {
3816
- ASSERT(Array.isArray(points), 'points must be an array');
4103
+ ASSERT(isArray(points), 'points must be an array');
3817
4104
  ASSERT(isNumber(width), 'width must be a number');
3818
4105
  ASSERT(isColor(color), 'color is invalid');
3819
4106
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3821,6 +4108,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3821
4108
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3822
4109
  if (useWebGL)
3823
4110
  {
4111
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3824
4112
  let scale = 1;
3825
4113
  if (screenSpace)
3826
4114
  {
@@ -3870,6 +4158,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3870
4158
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3871
4159
  const size = vec2(width, halfDelta.length()*2);
3872
4160
  pos = pos.add(posA.add(halfDelta));
4161
+ if (screenSpace)
4162
+ halfDelta.y *= -1; // flip angle Y if screen space
3873
4163
  angle += halfDelta.angle();
3874
4164
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3875
4165
  }
@@ -3897,7 +4187,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3897
4187
  for (let i=sides; i--;)
3898
4188
  {
3899
4189
  const a = (i/sides)*PI*2;
3900
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
4190
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
3901
4191
  }
3902
4192
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3903
4193
  }
@@ -3916,7 +4206,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3916
4206
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3917
4207
  {
3918
4208
  ASSERT(isVector2(pos), 'pos must be a vec2');
3919
- ASSERT(Array.isArray(points), 'points must be an array');
4209
+ ASSERT(isArray(points), 'points must be an array');
3920
4210
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3921
4211
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3922
4212
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -3924,6 +4214,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3924
4214
 
3925
4215
  if (useWebGL)
3926
4216
  {
4217
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3927
4218
  let scale = 1;
3928
4219
  if (screenSpace)
3929
4220
  {
@@ -4067,11 +4358,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
4067
4358
  * @param {string} [font=fontDefault]
4068
4359
  * @param {string} [fontStyle]
4069
4360
  * @param {number} [maxWidth]
4361
+ * @param {number} [angle]
4070
4362
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
4071
4363
  * @memberof Draw */
4072
- 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)
4073
4365
  {
4074
- 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);
4075
4367
  }
4076
4368
 
4077
4369
  /** Draw text on overlay canvas in world space
@@ -4086,10 +4378,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
4086
4378
  * @param {string} [font=fontDefault]
4087
4379
  * @param {string} [fontStyle]
4088
4380
  * @param {number} [maxWidth]
4381
+ * @param {number} [angle]
4089
4382
  * @memberof Draw */
4090
- 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)
4091
4384
  {
4092
- 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);
4093
4386
  }
4094
4387
 
4095
4388
  /** Draw text on overlay canvas in screen space
@@ -4104,9 +4397,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
4104
4397
  * @param {string} [font=fontDefault]
4105
4398
  * @param {string} [fontStyle]
4106
4399
  * @param {number} [maxWidth]
4400
+ * @param {number} [angle]
4107
4401
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
4108
4402
  * @memberof Draw */
4109
- 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)
4110
4404
  {
4111
4405
  ASSERT(isString(text), 'text must be a string');
4112
4406
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -4117,6 +4411,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4117
4411
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
4118
4412
  ASSERT(isString(font), 'font must be a string');
4119
4413
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
4414
+ ASSERT(isNumber(angle), 'angle must be a number');
4120
4415
 
4121
4416
  context.fillStyle = color.toString();
4122
4417
  context.strokeStyle = lineColor.toString();
@@ -4126,14 +4421,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4126
4421
  context.textBaseline = 'middle';
4127
4422
 
4128
4423
  const lines = (text+'').split('\n');
4129
- let posY = pos.y;
4130
- 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;
4131
4429
  lines.forEach(line=>
4132
4430
  {
4133
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
4134
- context.fillText(line, pos.x, posY, maxWidth);
4135
- posY += size;
4431
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
4432
+ context.fillText(line, 0, yOffset, maxWidth);
4433
+ yOffset += size;
4136
4434
  });
4435
+ context.restore();
4137
4436
  }
4138
4437
 
4139
4438
  ///////////////////////////////////////////////////////////////////////////////
@@ -4145,18 +4444,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
4145
4444
  * @memberof Draw */
4146
4445
  function screenToWorld(screenPos)
4147
4446
  {
4148
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4149
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
4447
+ let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
4448
+ let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
4150
4449
  if (cameraAngle)
4151
4450
  {
4152
4451
  // apply camera rotation
4153
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
4154
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
4155
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
4156
- cameraPosRelativeX = rotatedX;
4157
- cameraPosRelativeY = rotatedY;
4452
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
4453
+ const rotatedX = x * c - y * s;
4454
+ const rotatedY = x * s + y * c;
4455
+ x = rotatedX;
4456
+ y = rotatedY;
4158
4457
  }
4159
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
4458
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
4160
4459
  }
4161
4460
 
4162
4461
  /** Convert from world to screen space coordinates
@@ -4165,24 +4464,64 @@ function screenToWorld(screenPos)
4165
4464
  * @memberof Draw */
4166
4465
  function worldToScreen(worldPos)
4167
4466
  {
4168
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
4169
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
4467
+ let x = worldPos.x - cameraPos.x;
4468
+ let y = worldPos.y - cameraPos.y;
4170
4469
  if (cameraAngle)
4171
4470
  {
4172
4471
  // apply inverse camera rotation
4173
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
4174
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
4175
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
4176
- cameraPosRelativeX = rotatedX;
4177
- cameraPosRelativeY = rotatedY;
4472
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4473
+ const rotatedX = x * c - y * s;
4474
+ const rotatedY = x * s + y * c;
4475
+ x = rotatedX;
4476
+ y = rotatedY;
4178
4477
  }
4179
4478
  return new Vector2
4180
4479
  (
4181
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
4182
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
4480
+ x * cameraScale + mainCanvasSize.x/2 - .5,
4481
+ y * -cameraScale + mainCanvasSize.y/2 - .5
4183
4482
  );
4184
4483
  }
4185
4484
 
4485
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
4486
+ * @param {Vector2} screenDelta
4487
+ * @return {Vector2}
4488
+ * @memberof Draw */
4489
+ function screenToWorldDelta(screenDelta)
4490
+ {
4491
+ let x = screenDelta.x / cameraScale;
4492
+ let y = screenDelta.y / -cameraScale;
4493
+ if (cameraAngle)
4494
+ {
4495
+ // apply camera rotation
4496
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
4497
+ const rotatedX = x * c - y * s;
4498
+ const rotatedY = x * s + y * c;
4499
+ x = rotatedX;
4500
+ y = rotatedY;
4501
+ }
4502
+ return new Vector2(x, y);
4503
+ }
4504
+
4505
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
4506
+ * @param {Vector2} worldDelta
4507
+ * @return {Vector2}
4508
+ * @memberof Draw */
4509
+ function worldToScreenDelta(worldDelta)
4510
+ {
4511
+ let x = worldDelta.x;
4512
+ let y = worldDelta.y;
4513
+ if (cameraAngle)
4514
+ {
4515
+ // apply inverse camera rotation
4516
+ const c = cos(cameraAngle), s = sin(cameraAngle);
4517
+ const rotatedX = x * c - y * s;
4518
+ const rotatedY = x * s + y * c;
4519
+ x = rotatedX;
4520
+ y = rotatedY;
4521
+ }
4522
+ return new Vector2(x * cameraScale, y * -cameraScale);
4523
+ }
4524
+
4186
4525
  /** Get the camera's visible area in world space
4187
4526
  * @return {Vector2}
4188
4527
  * @memberof Draw */
@@ -4235,6 +4574,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4235
4574
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
4236
4575
  const sx2 = bleedScale;
4237
4576
  const sy2 = bleedScale;
4577
+ sWidth = max(1,sWidth|0);
4578
+ sHeight = max(1,sHeight|0);
4238
4579
  const sWidth2 = sWidth - 2*bleedScale;
4239
4580
  const sHeight2 = sHeight - 2*bleedScale;
4240
4581
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -4249,7 +4590,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
4249
4590
  // copy to offscreen canvas
4250
4591
  workCanvas.width = sWidth;
4251
4592
  workCanvas.height = sHeight;
4252
- workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
4593
+ workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
4253
4594
 
4254
4595
  // tint image using offscreen work context
4255
4596
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -4523,6 +4864,11 @@ let mouseDeltaScreen = vec2();
4523
4864
  * @memberof Input */
4524
4865
  let mouseWheel = 0;
4525
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
+
4526
4872
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
4527
4873
  * @type {boolean}
4528
4874
  * @memberof Input */
@@ -4586,9 +4932,9 @@ function inputUpdate()
4586
4932
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4587
4933
  inputClear();
4588
4934
 
4589
- // update mouse world space position
4935
+ // update mouse world space position and delta
4590
4936
  mousePos = screenToWorld(mousePosScreen);
4591
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
4937
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
4592
4938
 
4593
4939
  // update gamepads if enabled
4594
4940
  gamepadsUpdate();
@@ -4617,10 +4963,10 @@ function inputInit()
4617
4963
  document.addEventListener('mousedown', onMouseDown);
4618
4964
  document.addEventListener('mouseup', onMouseUp);
4619
4965
  document.addEventListener('mousemove', onMouseMove);
4966
+ document.addEventListener('mouseleave', onMouseLeave);
4620
4967
  document.addEventListener('wheel', onMouseWheel);
4621
4968
  document.addEventListener('contextmenu', onContextMenu);
4622
4969
  document.addEventListener('blur', onBlur);
4623
- document.addEventListener('mouseleave', onMouseLeave);
4624
4970
 
4625
4971
  // init touch input
4626
4972
  if (isTouchDevice && touchInputEnable)
@@ -4642,14 +4988,14 @@ function inputInit()
4642
4988
  if (inputWASDEmulateDirection)
4643
4989
  inputData[0][remapKey(e.code)] = 4;
4644
4990
  }
4645
- function remapKey(c)
4991
+ function remapKey(k)
4646
4992
  {
4647
4993
  // handle remapping wasd keys to directions
4648
4994
  return inputWASDEmulateDirection ?
4649
- c === 'KeyW' ? 'ArrowUp' :
4650
- c === 'KeyS' ? 'ArrowDown' :
4651
- c === 'KeyA' ? 'ArrowLeft' :
4652
- c === 'KeyD' ? 'ArrowRight' : c : c;
4995
+ k === 'KeyW' ? 'ArrowUp' :
4996
+ k === 'KeyS' ? 'ArrowDown' :
4997
+ k === 'KeyA' ? 'ArrowLeft' :
4998
+ k === 'KeyD' ? 'ArrowRight' : k : k;
4653
4999
  }
4654
5000
  function onMouseDown(e)
4655
5001
  {
@@ -4662,7 +5008,10 @@ function inputInit()
4662
5008
 
4663
5009
  isUsingGamepad = false;
4664
5010
  inputData[0][e.button] = 3;
5011
+
5012
+ const mousePosScreenLast = mousePosScreen;
4665
5013
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
5014
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4666
5015
  inputPreventDefault && e.button && e.preventDefault();
4667
5016
  }
4668
5017
  function onMouseUp(e)
@@ -4673,18 +5022,20 @@ function inputInit()
4673
5022
  }
4674
5023
  function onMouseMove(e)
4675
5024
  {
5025
+ mouseInWindow = true;
5026
+ const mousePosScreenLast = mousePosScreen;
4676
5027
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4677
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
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);
4678
5034
  }
5035
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
4679
5036
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4680
5037
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4681
5038
  function onBlur() { inputClear(); } // reset input when focus is lost
4682
- function onMouseLeave()
4683
- {
4684
- // set mouse position and delta when leaving canvas
4685
- mousePosScreen = vec2(-1);
4686
- mouseDeltaScreen = vec2(0);
4687
- }
4688
5039
  }
4689
5040
 
4690
5041
  // convert a mouse or touch event position to screen space
@@ -4728,8 +5079,8 @@ function gamepadsUpdate()
4728
5079
  else if (touchGamepadStick.lengthSquared() > .3)
4729
5080
  {
4730
5081
  // convert to 8 way dpad
4731
- sticks[0].x = Math.round(touchGamepadStick.x);
4732
- sticks[0].y = -Math.round(touchGamepadStick.y);
5082
+ sticks[0].x = round(touchGamepadStick.x);
5083
+ sticks[0].y = -round(touchGamepadStick.y);
4733
5084
  sticks[0] = sticks[0].clampLength();
4734
5085
  }
4735
5086
 
@@ -4820,7 +5171,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
4820
5171
  function touchGamepadButtonCenter()
4821
5172
  {
4822
5173
  // draw right face buttons
4823
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
5174
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4824
5175
  if (touchGamepadButtonCount <= 2)
4825
5176
  center.x += touchGamepadSize/2;
4826
5177
  return center;
@@ -4830,14 +5181,13 @@ function touchGamepadButtonCenter()
4830
5181
  function touchInputInit()
4831
5182
  {
4832
5183
  // add non passive touch event listeners
4833
- let handleTouch = handleTouchDefault;
4834
5184
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4835
5185
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4836
5186
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4837
5187
 
4838
5188
  // handle all touch events the same way
4839
5189
  let wasTouching;
4840
- function handleTouchDefault(e)
5190
+ function handleTouch(e)
4841
5191
  {
4842
5192
  if (!touchInputEnable)
4843
5193
  return;
@@ -4857,11 +5207,11 @@ function touchInputInit()
4857
5207
  {
4858
5208
  // set event pos and pass it along
4859
5209
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
4860
- const lastMousePosScreen = mousePosScreen;
5210
+ const mousePosScreenLast = mousePosScreen;
4861
5211
  mousePosScreen = mouseEventToScreen(pos);
4862
5212
  if (wasTouching)
4863
5213
  {
4864
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
5214
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4865
5215
  isUsingGamepad = touchGamepadEnable;
4866
5216
  }
4867
5217
  else
@@ -4893,7 +5243,7 @@ function touchInputInit()
4893
5243
  if (touching)
4894
5244
  {
4895
5245
  touchGamepadTimer.set();
4896
- if (paused && !wasTouching)
5246
+ if (touchGamepadCenterButton && !wasTouching && paused)
4897
5247
  {
4898
5248
  // touch anywhere to press start when paused
4899
5249
  touchGamepadButtons[9] = 1;
@@ -4932,7 +5282,8 @@ function touchInputInit()
4932
5282
  if (button < touchGamepadButtonCount)
4933
5283
  touchGamepadButtons[button] = 1;
4934
5284
  }
4935
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
5285
+ else if (touchGamepadCenterButton && !wasTouching &&
5286
+ startCenter.distance(touchPos) < touchGamepadSize)
4936
5287
  {
4937
5288
  // virtual start button in center
4938
5289
  touchGamepadButtons[9] = 1;
@@ -4987,7 +5338,7 @@ function touchGamepadRender()
4987
5338
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
4988
5339
  for (let i=0; i<touchGamepadButtonCount; i++)
4989
5340
  {
4990
- let j = mod(i-1, 4);
5341
+ const j = mod(i-1, 4);
4991
5342
  let button = touchGamepadButtonCount > 2 ?
4992
5343
  j : min(j, touchGamepadButtonCount-1);
4993
5344
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -5089,6 +5440,10 @@ class Sound
5089
5440
  {
5090
5441
  if (!soundEnable || headlessMode) return;
5091
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
+
5092
5447
  /** @property {number} - World space max range of sound */
5093
5448
  this.range = range;
5094
5449
  /** @property {number} - At what percentage of range should it start tapering */
@@ -5127,6 +5482,12 @@ class Sound
5127
5482
  */
5128
5483
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
5129
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
+
5130
5491
  if (!soundEnable || headlessMode) return;
5131
5492
  if (!this.sampleChannels) return;
5132
5493
 
@@ -5172,6 +5533,7 @@ class Sound
5172
5533
  */
5173
5534
  playNote(semitoneOffset=0, pos, volume)
5174
5535
  {
5536
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5175
5537
  const pitch = getNoteFrequency(semitoneOffset, 1);
5176
5538
  return this.play(pos, volume, pitch, 0);
5177
5539
  }
@@ -5180,7 +5542,7 @@ class Sound
5180
5542
  * @return {number} - How long the sound is in seconds (undefined if loading)
5181
5543
  */
5182
5544
  getDuration()
5183
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
5545
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
5184
5546
 
5185
5547
  /** Check if sound is loaded, for sounds fetched from a url
5186
5548
  * @return {boolean} - True if sound is loaded and ready to play
@@ -5222,6 +5584,7 @@ class SoundWave extends Sound
5222
5584
  super(undefined, range, taper);
5223
5585
  if (!soundEnable || headlessMode) return;
5224
5586
  ASSERT(!filename || isString(filename), 'filename must be a string');
5587
+ ASSERT(isNumber(randomness), 'randomness must be a number');
5225
5588
 
5226
5589
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5227
5590
  this.onloadCallback = onloadCallback;
@@ -5438,7 +5801,7 @@ class SoundInstance
5438
5801
  /** Get the total duration of this sound
5439
5802
  * @return {number} - Total duration in seconds
5440
5803
  */
5441
- getDuration() { return this.sound.getDuration() / this.rate; }
5804
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
5442
5805
 
5443
5806
  /** Get source of this sound instance
5444
5807
  * @return {AudioBufferSourceNode}
@@ -5628,10 +5991,10 @@ function zzfxG
5628
5991
 
5629
5992
  // biquad LP/HP filter
5630
5993
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5631
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5632
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5633
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5634
- b1 = -(sign(filter) + cos) / a0, b2 = b0,
5994
+ cosw = cos(w), alpha = sin(w) / 2 / quality,
5995
+ a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
5996
+ b0 = (1 + sign(filter) * cosw) / 2 / a0,
5997
+ b1 = -(sign(filter) + cosw) / a0, b2 = b0,
5635
5998
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5636
5999
 
5637
6000
  // scale by sample rate
@@ -5654,15 +6017,15 @@ function zzfxG
5654
6017
  if (!(++crush%(bitCrush*100|0))) // bit crush
5655
6018
  {
5656
6019
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
5657
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5658
- Math.sin(t**3) : // 4 noise
5659
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
5660
- 1-(2*t/PI2%2+2)%2: // 2 saw
5661
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
5662
- Math.sin(t); // 0 sin
6020
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
6021
+ sin(t**3) : // 4 noise
6022
+ max(min(tan(t),1),-1): // 3 tan
6023
+ 1-(2*t/PI2%2+2)%2: // 2 saw
6024
+ 1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
6025
+ sin(t); // 0 sin
5663
6026
 
5664
6027
  s = (repeatTime ?
5665
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
6028
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
5666
6029
  : 1) *
5667
6030
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
5668
6031
  (i < attack ? i/attack : // attack
@@ -5684,8 +6047,8 @@ function zzfxG
5684
6047
  }
5685
6048
 
5686
6049
  f = (frequency += slide += deltaSlide) *// frequency
5687
- Math.cos(modulation*modOffset++); // modulation
5688
- t += f + f*noise*Math.sin(i**5); // noise
6050
+ cos(modulation*modOffset++); // modulation
6051
+ t += f + f*noise*sin(i**5); // noise
5689
6052
 
5690
6053
  if (jump && ++jump > pitchJumpTime) // pitch jump
5691
6054
  {
@@ -5751,23 +6114,23 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5751
6114
  }
5752
6115
  }
5753
6116
 
5754
- /** Return the center of first tile hit, undefined if nothing was hit.
5755
- * This does not return the exact intersection, but the center of the tile hit.
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)
5756
6119
  * @param {Vector2} posStart
5757
6120
  * @param {Vector2} posEnd
5758
6121
  * @param {EngineObject} [object] - An object or undefined for generic test
6122
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5759
6123
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5760
- * @return {Vector2}
6124
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5761
6125
  * @memberof TileLayers */
5762
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
6126
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5763
6127
  {
5764
6128
  for (const layer of tileCollisionLayers)
5765
6129
  {
5766
6130
  if (!solidOnly || layer.isSolid)
5767
6131
  {
5768
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
5769
- if (hitPos)
5770
- return hitPos;
6132
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
6133
+ if (hitPos) return hitPos;
5771
6134
  }
5772
6135
  }
5773
6136
  }
@@ -5905,6 +6268,8 @@ class CanvasLayer extends EngineObject
5905
6268
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5906
6269
  const useWebGL = false; // do not use webgl by default
5907
6270
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
6271
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
6272
+ this.refreshWebGL = false;
5908
6273
 
5909
6274
  // disable physics by default
5910
6275
  this.mass = this.gravityScale = this.friction = this.restitution = 0;
@@ -5938,8 +6303,15 @@ class CanvasLayer extends EngineObject
5938
6303
  * @memberof Draw */
5939
6304
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5940
6305
  {
5941
- // draw the canvas layer as a single tile that uses the whole texture
5942
6306
  const useWebGL = glEnable && this.textureInfo.hasWebGL();
6307
+ if (useWebGL && this.refreshWebGL)
6308
+ {
6309
+ // update the WebGL texture
6310
+ this.textureInfo.createWebGLTexture();
6311
+ this.refreshWebGL = false;
6312
+ }
6313
+
6314
+ // draw the canvas layer as a single tile that uses the whole texture
5943
6315
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5944
6316
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5945
6317
  }
@@ -6009,9 +6381,18 @@ class CanvasLayer extends EngineObject
6009
6381
  { this.drawTile(pos, size, undefined, color, angle); }
6010
6382
 
6011
6383
  /** Create or update the WebGL texture for this layer
6012
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
6013
- useWebGL(enable=true)
6384
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture
6385
+ * @param {boolean} [immediate] - shoulkd the texture be updated immediately
6386
+ */
6387
+ useWebGL(enable=true, immediate=false)
6014
6388
  {
6389
+ if (!immediate && enable && this.textureInfo.hasWebGL())
6390
+ {
6391
+ // refresh the texture when needed
6392
+ this.refreshWebGL = true;
6393
+ return;
6394
+ }
6395
+
6015
6396
  if (enable)
6016
6397
  this.textureInfo.createWebGLTexture();
6017
6398
  else
@@ -6042,7 +6423,7 @@ class TileLayer extends CanvasLayer
6042
6423
  */
6043
6424
  constructor(position, size, tileInfo=tile(), renderOrder=0)
6044
6425
  {
6045
- const canvasSize = size.multiply(tileInfo.size);
6426
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6046
6427
  super(position, size, 0, renderOrder, canvasSize);
6047
6428
 
6048
6429
  // set tile info
@@ -6095,6 +6476,13 @@ class TileLayer extends CanvasLayer
6095
6476
  {
6096
6477
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
6097
6478
 
6479
+ if (this.refreshWebGL)
6480
+ {
6481
+ // update the WebGL texture
6482
+ this.textureInfo.createWebGLTexture();
6483
+ this.refreshWebGL = false;
6484
+ }
6485
+
6098
6486
  // draw the tile layer as a single tile
6099
6487
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
6100
6488
  const size = this.drawSize || this.size;
@@ -6131,8 +6519,9 @@ class TileLayer extends CanvasLayer
6131
6519
  drawCanvas = this.canvas;
6132
6520
  drawContext = this.context;
6133
6521
  cameraPos = this.size.scale(.5);
6134
- cameraScale = this.tileInfo.size.x;
6135
- mainCanvasSize = this.size.multiply(this.tileInfo.size);
6522
+ const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
6523
+ cameraScale = tileSize.x;
6524
+ mainCanvasSize = this.size.multiply(tileSize);
6136
6525
  if (clear)
6137
6526
  {
6138
6527
  // clear and set size
@@ -6292,66 +6681,45 @@ class TileCollisionLayer extends TileLayer
6292
6681
  // check if the object should collide with this tile
6293
6682
  const tileData = this.collisionData[y*this.size.x+x];
6294
6683
  if (tileData)
6295
- if (!object || object.collideWithTile(tileData, hitPos.set(x, y)))
6684
+ if (!object || object.collideWithTile(tileData,
6685
+ hitPos.set(x + this.pos.x, y + this.pos.y)))
6296
6686
  return true;
6297
6687
  }
6298
6688
  return false;
6299
6689
  }
6300
6690
 
6301
- /** Return the center of first tile hit, undefined if nothing was hit.
6302
- * This does not return the exact intersection, but the center of the tile hit.
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)
6303
6693
  * @param {Vector2} posStart
6304
6694
  * @param {Vector2} posEnd
6305
- * @param {EngineObject} [object]
6306
- * @return {Vector2} */
6307
- collisionRaycast(posStart, posEnd, object)
6695
+ * @param {EngineObject} [object] - An object or undefined for generic test
6696
+ * @param {Vector2} [normal] - Optional normal of the surface hit
6697
+ * @return {Vector2|undefined} */
6698
+ collisionRaycast(posStart, posEnd, object, normal)
6308
6699
  {
6309
6700
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6310
6701
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6311
-
6312
- // transform to local layer space
6313
- const posStartX = posStart.x - this.pos.x;
6314
- const posStartY = posStart.y - this.pos.y;
6315
- const posEndX = posEnd.x - this.pos.x;
6316
- const posEndY = posEnd.y - this.pos.y;
6317
-
6318
- // test if a ray collides with tiles from start to end
6319
- const deltaX = posEndX - posStartX;
6320
- const deltaY = posEndY - posStartY;
6321
- const totalLength = (deltaX**2 + deltaY**2)**.5;
6322
- const unitX = abs(totalLength/deltaX);
6323
- const unitY = abs(totalLength/deltaY);
6324
-
6325
- // setup iteration variables
6326
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
6327
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
6328
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
6329
-
6330
- // use line drawing algorithm to test for collisions
6331
- while (true)
6702
+
6703
+ const localPos = new Vector2;
6704
+ const collisionTest = (pos)=>
6332
6705
  {
6333
6706
  // check for tile collision
6334
- const tileData = this.getCollisionData(pos);
6335
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
6336
- {
6337
- pos.x += .5; pos.y += .5;
6338
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
6339
- debugRaycast && debugPoint(pos, '#ff0');
6340
- return pos;
6341
- }
6342
-
6343
- // check if past the end
6344
- if (xi >= totalLength && yi >= totalLength)
6345
- break;
6346
-
6347
- // get coordinates of next tile to check
6348
- if (xi > yi)
6349
- pos.y += signDeltaY, yi += unitY;
6350
- else
6351
- pos.x += signDeltaX, xi += unitX;
6707
+ localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
6708
+ const tileData = this.getCollisionData(localPos);
6709
+ return tileData && (!object || object.collideWithTile(tileData, pos));
6352
6710
  }
6353
-
6354
6711
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
6712
+ const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
6713
+ if (hitPos)
6714
+ {
6715
+ const tilePos = hitPos.floor().add(vec2(.5));
6716
+ debugRaycast && debugRect(tilePos, vec2(1), '#f008');
6717
+ debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
6718
+ debugRaycast && debugPoint(hitPos, '#0f0');
6719
+ debugRaycast && normal &&
6720
+ debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6721
+ return hitPos;
6722
+ }
6355
6723
  }
6356
6724
  }
6357
6725
  /**
@@ -6506,19 +6874,34 @@ class ParticleEmitter extends EngineObject
6506
6874
  this.particleCreateCallback = undefined;
6507
6875
  /** @property {number} - Track particle emit time */
6508
6876
  this.emitTimeBuffer = 0;
6877
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
6878
+ this.velocityInheritance = 0;
6879
+
6880
+ // track previous position and angle
6881
+ this.previousAngle = this.angle;
6882
+ this.previousPos = this.pos.copy();
6509
6883
  }
6510
6884
 
6511
6885
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6512
6886
  update()
6513
6887
  {
6514
- // only do default update to apply parent transforms
6515
- this.parent && super.update();
6888
+ if (this.velocityInheritance)
6889
+ {
6890
+ // pass emitter velocity to particles
6891
+ const p = this.velocityInheritance;
6892
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
6893
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
6894
+ this.angleVelocity = p * (this.angle - this.previousAngle);
6895
+ this.previousAngle = this.angle;
6896
+ this.previousPos.x = this.pos.x;
6897
+ this.previousPos.y = this.pos.y;
6898
+ }
6516
6899
 
6517
6900
  // update emitter
6518
6901
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6519
6902
  {
6520
6903
  // emit particles
6521
- if (this.emitRate * particleEmitRateScale)
6904
+ if (this.emitRate && particleEmitRateScale)
6522
6905
  {
6523
6906
  const rate = 1/this.emitRate/particleEmitRateScale;
6524
6907
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -6571,7 +6954,7 @@ class ParticleEmitter extends EngineObject
6571
6954
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
6572
6955
  particle.velocity = vec2().setAngle(velocityAngle, speed);
6573
6956
  particle.angleVelocity = angleSpeed;
6574
- if (!this.localSpace)
6957
+ if (!this.localSpace && this.velocityInheritance > 0)
6575
6958
  {
6576
6959
  // apply emitter velocity to particle
6577
6960
  particle.velocity.x += this.velocity.x;
@@ -6656,8 +7039,6 @@ class Particle extends EngineObject
6656
7039
  /** Update the object physics, called automatically by engine once each frame */
6657
7040
  update()
6658
7041
  {
6659
- super.update();
6660
-
6661
7042
  if (this.collideTiles || this.collideSolidObjects)
6662
7043
  {
6663
7044
  // only apply max circular speed if particle can collide
@@ -6669,6 +7050,16 @@ class Particle extends EngineObject
6669
7050
  this.velocity.y *= s;
6670
7051
  }
6671
7052
  }
7053
+
7054
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
7055
+ {
7056
+ // destroy particle when its time runs out
7057
+ const c = this.colorEnd;
7058
+ this.color.set(c.r, c.g, c.b, c.a);
7059
+ this.size.set(this.sizeEnd, this.sizeEnd);
7060
+ this.destroyCallback && this.destroyCallback(this);
7061
+ this.destroyed = 1;
7062
+ }
6672
7063
  }
6673
7064
 
6674
7065
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -6677,7 +7068,7 @@ class Particle extends EngineObject
6677
7068
  // lerp color and size
6678
7069
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6679
7070
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6680
- this.size.x = this.size.y = radius;
7071
+ const size = vec2(radius);
6681
7072
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6682
7073
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6683
7074
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -6697,7 +7088,7 @@ class Particle extends EngineObject
6697
7088
  {
6698
7089
  // in local space of emitter
6699
7090
  const a = this.localSpaceEmitter.angle;
6700
- const c = Math.cos(a), s = Math.sin(a);
7091
+ const c = cos(a), s = sin(a);
6701
7092
  pos = this.localSpaceEmitter.pos.add(
6702
7093
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6703
7094
  angle += this.localSpaceEmitter.angle;
@@ -6713,22 +7104,15 @@ class Particle extends EngineObject
6713
7104
  {
6714
7105
  // stretch in direction of motion
6715
7106
  const trailLength = speed * this.trailScale;
6716
- this.size.y = max(this.size.x, trailLength);
6717
- angle = Math.atan2(direction.x, direction.y);
6718
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
7107
+ size.y = max(size.x, trailLength);
7108
+ angle = atan2(direction.x, direction.y);
7109
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6719
7110
  }
6720
7111
  }
6721
7112
  else
6722
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
7113
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6723
7114
  this.additive && setBlendMode();
6724
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6725
-
6726
- if (p1 === 1)
6727
- {
6728
- // destroy particle when its time runs out
6729
- this.destroyCallback && this.destroyCallback(this);
6730
- this.destroyed = 1;
6731
- }
7115
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
6732
7116
  }
6733
7117
  }
6734
7118
  /**
@@ -6898,11 +7282,11 @@ class Medal
6898
7282
  const descriptionSize = height*.3;
6899
7283
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
6900
7284
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
6901
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
7285
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6902
7286
 
6903
7287
  // draw the description
6904
7288
  pos.y = y + height - gap.y*2 - descriptionSize/2;
6905
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
7289
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6906
7290
  context.restore();
6907
7291
  }
6908
7292
 
@@ -6951,7 +7335,7 @@ let glContext;
6951
7335
  let glAntialias = true;
6952
7336
 
6953
7337
  // WebGL internal variables not exposed to documentation
6954
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
7338
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
6955
7339
 
6956
7340
  // WebGL internal constants
6957
7341
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6970,7 +7354,11 @@ function glInit()
6970
7354
  // keep set of texture infos so they can be restored if context is lost
6971
7355
  glTextureInfos = new Set;
6972
7356
 
6973
- if (!glEnable || headlessMode) return;
7357
+ if (!glEnable || headlessMode)
7358
+ {
7359
+ glCanBeEnabled = false;
7360
+ return;
7361
+ }
6974
7362
 
6975
7363
  // create the canvas and textures
6976
7364
  glCanvas = document.createElement('canvas');
@@ -6981,10 +7369,11 @@ function glInit()
6981
7369
  console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6982
7370
  glCanvas = glContext = undefined;
6983
7371
  glEnable = false;
7372
+ glCanBeEnabled = false;
6984
7373
  return;
6985
7374
  }
6986
7375
 
6987
- // create the WebGL canvas
7376
+ // attach the WebGL canvas
6988
7377
  const rootElement = mainCanvas.parentElement;
6989
7378
  rootElement.appendChild(glCanvas);
6990
7379
 
@@ -7007,7 +7396,7 @@ function glInit()
7007
7396
  });
7008
7397
  glCanvas.addEventListener('webglcontextrestored', ()=>
7009
7398
  {
7010
- glEnable = true; // disable WebGL rendering
7399
+ glEnable = true; // re-enable WebGL rendering
7011
7400
  glCanvas.style.display = ''; // show the gl canvas
7012
7401
  LOG('WebGL context restored, reinitializing...');
7013
7402
 
@@ -7159,8 +7548,8 @@ function glPreRender()
7159
7548
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
7160
7549
  const rotatedCam = cameraPos.rotate(-cameraAngle);
7161
7550
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
7162
- const ca = Math.cos(cameraAngle);
7163
- const sa = Math.sin(cameraAngle);
7551
+ const ca = cos(cameraAngle);
7552
+ const sa = sin(cameraAngle);
7164
7553
  const transform = [
7165
7554
  s.x * ca, s.y * sa, 0, 0,
7166
7555
  -s.x * sa, s.y * ca, 0, 0,
@@ -7465,8 +7854,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
7465
7854
  // transform the point
7466
7855
  const px = p.x*sx;
7467
7856
  const py = p.y*sy;
7468
- const sa = Math.sin(-angle);
7469
- const ca = Math.cos(-angle);
7857
+ const sa = sin(-angle);
7858
+ const ca = cos(-angle);
7470
7859
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
7471
7860
  }
7472
7861
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
@@ -8383,12 +8772,12 @@ class UISystemPlugin
8383
8772
  /** Draw a rectangle to the UI context
8384
8773
  * @param {Vector2} pos
8385
8774
  * @param {Vector2} size
8386
- * @param {Color} [color=uiSystem.defaultColor]
8387
- * @param {number} [lineWidth=uiSystem.defaultLineWidth]
8388
- * @param {Color} [lineColor=uiSystem.defaultLineColor]
8389
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
8390
- * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
8391
- 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)
8392
8781
  {
8393
8782
  ASSERT(isVector2(pos), 'pos must be a vec2');
8394
8783
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -8467,10 +8856,14 @@ class UISystemPlugin
8467
8856
  * @param {string} [align]
8468
8857
  * @param {string} [font=uiSystem.defaultFont]
8469
8858
  * @param {string} [fontStyle]
8470
- * @param {boolean} [applyMaxWidth=true] */
8471
- 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)
8472
8863
  {
8473
- 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);
8474
8867
  }
8475
8868
 
8476
8869
  /**
@@ -8497,6 +8890,25 @@ class UISystemPlugin
8497
8890
  setCallback(onDragLeave, 'dragleave');
8498
8891
  setCallback(onDragOver, 'dragover');
8499
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
+ }
8500
8912
  }
8501
8913
 
8502
8914
  ///////////////////////////////////////////////////////////////////////////////
@@ -8573,6 +8985,9 @@ class UIObject
8573
8985
  /** @property {boolean} - True if this can be a hover object */
8574
8986
  this.canBeHover = true;
8575
8987
  uiSystem.uiObjects.push(this);
8988
+
8989
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
8990
+ this.textShadow = undefined;
8576
8991
  }
8577
8992
 
8578
8993
  /** Add a child UIObject to this object
@@ -8600,24 +9015,20 @@ class UIObject
8600
9015
  */
8601
9016
  isMouseOverlapping()
8602
9017
  {
9018
+ if (!mouseInWindow) return false;
9019
+
8603
9020
  const size = !isTouchDevice ? this.size :
8604
9021
  this.size.add(vec2(this.extraTouchSize || 0));
8605
- if (!uiSystem.nativeHeight)
8606
- return isOverlapping(this.pos, size, mousePosScreen);
8607
-
8608
- const s = mainCanvasSize.y / uiSystem.nativeHeight;
8609
- const sInv = 1/s;
8610
- let pos = mousePosScreen.copy();
8611
- pos.x += s*mainCanvasSize.x/2;
8612
- pos.x *= sInv;
8613
- pos.y *= sInv;
8614
- pos.x -= sInv*mainCanvasSize.x/2;
9022
+ const pos = uiSystem.screenToNative(mousePosScreen);
8615
9023
  return isOverlapping(this.pos, size, pos);
8616
9024
  }
8617
9025
 
8618
9026
  /** Update the object, called automatically by plugin once each frame */
8619
9027
  update()
8620
9028
  {
9029
+ // call the custom update callback
9030
+ this.onUpdate();
9031
+
8621
9032
  const wasHover = uiSystem.lastHoverObject === this;
8622
9033
  const isActive = this.isActiveObject();
8623
9034
  const mouseDown = mouseIsDown(0);
@@ -8674,7 +9085,7 @@ class UIObject
8674
9085
 
8675
9086
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8676
9087
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8677
- 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);
8678
9089
  }
8679
9090
 
8680
9091
  /** Special update when object is not visible */
@@ -8701,6 +9112,9 @@ class UIObject
8701
9112
  /** @return {boolean} - Is the mouse held onto this element */
8702
9113
  isActiveObject() { return uiSystem.activeObject === this; }
8703
9114
 
9115
+ /** Called each frame when object updates */
9116
+ onUpdate() {}
9117
+
8704
9118
  /** Called when the mouse enters the object */
8705
9119
  onEnter() {}
8706
9120
 
@@ -8755,8 +9169,9 @@ class UIText extends UIObject
8755
9169
  }
8756
9170
  render()
8757
9171
  {
9172
+ // only render the text
8758
9173
  const textSize = this.getTextSize();
8759
- 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);
8760
9175
  }
8761
9176
  }
8762
9177
 
@@ -8832,7 +9247,7 @@ class UIButton extends UIObject
8832
9247
  // draw the text scaled to fit
8833
9248
  const textSize = this.getTextSize();
8834
9249
  uiSystem.drawText(this.text, this.pos, textSize,
8835
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
9250
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8836
9251
  }
8837
9252
  }
8838
9253
 
@@ -8886,7 +9301,7 @@ class UICheckbox extends UIObject
8886
9301
  const textSize = this.getTextSize();
8887
9302
  const pos = this.pos.add(vec2(this.size.x,0));
8888
9303
  uiSystem.drawText(this.text, pos, textSize,
8889
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
9304
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
8890
9305
  }
8891
9306
  }
8892
9307
 
@@ -8941,9 +9356,11 @@ class UIScrollbar extends UIObject
8941
9356
  const p1 = centerPos - handleWidth/2;
8942
9357
  const p2 = centerPos + handleWidth/2;
8943
9358
  const oldValue = this.value;
9359
+
9360
+ const p = uiSystem.screenToNative(mousePosScreen);
8944
9361
  this.value = isHorizontal ?
8945
- percent(mousePosScreen.x, p1, p2) :
8946
- percent(mousePosScreen.y, p2, p1);
9362
+ percent(p.x, p1, p2) :
9363
+ percent(p.y, p2, p1);
8947
9364
  this.value === oldValue || this.onChange();
8948
9365
  }
8949
9366
  }
@@ -8965,20 +9382,20 @@ class UIScrollbar extends UIObject
8965
9382
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8966
9383
  vec2(this.pos.x, lerp(p2, p1, this.value))
8967
9384
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8968
- 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);
8969
9386
 
8970
9387
  // draw the text scaled to fit on the scrollbar
8971
9388
  const textSize = this.getTextSize();
8972
9389
  uiSystem.drawText(this.text, this.pos, textSize,
8973
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
9390
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8974
9391
  }
8975
9392
  }
8976
9393
  /**
8977
9394
  * LittleJS Box2D Physics Plugin
8978
9395
  * - Box2dObject extends EngineObject with Box2D physics
8979
- * - Call box2dInit() before engineInit() to enable
9396
+ * - Call box2dInit() to enable
8980
9397
  * - You will also need to include box2d.wasm.js
8981
- * - Uses a super fast web assembly port of Box2D
9398
+ * - Uses a super fast web assembly port of Box2D v2.3.1
8982
9399
  * - More info: https://github.com/kripken/box2d.js
8983
9400
  * - Functions to create polygon, circle, and edge shapes
8984
9401
  * - Contact begin and end callbacks
@@ -9008,9 +9425,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
9008
9425
  ///////////////////////////////////////////////////////////////////////////////
9009
9426
  /**
9010
9427
  * Box2D Object - extend with your own custom physics objects
9011
- * - A LittleJS object with Box2D physics
9012
- * - Each object has a Box2D body which can have multiple fixtures and joints
9428
+ * - A LittleJS object with Box2D physics, dynamic by default
9013
9429
  * - Provides interface for Box2D body and fixture functions
9430
+ * - Each object can have multiple fixtures and joints
9014
9431
  * @extends EngineObject
9015
9432
  * @memberof Box2D
9016
9433
  */
@@ -9024,7 +9441,7 @@ class Box2dObject extends EngineObject
9024
9441
  * @param {Color} [color]
9025
9442
  * @param {number} [bodyType]
9026
9443
  * @param {number} [renderOrder] */
9027
- 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)
9028
9445
  {
9029
9446
  super(pos, size, tileInfo, angle, color, renderOrder);
9030
9447
 
@@ -9036,24 +9453,27 @@ class Box2dObject extends EngineObject
9036
9453
  this.body = box2d.world.CreateBody(bodyDef);
9037
9454
  this.body.object = this;
9038
9455
  this.lineColor = BLACK;
9456
+ box2d.objects.push(this);
9457
+
9458
+ // edge lists and loops for drawing
9459
+ this.edgeLists = [];
9460
+ this.edgeLoops = [];
9039
9461
  }
9040
9462
 
9041
9463
  /** Destroy this object and its physics body */
9042
9464
  destroy()
9043
9465
  {
9466
+ if (this.destroyed)
9467
+ return;
9468
+
9044
9469
  // destroy physics body, fixtures, and joints
9045
- this.body && box2d.world.DestroyBody(this.body);
9046
- this.body = 0;
9470
+ ASSERT(this.body, 'Box2dObject has no body to destroy');
9471
+ box2d.world.DestroyBody(this.body);
9047
9472
  super.destroy();
9048
9473
  }
9049
9474
 
9050
- /** Copy box2d update sim data */
9051
- update()
9052
- {
9053
- // use box2d physics update instead of normal engine update
9054
- this.pos = box2d.vec2From(this.body.GetPosition());
9055
- this.angle = -this.body.GetAngle();
9056
- }
9475
+ /** Box2d objects updated with Box2d world step */
9476
+ updatePhysics() {}
9057
9477
 
9058
9478
  /** Render the object, uses box2d drawing if no tile info exists */
9059
9479
  render()
@@ -9079,10 +9499,23 @@ class Box2dObject extends EngineObject
9079
9499
  * @param {Color} [lineColor]
9080
9500
  * @param {number} [lineWidth]
9081
9501
  * @param {CanvasRenderingContext2D} [context] */
9082
- drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
9502
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
9083
9503
  {
9084
- this.getFixtureList().forEach(fixture=>
9085
- 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));
9086
9519
  }
9087
9520
 
9088
9521
  ///////////////////////////////////////////////////////////////////////////////
@@ -9107,6 +9540,10 @@ class Box2dObject extends EngineObject
9107
9540
  * @param {boolean} [isSensor] */
9108
9541
  addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
9109
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
+
9110
9547
  const fd = new box2d.instance.b2FixtureDef();
9111
9548
  fd.set_shape(shape);
9112
9549
  fd.set_density(density);
@@ -9126,6 +9563,11 @@ class Box2dObject extends EngineObject
9126
9563
  * @param {boolean} [isSensor] */
9127
9564
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
9128
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
+
9129
9571
  const shape = new box2d.instance.b2PolygonShape();
9130
9572
  shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
9131
9573
  return this.addShape(shape, density, friction, restitution, isSensor);
@@ -9139,6 +9581,8 @@ class Box2dObject extends EngineObject
9139
9581
  * @param {boolean} [isSensor] */
9140
9582
  addPoly(points, density, friction, restitution, isSensor)
9141
9583
  {
9584
+ ASSERT(isArray(points), 'points must be an array');
9585
+
9142
9586
  function box2dCreatePolygonShape(points)
9143
9587
  {
9144
9588
  function box2dCreatePointList(points)
@@ -9174,6 +9618,9 @@ class Box2dObject extends EngineObject
9174
9618
  * @param {boolean} [isSensor] */
9175
9619
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
9176
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
+
9177
9624
  const points = [];
9178
9625
  const radius = diameter/2;
9179
9626
  for (let i=sides; i--;)
@@ -9189,6 +9636,8 @@ class Box2dObject extends EngineObject
9189
9636
  * @param {boolean} [isSensor] */
9190
9637
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
9191
9638
  {
9639
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9640
+
9192
9641
  const sides = randInt(3, 9);
9193
9642
  const points = [];
9194
9643
  const radius = diameter/2;
@@ -9206,6 +9655,9 @@ class Box2dObject extends EngineObject
9206
9655
  * @param {boolean} [isSensor] */
9207
9656
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
9208
9657
  {
9658
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9659
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9660
+
9209
9661
  const shape = new box2d.instance.b2CircleShape();
9210
9662
  shape.set_m_p(box2d.vec2dTo(offset));
9211
9663
  shape.set_m_radius(diameter/2);
@@ -9221,53 +9673,63 @@ class Box2dObject extends EngineObject
9221
9673
  * @param {boolean} [isSensor] */
9222
9674
  addEdge(point1, point2, density, friction, restitution, isSensor)
9223
9675
  {
9676
+ ASSERT(isVector2(point1), 'point1 must be a Vector2');
9677
+ ASSERT(isVector2(point2), 'point2 must be a Vector2');
9678
+
9224
9679
  const shape = new box2d.instance.b2EdgeShape();
9225
9680
  shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
9226
9681
  return this.addShape(shape, density, friction, restitution, isSensor);
9227
9682
  }
9228
9683
 
9229
- /** Add an edge loop to the body, an edge loop connects the end points
9684
+ /** Add an edge list to the body
9230
9685
  * @param {Array<Vector2>} points
9231
9686
  * @param {number} [density]
9232
9687
  * @param {number} [friction]
9233
9688
  * @param {number} [restitution]
9234
9689
  * @param {boolean} [isSensor] */
9235
- addEdgeLoop(points, density, friction, restitution, isSensor)
9690
+ addEdgeList(points, density, friction, restitution, isSensor)
9236
9691
  {
9237
- const fixtures = [];
9238
- const getPoint = i=> points[mod(i,points.length)];
9239
- 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)
9240
9695
  {
9241
9696
  const shape = new box2d.instance.b2EdgeShape();
9242
- shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9243
- shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9244
- shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9245
- 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]));
9246
9701
  const f = this.addShape(shape, density, friction, restitution, isSensor);
9247
9702
  fixtures.push(f);
9703
+ edgePoints.push(points[i].copy());
9248
9704
  }
9705
+ edgePoints.push(points[points.length-1].copy());
9706
+ this.edgeLists.push(edgePoints);
9249
9707
  return fixtures;
9250
9708
  }
9251
9709
 
9252
- /** Add an edge list to the body
9710
+ /** Add an edge loop to the body, an edge loop connects the end points
9253
9711
  * @param {Array<Vector2>} points
9254
9712
  * @param {number} [density]
9255
9713
  * @param {number} [friction]
9256
9714
  * @param {number} [restitution]
9257
9715
  * @param {boolean} [isSensor] */
9258
- addEdgeList(points, density, friction, restitution, isSensor)
9716
+ addEdgeLoop(points, density, friction, restitution, isSensor)
9259
9717
  {
9260
- const fixtures = [];
9261
- 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)
9262
9722
  {
9263
9723
  const shape = new box2d.instance.b2EdgeShape();
9264
- points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9265
- points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9266
- points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9267
- 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)));
9268
9728
  const f = this.addShape(shape, density, friction, restitution, isSensor);
9269
9729
  fixtures.push(f);
9730
+ i < points.length && edgePoints.push(points[i].copy());
9270
9731
  }
9732
+ this.edgeLoops.push(edgePoints);
9271
9733
  return fixtures;
9272
9734
  }
9273
9735
 
@@ -9302,6 +9764,10 @@ class Box2dObject extends EngineObject
9302
9764
  * @return {number} */
9303
9765
  getBodyType() { return this.body.GetType(); }
9304
9766
 
9767
+ /** Get the speed of this object
9768
+ * @return {number} */
9769
+ getSpeed() { return this.getLinearVelocity().length(); }
9770
+
9305
9771
  ///////////////////////////////////////////////////////////////////////////////
9306
9772
  // physics set functions
9307
9773
 
@@ -9317,33 +9783,40 @@ class Box2dObject extends EngineObject
9317
9783
 
9318
9784
  /** Sets the position
9319
9785
  * @param {Vector2} pos */
9320
- setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
9786
+ setPosition(pos)
9787
+ { this.setTransform(pos, this.body.GetAngle()); }
9321
9788
 
9322
9789
  /** Sets the angle
9323
9790
  * @param {number} angle */
9324
- setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9791
+ setAngle(angle)
9792
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9325
9793
 
9326
9794
  /** Sets the linear velocity
9327
9795
  * @param {Vector2} velocity */
9328
- setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9796
+ setLinearVelocity(velocity)
9797
+ { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9329
9798
 
9330
9799
  /** Sets the angular velocity
9331
9800
  * @param {number} angularVelocity */
9332
- setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
9801
+ setAngularVelocity(angularVelocity)
9802
+ { this.body.SetAngularVelocity(angularVelocity); }
9333
9803
 
9334
9804
  /** Sets the linear damping
9335
9805
  * @param {number} damping */
9336
- setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
9806
+ setLinearDamping(damping)
9807
+ { this.body.SetLinearDamping(damping); }
9337
9808
 
9338
9809
  /** Sets the angular damping
9339
9810
  * @param {number} damping */
9340
- setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
9811
+ setAngularDamping(damping)
9812
+ { this.body.SetAngularDamping(damping); }
9341
9813
 
9342
9814
  /** Sets the gravity scale
9343
9815
  * @param {number} [scale] */
9344
- setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
9816
+ setGravityScale(scale=1)
9817
+ { this.body.SetGravityScale(this.gravityScale = scale); }
9345
9818
 
9346
- /** Should this body be treated like a bullet for continuous collision detection?
9819
+ /** Should be like a bullet for continuous collision detection?
9347
9820
  * @param {boolean} [isBullet] */
9348
9821
  setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
9349
9822
 
@@ -9357,11 +9830,13 @@ class Box2dObject extends EngineObject
9357
9830
 
9358
9831
  /** Set whether the body is allowed to sleep
9359
9832
  * @param {boolean} [isAllowed] */
9360
- setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
9833
+ setSleepingAllowed(isAllowed=true)
9834
+ { this.body.SetSleepingAllowed(isAllowed); }
9361
9835
 
9362
9836
  /** Set whether the body can rotate
9363
9837
  * @param {boolean} [isFixed] */
9364
- setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
9838
+ setFixedRotation(isFixed=true)
9839
+ { this.body.SetFixedRotation(isFixed); }
9365
9840
 
9366
9841
  /** Set the center of mass of the body
9367
9842
  * @param {Vector2} center */
@@ -9373,10 +9848,11 @@ class Box2dObject extends EngineObject
9373
9848
 
9374
9849
  /** Set the moment of inertia of the body
9375
9850
  * @param {number} momentOfInertia */
9376
- setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
9851
+ setMomentOfInertia(momentOfInertia)
9852
+ { this.setMassData(undefined, undefined, momentOfInertia) }
9377
9853
 
9378
9854
  /** Reset the mass, center of mass, and moment */
9379
- resetMassData() { this.body.ResetMassData(); }
9855
+ resetMassData() { this.body.ResetMassData(); }
9380
9856
 
9381
9857
  /** Set the mass data of the body
9382
9858
  * @param {Vector2} [localCenter]
@@ -10018,6 +10494,50 @@ class Box2dPrismaticJoint extends Box2dJoint
10018
10494
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
10019
10495
  }
10020
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
+
10021
10541
  ///////////////////////////////////////////////////////////////////////////////
10022
10542
  /**
10023
10543
  * Box2D Wheel Joint
@@ -10380,6 +10900,7 @@ class Box2dPlugin
10380
10900
  box2d = this;
10381
10901
  this.instance = instance;
10382
10902
  this.world = new box2d.instance.b2World();
10903
+ this.objects = [];
10383
10904
 
10384
10905
  /** @property {number} - Velocity iterations per update*/
10385
10906
  this.velocityIterations = 8;
@@ -10622,9 +11143,10 @@ class Box2dPlugin
10622
11143
 
10623
11144
  /** converts a box2d vec2 pointer to a Vector2
10624
11145
  * @param {Object} v */
10625
- vec2FromPointer(v)
11146
+ vec2FromPointer(vp)
10626
11147
  {
10627
- 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);
10628
11150
  }
10629
11151
 
10630
11152
  /** converts a Vector2 to a box2 vec2
@@ -10698,8 +11220,23 @@ async function box2dInit()
10698
11220
  // add the box2d plugin to the engine
10699
11221
  function box2dUpdate()
10700
11222
  {
10701
- if (!paused)
10702
- 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
+ }
10703
11240
  }
10704
11241
  function box2dRender()
10705
11242
  {
@@ -10988,6 +11525,7 @@ export
10988
11525
  gamepadDirectionEmulateStick,
10989
11526
  inputWASDEmulateDirection,
10990
11527
  touchGamepadEnable,
11528
+ touchGamepadCenterButton,
10991
11529
  touchGamepadAnalog,
10992
11530
  touchGamepadSize,
10993
11531
  touchGamepadAlpha,
@@ -11031,6 +11569,7 @@ export
11031
11569
  setGamepadDirectionEmulateStick,
11032
11570
  setInputWASDEmulateDirection,
11033
11571
  setTouchGamepadEnable,
11572
+ setTouchGamepadCenterButton,
11034
11573
  setTouchGamepadAnalog,
11035
11574
  setTouchGamepadSize,
11036
11575
  setTouchGamepadAlpha,
@@ -11049,9 +11588,18 @@ export
11049
11588
  // Utilities
11050
11589
  PI,
11051
11590
  abs,
11591
+ floor,
11592
+ ceil,
11593
+ round,
11052
11594
  min,
11053
11595
  max,
11054
11596
  sign,
11597
+ hypot,
11598
+ log2,
11599
+ sin,
11600
+ cos,
11601
+ tan,
11602
+ atan2,
11055
11603
  mod,
11056
11604
  clamp,
11057
11605
  percent,
@@ -11071,6 +11619,7 @@ export
11071
11619
  // Random
11072
11620
  rand,
11073
11621
  randInt,
11622
+ randBool,
11074
11623
  randSign,
11075
11624
  randInCircle,
11076
11625
  randVec2,
@@ -11088,6 +11637,7 @@ export
11088
11637
  isVector2,
11089
11638
  isNumber,
11090
11639
  isString,
11640
+ isArray,
11091
11641
 
11092
11642
  // Default Colors
11093
11643
  WHITE,
@@ -11119,6 +11669,8 @@ export
11119
11669
  drawCount,
11120
11670
  screenToWorld,
11121
11671
  worldToScreen,
11672
+ screenToWorldDelta,
11673
+ worldToScreenDelta,
11122
11674
  drawTile,
11123
11675
  drawRect,
11124
11676
  drawRectGradient,
@@ -11186,6 +11738,7 @@ export
11186
11738
  mouseDelta,
11187
11739
  mouseDeltaScreen,
11188
11740
  mouseWheel,
11741
+ mouseInWindow,
11189
11742
  isUsingGamepad,
11190
11743
  inputPreventDefault,
11191
11744
  setInputPreventDefault,
@@ -11274,6 +11827,8 @@ export
11274
11827
  box2dInit,
11275
11828
  Box2dPlugin,
11276
11829
  Box2dObject,
11830
+ Box2dStaticObject,
11831
+ Box2dKiematicObject,
11277
11832
  Box2dRaycastResult,
11278
11833
  Box2dJoint,
11279
11834
  Box2dTargetJoint,