littlejsengine 1.17.5 → 1.17.11

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 (42) hide show
  1. package/LICENSE +10 -10
  2. package/dist/littlejs.d.ts +189 -97
  3. package/dist/littlejs.esm.js +637 -477
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +633 -476
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +631 -474
  8. package/examples/electron/index.html +2 -2
  9. package/examples/games.jpg +0 -0
  10. package/examples/index.html +2 -2
  11. package/examples/logo.png +0 -0
  12. package/examples/logo2.png +0 -0
  13. package/examples/particles/index.html +1 -1
  14. package/examples/platformer/gameEffects.js +3 -3
  15. package/examples/screenshot.jpg +0 -0
  16. package/examples/shorts/base.html +1 -1
  17. package/examples/shorts/colors.js +1 -1
  18. package/examples/shorts/fontImage.js +1 -1
  19. package/examples/shorts/fps.js +1 -1
  20. package/examples/shorts/music.js +3 -2
  21. package/examples/shorts/musicPlayer.js +2 -1
  22. package/examples/shorts/parallax.js +2 -2
  23. package/examples/shorts/tiltedView.js +1 -1
  24. package/examples/starter/index.html +2 -2
  25. package/package.json +1 -1
  26. package/plugins/box2d.js +6 -7
  27. package/plugins/pluginExport.js +2 -1
  28. package/plugins/postProcess.js +1 -1
  29. package/plugins/uiSystem.js +33 -21
  30. package/reference.md +6 -6
  31. package/src/engine.js +106 -152
  32. package/src/engineAudio.js +5 -5
  33. package/src/engineDebug.js +1 -1
  34. package/src/engineDraw.js +36 -14
  35. package/src/engineExport.js +2 -0
  36. package/src/engineInput.js +1 -1
  37. package/src/engineMath.js +6 -4
  38. package/src/engineObject.js +24 -24
  39. package/src/engineParticles.js +241 -118
  40. package/src/engineTileLayer.js +66 -48
  41. package/src/engineUtilities.js +105 -78
  42. 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.5';
36
+ const engineVersion = '1.17.11';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -396,33 +396,11 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
396
396
  workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
397
397
 
398
398
  // create promises for loading images
399
- const promises = imageSources.map((src, textureIndex)=>
400
- new Promise(resolve =>
401
- {
402
- ASSERT(isString(src), 'imageSources must be an array of strings');
403
-
404
- const image = new Image;
405
- image.onerror = image.onload = ()=>
406
- {
407
- const textureInfo = new TextureInfo(image);
408
- textureInfos[textureIndex] = textureInfo;
409
- resolve();
410
- }
411
- image.crossOrigin = 'anonymous';
412
- image.src = src;
413
- })
414
- );
399
+ const promises = imageSources.map((src, i)=> loadTexture(i, src));
415
400
 
401
+ // no images to load
416
402
  if (!imageSources.length)
417
- {
418
- // no images to load
419
- promises.push(new Promise(resolve =>
420
- {
421
- const textureInfo = new TextureInfo(new Image);
422
- textureInfos[0] = textureInfo;
423
- resolve();
424
- }));
425
- }
403
+ promises.push(loadTexture(0));
426
404
 
427
405
  // load engine font image
428
406
  promises.push(fontImageInit());
@@ -464,13 +442,13 @@ function engineObjectsUpdate()
464
442
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
465
443
 
466
444
  // recursive object update
467
- function updateObject(o)
445
+ function updateChildObject(o)
468
446
  {
469
447
  if (o.destroyed) return;
470
448
 
471
449
  o.update();
472
450
  for (const child of o.children)
473
- updateObject(child);
451
+ updateChildObject(child);
474
452
  }
475
453
  for (const o of engineObjects)
476
454
  {
@@ -480,7 +458,7 @@ function engineObjectsUpdate()
480
458
  o.update();
481
459
  o.updatePhysics();
482
460
  for (const child of o.children)
483
- updateObject(child);
461
+ updateChildObject(child);
484
462
  o.updateTransforms();
485
463
  }
486
464
 
@@ -489,11 +467,14 @@ function engineObjectsUpdate()
489
467
  }
490
468
 
491
469
  /** Destroy and remove all objects
470
+ * - This can be used to clear out all objects when restarting a level
471
+ * - Objects can override their destroy function to do cleanup or stick around
472
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
492
473
  * @memberof Engine */
493
- function engineObjectsDestroy()
474
+ function engineObjectsDestroy(immediate=true)
494
475
  {
495
476
  for (const o of engineObjects)
496
- o.parent || o.destroy();
477
+ o.parent || o.destroy(immediate);
497
478
  engineObjects = engineObjects.filter(o=>!o.destroyed);
498
479
  }
499
480
 
@@ -568,165 +549,138 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
568
549
  ///////////////////////////////////////////////////////////////////////////////
569
550
  function drawEngineLogo(t)
570
551
  {
552
+ const blackAndWhite = 0;
553
+ const showName = 1;
554
+
571
555
  // LittleJS Logo and Splash Screen
572
556
  const x = mainContext;
573
557
  const w = mainCanvas.width = innerWidth;
574
558
  const h = mainCanvas.height = innerHeight;
575
-
576
559
  {
577
560
  // background
578
561
  const p3 = percent(t, 1, .8);
579
562
  const p4 = percent(t, 0, .5);
580
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
563
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
581
564
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
582
565
  g.addColorStop(1,hsl(0,0,0,p3).toString());
583
566
  x.save();
584
567
  x.fillStyle = g;
585
568
  x.fillRect(0,0,w,h);
586
569
  }
587
-
588
- // draw LittleJS logo...
589
- const rect = (X, Y, W, H, C)=>
570
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
571
+ {
572
+ if (C >= 0)
573
+ {
574
+ if (blackAndWhite)
575
+ x.fillStyle = '#fff';
576
+ else
577
+ {
578
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
579
+ g.addColorStop(0,color(C,2));
580
+ g.addColorStop(1,color(C,1));
581
+ }
582
+ }
583
+ else
584
+ x.fillStyle = '#000';
585
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
586
+ }
587
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
590
588
  {
591
589
  x.beginPath();
592
- x.rect(X,Y,W,C?H*p:H);
593
- x.fillStyle = C;
594
- C ? x.fill() : x.stroke();
595
- };
596
- const line = (X, Y, Z, W)=>
590
+ x.arc(X,Y,R,p*A,p*B);
591
+ gradient(X,Y-R,X,Y+R,C,S);
592
+ }
593
+ const rect = (X,Y,W,H,C)=>
597
594
  {
598
595
  x.beginPath();
599
- x.lineTo(X,Y);
600
- x.lineTo(Z,W);
601
- x.stroke();
602
- };
603
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
596
+ x.rect(X,Y,W,H*p);
597
+ gradient(X,Y+H,X+W,Y,C);
598
+ }
599
+ const poly = (points,C,Y,H)=>
604
600
  {
605
- const D = (A+B)/2, E = p*(B-A)/2;
606
601
  x.beginPath();
607
- F && x.lineTo(X,Y);
608
- x.arc(X,Y,R,D-E,D+E);
609
- x.fillStyle = C;
610
- C ? x.fill() : x.stroke();
611
- };
612
- const color = (c=0, l=0)=>
613
- hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
602
+ for (const p of points)
603
+ x.lineTo(p.x, p.y);
604
+ x.closePath();
605
+ gradient(0, Y, 0, Y+H,C);
606
+ }
607
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
608
+
609
+ // center and fit tos screen
614
610
  const alpha = wave(1,1,t);
615
611
  const p = percent(alpha, .1, .5);
616
-
617
- // setup
612
+ const size = min(6, min(w,h)/99);
618
613
  x.translate(w/2,h/2);
619
- const size = min(6, min(w,h)/99); // fit to screen
620
614
  x.scale(size,size);
621
615
  x.translate(-40,-35);
616
+ p < 1 && x.setLineDash([99*p,99]);
622
617
  x.lineJoin = x.lineCap = 'round';
623
618
  x.lineWidth = .1 + p*1.9;
619
+ //x.strokeStyle='#fff7';
620
+
621
+ if (showName)
622
+ {
623
+ // engine name text
624
+ const Y = 54;
625
+ const s = 'LittleJS';
626
+ x.font = '900 15.5px arial';
627
+ x.lineWidth = .1+p*3.9;
628
+ x.textAlign = 'center';
629
+ x.textBaseline = 'top';
630
+ rect(11,Y+1,59,8*p,-1);
631
+ x.beginPath();
632
+
633
+ let w2 = 0;
634
+ for (let i=0;i<s.length;++i)
635
+ w2 += x.measureText(s[i]).width;
636
+ for (let j=2;j--;)
637
+ for (let i=0,X=40-w2/2;i<s.length;++i)
638
+ {
639
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
640
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
641
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
642
+ X += w;
643
+ }
624
644
 
625
- // drawing effect
626
- const p2 = percent(alpha,.1,1);
627
- x.setLineDash([99*p2,99]);
628
-
629
- // cab top
630
- rect(7,16,18,-8,color(2,2));
631
- rect(7,8,18,4,color(2,3));
632
- rect(25,8,8,8,color(2,1));
633
- rect(25,8,-18,8);
634
- rect(25,8,8,8);
635
-
636
- // cab
637
- rect(25,16,7,24,color());
638
- rect(11,39,14,-23,color(1,1));
639
- rect(11,16,14,18,color(1,2));
640
- rect(11,16,14,8,color(1,3));
641
- rect(25,16,-14,24);
642
-
643
- // cab window
644
- rect(15,29,6,-9,color(2,2));
645
- circle(15,21,5,0,PI/2,color(2,4),1);
646
- rect(21,21,-6,9);
647
-
648
- // little stack
649
- rect(37,14,9,6,color(3,2));
650
- rect(37,14,4.5,6,color(3,3));
651
- rect(37,14,9,6);
645
+ x.lineWidth = .1 + p*1.9;
646
+ rect(3,Y,73,0); // bottom
647
+ }
652
648
 
649
+ rect(7,15,26,-7,0); // cab top
650
+ rect(25,15,8,25,-1); // cab front
651
+ rect(10,40,15,-25,1); // cab back
652
+ rect(14,21,7,9,2); // cab window
653
+ rect(38,20,6,-6,2); // little stack
654
+
653
655
  // big stack
654
- rect(50,20,10,-8,color(0,1));
655
- rect(50,20,6.5,-8,color(0,2));
656
- rect(50,20,3.5,-8,color(0,3));
657
- rect(50,20,10,-8);
658
- circle(55,2,11.4,.5,PI-.5,color(3,3));
659
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
660
- circle(55,2,11.4,.5,PI-.5);
661
- rect(45,7,20,-7,color(0,2));
662
- rect(45,-1,20,4,color(0,3));
663
- rect(45,-1,20,8);
656
+ rect(49,20,10,-6,0);
657
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
658
+ poly(stackPoints,2,8,6*p);
659
+ rect(44,8,20,-7,0);
664
660
 
665
661
  // engine
666
- for (let i=5; i--;)
667
- {
668
- // stagger radius to fix slight seam
669
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
670
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
671
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
672
- }
662
+ for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
663
+ circle(59,30,4,0,7,2); // light
673
664
 
674
665
  // engine outline
675
- circle(36,30,10,PI/2,PI*3/2);
676
- circle(48,30,10,PI/2,PI*3/2);
677
- circle(60,30,10);
678
- line(36,20,60,20);
666
+ rect(35,20,24,0); // top
667
+ circle(59,30,10); // front
668
+ circle(47,30,10,PI/2,PI*3/2); // middle
669
+ circle(35,30,10,PI/2,PI*3/2); // back
670
+ rect(7,40,13,7,-1); // bottom back
671
+ rect(17,40,43,14,-1); // bottom center
679
672
 
680
- // engine front light
681
- circle(60,30,4,PI,3*PI,color(3,2));
682
- circle(60,30,4,PI,2*PI,color(3,3));
683
- circle(60,30,4,PI,3*PI);
684
-
685
- // front brush
686
- for (let i=6; i--;)
673
+ // wheels
674
+ for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
675
+
676
+ // cowcatcher
677
+ for (let i=2;i--;)
687
678
  {
688
- x.beginPath();
689
- x.lineTo(53,54);
690
- x.lineTo(53,40);
691
- x.lineTo(53+(1+i*2.9)*p,40);
692
- x.lineTo(53+(4+i*3.5)*p,54);
693
- x.fillStyle = color(0,i%2+2);
694
- x.fill();
695
- i%2 && x.stroke();
679
+ let w=6, s=7, o=53+w*p*i
680
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
681
+ poly(points,0,40,14);
696
682
  }
697
683
 
698
- // wheels
699
- rect(6,40,5,5);
700
- rect(6,40,5,5,color());
701
- rect(15,54,38,-14,color());
702
- for (let i=3; i--;)
703
- for (let j=2; j--;)
704
- {
705
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
706
- x.stroke();
707
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
708
- x.stroke();
709
- }
710
- line(6,40,68,40); // center
711
- line(77,54,4,54); // bottom
712
-
713
- // draw engine name
714
- const s = engineName;
715
- x.font = '900 16px arial';
716
- x.textAlign = 'center';
717
- x.textBaseline = 'top';
718
- x.lineWidth = .1+p*3.9;
719
- let w2 = 0;
720
- for (let i=0; i<s.length; ++i)
721
- w2 += x.measureText(s[i]).width;
722
- for (let j=2; j--;)
723
- for (let i=0, X=41-w2/2; i<s.length; ++i)
724
- {
725
- x.fillStyle = color(i,2);
726
- const w = x.measureText(s[i]).width;
727
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
728
- X += w;
729
- }
730
684
  x.restore();
731
685
  }
732
686
  /**
@@ -1034,7 +988,9 @@ function wave(frequency=1, amplitude=1, t=time, offset=0)
1034
988
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1035
989
 
1036
990
  /**
1037
- * Check if object is a valid string or can be converted to one
991
+ * Check if object can be converted to a string (has a toString method)
992
+ * - Returns true for strings, numbers, and most objects
993
+ * - Returns false for null and undefined
1038
994
  * @param {any} s
1039
995
  * @return {boolean}
1040
996
  * @memberof Math */
@@ -1111,7 +1067,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1111
1067
  if (stepX < 0)
1112
1068
  hitPos.x -= e;
1113
1069
  }
1114
- if (stepY < 0)
1070
+ else if (stepY < 0)
1115
1071
  hitPos.y -= e;
1116
1072
 
1117
1073
  // set normal
@@ -1818,7 +1774,7 @@ class Color
1818
1774
  * @memberof Math */
1819
1775
  const WHITE = debugProtectConstant(rgb());
1820
1776
 
1821
- /** Color - Clear White #757474ff with 0 alpha
1777
+ /** Color - Clear White #ffffff00 with 0 alpha
1822
1778
  * @type {Color}
1823
1779
  * @memberof Math */
1824
1780
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
@@ -1828,7 +1784,7 @@ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1828
1784
  * @memberof Math */
1829
1785
  const BLACK = debugProtectConstant(rgb(0,0,0));
1830
1786
 
1831
- /** Color - Clear Black #000000 with 0 alpha
1787
+ /** Color - Clear Black #00000000 with 0 alpha
1832
1788
  * @type {Color}
1833
1789
  * @memberof Math */
1834
1790
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
@@ -1884,6 +1840,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1884
1840
  * @namespace Utilities
1885
1841
  */
1886
1842
 
1843
+ /**
1844
+ * Timer object tracks how long has passed since it was set
1845
+ * @memberof Engine
1846
+ * @example
1847
+ * let a = new Timer; // creates a timer that is not set
1848
+ * a.set(3); // sets the timer to 3 seconds
1849
+ *
1850
+ * let b = new Timer(1); // creates a timer with 1 second left
1851
+ * b.unset(); // unset the timer
1852
+ */
1853
+ class Timer
1854
+ {
1855
+ /** Create a timer object set time passed in
1856
+ * @param {number} [timeLeft] - How much time left before the timer
1857
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1858
+ constructor(timeLeft, useRealTime=false)
1859
+ {
1860
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1861
+ this.useRealTime = useRealTime;
1862
+ const globalTime = this.getGlobalTime();
1863
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1864
+ this.setTime = timeLeft;
1865
+ }
1866
+
1867
+ /** Set the timer with seconds passed in
1868
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
1869
+ set(timeLeft=0)
1870
+ {
1871
+ ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
1872
+ const globalTime = this.getGlobalTime();
1873
+ this.time = globalTime + timeLeft;
1874
+ this.setTime = timeLeft;
1875
+ }
1876
+
1877
+ /** Set if the timer should keep running even when the game is paused
1878
+ * @param {boolean} [useRealTime] */
1879
+ setUseRealTime(useRealTime=true)
1880
+ {
1881
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
1882
+ this.useRealTime = useRealTime;
1883
+ }
1884
+
1885
+ /** Unset the timer */
1886
+ unset() { this.time = undefined; }
1887
+
1888
+ /** Returns true if set
1889
+ * @return {boolean} */
1890
+ isSet() { return this.time !== undefined; }
1891
+
1892
+ /** Returns true if set and has not elapsed
1893
+ * @return {boolean} */
1894
+ active() { return this.getGlobalTime() < this.time; }
1895
+
1896
+ /** Returns true if set and elapsed
1897
+ * @return {boolean} */
1898
+ elapsed() { return this.getGlobalTime() >= this.time; }
1899
+
1900
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1901
+ * @return {number} */
1902
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
1903
+
1904
+ /** Get percentage elapsed based on time it was set to, returns 0 if not set
1905
+ * @return {number} */
1906
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
1907
+
1908
+ /** Get the time this timer was set to, returns 0 if not set
1909
+ * @return {number} */
1910
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
1911
+
1912
+ /** Get the current global time this timer is based on
1913
+ * @return {number} */
1914
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
1915
+
1916
+ /** Returns this timer expressed as a string
1917
+ * @return {string} */
1918
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
1919
+
1920
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
1921
+ * @return {number} */
1922
+ valueOf() { return this.get(); }
1923
+ }
1924
+
1925
+ ///////////////////////////////////////////////////////////////////////////////
1926
+
1887
1927
  /** Formats seconds to mm:ss style for display purposes
1888
1928
  * @param {number} t - time in seconds
1889
1929
  * @return {string}
@@ -1970,86 +2010,29 @@ function shareURL(title, url, callback)
1970
2010
 
1971
2011
  ///////////////////////////////////////////////////////////////////////////////
1972
2012
 
1973
- /**
1974
- * Timer object tracks how long has passed since it was set
1975
- * @memberof Engine
1976
- * @example
1977
- * let a = new Timer; // creates a timer that is not set
1978
- * a.set(3); // sets the timer to 3 seconds
1979
- *
1980
- * let b = new Timer(1); // creates a timer with 1 second left
1981
- * b.unset(); // unset the timer
1982
- */
1983
- class Timer
2013
+ /** Read save data from local storage
2014
+ * @param {string} saveName - unique name for the game/save
2015
+ * @param {Object} [defaultSaveData] - default values for save
2016
+ * @return {Object}
2017
+ * @memberof Utilities */
2018
+ function readSaveData(saveName, defaultSaveData)
1984
2019
  {
1985
- /** Create a timer object set time passed in
1986
- * @param {number} [timeLeft] - How much time left before the timer
1987
- * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
1988
- constructor(timeLeft, useRealTime=false)
1989
- {
1990
- ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
1991
- this.useRealTime = useRealTime;
1992
- const globalTime = this.getGlobalTime();
1993
- this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
1994
- this.setTime = timeLeft;
1995
- }
1996
-
1997
- /** Set the timer with seconds passed in
1998
- * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
1999
- set(timeLeft=0)
2000
- {
2001
- ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2002
- const globalTime = this.getGlobalTime();
2003
- this.time = globalTime + timeLeft;
2004
- this.setTime = timeLeft;
2005
- }
2006
-
2007
- /** Set if the timer should keep running even when the game is paused
2008
- * @param {boolean} [useRealTime] */
2009
- setUseRealTime(useRealTime=true)
2010
- {
2011
- ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2012
- this.useRealTime = useRealTime;
2013
- }
2014
-
2015
- /** Unset the timer */
2016
- unset() { this.time = undefined; }
2017
-
2018
- /** Returns true if set
2019
- * @return {boolean} */
2020
- isSet() { return this.time !== undefined; }
2021
-
2022
- /** Returns true if set and has not elapsed
2023
- * @return {boolean} */
2024
- active() { return this.getGlobalTime() < this.time; }
2025
-
2026
- /** Returns true if set and elapsed
2027
- * @return {boolean} */
2028
- elapsed() { return this.getGlobalTime() >= this.time; }
2029
-
2030
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2031
- * @return {number} */
2032
- get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2033
-
2034
- /** Get percentage elapsed based on time it was set to, returns 0 if not set
2035
- * @return {number} */
2036
- getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2037
-
2038
- /** Get the time this timer was set to, returns 0 if not set
2039
- * @return {number} */
2040
- getSetTime() { return this.isSet() ? this.setTime : 0; }
2041
-
2042
- /** Get the current global time this timer is based on
2043
- * @return {number} */
2044
- getGlobalTime() { return this.useRealTime ? timeReal : time; }
2045
-
2046
- /** Returns this timer expressed as a string
2047
- * @return {string} */
2048
- toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2020
+ ASSERT(isString(saveName), 'loadData requires saveName string');
2021
+
2022
+ // replace undefined values with defaults
2023
+ const data = localStorage[saveName];
2024
+ const loadedData = data ? JSON.parse(data) : {};
2025
+ return { ...defaultSaveData, ...loadedData };
2026
+ }
2049
2027
 
2050
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2051
- * @return {number} */
2052
- valueOf() { return this.get(); }
2028
+ /** Write save data to local storage
2029
+ * @param {string} saveName - unique name for the game/save
2030
+ * @param {Object} saveData - object containing data to be saved
2031
+ * @memberof Utilities */
2032
+ function writeSaveData(saveName, saveData)
2033
+ {
2034
+ ASSERT(isString(saveName), 'saveData requires saveName string');
2035
+ localStorage[saveName] = JSON.stringify(saveData);
2053
2036
  }
2054
2037
  /**
2055
2038
  * LittleJS Engine Settings
@@ -2710,17 +2693,19 @@ class EngineObject
2710
2693
  this.additiveColor = undefined;
2711
2694
  /** @property {boolean} - Should it flip along y axis when rendered */
2712
2695
  this.mirror = false;
2696
+ /** @property {boolean} - Has object been destroyed? */
2697
+ this.destroyed = false;
2713
2698
 
2714
2699
  // physical properties
2715
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
2700
+ /** @property {number} - How heavy the object is, static if 0 */
2716
2701
  this.mass = objectDefaultMass;
2717
- /** @property {number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
2702
+ /** @property {number} - How much to slow down velocity each frame (0-1) */
2718
2703
  this.damping = objectDefaultDamping;
2719
- /** @property {number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
2704
+ /** @property {number} - How much to slow down rotation each frame (0-1) */
2720
2705
  this.angleDamping = objectDefaultAngleDamping;
2721
- /** @property {number} [restitution=objectDefaultRestitution] - How bouncy the object is when colliding (0-1) */
2706
+ /** @property {number} - How bouncy the object is when colliding (0-1) */
2722
2707
  this.restitution = objectDefaultRestitution;
2723
- /** @property {number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
2708
+ /** @property {number} - How much friction to apply when sliding (0-1) */
2724
2709
  this.friction = objectDefaultFriction;
2725
2710
  /** @property {number} - How much to scale gravity by for this object */
2726
2711
  this.gravityScale = 1;
@@ -2929,10 +2914,10 @@ class EngineObject
2929
2914
  if (!tileCollisionTest(oldPos, this.size, this))
2930
2915
  {
2931
2916
  // test which side we bounced off (or both if a corner)
2932
- const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2933
- const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2934
-
2935
- if (blockedLayerX)
2917
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
2918
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
2919
+ const restitution = max(this.restitution, hitLayer.restitution);
2920
+ if (isBlockedX)
2936
2921
  {
2937
2922
  // try to move up a tiny bit
2938
2923
  const epsilon = 1e-3;
@@ -2948,16 +2933,12 @@ class EngineObject
2948
2933
  return;
2949
2934
  }
2950
2935
 
2951
- // move to previous position and bounce
2936
+ // move to previous X position and bounce
2952
2937
  this.pos.x = oldPos.x;
2953
- this.velocity.x *= -this.restitution;
2938
+ this.velocity.x *= -restitution;
2954
2939
  }
2955
- if (blockedLayerY || !blockedLayerX)
2940
+ if (isBlockedY || !isBlockedX)
2956
2941
  {
2957
- // bounce velocity
2958
- const restitution = max(this.restitution, hitLayer.restitution);
2959
- this.velocity.y *= -restitution;
2960
-
2961
2942
  if (wasFalling)
2962
2943
  {
2963
2944
  // adjust position to slightly away from nearest tile
@@ -2973,10 +2954,12 @@ class EngineObject
2973
2954
  }
2974
2955
  else
2975
2956
  {
2976
- // move to previous position
2957
+ // move to previous Y position
2977
2958
  this.pos.y = oldPos.y;
2978
2959
  this.groundObject = undefined;
2979
2960
  }
2961
+ // bounce velocity
2962
+ this.velocity.y *= -restitution;
2980
2963
  }
2981
2964
  debugPhysics && debugRect(this.pos, this.size, '#f00');
2982
2965
  }
@@ -2994,19 +2977,19 @@ class EngineObject
2994
2977
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2995
2978
  }
2996
2979
 
2997
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
2998
- destroy()
2980
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal
2981
+ * @param {boolean} [immediate] - should attached effects be allowed to die off? */
2982
+ destroy(immediate=false)
2999
2983
  {
3000
- if (this.destroyed)
3001
- return;
2984
+ if (this.destroyed) return;
3002
2985
 
3003
2986
  // disconnect from parent and destroy children
3004
- this.destroyed = 1;
2987
+ this.destroyed = true;
3005
2988
  this.parent?.removeChild(this);
3006
2989
  for (const child of this.children)
3007
2990
  {
3008
2991
  child.parent = undefined;
3009
- child.destroy();
2992
+ child.destroy(immediate);
3010
2993
  }
3011
2994
  }
3012
2995
 
@@ -3105,7 +3088,7 @@ class EngineObject
3105
3088
  }
3106
3089
 
3107
3090
  /** Check if overlapping another engine object
3108
- * Collisions are resoloved to prevent overlaps
3091
+ * Collisions are resolved to prevent overlaps
3109
3092
  * @param {EngineObject} object
3110
3093
  * @return {boolean} */
3111
3094
  isOverlappingObject(object)
@@ -3357,7 +3340,7 @@ class TileInfo
3357
3340
  }
3358
3341
 
3359
3342
  /**
3360
- * Returns a tile info for an index using this tile as refrence
3343
+ * Returns a tile info for an index using this tile as reference
3361
3344
  * @param {Vector2|number} [index=0]
3362
3345
  * @return {TileInfo}
3363
3346
  */
@@ -3896,6 +3879,31 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
3896
3879
  ///////////////////////////////////////////////////////////////////////////////
3897
3880
  // Drawing utilities
3898
3881
 
3882
+ /** Load a texture at a specific index
3883
+ * @param {number} textureIndex - Index to store the texture at
3884
+ * @param {string} [src] - Image source path
3885
+ * @return {Promise} Promise that resolves when texture is loaded
3886
+ * @memberof Draw */
3887
+ async function loadTexture(textureIndex, src)
3888
+ {
3889
+ ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
3890
+ ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
3891
+ ASSERT(!src || isString(src), 'image src must be a string');
3892
+
3893
+ const image = new Image;
3894
+ if (src)
3895
+ {
3896
+ await new Promise(resolve =>
3897
+ {
3898
+ image.onerror = image.onload = resolve;
3899
+ image.crossOrigin = 'anonymous';
3900
+ image.src = src;
3901
+ });
3902
+ }
3903
+
3904
+ textureInfos[textureIndex] = new TextureInfo(image);
3905
+ }
3906
+
3899
3907
  /** Convert from screen to world space coordinates
3900
3908
  * @param {Vector2} screenPos
3901
3909
  * @return {Vector2}
@@ -4187,7 +4195,7 @@ let engineFontImage;
4187
4195
  class FontImage
4188
4196
  {
4189
4197
  /** Create an image font
4190
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
4198
+ * @param {TileInfo} tileInfo - Tile info of first character in font
4191
4199
  */
4192
4200
  constructor(tileInfo)
4193
4201
  {
@@ -4276,23 +4284,20 @@ class FontImage
4276
4284
  }
4277
4285
 
4278
4286
  // load engine font, called automatically on startup
4279
- function fontImageInit()
4287
+ async function fontImageInit()
4280
4288
  {
4281
- return new Promise(resolve =>
4289
+ const image = new Image;
4290
+ await new Promise(resolve =>
4282
4291
  {
4283
- // create the engine font
4284
- const image = new Image;
4285
- image.onerror = image.onload = ()=>
4286
- {
4287
- const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4288
- const textureInfo = new TextureInfo(image);
4289
- const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4290
- engineFontImage = new FontImage(tileInfo);
4291
- resolve();
4292
- }
4292
+ image.onerror = image.onload = resolve;
4293
4293
  image.crossOrigin = 'anonymous';
4294
4294
  image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg==';
4295
4295
  });
4296
+
4297
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4298
+ const textureInfo = new TextureInfo(image);
4299
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4300
+ engineFontImage = new FontImage(tileInfo);
4296
4301
  }
4297
4302
  /**
4298
4303
  * LittleJS Input System
@@ -5063,7 +5068,7 @@ function inputRender()
5063
5068
  }
5064
5069
  }
5065
5070
 
5066
- // center position for right tocuh pad face buttons
5071
+ // center position for right touch pad face buttons
5067
5072
  function touchGamepadButtonCenter()
5068
5073
  {
5069
5074
  const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
@@ -5275,7 +5280,7 @@ class Sound
5275
5280
 
5276
5281
  /** Loads a sound from a URL and decodes it into sample data.
5277
5282
  * @param {string} filename
5278
- * @return {Promise<void>} */
5283
+ * @return {Promise} */
5279
5284
  async loadSound(filename)
5280
5285
  {
5281
5286
  const response = await fetch(filename);
@@ -5333,7 +5338,7 @@ class Sound
5333
5338
  * // Control the individual instance
5334
5339
  * instance.setVolume(.5);
5335
5340
  * instance.pause();
5336
- * instance.unpause();
5341
+ * instance.resume();
5337
5342
  * instance.stop();
5338
5343
  */
5339
5344
  class SoundInstance
@@ -5447,7 +5452,7 @@ class SoundInstance
5447
5452
  this.startTime = undefined;
5448
5453
  }
5449
5454
 
5450
- /** Unpauses this sound instance */
5455
+ /** Resume this sound instance */
5451
5456
  resume()
5452
5457
  {
5453
5458
  if (!this.isPaused()) return;
@@ -5461,8 +5466,8 @@ class SoundInstance
5461
5466
  */
5462
5467
  isPlaying() { return !!this.source; }
5463
5468
 
5464
- /** Check if this instance is paused and was not stopped
5465
- * @return {boolean} - True if paused
5469
+ /** Check if this instance is paused or stopped (not currently playing)
5470
+ * @return {boolean} - True if not playing
5466
5471
  */
5467
5472
  isPaused() { return !this.isPlaying(); }
5468
5473
 
@@ -5764,50 +5769,62 @@ const tileCollisionLayers = [];
5764
5769
 
5765
5770
  /** Get tile collision data for a given cell in the grid
5766
5771
  * @param {Vector2} pos
5772
+ * @param {boolean} [solidOnly] - Only check solid layers?
5767
5773
  * @return {number}
5768
5774
  * @memberof TileLayers */
5769
- function tileCollisionGetData(pos)
5775
+ function tileCollisionGetData(pos, solidOnly=true)
5770
5776
  {
5771
5777
  // check all tile collision layers
5772
5778
  for (const layer of tileCollisionLayers)
5779
+ if (!solidOnly || layer.isSolid)
5773
5780
  if (pos.arrayCheck(layer.size))
5774
- return layer.getCollisionData(pos);
5781
+ {
5782
+ const data = layer.getCollisionData(pos);
5783
+ if (data) return data;
5784
+ }
5775
5785
  return 0;
5776
5786
  }
5777
5787
 
5778
5788
  /** Check if a tile layer collides with another object
5779
- * @param {Vector2} pos
5780
- * @param {Vector2} [size=vec2()]
5781
- * @param {EngineObject} [object] - An object or undefined for generic test
5782
- * @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?
5783
5793
  * @return {TileCollisionLayer}
5784
5794
  * @memberof TileLayers */
5785
- function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
5795
+ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
5786
5796
  {
5787
5797
  for (const layer of tileCollisionLayers)
5788
5798
  {
5789
5799
  if (!solidOnly || layer.isSolid)
5790
- if (layer.collisionTest(pos, size, object))
5800
+ if (layer.collisionTest(pos, size, callbackObject))
5791
5801
  return layer;
5792
5802
  }
5793
5803
  }
5794
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
+
5795
5812
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
5796
- * The point will be inside the colliding tile if it hits (may have a tiny shift)
5797
- * @param {Vector2} posStart
5798
- * @param {Vector2} posEnd
5799
- * @param {EngineObject} [object] - An object or undefined for generic test
5800
- * @param {Vector2} [normal] - Optional normal of the surface hit
5801
- * @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?
5802
5819
  * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
5803
5820
  * @memberof TileLayers */
5804
- function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
5821
+ function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
5805
5822
  {
5806
5823
  for (const layer of tileCollisionLayers)
5807
5824
  {
5808
5825
  if (!solidOnly || layer.isSolid)
5809
5826
  {
5810
- const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
5827
+ const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, normal)
5811
5828
  if (hitPos) return hitPos;
5812
5829
  }
5813
5830
  }
@@ -6371,18 +6388,22 @@ class TileCollisionLayer extends TileLayer
6371
6388
  /** Check if collision with another object should occur
6372
6389
  * @param {Vector2} pos
6373
6390
  * @param {Vector2} [size=vec2()]
6374
- * @param {EngineObject} [object]
6391
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
6375
6392
  * @return {boolean} */
6376
- collisionTest(pos, size=new Vector2, object)
6393
+ collisionTest(pos, size=new Vector2, callbackObject)
6377
6394
  {
6378
6395
  ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
6379
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6380
-
6381
- // transform to local layer space
6382
- const posX = pos.x - this.pos.x;
6383
- const posY = pos.y - this.pos.y;
6396
+ ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a function 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;
6384
6403
 
6385
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;
6386
6407
  const minX = max(posX - size.x/2|0, 0);
6387
6408
  const minY = max(posY - size.y/2|0, 0);
6388
6409
  const maxX = min(posX + size.x/2, this.size.x);
@@ -6393,9 +6414,7 @@ class TileCollisionLayer extends TileLayer
6393
6414
  {
6394
6415
  // check if the object should collide with this tile
6395
6416
  const tileData = this.collisionData[y*this.size.x+x];
6396
- if (tileData)
6397
- if (!object || object.collideWithTile(tileData,
6398
- 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)))
6399
6418
  return true;
6400
6419
  }
6401
6420
  return false;
@@ -6403,52 +6422,68 @@ class TileCollisionLayer extends TileLayer
6403
6422
 
6404
6423
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
6405
6424
  * The point will be inside the colliding tile if it hits (may have a tiny shift)
6406
- * @param {Vector2} posStart
6407
- * @param {Vector2} posEnd
6408
- * @param {EngineObject} [object] - An object or undefined for generic test
6409
- * @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
6410
6429
  * @return {Vector2|undefined} */
6411
- collisionRaycast(posStart, posEnd, object, normal)
6430
+ collisionRaycast(posStart, posEnd, callbackObject, normal)
6412
6431
  {
6413
6432
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
6414
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
6433
+ ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a function or EngineObject');
6415
6434
 
6416
- const localPos = new Vector2;
6417
- 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)=>
6418
6441
  {
6419
- // check for tile collision
6420
- localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
6421
- const tileData = this.getCollisionData(localPos);
6422
- 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);
6423
6444
  }
6424
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
6425
- const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
6426
- 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)
6427
6450
  {
6428
6451
  const tilePos = hitPos.floor().add(vec2(.5));
6429
- debugRaycast && debugRect(tilePos, vec2(1), '#f008');
6430
- debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
6431
- debugRaycast && debugPoint(hitPos, '#0f0');
6432
- debugRaycast && normal &&
6433
- debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
6434
- 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);
6435
6457
  }
6458
+ return hitPos;
6436
6459
  }
6437
6460
  }
6438
6461
  /**
6439
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
6440
6467
  */
6441
6468
 
6442
6469
  /**
6443
- * @callback ParticleCallbackFunction - Function that processes a particle
6470
+ * @callback ParticleCallback - Function that processes a particle
6444
6471
  * @param {Particle} particle
6445
- * @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
6446
6481
  */
6447
6482
 
6448
6483
  /**
6449
6484
  * Particle Emitter - Spawns particles with the given settings
6450
6485
  * @extends EngineObject
6451
- * @memberof Engine
6486
+ * @memberof Particles
6452
6487
  * @example
6453
6488
  * // create a particle emitter
6454
6489
  * let pos = vec2(2,3);
@@ -6466,7 +6501,7 @@ class TileCollisionLayer extends TileLayer
6466
6501
  class ParticleEmitter extends EngineObject
6467
6502
  {
6468
6503
  /** Create a particle system with the given settings
6469
- * @param {Vector2} position - World space position of the emitter
6504
+ * @param {Vector2} pos - World space position of the emitter
6470
6505
  * @param {number} [angle] - Angle to emit the particles
6471
6506
  * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
6472
6507
  * @param {number} [emitTime] - How long to stay alive (0 is forever)
@@ -6496,7 +6531,7 @@ class ParticleEmitter extends EngineObject
6496
6531
  */
6497
6532
  constructor
6498
6533
  (
6499
- position,
6534
+ pos,
6500
6535
  angle,
6501
6536
  emitSize = 0,
6502
6537
  emitTime = 0,
@@ -6525,12 +6560,13 @@ class ParticleEmitter extends EngineObject
6525
6560
  localSpace = false
6526
6561
  )
6527
6562
  {
6528
- super(position, vec2(), tileInfo, angle, undefined, renderOrder);
6563
+ super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
6529
6564
 
6530
6565
  // emitter settings
6566
+ /** @property {boolean} - Should particles be emitted in a circle */
6567
+ this.emitCircle = typeof emitSize === 'number';
6531
6568
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
6532
- this.emitSize = emitSize instanceof Vector2 ?
6533
- emitSize.copy() : emitSize;
6569
+ this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
6534
6570
  /** @property {number} - How long to stay alive (0 is forever) */
6535
6571
  this.emitTime = emitTime;
6536
6572
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -6581,26 +6617,31 @@ class ParticleEmitter extends EngineObject
6581
6617
  this.localSpace = localSpace;
6582
6618
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
6583
6619
  this.trailScale = 0;
6584
- /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
6585
- this.particleDestroyCallback = undefined;
6586
- /** @property {ParticleCallbackFunction} - Callback when particle is created */
6620
+ /** @property {ParticleCallback} - Callback when particle is created */
6587
6621
  this.particleCreateCallback = undefined;
6588
- /** @property {number} - Track particle emit time */
6589
- 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;
6590
6626
  /** @property {number} - Percentage of velocity to pass to particles (0-1) */
6591
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 = [];
6592
6632
 
6593
6633
  // track previous position and angle
6594
6634
  this.previousAngle = this.angle;
6595
6635
  this.previousPos = this.pos.copy();
6596
6636
  }
6597
6637
 
6598
- /** Emitters do not have physics */
6599
- updatePhysics() {}
6600
-
6601
6638
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
6602
6639
  update()
6603
6640
  {
6641
+ // physics sanity checks
6642
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
6643
+ ASSERT(this.damping >= 0 && this.damping <= 1);
6644
+
6604
6645
  if (this.velocityInheritance)
6605
6646
  {
6606
6647
  // pass emitter velocity to particles
@@ -6614,7 +6655,7 @@ class ParticleEmitter extends EngineObject
6614
6655
  }
6615
6656
 
6616
6657
  // update emitter
6617
- if (!this.emitTime || this.getAliveTime() <= this.emitTime)
6658
+ if (this.isActive())
6618
6659
  {
6619
6660
  // emit particles
6620
6661
  if (this.emitRate && particleEmitRateScale)
@@ -6624,14 +6665,21 @@ class ParticleEmitter extends EngineObject
6624
6665
  this.emitParticle();
6625
6666
  }
6626
6667
  }
6627
- else
6628
- 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
+ });
6629
6677
 
6630
6678
  if (debugParticles)
6631
6679
  {
6632
6680
  // show emitter bounds
6633
- if (typeof this.emitSize === 'number')
6634
- debugCircle(this.pos, this.emitSize/2, '#0f0');
6681
+ if (this.emitCircle)
6682
+ debugCircle(this.pos, this.emitSize.x/2, '#0f0');
6635
6683
  else
6636
6684
  debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
6637
6685
  }
@@ -6642,14 +6690,15 @@ class ParticleEmitter extends EngineObject
6642
6690
  emitParticle()
6643
6691
  {
6644
6692
  // spawn a particle
6645
- let pos = typeof this.emitSize === 'number' ? // check if number was used
6646
- randInCircle(this.emitSize/2) // circle emitter
6647
- : 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
6648
6696
  .multiply(this.emitSize).rotate(this.angle)
6649
6697
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
6650
6698
  if (!this.localSpace)
6651
6699
  {
6652
- pos = this.pos.add(pos);
6700
+ pos.x += this.pos.x;
6701
+ pos.y += this.pos.y;
6653
6702
  angle += this.angle;
6654
6703
  }
6655
6704
 
@@ -6669,25 +6718,17 @@ class ParticleEmitter extends EngineObject
6669
6718
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
6670
6719
 
6671
6720
  // build particle
6672
- const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
6673
- particle.velocity = vec2().setAngle(velocityAngle, speed);
6674
- particle.angleVelocity = angleSpeed;
6721
+ const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
6722
+ let angleVelocity = angleSpeed;
6675
6723
  if (!this.localSpace && this.velocityInheritance > 0)
6676
6724
  {
6677
6725
  // apply emitter velocity to particle
6678
- particle.velocity.x += this.velocity.x;
6679
- particle.velocity.y += this.velocity.y;
6680
- particle.angleVelocity += this.angleVelocity;
6726
+ velocity.x += this.velocity.x;
6727
+ velocity.y += this.velocity.y;
6728
+ angleVelocity += this.angleVelocity;
6681
6729
  }
6682
- particle.fadeRate = this.fadeRate;
6683
- particle.damping = this.damping;
6684
- particle.angleDamping = this.angleDamping;
6685
- particle.restitution = this.restitution;
6686
- particle.friction = this.friction;
6687
- particle.gravityScale = this.gravityScale;
6688
- particle.collideTiles = this.collideTiles;
6689
- particle.renderOrder = this.renderOrder;
6690
- particle.mirror = randBool();
6730
+ const particle = new Particle(this, pos, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, velocity, angleVelocity);
6731
+ this.particles.push(particle);
6691
6732
 
6692
6733
  // call particle create callback
6693
6734
  this.particleCreateCallback?.(particle);
@@ -6696,140 +6737,245 @@ class ParticleEmitter extends EngineObject
6696
6737
  return particle;
6697
6738
  }
6698
6739
 
6699
- // Particle emitters are not rendered, only the particles are
6700
- 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
+ }
6701
6768
  }
6702
6769
 
6703
6770
  ///////////////////////////////////////////////////////////////////////////////
6704
6771
  /**
6705
6772
  * Particle Object - Created automatically by Particle Emitters
6706
- * @extends EngineObject
6707
- * @memberof Engine
6773
+ * @memberof Particles
6708
6774
  */
6709
- class Particle extends EngineObject
6775
+ class Particle
6710
6776
  {
6711
6777
  /**
6712
6778
  * Create a particle with the passed in settings
6713
6779
  * Typically this is created automatically by a ParticleEmitter
6714
- * @param {Vector2} position - World space position of the particle
6715
- * @param {TileInfo} tileInfo - Tile info to render particles
6716
- * @param {number} angle - Angle to rotate the particle
6717
- * @param {Color} colorStart - Color at start of life
6718
- * @param {Color} colorEnd - Color at end of life
6719
- * @param {number} lifeTime - How long to live for
6720
- * @param {number} sizeStart - Size at start of life
6721
- * @param {number} sizeEnd - Size at end of life
6722
- * @param {number} fadeRate - How quick to fade in/out
6723
- * @param {boolean} additive - Does it use additive blend mode
6724
- * @param {number} trailScale - If a trail, how long to make it
6725
- * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
6726
- * @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
6727
6790
  */
6728
- constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
6729
- )
6791
+ constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
6730
6792
  {
6731
- super(position, vec2(), tileInfo, angle);
6732
-
6733
- /** @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} */
6734
6804
  this.colorStart = colorStart;
6735
- /** @property {Color} - Color at end of life */
6805
+ /** @property {Color} */
6736
6806
  this.colorEnd = colorEnd;
6737
- /** @property {number} - How long to live for */
6807
+ /** @property {number} */
6738
6808
  this.lifeTime = lifeTime;
6739
- /** @property {number} - Size at start of life */
6809
+ /** @property {number} */
6740
6810
  this.sizeStart = sizeStart;
6741
- /** @property {number} - Size at end of life */
6811
+ /** @property {number} */
6742
6812
  this.sizeEnd = sizeEnd;
6743
- /** @property {number} - How quick to fade in/out */
6744
- this.fadeRate = fadeRate;
6745
- /** @property {boolean} - Is it additive */
6746
- this.additive = additive;
6747
- /** @property {number} - If a trail, how long to make it */
6748
- this.trailScale = trailScale;
6749
- /** @property {ParticleEmitter} - Parent emitter if local space */
6750
- this.localSpaceEmitter = localSpaceEmitter;
6751
- /** @property {ParticleCallbackFunction} - Called when particle dies */
6752
- this.destroyCallback = destroyCallback;
6753
- // particles do not clamp speed by default
6754
- this.clampSpeed = false;
6755
- }
6756
-
6757
- /** 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 */
6758
6830
  update()
6759
6831
  {
6760
- 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)
6761
6844
  {
6762
- // only apply max circular speed if particle can collide
6763
- const length2 = this.velocity.lengthSquared();
6764
- if (length2 > objectMaxSpeed*objectMaxSpeed)
6765
- {
6766
- const s = objectMaxSpeed / length2**.5;
6767
- this.velocity.x *= s;
6768
- this.velocity.y *= s;
6769
- }
6845
+ this.destroy();
6846
+ return;
6770
6847
  }
6771
6848
 
6772
- 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)
6773
6863
  {
6774
- // destroy particle when its time runs out
6775
- const c = this.colorEnd;
6776
- this.color.set(c.r, c.g, c.b, c.a);
6777
- this.size.set(this.sizeEnd, this.sizeEnd);
6778
- this.destroyCallback?.(this);
6779
- this.destroyed = 1;
6864
+ const s = objectMaxSpeed / length2**.5;
6865
+ this.velocity.x *= s;
6866
+ this.velocity.y *= s;
6780
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))
6878
+ {
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
+ }
6911
+ }
6912
+ }
6913
+
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);
6781
6923
  }
6782
6924
 
6783
- /** Render the particle, automatically called each frame, sorted by renderOrder */
6925
+ /** Render the particle, automatically called each frame */
6784
6926
  render()
6785
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
+
6786
6935
  // lerp color and size
6787
6936
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
6788
6937
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
6789
6938
  const size = vec2(radius);
6939
+ const alphaFade = p1 < fadeRate ? p1/fadeRate :
6940
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6790
6941
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
6791
6942
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
6792
6943
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
6793
- this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
6794
-
6795
- // fade alpha
6796
- const fadeRate = this.fadeRate/2;
6797
- this.color.a *= p1 < fadeRate ? p1/fadeRate :
6798
- p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
6944
+ this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
6799
6945
 
6800
6946
  // draw the particle
6801
- this.additive && setBlendMode(true);
6947
+ additive && setBlendMode(true);
6802
6948
 
6803
6949
  // update the position and angle for drawing
6804
- let pos = this.pos, angle = this.angle;
6805
- if (this.localSpaceEmitter)
6950
+ const pos = this.pos.copy();
6951
+ let angle = this.angle;
6952
+ if (localSpace)
6806
6953
  {
6807
6954
  // in local space of emitter
6808
- const a = this.localSpaceEmitter.angle;
6955
+ const a = emitter.angle;
6809
6956
  const c = cos(a), s = sin(a);
6810
- pos = this.localSpaceEmitter.pos.add(
6811
- new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
6812
- 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;
6813
6960
  }
6814
- if (this.trailScale)
6961
+ if (trailScale)
6815
6962
  {
6816
6963
  // trail style particles
6817
- const direction = this.localSpaceEmitter ?
6818
- this.velocity.rotate(-this.localSpaceEmitter.angle) :
6819
- this.velocity;
6820
- const speed = direction.length();
6964
+ const velocity = localSpace ?
6965
+ this.velocity.rotate(-emitter.angle) : this.velocity;
6966
+ const speed = velocity.length();
6821
6967
  if (speed)
6822
6968
  {
6823
6969
  // stretch in direction of motion
6824
- const trailLength = speed * this.trailScale;
6970
+ const trailLength = speed * trailScale;
6825
6971
  size.y = max(size.x, trailLength);
6826
- angle = atan2(direction.x, direction.y);
6972
+ angle = atan2(velocity.x, velocity.y);
6827
6973
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6828
6974
  }
6829
6975
  }
6830
6976
  else
6831
6977
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
6832
- this.additive && setBlendMode();
6978
+ additive && setBlendMode();
6833
6979
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
6834
6980
  }
6835
6981
  }
@@ -8109,7 +8255,7 @@ class PostProcessPlugin
8109
8255
  {
8110
8256
  /** Create global post processing shader
8111
8257
  * @param {string} shaderCode
8112
- * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8258
+ * @param {boolean} [includeMainCanvas] - combine mainCanvas onto glCanvas
8113
8259
  * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8114
8260
  * @example
8115
8261
  * // create the post process plugin object
@@ -8502,7 +8648,7 @@ class UISystemPlugin
8502
8648
  // navigation properties
8503
8649
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
8504
8650
  this.navigationObject = undefined;
8505
- /** @property {Timer} - Cooldown timer for navigation inputs */
8651
+ /** @property {Timer} - Cool down timer for navigation inputs */
8506
8652
  this.navigationTimer = new Timer(undefined, true);
8507
8653
  /** @property {number} - Time between navigation inputs in seconds */
8508
8654
  this.navigationDelay = .2;
@@ -8968,7 +9114,7 @@ class UISystemPlugin
8968
9114
 
8969
9115
  const savedNavigationDirection = uiSystem.navigationDirection;
8970
9116
 
8971
- // allow both axies for navigation
9117
+ // allow both axes for navigation
8972
9118
  uiSystem.navigationDirection = 2;
8973
9119
 
8974
9120
  // confirm menu
@@ -9063,7 +9209,7 @@ class UIObject
9063
9209
  this.lineWidth = uiSystem.defaultLineWidth;
9064
9210
  /** @property {number} - Corner radius for rounded rects */
9065
9211
  this.cornerRadius = uiSystem.defaultCornerRadius;
9066
- /** @property {string} - Font for this objecct */
9212
+ /** @property {string} - Font for this object */
9067
9213
  this.font = uiSystem.defaultFont;
9068
9214
  /** @property {string} - Font style for this object or undefined */
9069
9215
  this.fontStyle = undefined;
@@ -9551,6 +9697,8 @@ class UIScrollbar extends UIObject
9551
9697
  this.value = value;
9552
9698
  /** @property {Color} - Color for the handle part of the scrollbar */
9553
9699
  this.handleColor = handleColor.copy();
9700
+ /** @property {boolean} - Should it fill up like a progress bar? */
9701
+ this.fillMode = false;
9554
9702
 
9555
9703
  // set properties
9556
9704
  this.text = text;
@@ -9596,19 +9744,30 @@ class UIScrollbar extends UIObject
9596
9744
 
9597
9745
  // handle horizontal or vertical scrollbar
9598
9746
  const isHorizontal = this.size.x > this.size.y;
9599
- const handleSize = isHorizontal ? this.size.y : this.size.x;
9600
- const barSize = isHorizontal ? this.size.x : this.size.y;
9601
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
9602
-
9603
- // draw the scrollbar handle
9604
- const handleWidth = barSize - handleSize;
9605
- const p1 = centerPos - handleWidth/2;
9606
- const p2 = centerPos + handleWidth/2;
9607
- const handlePos = isHorizontal ?
9608
- vec2(lerp(p1, p2, this.value), this.pos.y) :
9609
- vec2(this.pos.x, lerp(p2, p1, this.value))
9610
- const handleColor = this.disabled ? this.disabledColor : this.handleColor;
9611
- 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 = clamp(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
+ }
9612
9771
 
9613
9772
  // draw the text scaled to fit on the scrollbar
9614
9773
  const textSize = this.getTextSize();
@@ -9630,7 +9789,7 @@ class UIScrollbar extends UIObject
9630
9789
  * @extends UIObject
9631
9790
  * @example
9632
9791
  * // Create a video player UI object
9633
- * 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);
9634
9793
  * video.play();
9635
9794
  * @memberof UISystem
9636
9795
  */
@@ -9671,12 +9830,11 @@ class UIVideo extends UIObject
9671
9830
 
9672
9831
  /** Play or resume the video
9673
9832
  * @return {Promise} Promise that resolves when playback starts */
9674
- play()
9833
+ async play()
9675
9834
  {
9676
9835
  // try to play the video, catch any errors (autoplay may be blocked)
9677
- const promise = this.video.play();
9678
- promise?.catch(()=>{});
9679
- return promise;
9836
+ try { await this.video.play(); }
9837
+ catch(e) {}
9680
9838
  }
9681
9839
 
9682
9840
  /** Pause the video */
@@ -10379,11 +10537,11 @@ class Box2dStaticObject extends Box2dObject
10379
10537
 
10380
10538
  ///////////////////////////////////////////////////////////////////////////////
10381
10539
  /**
10382
- * Box2D Kiematic Object - Box2d with a kinematic physics body
10540
+ * Box2D Kinematic Object - Box2d with a kinematic physics body
10383
10541
  * @extends Box2dObject
10384
10542
  * @memberof Box2D
10385
10543
  */
10386
- class Box2dKiematicObject extends Box2dObject
10544
+ class Box2dKinematicObject extends Box2dObject
10387
10545
  {
10388
10546
  /** Create a LittleJS object with Box2d physics
10389
10547
  * @param {Vector2} [pos]
@@ -10435,20 +10593,19 @@ class Box2dTileLayer extends Box2dStaticObject
10435
10593
  this.destroyAllFixtures();
10436
10594
 
10437
10595
  // create box2d object for this layer
10438
- const size = this.tileLayer.size;
10439
10596
  this.pos = this.tileLayer.pos.copy();
10440
- this.size = size.copy();
10597
+ this.size = this.tileLayer.size.copy();
10441
10598
 
10442
10599
  // track which tiles have been processed
10443
10600
  const processed = [];
10444
- const getIndex = (x, y)=> x + y * size.x;
10601
+ const getIndex = (x, y)=> x + y * this.size.x;
10445
10602
  const isSolidUnprocessed = (x, y)=>
10446
10603
  !processed[getIndex(x, y)] &&
10447
10604
  this.tileLayer.getCollisionData(vec2(x, y)) > 0;
10448
10605
 
10449
10606
  // combine tiles into larger boxes
10450
- for (let x = 0; x < size.x; ++x)
10451
- for (let y = 0; y < size.y; ++y)
10607
+ for (let x = 0; x < this.size.x; ++x)
10608
+ for (let y = 0; y < this.size.y; ++y)
10452
10609
  {
10453
10610
  if (!isSolidUnprocessed(x, y)) continue;
10454
10611