littlejsengine 1.17.5 → 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.
@@ -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.10';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -464,13 +464,13 @@ function engineObjectsUpdate()
464
464
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
465
465
 
466
466
  // recursive object update
467
- function updateObject(o)
467
+ function updateChildObject(o)
468
468
  {
469
469
  if (o.destroyed) return;
470
470
 
471
471
  o.update();
472
472
  for (const child of o.children)
473
- updateObject(child);
473
+ updateChildObject(child);
474
474
  }
475
475
  for (const o of engineObjects)
476
476
  {
@@ -480,7 +480,7 @@ function engineObjectsUpdate()
480
480
  o.update();
481
481
  o.updatePhysics();
482
482
  for (const child of o.children)
483
- updateObject(child);
483
+ updateChildObject(child);
484
484
  o.updateTransforms();
485
485
  }
486
486
 
@@ -489,11 +489,14 @@ function engineObjectsUpdate()
489
489
  }
490
490
 
491
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?
492
495
  * @memberof Engine */
493
- function engineObjectsDestroy()
496
+ function engineObjectsDestroy(immediate=true)
494
497
  {
495
498
  for (const o of engineObjects)
496
- o.parent || o.destroy();
499
+ o.parent || o.destroy(immediate);
497
500
  engineObjects = engineObjects.filter(o=>!o.destroyed);
498
501
  }
499
502
 
@@ -568,165 +571,140 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
568
571
  ///////////////////////////////////////////////////////////////////////////////
569
572
  function drawEngineLogo(t)
570
573
  {
574
+ const blackAndWhite = 0;
575
+ const showName = 1;
576
+
571
577
  // LittleJS Logo and Splash Screen
572
578
  const x = mainContext;
573
579
  const w = mainCanvas.width = innerWidth;
574
580
  const h = mainCanvas.height = innerHeight;
575
-
576
581
  {
577
582
  // background
578
583
  const p3 = percent(t, 1, .8);
579
584
  const p4 = percent(t, 0, .5);
580
- 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);
581
586
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
582
587
  g.addColorStop(1,hsl(0,0,0,p3).toString());
583
588
  x.save();
584
589
  x.fillStyle = g;
585
590
  x.fillRect(0,0,w,h);
586
591
  }
587
-
588
- // draw LittleJS logo...
589
- 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)=>
590
610
  {
591
611
  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)=>
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)=>
597
616
  {
598
617
  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)=>
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)=>
604
622
  {
605
- const D = (A+B)/2, E = p*(B-A)/2;
606
623
  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();
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
614
632
  const alpha = wave(1,1,t);
615
633
  const p = percent(alpha, .1, .5);
616
-
617
- // setup
634
+ const size = min(6, min(w,h)/99);
618
635
  x.translate(w/2,h/2);
619
- const size = min(6, min(w,h)/99); // fit to screen
620
636
  x.scale(size,size);
621
637
  x.translate(-40,-35);
638
+ p < 1 && x.setLineDash([99*p,99]);
622
639
  x.lineJoin = x.lineCap = 'round';
623
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();
624
654
 
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);
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
652
676
 
653
677
  // 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);
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);
664
682
 
665
683
  // 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
- }
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
673
686
 
674
687
  // 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);
679
-
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);
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
684
694
 
685
- // front brush
686
- 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--;)
687
702
  {
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();
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);
696
706
  }
697
707
 
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
708
  x.restore();
731
709
  }
732
710
  /**
@@ -1884,6 +1862,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
1884
1862
  * @namespace Utilities
1885
1863
  */
1886
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
+
1887
1949
  /** Formats seconds to mm:ss style for display purposes
1888
1950
  * @param {number} t - time in seconds
1889
1951
  * @return {string}
@@ -1970,86 +2032,29 @@ function shareURL(title, url, callback)
1970
2032
 
1971
2033
  ///////////////////////////////////////////////////////////////////////////////
1972
2034
 
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
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)
1984
2041
  {
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'; }
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
+ }
2049
2049
 
2050
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2051
- * @return {number} */
2052
- 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);
2053
2058
  }
2054
2059
  /**
2055
2060
  * LittleJS Engine Settings
@@ -2710,17 +2715,19 @@ class EngineObject
2710
2715
  this.additiveColor = undefined;
2711
2716
  /** @property {boolean} - Should it flip along y axis when rendered */
2712
2717
  this.mirror = false;
2718
+ /** @property {boolean} - Has object been destroyed? */
2719
+ this.destroyed = false;
2713
2720
 
2714
2721
  // physical properties
2715
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
2722
+ /** @property {number} - How heavy the object is, static if 0 */
2716
2723
  this.mass = objectDefaultMass;
2717
- /** @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) */
2718
2725
  this.damping = objectDefaultDamping;
2719
- /** @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) */
2720
2727
  this.angleDamping = objectDefaultAngleDamping;
2721
- /** @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) */
2722
2729
  this.restitution = objectDefaultRestitution;
2723
- /** @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) */
2724
2731
  this.friction = objectDefaultFriction;
2725
2732
  /** @property {number} - How much to scale gravity by for this object */
2726
2733
  this.gravityScale = 1;
@@ -2929,10 +2936,10 @@ class EngineObject
2929
2936
  if (!tileCollisionTest(oldPos, this.size, this))
2930
2937
  {
2931
2938
  // 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)
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)
2936
2943
  {
2937
2944
  // try to move up a tiny bit
2938
2945
  const epsilon = 1e-3;
@@ -2948,16 +2955,12 @@ class EngineObject
2948
2955
  return;
2949
2956
  }
2950
2957
 
2951
- // move to previous position and bounce
2958
+ // move to previous X position and bounce
2952
2959
  this.pos.x = oldPos.x;
2953
- this.velocity.x *= -this.restitution;
2960
+ this.velocity.x *= -restitution;
2954
2961
  }
2955
- if (blockedLayerY || !blockedLayerX)
2962
+ if (isBlockedY || !isBlockedX)
2956
2963
  {
2957
- // bounce velocity
2958
- const restitution = max(this.restitution, hitLayer.restitution);
2959
- this.velocity.y *= -restitution;
2960
-
2961
2964
  if (wasFalling)
2962
2965
  {
2963
2966
  // adjust position to slightly away from nearest tile
@@ -2973,10 +2976,12 @@ class EngineObject
2973
2976
  }
2974
2977
  else
2975
2978
  {
2976
- // move to previous position
2979
+ // move to previous Y position
2977
2980
  this.pos.y = oldPos.y;
2978
2981
  this.groundObject = undefined;
2979
2982
  }
2983
+ // bounce velocity
2984
+ this.velocity.y *= -restitution;
2980
2985
  }
2981
2986
  debugPhysics && debugRect(this.pos, this.size, '#f00');
2982
2987
  }
@@ -2994,19 +2999,19 @@ class EngineObject
2994
2999
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
2995
3000
  }
2996
3001
 
2997
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
2998
- 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)
2999
3005
  {
3000
- if (this.destroyed)
3001
- return;
3006
+ if (this.destroyed) return;
3002
3007
 
3003
3008
  // disconnect from parent and destroy children
3004
- this.destroyed = 1;
3009
+ this.destroyed = true;
3005
3010
  this.parent?.removeChild(this);
3006
3011
  for (const child of this.children)
3007
3012
  {
3008
3013
  child.parent = undefined;
3009
- child.destroy();
3014
+ child.destroy(immediate);
3010
3015
  }
3011
3016
  }
3012
3017
 
@@ -3105,7 +3110,7 @@ class EngineObject
3105
3110
  }
3106
3111
 
3107
3112
  /** Check if overlapping another engine object
3108
- * Collisions are resoloved to prevent overlaps
3113
+ * Collisions are resolved to prevent overlaps
3109
3114
  * @param {EngineObject} object
3110
3115
  * @return {boolean} */
3111
3116
  isOverlappingObject(object)
@@ -3357,7 +3362,7 @@ class TileInfo
3357
3362
  }
3358
3363
 
3359
3364
  /**
3360
- * 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
3361
3366
  * @param {Vector2|number} [index=0]
3362
3367
  * @return {TileInfo}
3363
3368
  */
@@ -4187,7 +4192,7 @@ let engineFontImage;
4187
4192
  class FontImage
4188
4193
  {
4189
4194
  /** Create an image font
4190
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
4195
+ * @param {TileInfo} tileInfo - Tile info of first character in font
4191
4196
  */
4192
4197
  constructor(tileInfo)
4193
4198
  {
@@ -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));
@@ -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 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;
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 funtion 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;
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
+ }
6780
6911
  }
6781
6912
  }
6782
6913
 
6783
- /** 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 */
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 = 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
  */
@@ -10379,11 +10538,11 @@ class Box2dStaticObject extends Box2dObject
10379
10538
 
10380
10539
  ///////////////////////////////////////////////////////////////////////////////
10381
10540
  /**
10382
- * Box2D Kiematic Object - Box2d with a kinematic physics body
10541
+ * Box2D Kinematic Object - Box2d with a kinematic physics body
10383
10542
  * @extends Box2dObject
10384
10543
  * @memberof Box2D
10385
10544
  */
10386
- class Box2dKiematicObject extends Box2dObject
10545
+ class Box2dKinematicObject extends Box2dObject
10387
10546
  {
10388
10547
  /** Create a LittleJS object with Box2d physics
10389
10548
  * @param {Vector2} [pos]
@@ -10435,20 +10594,19 @@ class Box2dTileLayer extends Box2dStaticObject
10435
10594
  this.destroyAllFixtures();
10436
10595
 
10437
10596
  // create box2d object for this layer
10438
- const size = this.tileLayer.size;
10439
10597
  this.pos = this.tileLayer.pos.copy();
10440
- this.size = size.copy();
10598
+ this.size = this.tileLayer.size.copy();
10441
10599
 
10442
10600
  // track which tiles have been processed
10443
10601
  const processed = [];
10444
- const getIndex = (x, y)=> x + y * size.x;
10602
+ const getIndex = (x, y)=> x + y * this.size.x;
10445
10603
  const isSolidUnprocessed = (x, y)=>
10446
10604
  !processed[getIndex(x, y)] &&
10447
10605
  this.tileLayer.getCollisionData(vec2(x, y)) > 0;
10448
10606
 
10449
10607
  // combine tiles into larger boxes
10450
- for (let x = 0; x < size.x; ++x)
10451
- for (let y = 0; y < size.y; ++y)
10608
+ for (let x = 0; x < this.size.x; ++x)
10609
+ for (let y = 0; y < this.size.y; ++y)
10452
10610
  {
10453
10611
  if (!isSolidUnprocessed(x, y)) continue;
10454
10612