littlejsengine 1.10.4 → 1.11.1

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 (75) hide show
  1. package/LittleJS.zip +0 -0
  2. package/README.md +8 -78
  3. package/dist/littlejs.d.ts +60 -25
  4. package/dist/littlejs.esm.js +164 -63
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +160 -62
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +160 -62
  9. package/examples/box2d/game.js +15 -4
  10. package/examples/box2d/gameObjects.js +10 -9
  11. package/examples/box2d/index.html +5 -5
  12. package/examples/box2d/scenes.js +4 -4
  13. package/examples/box2d/tiles.png +0 -0
  14. package/examples/breakout/index.html +3 -3
  15. package/examples/breakoutTutorial/index.html +2 -2
  16. package/examples/electron/game.js +3 -0
  17. package/examples/electron/index.html +2 -2
  18. package/examples/htmlMenu/index.html +3 -3
  19. package/examples/index.html +10 -3
  20. package/examples/js13k/game.js +3 -0
  21. package/examples/js13k/index.html +13 -13
  22. package/examples/module/game.js +4 -1
  23. package/examples/module/index.html +1 -1
  24. package/examples/particles/index.html +4 -1
  25. package/examples/platformer/game.js +19 -15
  26. package/examples/platformer/gameCharacter.js +3 -3
  27. package/examples/platformer/gameLevel.js +1 -0
  28. package/examples/platformer/index.html +8 -8
  29. package/examples/platformer/tiles.png +0 -0
  30. package/examples/puzzle/game.js +3 -2
  31. package/examples/puzzle/index.html +2 -2
  32. package/examples/starter/game.js +47 -10
  33. package/examples/starter/index.html +13 -13
  34. package/examples/starter/tiles.png +0 -0
  35. package/examples/stress/index.html +1 -1
  36. package/examples/typescript/game.js +2 -0
  37. package/examples/typescript/game.ts +3 -0
  38. package/examples/typescript/index.html +1 -1
  39. package/examples/uiSystem/game.js +23 -16
  40. package/examples/uiSystem/index.html +3 -14
  41. package/package.json +7 -2
  42. package/plugins/postProcess.js +2 -9
  43. package/plugins/uiSystem.js +85 -25
  44. package/reference.md +1 -1
  45. package/shortExamples/engine.html +71 -0
  46. package/shortExamples/index.html +47 -0
  47. package/shortExamples/index.js +0 -0
  48. package/shortExamples/tiles.png +0 -0
  49. package/src/engine.js +14 -12
  50. package/src/engineAudio.js +6 -9
  51. package/src/engineDraw.js +23 -7
  52. package/src/engineExport.js +4 -1
  53. package/src/engineInput.js +7 -3
  54. package/src/engineMedals.js +19 -5
  55. package/src/engineObject.js +8 -5
  56. package/src/engineSettings.js +13 -2
  57. package/src/engineTileLayer.js +9 -8
  58. package/src/engineUtilities.js +59 -10
  59. package/src/engineWebGL.js +1 -1
  60. package/examples/js13k/build/index.html +0 -1
  61. package/examples/js13k/build/index.js +0 -1
  62. package/examples/js13k/build/tiles.png +0 -0
  63. package/examples/js13k/game - Copy.zip +0 -0
  64. package/examples/js13k/game.zip +0 -0
  65. package/examples/starter/build/index.html +0 -2
  66. package/examples/starter/build/index.js +0 -1
  67. package/examples/starter/build/tiles.png +0 -0
  68. package/examples/starter/game.zip +0 -0
  69. package/examples/typescript/build/build/littlejs.esm.js +0 -4327
  70. package/examples/typescript/build/dist/littlejs.esm.js +0 -4425
  71. package/examples/typescript/build/examples/typescript/build.js +0 -24
  72. package/examples/typescript/build/examples/typescript/game.js +0 -100
  73. package/examples/typescript/build/examples/typescript/test/build/littlejs.esm.js +0 -3934
  74. package/examples/typescript/build/examples/typescript/test/examples/typescript/build.js +0 -86
  75. package/examples/typescript/build/examples/typescript/test/examples/typescript/game.js +0 -92
@@ -671,7 +671,8 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
671
671
  * @memberof Random */
672
672
  function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
673
673
 
674
- /** Returns a floored random value the two values passed in
674
+ /** Returns a floored random value between the two values passed in
675
+ * The upper bound is exclusive. (If 2 is passed in, result will be 0 or 1)
675
676
  * @param {Number} valueA
676
677
  * @param {Number} [valueB]
677
678
  * @return {Number}
@@ -800,18 +801,24 @@ class Vector2
800
801
  * @param {Number} [y] - Y axis location */
801
802
  constructor(x=0, y=0)
802
803
  {
803
- ASSERT(typeof x == 'number' && typeof y == 'number');
804
804
  /** @property {Number} - X axis location */
805
805
  this.x = x;
806
806
  /** @property {Number} - Y axis location */
807
807
  this.y = y;
808
+ ASSERT(this.isValid());
808
809
  }
809
810
 
810
811
  /** Sets values of this vector and returns self
811
812
  * @param {Number} [x] - X axis location
812
813
  * @param {Number} [y] - Y axis location
813
814
  * @return {Vector2} */
814
- set(x=0, y=0) { this.x=x; this.y=y; return this; }
815
+ set(x=0, y=0)
816
+ {
817
+ this.x = x;
818
+ this.y = y;
819
+ ASSERT(this.isValid());
820
+ return this;
821
+ }
815
822
 
816
823
  /** Returns a new vector that is a copy of this
817
824
  * @return {Vector2} */
@@ -1003,6 +1010,14 @@ class Vector2
1003
1010
  if (debug)
1004
1011
  return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1005
1012
  }
1013
+
1014
+ /** Checks if this is a valid vector
1015
+ * @return {Boolean} */
1016
+ isValid()
1017
+ {
1018
+ return typeof this.x == 'number' && !isNaN(this.x)
1019
+ && typeof this.y == 'number' && !isNaN(this.y);
1020
+ }
1006
1021
  }
1007
1022
 
1008
1023
  ///////////////////////////////////////////////////////////////////////////////
@@ -1063,6 +1078,7 @@ class Color
1063
1078
  this.b = b;
1064
1079
  /** @property {Number} - Alpha */
1065
1080
  this.a = a;
1081
+ ASSERT(this.isValid());
1066
1082
  }
1067
1083
 
1068
1084
  /** Sets values of this color and returns self
@@ -1072,7 +1088,14 @@ class Color
1072
1088
  * @param {Number} [a] - alpha
1073
1089
  * @return {Color} */
1074
1090
  set(r=1, g=1, b=1, a=1)
1075
- { this.r=r; this.g=g; this.b=b; this.a=a; return this; }
1091
+ {
1092
+ this.r = r;
1093
+ this.g = g;
1094
+ this.b = b;
1095
+ this.a = a;
1096
+ ASSERT(this.isValid());
1097
+ return this;
1098
+ }
1076
1099
 
1077
1100
  /** Returns a new color that is a copy of this
1078
1101
  * @return {Color} */
@@ -1155,6 +1178,7 @@ class Color
1155
1178
  this.g = f(p, q, h);
1156
1179
  this.b = f(p, q, h - 1/3);
1157
1180
  this.a = a;
1181
+ ASSERT(this.isValid());
1158
1182
  return this;
1159
1183
  }
1160
1184
 
@@ -1182,7 +1206,6 @@ class Color
1182
1206
  else if (b == max)
1183
1207
  h = (r - g) / d + 4;
1184
1208
  }
1185
-
1186
1209
  return [h / 6, s, l, a];
1187
1210
  }
1188
1211
 
@@ -1215,11 +1238,27 @@ class Color
1215
1238
  * @return {Color} */
1216
1239
  setHex(hex)
1217
1240
  {
1218
- const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1219
- this.r = fromHex(1);
1220
- this.g = fromHex(3),
1221
- this.b = fromHex(5);
1222
- this.a = hex.length > 7 ? fromHex(7) : 1;
1241
+ ASSERT(typeof hex == 'string' && hex[0] == '#');
1242
+ ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1243
+
1244
+ if (hex.length < 6)
1245
+ {
1246
+ const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
1247
+ this.r = fromHex(1);
1248
+ this.g = fromHex(2),
1249
+ this.b = fromHex(3);
1250
+ this.a = hex.length == 5 ? fromHex(4) : 1;
1251
+ }
1252
+ else
1253
+ {
1254
+ const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1255
+ this.r = fromHex(1);
1256
+ this.g = fromHex(3),
1257
+ this.b = fromHex(5);
1258
+ this.a = hex.length == 9 ? fromHex(7) : 1;
1259
+ }
1260
+
1261
+ ASSERT(this.isValid());
1223
1262
  return this;
1224
1263
  }
1225
1264
 
@@ -1233,6 +1272,16 @@ class Color
1233
1272
  const a = clamp(this.a)*255<<24;
1234
1273
  return r + g + b + a;
1235
1274
  }
1275
+
1276
+ /** Checks if this is a valid color
1277
+ * @return {Boolean} */
1278
+ isValid()
1279
+ {
1280
+ return typeof this.r == 'number' && !isNaN(this.r)
1281
+ && typeof this.g == 'number' && !isNaN(this.g)
1282
+ && typeof this.b == 'number' && !isNaN(this.b)
1283
+ && typeof this.a == 'number' && !isNaN(this.a);
1284
+ }
1236
1285
  }
1237
1286
 
1238
1287
  ///////////////////////////////////////////////////////////////////////////////
@@ -1384,12 +1433,18 @@ let canvasMaxSize = vec2(1920, 1080);
1384
1433
  * @memberof Settings */
1385
1434
  let canvasFixedSize = vec2();
1386
1435
 
1387
- /** Disables filtering for crisper pixel art if true
1436
+ /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1388
1437
  * @type {Boolean}
1389
1438
  * @default
1390
1439
  * @memberof Settings */
1391
1440
  let canvasPixelated = true;
1392
1441
 
1442
+ /** Disables texture filtering for crisper pixel art
1443
+ * @type {Boolean}
1444
+ * @default
1445
+ * @memberof Settings */
1446
+ let tilesPixelated = true;
1447
+
1393
1448
  /** Default font used for text rendering
1394
1449
  * @type {String}
1395
1450
  * @default
@@ -1638,11 +1693,16 @@ function setCanvasMaxSize(size) { canvasMaxSize = size; }
1638
1693
  * @memberof Settings */
1639
1694
  function setCanvasFixedSize(size) { canvasFixedSize = size; }
1640
1695
 
1641
- /** Disables anti aliasing for pixel art if true
1696
+ /** Use nearest neighbor scaling algorithm for canvas for more pixelated look
1642
1697
  * @param {Boolean} pixelated
1643
1698
  * @memberof Settings */
1644
1699
  function setCanvasPixelated(pixelated) { canvasPixelated = pixelated; }
1645
1700
 
1701
+ /** Disables texture filtering for crisper pixel art
1702
+ * @param {Boolean} pixelated
1703
+ * @memberof Settings */
1704
+ function setTilesPixelated(pixelated) { tilesPixelated = pixelated; }
1705
+
1646
1706
  /** Set default font used for text rendering
1647
1707
  * @param {String} font
1648
1708
  * @memberof Settings */
@@ -1868,7 +1928,7 @@ class EngineObject
1868
1928
  * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
1869
1929
  * @param {Number} [renderOrder] - Objects sorted by renderOrder before being rendered
1870
1930
  */
1871
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color, renderOrder=0)
1931
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
1872
1932
  {
1873
1933
  // set passed in params
1874
1934
  ASSERT(isVector2(pos) && isVector2(size), 'ensure pos and size are vec2s');
@@ -1982,15 +2042,18 @@ class EngineObject
1982
2042
 
1983
2043
  // apply physics
1984
2044
  const oldPos = this.pos.copy();
1985
- this.pos.x += this.velocity.x *= this.damping;
1986
- this.pos.y += this.velocity.y = this.damping * this.velocity.y
1987
- + gravity * this.gravityScale;
2045
+ this.velocity.x *= this.damping;
2046
+ this.velocity.y *= this.damping;
2047
+ if (this.mass) // dont apply gravity to static objects
2048
+ this.velocity.y += gravity * this.gravityScale;
2049
+ this.pos.x += this.velocity.x;
2050
+ this.pos.y += this.velocity.y;
1988
2051
  this.angle += this.angleVelocity *= this.angleDamping;
1989
2052
 
1990
2053
  // physics sanity checks
1991
2054
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1992
2055
  ASSERT(this.damping >= 0 && this.damping <= 1);
1993
- if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
2056
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for static objects
1994
2057
  return;
1995
2058
 
1996
2059
  const wasMovingDown = this.velocity.y < 0;
@@ -2351,7 +2414,7 @@ let drawCount;
2351
2414
  * tile(2) // a tile at index 2 using the default tile size of 16
2352
2415
  * tile(5, 8) // a tile at index 5 using a tile size of 8
2353
2416
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
2354
- * tile(vec2(4,8), vec2(30,10)) // a tile at pixel location (4,8) with a size of (30,10)
2417
+ * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
2355
2418
  * @memberof Draw
2356
2419
  */
2357
2420
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
@@ -2696,6 +2759,24 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2696
2759
  }
2697
2760
  }
2698
2761
 
2762
+ /** Draw text on main canvas in world space
2763
+ * Automatically splits new lines into rows
2764
+ * @param {String} text
2765
+ * @param {Vector2} pos
2766
+ * @param {Number} [size]
2767
+ * @param {Color} [color=(1,1,1,1)]
2768
+ * @param {Number} [lineWidth]
2769
+ * @param {Color} [lineColor=(0,0,0,1)]
2770
+ * @param {CanvasTextAlign} [textAlign='center']
2771
+ * @param {String} [font=fontDefault]
2772
+ * @param {Number} [maxWidth]
2773
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
2774
+ * @memberof Draw */
2775
+ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth, context=mainContext)
2776
+ {
2777
+ drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, maxWidth, context);
2778
+ }
2779
+
2699
2780
  /** Draw text on overlay canvas in world space
2700
2781
  * Automatically splits new lines into rows
2701
2782
  * @param {String} text
@@ -2706,12 +2787,11 @@ function setBlendMode(additive, useWebGL=glEnable, context)
2706
2787
  * @param {Color} [lineColor=(0,0,0,1)]
2707
2788
  * @param {CanvasTextAlign} [textAlign='center']
2708
2789
  * @param {String} [font=fontDefault]
2709
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2710
2790
  * @param {Number} [maxWidth]
2711
2791
  * @memberof Draw */
2712
- function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context, maxWidth)
2792
+ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, maxWidth)
2713
2793
  {
2714
- drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, context, maxWidth);
2794
+ drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, maxWidth, overlayContext);
2715
2795
  }
2716
2796
 
2717
2797
  /** Draw text on overlay canvas in screen space
@@ -2724,10 +2804,10 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
2724
2804
  * @param {Color} [lineColor=(0,0,0,1)]
2725
2805
  * @param {CanvasTextAlign} [textAlign]
2726
2806
  * @param {String} [font=fontDefault]
2727
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2728
2807
  * @param {Number} [maxWidth]
2808
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
2729
2809
  * @memberof Draw */
2730
- function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext, maxWidth=undefined)
2810
+ function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, maxWidth=undefined, context=overlayContext)
2731
2811
  {
2732
2812
  context.fillStyle = color.toString();
2733
2813
  context.lineWidth = lineWidth;
@@ -2803,7 +2883,6 @@ class FontImage
2803
2883
  {
2804
2884
  const context = this.context;
2805
2885
  context.save();
2806
- context.imageSmoothingEnabled = !canvasPixelated;
2807
2886
 
2808
2887
  const size = this.tileSize;
2809
2888
  const drawSize = size.add(this.paddingSize).scale(scale);
@@ -2900,7 +2979,7 @@ function keyWasReleased(key, device=0)
2900
2979
 
2901
2980
  /** Clears all input
2902
2981
  * @memberof Input */
2903
- function clearInput() { inputData = [[]]; }
2982
+ function clearInput() { inputData = [[]]; touchGamepadButtons = []; }
2904
2983
 
2905
2984
  /** Returns true if mouse button is down
2906
2985
  * @function
@@ -3065,6 +3144,7 @@ function inputInit()
3065
3144
  onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
3066
3145
  onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
3067
3146
  oncontextmenu = (e)=> false; // prevent right click menu
3147
+ onblur = (e) => clearInput(); // reset input when focus is lost
3068
3148
 
3069
3149
  // init touch input
3070
3150
  if (isTouchDevice && touchInputEnable)
@@ -3263,10 +3343,13 @@ function touchInputInit()
3263
3343
  if (touching)
3264
3344
  {
3265
3345
  touchGamepadTimer.set();
3266
- if (paused)
3346
+ if (paused && !wasTouching)
3267
3347
  {
3268
3348
  // touch anywhere to press start when paused
3269
3349
  touchGamepadButtons[9] = 1;
3350
+
3351
+ // call default touch handler so normal touch events still work
3352
+ handleTouchDefault(e);
3270
3353
  return;
3271
3354
  }
3272
3355
  }
@@ -3291,7 +3374,7 @@ function touchInputInit()
3291
3374
  const button = touchPos.subtract(buttonCenter).direction();
3292
3375
  touchGamepadButtons[button] = 1;
3293
3376
  }
3294
- else if (touchPos.distance(startCenter) < touchGamepadSize)
3377
+ else if (touchPos.distance(startCenter) < touchGamepadSize && !wasTouching)
3295
3378
  {
3296
3379
  // virtual start button in center
3297
3380
  touchGamepadButtons[9] = 1;
@@ -3379,7 +3462,7 @@ function touchGamepadRender()
3379
3462
  /** Audio context used by the engine
3380
3463
  * @type {AudioContext}
3381
3464
  * @memberof Audio */
3382
- let audioContext;
3465
+ let audioContext = new AudioContext;
3383
3466
 
3384
3467
  /** Master gain node for all audio to pass through
3385
3468
  * @type {GainNode}
@@ -3390,14 +3473,10 @@ function audioInit()
3390
3473
  {
3391
3474
  if (!soundEnable || headlessMode) return;
3392
3475
 
3393
- // create audio context
3394
- audioContext = new AudioContext;
3395
-
3396
- // create and connect gain node
3397
3476
  // (createGain is more widely spported then GainNode construtor)
3398
3477
  audioGainNode = audioContext.createGain();
3399
3478
  audioGainNode.connect(audioContext.destination);
3400
- setSoundVolume(soundVolume); // update gain volume
3479
+ audioGainNode.gain.value = soundVolume; // set starting value
3401
3480
  }
3402
3481
 
3403
3482
  ///////////////////////////////////////////////////////////////////////////////
@@ -3437,7 +3516,7 @@ class Sound
3437
3516
  {
3438
3517
  // generate zzfx sound now for fast playback
3439
3518
  const defaultRandomness = .05;
3440
- this.randomness = zzfxSound[1] || defaultRandomness;
3519
+ this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
3441
3520
  zzfxSound[1] = 0; // generate without randomness
3442
3521
  this.sampleChannels = [zzfxG(...zzfxSound)];
3443
3522
  this.sampleRate = zzfxR;
@@ -3479,10 +3558,11 @@ class Sound
3479
3558
  // play the sound
3480
3559
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
3481
3560
  this.gainNode = audioContext.createGain();
3482
- return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
3561
+ this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
3562
+ return this.source;
3483
3563
  }
3484
3564
 
3485
- /** Set the sound volume
3565
+ /** Set the sound volume of the most recently played instance of this sound
3486
3566
  * @param {Number} [volume] - How much to scale volume by
3487
3567
  */
3488
3568
  setVolume(volume=1)
@@ -3973,18 +4053,18 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
3973
4053
 
3974
4054
 
3975
4055
 
3976
- /** The tile collision layer array, use setTileCollisionData and getTileCollisionData to access
4056
+ /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
3977
4057
  * @type {Array}
3978
4058
  * @memberof TileCollision */
3979
4059
  let tileCollision = [];
3980
4060
 
3981
- /** Size of the tile collision layer
4061
+ /** Size of the tile collision layer 2d grid
3982
4062
  * @type {Vector2}
3983
4063
  * @memberof TileCollision */
3984
4064
  let tileCollisionSize = vec2();
3985
4065
 
3986
4066
  /** Clear and initialize tile collision
3987
- * @param {Vector2} size
4067
+ * @param {Vector2} size - width and height of tile collision 2d grid
3988
4068
  * @memberof TileCollision */
3989
4069
  function initTileCollision(size)
3990
4070
  {
@@ -3994,7 +4074,7 @@ function initTileCollision(size)
3994
4074
  tileCollision[i] = 0;
3995
4075
  }
3996
4076
 
3997
- /** Set tile collision data
4077
+ /** Set tile collision data for a given cell in the grid
3998
4078
  * @param {Vector2} pos
3999
4079
  * @param {Number} [data]
4000
4080
  * @memberof TileCollision */
@@ -4003,7 +4083,7 @@ function setTileCollisionData(pos, data=0)
4003
4083
  pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
4004
4084
  }
4005
4085
 
4006
- /** Get tile collision data
4086
+ /** Get tile collision data for a given cell in the grid
4007
4087
  * @param {Vector2} pos
4008
4088
  * @return {Number}
4009
4089
  * @memberof TileCollision */
@@ -4034,7 +4114,8 @@ function tileCollisionTest(pos, size=vec2(), object)
4034
4114
  return false;
4035
4115
  }
4036
4116
 
4037
- /** Return the center of first tile hit (does not return the exact intersection)
4117
+ /** Return the center of first tile hit, undefined if nothing was hit.
4118
+ * This does not return the exact intersection, but the center of the tile hit.
4038
4119
  * @param {Vector2} posStart
4039
4120
  * @param {Vector2} posEnd
4040
4121
  * @param {EngineObject} [object]
@@ -4242,7 +4323,7 @@ class TileLayer extends EngineObject
4242
4323
  }
4243
4324
 
4244
4325
  // disable smoothing for pixel art
4245
- this.context.imageSmoothingEnabled = !canvasPixelated;
4326
+ this.context.imageSmoothingEnabled = !tilesPixelated;
4246
4327
 
4247
4328
  // setup gl rendering if enabled
4248
4329
  glPreRender();
@@ -4279,8 +4360,8 @@ class TileLayer extends EngineObject
4279
4360
  const d = this.getData(layerPos);
4280
4361
  if (d.tile != undefined)
4281
4362
  {
4282
- const pos = this.pos.add(layerPos).add(vec2(.5));
4283
4363
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
4364
+ const pos = layerPos.add(vec2(.5));
4284
4365
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex);
4285
4366
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
4286
4367
  }
@@ -4709,7 +4790,7 @@ function medalsInit(saveName)
4709
4790
  // check if medals are unlocked
4710
4791
  medalsSaveName = saveName;
4711
4792
  if (!debugMedals)
4712
- medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4793
+ medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
4713
4794
 
4714
4795
  // engine automatically renders medals
4715
4796
  engineAddPlugin(undefined, medalsRender);
@@ -4772,14 +4853,28 @@ class Medal
4772
4853
  constructor(id, name, description='', icon='🏆', src)
4773
4854
  {
4774
4855
  ASSERT(id >= 0 && !medals[id]);
4775
-
4776
- // save attributes and add to list of medals
4777
- medals[this.id = id] = this;
4856
+
4857
+ /** @property {Number} - The unique identifier of the medal */
4858
+ this.id = id;
4859
+
4860
+ /** @property {String} - Name of the medal */
4778
4861
  this.name = name;
4862
+
4863
+ /** @property {String} - Description of the medal */
4779
4864
  this.description = description;
4865
+
4866
+ /** @property {String} - Icon for the medal */
4780
4867
  this.icon = icon;
4868
+
4869
+ /** @property {Boolean} - Is the medal unlocked? */
4870
+ this.unlocked = false;
4871
+
4872
+ // load the source image if provided
4781
4873
  if (src)
4782
4874
  (this.image = new Image).src = src;
4875
+
4876
+ // add this to list of medals
4877
+ medals[id] = this;
4783
4878
  }
4784
4879
 
4785
4880
  /** Unlocks a medal if not already unlocked */
@@ -4790,7 +4885,7 @@ class Medal
4790
4885
 
4791
4886
  // save the medal
4792
4887
  ASSERT(medalsSaveName, 'save name must be set');
4793
- localStorage[this.storageKey()] = this.unlocked = 1;
4888
+ localStorage[this.storageKey()] = this.unlocked = true;
4794
4889
  medalsDisplayQueue.push(this);
4795
4890
  }
4796
4891
 
@@ -5049,7 +5144,7 @@ function glCreateTexture(image)
5049
5144
  }
5050
5145
 
5051
5146
  // use point filtering for pixelated rendering
5052
- const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
5147
+ const filter = tilesPixelated ? gl_NEAREST : gl_LINEAR;
5053
5148
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
5054
5149
  glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
5055
5150
  return texture;
@@ -5198,7 +5293,7 @@ const engineName = 'LittleJS';
5198
5293
  * @type {String}
5199
5294
  * @default
5200
5295
  * @memberof Engine */
5201
- const engineVersion = '1.10.4';
5296
+ const engineVersion = '1.11.1';
5202
5297
 
5203
5298
  /** Frames per second to update
5204
5299
  * @type {Number}
@@ -5261,6 +5356,8 @@ const pluginUpdateList = [], pluginRenderList = [];
5261
5356
  * @memberof Engine */
5262
5357
  function engineAddPlugin(updateFunction, renderFunction)
5263
5358
  {
5359
+ ASSERT(!pluginUpdateList.includes(updateFunction));
5360
+ ASSERT(!pluginRenderList.includes(renderFunction));
5264
5361
  updateFunction && pluginUpdateList.push(updateFunction);
5265
5362
  renderFunction && pluginRenderList.push(renderFunction);
5266
5363
  }
@@ -5269,12 +5366,12 @@ function engineAddPlugin(updateFunction, renderFunction)
5269
5366
  // Main engine functions
5270
5367
 
5271
5368
  /** Startup LittleJS engine with your callback functions
5272
- * @param {Function} gameInit - Called once after the engine starts up, setup the game
5273
- * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
5274
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
5275
- * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
5276
- * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
5277
- * @param {Array} [imageSources=[]] - Image to load
5369
+ * @param {Function|function():Promise} gameInit - Called once after the engine starts up
5370
+ * @param {Function} gameUpdate - Called every frame before objects are updated
5371
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
5372
+ * @param {Function} gameRender - Called before objects are rendered, for drawing the background
5373
+ * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
5374
+ * @param {Array} [imageSources=[]] - List of images to load
5278
5375
  * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
5279
5376
  * @memberof Engine */
5280
5377
  function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
@@ -5288,7 +5385,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5288
5385
  mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
5289
5386
 
5290
5387
  // disable smoothing for pixel art
5291
- mainContext.imageSmoothingEnabled = !canvasPixelated;
5388
+ overlayContext.imageSmoothingEnabled =
5389
+ mainContext.imageSmoothingEnabled = !tilesPixelated;
5292
5390
 
5293
5391
  // setup gl rendering if enabled
5294
5392
  glPreRender();
@@ -5424,8 +5522,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5424
5522
 
5425
5523
  function startEngine()
5426
5524
  {
5427
- gameInit();
5428
- engineUpdate();
5525
+ new Promise((resolve) => resolve(gameInit())).then(engineUpdate);
5429
5526
  }
5430
5527
 
5431
5528
  if (headlessMode)
@@ -5440,9 +5537,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5440
5537
  'width:100vw;height:100vh;' + // fill the window
5441
5538
  'display:flex;' + // use flexbox
5442
5539
  'align-items:center;' + // horizontal center
5443
- (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5444
5540
  'justify-content:center;' + // vertical center
5445
5541
  'background:#000;' + // set background color
5542
+ (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5446
5543
  'user-select:none;' + // prevent hold to select
5447
5544
  '-webkit-user-select:none;' + // compatibility for ios
5448
5545
  (!touchInputEnable ? '' : // no touch css setttings
@@ -5778,7 +5875,8 @@ function drawEngineSplashScreen(t)
5778
5875
  }
5779
5876
 
5780
5877
  x.restore();
5781
- }
5878
+ }
5879
+
5782
5880
 
5783
5881
  /**
5784
5882
  * LittleJS Module Export
@@ -5830,6 +5928,7 @@ export {
5830
5928
  canvasMaxSize,
5831
5929
  canvasFixedSize,
5832
5930
  canvasPixelated,
5931
+ tilesPixelated,
5833
5932
  fontDefault,
5834
5933
  showSplashScreen,
5835
5934
  headlessMode,
@@ -5869,6 +5968,7 @@ export {
5869
5968
  setCanvasMaxSize,
5870
5969
  setCanvasFixedSize,
5871
5970
  setCanvasPixelated,
5971
+ setTilesPixelated,
5872
5972
  setFontDefault,
5873
5973
  setShowSplashScreen,
5874
5974
  setHeadlessMode,
@@ -5978,8 +6078,9 @@ export {
5978
6078
  drawCircle,
5979
6079
  drawCanvas2D,
5980
6080
  setBlendMode,
5981
- drawTextScreen,
5982
6081
  drawText,
6082
+ drawTextOverlay,
6083
+ drawTextScreen,
5983
6084
  engineFontImage,
5984
6085
  FontImage,
5985
6086
  isFullscreen,