littlejsengine 1.10.2 → 1.10.7

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 (53) hide show
  1. package/README.md +15 -93
  2. package/dist/littlejs.d.ts +79 -17
  3. package/dist/littlejs.esm.js +179 -90
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +165 -90
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +165 -90
  8. package/examples/box2d/game.js +15 -1
  9. package/examples/box2d/gameObjects.js +10 -9
  10. package/examples/box2d/index.html +5 -5
  11. package/examples/box2d/scenes.js +4 -4
  12. package/examples/box2d/tiles.png +0 -0
  13. package/examples/breakout/index.html +3 -3
  14. package/examples/breakoutTutorial/README.md +9 -7
  15. package/examples/breakoutTutorial/game.js +6 -6
  16. package/examples/breakoutTutorial/index.html +2 -2
  17. package/examples/electron/game.js +3 -0
  18. package/examples/electron/index.html +2 -2
  19. package/examples/htmlMenu/index.html +3 -3
  20. package/examples/js13k/game.js +3 -0
  21. package/examples/js13k/index.html +13 -13
  22. package/examples/module/game.js +3 -0
  23. package/examples/module/index.html +1 -1
  24. package/examples/particles/index.html +4 -1
  25. package/examples/platformer/game.js +21 -15
  26. package/examples/platformer/gameCharacter.js +15 -14
  27. package/examples/platformer/gameEffects.js +3 -4
  28. package/examples/platformer/gameLevel.js +18 -33
  29. package/examples/platformer/gameObjects.js +9 -14
  30. package/examples/platformer/index.html +8 -8
  31. package/examples/platformer/tiles.png +0 -0
  32. package/examples/puzzle/index.html +2 -2
  33. package/examples/starter/game.js +3 -0
  34. package/examples/starter/index.html +13 -13
  35. package/examples/stress/index.html +1 -1
  36. package/examples/typescript/game.js +2 -0
  37. package/examples/typescript/game.ts +4 -1
  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 +1 -1
  42. package/plugins/postProcess.js +1 -1
  43. package/plugins/uiSystem.js +84 -24
  44. package/src/engine.js +14 -22
  45. package/src/engineAudio.js +7 -13
  46. package/src/engineDraw.js +20 -19
  47. package/src/engineExport.js +14 -0
  48. package/src/engineInput.js +8 -6
  49. package/src/engineMedals.js +19 -5
  50. package/src/engineObject.js +9 -5
  51. package/src/engineTileLayer.js +10 -8
  52. package/src/engineUtilities.js +60 -10
  53. package/src/engineWebGL.js +17 -2
package/dist/littlejs.js CHANGED
@@ -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} */
@@ -953,6 +960,7 @@ class Vector2
953
960
  * @param {Number} [length] */
954
961
  setDirection(direction, length=1)
955
962
  {
963
+ direction = mod(direction, 4);
956
964
  ASSERT(direction==0 || direction==1 || direction==2 || direction==3);
957
965
  return vec2(direction%2 ? direction-1 ? -length : length : 0,
958
966
  direction%2 ? 0 : direction ? -length : length);
@@ -1002,6 +1010,14 @@ class Vector2
1002
1010
  if (debug)
1003
1011
  return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
1004
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
+ }
1005
1021
  }
1006
1022
 
1007
1023
  ///////////////////////////////////////////////////////////////////////////////
@@ -1062,6 +1078,7 @@ class Color
1062
1078
  this.b = b;
1063
1079
  /** @property {Number} - Alpha */
1064
1080
  this.a = a;
1081
+ ASSERT(this.isValid());
1065
1082
  }
1066
1083
 
1067
1084
  /** Sets values of this color and returns self
@@ -1071,7 +1088,14 @@ class Color
1071
1088
  * @param {Number} [a] - alpha
1072
1089
  * @return {Color} */
1073
1090
  set(r=1, g=1, b=1, a=1)
1074
- { 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
+ }
1075
1099
 
1076
1100
  /** Returns a new color that is a copy of this
1077
1101
  * @return {Color} */
@@ -1154,6 +1178,7 @@ class Color
1154
1178
  this.g = f(p, q, h);
1155
1179
  this.b = f(p, q, h - 1/3);
1156
1180
  this.a = a;
1181
+ ASSERT(this.isValid());
1157
1182
  return this;
1158
1183
  }
1159
1184
 
@@ -1181,7 +1206,6 @@ class Color
1181
1206
  else if (b == max)
1182
1207
  h = (r - g) / d + 4;
1183
1208
  }
1184
-
1185
1209
  return [h / 6, s, l, a];
1186
1210
  }
1187
1211
 
@@ -1214,11 +1238,27 @@ class Color
1214
1238
  * @return {Color} */
1215
1239
  setHex(hex)
1216
1240
  {
1217
- const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
1218
- this.r = fromHex(1);
1219
- this.g = fromHex(3),
1220
- this.b = fromHex(5);
1221
- 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());
1222
1262
  return this;
1223
1263
  }
1224
1264
 
@@ -1232,6 +1272,16 @@ class Color
1232
1272
  const a = clamp(this.a)*255<<24;
1233
1273
  return r + g + b + a;
1234
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
+ }
1235
1285
  }
1236
1286
 
1237
1287
  ///////////////////////////////////////////////////////////////////////////////
@@ -1867,7 +1917,7 @@ class EngineObject
1867
1917
  * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
1868
1918
  * @param {Number} [renderOrder] - Objects sorted by renderOrder before being rendered
1869
1919
  */
1870
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color, renderOrder=0)
1920
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
1871
1921
  {
1872
1922
  // set passed in params
1873
1923
  ASSERT(isVector2(pos) && isVector2(size), 'ensure pos and size are vec2s');
@@ -1981,15 +2031,18 @@ class EngineObject
1981
2031
 
1982
2032
  // apply physics
1983
2033
  const oldPos = this.pos.copy();
1984
- this.pos.x += this.velocity.x *= this.damping;
1985
- this.pos.y += this.velocity.y = this.damping * this.velocity.y
1986
- + gravity * this.gravityScale;
2034
+ this.velocity.x *= this.damping;
2035
+ this.velocity.y *= this.damping;
2036
+ if (this.mass) // dont apply gravity to static objects
2037
+ this.velocity.y += gravity * this.gravityScale;
2038
+ this.pos.x += this.velocity.x;
2039
+ this.pos.y += this.velocity.y;
1987
2040
  this.angle += this.angleVelocity *= this.angleDamping;
1988
2041
 
1989
2042
  // physics sanity checks
1990
2043
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1991
2044
  ASSERT(this.damping >= 0 && this.damping <= 1);
1992
- if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
2045
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for static objects
1993
2046
  return;
1994
2047
 
1995
2048
  const wasMovingDown = this.velocity.y < 0;
@@ -2135,6 +2188,7 @@ class EngineObject
2135
2188
  this.pos.x = oldPos.x;
2136
2189
  this.velocity.x *= -this.elasticity;
2137
2190
  }
2191
+ debugOverlay && debugPhysics && debugRect(this.pos, this.size, '#f00');
2138
2192
  }
2139
2193
  }
2140
2194
  }
@@ -2349,7 +2403,7 @@ let drawCount;
2349
2403
  * tile(2) // a tile at index 2 using the default tile size of 16
2350
2404
  * tile(5, 8) // a tile at index 5 using a tile size of 8
2351
2405
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
2352
- * tile(vec2(4,8), vec2(30,10)) // a tile at pixel location (4,8) with a size of (30,10)
2406
+ * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
2353
2407
  * @memberof Draw
2354
2408
  */
2355
2409
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
@@ -2567,6 +2621,22 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2567
2621
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2568
2622
  }
2569
2623
 
2624
+ /** Draw colored line between two points
2625
+ * @param {Vector2} posA
2626
+ * @param {Vector2} posB
2627
+ * @param {Number} [thickness]
2628
+ * @param {Color} [color=(1,1,1,1)]
2629
+ * @param {Boolean} [useWebGL=glEnable]
2630
+ * @param {Boolean} [screenSpace=false]
2631
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2632
+ * @memberof Draw */
2633
+ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2634
+ {
2635
+ const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
2636
+ const size = vec2(thickness, halfDelta.length()*2);
2637
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
2638
+ }
2639
+
2570
2640
  /** Draw colored polygon using passed in points
2571
2641
  * @param {Array} points - Array of Vector2 points
2572
2642
  * @param {Color} [color=(1,1,1,1)]
@@ -2635,22 +2705,6 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
2635
2705
  function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2636
2706
  { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
2637
2707
 
2638
- /** Draw colored line between two points
2639
- * @param {Vector2} posA
2640
- * @param {Vector2} posB
2641
- * @param {Number} [thickness]
2642
- * @param {Color} [color=(1,1,1,1)]
2643
- * @param {Boolean} [useWebGL=glEnable]
2644
- * @param {Boolean} [screenSpace=false]
2645
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2646
- * @memberof Draw */
2647
- function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2648
- {
2649
- const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
2650
- const size = vec2(thickness, halfDelta.length()*2);
2651
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
2652
- }
2653
-
2654
2708
  /** Draw directly to a 2d canvas context in world space
2655
2709
  * @param {Vector2} pos
2656
2710
  * @param {Vector2} size
@@ -2842,13 +2896,14 @@ function isFullscreen() { return !!document.fullscreenElement; }
2842
2896
  * @memberof Draw */
2843
2897
  function toggleFullscreen()
2844
2898
  {
2899
+ const rootElement = mainCanvas.parentElement;
2845
2900
  if (isFullscreen())
2846
2901
  {
2847
2902
  if (document.exitFullscreen)
2848
2903
  document.exitFullscreen();
2849
2904
  }
2850
- else if (engineRoot.requestFullscreen)
2851
- engineRoot.requestFullscreen();
2905
+ else if (rootElement.requestFullscreen)
2906
+ rootElement.requestFullscreen();
2852
2907
  }
2853
2908
  /**
2854
2909
  * LittleJS Input System
@@ -2897,7 +2952,7 @@ function keyWasReleased(key, device=0)
2897
2952
 
2898
2953
  /** Clears all input
2899
2954
  * @memberof Input */
2900
- function clearInput() { inputData = [[]]; }
2955
+ function clearInput() { inputData = [[]]; touchGamepadButtons = []; }
2901
2956
 
2902
2957
  /** Returns true if mouse button is down
2903
2958
  * @function
@@ -3019,7 +3074,6 @@ function inputInit()
3019
3074
 
3020
3075
  onkeydown = (e)=>
3021
3076
  {
3022
- if (debug && e.target != engineRoot) return;
3023
3077
  if (!e.repeat)
3024
3078
  {
3025
3079
  isUsingGamepad = false;
@@ -3032,7 +3086,6 @@ function inputInit()
3032
3086
 
3033
3087
  onkeyup = (e)=>
3034
3088
  {
3035
- if (debug && e.target != engineRoot) return;
3036
3089
  inputData[0][e.code] = 4;
3037
3090
  if (inputWASDEmulateDirection)
3038
3091
  inputData[0][remapKey(e.code)] = 4;
@@ -3064,6 +3117,7 @@ function inputInit()
3064
3117
  onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
3065
3118
  onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
3066
3119
  oncontextmenu = (e)=> false; // prevent right click menu
3120
+ onblur = (e) => clearInput(); // reset input when focus is lost
3067
3121
 
3068
3122
  // init touch input
3069
3123
  if (isTouchDevice && touchInputEnable)
@@ -3234,7 +3288,7 @@ function touchInputInit()
3234
3288
  // set event pos and pass it along
3235
3289
  const p = vec2(e.touches[0].clientX, e.touches[0].clientY);
3236
3290
  mousePosScreen = mouseToScreen(p);
3237
- wasTouching ? isUsingGamepad = false : inputData[0][button] = 3;
3291
+ wasTouching ? isUsingGamepad = touchGamepadEnable : inputData[0][button] = 3;
3238
3292
  }
3239
3293
  else if (wasTouching)
3240
3294
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -3262,10 +3316,13 @@ function touchInputInit()
3262
3316
  if (touching)
3263
3317
  {
3264
3318
  touchGamepadTimer.set();
3265
- if (paused)
3319
+ if (paused && !wasTouching)
3266
3320
  {
3267
3321
  // touch anywhere to press start when paused
3268
3322
  touchGamepadButtons[9] = 1;
3323
+
3324
+ // call default touch handler so normal touch events still work
3325
+ handleTouchDefault(e);
3269
3326
  return;
3270
3327
  }
3271
3328
  }
@@ -3290,7 +3347,7 @@ function touchInputInit()
3290
3347
  const button = touchPos.subtract(buttonCenter).direction();
3291
3348
  touchGamepadButtons[button] = 1;
3292
3349
  }
3293
- else if (touchPos.distance(startCenter) < touchGamepadSize)
3350
+ else if (touchPos.distance(startCenter) < touchGamepadSize && !wasTouching)
3294
3351
  {
3295
3352
  // virtual start button in center
3296
3353
  touchGamepadButtons[9] = 1;
@@ -3378,7 +3435,7 @@ function touchGamepadRender()
3378
3435
  /** Audio context used by the engine
3379
3436
  * @type {AudioContext}
3380
3437
  * @memberof Audio */
3381
- let audioContext;
3438
+ let audioContext = new AudioContext;
3382
3439
 
3383
3440
  /** Master gain node for all audio to pass through
3384
3441
  * @type {GainNode}
@@ -3389,14 +3446,10 @@ function audioInit()
3389
3446
  {
3390
3447
  if (!soundEnable || headlessMode) return;
3391
3448
 
3392
- // create audio context
3393
- audioContext = new AudioContext;
3394
-
3395
- // create and connect gain node
3396
3449
  // (createGain is more widely spported then GainNode construtor)
3397
3450
  audioGainNode = audioContext.createGain();
3398
3451
  audioGainNode.connect(audioContext.destination);
3399
- setSoundVolume(soundVolume); // update gain volume
3452
+ audioGainNode.gain.value = soundVolume; // set starting value
3400
3453
  }
3401
3454
 
3402
3455
  ///////////////////////////////////////////////////////////////////////////////
@@ -3431,12 +3484,15 @@ class Sound
3431
3484
 
3432
3485
  /** @property {Number} - How much to randomize frequency each time sound plays */
3433
3486
  this.randomness = 0;
3487
+
3488
+ /** @property {GainNode} - Gain node for this sound */
3489
+ this.gainNode = audioContext.createGain();
3434
3490
 
3435
3491
  if (zzfxSound)
3436
3492
  {
3437
3493
  // generate zzfx sound now for fast playback
3438
3494
  const defaultRandomness = .05;
3439
- this.randomness = zzfxSound[1] || defaultRandomness;
3495
+ this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
3440
3496
  zzfxSound[1] = 0; // generate without randomness
3441
3497
  this.sampleChannels = [zzfxG(...zzfxSound)];
3442
3498
  this.sampleRate = zzfxR;
@@ -3477,18 +3533,13 @@ class Sound
3477
3533
 
3478
3534
  // play the sound
3479
3535
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
3480
- this.gainNode = audioContext.createGain();
3481
3536
  return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
3482
3537
  }
3483
3538
 
3484
3539
  /** Set the sound volume
3485
3540
  * @param {Number} [volume] - How much to scale volume by
3486
3541
  */
3487
- setVolume(volume=1)
3488
- {
3489
- if (this.gainNode)
3490
- this.gainNode.gain.value = volume;
3491
- }
3542
+ setVolume(volume=1) { this.gainNode.gain.value = volume; }
3492
3543
 
3493
3544
  /** Stop the last instance of this sound that was played */
3494
3545
  stop()
@@ -3972,18 +4023,18 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
3972
4023
 
3973
4024
 
3974
4025
 
3975
- /** The tile collision layer array, use setTileCollisionData and getTileCollisionData to access
4026
+ /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
3976
4027
  * @type {Array}
3977
4028
  * @memberof TileCollision */
3978
4029
  let tileCollision = [];
3979
4030
 
3980
- /** Size of the tile collision layer
4031
+ /** Size of the tile collision layer 2d grid
3981
4032
  * @type {Vector2}
3982
4033
  * @memberof TileCollision */
3983
4034
  let tileCollisionSize = vec2();
3984
4035
 
3985
4036
  /** Clear and initialize tile collision
3986
- * @param {Vector2} size
4037
+ * @param {Vector2} size - width and height of tile collision 2d grid
3987
4038
  * @memberof TileCollision */
3988
4039
  function initTileCollision(size)
3989
4040
  {
@@ -3993,7 +4044,7 @@ function initTileCollision(size)
3993
4044
  tileCollision[i] = 0;
3994
4045
  }
3995
4046
 
3996
- /** Set tile collision data
4047
+ /** Set tile collision data for a given cell in the grid
3997
4048
  * @param {Vector2} pos
3998
4049
  * @param {Number} [data]
3999
4050
  * @memberof TileCollision */
@@ -4002,7 +4053,7 @@ function setTileCollisionData(pos, data=0)
4002
4053
  pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
4003
4054
  }
4004
4055
 
4005
- /** Get tile collision data
4056
+ /** Get tile collision data for a given cell in the grid
4006
4057
  * @param {Vector2} pos
4007
4058
  * @return {Number}
4008
4059
  * @memberof TileCollision */
@@ -4030,9 +4081,11 @@ function tileCollisionTest(pos, size=vec2(), object)
4030
4081
  if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
4031
4082
  return true;
4032
4083
  }
4084
+ return false;
4033
4085
  }
4034
4086
 
4035
- /** Return the center of first tile hit (does not return the exact intersection)
4087
+ /** Return the center of first tile hit, undefined if nothing was hit.
4088
+ * This does not return the exact intersection, but the center of the tile hit.
4036
4089
  * @param {Vector2} posStart
4037
4090
  * @param {Vector2} posEnd
4038
4091
  * @param {EngineObject} [object]
@@ -4053,7 +4106,7 @@ function tileCollisionRaycast(posStart, posEnd, object)
4053
4106
  let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
4054
4107
  let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
4055
4108
 
4056
- while (1)
4109
+ while (true)
4057
4110
  {
4058
4111
  // check for tile collision
4059
4112
  const tileData = getTileCollisionData(pos);
@@ -4277,8 +4330,8 @@ class TileLayer extends EngineObject
4277
4330
  const d = this.getData(layerPos);
4278
4331
  if (d.tile != undefined)
4279
4332
  {
4280
- const pos = this.pos.add(layerPos).add(vec2(.5));
4281
4333
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
4334
+ const pos = layerPos.add(vec2(.5));
4282
4335
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex);
4283
4336
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
4284
4337
  }
@@ -4707,7 +4760,7 @@ function medalsInit(saveName)
4707
4760
  // check if medals are unlocked
4708
4761
  medalsSaveName = saveName;
4709
4762
  if (!debugMedals)
4710
- medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4763
+ medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
4711
4764
 
4712
4765
  // engine automatically renders medals
4713
4766
  engineAddPlugin(undefined, medalsRender);
@@ -4770,14 +4823,28 @@ class Medal
4770
4823
  constructor(id, name, description='', icon='🏆', src)
4771
4824
  {
4772
4825
  ASSERT(id >= 0 && !medals[id]);
4773
-
4774
- // save attributes and add to list of medals
4775
- medals[this.id = id] = this;
4826
+
4827
+ /** @property {Number} - The unique identifier of the medal */
4828
+ this.id = id;
4829
+
4830
+ /** @property {String} - Name of the medal */
4776
4831
  this.name = name;
4832
+
4833
+ /** @property {String} - Description of the medal */
4777
4834
  this.description = description;
4835
+
4836
+ /** @property {String} - Icon for the medal */
4778
4837
  this.icon = icon;
4838
+
4839
+ /** @property {Boolean} - Is the medal unlocked? */
4840
+ this.unlocked = false;
4841
+
4842
+ // load the source image if provided
4779
4843
  if (src)
4780
4844
  (this.image = new Image).src = src;
4845
+
4846
+ // add this to list of medals
4847
+ medals[id] = this;
4781
4848
  }
4782
4849
 
4783
4850
  /** Unlocks a medal if not already unlocked */
@@ -4788,7 +4855,7 @@ class Medal
4788
4855
 
4789
4856
  // save the medal
4790
4857
  ASSERT(medalsSaveName, 'save name must be set');
4791
- localStorage[this.storageKey()] = this.unlocked = 1;
4858
+ localStorage[this.storageKey()] = this.unlocked = true;
4792
4859
  medalsDisplayQueue.push(this);
4793
4860
  }
4794
4861
 
@@ -4862,6 +4929,11 @@ let glCanvas;
4862
4929
  * @memberof WebGL */
4863
4930
  let glContext;
4864
4931
 
4932
+ /** Shoule webgl be setup with antialiasing, must be set before calling engineInit
4933
+ * @type {Boolean}
4934
+ * @memberof WebGL */
4935
+ let glAntialias = true;
4936
+
4865
4937
  // WebGL internal variables not exposed to documentation
4866
4938
  let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
4867
4939
 
@@ -4874,10 +4946,11 @@ function glInit()
4874
4946
 
4875
4947
  // create the canvas and textures
4876
4948
  glCanvas = document.createElement('canvas');
4877
- glContext = glCanvas.getContext('webgl2', {antialias:!canvasPixelated});
4949
+ glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
4878
4950
 
4879
4951
  // some browsers are much faster without copying the gl buffer so we just overlay it instead
4880
- glOverlay && engineRoot.appendChild(glCanvas);
4952
+ const rootElement = mainCanvas.parentElement;
4953
+ glOverlay && rootElement.appendChild(glCanvas);
4881
4954
 
4882
4955
  // setup vertex and fragment shaders
4883
4956
  glShader = glCreateProgram(
@@ -5081,6 +5154,15 @@ function glCopyToContext(context, forceDraw=false)
5081
5154
  context.drawImage(glCanvas, 0, 0);
5082
5155
  }
5083
5156
 
5157
+ /** Set antialiasing for webgl canvas
5158
+ * @param {Boolean} [antialias]
5159
+ * @memberof WebGL */
5160
+ function glSetAntialias(antialias=true)
5161
+ {
5162
+ ASSERT(!glCanvas, 'must be called before engineInit');
5163
+ glAntialias = antialias;
5164
+ }
5165
+
5084
5166
  /** Add a sprite to the gl draw list, used by all gl draw functions
5085
5167
  * @param {Number} x
5086
5168
  * @param {Number} y
@@ -5181,7 +5263,7 @@ const engineName = 'LittleJS';
5181
5263
  * @type {String}
5182
5264
  * @default
5183
5265
  * @memberof Engine */
5184
- const engineVersion = '1.10.2';
5266
+ const engineVersion = '1.10.7';
5185
5267
 
5186
5268
  /** Frames per second to update
5187
5269
  * @type {Number}
@@ -5225,13 +5307,6 @@ let timeReal = 0;
5225
5307
  * @default false
5226
5308
  * @memberof Engine */
5227
5309
  let paused = false;
5228
-
5229
- /** The root element that engine is attached to
5230
- * @type {HTMLElement}
5231
- * @default document.body
5232
- * @memberof Engine */
5233
- let engineRoot;
5234
-
5235
5310
  /** Set if game is paused
5236
5311
  * @param {Boolean} isPaused
5237
5312
  * @memberof Engine */
@@ -5251,6 +5326,8 @@ const pluginUpdateList = [], pluginRenderList = [];
5251
5326
  * @memberof Engine */
5252
5327
  function engineAddPlugin(updateFunction, renderFunction)
5253
5328
  {
5329
+ ASSERT(!pluginUpdateList.includes(updateFunction));
5330
+ ASSERT(!pluginRenderList.includes(renderFunction));
5254
5331
  updateFunction && pluginUpdateList.push(updateFunction);
5255
5332
  renderFunction && pluginRenderList.push(renderFunction);
5256
5333
  }
@@ -5259,12 +5336,12 @@ function engineAddPlugin(updateFunction, renderFunction)
5259
5336
  // Main engine functions
5260
5337
 
5261
5338
  /** Startup LittleJS engine with your callback functions
5262
- * @param {Function} gameInit - Called once after the engine starts up, setup the game
5263
- * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
5264
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
5265
- * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
5266
- * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
5267
- * @param {Array} [imageSources=['tiles.png']] - Image to load
5339
+ * @param {Function|function():Promise} gameInit - Called once after the engine starts up
5340
+ * @param {Function} gameUpdate - Called every frame before objects are updated
5341
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
5342
+ * @param {Function} gameRender - Called before objects are rendered, for drawing the background
5343
+ * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
5344
+ * @param {Array} [imageSources=[]] - List of images to load
5268
5345
  * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
5269
5346
  * @memberof Engine */
5270
5347
  function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
@@ -5414,8 +5491,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5414
5491
 
5415
5492
  function startEngine()
5416
5493
  {
5417
- gameInit();
5418
- engineUpdate();
5494
+ new Promise((resolve) => resolve(gameInit())).then(engineUpdate);
5419
5495
  }
5420
5496
 
5421
5497
  if (headlessMode)
@@ -5430,7 +5506,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5430
5506
  'width:100vw;height:100vh;' + // fill the window
5431
5507
  'display:flex;' + // use flexbox
5432
5508
  'align-items:center;' + // horizontal center
5433
- (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5434
5509
  'justify-content:center;' + // vertical center
5435
5510
  'background:#000;' + // set background color
5436
5511
  'user-select:none;' + // prevent hold to select
@@ -5438,9 +5513,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5438
5513
  (!touchInputEnable ? '' : // no touch css setttings
5439
5514
  'touch-action:none;' + // prevent mobile pinch to resize
5440
5515
  '-webkit-touch-callout:none');// compatibility for ios
5441
- engineRoot = rootElement;
5442
- engineRoot.style.cssText = styleRoot;
5443
- engineRoot.appendChild(mainCanvas = document.createElement('canvas'));
5516
+ rootElement.style.cssText = styleRoot;
5517
+ rootElement.appendChild(mainCanvas = document.createElement('canvas'));
5444
5518
  mainContext = mainCanvas.getContext('2d');
5445
5519
 
5446
5520
  // init stuff and start engine
@@ -5450,7 +5524,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5450
5524
  glInit();
5451
5525
 
5452
5526
  // create overlay canvas for hud to appear above gl canvas
5453
- engineRoot.appendChild(overlayCanvas = document.createElement('canvas'));
5527
+ rootElement.appendChild(overlayCanvas = document.createElement('canvas'));
5454
5528
  overlayContext = overlayCanvas.getContext('2d');
5455
5529
 
5456
5530
  // set canvas style
@@ -5769,4 +5843,5 @@ function drawEngineSplashScreen(t)
5769
5843
  }
5770
5844
 
5771
5845
  x.restore();
5772
- }
5846
+ }
5847
+