littlejsengine 1.17.1 → 1.17.10

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 (49) hide show
  1. package/LICENSE +10 -10
  2. package/dist/littlejs.d.ts +254 -171
  3. package/dist/littlejs.esm.js +892 -628
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +889 -626
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +885 -622
  8. package/examples/box2d/gameObjects.js +3 -3
  9. package/examples/electron/index.html +2 -2
  10. package/examples/index.html +10 -9
  11. package/examples/logo.png +0 -0
  12. package/examples/logo2.png +0 -0
  13. package/examples/module/build.bat +7 -0
  14. package/examples/module/build.js +121 -0
  15. package/examples/particles/index.html +1 -1
  16. package/examples/platformer/game.js +1 -1
  17. package/examples/platformer/gameEffects.js +5 -10
  18. package/examples/platformer/gameLevel.js +3 -3
  19. package/examples/puzzle/game.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/box2dPool.js +2 -2
  22. package/examples/shorts/box2dTileLayer.js +48 -0
  23. package/examples/shorts/colors.js +1 -1
  24. package/examples/shorts/fps.js +1 -1
  25. package/examples/shorts/music.js +5 -4
  26. package/examples/shorts/musicPlayer.js +4 -3
  27. package/examples/shorts/parallax.js +2 -2
  28. package/examples/shorts/tiltedView.js +1 -1
  29. package/examples/starter/index.html +2 -2
  30. package/package.json +5 -5
  31. package/plugins/box2d.js +166 -63
  32. package/plugins/pluginExport.js +1 -1
  33. package/plugins/postProcess.js +1 -1
  34. package/plugins/uiSystem.js +33 -20
  35. package/plugins/zzfxm.js +2 -2
  36. package/reference.md +9 -8
  37. package/src/engine.js +111 -134
  38. package/src/engineAudio.js +44 -58
  39. package/src/engineDebug.js +4 -4
  40. package/src/engineDraw.js +7 -4
  41. package/src/engineExport.js +2 -1
  42. package/src/engineInput.js +5 -5
  43. package/src/engineMath.js +9 -9
  44. package/src/engineObject.js +25 -25
  45. package/src/engineParticles.js +241 -118
  46. package/src/engineTileLayer.js +117 -88
  47. package/src/engineUtilities.js +105 -78
  48. package/src/engineWebGL.js +2 -2
  49. package/plugins/Box2D_License.txt +0 -17
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.17.1';
36
+ const engineVersion = '1.17.10';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -88,8 +88,9 @@ function getPaused() { return paused; }
88
88
  * @memberof Engine */
89
89
  function setPaused(isPaused=true) { paused = isPaused; }
90
90
 
91
- // Frame time tracking
91
+ // Engine internal variables
92
92
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
93
+ let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
95
96
  // plugin hooks
@@ -153,16 +154,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
153
154
  * @example
154
155
  * // Basic engine startup
155
156
  * engineInit(
156
- * () => { LOG('Game initialized!'); }, // gameInit
157
- * () => { updateGameLogic(); }, // gameUpdate
158
- * () => { updateUI(); }, // gameUpdatePost
159
- * () => { drawBackground(); }, // gameRender
160
- * () => { drawHUD(); }, // gameRenderPost
157
+ * ()=> { LOG('Game initialized!'); }, // gameInit
158
+ * ()=> { updateGameLogic(); }, // gameUpdate
159
+ * ()=> { updateUI(); }, // gameUpdatePost
160
+ * ()=> { drawBackground(); }, // gameRender
161
+ * ()=> { drawHUD(); }, // gameRenderPost
161
162
  * ['tiles.png', 'tilesLevel.png'] // images to load
162
163
  * );
163
164
  * @memberof Engine */
164
165
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
166
  {
167
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
166
168
  ASSERT(!mainContext, 'engine already initialized');
167
169
  ASSERT(isArray(imageSources), 'pass in images as array');
168
170
 
@@ -431,7 +433,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
431
433
  promises.push(new Promise(resolve =>
432
434
  {
433
435
  let t = 0;
434
- console.log(`${engineName} Engine v${engineVersion}`);
435
436
  updateSplash();
436
437
  function updateSplash()
437
438
  {
@@ -463,13 +464,13 @@ function engineObjectsUpdate()
463
464
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
464
465
 
465
466
  // recursive object update
466
- function updateObject(o)
467
+ function updateChildObject(o)
467
468
  {
468
469
  if (o.destroyed) return;
469
470
 
470
471
  o.update();
471
472
  for (const child of o.children)
472
- updateObject(child);
473
+ updateChildObject(child);
473
474
  }
474
475
  for (const o of engineObjects)
475
476
  {
@@ -479,7 +480,7 @@ function engineObjectsUpdate()
479
480
  o.update();
480
481
  o.updatePhysics();
481
482
  for (const child of o.children)
482
- updateObject(child);
483
+ updateChildObject(child);
483
484
  o.updateTransforms();
484
485
  }
485
486
 
@@ -488,11 +489,14 @@ function engineObjectsUpdate()
488
489
  }
489
490
 
490
491
  /** Destroy and remove all objects
492
+ * - This can be used to clear out all objects when restarting a level
493
+ * - Objects can override their destroy function to do cleanup or stick around
494
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
491
495
  * @memberof Engine */
492
- function engineObjectsDestroy()
496
+ function engineObjectsDestroy(immediate=true)
493
497
  {
494
498
  for (const o of engineObjects)
495
- o.parent || o.destroy();
499
+ o.parent || o.destroy(immediate);
496
500
  engineObjects = engineObjects.filter(o=>!o.destroyed);
497
501
  }
498
502
 
@@ -567,165 +571,140 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
567
571
  ///////////////////////////////////////////////////////////////////////////////
568
572
  function drawEngineLogo(t)
569
573
  {
574
+ const blackAndWhite = 0;
575
+ const showName = 1;
576
+
570
577
  // LittleJS Logo and Splash Screen
571
578
  const x = mainContext;
572
579
  const w = mainCanvas.width = innerWidth;
573
580
  const h = mainCanvas.height = innerHeight;
574
-
575
581
  {
576
582
  // background
577
583
  const p3 = percent(t, 1, .8);
578
584
  const p4 = percent(t, 0, .5);
579
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
585
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
580
586
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
581
587
  g.addColorStop(1,hsl(0,0,0,p3).toString());
582
588
  x.save();
583
589
  x.fillStyle = g;
584
590
  x.fillRect(0,0,w,h);
585
591
  }
586
-
587
- // draw LittleJS logo...
588
- const rect = (X, Y, W, H, C)=>
592
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
593
+ {
594
+ if (C >= 0)
595
+ {
596
+ if (blackAndWhite)
597
+ x.fillStyle = '#fff';
598
+ else
599
+ {
600
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
601
+ g.addColorStop(0,color(C,2));
602
+ g.addColorStop(1,color(C,1));
603
+ }
604
+ }
605
+ else
606
+ x.fillStyle = '#000';
607
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
608
+ }
609
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
589
610
  {
590
611
  x.beginPath();
591
- x.rect(X,Y,W,C?H*p:H);
592
- x.fillStyle = C;
593
- C ? x.fill() : x.stroke();
594
- };
595
- const line = (X, Y, Z, W)=>
612
+ x.arc(X,Y,R,p*A,p*B);
613
+ gradient(X,Y-R,X,Y+R,C,S);
614
+ }
615
+ const rect = (X,Y,W,H,C)=>
596
616
  {
597
617
  x.beginPath();
598
- x.lineTo(X,Y);
599
- x.lineTo(Z,W);
600
- x.stroke();
601
- };
602
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
618
+ x.rect(X,Y,W,H*p);
619
+ gradient(X,Y+H,X+W,Y,C);
620
+ }
621
+ const poly = (points,C,Y,H)=>
603
622
  {
604
- const D = (A+B)/2, E = p*(B-A)/2;
605
623
  x.beginPath();
606
- F && x.lineTo(X,Y);
607
- x.arc(X,Y,R,D-E,D+E);
608
- x.fillStyle = C;
609
- C ? x.fill() : x.stroke();
610
- };
611
- const color = (c=0, l=0) =>
612
- hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
624
+ for (const p of points)
625
+ x.lineTo(p.x, p.y);
626
+ x.closePath();
627
+ gradient(0, Y, 0, Y+H,C);
628
+ }
629
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
630
+
631
+ // center and fit tos screen
613
632
  const alpha = wave(1,1,t);
614
633
  const p = percent(alpha, .1, .5);
615
-
616
- // setup
634
+ const size = min(6, min(w,h)/99);
617
635
  x.translate(w/2,h/2);
618
- const size = min(6, min(w,h)/99); // fit to screen
619
636
  x.scale(size,size);
620
637
  x.translate(-40,-35);
638
+ p < 1 && x.setLineDash([99*p,99]);
621
639
  x.lineJoin = x.lineCap = 'round';
622
640
  x.lineWidth = .1 + p*1.9;
641
+ //x.strokeStyle='#fff7';
642
+
643
+ if (showName)
644
+ {
645
+ // engine name text
646
+ const Y = 54;
647
+ const s = 'LittleJS';
648
+ x.font = '900 15.5px arial';
649
+ x.lineWidth = .1+p*3.9;
650
+ x.textAlign = 'center';
651
+ x.textBaseline = 'top';
652
+ rect(11,Y+2,59,8*p,-1);
653
+ x.beginPath();
623
654
 
624
- // drawing effect
625
- const p2 = percent(alpha,.1,1);
626
- x.setLineDash([99*p2,99]);
627
-
628
- // cab top
629
- rect(7,16,18,-8,color(2,2));
630
- rect(7,8,18,4,color(2,3));
631
- rect(25,8,8,8,color(2,1));
632
- rect(25,8,-18,8);
633
- rect(25,8,8,8);
634
-
635
- // cab
636
- rect(25,16,7,24,color());
637
- rect(11,39,14,-23,color(1,1));
638
- rect(11,16,14,18,color(1,2));
639
- rect(11,16,14,8,color(1,3));
640
- rect(25,16,-14,24);
641
-
642
- // cab window
643
- rect(15,29,6,-9,color(2,2));
644
- circle(15,21,5,0,PI/2,color(2,4),1);
645
- rect(21,21,-6,9);
646
-
647
- // little stack
648
- rect(37,14,9,6,color(3,2));
649
- rect(37,14,4.5,6,color(3,3));
650
- rect(37,14,9,6);
655
+ let w2 = 0;
656
+ for (let i=0;i<s.length;++i)
657
+ w2 += x.measureText(s[i]).width;
658
+ for (let j=2;j--;)
659
+ for (let i=0,X=40-w2/2;i<s.length;++i)
660
+ {
661
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
662
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
663
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
664
+ X += w;
665
+ }
666
+
667
+ x.lineWidth = .1 + p*1.9;
668
+ rect(3,Y,73,0); // bottom
669
+ }
670
+
671
+ rect(7,15,26,-7,0); // cab top
672
+ rect(25,15,8,25,-1); // cab front
673
+ rect(10,40,15,-25,1); // cab back
674
+ rect(14,21,7,9,2); // cab window
675
+ rect(38,15,6,6,2); // little stack
651
676
 
652
677
  // big stack
653
- rect(50,20,10,-8,color(0,1));
654
- rect(50,20,6.5,-8,color(0,2));
655
- rect(50,20,3.5,-8,color(0,3));
656
- rect(50,20,10,-8);
657
- circle(55,2,11.4,.5,PI-.5,color(3,3));
658
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
659
- circle(55,2,11.4,.5,PI-.5);
660
- rect(45,7,20,-7,color(0,2));
661
- rect(45,-1,20,4,color(0,3));
662
- rect(45,-1,20,8);
678
+ rect(49,20,10,-6,0);
679
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
680
+ poly(stackPoints,2,8,6*p);
681
+ rect(44,8,20,-7,0);
663
682
 
664
683
  // engine
665
- for (let i=5; i--;)
666
- {
667
- // stagger radius to fix slight seam
668
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
669
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
670
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
671
- }
684
+ for (let i=5;i--;) circle(59-i*6,30,10,0,9,1,0);
685
+ circle(59,30,4,0,9,2); // light
672
686
 
673
687
  // engine outline
674
- circle(36,30,10,PI/2,PI*3/2);
675
- circle(48,30,10,PI/2,PI*3/2);
676
- circle(60,30,10);
677
- line(36,20,60,20);
678
-
679
- // engine front light
680
- circle(60,30,4,PI,3*PI,color(3,2));
681
- circle(60,30,4,PI,2*PI,color(3,3));
682
- circle(60,30,4,PI,3*PI);
688
+ rect(35,20,24,0); // top
689
+ circle(59,30,10); // front
690
+ circle(47,30,10,PI/2,PI*3/2); // middle
691
+ circle(35,30,10,PI/2,PI*3/2); // back
692
+ rect(7,40,13,7,-1); // bottom back
693
+ rect(17,40,43,14,-1); // bottom center
683
694
 
684
- // front brush
685
- for (let i=6; i--;)
695
+ // wheels
696
+ for (let i=3;i--;)
697
+ for (let j=2;j--;)
698
+ circle(15*i+17,47,j?7:1,PI,3*PI,2);
699
+
700
+ // cowcatcher
701
+ for (let i=2;i--;)
686
702
  {
687
- x.beginPath();
688
- x.lineTo(53,54);
689
- x.lineTo(53,40);
690
- x.lineTo(53+(1+i*2.9)*p,40);
691
- x.lineTo(53+(4+i*3.5)*p,54);
692
- x.fillStyle = color(0,i%2+2);
693
- x.fill();
694
- i%2 && x.stroke();
703
+ let w=6, s=7, o=53+w*p*i
704
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
705
+ poly(points,0,40,14);
695
706
  }
696
707
 
697
- // wheels
698
- rect(6,40,5,5);
699
- rect(6,40,5,5,color());
700
- rect(15,54,38,-14,color());
701
- for (let i=3; i--;)
702
- for (let j=2; j--;)
703
- {
704
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
705
- x.stroke();
706
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
707
- x.stroke();
708
- }
709
- line(6,40,68,40); // center
710
- line(77,54,4,54); // bottom
711
-
712
- // draw engine name
713
- const s = engineName;
714
- x.font = '900 16px arial';
715
- x.textAlign = 'center';
716
- x.textBaseline = 'top';
717
- x.lineWidth = .1+p*3.9;
718
- let w2 = 0;
719
- for (let i=0; i<s.length; ++i)
720
- w2 += x.measureText(s[i]).width;
721
- for (let j=2; j--;)
722
- for (let i=0, X=41-w2/2; i<s.length; ++i)
723
- {
724
- x.fillStyle = color(i,2);
725
- const w = x.measureText(s[i]).width;
726
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
727
- X += w;
728
- }
729
708
  x.restore();
730
709
  }
731
710
  /**
@@ -782,25 +761,25 @@ function debugProtectConstant(o){ return o; }
782
761
  const PI = Math.PI;
783
762
 
784
763
  /** Returns absolute value of value passed in
785
- * @param {number} value
764
+ * @param {number} x
786
765
  * @return {number}
787
766
  * @memberof Math */
788
767
  const abs = Math.abs;
789
768
 
790
769
  /** Returns floored value of value passed in
791
- * @param {number} value
770
+ * @param {number} x
792
771
  * @return {number}
793
772
  * @memberof Math */
794
773
  const floor = Math.floor;
795
774
 
796
775
  /** Returns ceiled value of value passed in
797
- * @param {number} value
776
+ * @param {number} x
798
777
  * @return {number}
799
778
  * @memberof Math */
800
779
  const ceil = Math.ceil;
801
780
 
802
781
  /** Returns rounded value passed in
803
- * @param {number} value
782
+ * @param {number} x
804
783
  * @return {number}
805
784
  * @memberof Math */
806
785
  const round = Math.round;
@@ -818,7 +797,7 @@ const min = Math.min;
818
797
  const max = Math.max;
819
798
 
820
799
  /** Returns the sign of value passed in
821
- * @param {number} value
800
+ * @param {number} x
822
801
  * @return {number}
823
802
  * @memberof Math */
824
803
  const sign = Math.sign;
@@ -830,25 +809,25 @@ const sign = Math.sign;
830
809
  const hypot = Math.hypot;
831
810
 
832
811
  /** Returns log2 of value passed in
833
- * @param {number} value
812
+ * @param {number} x
834
813
  * @return {number}
835
814
  * @memberof Math */
836
815
  const log2 = Math.log2;
837
816
 
838
817
  /** Returns sin of value passed in
839
- * @param {number} value
818
+ * @param {number} x
840
819
  * @return {number}
841
820
  * @memberof Math */
842
821
  const sin = Math.sin;
843
822
 
844
823
  /** Returns cos of value passed in
845
- * @param {number} value
824
+ * @param {number} x
846
825
  * @return {number}
847
826
  * @memberof Math */
848
827
  const cos = Math.cos;
849
828
 
850
829
  /** Returns tan of value passed in
851
- * @param {number} value
830
+ * @param {number} x
852
831
  * @return {number}
853
832
  * @memberof Math */
854
833
  const tan = Math.tan;
@@ -1883,6 +1862,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1883
1862
  * @namespace Utilities
1884
1863
  */
1885
1864
 
1865
+ /**
1866
+ * Timer object tracks how long has passed since it was set
1867
+ * @memberof Engine
1868
+ * @example
1869
+ * let a = new Timer; // creates a timer that is not set
1870
+ * a.set(3); // sets the timer to 3 seconds
1871
+ *
1872
+ * let b = new Timer(1); // creates a timer with 1 second left
1873
+ * b.unset(); // unset the timer
1874
+ */
1875
+ class Timer
1876
+ {
1877
+ /** Create a timer object set time passed in
1878
+ * @param {number} [timeLeft] - How much time left before the timer
1879
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1880
+ constructor(timeLeft, useRealTime=false)
1881
+ {
1882
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1883
+ this.useRealTime = useRealTime;
1884
+ const globalTime = this.getGlobalTime();
1885
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1886
+ this.setTime = timeLeft;
1887
+ }
1888
+
1889
+ /** Set the timer with seconds passed in
1890
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
1891
+ set(timeLeft=0)
1892
+ {
1893
+ ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
1894
+ const globalTime = this.getGlobalTime();
1895
+ this.time = globalTime + timeLeft;
1896
+ this.setTime = timeLeft;
1897
+ }
1898
+
1899
+ /** Set if the timer should keep running even when the game is paused
1900
+ * @param {boolean} [useRealTime] */
1901
+ setUseRealTime(useRealTime=true)
1902
+ {
1903
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
1904
+ this.useRealTime = useRealTime;
1905
+ }
1906
+
1907
+ /** Unset the timer */
1908
+ unset() { this.time = undefined; }
1909
+
1910
+ /** Returns true if set
1911
+ * @return {boolean} */
1912
+ isSet() { return this.time !== undefined; }
1913
+
1914
+ /** Returns true if set and has not elapsed
1915
+ * @return {boolean} */
1916
+ active() { return this.getGlobalTime() < this.time; }
1917
+
1918
+ /** Returns true if set and elapsed
1919
+ * @return {boolean} */
1920
+ elapsed() { return this.getGlobalTime() >= this.time; }
1921
+
1922
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1923
+ * @return {number} */
1924
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
1925
+
1926
+ /** Get percentage elapsed based on time it was set to, returns 0 if not set
1927
+ * @return {number} */
1928
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
1929
+
1930
+ /** Get the time this timer was set to, returns 0 if not set
1931
+ * @return {number} */
1932
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
1933
+
1934
+ /** Get the current global time this timer is based on
1935
+ * @return {number} */
1936
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
1937
+
1938
+ /** Returns this timer expressed as a string
1939
+ * @return {string} */
1940
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1941
+
1942
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1943
+ * @return {number} */
1944
+ valueOf() { return this.get(); }
1945
+ }
1946
+
1947
+ ///////////////////////////////////////////////////////////////////////////////
1948
+
1886
1949
  /** Formats seconds to mm:ss style for display purposes
1887
1950
  * @param {number} t - time in seconds
1888
1951
  * @return {string}
@@ -1969,86 +2032,29 @@ function shareURL(title, url, callback)
1969
2032
 
1970
2033
  ///////////////////////////////////////////////////////////////////////////////
1971
2034
 
1972
- /**
1973
- * Timer object tracks how long has passed since it was set
1974
- * @memberof Engine
1975
- * @example
1976
- * let a = new Timer; // creates a timer that is not set
1977
- * a.set(3); // sets the timer to 3 seconds
1978
- *
1979
- * let b = new Timer(1); // creates a timer with 1 second left
1980
- * b.unset(); // unset the timer
1981
- */
1982
- class Timer
2035
+ /** Read save data from local storage
2036
+ * @param {string} saveName - unique name for the game/save
2037
+ * @param {Object} [defaultSaveData] - default values for save
2038
+ * @return {Object}
2039
+ * @memberof Utilities */
2040
+ function readSaveData(saveName, defaultSaveData)
1983
2041
  {
1984
- /** Create a timer object set time passed in
1985
- * @param {number} [timeLeft] - How much time left before the timer
1986
- * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1987
- constructor(timeLeft, useRealTime=false)
1988
- {
1989
- ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1990
- this.useRealTime = useRealTime;
1991
- const globalTime = this.getGlobalTime();
1992
- this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1993
- this.setTime = timeLeft;
1994
- }
1995
-
1996
- /** Set the timer with seconds passed in
1997
- * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
1998
- set(timeLeft=0)
1999
- {
2000
- ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2001
- const globalTime = this.getGlobalTime();
2002
- this.time = globalTime + timeLeft;
2003
- this.setTime = timeLeft;
2004
- }
2005
-
2006
- /** Set if the timer should keep running even when the game is paused
2007
- * @param {boolean} [useRealTime] */
2008
- setUseRealTime(useRealTime=true)
2009
- {
2010
- ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2011
- this.useRealTime = useRealTime;
2012
- }
2013
-
2014
- /** Unset the timer */
2015
- unset() { this.time = undefined; }
2016
-
2017
- /** Returns true if set
2018
- * @return {boolean} */
2019
- isSet() { return this.time !== undefined; }
2020
-
2021
- /** Returns true if set and has not elapsed
2022
- * @return {boolean} */
2023
- active() { return this.getGlobalTime() < this.time; }
2024
-
2025
- /** Returns true if set and elapsed
2026
- * @return {boolean} */
2027
- elapsed() { return this.getGlobalTime() >= this.time; }
2028
-
2029
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2030
- * @return {number} */
2031
- get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2032
-
2033
- /** Get percentage elapsed based on time it was set to, returns 0 if not set
2034
- * @return {number} */
2035
- getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2036
-
2037
- /** Get the time this timer was set to, returns 0 if not set
2038
- * @return {number} */
2039
- getSetTime() { return this.isSet() ? this.setTime : 0; }
2040
-
2041
- /** Get the current global time this timer is based on
2042
- * @return {number} */
2043
- getGlobalTime() { return this.useRealTime ? timeReal : time; }
2044
-
2045
- /** Returns this timer expressed as a string
2046
- * @return {string} */
2047
- toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2042
+ ASSERT(isString(saveName), 'loadData requires saveName string');
2043
+
2044
+ // replace undefined values with defaults
2045
+ const data = localStorage[saveName];
2046
+ const loadedData = data ? JSON.parse(data) : {};
2047
+ return { ...defaultSaveData, ...loadedData };
2048
+ }
2048
2049
 
2049
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2050
- * @return {number} */
2051
- valueOf() { return this.get(); }
2050
+ /** Write save data to local storage
2051
+ * @param {string} saveName - unique name for the game/save
2052
+ * @param {Object} saveData - object containing data to be saved
2053
+ * @memberof Utilities */
2054
+ function writeSaveData(saveName, saveData)
2055
+ {
2056
+ ASSERT(isString(saveName), 'saveData requires saveName string');
2057
+ localStorage[saveName] = JSON.stringify(saveData);
2052
2058
  }
2053
2059
  /**
2054
2060
  * LittleJS Engine Settings
@@ -2709,17 +2715,19 @@ class EngineObject
2709
2715
  this.additiveColor = undefined;
2710
2716
  /** @property {boolean} - Should it flip along y axis when rendered */
2711
2717
  this.mirror = false;
2718
+ /** @property {boolean} - Has object been destroyed? */
2719
+ this.destroyed = false;
2712
2720
 
2713
2721
  // physical properties
2714
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
2722
+ /** @property {number} - How heavy the object is, static if 0 */
2715
2723
  this.mass = objectDefaultMass;
2716
- /** @property {number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
2724
+ /** @property {number} - How much to slow down velocity each frame (0-1) */
2717
2725
  this.damping = objectDefaultDamping;
2718
- /** @property {number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
2726
+ /** @property {number} - How much to slow down rotation each frame (0-1) */
2719
2727
  this.angleDamping = objectDefaultAngleDamping;
2720
- /** @property {number} [restitution=objectDefaultRestitution] - How bouncy the object is when colliding (0-1) */
2728
+ /** @property {number} - How bouncy the object is when colliding (0-1) */
2721
2729
  this.restitution = objectDefaultRestitution;
2722
- /** @property {number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
2730
+ /** @property {number} - How much friction to apply when sliding (0-1) */
2723
2731
  this.friction = objectDefaultFriction;
2724
2732
  /** @property {number} - How much to scale gravity by for this object */
2725
2733
  this.gravityScale = 1;
@@ -2920,7 +2928,7 @@ class EngineObject
2920
2928
  if (this.collideTiles)
2921
2929
  {
2922
2930
  // check collision against tiles
2923
- const hitLayer = tileCollisionTest(this.pos, this.size, this)
2931
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
2924
2932
  if (hitLayer)
2925
2933
  {
2926
2934
  // if already was stuck in collision, don't do anything
@@ -2928,10 +2936,10 @@ class EngineObject
2928
2936
  if (!tileCollisionTest(oldPos, this.size, this))
2929
2937
  {
2930
2938
  // test which side we bounced off (or both if a corner)
2931
- const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2932
- const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2933
-
2934
- if (blockedLayerX)
2939
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2940
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2941
+ const restitution = max(this.restitution, hitLayer.restitution);
2942
+ if (isBlockedX)
2935
2943
  {
2936
2944
  // try to move up a tiny bit
2937
2945
  const epsilon = 1e-3;
@@ -2947,16 +2955,12 @@ class EngineObject
2947
2955
  return;
2948
2956
  }
2949
2957
 
2950
- // move to previous position and bounce
2958
+ // move to previous X position and bounce
2951
2959
  this.pos.x = oldPos.x;
2952
- this.velocity.x *= -this.restitution;
2960
+ this.velocity.x *= -restitution;
2953
2961
  }
2954
- if (blockedLayerY || !blockedLayerX)
2962
+ if (isBlockedY || !isBlockedX)
2955
2963
  {
2956
- // bounce velocity
2957
- const restitution = max(this.restitution, hitLayer.restitution);
2958
- this.velocity.y *= -restitution;
2959
-
2960
2964
  if (wasFalling)
2961
2965
  {
2962
2966
  // adjust position to slightly away from nearest tile
@@ -2972,10 +2976,12 @@ class EngineObject
2972
2976
  }
2973
2977
  else
2974
2978
  {
2975
- // move to previous position
2979
+ // move to previous Y position
2976
2980
  this.pos.y = oldPos.y;
2977
2981
  this.groundObject = undefined;
2978
2982
  }
2983
+ // bounce velocity
2984
+ this.velocity.y *= -restitution;
2979
2985
  }
2980
2986
  debugPhysics && debugRect(this.pos, this.size, '#f00');
2981
2987
  }
@@ -2993,19 +2999,19 @@ class EngineObject
2993
2999
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2994
3000
  }
2995
3001
 
2996
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
2997
- destroy()
3002
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal
3003
+ * @param {boolean} [immediate] - should attached effects be allowed to die off? */
3004
+ destroy(immediate=false)
2998
3005
  {
2999
- if (this.destroyed)
3000
- return;
3006
+ if (this.destroyed) return;
3001
3007
 
3002
3008
  // disconnect from parent and destroy children
3003
- this.destroyed = 1;
3009
+ this.destroyed = true;
3004
3010
  this.parent?.removeChild(this);
3005
3011
  for (const child of this.children)
3006
3012
  {
3007
3013
  child.parent = undefined;
3008
- child.destroy();
3014
+ child.destroy(immediate);
3009
3015
  }
3010
3016
  }
3011
3017
 
@@ -3104,7 +3110,7 @@ class EngineObject
3104
3110
  }
3105
3111
 
3106
3112
  /** Check if overlapping another engine object
3107
- * Collisions are resoloved to prevent overlaps
3113
+ * Collisions are resolved to prevent overlaps
3108
3114
  * @param {EngineObject} object
3109
3115
  * @return {boolean} */
3110
3116
  isOverlappingObject(object)
@@ -3356,7 +3362,7 @@ class TileInfo
3356
3362
  }
3357
3363
 
3358
3364
  /**
3359
- * Returns a tile info for an index using this tile as refrence
3365
+ * Returns a tile info for an index using this tile as reference
3360
3366
  * @param {Vector2|number} [index=0]
3361
3367
  * @return {TileInfo}
3362
3368
  */
@@ -3424,7 +3430,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
3424
3430
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
3425
3431
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3426
3432
 
3427
- const textureInfo = tileInfo && tileInfo.textureInfo;
3433
+ const textureInfo = tileInfo?.textureInfo;
3428
3434
  const bleed = tileInfo?.bleed ?? 0;
3429
3435
  if (useWebGL && glEnable)
3430
3436
  {
@@ -3730,9 +3736,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
3730
3736
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
3731
3737
  ASSERT(isNumber(angle), 'angle must be a number');
3732
3738
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
3733
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
3739
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
3734
3740
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
3735
3741
 
3742
+ // clamp line width to prevent artifacts
3743
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
3744
+
3736
3745
  if (useWebGL && glEnable)
3737
3746
  {
3738
3747
  // draw as a regular polygon
@@ -4183,7 +4192,7 @@ let engineFontImage;
4183
4192
  class FontImage
4184
4193
  {
4185
4194
  /** Create an image font
4186
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
4195
+ * @param {TileInfo} tileInfo - Tile info of first character in font
4187
4196
  */
4188
4197
  constructor(tileInfo)
4189
4198
  {
@@ -4698,9 +4707,9 @@ function inputInit()
4698
4707
  function touchInputInit()
4699
4708
  {
4700
4709
  // add non passive touch event listeners
4701
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
4702
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
4703
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
4710
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
4711
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
4712
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
4704
4713
 
4705
4714
  // handle all touch events the same way
4706
4715
  let wasTouching;
@@ -4906,7 +4915,7 @@ function inputUpdate()
4906
4915
  // poll gamepads
4907
4916
  const maxGamepads = 8;
4908
4917
  const gamepads = navigator.getGamepads();
4909
- const gamepadCount = min(maxGamepads, gamepads.length)
4918
+ const gamepadCount = min(maxGamepads, gamepads.length);
4910
4919
  for (let i=0; i<gamepadCount; ++i)
4911
4920
  {
4912
4921
  // get or create gamepad data
@@ -5059,7 +5068,7 @@ function inputRender()
5059
5068
  }
5060
5069
  }
5061
5070
 
5062
- // center position for right tocuh pad face buttons
5071
+ // center position for right touch pad face buttons
5063
5072
  function touchGamepadButtonCenter()
5064
5073
  {
5065
5074
  const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
@@ -5111,29 +5120,46 @@ function audioInit()
5111
5120
  ///////////////////////////////////////////////////////////////////////////////
5112
5121
 
5113
5122
  /**
5114
- * Sound Object - Stores a sound for later use and can be played positionally
5123
+ * Sound Object - Stores a sound for later
5124
+ * - this can be used to load and play wave, mp3, and ogg files
5125
+ * - it can also create sounds using the ZzFX sound generator
5126
+ * - can attenuate and apply stereo panning to sounds
5127
+ * - sound instance control with pause/resume capability
5115
5128
  *
5116
5129
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5117
5130
  * @memberof Audio
5118
5131
  * @example
5119
- * // create a sound
5132
+ * // load an audio asset file
5133
+ * const sound_example = new Sound('sound.mp3');
5134
+ *
5135
+ * // create a zzfx sound
5120
5136
  * const sound_example = new Sound([.5,.5]);
5121
5137
  *
5122
- * // play the sound
5138
+ * // play a sound
5123
5139
  * sound_example.play();
5124
5140
  */
5125
5141
  class Sound
5126
5142
  {
5127
- /** Create a sound object and cache the zzfx samples for later use
5128
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
5143
+ /**
5144
+ * @callback SoundLoadCallback - Function called when sound is loaded
5145
+ * @param {Sound} sound
5146
+ * @memberof Audio
5147
+ */
5148
+
5149
+ /** Create a sound object and cache the audio for later use
5150
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
5151
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
5129
5152
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5130
5153
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5154
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5131
5155
  */
5132
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
5156
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
5133
5157
  {
5134
5158
  if (!soundEnable || headlessMode) return;
5135
5159
 
5136
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5160
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5161
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5162
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5137
5163
  ASSERT(isNumber(range), 'range must be a number');
5138
5164
  ASSERT(isNumber(taper), 'taper must be a number');
5139
5165
 
@@ -5142,23 +5168,35 @@ class Sound
5142
5168
  /** @property {number} - At what percentage of range should it start tapering */
5143
5169
  this.taper = taper;
5144
5170
  /** @property {number} - How much to randomize frequency each time sound plays */
5145
- this.randomness = 0;
5171
+ this.randomness = randomness ?? 0;
5146
5172
  /** @property {number} - Sample rate for this sound */
5147
5173
  this.sampleRate = audioDefaultSampleRate;
5148
5174
  /** @property {number} - Percentage of this sound currently loaded */
5149
5175
  this.loadedPercent = 0;
5176
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
5177
+ this.onloadCallback = onloadCallback;
5150
5178
 
5151
- // generate zzfx sound now for fast playback
5152
- if (zzfxSound)
5179
+ if (Array.isArray(asset))
5153
5180
  {
5181
+ // generate zzfx sound
5182
+ const zzfxSound = asset;
5183
+
5154
5184
  // remove randomness so it can be applied on playback
5155
- const randomnessIndex = 1, defaultRandomness = .05;
5185
+ const defaultRandomness = randomness ?? .05;
5186
+ const randomnessIndex = 1;
5156
5187
  this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5157
5188
  zzfxSound[randomnessIndex] = 0;
5158
5189
 
5159
5190
  // generate the zzfx samples
5160
5191
  this.sampleChannels = [zzfxG(...zzfxSound)];
5161
5192
  this.loadedPercent = 1;
5193
+ onloadCallback?.(this);
5194
+ }
5195
+ else if (typeof asset === 'string')
5196
+ {
5197
+ // load the audio file
5198
+ const filename = asset;
5199
+ this.loadSound(filename);
5162
5200
  }
5163
5201
  }
5164
5202
 
@@ -5210,7 +5248,7 @@ class Sound
5210
5248
  * @param {number} [volume] - Volume to play the music at
5211
5249
  * @param {boolean} [loop] - Should the music loop?
5212
5250
  * @param {boolean} [paused] - Should the music start paused
5213
- * @return {SoundInstance} - The audio source node
5251
+ * @return {SoundInstance} - The sound instance
5214
5252
  */
5215
5253
  playMusic(volume=1, loop=true, paused=false)
5216
5254
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -5220,70 +5258,27 @@ class Sound
5220
5258
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5221
5259
  * @param {Vector2} [pos] - World space position to play the sound if any
5222
5260
  * @param {number} [volume=1] - How much to scale volume by
5223
- * @return {SoundInstance} - The audio source node
5261
+ * @return {SoundInstance} - The sound instance
5224
5262
  */
5225
5263
  playNote(semitoneOffset=0, pos, volume)
5226
- {
5227
- ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5228
- const pitch = getNoteFrequency(semitoneOffset, 1);
5229
- return this.play(pos, volume, pitch, 0);
5230
- }
5231
-
5232
- /** Get how long this sound is in seconds
5233
- * @return {number} - How long the sound is in seconds (undefined if loading)
5234
- */
5235
- getDuration()
5236
- { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5237
-
5238
- /** Check if sound is loaded, for sounds fetched from a url
5239
- * @return {boolean} - True if sound is loaded and ready to play
5240
- */
5241
- isLoaded() { return this.loadedPercent === 1; }
5242
- }
5243
-
5244
- ///////////////////////////////////////////////////////////////////////////////
5245
-
5246
- /**
5247
- * Sound Wave Object - Loads and stores an audio file for later use
5248
- * - this can be used to load and play wave, mp3, and ogg files
5249
- * @extends Sound
5250
- * @memberof Audio
5251
- * @example
5252
- * // load an audio asset file
5253
- * const sound_example = new SoundWave('sound.mp3');
5254
- *
5255
- * // play the sound
5256
- * sound_example.play();
5257
- */
5258
- class SoundWave extends Sound
5259
- {
5260
- /**
5261
- * @callback SoundLoadCallback - Function called when sound is loaded
5262
- * @param {SoundWave} sound
5263
- * @memberof Audio
5264
- */
5265
-
5266
- /** Create a sound object and cache the wave file for later use
5267
- * @param {string} filename - Filename of audio file to load
5268
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
5269
- * @param {number} [range=soundDefaultRange] - World space max range of sound
5270
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5271
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5272
- */
5273
- constructor(filename, randomness=0, range, taper, onloadCallback)
5274
- {
5275
- super(undefined, range, taper);
5276
- if (!soundEnable || headlessMode) return;
5277
- ASSERT(!filename || isString(filename), 'filename must be a string');
5278
- ASSERT(isNumber(randomness), 'randomness must be a number');
5279
-
5280
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5281
- this.onloadCallback = onloadCallback;
5282
- this.randomness = randomness;
5283
- filename && this.loadSound(filename);
5264
+ {
5265
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5266
+ const pitch = getNoteFrequency(semitoneOffset, 1);
5267
+ return this.play(pos, volume, pitch, 0);
5284
5268
  }
5285
5269
 
5286
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
5270
+ /** Get how long this sound is in seconds
5271
+ * @return {number} - How long the sound is in seconds (undefined if loading)
5272
+ */
5273
+ getDuration()
5274
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
5275
+
5276
+ /** Check if sound is loaded, for sounds fetched from a url
5277
+ * @return {boolean} - True if sound is loaded and ready to play
5278
+ */
5279
+ isLoaded() { return this.loadedPercent === 1; }
5280
+
5281
+ /** Loads a sound from a URL and decodes it into sample data.
5287
5282
  * @param {string} filename
5288
5283
  * @return {Promise<void>} */
5289
5284
  async loadSound(filename)
@@ -5774,50 +5769,62 @@ const tileCollisionLayers = [];
5774
5769
 
5775
5770
  /** Get tile collision data for a given cell in the grid
5776
5771
  * @param {Vector2} pos
5772
+ * @param {boolean} [solidOnly] - Only check solid layers?
5777
5773
  * @return {number}
5778
5774
  * @memberof TileLayers */
5779
- function tileCollisionGetData(pos)
5775
+ function tileCollisionGetData(pos, solidOnly=true)
5780
5776
  {
5781
5777
  // check all tile collision layers
5782
5778
  for (const layer of tileCollisionLayers)
5779
+ if (!solidOnly || layer.isSolid)
5783
5780
  if (pos.arrayCheck(layer.size))
5784
- return layer.getCollisionData(pos);
5781
+ {
5782
+ const data = layer.getCollisionData(pos);
5783
+ if (data) return data;
5784
+ }
5785
5785
  return 0;
5786
5786
  }
5787
5787
 
5788
5788
  /** Check if a tile layer collides with another object
5789
- * @param {Vector2} pos
5790
- * @param {Vector2} [size=vec2()]
5791
- * @param {EngineObject} [object] - An object or undefined for generic test
5792
- * @param {boolean} [solidOnly] - Only check solid layers if true
5789
+ * @param {Vector2} pos
5790
+ * @param {Vector2} [size=vec2()]
5791
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
5792
+ * @param {boolean} [solidOnly] - Only check solid layers?
5793
5793
  * @return {TileCollisionLayer}
5794
5794
  * @memberof TileLayers */
5795
- function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5795
+ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
5796
5796
  {
5797
5797
  for (const layer of tileCollisionLayers)
5798
5798
  {
5799
5799
  if (!solidOnly || layer.isSolid)
5800
- if (layer.collisionTest(pos, size, object))
5800
+ if (layer.collisionTest(pos, size, callbackObject))
5801
5801
  return layer;
5802
5802
  }
5803
5803
  }
5804
5804
 
5805
+ /**
5806
+ * @callback TileCollisionCallback - Function to handle a tile collision test
5807
+ * @param {number} tileData - the value of the tile at the position
5808
+ * @param {Vector2} pos - world space position of tile where the collision occurred
5809
+ * @memberof TileLayers
5810
+ */
5811
+
5805
5812
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
5806
- * The point will be inside the colliding tile if it hits (may have a tiny shift)
5807
- * @param {Vector2} posStart
5808
- * @param {Vector2} posEnd
5809
- * @param {EngineObject} [object] - An object or undefined for generic test
5810
- * @param {Vector2} [normal] - Optional normal of the surface hit
5811
- * @param {boolean} [solidOnly=true] - Only check solid layers if true
5813
+ * The point will be inside the colliding tile if it hits
5814
+ * @param {Vector2} posStart
5815
+ * @param {Vector2} posEnd
5816
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
5817
+ * @param {Vector2} [normal] - Optional normal of the surface hit
5818
+ * @param {boolean} [solidOnly=true] - Only check solid layers?
5812
5819
  * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5813
5820
  * @memberof TileLayers */
5814
- function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5821
+ function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
5815
5822
  {
5816
5823
  for (const layer of tileCollisionLayers)
5817
5824
  {
5818
5825
  if (!solidOnly || layer.isSolid)
5819
5826
  {
5820
- const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
5827
+ const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, normal)
5821
5828
  if (hitPos) return hitPos;
5822
5829
  }
5823
5830
  }
@@ -5938,17 +5945,17 @@ class TileLayerData
5938
5945
  class CanvasLayer extends EngineObject
5939
5946
  {
5940
5947
  /** Create a canvas layer object
5941
- * @param {Vector2} [position] - World space position of the layer
5948
+ * @param {Vector2} [pos] - World space position of the layer
5942
5949
  * @param {Vector2} [size] - World space size of the layer
5943
5950
  * @param {number} [angle] - Angle the layer is rotated by
5944
5951
  * @param {number} [renderOrder] - Objects sorted by renderOrder
5945
5952
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
5946
5953
  * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
5947
5954
  */
5948
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
5955
+ constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
5949
5956
  {
5950
5957
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
5951
- super(position, size, undefined, angle, WHITE, renderOrder);
5958
+ super(pos, size, undefined, angle, WHITE, renderOrder);
5952
5959
 
5953
5960
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
5954
5961
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -6052,16 +6059,16 @@ class CanvasLayer extends EngineObject
6052
6059
  class TileLayer extends CanvasLayer
6053
6060
  {
6054
6061
  /** Create a tile layer object
6055
- * @param {Vector2} position - World space position
6062
+ * @param {Vector2} pos - World space position
6056
6063
  * @param {Vector2} size - World space size
6057
6064
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6058
6065
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6066
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6059
6067
  */
6060
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6068
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6061
6069
  {
6062
6070
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6063
- const useWebGL = true;
6064
- super(position, size, 0, renderOrder, canvasSize, useWebGL);
6071
+ super(pos, size, 0, renderOrder, canvasSize, useWebGL);
6065
6072
 
6066
6073
  /** @property {TileInfo} - Default tile info for layer */
6067
6074
  this.tileInfo = undefined;
@@ -6073,15 +6080,15 @@ class TileLayer extends CanvasLayer
6073
6080
  if (headlessMode)
6074
6081
  {
6075
6082
  // disable rendering in headless mode
6076
- this.render = () => {};
6077
- this.redraw = () => {};
6078
- this.redrawStart = () => {};
6079
- this.redrawEnd = () => {};
6080
- this.drawTileData = () => {};
6081
- this.redrawTileData = () => {};
6082
- this.drawLayerTile = () => {};
6083
- this.drawLayerRect = () => {};
6084
- this.clearLayerRect = () => {};
6083
+ this.render = ()=> {};
6084
+ this.redraw = ()=> {};
6085
+ this.redrawStart = ()=> {};
6086
+ this.redrawEnd = ()=> {};
6087
+ this.drawTileData = ()=> {};
6088
+ this.redrawTileData = ()=> {};
6089
+ this.drawLayerTile = ()=> {};
6090
+ this.drawLayerRect = ()=> {};
6091
+ this.clearLayerRect = ()=> {};
6085
6092
  return;
6086
6093
  }
6087
6094
 
@@ -6115,6 +6122,12 @@ class TileLayer extends CanvasLayer
6115
6122
  isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6116
6123
  }
6117
6124
 
6125
+ /** Clear data at a given position in the array
6126
+ * @param {Vector2} layerPos - Local position in array
6127
+ * @param {boolean} [redraw] - Force the tile to redraw if true */
6128
+ clearData(layerPos, redraw=false)
6129
+ { this.setData(layerPos, new TileLayerData, redraw) }
6130
+
6118
6131
  /** Get data at a given position in the array
6119
6132
  * @param {Vector2} layerPos - Local position in array
6120
6133
  * @return {TileLayerData} */
@@ -6156,7 +6169,7 @@ class TileLayer extends CanvasLayer
6156
6169
  for (let x = this.size.x; x--;)
6157
6170
  for (let y = this.size.y; y--;)
6158
6171
  this.drawTileData(vec2(x,y), false);
6159
- this.hasWebGL() && glFlush();
6172
+ this.isUsingWebGL && glFlush();
6160
6173
  this.onRedraw();
6161
6174
  this.redrawEnd();
6162
6175
  }
@@ -6205,14 +6218,13 @@ class TileLayer extends CanvasLayer
6205
6218
  if (!this.context) return;
6206
6219
  ASSERT(drawContext === this.context);
6207
6220
 
6208
- if (glEnable && this.textureInfo.glTexture)
6209
- glSetRenderTarget();
6210
-
6211
6221
  // set stuff back to normal
6222
+ if (this.isUsingWebGL)
6223
+ glSetRenderTarget();
6212
6224
  [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6213
6225
  }
6214
6226
 
6215
- /** Draw the tile at a given position in the tile grid
6227
+ /** Draw the tile at a given position in the tile layer
6216
6228
  * This can be used to clear out tiles when they are destroyed
6217
6229
  * Tiles can also be redrawn if inside a redrawStart/End block
6218
6230
  * @param {Vector2} layerPos
@@ -6236,7 +6248,7 @@ class TileLayer extends CanvasLayer
6236
6248
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6237
6249
  }
6238
6250
 
6239
- /** Draw the tile at a given position in the tile grid
6251
+ /** Draw the tile at a given position in the tile layer
6240
6252
  * This can be used to clear tiles when they are destroyed
6241
6253
  * For better performance use drawTileData inside a redrawStart/End block
6242
6254
  * @param {Vector2} layerPos
@@ -6264,7 +6276,7 @@ class TileLayer extends CanvasLayer
6264
6276
  angle=0, mirror, additiveColor)
6265
6277
  {
6266
6278
  const drawPos = pos.add(size.scale(.5));
6267
- drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6279
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL);
6268
6280
  }
6269
6281
 
6270
6282
  /** Clear a rectangle in layer space
@@ -6304,14 +6316,15 @@ class TileLayer extends CanvasLayer
6304
6316
  class TileCollisionLayer extends TileLayer
6305
6317
  {
6306
6318
  /** Create a tile layer object
6307
- * @param {Vector2} position - World space position
6308
- * @param {Vector2} size - World space size
6309
- * @param {TileInfo} [tileInfo] - Tile info for layer
6319
+ * @param {Vector2} pos - World space position
6320
+ * @param {Vector2} size - World space size
6321
+ * @param {TileInfo} [tileInfo] - Tile info for layer
6310
6322
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6323
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6311
6324
  */
6312
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6325
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6313
6326
  {
6314
- super(position, size.floor(), tileInfo, renderOrder);
6327
+ super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
6315
6328
 
6316
6329
  /** @property {Array<number>} - The tile collision grid */
6317
6330
  this.collisionData = [];
@@ -6347,41 +6360,50 @@ class TileCollisionLayer extends TileLayer
6347
6360
  this.collisionData.fill(0);
6348
6361
  }
6349
6362
 
6350
- /** Set tile collision data for a given cell in the grid
6351
- * @param {Vector2} gridPos
6363
+ /** Set tile collision data for a given cell in the layer
6364
+ * @param {Vector2} layerPos
6352
6365
  * @param {number} [data] */
6353
- setCollisionData(gridPos, data=1)
6366
+ setCollisionData(layerPos, data=1)
6354
6367
  {
6355
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6356
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6357
- gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6368
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6369
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
6370
+ layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
6358
6371
  }
6359
6372
 
6360
- /** Get tile collision data for a given cell in the grid
6361
- * @param {Vector2} gridPos
6373
+ /** Clear tile collision data for a given cell in the layer
6374
+ * @param {Vector2} layerPos */
6375
+ clearCollisionData(layerPos)
6376
+ { this.setCollisionData(layerPos, 0); }
6377
+
6378
+ /** Get tile collision data for a given cell in the layer
6379
+ * @param {Vector2} layerPos
6362
6380
  * @return {number} */
6363
- getCollisionData(gridPos)
6381
+ getCollisionData(layerPos)
6364
6382
  {
6365
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
6366
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
6367
- return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6383
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
6384
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
6385
+ return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
6368
6386
  }
6369
6387
 
6370
6388
  /** Check if collision with another object should occur
6371
6389
  * @param {Vector2} pos
6372
6390
  * @param {Vector2} [size=vec2()]
6373
- * @param {EngineObject} [object]
6391
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
6374
6392
  * @return {boolean} */
6375
- collisionTest(pos, size=new Vector2, object)
6393
+ collisionTest(pos, size=new Vector2, callbackObject)
6376
6394
  {
6377
6395
  ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
6378
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6379
-
6380
- // transform to local layer space
6381
- const posX = pos.x - this.pos.x;
6382
- const posY = pos.y - this.pos.y;
6396
+ ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
6397
+
6398
+ // make function to check for collision
6399
+ const collisionTest = callbackObject ? typeof callbackObject === 'function' ?
6400
+ (tileData, pos)=> callbackObject(tileData, pos) :
6401
+ (tileData, pos)=> callbackObject.collideWithTile(tileData, pos) :
6402
+ ()=> true;
6383
6403
 
6384
6404
  // check any tiles in the area for collision
6405
+ const posX = pos.x - this.pos.x;
6406
+ const posY = pos.y - this.pos.y;
6385
6407
  const minX = max(posX - size.x/2|0, 0);
6386
6408
  const minY = max(posY - size.y/2|0, 0);
6387
6409
  const maxX = min(posX + size.x/2, this.size.x);
@@ -6392,9 +6414,7 @@ class TileCollisionLayer extends TileLayer
6392
6414
  {
6393
6415
  // check if the object should collide with this tile
6394
6416
  const tileData = this.collisionData[y*this.size.x+x];
6395
- if (tileData)
6396
- if (!object || object.collideWithTile(tileData,
6397
- hitPos.set(x + this.pos.x, y + this.pos.y)))
6417
+ if (tileData && collisionTest(tileData, hitPos.set(x+this.pos.x, y+this.pos.y)))
6398
6418
  return true;
6399
6419
  }
6400
6420
  return false;
@@ -6402,52 +6422,68 @@ class TileCollisionLayer extends TileLayer
6402
6422
 
6403
6423
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
6404
6424
  * The point will be inside the colliding tile if it hits (may have a tiny shift)
6405
- * @param {Vector2} posStart
6406
- * @param {Vector2} posEnd
6407
- * @param {EngineObject} [object] - An object or undefined for generic test
6408
- * @param {Vector2} [normal] - Optional normal of the surface hit
6425
+ * @param {Vector2} posStart
6426
+ * @param {Vector2} posEnd
6427
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
6428
+ * @param {Vector2} [normal] - Optional normal of the surface hit
6409
6429
  * @return {Vector2|undefined} */
6410
- collisionRaycast(posStart, posEnd, object, normal)
6430
+ collisionRaycast(posStart, posEnd, callbackObject, normal)
6411
6431
  {
6412
6432
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6413
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6433
+ ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
6414
6434
 
6415
- const localPos = new Vector2;
6416
- const collisionTest = (pos)=>
6435
+ // make function to check for collision
6436
+ const collisionTest = callbackObject ? typeof callbackObject === 'function' ?
6437
+ (tileData, pos)=> callbackObject(tileData, pos) :
6438
+ (tileData, pos)=> callbackObject.collideWithTile(tileData, pos) :
6439
+ (tileData)=> tileData > 0;
6440
+ const testFunction = (pos)=>
6417
6441
  {
6418
- // check for tile collision
6419
- localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
6420
- const tileData = this.getCollisionData(localPos);
6421
- return tileData && (!object || object.collideWithTile(tileData, pos));
6442
+ const tileData = this.getCollisionData(localPos.set(pos.x - this.pos.x, pos.y - this.pos.y));
6443
+ return tileData && collisionTest(tileData, pos);
6422
6444
  }
6423
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
6424
- const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
6425
- if (hitPos)
6445
+
6446
+ // use line test against tile collision
6447
+ const localPos = new Vector2;
6448
+ const hitPos = lineTest(posStart, posEnd, testFunction, normal);
6449
+ if (debugRaycast && hitPos)
6426
6450
  {
6427
6451
  const tilePos = hitPos.floor().add(vec2(.5));
6428
- debugRaycast && debugRect(tilePos, vec2(1), '#f008');
6429
- debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
6430
- debugRaycast && debugPoint(hitPos, '#0f0');
6431
- debugRaycast && normal &&
6432
- debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6433
- return hitPos;
6452
+ debugRect(tilePos, vec2(1), '#f008');
6453
+ debugLine(posStart, posEnd, '#00f', .02);
6454
+ debugLine(posStart, hitPos, '#f00', .02);
6455
+ debugPoint(hitPos, '#0f0');
6456
+ normal && debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6434
6457
  }
6458
+ return hitPos;
6435
6459
  }
6436
6460
  }
6437
6461
  /**
6438
6462
  * LittleJS Particle System
6463
+ * - A simple but fast and flexible particle system
6464
+ * - Lightweight Particles are created and managed by ParticleEmitters
6465
+ * - The particle design tool can be used to help create emitters
6466
+ * @namespace Particles
6439
6467
  */
6440
6468
 
6441
6469
  /**
6442
- * @callback ParticleCallbackFunction - Function that processes a particle
6470
+ * @callback ParticleCallback - Function that processes a particle
6443
6471
  * @param {Particle} particle
6444
- * @memberof Engine
6472
+ * @memberof Particles
6473
+ */
6474
+
6475
+ /**
6476
+ * @callback ParticleCollideCallback - Collide callback for particles
6477
+ * @param {Particle} particle
6478
+ * @param {number} tileData
6479
+ * @param {Vector2} pos
6480
+ * @memberof Particles
6445
6481
  */
6446
6482
 
6447
6483
  /**
6448
6484
  * Particle Emitter - Spawns particles with the given settings
6449
6485
  * @extends EngineObject
6450
- * @memberof Engine
6486
+ * @memberof Particles
6451
6487
  * @example
6452
6488
  * // create a particle emitter
6453
6489
  * let pos = vec2(2,3);
@@ -6465,7 +6501,7 @@ class TileCollisionLayer extends TileLayer
6465
6501
  class ParticleEmitter extends EngineObject
6466
6502
  {
6467
6503
  /** Create a particle system with the given settings
6468
- * @param {Vector2} position - World space position of the emitter
6504
+ * @param {Vector2} pos - World space position of the emitter
6469
6505
  * @param {number} [angle] - Angle to emit the particles
6470
6506
  * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
6471
6507
  * @param {number} [emitTime] - How long to stay alive (0 is forever)
@@ -6495,7 +6531,7 @@ class ParticleEmitter extends EngineObject
6495
6531
  */
6496
6532
  constructor
6497
6533
  (
6498
- position,
6534
+ pos,
6499
6535
  angle,
6500
6536
  emitSize = 0,
6501
6537
  emitTime = 0,
@@ -6524,12 +6560,13 @@ class ParticleEmitter extends EngineObject
6524
6560
  localSpace = false
6525
6561
  )
6526
6562
  {
6527
- super(position, vec2(), tileInfo, angle, undefined, renderOrder);
6563
+ super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
6528
6564
 
6529
6565
  // emitter settings
6566
+ /** @property {boolean} - Should particles be emitted in a circle */
6567
+ this.emitCircle = typeof emitSize === 'number';
6530
6568
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6531
- this.emitSize = emitSize instanceof Vector2 ?
6532
- emitSize.copy() : emitSize;
6569
+ this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
6533
6570
  /** @property {number} - How long to stay alive (0 is forever) */
6534
6571
  this.emitTime = emitTime;
6535
6572
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -6580,26 +6617,31 @@ class ParticleEmitter extends EngineObject
6580
6617
  this.localSpace = localSpace;
6581
6618
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6582
6619
  this.trailScale = 0;
6583
- /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
6584
- this.particleDestroyCallback = undefined;
6585
- /** @property {ParticleCallbackFunction} - Callback when particle is created */
6620
+ /** @property {ParticleCallback} - Callback when particle is created */
6586
6621
  this.particleCreateCallback = undefined;
6587
- /** @property {number} - Track particle emit time */
6588
- this.emitTimeBuffer = 0;
6622
+ /** @property {ParticleCallback} - Callback when particle is destroyed */
6623
+ this.particleDestroyCallback = undefined;
6624
+ /** @property {ParticleCollideCallback} - Callback when particle collides */
6625
+ this.particleCollideCallback = undefined;
6589
6626
  /** @property {number} - Percentage of velocity to pass to particles (0-1) */
6590
6627
  this.velocityInheritance = 0;
6628
+ /** @property {number} - Track particle emit time */
6629
+ this.emitTimeBuffer = 0;
6630
+ /** @property {Array<Particle>} - Array of particles for this emitter */
6631
+ this.particles = [];
6591
6632
 
6592
6633
  // track previous position and angle
6593
6634
  this.previousAngle = this.angle;
6594
6635
  this.previousPos = this.pos.copy();
6595
6636
  }
6596
6637
 
6597
- /** Emitters do not have physics */
6598
- updatePhysics() {}
6599
-
6600
6638
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6601
6639
  update()
6602
6640
  {
6641
+ // physics sanity checks
6642
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
6643
+ ASSERT(this.damping >= 0 && this.damping <= 1);
6644
+
6603
6645
  if (this.velocityInheritance)
6604
6646
  {
6605
6647
  // pass emitter velocity to particles
@@ -6613,7 +6655,7 @@ class ParticleEmitter extends EngineObject
6613
6655
  }
6614
6656
 
6615
6657
  // update emitter
6616
- if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6658
+ if (this.isActive())
6617
6659
  {
6618
6660
  // emit particles
6619
6661
  if (this.emitRate && particleEmitRateScale)
@@ -6623,14 +6665,21 @@ class ParticleEmitter extends EngineObject
6623
6665
  this.emitParticle();
6624
6666
  }
6625
6667
  }
6626
- else
6627
- this.destroy();
6668
+ else if (this.particles.length === 0)
6669
+ this.destroy(true);
6670
+
6671
+ // update and remove destroyed particles
6672
+ this.particles = this.particles.filter((p)=>
6673
+ {
6674
+ p.update();
6675
+ return !p.destroyed;
6676
+ });
6628
6677
 
6629
6678
  if (debugParticles)
6630
6679
  {
6631
6680
  // show emitter bounds
6632
- if (typeof this.emitSize === 'number')
6633
- debugCircle(this.pos, this.emitSize/2, '#0f0');
6681
+ if (this.emitCircle)
6682
+ debugCircle(this.pos, this.emitSize.x/2, '#0f0');
6634
6683
  else
6635
6684
  debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
6636
6685
  }
@@ -6641,14 +6690,15 @@ class ParticleEmitter extends EngineObject
6641
6690
  emitParticle()
6642
6691
  {
6643
6692
  // spawn a particle
6644
- let pos = typeof this.emitSize === 'number' ? // check if number was used
6645
- randInCircle(this.emitSize/2) // circle emitter
6646
- : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
6693
+ let pos = this.emitCircle ? // check if circle emitter
6694
+ randInCircle(this.emitSize.x/2) // circle emitter
6695
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
6647
6696
  .multiply(this.emitSize).rotate(this.angle)
6648
6697
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
6649
6698
  if (!this.localSpace)
6650
6699
  {
6651
- pos = this.pos.add(pos);
6700
+ pos.x += this.pos.x;
6701
+ pos.y += this.pos.y;
6652
6702
  angle += this.angle;
6653
6703
  }
6654
6704
 
@@ -6668,25 +6718,17 @@ class ParticleEmitter extends EngineObject
6668
6718
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
6669
6719
 
6670
6720
  // build particle
6671
- const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
6672
- particle.velocity = vec2().setAngle(velocityAngle, speed);
6673
- particle.angleVelocity = angleSpeed;
6721
+ const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
6722
+ let angleVelocity = angleSpeed;
6674
6723
  if (!this.localSpace && this.velocityInheritance > 0)
6675
6724
  {
6676
6725
  // apply emitter velocity to particle
6677
- particle.velocity.x += this.velocity.x;
6678
- particle.velocity.y += this.velocity.y;
6679
- particle.angleVelocity += this.angleVelocity;
6726
+ velocity.x += this.velocity.x;
6727
+ velocity.y += this.velocity.y;
6728
+ angleVelocity += this.angleVelocity;
6680
6729
  }
6681
- particle.fadeRate = this.fadeRate;
6682
- particle.damping = this.damping;
6683
- particle.angleDamping = this.angleDamping;
6684
- particle.restitution = this.restitution;
6685
- particle.friction = this.friction;
6686
- particle.gravityScale = this.gravityScale;
6687
- particle.collideTiles = this.collideTiles;
6688
- particle.renderOrder = this.renderOrder;
6689
- particle.mirror = randBool();
6730
+ const particle = new Particle(this, pos, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, velocity, angleVelocity);
6731
+ this.particles.push(particle);
6690
6732
 
6691
6733
  // call particle create callback
6692
6734
  this.particleCreateCallback?.(particle);
@@ -6695,140 +6737,245 @@ class ParticleEmitter extends EngineObject
6695
6737
  return particle;
6696
6738
  }
6697
6739
 
6698
- // Particle emitters are not rendered, only the particles are
6699
- render() {}
6740
+ /** Particle emitters do not have physics */
6741
+ updatePhysics() {}
6742
+
6743
+ /** Render all particles for this emitter */
6744
+ render()
6745
+ {
6746
+ // render all particles
6747
+ for (const particle of this.particles)
6748
+ particle.render();
6749
+ }
6750
+
6751
+ /** is emitter actively spawning */
6752
+ isActive() { return !this.emitTime || this.getAliveTime() < this.emitTime; }
6753
+
6754
+ /** Destroy the particle emitter
6755
+ * @param {boolean} [immediate] - should particle emitters and other attached effects be allowed to die off */
6756
+ destroy(immediate=false)
6757
+ {
6758
+ if (this.destroyed) return;
6759
+
6760
+ super.destroy(immediate);
6761
+ if (!immediate && this.particles.length > 0)
6762
+ {
6763
+ // wait for particles to die off
6764
+ this.destroyed = false;
6765
+ this.emitTime = -1;
6766
+ }
6767
+ }
6700
6768
  }
6701
6769
 
6702
6770
  ///////////////////////////////////////////////////////////////////////////////
6703
6771
  /**
6704
6772
  * Particle Object - Created automatically by Particle Emitters
6705
- * @extends EngineObject
6706
- * @memberof Engine
6773
+ * @memberof Particles
6707
6774
  */
6708
- class Particle extends EngineObject
6775
+ class Particle
6709
6776
  {
6710
6777
  /**
6711
6778
  * Create a particle with the passed in settings
6712
6779
  * Typically this is created automatically by a ParticleEmitter
6713
- * @param {Vector2} position - World space position of the particle
6714
- * @param {TileInfo} tileInfo - Tile info to render particles
6715
- * @param {number} angle - Angle to rotate the particle
6716
- * @param {Color} colorStart - Color at start of life
6717
- * @param {Color} colorEnd - Color at end of life
6718
- * @param {number} lifeTime - How long to live for
6719
- * @param {number} sizeStart - Size at start of life
6720
- * @param {number} sizeEnd - Size at end of life
6721
- * @param {number} fadeRate - How quick to fade in/out
6722
- * @param {boolean} additive - Does it use additive blend mode
6723
- * @param {number} trailScale - If a trail, how long to make it
6724
- * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
6725
- * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
6780
+ * @param {ParticleEmitter} emitter - The emitter that created this particle
6781
+ * @param {Vector2} pos - World or local space position
6782
+ * @param {number} angle - Angle of the particle
6783
+ * @param {Color} colorStart - Color at start of life
6784
+ * @param {Color} colorEnd - Color at end of life
6785
+ * @param {number} lifeTime - How long to live for
6786
+ * @param {number} sizeStart - Size at start of life
6787
+ * @param {number} sizeEnd - Size at end of life
6788
+ * @param {Vector2} [velocity] - Velocity of the particle
6789
+ * @param {number} [angleVelocity] - Angular speed of the particle
6726
6790
  */
6727
- constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
6728
- )
6791
+ constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
6729
6792
  {
6730
- super(position, vec2(), tileInfo, angle);
6731
-
6732
- /** @property {Color} - Color at start of life */
6793
+ /** @property {ParticleEmitter} */
6794
+ this.emitter = emitter;
6795
+ /** @property {Vector2} */
6796
+ this.pos = pos;
6797
+ /** @property {number} */
6798
+ this.angle = angle;
6799
+ /** @property {Vector2} */
6800
+ this.size = vec2(sizeStart);
6801
+ /** @property {Color} */
6802
+ this.color = colorStart.copy();
6803
+ /** @property {Color} */
6733
6804
  this.colorStart = colorStart;
6734
- /** @property {Color} - Color at end of life */
6805
+ /** @property {Color} */
6735
6806
  this.colorEnd = colorEnd;
6736
- /** @property {number} - How long to live for */
6807
+ /** @property {number} */
6737
6808
  this.lifeTime = lifeTime;
6738
- /** @property {number} - Size at start of life */
6809
+ /** @property {number} */
6739
6810
  this.sizeStart = sizeStart;
6740
- /** @property {number} - Size at end of life */
6811
+ /** @property {number} */
6741
6812
  this.sizeEnd = sizeEnd;
6742
- /** @property {number} - How quick to fade in/out */
6743
- this.fadeRate = fadeRate;
6744
- /** @property {boolean} - Is it additive */
6745
- this.additive = additive;
6746
- /** @property {number} - If a trail, how long to make it */
6747
- this.trailScale = trailScale;
6748
- /** @property {ParticleEmitter} - Parent emitter if local space */
6749
- this.localSpaceEmitter = localSpaceEmitter;
6750
- /** @property {ParticleCallbackFunction} - Called when particle dies */
6751
- this.destroyCallback = destroyCallback;
6752
- // particles do not clamp speed by default
6753
- this.clampSpeed = false;
6754
- }
6755
-
6756
- /** Update the object physics, called automatically by engine once each frame */
6813
+ /** @property {Vector2} */
6814
+ this.velocity = velocity;
6815
+ /** @property {number} */
6816
+ this.angleVelocity = angleVelocity;
6817
+ /** @property {number} */
6818
+ this.spawnTime = time;
6819
+ /** @property {boolean} */
6820
+ this.mirror = randBool();
6821
+ /** @property {EngineObject} */
6822
+ this.groundObject = undefined;
6823
+ /** @property {boolean} */
6824
+ this.destroyed = false;
6825
+ /** @property {TileInfo} */
6826
+ this.tileInfo = emitter.tileInfo;
6827
+ }
6828
+
6829
+ /** Update the particle */
6757
6830
  update()
6758
6831
  {
6759
- if (this.collideTiles || this.collideSolidObjects)
6832
+ // emitter properties
6833
+ const emitter = this.emitter;
6834
+ const damping = emitter.damping;
6835
+ const angleDamping = emitter.angleDamping;
6836
+ const restitution = emitter.restitution;
6837
+ const friction = emitter.friction;
6838
+ const gravityScale = emitter.gravityScale;
6839
+ const collideTiles = emitter.collideTiles;
6840
+ const collideCallback = emitter.particleCollideCallback;
6841
+
6842
+ // destroy particle when its time runs out
6843
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
6760
6844
  {
6761
- // only apply max circular speed if particle can collide
6762
- const length2 = this.velocity.lengthSquared();
6763
- if (length2 > objectMaxSpeed*objectMaxSpeed)
6764
- {
6765
- const s = objectMaxSpeed / length2**.5;
6766
- this.velocity.x *= s;
6767
- this.velocity.y *= s;
6768
- }
6845
+ this.destroy();
6846
+ return;
6769
6847
  }
6770
6848
 
6771
- if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
6849
+ // apply physics
6850
+ const oldPos = this.pos.copy();
6851
+ this.velocity.x *= damping;
6852
+ this.velocity.y *= damping;
6853
+ this.pos.x += this.velocity.x += gravity.x * gravityScale;
6854
+ this.pos.y += this.velocity.y += gravity.y * gravityScale;
6855
+ this.angle += this.angleVelocity *= angleDamping;
6856
+
6857
+ // don't do collision if solver disabled
6858
+ if (!enablePhysicsSolver || !collideTiles) return;
6859
+
6860
+ // apply max circular speed to prevent going through collision
6861
+ const length2 = this.velocity.lengthSquared();
6862
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
6863
+ {
6864
+ const s = objectMaxSpeed / length2**.5;
6865
+ this.velocity.x *= s;
6866
+ this.velocity.y *= s;
6867
+ }
6868
+
6869
+ // check collision against tiles
6870
+ this.groundObject = undefined;
6871
+ const testCollision = collideCallback ? (pos)=>
6872
+ {
6873
+ const data = tileCollisionGetData(pos);
6874
+ return data && collideCallback(this, data, pos);
6875
+ } : (pos)=> tileCollisionGetData(pos) > 0;
6876
+
6877
+ if (testCollision(this.pos))
6772
6878
  {
6773
- // destroy particle when its time runs out
6774
- const c = this.colorEnd;
6775
- this.color.set(c.r, c.g, c.b, c.a);
6776
- this.size.set(this.sizeEnd, this.sizeEnd);
6777
- this.destroyCallback?.(this);
6778
- this.destroyed = 1;
6879
+ // if already was stuck in collision, don't do anything
6880
+ const hitLayer = tileCollisionTest(this.pos);
6881
+ if (!testCollision(oldPos))
6882
+ {
6883
+ if (!collideCallback || collideCallback?.(this, hitLayer))
6884
+ {
6885
+ // test which side we bounced off (or both if a corner)
6886
+ const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
6887
+ const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
6888
+ const hitRestitution = max(restitution, hitLayer.restitution);
6889
+ const hitFriction = max(friction, hitLayer.friction);
6890
+ if (isBlockedX)
6891
+ {
6892
+ // move to previous X position and bounce
6893
+ this.pos.x = oldPos.x;
6894
+ this.velocity.x *= -hitRestitution;
6895
+ this.velocity.y *= hitFriction;
6896
+ }
6897
+ if (isBlockedY || !isBlockedX)
6898
+ {
6899
+ const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
6900
+ if (wasFalling)
6901
+ this.groundObject = hitLayer;
6902
+
6903
+ // move to previous Y position and bounce
6904
+ this.pos.y = oldPos.y;
6905
+ this.velocity.y *= -hitRestitution;
6906
+ this.velocity.x *= hitFriction;
6907
+ }
6908
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
6909
+ }
6910
+ }
6779
6911
  }
6780
6912
  }
6781
6913
 
6782
- /** Render the particle, automatically called each frame, sorted by renderOrder */
6914
+ /** Destroy this particle */
6915
+ destroy()
6916
+ {
6917
+ const destroyCallback = this.emitter.particleDestroyCallback;
6918
+ const c = this.colorEnd;
6919
+ this.color.set(c.r, c.g, c.b, c.a);
6920
+ this.size.set(this.sizeEnd, this.sizeEnd);
6921
+ this.destroyed = true;
6922
+ destroyCallback?.(this);
6923
+ }
6924
+
6925
+ /** Render the particle, automatically called each frame */
6783
6926
  render()
6784
6927
  {
6928
+ // emitter properties
6929
+ const emitter = this.emitter;
6930
+ const localSpace = emitter.localSpace;
6931
+ const additive = emitter.additive;
6932
+ const trailScale = emitter.trailScale;
6933
+ const fadeRate = emitter.fadeRate / 2;
6934
+
6785
6935
  // lerp color and size
6786
6936
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6787
6937
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6788
6938
  const size = vec2(radius);
6939
+ const alphaFade = p1 < fadeRate ? p1/fadeRate :
6940
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6789
6941
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6790
6942
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6791
6943
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6792
- this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6793
-
6794
- // fade alpha
6795
- const fadeRate = this.fadeRate/2;
6796
- this.color.a *= p1 < fadeRate ? p1/fadeRate :
6797
- p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6944
+ this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
6798
6945
 
6799
6946
  // draw the particle
6800
- this.additive && setBlendMode(true);
6947
+ additive && setBlendMode(true);
6801
6948
 
6802
6949
  // update the position and angle for drawing
6803
- let pos = this.pos, angle = this.angle;
6804
- if (this.localSpaceEmitter)
6950
+ const pos = this.pos.copy();
6951
+ let angle = this.angle;
6952
+ if (localSpace)
6805
6953
  {
6806
6954
  // in local space of emitter
6807
- const a = this.localSpaceEmitter.angle;
6955
+ const a = emitter.angle;
6808
6956
  const c = cos(a), s = sin(a);
6809
- pos = this.localSpaceEmitter.pos.add(
6810
- new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6811
- angle += this.localSpaceEmitter.angle;
6957
+ pos.set(emitter.pos.x + pos.x*c - pos.y*s,
6958
+ emitter.pos.y + pos.x*s + pos.y*c);
6959
+ angle += a;
6812
6960
  }
6813
- if (this.trailScale)
6961
+ if (trailScale)
6814
6962
  {
6815
6963
  // trail style particles
6816
- const direction = this.localSpaceEmitter ?
6817
- this.velocity.rotate(-this.localSpaceEmitter.angle) :
6818
- this.velocity;
6819
- const speed = direction.length();
6964
+ const velocity = localSpace ?
6965
+ this.velocity.rotate(-emitter.angle) : this.velocity;
6966
+ const speed = velocity.length();
6820
6967
  if (speed)
6821
6968
  {
6822
6969
  // stretch in direction of motion
6823
- const trailLength = speed * this.trailScale;
6970
+ const trailLength = speed * trailScale;
6824
6971
  size.y = max(size.x, trailLength);
6825
- angle = atan2(direction.x, direction.y);
6972
+ angle = atan2(velocity.x, velocity.y);
6826
6973
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6827
6974
  }
6828
6975
  }
6829
6976
  else
6830
6977
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6831
- this.additive && setBlendMode();
6978
+ additive && setBlendMode();
6832
6979
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
6833
6980
  }
6834
6981
  }
@@ -7282,7 +7429,7 @@ function glPreRender(clear=true)
7282
7429
  p.x, p.y, 0, 1];
7283
7430
 
7284
7431
  // set the same transform matrix for both shaders
7285
- const initUniform = (program, uniform, value) =>
7432
+ const initUniform = (program, uniform, value)=>
7286
7433
  {
7287
7434
  glContext.useProgram(program);
7288
7435
  const location = glContext.getUniformLocation(program, uniform);
@@ -7676,7 +7823,7 @@ function glSetRenderTarget(texture, clear=false)
7676
7823
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
7677
7824
  glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
7678
7825
  glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
7679
- glPreRender(clear);
7826
+ glPreRender(clear);
7680
7827
  }
7681
7828
  else
7682
7829
  {
@@ -8108,7 +8255,7 @@ class PostProcessPlugin
8108
8255
  {
8109
8256
  /** Create global post processing shader
8110
8257
  * @param {string} shaderCode
8111
- * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8258
+ * @param {boolean} [includeMainCanvas] - combine mainCanvas onto glCanvas
8112
8259
  * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8113
8260
  * @example
8114
8261
  * // create the post process plugin object
@@ -8295,7 +8442,7 @@ class ZzFXMusic extends Sound
8295
8442
  /** Play the music that loops by default
8296
8443
  * @param {number} [volume] - Volume to play the music at
8297
8444
  * @param {boolean} [loop] - Should the music loop?
8298
- * @return {AudioBufferSourceNode} - The audio source node
8445
+ * @return {SoundInstance} - The sound instance
8299
8446
  */
8300
8447
  playMusic(volume=1, loop=true)
8301
8448
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -8342,7 +8489,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
8342
8489
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
8343
8490
 
8344
8491
  // for each pattern in sequence
8345
- sequence.forEach((patternIndex, sequenceIndex) => {
8492
+ sequence.forEach((patternIndex, sequenceIndex)=> {
8346
8493
  // get pattern for current channel, use empty 1 note pattern if none found
8347
8494
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
8348
8495
 
@@ -8435,7 +8582,7 @@ let uiDebug = 0;
8435
8582
 
8436
8583
  /** Enable UI system debug drawing
8437
8584
  * 0=off, 1=normal, 2=show invisible
8438
- * @param {number|boolean} enable
8585
+ * @param {number|boolean} debugMode
8439
8586
  * @memberof UISystem */
8440
8587
  function uiSetDebug(debugMode)
8441
8588
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -8501,7 +8648,7 @@ class UISystemPlugin
8501
8648
  // navigation properties
8502
8649
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
8503
8650
  this.navigationObject = undefined;
8504
- /** @property {Timer} - Cooldown timer for navigation inputs */
8651
+ /** @property {Timer} - Cool down timer for navigation inputs */
8505
8652
  this.navigationTimer = new Timer(undefined, true);
8506
8653
  /** @property {number} - Time between navigation inputs in seconds */
8507
8654
  this.navigationDelay = .2;
@@ -8967,7 +9114,7 @@ class UISystemPlugin
8967
9114
 
8968
9115
  const savedNavigationDirection = uiSystem.navigationDirection;
8969
9116
 
8970
- // allow both axies for navigation
9117
+ // allow both axes for navigation
8971
9118
  uiSystem.navigationDirection = 2;
8972
9119
 
8973
9120
  // confirm menu
@@ -9062,7 +9209,7 @@ class UIObject
9062
9209
  this.lineWidth = uiSystem.defaultLineWidth;
9063
9210
  /** @property {number} - Corner radius for rounded rects */
9064
9211
  this.cornerRadius = uiSystem.defaultCornerRadius;
9065
- /** @property {string} - Font for this objecct */
9212
+ /** @property {string} - Font for this object */
9066
9213
  this.font = uiSystem.defaultFont;
9067
9214
  /** @property {string} - Font style for this object or undefined */
9068
9215
  this.fontStyle = undefined;
@@ -9550,6 +9697,8 @@ class UIScrollbar extends UIObject
9550
9697
  this.value = value;
9551
9698
  /** @property {Color} - Color for the handle part of the scrollbar */
9552
9699
  this.handleColor = handleColor.copy();
9700
+ /** @property {boolean} - Should it fill up like a progress bar? */
9701
+ this.fillMode = false;
9553
9702
 
9554
9703
  // set properties
9555
9704
  this.text = text;
@@ -9595,19 +9744,30 @@ class UIScrollbar extends UIObject
9595
9744
 
9596
9745
  // handle horizontal or vertical scrollbar
9597
9746
  const isHorizontal = this.size.x > this.size.y;
9598
- const handleSize = isHorizontal ? this.size.y : this.size.x;
9599
- const barSize = isHorizontal ? this.size.x : this.size.y;
9600
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
9601
-
9602
- // draw the scrollbar handle
9603
- const handleWidth = barSize - handleSize;
9604
- const p1 = centerPos - handleWidth/2;
9605
- const p2 = centerPos + handleWidth/2;
9606
- const handlePos = isHorizontal ?
9607
- vec2(lerp(p1, p2, this.value), this.pos.y) :
9608
- vec2(this.pos.x, lerp(p2, p1, this.value))
9609
- const handleColor = this.disabled ? this.disabledColor : this.handleColor;
9610
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
9747
+ const barWidth = isHorizontal ? this.size.x : this.size.y;
9748
+ const handleWidth = isHorizontal ? this.size.y : this.size.x;
9749
+ if (this.fillMode)
9750
+ {
9751
+ // draw progress bar
9752
+ const minWidth = min(handleWidth, this.cornerRadius * 2);
9753
+ const progressWidth = lerp(minWidth, barWidth, this.value);
9754
+ const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
9755
+ const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
9756
+ const color = this.disabled ? this.disabledColor : this.handleColor;
9757
+ const drawSize = isHorizontal ?
9758
+ vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
9759
+ uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
9760
+ }
9761
+ else
9762
+ {
9763
+ // draw the scrollbar handle
9764
+ const value = isHorizontal ? this.value : 1 - this.value;
9765
+ const p = (barWidth - handleWidth) * (value - .5);
9766
+ const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
9767
+ const color = this.disabled ? this.disabledColor : this.handleColor;
9768
+ const drawSize = vec2(handleWidth);
9769
+ uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
9770
+ }
9611
9771
 
9612
9772
  // draw the text scaled to fit on the scrollbar
9613
9773
  const textSize = this.getTextSize();
@@ -9629,15 +9789,15 @@ class UIScrollbar extends UIObject
9629
9789
  * @extends UIObject
9630
9790
  * @example
9631
9791
  * // Create a video player UI object
9632
- * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
9792
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'video.mp4', true);
9633
9793
  * video.play();
9634
9794
  * @memberof UISystem
9635
9795
  */
9636
9796
  class UIVideo extends UIObject
9637
9797
  {
9638
9798
  /** Create a video player UI object
9639
- * @param {Vector2} [pos]
9640
- * @param {Vector2} [size]
9799
+ * @param {Vector2} pos
9800
+ * @param {Vector2} size
9641
9801
  * @param {string} src - Video file path or URL
9642
9802
  * @param {boolean} [autoplay=false] - Start playing immediately?
9643
9803
  * @param {boolean} [loop=false] - Loop the video?
@@ -9777,6 +9937,7 @@ class UIVideo extends UIObject
9777
9937
  * - Contact begin and end callbacks
9778
9938
  * - Wraps b2Vec2 type to/from Vector2
9779
9939
  * - Raycasting and querying
9940
+ * - Box2dTileLayer for grid based collision
9780
9941
  * - Every type of joint
9781
9942
  * - Debug physics drawing
9782
9943
  * @namespace Box2D
@@ -9826,21 +9987,24 @@ class Box2dObject extends EngineObject
9826
9987
  bodyDef.set_type(bodyType);
9827
9988
  bodyDef.set_position(box2d.vec2dTo(pos));
9828
9989
  bodyDef.set_angle(-angle);
9990
+
9991
+ /** @property {Object} - The Box2d body */
9829
9992
  this.body = box2d.world.CreateBody(bodyDef);
9830
- this.body.object = this;
9993
+ /** @property {Color} - Line color used for default box2d drawing */
9831
9994
  this.lineColor = BLACK;
9832
- box2d.objects.push(this);
9833
-
9834
- // edge lists and loops for drawing
9995
+ /** @property {Array<Object>} - List of all edges for default box2d drawing */
9835
9996
  this.edgeLists = [];
9997
+ /** @property {Array<Object>} - List of all edge loops for default box2d drawing */
9836
9998
  this.edgeLoops = [];
9999
+
10000
+ this.body.object = this; // link body to this object
10001
+ box2d.objects.push(this); // keep track of all box2d objects
9837
10002
  }
9838
10003
 
9839
10004
  /** Destroy this object and its physics body */
9840
10005
  destroy()
9841
10006
  {
9842
- if (this.destroyed)
9843
- return;
10007
+ if (this.destroyed) return;
9844
10008
 
9845
10009
  // destroy physics body, fixtures, and joints
9846
10010
  ASSERT(this.body, 'Box2dObject has no body to destroy');
@@ -9871,11 +10035,12 @@ class Box2dObject extends EngineObject
9871
10035
  }
9872
10036
 
9873
10037
  /** Draws all this object's fixtures
9874
- * @param {Color} [color]
9875
- * @param {Color} [lineColor]
9876
- * @param {number} [lineWidth]
10038
+ * @param {Color} [color]
10039
+ * @param {Color} [lineColor]
10040
+ * @param {number} [lineWidth]
10041
+ * @param {boolean} [useWebGL=glEnable]
9877
10042
  * @param {CanvasRenderingContext2D} [context] */
9878
- drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
10043
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
9879
10044
  {
9880
10045
  // draw non-edge fixtures
9881
10046
  this.getFixtureList().forEach((fixture)=>
@@ -9883,7 +10048,7 @@ class Box2dObject extends EngineObject
9883
10048
  const shape = box2d.castObjectType(fixture.GetShape());
9884
10049
  if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
9885
10050
  {
9886
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
10051
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
9887
10052
  }
9888
10053
  });
9889
10054
 
@@ -10109,6 +10274,14 @@ class Box2dObject extends EngineObject
10109
10274
  return fixtures;
10110
10275
  }
10111
10276
 
10277
+ /** Destroy a fixture from the body
10278
+ * @param {Object} [fixture] */
10279
+ destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
10280
+
10281
+ /** Destroy all fixture from the body */
10282
+ destroyAllFixtures()
10283
+ { this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
10284
+
10112
10285
  ///////////////////////////////////////////////////////////////////////////////
10113
10286
  // physics get functions
10114
10287
 
@@ -10341,6 +10514,135 @@ class Box2dObject extends EngineObject
10341
10514
  }
10342
10515
  }
10343
10516
 
10517
+ ///////////////////////////////////////////////////////////////////////////////
10518
+ /**
10519
+ * Box2D Static Object - Box2d with a static physics body
10520
+ * @extends Box2dObject
10521
+ * @memberof Box2D
10522
+ */
10523
+ class Box2dStaticObject extends Box2dObject
10524
+ {
10525
+ /** Create a LittleJS object with Box2d physics
10526
+ * @param {Vector2} [pos]
10527
+ * @param {Vector2} [size]
10528
+ * @param {TileInfo} [tileInfo]
10529
+ * @param {number} [angle]
10530
+ * @param {Color} [color]
10531
+ * @param {number} [renderOrder] */
10532
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10533
+ {
10534
+ const bodyType = box2d.bodyTypeStatic;
10535
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10536
+ }
10537
+ }
10538
+
10539
+ ///////////////////////////////////////////////////////////////////////////////
10540
+ /**
10541
+ * Box2D Kinematic Object - Box2d with a kinematic physics body
10542
+ * @extends Box2dObject
10543
+ * @memberof Box2D
10544
+ */
10545
+ class Box2dKinematicObject extends Box2dObject
10546
+ {
10547
+ /** Create a LittleJS object with Box2d physics
10548
+ * @param {Vector2} [pos]
10549
+ * @param {Vector2} [size]
10550
+ * @param {TileInfo} [tileInfo]
10551
+ * @param {number} [angle]
10552
+ * @param {Color} [color]
10553
+ * @param {number} [renderOrder] */
10554
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10555
+ {
10556
+ const bodyType = box2d.bodyTypeKinematic;
10557
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10558
+ }
10559
+ }
10560
+
10561
+ ///////////////////////////////////////////////////////////////////////////////
10562
+ /**
10563
+ * Box2d Tile Layer
10564
+ * - adds Box2d support to tile layers
10565
+ * - creates static box2d fixtures for solid tiles
10566
+ * @extends Box2dObject
10567
+ * @memberof Box2D
10568
+ */
10569
+ class Box2dTileLayer extends Box2dStaticObject
10570
+ {
10571
+ /** Create a Box2d tile layer object
10572
+ * @param {TileCollisionLayer} tileLayer - Tile layer for this object */
10573
+ constructor(tileLayer)
10574
+ {
10575
+ ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
10576
+ super(tileLayer.pos, tileLayer.size);
10577
+
10578
+ /** @property {TileLayer} - The tile layer */
10579
+ this.tileLayer = tileLayer;
10580
+ this.addChild(tileLayer);
10581
+ }
10582
+
10583
+ render()
10584
+ {
10585
+ // do not render fixtures, tile layer handles rendering
10586
+ }
10587
+
10588
+ /** Create box2d collision fixtures for solid tiles
10589
+ * @param {number} [friction]
10590
+ * @param {number} [restitution] */
10591
+ buildCollision(friction=.2, restitution=0)
10592
+ {
10593
+ // destroy all fixtures and create new ones
10594
+ this.destroyAllFixtures();
10595
+
10596
+ // create box2d object for this layer
10597
+ this.pos = this.tileLayer.pos.copy();
10598
+ this.size = this.tileLayer.size.copy();
10599
+
10600
+ // track which tiles have been processed
10601
+ const processed = [];
10602
+ const getIndex = (x, y)=> x + y * this.size.x;
10603
+ const isSolidUnprocessed = (x, y)=>
10604
+ !processed[getIndex(x, y)] &&
10605
+ this.tileLayer.getCollisionData(vec2(x, y)) > 0;
10606
+
10607
+ // combine tiles into larger boxes
10608
+ for (let x = 0; x < this.size.x; ++x)
10609
+ for (let y = 0; y < this.size.y; ++y)
10610
+ {
10611
+ if (!isSolidUnprocessed(x, y)) continue;
10612
+
10613
+ // find max width by scanning right
10614
+ let width = 1, height = 1, canExpand = true;
10615
+ while (isSolidUnprocessed(x + width, y))
10616
+ ++width;
10617
+
10618
+ // find max height by scanning up, ensuring all rows have the same width
10619
+ while (canExpand)
10620
+ {
10621
+ for (let checkX = 0; checkX < width; ++checkX)
10622
+ {
10623
+ if (!isSolidUnprocessed(x + checkX, y + height))
10624
+ {
10625
+ canExpand = false;
10626
+ break;
10627
+ }
10628
+ }
10629
+ if (canExpand)
10630
+ ++height;
10631
+ }
10632
+
10633
+ // mark all tiles in this rectangle as processed
10634
+ for (let rectX = width; rectX--;)
10635
+ for (let rectY = height; rectY--;)
10636
+ processed[getIndex(x + rectX, y + rectY)] = true;
10637
+
10638
+ // create a single fixture for the entire rectangle
10639
+ const shapeSize = vec2(width, height);
10640
+ const offset = vec2(x + width/2, y + height/2);
10641
+ this.addBox(shapeSize, offset, 0, 0, friction, restitution);
10642
+ }
10643
+ }
10644
+ }
10645
+
10344
10646
  ///////////////////////////////////////////////////////////////////////////////
10345
10647
  /**
10346
10648
  * Box2D Raycast Result
@@ -10382,6 +10684,7 @@ class Box2dJoint
10382
10684
  * @param {Object} jointDef */
10383
10685
  constructor(jointDef)
10384
10686
  {
10687
+ /** @property {Object} - The Box2d joint */
10385
10688
  this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
10386
10689
  }
10387
10690
 
@@ -10728,7 +11031,7 @@ class Box2dGearJoint extends Box2dJoint
10728
11031
  * @param {Box2dObject} objectB
10729
11032
  * @param {Box2dJoint} joint1
10730
11033
  * @param {Box2dJoint} joint2
10731
- * @param {ratio} [ratio] */
11034
+ * @param {number} [ratio] */
10732
11035
  constructor(objectA, objectB, joint1, joint2, ratio=1)
10733
11036
  {
10734
11037
  const jointDef = new box2d.instance.b2GearJointDef();
@@ -10870,50 +11173,6 @@ class Box2dPrismaticJoint extends Box2dJoint
10870
11173
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
10871
11174
  }
10872
11175
 
10873
- ///////////////////////////////////////////////////////////////////////////////
10874
- /**
10875
- * Box2D Static Object - Box2d with a static physics body
10876
- * @extends Box2dObject
10877
- * @memberof Box2D
10878
- */
10879
- class Box2dStaticObject extends Box2dObject
10880
- {
10881
- /** Create a LittleJS object with Box2d physics
10882
- * @param {Vector2} [pos]
10883
- * @param {Vector2} [size]
10884
- * @param {TileInfo} [tileInfo]
10885
- * @param {number} [angle]
10886
- * @param {Color} [color]
10887
- * @param {number} [renderOrder] */
10888
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10889
- {
10890
- const bodyType = box2d.bodyTypeStatic;
10891
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10892
- }
10893
- }
10894
-
10895
- ///////////////////////////////////////////////////////////////////////////////
10896
- /**
10897
- * Box2D Kiematic Object - Box2d with a kinematic physics body
10898
- * @extends Box2dObject
10899
- * @memberof Box2D
10900
- */
10901
- class Box2dKiematicObject extends Box2dObject
10902
- {
10903
- /** Create a LittleJS object with Box2d physics
10904
- * @param {Vector2} [pos]
10905
- * @param {Vector2} [size]
10906
- * @param {TileInfo} [tileInfo]
10907
- * @param {number} [angle]
10908
- * @param {Color} [color]
10909
- * @param {number} [renderOrder] */
10910
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
10911
- {
10912
- const bodyType = box2d.bodyTypeKinematic;
10913
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
10914
- }
10915
- }
10916
-
10917
11176
  ///////////////////////////////////////////////////////////////////////////////
10918
11177
  /**
10919
11178
  * Box2D Wheel Joint
@@ -11274,10 +11533,13 @@ class Box2dPlugin
11274
11533
  {
11275
11534
  ASSERT(!box2d, 'Box2D already initialized');
11276
11535
  box2d = this;
11536
+
11537
+ /** @property {Object} - The Box2d instance */
11277
11538
  this.instance = instance;
11539
+ /** @property {Object} - The Box2d world */
11278
11540
  this.world = new box2d.instance.b2World();
11541
+ /** @property {Array<Box2dObject>} - List of all Box2d objects */
11279
11542
  this.objects = [];
11280
-
11281
11543
  /** @property {number} - Velocity iterations per update*/
11282
11544
  this.velocityIterations = 8;
11283
11545
  /** @property {number} - Position iterations per update*/
@@ -11476,8 +11738,9 @@ class Box2dPlugin
11476
11738
  * @param {Color} [color]
11477
11739
  * @param {Color} [lineColor]
11478
11740
  * @param {number} [lineWidth]
11741
+ * @param {boolean} [useWebGL=glEnable]
11479
11742
  * @param {CanvasRenderingContext2D} [context] */
11480
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
11743
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
11481
11744
  {
11482
11745
  const shape = box2d.castObjectType(fixture.GetShape());
11483
11746
  switch (shape.GetType())
@@ -11487,20 +11750,20 @@ class Box2dPlugin
11487
11750
  let points = [];
11488
11751
  for (let i=shape.GetVertexCount(); i--;)
11489
11752
  points.push(box2d.vec2From(shape.GetVertex(i)));
11490
- drawPoly(points, color, lineWidth, lineColor, pos, angle);
11753
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
11491
11754
  break;
11492
11755
  }
11493
11756
  case box2d.instance.b2Shape.e_circle:
11494
11757
  {
11495
11758
  const radius = shape.get_m_radius();
11496
- drawCircle(pos, radius*2, color, lineWidth, lineColor);
11759
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
11497
11760
  break;
11498
11761
  }
11499
11762
  case box2d.instance.b2Shape.e_edge:
11500
11763
  {
11501
11764
  const v1 = box2d.vec2From(shape.get_m_vertex1());
11502
11765
  const v2 = box2d.vec2From(shape.get_m_vertex2());
11503
- drawLine(v1, v2, lineWidth, lineColor, pos, angle);
11766
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
11504
11767
  break;
11505
11768
  }
11506
11769
  }
@@ -11518,7 +11781,7 @@ class Box2dPlugin
11518
11781
  }
11519
11782
 
11520
11783
  /** converts a box2d vec2 pointer to a Vector2
11521
- * @param {Object} v */
11784
+ * @param {Object} vp */
11522
11785
  vec2FromPointer(vp)
11523
11786
  {
11524
11787
  const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
@@ -11630,7 +11893,7 @@ async function box2dInit()
11630
11893
  const box2dColorPointer = (c)=>
11631
11894
  box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
11632
11895
  const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
11633
- const getPointsList = (vertices, vertexCount) =>
11896
+ const getPointsList = (vertices, vertexCount)=>
11634
11897
  {
11635
11898
  const points = [];
11636
11899
  for (let i=vertexCount; i--;)