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();
@@ -755,7 +758,8 @@ function debugShowErrors(){}
755
758
  function debugVideoCaptureIsActive(){ return false; }
756
759
  function debugVideoCaptureStart (){}
757
760
  function debugVideoCaptureStop (){}
758
- function debugVideoCaptureUpdate(){}
761
+ function debugVideoCaptureUpdate(){}
762
+ function debugProtectConstant(o){ return o; }
759
763
  /**
760
764
  * LittleJS Utility Classes and Functions
761
765
  * - General purpose math library
@@ -766,7 +770,7 @@ function debugVideoCaptureUpdate(){}
766
770
  * @namespace Utilities
767
771
  */
768
772
 
769
- /** A shortcut to get Math.PI
773
+ /** The value of PI
770
774
  * @type {number}
771
775
  * @default Math.PI
772
776
  * @memberof Utilities */
@@ -776,25 +780,80 @@ const PI = Math.PI;
776
780
  * @param {number} value
777
781
  * @return {number}
778
782
  * @memberof Utilities */
779
- function abs(value) { return Math.abs(value); }
783
+ const abs = Math.abs;
784
+
785
+ /** Returns floored value of value passed in
786
+ * @param {number} value
787
+ * @return {number}
788
+ * @memberof Utilities */
789
+ const floor = Math.floor;
790
+
791
+ /** Returns ceiled value of value passed in
792
+ * @param {number} value
793
+ * @return {number}
794
+ * @memberof Utilities */
795
+ const ceil = Math.ceil;
796
+
797
+ /** Returns rounded value passed in
798
+ * @param {number} value
799
+ * @return {number}
800
+ * @memberof Utilities */
801
+ const round = Math.round;
780
802
 
781
803
  /** Returns lowest value passed in
782
804
  * @param {...number} values
783
805
  * @return {number}
784
806
  * @memberof Utilities */
785
- function min(...values) { return Math.min(...values); }
807
+ const min = Math.min;
786
808
 
787
809
  /** Returns highest value passed in
788
810
  * @param {...number} values
789
811
  * @return {number}
790
812
  * @memberof Utilities */
791
- function max(...values) { return Math.max(...values); }
813
+ const max = Math.max;
792
814
 
793
815
  /** Returns the sign of value passed in
794
816
  * @param {number} value
795
817
  * @return {number}
796
818
  * @memberof Utilities */
797
- function sign(value) { return Math.sign(value); }
819
+ const sign = Math.sign;
820
+
821
+ /** Returns hypotenuse of values passed in
822
+ * @param {...number} values
823
+ * @return {number}
824
+ * @memberof Utilities */
825
+ const hypot = Math.hypot;
826
+
827
+ /** Returns log2 of value passed in
828
+ * @param {number} value
829
+ * @return {number}
830
+ * @memberof Utilities */
831
+ const log2 = Math.log2;
832
+
833
+ /** Returns sin of value passed in
834
+ * @param {number} value
835
+ * @return {number}
836
+ * @memberof Utilities */
837
+ const sin = Math.sin;
838
+
839
+ /** Returns cos of value passed in
840
+ * @param {number} value
841
+ * @return {number}
842
+ * @memberof Utilities */
843
+ const cos = Math.cos;
844
+
845
+ /** Returns tan of value passed in
846
+ * @param {number} value
847
+ * @return {number}
848
+ * @memberof Utilities */
849
+ const tan = Math.tan;
850
+
851
+ /** Returns atan2 of values passed in
852
+ * @param {number} y
853
+ * @param {number} x
854
+ * @return {number}
855
+ * @memberof Utilities */
856
+ const atan2 = Math.atan2;
798
857
 
799
858
  /** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
800
859
  * @param {number} dividend
@@ -899,7 +958,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
899
958
  * @param {number} value
900
959
  * @return {number}
901
960
  * @memberof Utilities */
902
- function nearestPowerOfTwo(value) { return 2**Math.ceil(Math.log2(value)); }
961
+ function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
903
962
 
904
963
  /** Returns true if two axis aligned bounding boxes are overlapping
905
964
  * this can be used for simple collision detection between objects
@@ -967,7 +1026,7 @@ function isIntersecting(start, end, pos, size)
967
1026
  * @return {number} - Value waving between 0 and amplitude
968
1027
  * @memberof Utilities */
969
1028
  function wave(frequency=1, amplitude=1, t=time, offset=0)
970
- { return amplitude/2 * (1 - Math.cos(offset + t*frequency*2*PI)); }
1029
+ { return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
971
1030
 
972
1031
  /** Formats seconds to mm:ss style for display purposes
973
1032
  * @param {number} t - time in seconds
@@ -1006,6 +1065,103 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1006
1065
  * @memberof Utilities */
1007
1066
  function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
1008
1067
 
1068
+ /**
1069
+ * Check if object is an array
1070
+ * @param {any} a
1071
+ * @return {boolean}
1072
+ * @memberof Utilities */
1073
+ function isArray(a) { return Array.isArray(a); }
1074
+
1075
+ /**
1076
+ * @callback LineTestFunction - Checks if a position is colliding
1077
+ * @param {Vector2} pos
1078
+ * @memberof Draw
1079
+ */
1080
+
1081
+ /**
1082
+ * Casts a ray and returns position of the first collision found, or undefined if none are found
1083
+ * @param {Vector2} posStart
1084
+ * @param {Vector2} posEnd
1085
+ * @param {LineTestFunction} testFunction - Check if colliding
1086
+ * @param {Vector2} [normal] - Optional vector to store the normal
1087
+ * @return {Vector2|undefined} - Position of the collision or undefined if none found
1088
+ * @memberof Utilities */
1089
+ function lineTest(posStart, posEnd, testFunction, normal)
1090
+ {
1091
+ ASSERT(isVector2(posStart), 'posStart must be a vec2');
1092
+ ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
1093
+ ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
1094
+ ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
1095
+
1096
+ // get ray direction and length
1097
+ const dx = posEnd.x - posStart.x;
1098
+ const dy = posEnd.y - posStart.y;
1099
+ const totalLength = hypot(dx, dy);
1100
+ if (!totalLength)
1101
+ return;
1102
+
1103
+ // current integer cell we are in
1104
+ const pos = posStart.floor();
1105
+
1106
+ // normalize ray direction
1107
+ const dirX = dx / totalLength;
1108
+ const dirY = dy / totalLength;
1109
+
1110
+ // step direction in grid
1111
+ const stepX = sign(dirX);
1112
+ const stepY = sign(dirY);
1113
+
1114
+ // distance along the ray to cross one full cell in X or Y
1115
+ const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
1116
+ const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
1117
+
1118
+ // distance along the ray from start to the first grid boundary
1119
+ const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
1120
+ const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
1121
+ const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
1122
+ const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
1123
+
1124
+ // use line drawing algorithm to test for collisions
1125
+ let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
1126
+ while (t < totalLength)
1127
+ {
1128
+ if (testFunction(pos))
1129
+ {
1130
+ // set hit point
1131
+ const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
1132
+
1133
+ // move inside of tile if on positive edge
1134
+ const e = 1e-9;
1135
+ if (wasX)
1136
+ {
1137
+ if (stepX < 0)
1138
+ hitPos.x -= e;
1139
+ }
1140
+ if (stepY < 0)
1141
+ hitPos.y -= e;
1142
+
1143
+ // set normal
1144
+ if (normal)
1145
+ wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
1146
+ return hitPos;
1147
+ }
1148
+
1149
+ // advance to the next grid boundary
1150
+ if (wasX = tX < tY)
1151
+ {
1152
+ pos.x += stepX;
1153
+ t = tX;
1154
+ tX += tDeltaX;
1155
+ }
1156
+ else
1157
+ {
1158
+ pos.y += stepY;
1159
+ t = tY;
1160
+ tY += tDeltaY;
1161
+ }
1162
+ }
1163
+ }
1164
+
1009
1165
  ///////////////////////////////////////////////////////////////////////////////
1010
1166
 
1011
1167
  /** Random global functions
@@ -1024,7 +1180,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
1024
1180
  * @param {number} [valueB]
1025
1181
  * @return {number}
1026
1182
  * @memberof Random */
1027
- function randInt(valueA, valueB=0) { return Math.floor(rand(valueA,valueB)); }
1183
+ function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
1028
1184
 
1029
1185
  /** Randomly returns true or false given the chance of true passed in
1030
1186
  * @param {number} [chance]
@@ -1103,7 +1259,7 @@ class RandomGenerator
1103
1259
  * @param {number} valueA
1104
1260
  * @param {number} [valueB]
1105
1261
  * @return {number} */
1106
- int(valueA, valueB=0) { return Math.floor(this.float(valueA, valueB)); }
1262
+ int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
1107
1263
 
1108
1264
  /** Randomly returns true or false given the chance of true passed in
1109
1265
  * @param {number} [chance]
@@ -1130,6 +1286,39 @@ class RandomGenerator
1130
1286
  * @return {Vector2} */
1131
1287
  vec2(valueA=1, valueB=0)
1132
1288
  { return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
1289
+
1290
+ /** Returns a random color between the two passed in colors, combine components if linear
1291
+ * @param {Color} [colorA=(1,1,1,1)]
1292
+ * @param {Color} [colorB=(0,0,0,1)]
1293
+ * @param {boolean} [linear]
1294
+ * @return {Color} */
1295
+ randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
1296
+ {
1297
+ return linear ? colorA.lerp(colorB, this.float()) :
1298
+ new Color(
1299
+ this.float(colorA.r,colorB.r),
1300
+ this.float(colorA.g,colorB.g),
1301
+ this.float(colorA.b,colorB.b),
1302
+ this.float(colorA.a,colorB.a));
1303
+ }
1304
+
1305
+ /** Returns a new color that has each component randomly adjusted
1306
+ * @param {Color} color
1307
+ * @param {number} [amount]
1308
+ * @param {number} [alphaAmount]
1309
+ * @return {Color} */
1310
+ mutateColor(color, amount=.05, alphaAmount=0)
1311
+ {
1312
+ ASSERT_NUMBER_VALID(amount);
1313
+ ASSERT_NUMBER_VALID(alphaAmount);
1314
+ return new Color
1315
+ (
1316
+ color.r + this.float(amount, -amount),
1317
+ color.g + this.float(amount, -amount),
1318
+ color.b + this.float(amount, -amount),
1319
+ color.a + this.float(alphaAmount, -alphaAmount)
1320
+ ).clamp();
1321
+ }
1133
1322
  }
1134
1323
 
1135
1324
  ///////////////////////////////////////////////////////////////////////////////
@@ -1281,7 +1470,7 @@ class Vector2
1281
1470
 
1282
1471
  /** Returns the clockwise angle of this vector, up is angle 0
1283
1472
  * @return {number} */
1284
- angle() { return Math.atan2(this.x, this.y); }
1473
+ angle() { return atan2(this.x, this.y); }
1285
1474
 
1286
1475
  /** Sets this vector with clockwise angle and length passed in
1287
1476
  * @param {number} [angle]
@@ -1291,8 +1480,8 @@ class Vector2
1291
1480
  {
1292
1481
  ASSERT_NUMBER_VALID(angle);
1293
1482
  ASSERT_NUMBER_VALID(length);
1294
- this.x = length*Math.sin(angle);
1295
- this.y = length*Math.cos(angle);
1483
+ this.x = length*sin(angle);
1484
+ this.y = length*cos(angle);
1296
1485
  return this;
1297
1486
  }
1298
1487
 
@@ -1302,7 +1491,7 @@ class Vector2
1302
1491
  rotate(angle)
1303
1492
  {
1304
1493
  ASSERT_NUMBER_VALID(angle);
1305
- const c = Math.cos(-angle), s = Math.sin(-angle);
1494
+ const c = cos(-angle), s = sin(-angle);
1306
1495
  return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
1307
1496
  }
1308
1497
 
@@ -1334,7 +1523,7 @@ class Vector2
1334
1523
 
1335
1524
  /** Returns a copy of this vector with each axis floored
1336
1525
  * @return {Vector2} */
1337
- floor() { return new Vector2(Math.floor(this.x), Math.floor(this.y)); }
1526
+ floor() { return new Vector2(floor(this.x), floor(this.y)); }
1338
1527
 
1339
1528
  /** Returns new vec2 with modded values
1340
1529
  * @param {number} [divisor]
@@ -1582,7 +1771,6 @@ class Color
1582
1771
  * @return {string} */
1583
1772
  toString(useAlpha = true)
1584
1773
  {
1585
- ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
1586
1774
  if (debug && !this.isValid())
1587
1775
  return `#000`;
1588
1776
  const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
@@ -1642,67 +1830,67 @@ class Color
1642
1830
  /** Color - White #ffffff
1643
1831
  * @type {Color}
1644
1832
  * @memberof Utilities */
1645
- const WHITE = protectEngineConstant(rgb());
1833
+ const WHITE = debugProtectConstant(rgb());
1646
1834
 
1647
1835
  /** Color - Clear White #757474ff with 0 alpha
1648
1836
  * @type {Color}
1649
1837
  * @memberof Utilities */
1650
- const CLEAR_WHITE = protectEngineConstant(rgb(1,1,1,0));
1838
+ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1651
1839
 
1652
1840
  /** Color - Black #000000
1653
1841
  * @type {Color}
1654
1842
  * @memberof Utilities */
1655
- const BLACK = protectEngineConstant(rgb(0,0,0));
1843
+ const BLACK = debugProtectConstant(rgb(0,0,0));
1656
1844
 
1657
1845
  /** Color - Clear Black #000000 with 0 alpha
1658
1846
  * @type {Color}
1659
1847
  * @memberof Utilities */
1660
- const CLEAR_BLACK = protectEngineConstant(rgb(0,0,0,0));
1848
+ const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
1661
1849
 
1662
1850
  /** Color - Gray #808080
1663
1851
  * @type {Color}
1664
1852
  * @memberof Utilities */
1665
- const GRAY = protectEngineConstant(rgb(.5,.5,.5));
1853
+ const GRAY = debugProtectConstant(rgb(.5,.5,.5));
1666
1854
 
1667
1855
  /** Color - Red #ff0000
1668
1856
  * @type {Color}
1669
1857
  * @memberof Utilities */
1670
- const RED = protectEngineConstant(rgb(1,0,0));
1858
+ const RED = debugProtectConstant(rgb(1,0,0));
1671
1859
 
1672
1860
  /** Color - Orange #ff8000
1673
1861
  * @type {Color}
1674
1862
  * @memberof Utilities */
1675
- const ORANGE = protectEngineConstant(rgb(1,.5,0));
1863
+ const ORANGE = debugProtectConstant(rgb(1,.5,0));
1676
1864
 
1677
1865
  /** Color - Yellow #ffff00
1678
1866
  * @type {Color}
1679
1867
  * @memberof Utilities */
1680
- const YELLOW = protectEngineConstant(rgb(1,1,0));
1868
+ const YELLOW = debugProtectConstant(rgb(1,1,0));
1681
1869
 
1682
1870
  /** Color - Green #00ff00
1683
1871
  * @type {Color}
1684
1872
  * @memberof Utilities */
1685
- const GREEN = protectEngineConstant(rgb(0,1,0));
1873
+ const GREEN = debugProtectConstant(rgb(0,1,0));
1686
1874
 
1687
1875
  /** Color - Cyan #00ffff
1688
1876
  * @type {Color}
1689
1877
  * @memberof Utilities */
1690
- const CYAN = protectEngineConstant(rgb(0,1,1));
1878
+ const CYAN = debugProtectConstant(rgb(0,1,1));
1691
1879
 
1692
1880
  /** Color - Blue #0000ff
1693
1881
  * @type {Color}
1694
1882
  * @memberof Utilities */
1695
- const BLUE = protectEngineConstant(rgb(0,0,1));
1883
+ const BLUE = debugProtectConstant(rgb(0,0,1));
1696
1884
 
1697
1885
  /** Color - Purple #8000ff
1698
1886
  * @type {Color}
1699
1887
  * @memberof Utilities */
1700
- const PURPLE = protectEngineConstant(rgb(.5,0,1));
1888
+ const PURPLE = debugProtectConstant(rgb(.5,0,1));
1701
1889
 
1702
1890
  /** Color - Magenta #ff00ff
1703
1891
  * @type {Color}
1704
1892
  * @memberof Utilities */
1705
- const MAGENTA = protectEngineConstant(rgb(1,0,1));
1893
+ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1706
1894
 
1707
1895
  ///////////////////////////////////////////////////////////////////////////////
1708
1896
 
@@ -1765,41 +1953,11 @@ class Timer
1765
1953
 
1766
1954
  /** Returns this timer expressed as a string
1767
1955
  * @return {string} */
1768
- toString() { return this.isSet() ? Math.abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1956
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1769
1957
 
1770
1958
  /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1771
1959
  * @return {number} */
1772
1960
  valueOf() { return this.get(); }
1773
- }
1774
-
1775
- ///////////////////////////////////////////////////////////////////////////////
1776
- // Helper functions used by the engine
1777
-
1778
- // make color constants immutable with debug assertions
1779
- function protectEngineConstant(obj)
1780
- {
1781
- if (debug)
1782
- {
1783
- // get properties and store original values
1784
- const props = Object.keys(obj), values = {};
1785
- props.forEach(prop => values[prop] = obj[prop]);
1786
-
1787
- // replace with getters/setters that assert
1788
- props.forEach(prop =>
1789
- {
1790
- Object.defineProperty(obj, prop, {
1791
- get: () => values[prop],
1792
- set: (value) =>
1793
- {
1794
- ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1795
- },
1796
- enumerable: true
1797
- });
1798
- });
1799
- }
1800
-
1801
- // freeze the object to prevent adding new properties
1802
- return Object.freeze(obj);
1803
1961
  }
1804
1962
  /**
1805
1963
  * LittleJS Engine Settings
@@ -2017,6 +2175,14 @@ let touchInputEnable = true;
2017
2175
  * @memberof Settings */
2018
2176
  let touchGamepadEnable = false;
2019
2177
 
2178
+ /** True if touch gamepad should have start button in the center
2179
+ * - When the game is paused, any touch will press the button
2180
+ * - This can function as a way to pause/unpause the game
2181
+ * @type {boolean}
2182
+ * @default
2183
+ * @memberof Settings */
2184
+ let touchGamepadCenterButton = true;
2185
+
2020
2186
  /** True if touch gamepad should be analog stick or false to use if 8 way dpad
2021
2187
  * @type {boolean}
2022
2188
  * @default
@@ -2192,9 +2358,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
2192
2358
  * @memberof Settings */
2193
2359
  function setGLEnable(enable)
2194
2360
  {
2361
+ if (enable && !glCanBeEnabled)
2362
+ {
2363
+ console.warn('Can not enable WebGL if it was disabled on start.');
2364
+ return;
2365
+ }
2195
2366
  glEnable = enable;
2196
2367
  if (glCanvas) // hide glCanvas if WebGL is disabled
2197
- glCanvas.style.visibility = enable ? 'visible' : 'hidden';
2368
+ glCanvas.style.display = enable ? '' : 'none';
2198
2369
  }
2199
2370
 
2200
2371
  /** Set how many sided polygons to use when drawing circles and ellipses with WebGL
@@ -2282,6 +2453,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
2282
2453
  * @memberof Settings */
2283
2454
  function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
2284
2455
 
2456
+ /** True if touch gamepad should have start button in the center
2457
+ * - This can function as a way to pause/unpause the game
2458
+ * @param {boolean} enable
2459
+ * @memberof Settings */
2460
+ function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
2461
+
2285
2462
  /** Set if touch gamepad should be analog stick or 8 way dpad
2286
2463
  * @param {boolean} analog
2287
2464
  * @memberof Settings */
@@ -2491,11 +2668,10 @@ class EngineObject
2491
2668
  }
2492
2669
 
2493
2670
  /** Update the object physics, called automatically by engine once each frame */
2494
- update()
2671
+ updatePhysics()
2495
2672
  {
2496
2673
  // child objects do not have physics
2497
- if (this.parent)
2498
- return;
2674
+ ASSERT(!this.parent);
2499
2675
 
2500
2676
  if (this.clampSpeed)
2501
2677
  {
@@ -2545,7 +2721,7 @@ class EngineObject
2545
2721
  continue;
2546
2722
 
2547
2723
  // check collision
2548
- if (!isOverlapping(this.pos, this.size, o.pos, o.size))
2724
+ if (!this.isOverlappingObject(o))
2549
2725
  continue;
2550
2726
 
2551
2727
  // notify objects of collision and check if should be resolved
@@ -2643,6 +2819,27 @@ class EngineObject
2643
2819
  // test which side we bounced off (or both if a corner)
2644
2820
  const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2645
2821
  const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2822
+
2823
+ if (blockedLayerX)
2824
+ {
2825
+ // try to move up a tiny bit
2826
+ const epsilon = 1e-3;
2827
+ const maxMoveUp = .1;
2828
+ const y = floor(oldPos.y-this.size.y/2+1) +
2829
+ this.size.y/2 + epsilon;
2830
+ const delta = y - this.pos.y;
2831
+ if (delta < maxMoveUp)
2832
+ if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
2833
+ {
2834
+ this.pos.y = y;
2835
+ debugPhysics && debugRect(this.pos, this.size, '#ff0');
2836
+ return;
2837
+ }
2838
+
2839
+ // move to previous position and bounce
2840
+ this.pos.x = oldPos.x;
2841
+ this.velocity.x *= -this.restitution;
2842
+ }
2646
2843
  if (blockedLayerY || !blockedLayerX)
2647
2844
  {
2648
2845
  // bounce velocity
@@ -2666,18 +2863,15 @@ class EngineObject
2666
2863
  this.groundObject = undefined;
2667
2864
  }
2668
2865
  }
2669
- if (blockedLayerX)
2670
- {
2671
- // move to previous position and bounce
2672
- this.pos.x = oldPos.x;
2673
- this.velocity.x *= -this.restitution;
2674
- }
2675
2866
  debugPhysics && debugRect(this.pos, this.size, '#f00');
2676
2867
  }
2677
2868
  }
2678
2869
  }
2679
2870
  }
2680
2871
 
2872
+ /** Update the object, called automatically by engine once each frame. Does nothing by default. */
2873
+ update() {}
2874
+
2681
2875
  /** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
2682
2876
  render()
2683
2877
  {
@@ -2729,10 +2923,24 @@ class EngineObject
2729
2923
  */
2730
2924
  collideWithObject(object) { return true; }
2731
2925
 
2926
+ /** Get this object's up vector
2927
+ * @param {number} [scale] - length of the vector
2928
+ * @return {Vector2} */
2929
+ getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
2930
+
2931
+ /** Get this object's right vector
2932
+ * @param {number} [scale] - length of the vector
2933
+ * @return {Vector2} */
2934
+ getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
2935
+
2732
2936
  /** How long since the object was created
2733
2937
  * @return {number} */
2734
2938
  getAliveTime() { return time - this.spawnTime; }
2735
2939
 
2940
+ /** Get the speed of this object
2941
+ * @return {number} */
2942
+ getSpeed() { return this.velocity.length(); }
2943
+
2736
2944
  /** Apply acceleration to this object (adjust velocity, not affected by mass)
2737
2945
  * @param {Vector2} acceleration */
2738
2946
  applyAcceleration(acceleration)
@@ -2777,6 +2985,20 @@ class EngineObject
2777
2985
  child.parent = 0;
2778
2986
  }
2779
2987
 
2988
+ /** Check if overlapping another engine object
2989
+ * Collisions are resoloved to prevent overlaps
2990
+ * @param {EngineObject} object
2991
+ * @return {boolean} */
2992
+ isOverlappingObject(object)
2993
+ { return this.isOverlapping(object.pos, object.size); }
2994
+
2995
+ /** Check if overlapping a point or aligned bounding box
2996
+ * @param {Vector2} pos - Center of box
2997
+ * @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
2998
+ * @return {boolean} */
2999
+ isOverlapping(pos, size=vec2())
3000
+ { return isOverlapping(this.pos, this.size, pos, size); }
3001
+
2780
3002
  /** Set how this object collides
2781
3003
  * @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
2782
3004
  * @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
@@ -3080,6 +3302,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
3080
3302
  const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
3081
3303
  if (useWebGL)
3082
3304
  {
3305
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3083
3306
  if (screenSpace)
3084
3307
  {
3085
3308
  // convert to world space
@@ -3175,6 +3398,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3175
3398
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3176
3399
  if (useWebGL)
3177
3400
  {
3401
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3178
3402
  if (screenSpace)
3179
3403
  {
3180
3404
  // convert to world space
@@ -3186,7 +3410,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3186
3410
  const halfSizeX = size.x/2, halfSizeY = size.y/2;
3187
3411
  const colorTopInt = colorTop.rgbaInt();
3188
3412
  const colorBottomInt = colorBottom.rgbaInt();
3189
- const c = Math.cos(-angle), s = Math.sin(-angle);
3413
+ const c = cos(-angle), s = sin(-angle);
3190
3414
  for (let i=4; i--;)
3191
3415
  {
3192
3416
  const x = i & 1 ? halfSizeX : -halfSizeX;
@@ -3229,7 +3453,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
3229
3453
  * @memberof Draw */
3230
3454
  function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
3231
3455
  {
3232
- ASSERT(Array.isArray(points), 'points must be an array');
3456
+ ASSERT(isArray(points), 'points must be an array');
3233
3457
  ASSERT(isNumber(width), 'width must be a number');
3234
3458
  ASSERT(isColor(color), 'color is invalid');
3235
3459
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3237,6 +3461,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
3237
3461
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3238
3462
  if (useWebGL)
3239
3463
  {
3464
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3240
3465
  let scale = 1;
3241
3466
  if (screenSpace)
3242
3467
  {
@@ -3286,6 +3511,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
3286
3511
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
3287
3512
  const size = vec2(width, halfDelta.length()*2);
3288
3513
  pos = pos.add(posA.add(halfDelta));
3514
+ if (screenSpace)
3515
+ halfDelta.y *= -1; // flip angle Y if screen space
3289
3516
  angle += halfDelta.angle();
3290
3517
  drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
3291
3518
  }
@@ -3313,7 +3540,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3313
3540
  for (let i=sides; i--;)
3314
3541
  {
3315
3542
  const a = (i/sides)*PI*2;
3316
- points.push(vec2(Math.sin(a)*sizeX, Math.cos(a)*sizeY));
3543
+ points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
3317
3544
  }
3318
3545
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
3319
3546
  }
@@ -3332,7 +3559,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
3332
3559
  function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
3333
3560
  {
3334
3561
  ASSERT(isVector2(pos), 'pos must be a vec2');
3335
- ASSERT(Array.isArray(points), 'points must be an array');
3562
+ ASSERT(isArray(points), 'points must be an array');
3336
3563
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3337
3564
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3338
3565
  ASSERT(isNumber(angle), 'angle must be a number');
@@ -3340,6 +3567,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
3340
3567
 
3341
3568
  if (useWebGL)
3342
3569
  {
3570
+ ASSERT(!!glContext, 'WebGL is not enabled!');
3343
3571
  let scale = 1;
3344
3572
  if (screenSpace)
3345
3573
  {
@@ -3483,11 +3711,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
3483
3711
  * @param {string} [font=fontDefault]
3484
3712
  * @param {string} [fontStyle]
3485
3713
  * @param {number} [maxWidth]
3714
+ * @param {number} [angle]
3486
3715
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
3487
3716
  * @memberof Draw */
3488
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
3717
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
3489
3718
  {
3490
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
3719
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
3491
3720
  }
3492
3721
 
3493
3722
  /** Draw text on overlay canvas in world space
@@ -3502,10 +3731,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
3502
3731
  * @param {string} [font=fontDefault]
3503
3732
  * @param {string} [fontStyle]
3504
3733
  * @param {number} [maxWidth]
3734
+ * @param {number} [angle]
3505
3735
  * @memberof Draw */
3506
- function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
3736
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
3507
3737
  {
3508
- drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
3738
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
3509
3739
  }
3510
3740
 
3511
3741
  /** Draw text on overlay canvas in screen space
@@ -3520,9 +3750,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
3520
3750
  * @param {string} [font=fontDefault]
3521
3751
  * @param {string} [fontStyle]
3522
3752
  * @param {number} [maxWidth]
3753
+ * @param {number} [angle]
3523
3754
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
3524
3755
  * @memberof Draw */
3525
- function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
3756
+ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
3526
3757
  {
3527
3758
  ASSERT(isString(text), 'text must be a string');
3528
3759
  ASSERT(isVector2(pos), 'pos must be a vec2');
@@ -3533,6 +3764,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3533
3764
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
3534
3765
  ASSERT(isString(font), 'font must be a string');
3535
3766
  ASSERT(isString(fontStyle), 'fontStyle must be a string');
3767
+ ASSERT(isNumber(angle), 'angle must be a number');
3536
3768
 
3537
3769
  context.fillStyle = color.toString();
3538
3770
  context.strokeStyle = lineColor.toString();
@@ -3542,14 +3774,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3542
3774
  context.textBaseline = 'middle';
3543
3775
 
3544
3776
  const lines = (text+'').split('\n');
3545
- let posY = pos.y;
3546
- posY -= (lines.length-1) * size/2; // center text vertically
3777
+ const posY = pos.y - (lines.length-1) * size/2; // center vertically
3778
+ context.save();
3779
+ context.translate(pos.x, posY);
3780
+ context.rotate(-angle);
3781
+ let yOffset = 0;
3547
3782
  lines.forEach(line=>
3548
3783
  {
3549
- lineWidth && context.strokeText(line, pos.x, posY, maxWidth);
3550
- context.fillText(line, pos.x, posY, maxWidth);
3551
- posY += size;
3784
+ lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
3785
+ context.fillText(line, 0, yOffset, maxWidth);
3786
+ yOffset += size;
3552
3787
  });
3788
+ context.restore();
3553
3789
  }
3554
3790
 
3555
3791
  ///////////////////////////////////////////////////////////////////////////////
@@ -3561,18 +3797,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
3561
3797
  * @memberof Draw */
3562
3798
  function screenToWorld(screenPos)
3563
3799
  {
3564
- let cameraPosRelativeX = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3565
- let cameraPosRelativeY = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3800
+ let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
3801
+ let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
3566
3802
  if (cameraAngle)
3567
3803
  {
3568
3804
  // apply camera rotation
3569
- const cos = Math.cos(-cameraAngle), sin = Math.sin(-cameraAngle);
3570
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3571
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3572
- cameraPosRelativeX = rotatedX;
3573
- cameraPosRelativeY = rotatedY;
3805
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
3806
+ const rotatedX = x * c - y * s;
3807
+ const rotatedY = x * s + y * c;
3808
+ x = rotatedX;
3809
+ y = rotatedY;
3574
3810
  }
3575
- return new Vector2(cameraPosRelativeX + cameraPos.x, cameraPosRelativeY + cameraPos.y);
3811
+ return new Vector2(x + cameraPos.x, y + cameraPos.y);
3576
3812
  }
3577
3813
 
3578
3814
  /** Convert from world to screen space coordinates
@@ -3581,24 +3817,64 @@ function screenToWorld(screenPos)
3581
3817
  * @memberof Draw */
3582
3818
  function worldToScreen(worldPos)
3583
3819
  {
3584
- let cameraPosRelativeX = worldPos.x - cameraPos.x;
3585
- let cameraPosRelativeY = worldPos.y - cameraPos.y;
3820
+ let x = worldPos.x - cameraPos.x;
3821
+ let y = worldPos.y - cameraPos.y;
3586
3822
  if (cameraAngle)
3587
3823
  {
3588
3824
  // apply inverse camera rotation
3589
- const cos = Math.cos(cameraAngle), sin = Math.sin(cameraAngle);
3590
- const rotatedX = cameraPosRelativeX * cos - cameraPosRelativeY * sin;
3591
- const rotatedY = cameraPosRelativeX * sin + cameraPosRelativeY * cos;
3592
- cameraPosRelativeX = rotatedX;
3593
- cameraPosRelativeY = rotatedY;
3825
+ const c = cos(cameraAngle), s = sin(cameraAngle);
3826
+ const rotatedX = x * c - y * s;
3827
+ const rotatedY = x * s + y * c;
3828
+ x = rotatedX;
3829
+ y = rotatedY;
3594
3830
  }
3595
3831
  return new Vector2
3596
3832
  (
3597
- cameraPosRelativeX * cameraScale + mainCanvasSize.x/2 - .5,
3598
- cameraPosRelativeY * -cameraScale + mainCanvasSize.y/2 - .5
3833
+ x * cameraScale + mainCanvasSize.x/2 - .5,
3834
+ y * -cameraScale + mainCanvasSize.y/2 - .5
3599
3835
  );
3600
3836
  }
3601
3837
 
3838
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
3839
+ * @param {Vector2} screenDelta
3840
+ * @return {Vector2}
3841
+ * @memberof Draw */
3842
+ function screenToWorldDelta(screenDelta)
3843
+ {
3844
+ let x = screenDelta.x / cameraScale;
3845
+ let y = screenDelta.y / -cameraScale;
3846
+ if (cameraAngle)
3847
+ {
3848
+ // apply camera rotation
3849
+ const c = cos(-cameraAngle), s = sin(-cameraAngle);
3850
+ const rotatedX = x * c - y * s;
3851
+ const rotatedY = x * s + y * c;
3852
+ x = rotatedX;
3853
+ y = rotatedY;
3854
+ }
3855
+ return new Vector2(x, y);
3856
+ }
3857
+
3858
+ /** Convert from screen to world space coordinates for a directional vector (no translation)
3859
+ * @param {Vector2} worldDelta
3860
+ * @return {Vector2}
3861
+ * @memberof Draw */
3862
+ function worldToScreenDelta(worldDelta)
3863
+ {
3864
+ let x = worldDelta.x;
3865
+ let y = worldDelta.y;
3866
+ if (cameraAngle)
3867
+ {
3868
+ // apply inverse camera rotation
3869
+ const c = cos(cameraAngle), s = sin(cameraAngle);
3870
+ const rotatedX = x * c - y * s;
3871
+ const rotatedY = x * s + y * c;
3872
+ x = rotatedX;
3873
+ y = rotatedY;
3874
+ }
3875
+ return new Vector2(x * cameraScale, y * -cameraScale);
3876
+ }
3877
+
3602
3878
  /** Get the camera's visible area in world space
3603
3879
  * @return {Vector2}
3604
3880
  * @memberof Draw */
@@ -3651,6 +3927,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3651
3927
  function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
3652
3928
  const sx2 = bleedScale;
3653
3929
  const sy2 = bleedScale;
3930
+ sWidth = max(1,sWidth|0);
3931
+ sHeight = max(1,sHeight|0);
3654
3932
  const sWidth2 = sWidth - 2*bleedScale;
3655
3933
  const sHeight2 = sHeight - 2*bleedScale;
3656
3934
  if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
@@ -3665,7 +3943,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
3665
3943
  // copy to offscreen canvas
3666
3944
  workCanvas.width = sWidth;
3667
3945
  workCanvas.height = sHeight;
3668
- workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
3946
+ workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
3669
3947
 
3670
3948
  // tint image using offscreen work context
3671
3949
  const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
@@ -3939,6 +4217,11 @@ let mouseDeltaScreen = vec2();
3939
4217
  * @memberof Input */
3940
4218
  let mouseWheel = 0;
3941
4219
 
4220
+ /** True if mouse was inside the document window, set to false when mouse leaves
4221
+ * @type {boolean}
4222
+ * @memberof Input */
4223
+ let mouseInWindow = true;
4224
+
3942
4225
  /** Returns true if user is using gamepad (has more recently pressed a gamepad button)
3943
4226
  * @type {boolean}
3944
4227
  * @memberof Input */
@@ -4002,9 +4285,9 @@ function inputUpdate()
4002
4285
  if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
4003
4286
  inputClear();
4004
4287
 
4005
- // update mouse world space position
4288
+ // update mouse world space position and delta
4006
4289
  mousePos = screenToWorld(mousePosScreen);
4007
- mouseDelta = mouseDeltaScreen.multiply(vec2(1,-1)).rotate(-cameraAngle);
4290
+ mouseDelta = screenToWorldDelta(mouseDeltaScreen);
4008
4291
 
4009
4292
  // update gamepads if enabled
4010
4293
  gamepadsUpdate();
@@ -4033,10 +4316,10 @@ function inputInit()
4033
4316
  document.addEventListener('mousedown', onMouseDown);
4034
4317
  document.addEventListener('mouseup', onMouseUp);
4035
4318
  document.addEventListener('mousemove', onMouseMove);
4319
+ document.addEventListener('mouseleave', onMouseLeave);
4036
4320
  document.addEventListener('wheel', onMouseWheel);
4037
4321
  document.addEventListener('contextmenu', onContextMenu);
4038
4322
  document.addEventListener('blur', onBlur);
4039
- document.addEventListener('mouseleave', onMouseLeave);
4040
4323
 
4041
4324
  // init touch input
4042
4325
  if (isTouchDevice && touchInputEnable)
@@ -4058,14 +4341,14 @@ function inputInit()
4058
4341
  if (inputWASDEmulateDirection)
4059
4342
  inputData[0][remapKey(e.code)] = 4;
4060
4343
  }
4061
- function remapKey(c)
4344
+ function remapKey(k)
4062
4345
  {
4063
4346
  // handle remapping wasd keys to directions
4064
4347
  return inputWASDEmulateDirection ?
4065
- c === 'KeyW' ? 'ArrowUp' :
4066
- c === 'KeyS' ? 'ArrowDown' :
4067
- c === 'KeyA' ? 'ArrowLeft' :
4068
- c === 'KeyD' ? 'ArrowRight' : c : c;
4348
+ k === 'KeyW' ? 'ArrowUp' :
4349
+ k === 'KeyS' ? 'ArrowDown' :
4350
+ k === 'KeyA' ? 'ArrowLeft' :
4351
+ k === 'KeyD' ? 'ArrowRight' : k : k;
4069
4352
  }
4070
4353
  function onMouseDown(e)
4071
4354
  {
@@ -4078,7 +4361,10 @@ function inputInit()
4078
4361
 
4079
4362
  isUsingGamepad = false;
4080
4363
  inputData[0][e.button] = 3;
4364
+
4365
+ const mousePosScreenLast = mousePosScreen;
4081
4366
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4367
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4082
4368
  inputPreventDefault && e.button && e.preventDefault();
4083
4369
  }
4084
4370
  function onMouseUp(e)
@@ -4089,18 +4375,20 @@ function inputInit()
4089
4375
  }
4090
4376
  function onMouseMove(e)
4091
4377
  {
4378
+ mouseInWindow = true;
4379
+ const mousePosScreenLast = mousePosScreen;
4092
4380
  mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
4093
- mouseDeltaScreen = mouseDeltaScreen.add(vec2(e.movementX, e.movementY));
4381
+
4382
+ // when pointer is locked use movementX/Y for delta
4383
+ const movement = pointerLockIsActive() ?
4384
+ vec2(e.movementX, e.movementY) :
4385
+ mousePosScreen.subtract(mousePosScreenLast);
4386
+ mouseDeltaScreen = mouseDeltaScreen.add(movement);
4094
4387
  }
4388
+ function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
4095
4389
  function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
4096
4390
  function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
4097
4391
  function onBlur() { inputClear(); } // reset input when focus is lost
4098
- function onMouseLeave()
4099
- {
4100
- // set mouse position and delta when leaving canvas
4101
- mousePosScreen = vec2(-1);
4102
- mouseDeltaScreen = vec2(0);
4103
- }
4104
4392
  }
4105
4393
 
4106
4394
  // convert a mouse or touch event position to screen space
@@ -4144,8 +4432,8 @@ function gamepadsUpdate()
4144
4432
  else if (touchGamepadStick.lengthSquared() > .3)
4145
4433
  {
4146
4434
  // convert to 8 way dpad
4147
- sticks[0].x = Math.round(touchGamepadStick.x);
4148
- sticks[0].y = -Math.round(touchGamepadStick.y);
4435
+ sticks[0].x = round(touchGamepadStick.x);
4436
+ sticks[0].y = -round(touchGamepadStick.y);
4149
4437
  sticks[0] = sticks[0].clampLength();
4150
4438
  }
4151
4439
 
@@ -4236,7 +4524,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
4236
4524
  function touchGamepadButtonCenter()
4237
4525
  {
4238
4526
  // draw right face buttons
4239
- let center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4527
+ const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
4240
4528
  if (touchGamepadButtonCount <= 2)
4241
4529
  center.x += touchGamepadSize/2;
4242
4530
  return center;
@@ -4246,14 +4534,13 @@ function touchGamepadButtonCenter()
4246
4534
  function touchInputInit()
4247
4535
  {
4248
4536
  // add non passive touch event listeners
4249
- let handleTouch = handleTouchDefault;
4250
4537
  document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4251
4538
  document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4252
4539
  document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4253
4540
 
4254
4541
  // handle all touch events the same way
4255
4542
  let wasTouching;
4256
- function handleTouchDefault(e)
4543
+ function handleTouch(e)
4257
4544
  {
4258
4545
  if (!touchInputEnable)
4259
4546
  return;
@@ -4273,11 +4560,11 @@ function touchInputInit()
4273
4560
  {
4274
4561
  // set event pos and pass it along
4275
4562
  const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
4276
- const lastMousePosScreen = mousePosScreen;
4563
+ const mousePosScreenLast = mousePosScreen;
4277
4564
  mousePosScreen = mouseEventToScreen(pos);
4278
4565
  if (wasTouching)
4279
4566
  {
4280
- mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(lastMousePosScreen));
4567
+ mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
4281
4568
  isUsingGamepad = touchGamepadEnable;
4282
4569
  }
4283
4570
  else
@@ -4309,7 +4596,7 @@ function touchInputInit()
4309
4596
  if (touching)
4310
4597
  {
4311
4598
  touchGamepadTimer.set();
4312
- if (paused && !wasTouching)
4599
+ if (touchGamepadCenterButton && !wasTouching && paused)
4313
4600
  {
4314
4601
  // touch anywhere to press start when paused
4315
4602
  touchGamepadButtons[9] = 1;
@@ -4348,7 +4635,8 @@ function touchInputInit()
4348
4635
  if (button < touchGamepadButtonCount)
4349
4636
  touchGamepadButtons[button] = 1;
4350
4637
  }
4351
- else if (startCenter.distance(touchPos) < touchGamepadSize && !wasTouching)
4638
+ else if (touchGamepadCenterButton && !wasTouching &&
4639
+ startCenter.distance(touchPos) < touchGamepadSize)
4352
4640
  {
4353
4641
  // virtual start button in center
4354
4642
  touchGamepadButtons[9] = 1;
@@ -4403,7 +4691,7 @@ function touchGamepadRender()
4403
4691
  const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
4404
4692
  for (let i=0; i<touchGamepadButtonCount; i++)
4405
4693
  {
4406
- let j = mod(i-1, 4);
4694
+ const j = mod(i-1, 4);
4407
4695
  let button = touchGamepadButtonCount > 2 ?
4408
4696
  j : min(j, touchGamepadButtonCount-1);
4409
4697
  // fix button locations (swap 2 and 3 to match gamepad layout)
@@ -4505,6 +4793,10 @@ class Sound
4505
4793
  {
4506
4794
  if (!soundEnable || headlessMode) return;
4507
4795
 
4796
+ ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
4797
+ ASSERT(isNumber(range), 'range must be a number');
4798
+ ASSERT(isNumber(taper), 'taper must be a number');
4799
+
4508
4800
  /** @property {number} - World space max range of sound */
4509
4801
  this.range = range;
4510
4802
  /** @property {number} - At what percentage of range should it start tapering */
@@ -4543,6 +4835,12 @@ class Sound
4543
4835
  */
4544
4836
  play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
4545
4837
  {
4838
+ ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
4839
+ ASSERT(isNumber(volume), 'volume must be a number');
4840
+ ASSERT(isNumber(pitch), 'pitch must be a number');
4841
+ ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
4842
+
4843
+
4546
4844
  if (!soundEnable || headlessMode) return;
4547
4845
  if (!this.sampleChannels) return;
4548
4846
 
@@ -4588,6 +4886,7 @@ class Sound
4588
4886
  */
4589
4887
  playNote(semitoneOffset=0, pos, volume)
4590
4888
  {
4889
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
4591
4890
  const pitch = getNoteFrequency(semitoneOffset, 1);
4592
4891
  return this.play(pos, volume, pitch, 0);
4593
4892
  }
@@ -4596,7 +4895,7 @@ class Sound
4596
4895
  * @return {number} - How long the sound is in seconds (undefined if loading)
4597
4896
  */
4598
4897
  getDuration()
4599
- { return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
4898
+ { return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
4600
4899
 
4601
4900
  /** Check if sound is loaded, for sounds fetched from a url
4602
4901
  * @return {boolean} - True if sound is loaded and ready to play
@@ -4638,6 +4937,7 @@ class SoundWave extends Sound
4638
4937
  super(undefined, range, taper);
4639
4938
  if (!soundEnable || headlessMode) return;
4640
4939
  ASSERT(!filename || isString(filename), 'filename must be a string');
4940
+ ASSERT(isNumber(randomness), 'randomness must be a number');
4641
4941
 
4642
4942
  /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
4643
4943
  this.onloadCallback = onloadCallback;
@@ -4854,7 +5154,7 @@ class SoundInstance
4854
5154
  /** Get the total duration of this sound
4855
5155
  * @return {number} - Total duration in seconds
4856
5156
  */
4857
- getDuration() { return this.sound.getDuration() / this.rate; }
5157
+ getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
4858
5158
 
4859
5159
  /** Get source of this sound instance
4860
5160
  * @return {AudioBufferSourceNode}
@@ -5044,10 +5344,10 @@ function zzfxG
5044
5344
 
5045
5345
  // biquad LP/HP filter
5046
5346
  quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
5047
- cos = Math.cos(w), alpha = Math.sin(w) / 2 / quality,
5048
- a0 = 1 + alpha, a1 = -2*cos / a0, a2 = (1 - alpha) / a0,
5049
- b0 = (1 + sign(filter) * cos) / 2 / a0,
5050
- b1 = -(sign(filter) + cos) / a0, b2 = b0,
5347
+ cosw = cos(w), alpha = sin(w) / 2 / quality,
5348
+ a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
5349
+ b0 = (1 + sign(filter) * cosw) / 2 / a0,
5350
+ b1 = -(sign(filter) + cosw) / a0, b2 = b0,
5051
5351
  x2 = 0, x1 = 0, y2 = 0, y1 = 0;
5052
5352
 
5053
5353
  // scale by sample rate
@@ -5070,15 +5370,15 @@ function zzfxG
5070
5370
  if (!(++crush%(bitCrush*100|0))) // bit crush
5071
5371
  {
5072
5372
  s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
5073
- (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5074
- Math.sin(t**3) : // 4 noise
5075
- Math.max(Math.min(Math.tan(t),1),-1): // 3 tan
5076
- 1-(2*t/PI2%2+2)%2: // 2 saw
5077
- 1-4*abs(Math.round(t/PI2)-t/PI2): // 1 triangle
5078
- Math.sin(t); // 0 sin
5373
+ (t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
5374
+ sin(t**3) : // 4 noise
5375
+ max(min(tan(t),1),-1): // 3 tan
5376
+ 1-(2*t/PI2%2+2)%2: // 2 saw
5377
+ 1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
5378
+ sin(t); // 0 sin
5079
5379
 
5080
5380
  s = (repeatTime ?
5081
- 1 - tremolo + tremolo*Math.sin(PI2*i/repeatTime) // tremolo
5381
+ 1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
5082
5382
  : 1) *
5083
5383
  (shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
5084
5384
  (i < attack ? i/attack : // attack
@@ -5100,8 +5400,8 @@ function zzfxG
5100
5400
  }
5101
5401
 
5102
5402
  f = (frequency += slide += deltaSlide) *// frequency
5103
- Math.cos(modulation*modOffset++); // modulation
5104
- t += f + f*noise*Math.sin(i**5); // noise
5403
+ cos(modulation*modOffset++); // modulation
5404
+ t += f + f*noise*sin(i**5); // noise
5105
5405
 
5106
5406
  if (jump && ++jump > pitchJumpTime) // pitch jump
5107
5407
  {
@@ -5167,23 +5467,23 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5167
5467
  }
5168
5468
  }
5169
5469
 
5170
- /** Return the center of first tile hit, undefined if nothing was hit.
5171
- * This does not return the exact intersection, but the center of the tile hit.
5470
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
5471
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
5172
5472
  * @param {Vector2} posStart
5173
5473
  * @param {Vector2} posEnd
5174
5474
  * @param {EngineObject} [object] - An object or undefined for generic test
5475
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5175
5476
  * @param {boolean} [solidOnly=true] - Only check solid layers if true
5176
- * @return {Vector2}
5477
+ * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5177
5478
  * @memberof TileLayers */
5178
- function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
5479
+ function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5179
5480
  {
5180
5481
  for (const layer of tileCollisionLayers)
5181
5482
  {
5182
5483
  if (!solidOnly || layer.isSolid)
5183
5484
  {
5184
- const hitPos = layer.collisionRaycast(posStart, posEnd, object)
5185
- if (hitPos)
5186
- return hitPos;
5485
+ const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
5486
+ if (hitPos) return hitPos;
5187
5487
  }
5188
5488
  }
5189
5489
  }
@@ -5321,6 +5621,8 @@ class CanvasLayer extends EngineObject
5321
5621
  /** @property {TextureInfo} - Texture info to use for this object rendering */
5322
5622
  const useWebGL = false; // do not use webgl by default
5323
5623
  this.textureInfo = new TextureInfo(this.canvas, useWebGL);
5624
+ /** @property {boolean} - True if WebGL texture needs to be refreshed */
5625
+ this.refreshWebGL = false;
5324
5626
 
5325
5627
  // disable physics by default
5326
5628
  this.mass = this.gravityScale = this.friction = this.restitution = 0;
@@ -5354,8 +5656,15 @@ class CanvasLayer extends EngineObject
5354
5656
  * @memberof Draw */
5355
5657
  draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
5356
5658
  {
5357
- // draw the canvas layer as a single tile that uses the whole texture
5358
5659
  const useWebGL = glEnable && this.textureInfo.hasWebGL();
5660
+ if (useWebGL && this.refreshWebGL)
5661
+ {
5662
+ // update the WebGL texture
5663
+ this.textureInfo.createWebGLTexture();
5664
+ this.refreshWebGL = false;
5665
+ }
5666
+
5667
+ // draw the canvas layer as a single tile that uses the whole texture
5359
5668
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5360
5669
  drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
5361
5670
  }
@@ -5425,9 +5734,18 @@ class CanvasLayer extends EngineObject
5425
5734
  { this.drawTile(pos, size, undefined, color, angle); }
5426
5735
 
5427
5736
  /** Create or update the WebGL texture for this layer
5428
- * @param {boolean} [enable] - enable WebGL rendering and update the texture */
5429
- useWebGL(enable=true)
5737
+ * @param {boolean} [enable] - enable WebGL rendering and update the texture
5738
+ * @param {boolean} [immediate] - shoulkd the texture be updated immediately
5739
+ */
5740
+ useWebGL(enable=true, immediate=false)
5430
5741
  {
5742
+ if (!immediate && enable && this.textureInfo.hasWebGL())
5743
+ {
5744
+ // refresh the texture when needed
5745
+ this.refreshWebGL = true;
5746
+ return;
5747
+ }
5748
+
5431
5749
  if (enable)
5432
5750
  this.textureInfo.createWebGLTexture();
5433
5751
  else
@@ -5458,7 +5776,7 @@ class TileLayer extends CanvasLayer
5458
5776
  */
5459
5777
  constructor(position, size, tileInfo=tile(), renderOrder=0)
5460
5778
  {
5461
- const canvasSize = size.multiply(tileInfo.size);
5779
+ const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
5462
5780
  super(position, size, 0, renderOrder, canvasSize);
5463
5781
 
5464
5782
  // set tile info
@@ -5511,6 +5829,13 @@ class TileLayer extends CanvasLayer
5511
5829
  {
5512
5830
  ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
5513
5831
 
5832
+ if (this.refreshWebGL)
5833
+ {
5834
+ // update the WebGL texture
5835
+ this.textureInfo.createWebGLTexture();
5836
+ this.refreshWebGL = false;
5837
+ }
5838
+
5514
5839
  // draw the tile layer as a single tile
5515
5840
  const tileInfo = new TileInfo().setFullImage(this.textureInfo);
5516
5841
  const size = this.drawSize || this.size;
@@ -5547,8 +5872,9 @@ class TileLayer extends CanvasLayer
5547
5872
  drawCanvas = this.canvas;
5548
5873
  drawContext = this.context;
5549
5874
  cameraPos = this.size.scale(.5);
5550
- cameraScale = this.tileInfo.size.x;
5551
- mainCanvasSize = this.size.multiply(this.tileInfo.size);
5875
+ const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
5876
+ cameraScale = tileSize.x;
5877
+ mainCanvasSize = this.size.multiply(tileSize);
5552
5878
  if (clear)
5553
5879
  {
5554
5880
  // clear and set size
@@ -5708,66 +6034,45 @@ class TileCollisionLayer extends TileLayer
5708
6034
  // check if the object should collide with this tile
5709
6035
  const tileData = this.collisionData[y*this.size.x+x];
5710
6036
  if (tileData)
5711
- if (!object || object.collideWithTile(tileData, hitPos.set(x, y)))
6037
+ if (!object || object.collideWithTile(tileData,
6038
+ hitPos.set(x + this.pos.x, y + this.pos.y)))
5712
6039
  return true;
5713
6040
  }
5714
6041
  return false;
5715
6042
  }
5716
6043
 
5717
- /** Return the center of first tile hit, undefined if nothing was hit.
5718
- * This does not return the exact intersection, but the center of the tile hit.
6044
+ /** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
6045
+ * The point will be inside the colliding tile if it hits (may have a tiny shift)
5719
6046
  * @param {Vector2} posStart
5720
6047
  * @param {Vector2} posEnd
5721
- * @param {EngineObject} [object]
5722
- * @return {Vector2} */
5723
- collisionRaycast(posStart, posEnd, object)
6048
+ * @param {EngineObject} [object] - An object or undefined for generic test
6049
+ * @param {Vector2} [normal] - Optional normal of the surface hit
6050
+ * @return {Vector2|undefined} */
6051
+ collisionRaycast(posStart, posEnd, object, normal)
5724
6052
  {
5725
6053
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
5726
6054
  ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
5727
-
5728
- // transform to local layer space
5729
- const posStartX = posStart.x - this.pos.x;
5730
- const posStartY = posStart.y - this.pos.y;
5731
- const posEndX = posEnd.x - this.pos.x;
5732
- const posEndY = posEnd.y - this.pos.y;
5733
-
5734
- // test if a ray collides with tiles from start to end
5735
- const deltaX = posEndX - posStartX;
5736
- const deltaY = posEndY - posStartY;
5737
- const totalLength = (deltaX**2 + deltaY**2)**.5;
5738
- const unitX = abs(totalLength/deltaX);
5739
- const unitY = abs(totalLength/deltaY);
5740
-
5741
- // setup iteration variables
5742
- const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
5743
- let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
5744
- let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
5745
-
5746
- // use line drawing algorithm to test for collisions
5747
- while (true)
6055
+
6056
+ const localPos = new Vector2;
6057
+ const collisionTest = (pos)=>
5748
6058
  {
5749
6059
  // check for tile collision
5750
- const tileData = this.getCollisionData(pos);
5751
- if (tileData && (!object || object.collideWithTile(tileData, pos)))
5752
- {
5753
- pos.x += .5; pos.y += .5;
5754
- debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
5755
- debugRaycast && debugPoint(pos, '#ff0');
5756
- return pos;
5757
- }
5758
-
5759
- // check if past the end
5760
- if (xi >= totalLength && yi >= totalLength)
5761
- break;
5762
-
5763
- // get coordinates of next tile to check
5764
- if (xi > yi)
5765
- pos.y += signDeltaY, yi += unitY;
5766
- else
5767
- pos.x += signDeltaX, xi += unitX;
6060
+ localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
6061
+ const tileData = this.getCollisionData(localPos);
6062
+ return tileData && (!object || object.collideWithTile(tileData, pos));
5768
6063
  }
5769
-
5770
6064
  debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
6065
+ const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
6066
+ if (hitPos)
6067
+ {
6068
+ const tilePos = hitPos.floor().add(vec2(.5));
6069
+ debugRaycast && debugRect(tilePos, vec2(1), '#f008');
6070
+ debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
6071
+ debugRaycast && debugPoint(hitPos, '#0f0');
6072
+ debugRaycast && normal &&
6073
+ debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6074
+ return hitPos;
6075
+ }
5771
6076
  }
5772
6077
  }
5773
6078
  /**
@@ -5922,19 +6227,34 @@ class ParticleEmitter extends EngineObject
5922
6227
  this.particleCreateCallback = undefined;
5923
6228
  /** @property {number} - Track particle emit time */
5924
6229
  this.emitTimeBuffer = 0;
6230
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
6231
+ this.velocityInheritance = 0;
6232
+
6233
+ // track previous position and angle
6234
+ this.previousAngle = this.angle;
6235
+ this.previousPos = this.pos.copy();
5925
6236
  }
5926
6237
 
5927
6238
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
5928
6239
  update()
5929
6240
  {
5930
- // only do default update to apply parent transforms
5931
- this.parent && super.update();
6241
+ if (this.velocityInheritance)
6242
+ {
6243
+ // pass emitter velocity to particles
6244
+ const p = this.velocityInheritance;
6245
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
6246
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
6247
+ this.angleVelocity = p * (this.angle - this.previousAngle);
6248
+ this.previousAngle = this.angle;
6249
+ this.previousPos.x = this.pos.x;
6250
+ this.previousPos.y = this.pos.y;
6251
+ }
5932
6252
 
5933
6253
  // update emitter
5934
6254
  if (!this.emitTime || this.getAliveTime() <= this.emitTime)
5935
6255
  {
5936
6256
  // emit particles
5937
- if (this.emitRate * particleEmitRateScale)
6257
+ if (this.emitRate && particleEmitRateScale)
5938
6258
  {
5939
6259
  const rate = 1/this.emitRate/particleEmitRateScale;
5940
6260
  for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
@@ -5987,7 +6307,7 @@ class ParticleEmitter extends EngineObject
5987
6307
  const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
5988
6308
  particle.velocity = vec2().setAngle(velocityAngle, speed);
5989
6309
  particle.angleVelocity = angleSpeed;
5990
- if (!this.localSpace)
6310
+ if (!this.localSpace && this.velocityInheritance > 0)
5991
6311
  {
5992
6312
  // apply emitter velocity to particle
5993
6313
  particle.velocity.x += this.velocity.x;
@@ -6072,8 +6392,6 @@ class Particle extends EngineObject
6072
6392
  /** Update the object physics, called automatically by engine once each frame */
6073
6393
  update()
6074
6394
  {
6075
- super.update();
6076
-
6077
6395
  if (this.collideTiles || this.collideSolidObjects)
6078
6396
  {
6079
6397
  // only apply max circular speed if particle can collide
@@ -6085,6 +6403,16 @@ class Particle extends EngineObject
6085
6403
  this.velocity.y *= s;
6086
6404
  }
6087
6405
  }
6406
+
6407
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
6408
+ {
6409
+ // destroy particle when its time runs out
6410
+ const c = this.colorEnd;
6411
+ this.color.set(c.r, c.g, c.b, c.a);
6412
+ this.size.set(this.sizeEnd, this.sizeEnd);
6413
+ this.destroyCallback && this.destroyCallback(this);
6414
+ this.destroyed = 1;
6415
+ }
6088
6416
  }
6089
6417
 
6090
6418
  /** Render the particle, automatically called each frame, sorted by renderOrder */
@@ -6093,7 +6421,7 @@ class Particle extends EngineObject
6093
6421
  // lerp color and size
6094
6422
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6095
6423
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6096
- this.size.x = this.size.y = radius;
6424
+ const size = vec2(radius);
6097
6425
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6098
6426
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6099
6427
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
@@ -6113,7 +6441,7 @@ class Particle extends EngineObject
6113
6441
  {
6114
6442
  // in local space of emitter
6115
6443
  const a = this.localSpaceEmitter.angle;
6116
- const c = Math.cos(a), s = Math.sin(a);
6444
+ const c = cos(a), s = sin(a);
6117
6445
  pos = this.localSpaceEmitter.pos.add(
6118
6446
  new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6119
6447
  angle += this.localSpaceEmitter.angle;
@@ -6129,22 +6457,15 @@ class Particle extends EngineObject
6129
6457
  {
6130
6458
  // stretch in direction of motion
6131
6459
  const trailLength = speed * this.trailScale;
6132
- this.size.y = max(this.size.x, trailLength);
6133
- angle = Math.atan2(direction.x, direction.y);
6134
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6460
+ size.y = max(size.x, trailLength);
6461
+ angle = atan2(direction.x, direction.y);
6462
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6135
6463
  }
6136
6464
  }
6137
6465
  else
6138
- drawTile(pos, this.size, this.tileInfo, this.color, angle, this.mirror);
6466
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6139
6467
  this.additive && setBlendMode();
6140
- debugParticles && debugRect(pos, this.size, '#f005', 0, angle);
6141
-
6142
- if (p1 === 1)
6143
- {
6144
- // destroy particle when its time runs out
6145
- this.destroyCallback && this.destroyCallback(this);
6146
- this.destroyed = 1;
6147
- }
6468
+ debugParticles && debugRect(pos, size, '#f005', 0, angle);
6148
6469
  }
6149
6470
  }
6150
6471
  /**
@@ -6314,11 +6635,11 @@ class Medal
6314
6635
  const descriptionSize = height*.3;
6315
6636
  const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
6316
6637
  const textWidth = width - medalDisplayIconSize - 3*gap.x;
6317
- drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6638
+ drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6318
6639
 
6319
6640
  // draw the description
6320
6641
  pos.y = y + height - gap.y*2 - descriptionSize/2;
6321
- drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
6642
+ drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
6322
6643
  context.restore();
6323
6644
  }
6324
6645
 
@@ -6367,7 +6688,7 @@ let glContext;
6367
6688
  let glAntialias = true;
6368
6689
 
6369
6690
  // WebGL internal variables not exposed to documentation
6370
- let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
6691
+ let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
6371
6692
 
6372
6693
  // WebGL internal constants
6373
6694
  const gl_ARRAY_BUFFER_SIZE = 5e5;
@@ -6386,7 +6707,11 @@ function glInit()
6386
6707
  // keep set of texture infos so they can be restored if context is lost
6387
6708
  glTextureInfos = new Set;
6388
6709
 
6389
- if (!glEnable || headlessMode) return;
6710
+ if (!glEnable || headlessMode)
6711
+ {
6712
+ glCanBeEnabled = false;
6713
+ return;
6714
+ }
6390
6715
 
6391
6716
  // create the canvas and textures
6392
6717
  glCanvas = document.createElement('canvas');
@@ -6397,10 +6722,11 @@ function glInit()
6397
6722
  console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
6398
6723
  glCanvas = glContext = undefined;
6399
6724
  glEnable = false;
6725
+ glCanBeEnabled = false;
6400
6726
  return;
6401
6727
  }
6402
6728
 
6403
- // create the WebGL canvas
6729
+ // attach the WebGL canvas
6404
6730
  const rootElement = mainCanvas.parentElement;
6405
6731
  rootElement.appendChild(glCanvas);
6406
6732
 
@@ -6423,7 +6749,7 @@ function glInit()
6423
6749
  });
6424
6750
  glCanvas.addEventListener('webglcontextrestored', ()=>
6425
6751
  {
6426
- glEnable = true; // disable WebGL rendering
6752
+ glEnable = true; // re-enable WebGL rendering
6427
6753
  glCanvas.style.display = ''; // show the gl canvas
6428
6754
  LOG('WebGL context restored, reinitializing...');
6429
6755
 
@@ -6575,8 +6901,8 @@ function glPreRender()
6575
6901
  const s = vec2(2*cameraScale).divide(mainCanvasSize);
6576
6902
  const rotatedCam = cameraPos.rotate(-cameraAngle);
6577
6903
  const p = vec2(-1).subtract(rotatedCam.multiply(s));
6578
- const ca = Math.cos(cameraAngle);
6579
- const sa = Math.sin(cameraAngle);
6904
+ const ca = cos(cameraAngle);
6905
+ const sa = sin(cameraAngle);
6580
6906
  const transform = [
6581
6907
  s.x * ca, s.y * sa, 0, 0,
6582
6908
  -s.x * sa, s.y * ca, 0, 0,
@@ -6881,8 +7207,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
6881
7207
  // transform the point
6882
7208
  const px = p.x*sx;
6883
7209
  const py = p.y*sy;
6884
- const sa = Math.sin(-angle);
6885
- const ca = Math.cos(-angle);
7210
+ const sa = sin(-angle);
7211
+ const ca = cos(-angle);
6886
7212
  pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
6887
7213
  }
6888
7214
  const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
@@ -7799,12 +8125,12 @@ class UISystemPlugin
7799
8125
  /** Draw a rectangle to the UI context
7800
8126
  * @param {Vector2} pos
7801
8127
  * @param {Vector2} size
7802
- * @param {Color} [color=uiSystem.defaultColor]
7803
- * @param {number} [lineWidth=uiSystem.defaultLineWidth]
7804
- * @param {Color} [lineColor=uiSystem.defaultLineColor]
7805
- * @param {number} [cornerRadius=uiSystem.defaultCornerRadius]
7806
- * @param {Color} [gradientColor=uiSystem.defaultGradientColor] */
7807
- drawRect(pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, cornerRadius=uiSystem.defaultCornerRadius, gradientColor=uiSystem.defaultGradientColor)
8128
+ * @param {Color} [color]
8129
+ * @param {number} [lineWidth]
8130
+ * @param {Color} [lineColor]
8131
+ * @param {number} [cornerRadius]
8132
+ * @param {Color} [gradientColor] */
8133
+ drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
7808
8134
  {
7809
8135
  ASSERT(isVector2(pos), 'pos must be a vec2');
7810
8136
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -7883,10 +8209,14 @@ class UISystemPlugin
7883
8209
  * @param {string} [align]
7884
8210
  * @param {string} [font=uiSystem.defaultFont]
7885
8211
  * @param {string} [fontStyle]
7886
- * @param {boolean} [applyMaxWidth=true] */
7887
- drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true)
8212
+ * @param {boolean} [applyMaxWidth=true]
8213
+ * @param {Vector2} [textShadow]
8214
+ */
8215
+ drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
7888
8216
  {
7889
- drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, uiSystem.uiContext);
8217
+ if (textShadow)
8218
+ drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
8219
+ drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
7890
8220
  }
7891
8221
 
7892
8222
  /**
@@ -7913,6 +8243,25 @@ class UISystemPlugin
7913
8243
  setCallback(onDragLeave, 'dragleave');
7914
8244
  setCallback(onDragOver, 'dragover');
7915
8245
  }
8246
+
8247
+ /** Convert a screen space position to native UI position
8248
+ * @param {Vector2} pos
8249
+ * @return {Vector2}
8250
+ */
8251
+ screenToNative(pos)
8252
+ {
8253
+ if (!uiSystem.nativeHeight)
8254
+ return pos;
8255
+
8256
+ const s = mainCanvasSize.y / uiSystem.nativeHeight;
8257
+ const sInv = 1/s;
8258
+ const p = pos.copy();
8259
+ p.x += s*mainCanvasSize.x/2;
8260
+ p.x *= sInv;
8261
+ p.y *= sInv;
8262
+ p.x -= sInv*mainCanvasSize.x/2;
8263
+ return p;
8264
+ }
7916
8265
  }
7917
8266
 
7918
8267
  ///////////////////////////////////////////////////////////////////////////////
@@ -7989,6 +8338,9 @@ class UIObject
7989
8338
  /** @property {boolean} - True if this can be a hover object */
7990
8339
  this.canBeHover = true;
7991
8340
  uiSystem.uiObjects.push(this);
8341
+
8342
+ /** @property {Vector2} - How much to offset the text shadow or undefined */
8343
+ this.textShadow = undefined;
7992
8344
  }
7993
8345
 
7994
8346
  /** Add a child UIObject to this object
@@ -8016,24 +8368,20 @@ class UIObject
8016
8368
  */
8017
8369
  isMouseOverlapping()
8018
8370
  {
8371
+ if (!mouseInWindow) return false;
8372
+
8019
8373
  const size = !isTouchDevice ? this.size :
8020
8374
  this.size.add(vec2(this.extraTouchSize || 0));
8021
- if (!uiSystem.nativeHeight)
8022
- return isOverlapping(this.pos, size, mousePosScreen);
8023
-
8024
- const s = mainCanvasSize.y / uiSystem.nativeHeight;
8025
- const sInv = 1/s;
8026
- let pos = mousePosScreen.copy();
8027
- pos.x += s*mainCanvasSize.x/2;
8028
- pos.x *= sInv;
8029
- pos.y *= sInv;
8030
- pos.x -= sInv*mainCanvasSize.x/2;
8375
+ const pos = uiSystem.screenToNative(mousePosScreen);
8031
8376
  return isOverlapping(this.pos, size, pos);
8032
8377
  }
8033
8378
 
8034
8379
  /** Update the object, called automatically by plugin once each frame */
8035
8380
  update()
8036
8381
  {
8382
+ // call the custom update callback
8383
+ this.onUpdate();
8384
+
8037
8385
  const wasHover = uiSystem.lastHoverObject === this;
8038
8386
  const isActive = this.isActiveObject();
8039
8387
  const mouseDown = mouseIsDown(0);
@@ -8090,7 +8438,7 @@ class UIObject
8090
8438
 
8091
8439
  const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
8092
8440
  const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
8093
- uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
8441
+ uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
8094
8442
  }
8095
8443
 
8096
8444
  /** Special update when object is not visible */
@@ -8117,6 +8465,9 @@ class UIObject
8117
8465
  /** @return {boolean} - Is the mouse held onto this element */
8118
8466
  isActiveObject() { return uiSystem.activeObject === this; }
8119
8467
 
8468
+ /** Called each frame when object updates */
8469
+ onUpdate() {}
8470
+
8120
8471
  /** Called when the mouse enters the object */
8121
8472
  onEnter() {}
8122
8473
 
@@ -8171,8 +8522,9 @@ class UIText extends UIObject
8171
8522
  }
8172
8523
  render()
8173
8524
  {
8525
+ // only render the text
8174
8526
  const textSize = this.getTextSize();
8175
- uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
8527
+ uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
8176
8528
  }
8177
8529
  }
8178
8530
 
@@ -8248,7 +8600,7 @@ class UIButton extends UIObject
8248
8600
  // draw the text scaled to fit
8249
8601
  const textSize = this.getTextSize();
8250
8602
  uiSystem.drawText(this.text, this.pos, textSize,
8251
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8603
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8252
8604
  }
8253
8605
  }
8254
8606
 
@@ -8302,7 +8654,7 @@ class UICheckbox extends UIObject
8302
8654
  const textSize = this.getTextSize();
8303
8655
  const pos = this.pos.add(vec2(this.size.x,0));
8304
8656
  uiSystem.drawText(this.text, pos, textSize,
8305
- this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
8657
+ this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
8306
8658
  }
8307
8659
  }
8308
8660
 
@@ -8357,9 +8709,11 @@ class UIScrollbar extends UIObject
8357
8709
  const p1 = centerPos - handleWidth/2;
8358
8710
  const p2 = centerPos + handleWidth/2;
8359
8711
  const oldValue = this.value;
8712
+
8713
+ const p = uiSystem.screenToNative(mousePosScreen);
8360
8714
  this.value = isHorizontal ?
8361
- percent(mousePosScreen.x, p1, p2) :
8362
- percent(mousePosScreen.y, p2, p1);
8715
+ percent(p.x, p1, p2) :
8716
+ percent(p.y, p2, p1);
8363
8717
  this.value === oldValue || this.onChange();
8364
8718
  }
8365
8719
  }
@@ -8381,20 +8735,20 @@ class UIScrollbar extends UIObject
8381
8735
  vec2(lerp(p1, p2, this.value), this.pos.y) :
8382
8736
  vec2(this.pos.x, lerp(p2, p1, this.value))
8383
8737
  const handleColor = this.disabled ? this.disabledColor : this.handleColor;
8384
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
8738
+ uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
8385
8739
 
8386
8740
  // draw the text scaled to fit on the scrollbar
8387
8741
  const textSize = this.getTextSize();
8388
8742
  uiSystem.drawText(this.text, this.pos, textSize,
8389
- this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
8743
+ this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
8390
8744
  }
8391
8745
  }
8392
8746
  /**
8393
8747
  * LittleJS Box2D Physics Plugin
8394
8748
  * - Box2dObject extends EngineObject with Box2D physics
8395
- * - Call box2dInit() before engineInit() to enable
8749
+ * - Call box2dInit() to enable
8396
8750
  * - You will also need to include box2d.wasm.js
8397
- * - Uses a super fast web assembly port of Box2D
8751
+ * - Uses a super fast web assembly port of Box2D v2.3.1
8398
8752
  * - More info: https://github.com/kripken/box2d.js
8399
8753
  * - Functions to create polygon, circle, and edge shapes
8400
8754
  * - Contact begin and end callbacks
@@ -8424,9 +8778,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
8424
8778
  ///////////////////////////////////////////////////////////////////////////////
8425
8779
  /**
8426
8780
  * Box2D Object - extend with your own custom physics objects
8427
- * - A LittleJS object with Box2D physics
8428
- * - Each object has a Box2D body which can have multiple fixtures and joints
8781
+ * - A LittleJS object with Box2D physics, dynamic by default
8429
8782
  * - Provides interface for Box2D body and fixture functions
8783
+ * - Each object can have multiple fixtures and joints
8430
8784
  * @extends EngineObject
8431
8785
  * @memberof Box2D
8432
8786
  */
@@ -8440,7 +8794,7 @@ class Box2dObject extends EngineObject
8440
8794
  * @param {Color} [color]
8441
8795
  * @param {number} [bodyType]
8442
8796
  * @param {number} [renderOrder] */
8443
- constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8797
+ constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
8444
8798
  {
8445
8799
  super(pos, size, tileInfo, angle, color, renderOrder);
8446
8800
 
@@ -8452,24 +8806,27 @@ class Box2dObject extends EngineObject
8452
8806
  this.body = box2d.world.CreateBody(bodyDef);
8453
8807
  this.body.object = this;
8454
8808
  this.lineColor = BLACK;
8809
+ box2d.objects.push(this);
8810
+
8811
+ // edge lists and loops for drawing
8812
+ this.edgeLists = [];
8813
+ this.edgeLoops = [];
8455
8814
  }
8456
8815
 
8457
8816
  /** Destroy this object and its physics body */
8458
8817
  destroy()
8459
8818
  {
8819
+ if (this.destroyed)
8820
+ return;
8821
+
8460
8822
  // destroy physics body, fixtures, and joints
8461
- this.body && box2d.world.DestroyBody(this.body);
8462
- this.body = 0;
8823
+ ASSERT(this.body, 'Box2dObject has no body to destroy');
8824
+ box2d.world.DestroyBody(this.body);
8463
8825
  super.destroy();
8464
8826
  }
8465
8827
 
8466
- /** Copy box2d update sim data */
8467
- update()
8468
- {
8469
- // use box2d physics update instead of normal engine update
8470
- this.pos = box2d.vec2From(this.body.GetPosition());
8471
- this.angle = -this.body.GetAngle();
8472
- }
8828
+ /** Box2d objects updated with Box2d world step */
8829
+ updatePhysics() {}
8473
8830
 
8474
8831
  /** Render the object, uses box2d drawing if no tile info exists */
8475
8832
  render()
@@ -8495,10 +8852,23 @@ class Box2dObject extends EngineObject
8495
8852
  * @param {Color} [lineColor]
8496
8853
  * @param {number} [lineWidth]
8497
8854
  * @param {CanvasRenderingContext2D} [context] */
8498
- drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
8855
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
8499
8856
  {
8500
- this.getFixtureList().forEach(fixture=>
8501
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context));
8857
+ // draw non-edge fixtures
8858
+ this.getFixtureList().forEach((fixture)=>
8859
+ {
8860
+ const shape = box2d.castObjectType(fixture.GetShape());
8861
+ if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
8862
+ {
8863
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
8864
+ }
8865
+ });
8866
+
8867
+ // draw edges using a single draw line for better connections
8868
+ this.edgeLists.forEach(points=>
8869
+ drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
8870
+ this.edgeLoops.forEach(points=>
8871
+ drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
8502
8872
  }
8503
8873
 
8504
8874
  ///////////////////////////////////////////////////////////////////////////////
@@ -8523,6 +8893,10 @@ class Box2dObject extends EngineObject
8523
8893
  * @param {boolean} [isSensor] */
8524
8894
  addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
8525
8895
  {
8896
+ ASSERT(isNumber(density), 'density must be a number');
8897
+ ASSERT(isNumber(friction), 'friction must be a number');
8898
+ ASSERT(isNumber(restitution), 'restitution must be a number');
8899
+
8526
8900
  const fd = new box2d.instance.b2FixtureDef();
8527
8901
  fd.set_shape(shape);
8528
8902
  fd.set_density(density);
@@ -8542,6 +8916,11 @@ class Box2dObject extends EngineObject
8542
8916
  * @param {boolean} [isSensor] */
8543
8917
  addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
8544
8918
  {
8919
+ ASSERT(isVector2(size), 'size must be a Vector2');
8920
+ ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
8921
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
8922
+ ASSERT(isNumber(angle), 'angle must be a number');
8923
+
8545
8924
  const shape = new box2d.instance.b2PolygonShape();
8546
8925
  shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
8547
8926
  return this.addShape(shape, density, friction, restitution, isSensor);
@@ -8555,6 +8934,8 @@ class Box2dObject extends EngineObject
8555
8934
  * @param {boolean} [isSensor] */
8556
8935
  addPoly(points, density, friction, restitution, isSensor)
8557
8936
  {
8937
+ ASSERT(isArray(points), 'points must be an array');
8938
+
8558
8939
  function box2dCreatePolygonShape(points)
8559
8940
  {
8560
8941
  function box2dCreatePointList(points)
@@ -8590,6 +8971,9 @@ class Box2dObject extends EngineObject
8590
8971
  * @param {boolean} [isSensor] */
8591
8972
  addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
8592
8973
  {
8974
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
8975
+ ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
8976
+
8593
8977
  const points = [];
8594
8978
  const radius = diameter/2;
8595
8979
  for (let i=sides; i--;)
@@ -8605,6 +8989,8 @@ class Box2dObject extends EngineObject
8605
8989
  * @param {boolean} [isSensor] */
8606
8990
  addRandomPoly(diameter=1, density, friction, restitution, isSensor)
8607
8991
  {
8992
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
8993
+
8608
8994
  const sides = randInt(3, 9);
8609
8995
  const points = [];
8610
8996
  const radius = diameter/2;
@@ -8622,6 +9008,9 @@ class Box2dObject extends EngineObject
8622
9008
  * @param {boolean} [isSensor] */
8623
9009
  addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
8624
9010
  {
9011
+ ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
9012
+ ASSERT(isVector2(offset), 'offset must be a Vector2');
9013
+
8625
9014
  const shape = new box2d.instance.b2CircleShape();
8626
9015
  shape.set_m_p(box2d.vec2dTo(offset));
8627
9016
  shape.set_m_radius(diameter/2);
@@ -8637,53 +9026,63 @@ class Box2dObject extends EngineObject
8637
9026
  * @param {boolean} [isSensor] */
8638
9027
  addEdge(point1, point2, density, friction, restitution, isSensor)
8639
9028
  {
9029
+ ASSERT(isVector2(point1), 'point1 must be a Vector2');
9030
+ ASSERT(isVector2(point2), 'point2 must be a Vector2');
9031
+
8640
9032
  const shape = new box2d.instance.b2EdgeShape();
8641
9033
  shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
8642
9034
  return this.addShape(shape, density, friction, restitution, isSensor);
8643
9035
  }
8644
9036
 
8645
- /** Add an edge loop to the body, an edge loop connects the end points
9037
+ /** Add an edge list to the body
8646
9038
  * @param {Array<Vector2>} points
8647
9039
  * @param {number} [density]
8648
9040
  * @param {number} [friction]
8649
9041
  * @param {number} [restitution]
8650
9042
  * @param {boolean} [isSensor] */
8651
- addEdgeLoop(points, density, friction, restitution, isSensor)
9043
+ addEdgeList(points, density, friction, restitution, isSensor)
8652
9044
  {
8653
- const fixtures = [];
8654
- const getPoint = i=> points[mod(i,points.length)];
8655
- for (let i=0; i<points.length; ++i)
9045
+ ASSERT(isArray(points), 'points must be an array');
9046
+ const fixtures = [], edgePoints = [];
9047
+ for (let i=0; i<points.length-1; ++i)
8656
9048
  {
8657
9049
  const shape = new box2d.instance.b2EdgeShape();
8658
- shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
8659
- shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
8660
- shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
8661
- shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
9050
+ points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
9051
+ points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
9052
+ points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
9053
+ points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
8662
9054
  const f = this.addShape(shape, density, friction, restitution, isSensor);
8663
9055
  fixtures.push(f);
9056
+ edgePoints.push(points[i].copy());
8664
9057
  }
9058
+ edgePoints.push(points[points.length-1].copy());
9059
+ this.edgeLists.push(edgePoints);
8665
9060
  return fixtures;
8666
9061
  }
8667
9062
 
8668
- /** Add an edge list to the body
9063
+ /** Add an edge loop to the body, an edge loop connects the end points
8669
9064
  * @param {Array<Vector2>} points
8670
9065
  * @param {number} [density]
8671
9066
  * @param {number} [friction]
8672
9067
  * @param {number} [restitution]
8673
9068
  * @param {boolean} [isSensor] */
8674
- addEdgeList(points, density, friction, restitution, isSensor)
9069
+ addEdgeLoop(points, density, friction, restitution, isSensor)
8675
9070
  {
8676
- const fixtures = [];
8677
- for (let i=0; i<points.length-1; ++i)
9071
+ ASSERT(isArray(points), 'points must be an array');
9072
+ const fixtures = [], edgePoints = [];
9073
+ const getPoint = i=> points[mod(i,points.length)];
9074
+ for (let i=0; i<points.length; ++i)
8678
9075
  {
8679
9076
  const shape = new box2d.instance.b2EdgeShape();
8680
- points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
8681
- points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
8682
- points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
8683
- points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
9077
+ shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
9078
+ shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
9079
+ shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
9080
+ shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
8684
9081
  const f = this.addShape(shape, density, friction, restitution, isSensor);
8685
9082
  fixtures.push(f);
9083
+ i < points.length && edgePoints.push(points[i].copy());
8686
9084
  }
9085
+ this.edgeLoops.push(edgePoints);
8687
9086
  return fixtures;
8688
9087
  }
8689
9088
 
@@ -8718,6 +9117,10 @@ class Box2dObject extends EngineObject
8718
9117
  * @return {number} */
8719
9118
  getBodyType() { return this.body.GetType(); }
8720
9119
 
9120
+ /** Get the speed of this object
9121
+ * @return {number} */
9122
+ getSpeed() { return this.getLinearVelocity().length(); }
9123
+
8721
9124
  ///////////////////////////////////////////////////////////////////////////////
8722
9125
  // physics set functions
8723
9126
 
@@ -8733,33 +9136,40 @@ class Box2dObject extends EngineObject
8733
9136
 
8734
9137
  /** Sets the position
8735
9138
  * @param {Vector2} pos */
8736
- setPosition(pos) { this.setTransform(pos, this.body.GetAngle()); }
9139
+ setPosition(pos)
9140
+ { this.setTransform(pos, this.body.GetAngle()); }
8737
9141
 
8738
9142
  /** Sets the angle
8739
9143
  * @param {number} angle */
8740
- setAngle(angle) { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
9144
+ setAngle(angle)
9145
+ { this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
8741
9146
 
8742
9147
  /** Sets the linear velocity
8743
9148
  * @param {Vector2} velocity */
8744
- setLinearVelocity(velocity) { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
9149
+ setLinearVelocity(velocity)
9150
+ { this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
8745
9151
 
8746
9152
  /** Sets the angular velocity
8747
9153
  * @param {number} angularVelocity */
8748
- setAngularVelocity(angularVelocity) { this.body.SetAngularVelocity(angularVelocity); }
9154
+ setAngularVelocity(angularVelocity)
9155
+ { this.body.SetAngularVelocity(angularVelocity); }
8749
9156
 
8750
9157
  /** Sets the linear damping
8751
9158
  * @param {number} damping */
8752
- setLinearDamping(damping) { this.body.SetLinearDamping(damping); }
9159
+ setLinearDamping(damping)
9160
+ { this.body.SetLinearDamping(damping); }
8753
9161
 
8754
9162
  /** Sets the angular damping
8755
9163
  * @param {number} damping */
8756
- setAngularDamping(damping) { this.body.SetAngularDamping(damping); }
9164
+ setAngularDamping(damping)
9165
+ { this.body.SetAngularDamping(damping); }
8757
9166
 
8758
9167
  /** Sets the gravity scale
8759
9168
  * @param {number} [scale] */
8760
- setGravityScale(scale=1) { this.body.SetGravityScale(this.gravityScale = scale); }
9169
+ setGravityScale(scale=1)
9170
+ { this.body.SetGravityScale(this.gravityScale = scale); }
8761
9171
 
8762
- /** Should this body be treated like a bullet for continuous collision detection?
9172
+ /** Should be like a bullet for continuous collision detection?
8763
9173
  * @param {boolean} [isBullet] */
8764
9174
  setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
8765
9175
 
@@ -8773,11 +9183,13 @@ class Box2dObject extends EngineObject
8773
9183
 
8774
9184
  /** Set whether the body is allowed to sleep
8775
9185
  * @param {boolean} [isAllowed] */
8776
- setSleepingAllowed(isAllowed=true) { this.body.SetSleepingAllowed(isAllowed); }
9186
+ setSleepingAllowed(isAllowed=true)
9187
+ { this.body.SetSleepingAllowed(isAllowed); }
8777
9188
 
8778
9189
  /** Set whether the body can rotate
8779
9190
  * @param {boolean} [isFixed] */
8780
- setFixedRotation(isFixed=true) { this.body.SetFixedRotation(isFixed); }
9191
+ setFixedRotation(isFixed=true)
9192
+ { this.body.SetFixedRotation(isFixed); }
8781
9193
 
8782
9194
  /** Set the center of mass of the body
8783
9195
  * @param {Vector2} center */
@@ -8789,10 +9201,11 @@ class Box2dObject extends EngineObject
8789
9201
 
8790
9202
  /** Set the moment of inertia of the body
8791
9203
  * @param {number} momentOfInertia */
8792
- setMomentOfInertia(momentOfInertia) { this.setMassData(undefined, undefined, momentOfInertia) }
9204
+ setMomentOfInertia(momentOfInertia)
9205
+ { this.setMassData(undefined, undefined, momentOfInertia) }
8793
9206
 
8794
9207
  /** Reset the mass, center of mass, and moment */
8795
- resetMassData() { this.body.ResetMassData(); }
9208
+ resetMassData() { this.body.ResetMassData(); }
8796
9209
 
8797
9210
  /** Set the mass data of the body
8798
9211
  * @param {Vector2} [localCenter]
@@ -9434,6 +9847,50 @@ class Box2dPrismaticJoint extends Box2dJoint
9434
9847
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
9435
9848
  }
9436
9849
 
9850
+ ///////////////////////////////////////////////////////////////////////////////
9851
+ /**
9852
+ * Box2D Static Object - Box2d with a static physics body
9853
+ * @extends Box2dObject
9854
+ * @memberof Box2D
9855
+ */
9856
+ class Box2dStaticObject extends Box2dObject
9857
+ {
9858
+ /** Create a LittleJS object with Box2d physics
9859
+ * @param {Vector2} [pos]
9860
+ * @param {Vector2} [size]
9861
+ * @param {TileInfo} [tileInfo]
9862
+ * @param {number} [angle]
9863
+ * @param {Color} [color]
9864
+ * @param {number} [renderOrder] */
9865
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
9866
+ {
9867
+ const bodyType = box2d.bodyTypeStatic;
9868
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
9869
+ }
9870
+ }
9871
+
9872
+ ///////////////////////////////////////////////////////////////////////////////
9873
+ /**
9874
+ * Box2D Kiematic Object - Box2d with a kinematic physics body
9875
+ * @extends Box2dObject
9876
+ * @memberof Box2D
9877
+ */
9878
+ class Box2dKiematicObject extends Box2dObject
9879
+ {
9880
+ /** Create a LittleJS object with Box2d physics
9881
+ * @param {Vector2} [pos]
9882
+ * @param {Vector2} [size]
9883
+ * @param {TileInfo} [tileInfo]
9884
+ * @param {number} [angle]
9885
+ * @param {Color} [color]
9886
+ * @param {number} [renderOrder] */
9887
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
9888
+ {
9889
+ const bodyType = box2d.bodyTypeKinematic;
9890
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
9891
+ }
9892
+ }
9893
+
9437
9894
  ///////////////////////////////////////////////////////////////////////////////
9438
9895
  /**
9439
9896
  * Box2D Wheel Joint
@@ -9796,6 +10253,7 @@ class Box2dPlugin
9796
10253
  box2d = this;
9797
10254
  this.instance = instance;
9798
10255
  this.world = new box2d.instance.b2World();
10256
+ this.objects = [];
9799
10257
 
9800
10258
  /** @property {number} - Velocity iterations per update*/
9801
10259
  this.velocityIterations = 8;
@@ -10038,9 +10496,10 @@ class Box2dPlugin
10038
10496
 
10039
10497
  /** converts a box2d vec2 pointer to a Vector2
10040
10498
  * @param {Object} v */
10041
- vec2FromPointer(v)
10499
+ vec2FromPointer(vp)
10042
10500
  {
10043
- return box2d.vec2From(box2d.instance.wrapPointer(v, box2d.instance.b2Vec2));
10501
+ const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
10502
+ return box2d.vec2From(v);
10044
10503
  }
10045
10504
 
10046
10505
  /** converts a Vector2 to a box2 vec2
@@ -10114,8 +10573,23 @@ async function box2dInit()
10114
10573
  // add the box2d plugin to the engine
10115
10574
  function box2dUpdate()
10116
10575
  {
10117
- if (!paused)
10118
- box2d.step();
10576
+ if (paused)
10577
+ return;
10578
+
10579
+ box2d.step();
10580
+
10581
+ // remove destroyed objects
10582
+ box2d.objects = box2d.objects.filter(o=>!o.destroyed);
10583
+
10584
+ // copy box2d physics results to engine objects
10585
+ for (const o of box2d.objects)
10586
+ {
10587
+ if (o.body)
10588
+ {
10589
+ o.pos = box2d.vec2From(o.body.GetPosition());
10590
+ o.angle = -o.body.GetAngle();
10591
+ }
10592
+ }
10119
10593
  }
10120
10594
  function box2dRender()
10121
10595
  {