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
  /**
@@ -2565,6 +2543,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2565
2543
  * @namespace Utilities
2566
2544
  */
2567
2545
 
2546
+ /**
2547
+ * Timer object tracks how long has passed since it was set
2548
+ * @memberof Engine
2549
+ * @example
2550
+ * let a = new Timer; // creates a timer that is not set
2551
+ * a.set(3); // sets the timer to 3 seconds
2552
+ *
2553
+ * let b = new Timer(1); // creates a timer with 1 second left
2554
+ * b.unset(); // unset the timer
2555
+ */
2556
+ class Timer
2557
+ {
2558
+ /** Create a timer object set time passed in
2559
+ * @param {number} [timeLeft] - How much time left before the timer
2560
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
2561
+ constructor(timeLeft, useRealTime=false)
2562
+ {
2563
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
2564
+ this.useRealTime = useRealTime;
2565
+ const globalTime = this.getGlobalTime();
2566
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
2567
+ this.setTime = timeLeft;
2568
+ }
2569
+
2570
+ /** Set the timer with seconds passed in
2571
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
2572
+ set(timeLeft=0)
2573
+ {
2574
+ ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2575
+ const globalTime = this.getGlobalTime();
2576
+ this.time = globalTime + timeLeft;
2577
+ this.setTime = timeLeft;
2578
+ }
2579
+
2580
+ /** Set if the timer should keep running even when the game is paused
2581
+ * @param {boolean} [useRealTime] */
2582
+ setUseRealTime(useRealTime=true)
2583
+ {
2584
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2585
+ this.useRealTime = useRealTime;
2586
+ }
2587
+
2588
+ /** Unset the timer */
2589
+ unset() { this.time = undefined; }
2590
+
2591
+ /** Returns true if set
2592
+ * @return {boolean} */
2593
+ isSet() { return this.time !== undefined; }
2594
+
2595
+ /** Returns true if set and has not elapsed
2596
+ * @return {boolean} */
2597
+ active() { return this.getGlobalTime() < this.time; }
2598
+
2599
+ /** Returns true if set and elapsed
2600
+ * @return {boolean} */
2601
+ elapsed() { return this.getGlobalTime() >= this.time; }
2602
+
2603
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2604
+ * @return {number} */
2605
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2606
+
2607
+ /** Get percentage elapsed based on time it was set to, returns 0 if not set
2608
+ * @return {number} */
2609
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2610
+
2611
+ /** Get the time this timer was set to, returns 0 if not set
2612
+ * @return {number} */
2613
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
2614
+
2615
+ /** Get the current global time this timer is based on
2616
+ * @return {number} */
2617
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
2618
+
2619
+ /** Returns this timer expressed as a string
2620
+ * @return {string} */
2621
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2622
+
2623
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2624
+ * @return {number} */
2625
+ valueOf() { return this.get(); }
2626
+ }
2627
+
2628
+ ///////////////////////////////////////////////////////////////////////////////
2629
+
2568
2630
  /** Formats seconds to mm:ss style for display purposes
2569
2631
  * @param {number} t - time in seconds
2570
2632
  * @return {string}
@@ -2651,86 +2713,29 @@ function shareURL(title, url, callback)
2651
2713
 
2652
2714
  ///////////////////////////////////////////////////////////////////////////////
2653
2715
 
2654
- /**
2655
- * Timer object tracks how long has passed since it was set
2656
- * @memberof Engine
2657
- * @example
2658
- * let a = new Timer; // creates a timer that is not set
2659
- * a.set(3); // sets the timer to 3 seconds
2660
- *
2661
- * let b = new Timer(1); // creates a timer with 1 second left
2662
- * b.unset(); // unset the timer
2663
- */
2664
- class Timer
2716
+ /** Read save data from local storage
2717
+ * @param {string} saveName - unique name for the game/save
2718
+ * @param {Object} [defaultSaveData] - default values for save
2719
+ * @return {Object}
2720
+ * @memberof Utilities */
2721
+ function readSaveData(saveName, defaultSaveData)
2665
2722
  {
2666
- /** Create a timer object set time passed in
2667
- * @param {number} [timeLeft] - How much time left before the timer
2668
- * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
2669
- constructor(timeLeft, useRealTime=false)
2670
- {
2671
- ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
2672
- this.useRealTime = useRealTime;
2673
- const globalTime = this.getGlobalTime();
2674
- this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
2675
- this.setTime = timeLeft;
2676
- }
2677
-
2678
- /** Set the timer with seconds passed in
2679
- * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
2680
- set(timeLeft=0)
2681
- {
2682
- ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2683
- const globalTime = this.getGlobalTime();
2684
- this.time = globalTime + timeLeft;
2685
- this.setTime = timeLeft;
2686
- }
2687
-
2688
- /** Set if the timer should keep running even when the game is paused
2689
- * @param {boolean} [useRealTime] */
2690
- setUseRealTime(useRealTime=true)
2691
- {
2692
- ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2693
- this.useRealTime = useRealTime;
2694
- }
2695
-
2696
- /** Unset the timer */
2697
- unset() { this.time = undefined; }
2698
-
2699
- /** Returns true if set
2700
- * @return {boolean} */
2701
- isSet() { return this.time !== undefined; }
2702
-
2703
- /** Returns true if set and has not elapsed
2704
- * @return {boolean} */
2705
- active() { return this.getGlobalTime() < this.time; }
2706
-
2707
- /** Returns true if set and elapsed
2708
- * @return {boolean} */
2709
- elapsed() { return this.getGlobalTime() >= this.time; }
2710
-
2711
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2712
- * @return {number} */
2713
- get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2714
-
2715
- /** Get percentage elapsed based on time it was set to, returns 0 if not set
2716
- * @return {number} */
2717
- getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2718
-
2719
- /** Get the time this timer was set to, returns 0 if not set
2720
- * @return {number} */
2721
- getSetTime() { return this.isSet() ? this.setTime : 0; }
2722
-
2723
- /** Get the current global time this timer is based on
2724
- * @return {number} */
2725
- getGlobalTime() { return this.useRealTime ? timeReal : time; }
2726
-
2727
- /** Returns this timer expressed as a string
2728
- * @return {string} */
2729
- toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2723
+ ASSERT(isString(saveName), 'loadData requires saveName string');
2724
+
2725
+ // replace undefined values with defaults
2726
+ const data = localStorage[saveName];
2727
+ const loadedData = data ? JSON.parse(data) : {};
2728
+ return { ...defaultSaveData, ...loadedData };
2729
+ }
2730
2730
 
2731
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2732
- * @return {number} */
2733
- valueOf() { return this.get(); }
2731
+ /** Write save data to local storage
2732
+ * @param {string} saveName - unique name for the game/save
2733
+ * @param {Object} saveData - object containing data to be saved
2734
+ * @memberof Utilities */
2735
+ function writeSaveData(saveName, saveData)
2736
+ {
2737
+ ASSERT(isString(saveName), 'saveData requires saveName string');
2738
+ localStorage[saveName] = JSON.stringify(saveData);
2734
2739
  }
2735
2740
  /**
2736
2741
  * LittleJS Engine Settings
@@ -3391,17 +3396,19 @@ class EngineObject
3391
3396
  this.additiveColor = undefined;
3392
3397
  /** @property {boolean} - Should it flip along y axis when rendered */
3393
3398
  this.mirror = false;
3399
+ /** @property {boolean} - Has object been destroyed? */
3400
+ this.destroyed = false;
3394
3401
 
3395
3402
  // physical properties
3396
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
3403
+ /** @property {number} - How heavy the object is, static if 0 */
3397
3404
  this.mass = objectDefaultMass;
3398
- /** @property {number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
3405
+ /** @property {number} - How much to slow down velocity each frame (0-1) */
3399
3406
  this.damping = objectDefaultDamping;
3400
- /** @property {number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
3407
+ /** @property {number} - How much to slow down rotation each frame (0-1) */
3401
3408
  this.angleDamping = objectDefaultAngleDamping;
3402
- /** @property {number} [restitution=objectDefaultRestitution] - How bouncy the object is when colliding (0-1) */
3409
+ /** @property {number} - How bouncy the object is when colliding (0-1) */
3403
3410
  this.restitution = objectDefaultRestitution;
3404
- /** @property {number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
3411
+ /** @property {number} - How much friction to apply when sliding (0-1) */
3405
3412
  this.friction = objectDefaultFriction;
3406
3413
  /** @property {number} - How much to scale gravity by for this object */
3407
3414
  this.gravityScale = 1;
@@ -3610,10 +3617,10 @@ class EngineObject
3610
3617
  if (!tileCollisionTest(oldPos, this.size, this))
3611
3618
  {
3612
3619
  // test which side we bounced off (or both if a corner)
3613
- const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3614
- const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
3615
-
3616
- if (blockedLayerX)
3620
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
3621
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3622
+ const restitution = max(this.restitution, hitLayer.restitution);
3623
+ if (isBlockedX)
3617
3624
  {
3618
3625
  // try to move up a tiny bit
3619
3626
  const epsilon = 1e-3;
@@ -3629,16 +3636,12 @@ class EngineObject
3629
3636
  return;
3630
3637
  }
3631
3638
 
3632
- // move to previous position and bounce
3639
+ // move to previous X position and bounce
3633
3640
  this.pos.x = oldPos.x;
3634
- this.velocity.x *= -this.restitution;
3641
+ this.velocity.x *= -restitution;
3635
3642
  }
3636
- if (blockedLayerY || !blockedLayerX)
3643
+ if (isBlockedY || !isBlockedX)
3637
3644
  {
3638
- // bounce velocity
3639
- const restitution = max(this.restitution, hitLayer.restitution);
3640
- this.velocity.y *= -restitution;
3641
-
3642
3645
  if (wasFalling)
3643
3646
  {
3644
3647
  // adjust position to slightly away from nearest tile
@@ -3654,10 +3657,12 @@ class EngineObject
3654
3657
  }
3655
3658
  else
3656
3659
  {
3657
- // move to previous position
3660
+ // move to previous Y position
3658
3661
  this.pos.y = oldPos.y;
3659
3662
  this.groundObject = undefined;
3660
3663
  }
3664
+ // bounce velocity
3665
+ this.velocity.y *= -restitution;
3661
3666
  }
3662
3667
  debugPhysics && debugRect(this.pos, this.size, '#f00');
3663
3668
  }
@@ -3675,19 +3680,19 @@ class EngineObject
3675
3680
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3676
3681
  }
3677
3682
 
3678
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
3679
- destroy()
3683
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal
3684
+ * @param {boolean} [immediate] - should attached effects be allowed to die off? */
3685
+ destroy(immediate=false)
3680
3686
  {
3681
- if (this.destroyed)
3682
- return;
3687
+ if (this.destroyed) return;
3683
3688
 
3684
3689
  // disconnect from parent and destroy children
3685
- this.destroyed = 1;
3690
+ this.destroyed = true;
3686
3691
  this.parent?.removeChild(this);
3687
3692
  for (const child of this.children)
3688
3693
  {
3689
3694
  child.parent = undefined;
3690
- child.destroy();
3695
+ child.destroy(immediate);
3691
3696
  }
3692
3697
  }
3693
3698
 
@@ -3786,7 +3791,7 @@ class EngineObject
3786
3791
  }
3787
3792
 
3788
3793
  /** Check if overlapping another engine object
3789
- * Collisions are resoloved to prevent overlaps
3794
+ * Collisions are resolved to prevent overlaps
3790
3795
  * @param {EngineObject} object
3791
3796
  * @return {boolean} */
3792
3797
  isOverlappingObject(object)
@@ -4038,7 +4043,7 @@ class TileInfo
4038
4043
  }
4039
4044
 
4040
4045
  /**
4041
- * Returns a tile info for an index using this tile as refrence
4046
+ * Returns a tile info for an index using this tile as reference
4042
4047
  * @param {Vector2|number} [index=0]
4043
4048
  * @return {TileInfo}
4044
4049
  */
@@ -4868,7 +4873,7 @@ let engineFontImage;
4868
4873
  class FontImage
4869
4874
  {
4870
4875
  /** Create an image font
4871
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
4876
+ * @param {TileInfo} tileInfo - Tile info of first character in font
4872
4877
  */
4873
4878
  constructor(tileInfo)
4874
4879
  {
@@ -5744,7 +5749,7 @@ function inputRender()
5744
5749
  }
5745
5750
  }
5746
5751
 
5747
- // center position for right tocuh pad face buttons
5752
+ // center position for right touch pad face buttons
5748
5753
  function touchGamepadButtonCenter()
5749
5754
  {
5750
5755
  const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
@@ -6445,50 +6450,62 @@ const tileCollisionLayers = [];
6445
6450
 
6446
6451
  /** Get tile collision data for a given cell in the grid
6447
6452
  * @param {Vector2} pos
6453
+ * @param {boolean} [solidOnly] - Only check solid layers?
6448
6454
  * @return {number}
6449
6455
  * @memberof TileLayers */
6450
- function tileCollisionGetData(pos)
6456
+ function tileCollisionGetData(pos, solidOnly=true)
6451
6457
  {
6452
6458
  // check all tile collision layers
6453
6459
  for (const layer of tileCollisionLayers)
6460
+ if (!solidOnly || layer.isSolid)
6454
6461
  if (pos.arrayCheck(layer.size))
6455
- return layer.getCollisionData(pos);
6462
+ {
6463
+ const data = layer.getCollisionData(pos);
6464
+ if (data) return data;
6465
+ }
6456
6466
  return 0;
6457
6467
  }
6458
6468
 
6459
6469
  /** Check if a tile layer collides with another object
6460
- * @param {Vector2} pos
6461
- * @param {Vector2} [size=vec2()]
6462
- * @param {EngineObject} [object] - An object or undefined for generic test
6463
- * @param {boolean} [solidOnly] - Only check solid layers if true
6470
+ * @param {Vector2} pos
6471
+ * @param {Vector2} [size=vec2()]
6472
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
6473
+ * @param {boolean} [solidOnly] - Only check solid layers?
6464
6474
  * @return {TileCollisionLayer}
6465
6475
  * @memberof TileLayers */
6466
- function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
6476
+ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
6467
6477
  {
6468
6478
  for (const layer of tileCollisionLayers)
6469
6479
  {
6470
6480
  if (!solidOnly || layer.isSolid)
6471
- if (layer.collisionTest(pos, size, object))
6481
+ if (layer.collisionTest(pos, size, callbackObject))
6472
6482
  return layer;
6473
6483
  }
6474
6484
  }
6475
6485
 
6486
+ /**
6487
+ * @callback TileCollisionCallback - Function to handle a tile collision test
6488
+ * @param {number} tileData - the value of the tile at the position
6489
+ * @param {Vector2} pos - world space position of tile where the collision occurred
6490
+ * @memberof TileLayers
6491
+ */
6492
+
6476
6493
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
6477
- * The point will be inside the colliding tile if it hits (may have a tiny shift)
6478
- * @param {Vector2} posStart
6479
- * @param {Vector2} posEnd
6480
- * @param {EngineObject} [object] - An object or undefined for generic test
6481
- * @param {Vector2} [normal] - Optional normal of the surface hit
6482
- * @param {boolean} [solidOnly=true] - Only check solid layers if true
6494
+ * The point will be inside the colliding tile if it hits
6495
+ * @param {Vector2} posStart
6496
+ * @param {Vector2} posEnd
6497
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
6498
+ * @param {Vector2} [normal] - Optional normal of the surface hit
6499
+ * @param {boolean} [solidOnly=true] - Only check solid layers?
6483
6500
  * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
6484
6501
  * @memberof TileLayers */
6485
- function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
6502
+ function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
6486
6503
  {
6487
6504
  for (const layer of tileCollisionLayers)
6488
6505
  {
6489
6506
  if (!solidOnly || layer.isSolid)
6490
6507
  {
6491
- const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
6508
+ const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, normal)
6492
6509
  if (hitPos) return hitPos;
6493
6510
  }
6494
6511
  }
@@ -7052,18 +7069,22 @@ class TileCollisionLayer extends TileLayer
7052
7069
  /** Check if collision with another object should occur
7053
7070
  * @param {Vector2} pos
7054
7071
  * @param {Vector2} [size=vec2()]
7055
- * @param {EngineObject} [object]
7072
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
7056
7073
  * @return {boolean} */
7057
- collisionTest(pos, size=new Vector2, object)
7074
+ collisionTest(pos, size=new Vector2, callbackObject)
7058
7075
  {
7059
7076
  ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
7060
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
7061
-
7062
- // transform to local layer space
7063
- const posX = pos.x - this.pos.x;
7064
- const posY = pos.y - this.pos.y;
7077
+ ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
7078
+
7079
+ // make function to check for collision
7080
+ const collisionTest = callbackObject ? typeof callbackObject === 'function' ?
7081
+ (tileData, pos)=> callbackObject(tileData, pos) :
7082
+ (tileData, pos)=> callbackObject.collideWithTile(tileData, pos) :
7083
+ ()=> true;
7065
7084
 
7066
7085
  // check any tiles in the area for collision
7086
+ const posX = pos.x - this.pos.x;
7087
+ const posY = pos.y - this.pos.y;
7067
7088
  const minX = max(posX - size.x/2|0, 0);
7068
7089
  const minY = max(posY - size.y/2|0, 0);
7069
7090
  const maxX = min(posX + size.x/2, this.size.x);
@@ -7074,9 +7095,7 @@ class TileCollisionLayer extends TileLayer
7074
7095
  {
7075
7096
  // check if the object should collide with this tile
7076
7097
  const tileData = this.collisionData[y*this.size.x+x];
7077
- if (tileData)
7078
- if (!object || object.collideWithTile(tileData,
7079
- hitPos.set(x + this.pos.x, y + this.pos.y)))
7098
+ if (tileData && collisionTest(tileData, hitPos.set(x+this.pos.x, y+this.pos.y)))
7080
7099
  return true;
7081
7100
  }
7082
7101
  return false;
@@ -7084,52 +7103,68 @@ class TileCollisionLayer extends TileLayer
7084
7103
 
7085
7104
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
7086
7105
  * The point will be inside the colliding tile if it hits (may have a tiny shift)
7087
- * @param {Vector2} posStart
7088
- * @param {Vector2} posEnd
7089
- * @param {EngineObject} [object] - An object or undefined for generic test
7090
- * @param {Vector2} [normal] - Optional normal of the surface hit
7106
+ * @param {Vector2} posStart
7107
+ * @param {Vector2} posEnd
7108
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
7109
+ * @param {Vector2} [normal] - Optional normal of the surface hit
7091
7110
  * @return {Vector2|undefined} */
7092
- collisionRaycast(posStart, posEnd, object, normal)
7111
+ collisionRaycast(posStart, posEnd, callbackObject, normal)
7093
7112
  {
7094
7113
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
7095
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
7114
+ ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
7096
7115
 
7097
- const localPos = new Vector2;
7098
- const collisionTest = (pos)=>
7116
+ // make function to check for collision
7117
+ const collisionTest = callbackObject ? typeof callbackObject === 'function' ?
7118
+ (tileData, pos)=> callbackObject(tileData, pos) :
7119
+ (tileData, pos)=> callbackObject.collideWithTile(tileData, pos) :
7120
+ (tileData)=> tileData > 0;
7121
+ const testFunction = (pos)=>
7099
7122
  {
7100
- // check for tile collision
7101
- localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
7102
- const tileData = this.getCollisionData(localPos);
7103
- return tileData && (!object || object.collideWithTile(tileData, pos));
7123
+ const tileData = this.getCollisionData(localPos.set(pos.x - this.pos.x, pos.y - this.pos.y));
7124
+ return tileData && collisionTest(tileData, pos);
7104
7125
  }
7105
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
7106
- const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
7107
- if (hitPos)
7126
+
7127
+ // use line test against tile collision
7128
+ const localPos = new Vector2;
7129
+ const hitPos = lineTest(posStart, posEnd, testFunction, normal);
7130
+ if (debugRaycast && hitPos)
7108
7131
  {
7109
7132
  const tilePos = hitPos.floor().add(vec2(.5));
7110
- debugRaycast && debugRect(tilePos, vec2(1), '#f008');
7111
- debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
7112
- debugRaycast && debugPoint(hitPos, '#0f0');
7113
- debugRaycast && normal &&
7114
- debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
7115
- return hitPos;
7133
+ debugRect(tilePos, vec2(1), '#f008');
7134
+ debugLine(posStart, posEnd, '#00f', .02);
7135
+ debugLine(posStart, hitPos, '#f00', .02);
7136
+ debugPoint(hitPos, '#0f0');
7137
+ normal && debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
7116
7138
  }
7139
+ return hitPos;
7117
7140
  }
7118
7141
  }
7119
7142
  /**
7120
7143
  * LittleJS Particle System
7144
+ * - A simple but fast and flexible particle system
7145
+ * - Lightweight Particles are created and managed by ParticleEmitters
7146
+ * - The particle design tool can be used to help create emitters
7147
+ * @namespace Particles
7121
7148
  */
7122
7149
 
7123
7150
  /**
7124
- * @callback ParticleCallbackFunction - Function that processes a particle
7151
+ * @callback ParticleCallback - Function that processes a particle
7125
7152
  * @param {Particle} particle
7126
- * @memberof Engine
7153
+ * @memberof Particles
7154
+ */
7155
+
7156
+ /**
7157
+ * @callback ParticleCollideCallback - Collide callback for particles
7158
+ * @param {Particle} particle
7159
+ * @param {number} tileData
7160
+ * @param {Vector2} pos
7161
+ * @memberof Particles
7127
7162
  */
7128
7163
 
7129
7164
  /**
7130
7165
  * Particle Emitter - Spawns particles with the given settings
7131
7166
  * @extends EngineObject
7132
- * @memberof Engine
7167
+ * @memberof Particles
7133
7168
  * @example
7134
7169
  * // create a particle emitter
7135
7170
  * let pos = vec2(2,3);
@@ -7147,7 +7182,7 @@ class TileCollisionLayer extends TileLayer
7147
7182
  class ParticleEmitter extends EngineObject
7148
7183
  {
7149
7184
  /** Create a particle system with the given settings
7150
- * @param {Vector2} position - World space position of the emitter
7185
+ * @param {Vector2} pos - World space position of the emitter
7151
7186
  * @param {number} [angle] - Angle to emit the particles
7152
7187
  * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
7153
7188
  * @param {number} [emitTime] - How long to stay alive (0 is forever)
@@ -7177,7 +7212,7 @@ class ParticleEmitter extends EngineObject
7177
7212
  */
7178
7213
  constructor
7179
7214
  (
7180
- position,
7215
+ pos,
7181
7216
  angle,
7182
7217
  emitSize = 0,
7183
7218
  emitTime = 0,
@@ -7206,12 +7241,13 @@ class ParticleEmitter extends EngineObject
7206
7241
  localSpace = false
7207
7242
  )
7208
7243
  {
7209
- super(position, vec2(), tileInfo, angle, undefined, renderOrder);
7244
+ super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
7210
7245
 
7211
7246
  // emitter settings
7247
+ /** @property {boolean} - Should particles be emitted in a circle */
7248
+ this.emitCircle = typeof emitSize === 'number';
7212
7249
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
7213
- this.emitSize = emitSize instanceof Vector2 ?
7214
- emitSize.copy() : emitSize;
7250
+ this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
7215
7251
  /** @property {number} - How long to stay alive (0 is forever) */
7216
7252
  this.emitTime = emitTime;
7217
7253
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -7262,26 +7298,31 @@ class ParticleEmitter extends EngineObject
7262
7298
  this.localSpace = localSpace;
7263
7299
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
7264
7300
  this.trailScale = 0;
7265
- /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
7266
- this.particleDestroyCallback = undefined;
7267
- /** @property {ParticleCallbackFunction} - Callback when particle is created */
7301
+ /** @property {ParticleCallback} - Callback when particle is created */
7268
7302
  this.particleCreateCallback = undefined;
7269
- /** @property {number} - Track particle emit time */
7270
- this.emitTimeBuffer = 0;
7303
+ /** @property {ParticleCallback} - Callback when particle is destroyed */
7304
+ this.particleDestroyCallback = undefined;
7305
+ /** @property {ParticleCollideCallback} - Callback when particle collides */
7306
+ this.particleCollideCallback = undefined;
7271
7307
  /** @property {number} - Percentage of velocity to pass to particles (0-1) */
7272
7308
  this.velocityInheritance = 0;
7309
+ /** @property {number} - Track particle emit time */
7310
+ this.emitTimeBuffer = 0;
7311
+ /** @property {Array<Particle>} - Array of particles for this emitter */
7312
+ this.particles = [];
7273
7313
 
7274
7314
  // track previous position and angle
7275
7315
  this.previousAngle = this.angle;
7276
7316
  this.previousPos = this.pos.copy();
7277
7317
  }
7278
7318
 
7279
- /** Emitters do not have physics */
7280
- updatePhysics() {}
7281
-
7282
7319
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
7283
7320
  update()
7284
7321
  {
7322
+ // physics sanity checks
7323
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
7324
+ ASSERT(this.damping >= 0 && this.damping <= 1);
7325
+
7285
7326
  if (this.velocityInheritance)
7286
7327
  {
7287
7328
  // pass emitter velocity to particles
@@ -7295,7 +7336,7 @@ class ParticleEmitter extends EngineObject
7295
7336
  }
7296
7337
 
7297
7338
  // update emitter
7298
- if (!this.emitTime || this.getAliveTime() <= this.emitTime)
7339
+ if (this.isActive())
7299
7340
  {
7300
7341
  // emit particles
7301
7342
  if (this.emitRate && particleEmitRateScale)
@@ -7305,14 +7346,21 @@ class ParticleEmitter extends EngineObject
7305
7346
  this.emitParticle();
7306
7347
  }
7307
7348
  }
7308
- else
7309
- this.destroy();
7349
+ else if (this.particles.length === 0)
7350
+ this.destroy(true);
7351
+
7352
+ // update and remove destroyed particles
7353
+ this.particles = this.particles.filter((p)=>
7354
+ {
7355
+ p.update();
7356
+ return !p.destroyed;
7357
+ });
7310
7358
 
7311
7359
  if (debugParticles)
7312
7360
  {
7313
7361
  // show emitter bounds
7314
- if (typeof this.emitSize === 'number')
7315
- debugCircle(this.pos, this.emitSize/2, '#0f0');
7362
+ if (this.emitCircle)
7363
+ debugCircle(this.pos, this.emitSize.x/2, '#0f0');
7316
7364
  else
7317
7365
  debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
7318
7366
  }
@@ -7323,14 +7371,15 @@ class ParticleEmitter extends EngineObject
7323
7371
  emitParticle()
7324
7372
  {
7325
7373
  // spawn a particle
7326
- let pos = typeof this.emitSize === 'number' ? // check if number was used
7327
- randInCircle(this.emitSize/2) // circle emitter
7328
- : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
7374
+ let pos = this.emitCircle ? // check if circle emitter
7375
+ randInCircle(this.emitSize.x/2) // circle emitter
7376
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
7329
7377
  .multiply(this.emitSize).rotate(this.angle)
7330
7378
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
7331
7379
  if (!this.localSpace)
7332
7380
  {
7333
- pos = this.pos.add(pos);
7381
+ pos.x += this.pos.x;
7382
+ pos.y += this.pos.y;
7334
7383
  angle += this.angle;
7335
7384
  }
7336
7385
 
@@ -7350,25 +7399,17 @@ class ParticleEmitter extends EngineObject
7350
7399
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
7351
7400
 
7352
7401
  // build particle
7353
- const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
7354
- particle.velocity = vec2().setAngle(velocityAngle, speed);
7355
- particle.angleVelocity = angleSpeed;
7402
+ const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
7403
+ let angleVelocity = angleSpeed;
7356
7404
  if (!this.localSpace && this.velocityInheritance > 0)
7357
7405
  {
7358
7406
  // apply emitter velocity to particle
7359
- particle.velocity.x += this.velocity.x;
7360
- particle.velocity.y += this.velocity.y;
7361
- particle.angleVelocity += this.angleVelocity;
7362
- }
7363
- particle.fadeRate = this.fadeRate;
7364
- particle.damping = this.damping;
7365
- particle.angleDamping = this.angleDamping;
7366
- particle.restitution = this.restitution;
7367
- particle.friction = this.friction;
7368
- particle.gravityScale = this.gravityScale;
7369
- particle.collideTiles = this.collideTiles;
7370
- particle.renderOrder = this.renderOrder;
7371
- particle.mirror = randBool();
7407
+ velocity.x += this.velocity.x;
7408
+ velocity.y += this.velocity.y;
7409
+ angleVelocity += this.angleVelocity;
7410
+ }
7411
+ const particle = new Particle(this, pos, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, velocity, angleVelocity);
7412
+ this.particles.push(particle);
7372
7413
 
7373
7414
  // call particle create callback
7374
7415
  this.particleCreateCallback?.(particle);
@@ -7377,140 +7418,245 @@ class ParticleEmitter extends EngineObject
7377
7418
  return particle;
7378
7419
  }
7379
7420
 
7380
- // Particle emitters are not rendered, only the particles are
7381
- render() {}
7421
+ /** Particle emitters do not have physics */
7422
+ updatePhysics() {}
7423
+
7424
+ /** Render all particles for this emitter */
7425
+ render()
7426
+ {
7427
+ // render all particles
7428
+ for (const particle of this.particles)
7429
+ particle.render();
7430
+ }
7431
+
7432
+ /** is emitter actively spawning */
7433
+ isActive() { return !this.emitTime || this.getAliveTime() < this.emitTime; }
7434
+
7435
+ /** Destroy the particle emitter
7436
+ * @param {boolean} [immediate] - should particle emitters and other attached effects be allowed to die off */
7437
+ destroy(immediate=false)
7438
+ {
7439
+ if (this.destroyed) return;
7440
+
7441
+ super.destroy(immediate);
7442
+ if (!immediate && this.particles.length > 0)
7443
+ {
7444
+ // wait for particles to die off
7445
+ this.destroyed = false;
7446
+ this.emitTime = -1;
7447
+ }
7448
+ }
7382
7449
  }
7383
7450
 
7384
7451
  ///////////////////////////////////////////////////////////////////////////////
7385
7452
  /**
7386
7453
  * Particle Object - Created automatically by Particle Emitters
7387
- * @extends EngineObject
7388
- * @memberof Engine
7454
+ * @memberof Particles
7389
7455
  */
7390
- class Particle extends EngineObject
7456
+ class Particle
7391
7457
  {
7392
7458
  /**
7393
7459
  * Create a particle with the passed in settings
7394
7460
  * Typically this is created automatically by a ParticleEmitter
7395
- * @param {Vector2} position - World space position of the particle
7396
- * @param {TileInfo} tileInfo - Tile info to render particles
7397
- * @param {number} angle - Angle to rotate the particle
7398
- * @param {Color} colorStart - Color at start of life
7399
- * @param {Color} colorEnd - Color at end of life
7400
- * @param {number} lifeTime - How long to live for
7401
- * @param {number} sizeStart - Size at start of life
7402
- * @param {number} sizeEnd - Size at end of life
7403
- * @param {number} fadeRate - How quick to fade in/out
7404
- * @param {boolean} additive - Does it use additive blend mode
7405
- * @param {number} trailScale - If a trail, how long to make it
7406
- * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
7407
- * @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
7461
+ * @param {ParticleEmitter} emitter - The emitter that created this particle
7462
+ * @param {Vector2} pos - World or local space position
7463
+ * @param {number} angle - Angle of the particle
7464
+ * @param {Color} colorStart - Color at start of life
7465
+ * @param {Color} colorEnd - Color at end of life
7466
+ * @param {number} lifeTime - How long to live for
7467
+ * @param {number} sizeStart - Size at start of life
7468
+ * @param {number} sizeEnd - Size at end of life
7469
+ * @param {Vector2} [velocity] - Velocity of the particle
7470
+ * @param {number} [angleVelocity] - Angular speed of the particle
7408
7471
  */
7409
- constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
7410
- )
7472
+ constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
7411
7473
  {
7412
- super(position, vec2(), tileInfo, angle);
7413
-
7414
- /** @property {Color} - Color at start of life */
7474
+ /** @property {ParticleEmitter} */
7475
+ this.emitter = emitter;
7476
+ /** @property {Vector2} */
7477
+ this.pos = pos;
7478
+ /** @property {number} */
7479
+ this.angle = angle;
7480
+ /** @property {Vector2} */
7481
+ this.size = vec2(sizeStart);
7482
+ /** @property {Color} */
7483
+ this.color = colorStart.copy();
7484
+ /** @property {Color} */
7415
7485
  this.colorStart = colorStart;
7416
- /** @property {Color} - Color at end of life */
7486
+ /** @property {Color} */
7417
7487
  this.colorEnd = colorEnd;
7418
- /** @property {number} - How long to live for */
7488
+ /** @property {number} */
7419
7489
  this.lifeTime = lifeTime;
7420
- /** @property {number} - Size at start of life */
7490
+ /** @property {number} */
7421
7491
  this.sizeStart = sizeStart;
7422
- /** @property {number} - Size at end of life */
7492
+ /** @property {number} */
7423
7493
  this.sizeEnd = sizeEnd;
7424
- /** @property {number} - How quick to fade in/out */
7425
- this.fadeRate = fadeRate;
7426
- /** @property {boolean} - Is it additive */
7427
- this.additive = additive;
7428
- /** @property {number} - If a trail, how long to make it */
7429
- this.trailScale = trailScale;
7430
- /** @property {ParticleEmitter} - Parent emitter if local space */
7431
- this.localSpaceEmitter = localSpaceEmitter;
7432
- /** @property {ParticleCallbackFunction} - Called when particle dies */
7433
- this.destroyCallback = destroyCallback;
7434
- // particles do not clamp speed by default
7435
- this.clampSpeed = false;
7436
- }
7437
-
7438
- /** Update the object physics, called automatically by engine once each frame */
7494
+ /** @property {Vector2} */
7495
+ this.velocity = velocity;
7496
+ /** @property {number} */
7497
+ this.angleVelocity = angleVelocity;
7498
+ /** @property {number} */
7499
+ this.spawnTime = time;
7500
+ /** @property {boolean} */
7501
+ this.mirror = randBool();
7502
+ /** @property {EngineObject} */
7503
+ this.groundObject = undefined;
7504
+ /** @property {boolean} */
7505
+ this.destroyed = false;
7506
+ /** @property {TileInfo} */
7507
+ this.tileInfo = emitter.tileInfo;
7508
+ }
7509
+
7510
+ /** Update the particle */
7439
7511
  update()
7440
7512
  {
7441
- if (this.collideTiles || this.collideSolidObjects)
7513
+ // emitter properties
7514
+ const emitter = this.emitter;
7515
+ const damping = emitter.damping;
7516
+ const angleDamping = emitter.angleDamping;
7517
+ const restitution = emitter.restitution;
7518
+ const friction = emitter.friction;
7519
+ const gravityScale = emitter.gravityScale;
7520
+ const collideTiles = emitter.collideTiles;
7521
+ const collideCallback = emitter.particleCollideCallback;
7522
+
7523
+ // destroy particle when its time runs out
7524
+ if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
7442
7525
  {
7443
- // only apply max circular speed if particle can collide
7444
- const length2 = this.velocity.lengthSquared();
7445
- if (length2 > objectMaxSpeed*objectMaxSpeed)
7446
- {
7447
- const s = objectMaxSpeed / length2**.5;
7448
- this.velocity.x *= s;
7449
- this.velocity.y *= s;
7450
- }
7526
+ this.destroy();
7527
+ return;
7451
7528
  }
7452
7529
 
7453
- if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
7530
+ // apply physics
7531
+ const oldPos = this.pos.copy();
7532
+ this.velocity.x *= damping;
7533
+ this.velocity.y *= damping;
7534
+ this.pos.x += this.velocity.x += gravity.x * gravityScale;
7535
+ this.pos.y += this.velocity.y += gravity.y * gravityScale;
7536
+ this.angle += this.angleVelocity *= angleDamping;
7537
+
7538
+ // don't do collision if solver disabled
7539
+ if (!enablePhysicsSolver || !collideTiles) return;
7540
+
7541
+ // apply max circular speed to prevent going through collision
7542
+ const length2 = this.velocity.lengthSquared();
7543
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
7544
+ {
7545
+ const s = objectMaxSpeed / length2**.5;
7546
+ this.velocity.x *= s;
7547
+ this.velocity.y *= s;
7548
+ }
7549
+
7550
+ // check collision against tiles
7551
+ this.groundObject = undefined;
7552
+ const testCollision = collideCallback ? (pos)=>
7454
7553
  {
7455
- // destroy particle when its time runs out
7456
- const c = this.colorEnd;
7457
- this.color.set(c.r, c.g, c.b, c.a);
7458
- this.size.set(this.sizeEnd, this.sizeEnd);
7459
- this.destroyCallback?.(this);
7460
- this.destroyed = 1;
7554
+ const data = tileCollisionGetData(pos);
7555
+ return data && collideCallback(this, data, pos);
7556
+ } : (pos)=> tileCollisionGetData(pos) > 0;
7557
+
7558
+ if (testCollision(this.pos))
7559
+ {
7560
+ // if already was stuck in collision, don't do anything
7561
+ const hitLayer = tileCollisionTest(this.pos);
7562
+ if (!testCollision(oldPos))
7563
+ {
7564
+ if (!collideCallback || collideCallback?.(this, hitLayer))
7565
+ {
7566
+ // test which side we bounced off (or both if a corner)
7567
+ const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
7568
+ const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
7569
+ const hitRestitution = max(restitution, hitLayer.restitution);
7570
+ const hitFriction = max(friction, hitLayer.friction);
7571
+ if (isBlockedX)
7572
+ {
7573
+ // move to previous X position and bounce
7574
+ this.pos.x = oldPos.x;
7575
+ this.velocity.x *= -hitRestitution;
7576
+ this.velocity.y *= hitFriction;
7577
+ }
7578
+ if (isBlockedY || !isBlockedX)
7579
+ {
7580
+ const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
7581
+ if (wasFalling)
7582
+ this.groundObject = hitLayer;
7583
+
7584
+ // move to previous Y position and bounce
7585
+ this.pos.y = oldPos.y;
7586
+ this.velocity.y *= -hitRestitution;
7587
+ this.velocity.x *= hitFriction;
7588
+ }
7589
+ debugPhysics && debugRect(this.pos, this.size, '#f00');
7590
+ }
7591
+ }
7461
7592
  }
7462
7593
  }
7463
7594
 
7464
- /** Render the particle, automatically called each frame, sorted by renderOrder */
7595
+ /** Destroy this particle */
7596
+ destroy()
7597
+ {
7598
+ const destroyCallback = this.emitter.particleDestroyCallback;
7599
+ const c = this.colorEnd;
7600
+ this.color.set(c.r, c.g, c.b, c.a);
7601
+ this.size.set(this.sizeEnd, this.sizeEnd);
7602
+ this.destroyed = true;
7603
+ destroyCallback?.(this);
7604
+ }
7605
+
7606
+ /** Render the particle, automatically called each frame */
7465
7607
  render()
7466
7608
  {
7609
+ // emitter properties
7610
+ const emitter = this.emitter;
7611
+ const localSpace = emitter.localSpace;
7612
+ const additive = emitter.additive;
7613
+ const trailScale = emitter.trailScale;
7614
+ const fadeRate = emitter.fadeRate / 2;
7615
+
7467
7616
  // lerp color and size
7468
7617
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
7469
7618
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
7470
7619
  const size = vec2(radius);
7620
+ const alphaFade = p1 < fadeRate ? p1/fadeRate :
7621
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
7471
7622
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
7472
7623
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
7473
7624
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
7474
- this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
7475
-
7476
- // fade alpha
7477
- const fadeRate = this.fadeRate/2;
7478
- this.color.a *= p1 < fadeRate ? p1/fadeRate :
7479
- p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
7625
+ this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
7480
7626
 
7481
7627
  // draw the particle
7482
- this.additive && setBlendMode(true);
7628
+ additive && setBlendMode(true);
7483
7629
 
7484
7630
  // update the position and angle for drawing
7485
- let pos = this.pos, angle = this.angle;
7486
- if (this.localSpaceEmitter)
7631
+ const pos = this.pos.copy();
7632
+ let angle = this.angle;
7633
+ if (localSpace)
7487
7634
  {
7488
7635
  // in local space of emitter
7489
- const a = this.localSpaceEmitter.angle;
7636
+ const a = emitter.angle;
7490
7637
  const c = cos(a), s = sin(a);
7491
- pos = this.localSpaceEmitter.pos.add(
7492
- new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
7493
- angle += this.localSpaceEmitter.angle;
7638
+ pos.set(emitter.pos.x + pos.x*c - pos.y*s,
7639
+ emitter.pos.y + pos.x*s + pos.y*c);
7640
+ angle += a;
7494
7641
  }
7495
- if (this.trailScale)
7642
+ if (trailScale)
7496
7643
  {
7497
7644
  // trail style particles
7498
- const direction = this.localSpaceEmitter ?
7499
- this.velocity.rotate(-this.localSpaceEmitter.angle) :
7500
- this.velocity;
7501
- const speed = direction.length();
7645
+ const velocity = localSpace ?
7646
+ this.velocity.rotate(-emitter.angle) : this.velocity;
7647
+ const speed = velocity.length();
7502
7648
  if (speed)
7503
7649
  {
7504
7650
  // stretch in direction of motion
7505
- const trailLength = speed * this.trailScale;
7651
+ const trailLength = speed * trailScale;
7506
7652
  size.y = max(size.x, trailLength);
7507
- angle = atan2(direction.x, direction.y);
7653
+ angle = atan2(velocity.x, velocity.y);
7508
7654
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
7509
7655
  }
7510
7656
  }
7511
7657
  else
7512
7658
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
7513
- this.additive && setBlendMode();
7659
+ additive && setBlendMode();
7514
7660
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
7515
7661
  }
7516
7662
  }
@@ -8790,7 +8936,7 @@ class PostProcessPlugin
8790
8936
  {
8791
8937
  /** Create global post processing shader
8792
8938
  * @param {string} shaderCode
8793
- * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8939
+ * @param {boolean} [includeMainCanvas] - combine mainCanvas onto glCanvas
8794
8940
  * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8795
8941
  * @example
8796
8942
  * // create the post process plugin object
@@ -9183,7 +9329,7 @@ class UISystemPlugin
9183
9329
  // navigation properties
9184
9330
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
9185
9331
  this.navigationObject = undefined;
9186
- /** @property {Timer} - Cooldown timer for navigation inputs */
9332
+ /** @property {Timer} - Cool down timer for navigation inputs */
9187
9333
  this.navigationTimer = new Timer(undefined, true);
9188
9334
  /** @property {number} - Time between navigation inputs in seconds */
9189
9335
  this.navigationDelay = .2;
@@ -9649,7 +9795,7 @@ class UISystemPlugin
9649
9795
 
9650
9796
  const savedNavigationDirection = uiSystem.navigationDirection;
9651
9797
 
9652
- // allow both axies for navigation
9798
+ // allow both axes for navigation
9653
9799
  uiSystem.navigationDirection = 2;
9654
9800
 
9655
9801
  // confirm menu
@@ -9744,7 +9890,7 @@ class UIObject
9744
9890
  this.lineWidth = uiSystem.defaultLineWidth;
9745
9891
  /** @property {number} - Corner radius for rounded rects */
9746
9892
  this.cornerRadius = uiSystem.defaultCornerRadius;
9747
- /** @property {string} - Font for this objecct */
9893
+ /** @property {string} - Font for this object */
9748
9894
  this.font = uiSystem.defaultFont;
9749
9895
  /** @property {string} - Font style for this object or undefined */
9750
9896
  this.fontStyle = undefined;
@@ -10232,6 +10378,8 @@ class UIScrollbar extends UIObject
10232
10378
  this.value = value;
10233
10379
  /** @property {Color} - Color for the handle part of the scrollbar */
10234
10380
  this.handleColor = handleColor.copy();
10381
+ /** @property {boolean} - Should it fill up like a progress bar? */
10382
+ this.fillMode = false;
10235
10383
 
10236
10384
  // set properties
10237
10385
  this.text = text;
@@ -10277,19 +10425,30 @@ class UIScrollbar extends UIObject
10277
10425
 
10278
10426
  // handle horizontal or vertical scrollbar
10279
10427
  const isHorizontal = this.size.x > this.size.y;
10280
- const handleSize = isHorizontal ? this.size.y : this.size.x;
10281
- const barSize = isHorizontal ? this.size.x : this.size.y;
10282
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
10283
-
10284
- // draw the scrollbar handle
10285
- const handleWidth = barSize - handleSize;
10286
- const p1 = centerPos - handleWidth/2;
10287
- const p2 = centerPos + handleWidth/2;
10288
- const handlePos = isHorizontal ?
10289
- vec2(lerp(p1, p2, this.value), this.pos.y) :
10290
- vec2(this.pos.x, lerp(p2, p1, this.value))
10291
- const handleColor = this.disabled ? this.disabledColor : this.handleColor;
10292
- uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
10428
+ const barWidth = isHorizontal ? this.size.x : this.size.y;
10429
+ const handleWidth = isHorizontal ? this.size.y : this.size.x;
10430
+ if (this.fillMode)
10431
+ {
10432
+ // draw progress bar
10433
+ const minWidth = min(handleWidth, this.cornerRadius * 2);
10434
+ const progressWidth = lerp(minWidth, barWidth, this.value);
10435
+ const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
10436
+ const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
10437
+ const color = this.disabled ? this.disabledColor : this.handleColor;
10438
+ const drawSize = isHorizontal ?
10439
+ vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
10440
+ uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
10441
+ }
10442
+ else
10443
+ {
10444
+ // draw the scrollbar handle
10445
+ const value = isHorizontal ? this.value : 1 - this.value;
10446
+ const p = (barWidth - handleWidth) * (value - .5);
10447
+ const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
10448
+ const color = this.disabled ? this.disabledColor : this.handleColor;
10449
+ const drawSize = vec2(handleWidth);
10450
+ uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
10451
+ }
10293
10452
 
10294
10453
  // draw the text scaled to fit on the scrollbar
10295
10454
  const textSize = this.getTextSize();
@@ -10311,7 +10470,7 @@ class UIScrollbar extends UIObject
10311
10470
  * @extends UIObject
10312
10471
  * @example
10313
10472
  * // Create a video player UI object
10314
- * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
10473
+ * const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'video.mp4', true);
10315
10474
  * video.play();
10316
10475
  * @memberof UISystem
10317
10476
  */
@@ -11060,11 +11219,11 @@ class Box2dStaticObject extends Box2dObject
11060
11219
 
11061
11220
  ///////////////////////////////////////////////////////////////////////////////
11062
11221
  /**
11063
- * Box2D Kiematic Object - Box2d with a kinematic physics body
11222
+ * Box2D Kinematic Object - Box2d with a kinematic physics body
11064
11223
  * @extends Box2dObject
11065
11224
  * @memberof Box2D
11066
11225
  */
11067
- class Box2dKiematicObject extends Box2dObject
11226
+ class Box2dKinematicObject extends Box2dObject
11068
11227
  {
11069
11228
  /** Create a LittleJS object with Box2d physics
11070
11229
  * @param {Vector2} [pos]
@@ -11116,20 +11275,19 @@ class Box2dTileLayer extends Box2dStaticObject
11116
11275
  this.destroyAllFixtures();
11117
11276
 
11118
11277
  // create box2d object for this layer
11119
- const size = this.tileLayer.size;
11120
11278
  this.pos = this.tileLayer.pos.copy();
11121
- this.size = size.copy();
11279
+ this.size = this.tileLayer.size.copy();
11122
11280
 
11123
11281
  // track which tiles have been processed
11124
11282
  const processed = [];
11125
- const getIndex = (x, y)=> x + y * size.x;
11283
+ const getIndex = (x, y)=> x + y * this.size.x;
11126
11284
  const isSolidUnprocessed = (x, y)=>
11127
11285
  !processed[getIndex(x, y)] &&
11128
11286
  this.tileLayer.getCollisionData(vec2(x, y)) > 0;
11129
11287
 
11130
11288
  // combine tiles into larger boxes
11131
- for (let x = 0; x < size.x; ++x)
11132
- for (let y = 0; y < size.y; ++y)
11289
+ for (let x = 0; x < this.size.x; ++x)
11290
+ for (let y = 0; y < this.size.y; ++y)
11133
11291
  {
11134
11292
  if (!isSolidUnprocessed(x, y)) continue;
11135
11293
 
@@ -12785,6 +12943,8 @@ export
12785
12943
  saveCanvas,
12786
12944
  saveDataURL,
12787
12945
  shareURL,
12946
+ readSaveData,
12947
+ writeSaveData,
12788
12948
 
12789
12949
  // Random
12790
12950
  rand,
@@ -12994,7 +13154,7 @@ export
12994
13154
  Box2dPlugin,
12995
13155
  Box2dObject,
12996
13156
  Box2dStaticObject,
12997
- Box2dKiematicObject,
13157
+ Box2dKinematicObject,
12998
13158
  Box2dRaycastResult,
12999
13159
  Box2dJoint,
13000
13160
  Box2dTargetJoint,