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
@@ -239,7 +239,8 @@ function formatTime(t) { return (t/60|0) + ':' + (t%60<10?'0':'') + (t%60|0); }
239
239
  * @memberof Random */
240
240
  function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valueB); }
241
241
 
242
- /** Returns a floored random value the two values passed in
242
+ /** Returns a floored random value between the two values passed in
243
+ * The upper bound is exclusive. (If 2 is passed in, result will be 0 or 1)
243
244
  * @param {Number} valueA
244
245
  * @param {Number} [valueB]
245
246
  * @return {Number}
@@ -368,18 +369,24 @@ class Vector2
368
369
  * @param {Number} [y] - Y axis location */
369
370
  constructor(x=0, y=0)
370
371
  {
371
- ASSERT(typeof x == 'number' && typeof y == 'number');
372
372
  /** @property {Number} - X axis location */
373
373
  this.x = x;
374
374
  /** @property {Number} - Y axis location */
375
375
  this.y = y;
376
+ ASSERT(this.isValid());
376
377
  }
377
378
 
378
379
  /** Sets values of this vector and returns self
379
380
  * @param {Number} [x] - X axis location
380
381
  * @param {Number} [y] - Y axis location
381
382
  * @return {Vector2} */
382
- set(x=0, y=0) { this.x=x; this.y=y; return this; }
383
+ set(x=0, y=0)
384
+ {
385
+ this.x = x;
386
+ this.y = y;
387
+ ASSERT(this.isValid());
388
+ return this;
389
+ }
383
390
 
384
391
  /** Returns a new vector that is a copy of this
385
392
  * @return {Vector2} */
@@ -521,6 +528,7 @@ class Vector2
521
528
  * @param {Number} [length] */
522
529
  setDirection(direction, length=1)
523
530
  {
531
+ direction = mod(direction, 4);
524
532
  ASSERT(direction==0 || direction==1 || direction==2 || direction==3);
525
533
  return vec2(direction%2 ? direction-1 ? -length : length : 0,
526
534
  direction%2 ? 0 : direction ? -length : length);
@@ -570,6 +578,14 @@ class Vector2
570
578
  if (debug)
571
579
  return `(${(this.x<0?'':' ') + this.x.toFixed(digits)},${(this.y<0?'':' ') + this.y.toFixed(digits)} )`;
572
580
  }
581
+
582
+ /** Checks if this is a valid vector
583
+ * @return {Boolean} */
584
+ isValid()
585
+ {
586
+ return typeof this.x == 'number' && !isNaN(this.x)
587
+ && typeof this.y == 'number' && !isNaN(this.y);
588
+ }
573
589
  }
574
590
 
575
591
  ///////////////////////////////////////////////////////////////////////////////
@@ -630,6 +646,7 @@ class Color
630
646
  this.b = b;
631
647
  /** @property {Number} - Alpha */
632
648
  this.a = a;
649
+ ASSERT(this.isValid());
633
650
  }
634
651
 
635
652
  /** Sets values of this color and returns self
@@ -639,7 +656,14 @@ class Color
639
656
  * @param {Number} [a] - alpha
640
657
  * @return {Color} */
641
658
  set(r=1, g=1, b=1, a=1)
642
- { this.r=r; this.g=g; this.b=b; this.a=a; return this; }
659
+ {
660
+ this.r = r;
661
+ this.g = g;
662
+ this.b = b;
663
+ this.a = a;
664
+ ASSERT(this.isValid());
665
+ return this;
666
+ }
643
667
 
644
668
  /** Returns a new color that is a copy of this
645
669
  * @return {Color} */
@@ -722,6 +746,7 @@ class Color
722
746
  this.g = f(p, q, h);
723
747
  this.b = f(p, q, h - 1/3);
724
748
  this.a = a;
749
+ ASSERT(this.isValid());
725
750
  return this;
726
751
  }
727
752
 
@@ -749,7 +774,6 @@ class Color
749
774
  else if (b == max)
750
775
  h = (r - g) / d + 4;
751
776
  }
752
-
753
777
  return [h / 6, s, l, a];
754
778
  }
755
779
 
@@ -782,11 +806,27 @@ class Color
782
806
  * @return {Color} */
783
807
  setHex(hex)
784
808
  {
785
- const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
786
- this.r = fromHex(1);
787
- this.g = fromHex(3),
788
- this.b = fromHex(5);
789
- this.a = hex.length > 7 ? fromHex(7) : 1;
809
+ ASSERT(typeof hex == 'string' && hex[0] == '#');
810
+ ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
811
+
812
+ if (hex.length < 6)
813
+ {
814
+ const fromHex = (c)=> clamp(parseInt(hex[c],16)/15);
815
+ this.r = fromHex(1);
816
+ this.g = fromHex(2),
817
+ this.b = fromHex(3);
818
+ this.a = hex.length == 5 ? fromHex(4) : 1;
819
+ }
820
+ else
821
+ {
822
+ const fromHex = (c)=> clamp(parseInt(hex.slice(c,c+2),16)/255);
823
+ this.r = fromHex(1);
824
+ this.g = fromHex(3),
825
+ this.b = fromHex(5);
826
+ this.a = hex.length == 9 ? fromHex(7) : 1;
827
+ }
828
+
829
+ ASSERT(this.isValid());
790
830
  return this;
791
831
  }
792
832
 
@@ -800,6 +840,16 @@ class Color
800
840
  const a = clamp(this.a)*255<<24;
801
841
  return r + g + b + a;
802
842
  }
843
+
844
+ /** Checks if this is a valid color
845
+ * @return {Boolean} */
846
+ isValid()
847
+ {
848
+ return typeof this.r == 'number' && !isNaN(this.r)
849
+ && typeof this.g == 'number' && !isNaN(this.g)
850
+ && typeof this.b == 'number' && !isNaN(this.b)
851
+ && typeof this.a == 'number' && !isNaN(this.a);
852
+ }
803
853
  }
804
854
 
805
855
  ///////////////////////////////////////////////////////////////////////////////
@@ -1435,7 +1485,7 @@ class EngineObject
1435
1485
  * @param {Color} [color=(1,1,1,1)] - Color to apply to tile when rendered
1436
1486
  * @param {Number} [renderOrder] - Objects sorted by renderOrder before being rendered
1437
1487
  */
1438
- constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color, renderOrder=0)
1488
+ constructor(pos=vec2(), size=vec2(1), tileInfo, angle=0, color=new Color, renderOrder=0)
1439
1489
  {
1440
1490
  // set passed in params
1441
1491
  ASSERT(isVector2(pos) && isVector2(size), 'ensure pos and size are vec2s');
@@ -1549,15 +1599,18 @@ class EngineObject
1549
1599
 
1550
1600
  // apply physics
1551
1601
  const oldPos = this.pos.copy();
1552
- this.pos.x += this.velocity.x *= this.damping;
1553
- this.pos.y += this.velocity.y = this.damping * this.velocity.y
1554
- + gravity * this.gravityScale;
1602
+ this.velocity.x *= this.damping;
1603
+ this.velocity.y *= this.damping;
1604
+ if (this.mass) // dont apply gravity to static objects
1605
+ this.velocity.y += gravity * this.gravityScale;
1606
+ this.pos.x += this.velocity.x;
1607
+ this.pos.y += this.velocity.y;
1555
1608
  this.angle += this.angleVelocity *= this.angleDamping;
1556
1609
 
1557
1610
  // physics sanity checks
1558
1611
  ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
1559
1612
  ASSERT(this.damping >= 0 && this.damping <= 1);
1560
- if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
1613
+ if (!enablePhysicsSolver || !this.mass) // dont do collision for static objects
1561
1614
  return;
1562
1615
 
1563
1616
  const wasMovingDown = this.velocity.y < 0;
@@ -1703,6 +1756,7 @@ class EngineObject
1703
1756
  this.pos.x = oldPos.x;
1704
1757
  this.velocity.x *= -this.elasticity;
1705
1758
  }
1759
+ debugOverlay && debugPhysics && debugRect(this.pos, this.size, '#f00');
1706
1760
  }
1707
1761
  }
1708
1762
  }
@@ -1917,7 +1971,7 @@ let drawCount;
1917
1971
  * tile(2) // a tile at index 2 using the default tile size of 16
1918
1972
  * tile(5, 8) // a tile at index 5 using a tile size of 8
1919
1973
  * tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
1920
- * tile(vec2(4,8), vec2(30,10)) // a tile at pixel location (4,8) with a size of (30,10)
1974
+ * tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
1921
1975
  * @memberof Draw
1922
1976
  */
1923
1977
  function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0, padding=0)
@@ -2135,6 +2189,22 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
2135
2189
  drawTile(pos, size, undefined, color, angle, false, undefined, useWebGL, screenSpace, context);
2136
2190
  }
2137
2191
 
2192
+ /** Draw colored line between two points
2193
+ * @param {Vector2} posA
2194
+ * @param {Vector2} posB
2195
+ * @param {Number} [thickness]
2196
+ * @param {Color} [color=(1,1,1,1)]
2197
+ * @param {Boolean} [useWebGL=glEnable]
2198
+ * @param {Boolean} [screenSpace=false]
2199
+ * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2200
+ * @memberof Draw */
2201
+ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2202
+ {
2203
+ const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
2204
+ const size = vec2(thickness, halfDelta.length()*2);
2205
+ drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
2206
+ }
2207
+
2138
2208
  /** Draw colored polygon using passed in points
2139
2209
  * @param {Array} points - Array of Vector2 points
2140
2210
  * @param {Color} [color=(1,1,1,1)]
@@ -2203,22 +2273,6 @@ function drawEllipse(pos, width=1, height=1, angle=0, color=new Color, lineWidth
2203
2273
  function drawCircle(pos, radius=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), screenSpace, context=mainContext)
2204
2274
  { drawEllipse(pos, radius, radius, 0, color, lineWidth, lineColor, screenSpace, context); }
2205
2275
 
2206
- /** Draw colored line between two points
2207
- * @param {Vector2} posA
2208
- * @param {Vector2} posB
2209
- * @param {Number} [thickness]
2210
- * @param {Color} [color=(1,1,1,1)]
2211
- * @param {Boolean} [useWebGL=glEnable]
2212
- * @param {Boolean} [screenSpace=false]
2213
- * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
2214
- * @memberof Draw */
2215
- function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
2216
- {
2217
- const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
2218
- const size = vec2(thickness, halfDelta.length()*2);
2219
- drawRect(posA.add(halfDelta), size, color, halfDelta.angle(), useWebGL, screenSpace, context);
2220
- }
2221
-
2222
2276
  /** Draw directly to a 2d canvas context in world space
2223
2277
  * @param {Vector2} pos
2224
2278
  * @param {Vector2} size
@@ -2410,13 +2464,14 @@ function isFullscreen() { return !!document.fullscreenElement; }
2410
2464
  * @memberof Draw */
2411
2465
  function toggleFullscreen()
2412
2466
  {
2467
+ const rootElement = mainCanvas.parentElement;
2413
2468
  if (isFullscreen())
2414
2469
  {
2415
2470
  if (document.exitFullscreen)
2416
2471
  document.exitFullscreen();
2417
2472
  }
2418
- else if (engineRoot.requestFullscreen)
2419
- engineRoot.requestFullscreen();
2473
+ else if (rootElement.requestFullscreen)
2474
+ rootElement.requestFullscreen();
2420
2475
  }
2421
2476
  /**
2422
2477
  * LittleJS Input System
@@ -2465,7 +2520,7 @@ function keyWasReleased(key, device=0)
2465
2520
 
2466
2521
  /** Clears all input
2467
2522
  * @memberof Input */
2468
- function clearInput() { inputData = [[]]; }
2523
+ function clearInput() { inputData = [[]]; touchGamepadButtons = []; }
2469
2524
 
2470
2525
  /** Returns true if mouse button is down
2471
2526
  * @function
@@ -2587,7 +2642,6 @@ function inputInit()
2587
2642
 
2588
2643
  onkeydown = (e)=>
2589
2644
  {
2590
- if (debug && e.target != engineRoot) return;
2591
2645
  if (!e.repeat)
2592
2646
  {
2593
2647
  isUsingGamepad = false;
@@ -2600,7 +2654,6 @@ function inputInit()
2600
2654
 
2601
2655
  onkeyup = (e)=>
2602
2656
  {
2603
- if (debug && e.target != engineRoot) return;
2604
2657
  inputData[0][e.code] = 4;
2605
2658
  if (inputWASDEmulateDirection)
2606
2659
  inputData[0][remapKey(e.code)] = 4;
@@ -2632,6 +2685,7 @@ function inputInit()
2632
2685
  onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
2633
2686
  onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
2634
2687
  oncontextmenu = (e)=> false; // prevent right click menu
2688
+ onblur = (e) => clearInput(); // reset input when focus is lost
2635
2689
 
2636
2690
  // init touch input
2637
2691
  if (isTouchDevice && touchInputEnable)
@@ -2802,7 +2856,7 @@ function touchInputInit()
2802
2856
  // set event pos and pass it along
2803
2857
  const p = vec2(e.touches[0].clientX, e.touches[0].clientY);
2804
2858
  mousePosScreen = mouseToScreen(p);
2805
- wasTouching ? isUsingGamepad = false : inputData[0][button] = 3;
2859
+ wasTouching ? isUsingGamepad = touchGamepadEnable : inputData[0][button] = 3;
2806
2860
  }
2807
2861
  else if (wasTouching)
2808
2862
  inputData[0][button] = inputData[0][button] & 2 | 4;
@@ -2830,10 +2884,13 @@ function touchInputInit()
2830
2884
  if (touching)
2831
2885
  {
2832
2886
  touchGamepadTimer.set();
2833
- if (paused)
2887
+ if (paused && !wasTouching)
2834
2888
  {
2835
2889
  // touch anywhere to press start when paused
2836
2890
  touchGamepadButtons[9] = 1;
2891
+
2892
+ // call default touch handler so normal touch events still work
2893
+ handleTouchDefault(e);
2837
2894
  return;
2838
2895
  }
2839
2896
  }
@@ -2858,7 +2915,7 @@ function touchInputInit()
2858
2915
  const button = touchPos.subtract(buttonCenter).direction();
2859
2916
  touchGamepadButtons[button] = 1;
2860
2917
  }
2861
- else if (touchPos.distance(startCenter) < touchGamepadSize)
2918
+ else if (touchPos.distance(startCenter) < touchGamepadSize && !wasTouching)
2862
2919
  {
2863
2920
  // virtual start button in center
2864
2921
  touchGamepadButtons[9] = 1;
@@ -2946,7 +3003,7 @@ function touchGamepadRender()
2946
3003
  /** Audio context used by the engine
2947
3004
  * @type {AudioContext}
2948
3005
  * @memberof Audio */
2949
- let audioContext;
3006
+ let audioContext = new AudioContext;
2950
3007
 
2951
3008
  /** Master gain node for all audio to pass through
2952
3009
  * @type {GainNode}
@@ -2957,14 +3014,10 @@ function audioInit()
2957
3014
  {
2958
3015
  if (!soundEnable || headlessMode) return;
2959
3016
 
2960
- // create audio context
2961
- audioContext = new AudioContext;
2962
-
2963
- // create and connect gain node
2964
3017
  // (createGain is more widely spported then GainNode construtor)
2965
3018
  audioGainNode = audioContext.createGain();
2966
3019
  audioGainNode.connect(audioContext.destination);
2967
- setSoundVolume(soundVolume); // update gain volume
3020
+ audioGainNode.gain.value = soundVolume; // set starting value
2968
3021
  }
2969
3022
 
2970
3023
  ///////////////////////////////////////////////////////////////////////////////
@@ -2999,12 +3052,15 @@ class Sound
2999
3052
 
3000
3053
  /** @property {Number} - How much to randomize frequency each time sound plays */
3001
3054
  this.randomness = 0;
3055
+
3056
+ /** @property {GainNode} - Gain node for this sound */
3057
+ this.gainNode = audioContext.createGain();
3002
3058
 
3003
3059
  if (zzfxSound)
3004
3060
  {
3005
3061
  // generate zzfx sound now for fast playback
3006
3062
  const defaultRandomness = .05;
3007
- this.randomness = zzfxSound[1] || defaultRandomness;
3063
+ this.randomness = zzfxSound[1] != undefined ? zzfxSound[1] : defaultRandomness;
3008
3064
  zzfxSound[1] = 0; // generate without randomness
3009
3065
  this.sampleChannels = [zzfxG(...zzfxSound)];
3010
3066
  this.sampleRate = zzfxR;
@@ -3045,18 +3101,13 @@ class Sound
3045
3101
 
3046
3102
  // play the sound
3047
3103
  const playbackRate = pitch + pitch * this.randomness*randomnessScale*rand(-1,1);
3048
- this.gainNode = audioContext.createGain();
3049
3104
  return this.source = playSamples(this.sampleChannels, volume, playbackRate, pan, loop, this.sampleRate, this.gainNode);
3050
3105
  }
3051
3106
 
3052
3107
  /** Set the sound volume
3053
3108
  * @param {Number} [volume] - How much to scale volume by
3054
3109
  */
3055
- setVolume(volume=1)
3056
- {
3057
- if (this.gainNode)
3058
- this.gainNode.gain.value = volume;
3059
- }
3110
+ setVolume(volume=1) { this.gainNode.gain.value = volume; }
3060
3111
 
3061
3112
  /** Stop the last instance of this sound that was played */
3062
3113
  stop()
@@ -3540,18 +3591,18 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
3540
3591
 
3541
3592
 
3542
3593
 
3543
- /** The tile collision layer array, use setTileCollisionData and getTileCollisionData to access
3594
+ /** The tile collision layer grid, use setTileCollisionData and getTileCollisionData to access
3544
3595
  * @type {Array}
3545
3596
  * @memberof TileCollision */
3546
3597
  let tileCollision = [];
3547
3598
 
3548
- /** Size of the tile collision layer
3599
+ /** Size of the tile collision layer 2d grid
3549
3600
  * @type {Vector2}
3550
3601
  * @memberof TileCollision */
3551
3602
  let tileCollisionSize = vec2();
3552
3603
 
3553
3604
  /** Clear and initialize tile collision
3554
- * @param {Vector2} size
3605
+ * @param {Vector2} size - width and height of tile collision 2d grid
3555
3606
  * @memberof TileCollision */
3556
3607
  function initTileCollision(size)
3557
3608
  {
@@ -3561,7 +3612,7 @@ function initTileCollision(size)
3561
3612
  tileCollision[i] = 0;
3562
3613
  }
3563
3614
 
3564
- /** Set tile collision data
3615
+ /** Set tile collision data for a given cell in the grid
3565
3616
  * @param {Vector2} pos
3566
3617
  * @param {Number} [data]
3567
3618
  * @memberof TileCollision */
@@ -3570,7 +3621,7 @@ function setTileCollisionData(pos, data=0)
3570
3621
  pos.arrayCheck(tileCollisionSize) && (tileCollision[(pos.y|0)*tileCollisionSize.x+pos.x|0] = data);
3571
3622
  }
3572
3623
 
3573
- /** Get tile collision data
3624
+ /** Get tile collision data for a given cell in the grid
3574
3625
  * @param {Vector2} pos
3575
3626
  * @return {Number}
3576
3627
  * @memberof TileCollision */
@@ -3598,9 +3649,11 @@ function tileCollisionTest(pos, size=vec2(), object)
3598
3649
  if (tileData && (!object || object.collideWithTile(tileData, vec2(x, y))))
3599
3650
  return true;
3600
3651
  }
3652
+ return false;
3601
3653
  }
3602
3654
 
3603
- /** Return the center of first tile hit (does not return the exact intersection)
3655
+ /** Return the center of first tile hit, undefined if nothing was hit.
3656
+ * This does not return the exact intersection, but the center of the tile hit.
3604
3657
  * @param {Vector2} posStart
3605
3658
  * @param {Vector2} posEnd
3606
3659
  * @param {EngineObject} [object]
@@ -3621,7 +3674,7 @@ function tileCollisionRaycast(posStart, posEnd, object)
3621
3674
  let xi = unit.x * (delta.x < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1);
3622
3675
  let yi = unit.y * (delta.y < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1);
3623
3676
 
3624
- while (1)
3677
+ while (true)
3625
3678
  {
3626
3679
  // check for tile collision
3627
3680
  const tileData = getTileCollisionData(pos);
@@ -3845,8 +3898,8 @@ class TileLayer extends EngineObject
3845
3898
  const d = this.getData(layerPos);
3846
3899
  if (d.tile != undefined)
3847
3900
  {
3848
- const pos = this.pos.add(layerPos).add(vec2(.5));
3849
3901
  ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
3902
+ const pos = layerPos.add(vec2(.5));
3850
3903
  const tileInfo = tile(d.tile, s, this.tileInfo.textureIndex);
3851
3904
  drawTile(pos, vec2(1), tileInfo, d.color, d.direction*PI/2, d.mirror);
3852
3905
  }
@@ -4275,7 +4328,7 @@ function medalsInit(saveName)
4275
4328
  // check if medals are unlocked
4276
4329
  medalsSaveName = saveName;
4277
4330
  if (!debugMedals)
4278
- medalsForEach(medal=> medal.unlocked = (localStorage[medal.storageKey()] | 0));
4331
+ medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
4279
4332
 
4280
4333
  // engine automatically renders medals
4281
4334
  engineAddPlugin(undefined, medalsRender);
@@ -4338,14 +4391,28 @@ class Medal
4338
4391
  constructor(id, name, description='', icon='🏆', src)
4339
4392
  {
4340
4393
  ASSERT(id >= 0 && !medals[id]);
4341
-
4342
- // save attributes and add to list of medals
4343
- medals[this.id = id] = this;
4394
+
4395
+ /** @property {Number} - The unique identifier of the medal */
4396
+ this.id = id;
4397
+
4398
+ /** @property {String} - Name of the medal */
4344
4399
  this.name = name;
4400
+
4401
+ /** @property {String} - Description of the medal */
4345
4402
  this.description = description;
4403
+
4404
+ /** @property {String} - Icon for the medal */
4346
4405
  this.icon = icon;
4406
+
4407
+ /** @property {Boolean} - Is the medal unlocked? */
4408
+ this.unlocked = false;
4409
+
4410
+ // load the source image if provided
4347
4411
  if (src)
4348
4412
  (this.image = new Image).src = src;
4413
+
4414
+ // add this to list of medals
4415
+ medals[id] = this;
4349
4416
  }
4350
4417
 
4351
4418
  /** Unlocks a medal if not already unlocked */
@@ -4356,7 +4423,7 @@ class Medal
4356
4423
 
4357
4424
  // save the medal
4358
4425
  ASSERT(medalsSaveName, 'save name must be set');
4359
- localStorage[this.storageKey()] = this.unlocked = 1;
4426
+ localStorage[this.storageKey()] = this.unlocked = true;
4360
4427
  medalsDisplayQueue.push(this);
4361
4428
  }
4362
4429
 
@@ -4430,6 +4497,11 @@ let glCanvas;
4430
4497
  * @memberof WebGL */
4431
4498
  let glContext;
4432
4499
 
4500
+ /** Shoule webgl be setup with antialiasing, must be set before calling engineInit
4501
+ * @type {Boolean}
4502
+ * @memberof WebGL */
4503
+ let glAntialias = true;
4504
+
4433
4505
  // WebGL internal variables not exposed to documentation
4434
4506
  let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glInstanceCount, glAdditive, glBatchAdditive;
4435
4507
 
@@ -4442,10 +4514,11 @@ function glInit()
4442
4514
 
4443
4515
  // create the canvas and textures
4444
4516
  glCanvas = document.createElement('canvas');
4445
- glContext = glCanvas.getContext('webgl2', {antialias:!canvasPixelated});
4517
+ glContext = glCanvas.getContext('webgl2', {antialias:glAntialias});
4446
4518
 
4447
4519
  // some browsers are much faster without copying the gl buffer so we just overlay it instead
4448
- glOverlay && engineRoot.appendChild(glCanvas);
4520
+ const rootElement = mainCanvas.parentElement;
4521
+ glOverlay && rootElement.appendChild(glCanvas);
4449
4522
 
4450
4523
  // setup vertex and fragment shaders
4451
4524
  glShader = glCreateProgram(
@@ -4649,6 +4722,15 @@ function glCopyToContext(context, forceDraw=false)
4649
4722
  context.drawImage(glCanvas, 0, 0);
4650
4723
  }
4651
4724
 
4725
+ /** Set antialiasing for webgl canvas
4726
+ * @param {Boolean} [antialias]
4727
+ * @memberof WebGL */
4728
+ function glSetAntialias(antialias=true)
4729
+ {
4730
+ ASSERT(!glCanvas, 'must be called before engineInit');
4731
+ glAntialias = antialias;
4732
+ }
4733
+
4652
4734
  /** Add a sprite to the gl draw list, used by all gl draw functions
4653
4735
  * @param {Number} x
4654
4736
  * @param {Number} y
@@ -4749,7 +4831,7 @@ const engineName = 'LittleJS';
4749
4831
  * @type {String}
4750
4832
  * @default
4751
4833
  * @memberof Engine */
4752
- const engineVersion = '1.10.2';
4834
+ const engineVersion = '1.10.7';
4753
4835
 
4754
4836
  /** Frames per second to update
4755
4837
  * @type {Number}
@@ -4793,13 +4875,6 @@ let timeReal = 0;
4793
4875
  * @default false
4794
4876
  * @memberof Engine */
4795
4877
  let paused = false;
4796
-
4797
- /** The root element that engine is attached to
4798
- * @type {HTMLElement}
4799
- * @default document.body
4800
- * @memberof Engine */
4801
- let engineRoot;
4802
-
4803
4878
  /** Set if game is paused
4804
4879
  * @param {Boolean} isPaused
4805
4880
  * @memberof Engine */
@@ -4819,6 +4894,8 @@ const pluginUpdateList = [], pluginRenderList = [];
4819
4894
  * @memberof Engine */
4820
4895
  function engineAddPlugin(updateFunction, renderFunction)
4821
4896
  {
4897
+ ASSERT(!pluginUpdateList.includes(updateFunction));
4898
+ ASSERT(!pluginRenderList.includes(renderFunction));
4822
4899
  updateFunction && pluginUpdateList.push(updateFunction);
4823
4900
  renderFunction && pluginRenderList.push(renderFunction);
4824
4901
  }
@@ -4827,12 +4904,12 @@ function engineAddPlugin(updateFunction, renderFunction)
4827
4904
  // Main engine functions
4828
4905
 
4829
4906
  /** Startup LittleJS engine with your callback functions
4830
- * @param {Function} gameInit - Called once after the engine starts up, setup the game
4831
- * @param {Function} gameUpdate - Called every frame at 60 frames per second, handle input and update the game state
4832
- * @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
4833
- * @param {Function} gameRender - Called before objects are rendered, draw any background effects that appear behind objects
4834
- * @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
4835
- * @param {Array} [imageSources=['tiles.png']] - Image to load
4907
+ * @param {Function|function():Promise} gameInit - Called once after the engine starts up
4908
+ * @param {Function} gameUpdate - Called every frame before objects are updated
4909
+ * @param {Function} gameUpdatePost - Called after physics and objects are updated, even when paused
4910
+ * @param {Function} gameRender - Called before objects are rendered, for drawing the background
4911
+ * @param {Function} gameRenderPost - Called after objects are rendered, useful for drawing UI
4912
+ * @param {Array} [imageSources=[]] - List of images to load
4836
4913
  * @param {HTMLElement} [rootElement] - Root element to attach to, the document body by default
4837
4914
  * @memberof Engine */
4838
4915
  function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
@@ -4982,8 +5059,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4982
5059
 
4983
5060
  function startEngine()
4984
5061
  {
4985
- gameInit();
4986
- engineUpdate();
5062
+ new Promise((resolve) => resolve(gameInit())).then(engineUpdate);
4987
5063
  }
4988
5064
 
4989
5065
  if (headlessMode)
@@ -4998,7 +5074,6 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
4998
5074
  'width:100vw;height:100vh;' + // fill the window
4999
5075
  'display:flex;' + // use flexbox
5000
5076
  'align-items:center;' + // horizontal center
5001
- (canvasPixelated ? 'image-rendering:pixelated;' : '') + // pixel art
5002
5077
  'justify-content:center;' + // vertical center
5003
5078
  'background:#000;' + // set background color
5004
5079
  'user-select:none;' + // prevent hold to select
@@ -5006,9 +5081,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5006
5081
  (!touchInputEnable ? '' : // no touch css setttings
5007
5082
  'touch-action:none;' + // prevent mobile pinch to resize
5008
5083
  '-webkit-touch-callout:none');// compatibility for ios
5009
- engineRoot = rootElement;
5010
- engineRoot.style.cssText = styleRoot;
5011
- engineRoot.appendChild(mainCanvas = document.createElement('canvas'));
5084
+ rootElement.style.cssText = styleRoot;
5085
+ rootElement.appendChild(mainCanvas = document.createElement('canvas'));
5012
5086
  mainContext = mainCanvas.getContext('2d');
5013
5087
 
5014
5088
  // init stuff and start engine
@@ -5018,7 +5092,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
5018
5092
  glInit();
5019
5093
 
5020
5094
  // create overlay canvas for hud to appear above gl canvas
5021
- engineRoot.appendChild(overlayCanvas = document.createElement('canvas'));
5095
+ rootElement.appendChild(overlayCanvas = document.createElement('canvas'));
5022
5096
  overlayContext = overlayCanvas.getContext('2d');
5023
5097
 
5024
5098
  // set canvas style
@@ -5337,4 +5411,5 @@ function drawEngineSplashScreen(t)
5337
5411
  }
5338
5412
 
5339
5413
  x.restore();
5340
- }
5414
+ }
5415
+
@@ -19,6 +19,7 @@ setShowSplashScreen(true);
19
19
  const maxScenes = 11;
20
20
  let scene = 0;
21
21
  let sceneName;
22
+ let spriteAtlas;
22
23
  let groundObject;
23
24
  let mouseJoint;
24
25
  let car;
@@ -27,6 +28,19 @@ let repeatSpawnTimer = new Timer;
27
28
  ///////////////////////////////////////////////////////////////////////////////
28
29
  function gameInit()
29
30
  {
31
+ // create a table of all sprites
32
+ const gameTile = (i)=> tile(i, 16, 0, 1);
33
+ spriteAtlas =
34
+ {
35
+ circle: gameTile(0),
36
+ dot: gameTile(1),
37
+ circleOutline: gameTile(2),
38
+ squareOutline: gameTile(3),
39
+ wheel: gameTile(4),
40
+ gear: gameTile(5),
41
+ squareOutline2: gameTile(6),
42
+ };
43
+
30
44
  loadScene(scene);
31
45
  }
32
46
 
@@ -125,7 +139,7 @@ function gameRenderPost()
125
139
  {
126
140
  // draw mouse joint
127
141
  const ab = vec2(mouseJoint.GetAnchorB());
128
- drawTile(ab, vec2(.3), tile(0), BLACK);
142
+ drawTile(ab, vec2(.3), spriteAtlas.circle, BLACK);
129
143
  drawLine(mousePos, ab, .1, BLACK);
130
144
  }
131
145