littlejsengine 1.17.5 → 1.17.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +10 -10
  2. package/dist/littlejs.d.ts +189 -97
  3. package/dist/littlejs.esm.js +637 -477
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +633 -476
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +631 -474
  8. package/examples/electron/index.html +2 -2
  9. package/examples/games.jpg +0 -0
  10. package/examples/index.html +2 -2
  11. package/examples/logo.png +0 -0
  12. package/examples/logo2.png +0 -0
  13. package/examples/particles/index.html +1 -1
  14. package/examples/platformer/gameEffects.js +3 -3
  15. package/examples/screenshot.jpg +0 -0
  16. package/examples/shorts/base.html +1 -1
  17. package/examples/shorts/colors.js +1 -1
  18. package/examples/shorts/fontImage.js +1 -1
  19. package/examples/shorts/fps.js +1 -1
  20. package/examples/shorts/music.js +3 -2
  21. package/examples/shorts/musicPlayer.js +2 -1
  22. package/examples/shorts/parallax.js +2 -2
  23. package/examples/shorts/tiltedView.js +1 -1
  24. package/examples/starter/index.html +2 -2
  25. package/package.json +1 -1
  26. package/plugins/box2d.js +6 -7
  27. package/plugins/pluginExport.js +2 -1
  28. package/plugins/postProcess.js +1 -1
  29. package/plugins/uiSystem.js +33 -21
  30. package/reference.md +6 -6
  31. package/src/engine.js +106 -152
  32. package/src/engineAudio.js +5 -5
  33. package/src/engineDebug.js +1 -1
  34. package/src/engineDraw.js +36 -14
  35. package/src/engineExport.js +2 -0
  36. package/src/engineInput.js +1 -1
  37. package/src/engineMath.js +6 -4
  38. package/src/engineObject.js +24 -24
  39. package/src/engineParticles.js +241 -118
  40. package/src/engineTileLayer.js +66 -48
  41. package/src/engineUtilities.js +105 -78
  42. package/plugins/Box2D_License.txt +0 -17
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.17.5';
36
+ const engineVersion = '1.17.11';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -396,33 +396,11 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
396
396
  workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
397
397
 
398
398
  // create promises for loading images
399
- const promises = imageSources.map((src, textureIndex)=>
400
- new Promise(resolve =>
401
- {
402
- ASSERT(isString(src), 'imageSources must be an array of strings');
403
-
404
- const image = new Image;
405
- image.onerror = image.onload = ()=>
406
- {
407
- const textureInfo = new TextureInfo(image);
408
- textureInfos[textureIndex] = textureInfo;
409
- resolve();
410
- }
411
- image.crossOrigin = 'anonymous';
412
- image.src = src;
413
- })
414
- );
399
+ const promises = imageSources.map((src, i)=> loadTexture(i, src));
415
400
 
401
+ // no images to load
416
402
  if (!imageSources.length)
417
- {
418
- // no images to load
419
- promises.push(new Promise(resolve =>
420
- {
421
- const textureInfo = new TextureInfo(new Image);
422
- textureInfos[0] = textureInfo;
423
- resolve();
424
- }));
425
- }
403
+ promises.push(loadTexture(0));
426
404
 
427
405
  // load engine font image
428
406
  promises.push(fontImageInit());
@@ -464,13 +442,13 @@ function engineObjectsUpdate()
464
442
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
465
443
 
466
444
  // recursive object update
467
- function updateObject(o)
445
+ function updateChildObject(o)
468
446
  {
469
447
  if (o.destroyed) return;
470
448
 
471
449
  o.update();
472
450
  for (const child of o.children)
473
- updateObject(child);
451
+ updateChildObject(child);
474
452
  }
475
453
  for (const o of engineObjects)
476
454
  {
@@ -480,7 +458,7 @@ function engineObjectsUpdate()
480
458
  o.update();
481
459
  o.updatePhysics();
482
460
  for (const child of o.children)
483
- updateObject(child);
461
+ updateChildObject(child);
484
462
  o.updateTransforms();
485
463
  }
486
464
 
@@ -489,11 +467,14 @@ function engineObjectsUpdate()
489
467
  }
490
468
 
491
469
  /** Destroy and remove all objects
470
+ * - This can be used to clear out all objects when restarting a level
471
+ * - Objects can override their destroy function to do cleanup or stick around
472
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
492
473
  * @memberof Engine */
493
- function engineObjectsDestroy()
474
+ function engineObjectsDestroy(immediate=true)
494
475
  {
495
476
  for (const o of engineObjects)
496
- o.parent || o.destroy();
477
+ o.parent || o.destroy(immediate);
497
478
  engineObjects = engineObjects.filter(o=>!o.destroyed);
498
479
  }
499
480
 
@@ -568,165 +549,138 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
568
549
  ///////////////////////////////////////////////////////////////////////////////
569
550
  function drawEngineLogo(t)
570
551
  {
552
+ const blackAndWhite = 0;
553
+ const showName = 1;
554
+
571
555
  // LittleJS Logo and Splash Screen
572
556
  const x = mainContext;
573
557
  const w = mainCanvas.width = innerWidth;
574
558
  const h = mainCanvas.height = innerHeight;
575
-
576
559
  {
577
560
  // background
578
561
  const p3 = percent(t, 1, .8);
579
562
  const p4 = percent(t, 0, .5);
580
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
563
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
581
564
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
582
565
  g.addColorStop(1,hsl(0,0,0,p3).toString());
583
566
  x.save();
584
567
  x.fillStyle = g;
585
568
  x.fillRect(0,0,w,h);
586
569
  }
587
-
588
- // draw LittleJS logo...
589
- const rect = (X, Y, W, H, C)=>
570
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
571
+ {
572
+ if (C >= 0)
573
+ {
574
+ if (blackAndWhite)
575
+ x.fillStyle = '#fff';
576
+ else
577
+ {
578
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
579
+ g.addColorStop(0,color(C,2));
580
+ g.addColorStop(1,color(C,1));
581
+ }
582
+ }
583
+ else
584
+ x.fillStyle = '#000';
585
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
586
+ }
587
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
590
588
  {
591
589
  x.beginPath();
592
- x.rect(X,Y,W,C?H*p:H);
593
- x.fillStyle = C;
594
- C ? x.fill() : x.stroke();
595
- };
596
- const line = (X, Y, Z, W)=>
590
+ x.arc(X,Y,R,p*A,p*B);
591
+ gradient(X,Y-R,X,Y+R,C,S);
592
+ }
593
+ const rect = (X,Y,W,H,C)=>
597
594
  {
598
595
  x.beginPath();
599
- x.lineTo(X,Y);
600
- x.lineTo(Z,W);
601
- x.stroke();
602
- };
603
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
596
+ x.rect(X,Y,W,H*p);
597
+ gradient(X,Y+H,X+W,Y,C);
598
+ }
599
+ const poly = (points,C,Y,H)=>
604
600
  {
605
- const D = (A+B)/2, E = p*(B-A)/2;
606
601
  x.beginPath();
607
- F && x.lineTo(X,Y);
608
- x.arc(X,Y,R,D-E,D+E);
609
- x.fillStyle = C;
610
- C ? x.fill() : x.stroke();
611
- };
612
- const color = (c=0, l=0)=>
613
- hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
602
+ for (const p of points)
603
+ x.lineTo(p.x, p.y);
604
+ x.closePath();
605
+ gradient(0, Y, 0, Y+H,C);
606
+ }
607
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
608
+
609
+ // center and fit tos screen
614
610
  const alpha = wave(1,1,t);
615
611
  const p = percent(alpha, .1, .5);
616
-
617
- // setup
612
+ const size = min(6, min(w,h)/99);
618
613
  x.translate(w/2,h/2);
619
- const size = min(6, min(w,h)/99); // fit to screen
620
614
  x.scale(size,size);
621
615
  x.translate(-40,-35);
616
+ p < 1 && x.setLineDash([99*p,99]);
622
617
  x.lineJoin = x.lineCap = 'round';
623
618
  x.lineWidth = .1 + p*1.9;
619
+ //x.strokeStyle='#fff7';
620
+
621
+ if (showName)
622
+ {
623
+ // engine name text
624
+ const Y = 54;
625
+ const s = 'LittleJS';
626
+ x.font = '900 15.5px arial';
627
+ x.lineWidth = .1+p*3.9;
628
+ x.textAlign = 'center';
629
+ x.textBaseline = 'top';
630
+ rect(11,Y+1,59,8*p,-1);
631
+ x.beginPath();
632
+
633
+ let w2 = 0;
634
+ for (let i=0;i<s.length;++i)
635
+ w2 += x.measureText(s[i]).width;
636
+ for (let j=2;j--;)
637
+ for (let i=0,X=40-w2/2;i<s.length;++i)
638
+ {
639
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
640
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
641
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
642
+ X += w;
643
+ }
624
644
 
625
- // drawing effect
626
- const p2 = percent(alpha,.1,1);
627
- x.setLineDash([99*p2,99]);
628
-
629
- // cab top
630
- rect(7,16,18,-8,color(2,2));
631
- rect(7,8,18,4,color(2,3));
632
- rect(25,8,8,8,color(2,1));
633
- rect(25,8,-18,8);
634
- rect(25,8,8,8);
635
-
636
- // cab
637
- rect(25,16,7,24,color());
638
- rect(11,39,14,-23,color(1,1));
639
- rect(11,16,14,18,color(1,2));
640
- rect(11,16,14,8,color(1,3));
641
- rect(25,16,-14,24);
642
-
643
- // cab window
644
- rect(15,29,6,-9,color(2,2));
645
- circle(15,21,5,0,PI/2,color(2,4),1);
646
- rect(21,21,-6,9);
647
-
648
- // little stack
649
- rect(37,14,9,6,color(3,2));
650
- rect(37,14,4.5,6,color(3,3));
651
- rect(37,14,9,6);
645
+ x.lineWidth = .1 + p*1.9;
646
+ rect(3,Y,73,0); // bottom
647
+ }
652
648
 
649
+ rect(7,15,26,-7,0); // cab top
650
+ rect(25,15,8,25,-1); // cab front
651
+ rect(10,40,15,-25,1); // cab back
652
+ rect(14,21,7,9,2); // cab window
653
+ rect(38,20,6,-6,2); // little stack
654
+
653
655
  // big stack
654
- rect(50,20,10,-8,color(0,1));
655
- rect(50,20,6.5,-8,color(0,2));
656
- rect(50,20,3.5,-8,color(0,3));
657
- rect(50,20,10,-8);
658
- circle(55,2,11.4,.5,PI-.5,color(3,3));
659
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
660
- circle(55,2,11.4,.5,PI-.5);
661
- rect(45,7,20,-7,color(0,2));
662
- rect(45,-1,20,4,color(0,3));
663
- rect(45,-1,20,8);
656
+ rect(49,20,10,-6,0);
657
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
658
+ poly(stackPoints,2,8,6*p);
659
+ rect(44,8,20,-7,0);
664
660
 
665
661
  // engine
666
- for (let i=5; i--;)
667
- {
668
- // stagger radius to fix slight seam
669
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
670
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
671
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
672
- }
662
+ for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
663
+ circle(59,30,4,0,7,2); // light
673
664
 
674
665
  // engine outline
675
- circle(36,30,10,PI/2,PI*3/2);
676
- circle(48,30,10,PI/2,PI*3/2);
677
- circle(60,30,10);
678
- line(36,20,60,20);
666
+ rect(35,20,24,0); // top
667
+ circle(59,30,10); // front
668
+ circle(47,30,10,PI/2,PI*3/2); // middle
669
+ circle(35,30,10,PI/2,PI*3/2); // back
670
+ rect(7,40,13,7,-1); // bottom back
671
+ rect(17,40,43,14,-1); // bottom center
679
672
 
680
- // engine front light
681
- circle(60,30,4,PI,3*PI,color(3,2));
682
- circle(60,30,4,PI,2*PI,color(3,3));
683
- circle(60,30,4,PI,3*PI);
684
-
685
- // front brush
686
- for (let i=6; i--;)
673
+ // wheels
674
+ for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
675
+
676
+ // cowcatcher
677
+ for (let i=2;i--;)
687
678
  {
688
- x.beginPath();
689
- x.lineTo(53,54);
690
- x.lineTo(53,40);
691
- x.lineTo(53+(1+i*2.9)*p,40);
692
- x.lineTo(53+(4+i*3.5)*p,54);
693
- x.fillStyle = color(0,i%2+2);
694
- x.fill();
695
- i%2 && x.stroke();
679
+ let w=6, s=7, o=53+w*p*i
680
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
681
+ poly(points,0,40,14);
696
682
  }
697
683
 
698
- // wheels
699
- rect(6,40,5,5);
700
- rect(6,40,5,5,color());
701
- rect(15,54,38,-14,color());
702
- for (let i=3; i--;)
703
- for (let j=2; j--;)
704
- {
705
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
706
- x.stroke();
707
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
708
- x.stroke();
709
- }
710
- line(6,40,68,40); // center
711
- line(77,54,4,54); // bottom
712
-
713
- // draw engine name
714
- const s = engineName;
715
- x.font = '900 16px arial';
716
- x.textAlign = 'center';
717
- x.textBaseline = 'top';
718
- x.lineWidth = .1+p*3.9;
719
- let w2 = 0;
720
- for (let i=0; i<s.length; ++i)
721
- w2 += x.measureText(s[i]).width;
722
- for (let j=2; j--;)
723
- for (let i=0, X=41-w2/2; i<s.length; ++i)
724
- {
725
- x.fillStyle = color(i,2);
726
- const w = x.measureText(s[i]).width;
727
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
728
- X += w;
729
- }
730
684
  x.restore();
731
685
  }
732
686
  /**
@@ -887,7 +841,7 @@ function debugPoint(pos, color, time, angle, screenSpace=false)
887
841
  function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
888
842
  {
889
843
  ASSERT(isVector2(posA), 'posA must be a vec2');
890
- ASSERT(isVector2(posB), 'posB must be sa vec2');
844
+ ASSERT(isVector2(posB), 'posB must be a vec2');
891
845
  ASSERT(isNumber(width), 'width must be a number');
892
846
 
893
847
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
@@ -1715,7 +1669,9 @@ function wave(frequency=1, amplitude=1, t=time, offset=0)
1715
1669
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
1716
1670
 
1717
1671
  /**
1718
- * Check if object is a valid string or can be converted to one
1672
+ * Check if object can be converted to a string (has a toString method)
1673
+ * - Returns true for strings, numbers, and most objects
1674
+ * - Returns false for null and undefined
1719
1675
  * @param {any} s
1720
1676
  * @return {boolean}
1721
1677
  * @memberof Math */
@@ -1792,7 +1748,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
1792
1748
  if (stepX < 0)
1793
1749
  hitPos.x -= e;
1794
1750
  }
1795
- if (stepY < 0)
1751
+ else if (stepY < 0)
1796
1752
  hitPos.y -= e;
1797
1753
 
1798
1754
  // set normal
@@ -2499,7 +2455,7 @@ class Color
2499
2455
  * @memberof Math */
2500
2456
  const WHITE = debugProtectConstant(rgb());
2501
2457
 
2502
- /** Color - Clear White #757474ff with 0 alpha
2458
+ /** Color - Clear White #ffffff00 with 0 alpha
2503
2459
  * @type {Color}
2504
2460
  * @memberof Math */
2505
2461
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
@@ -2509,7 +2465,7 @@ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
2509
2465
  * @memberof Math */
2510
2466
  const BLACK = debugProtectConstant(rgb(0,0,0));
2511
2467
 
2512
- /** Color - Clear Black #000000 with 0 alpha
2468
+ /** Color - Clear Black #00000000 with 0 alpha
2513
2469
  * @type {Color}
2514
2470
  * @memberof Math */
2515
2471
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
@@ -2565,6 +2521,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2565
2521
  * @namespace Utilities
2566
2522
  */
2567
2523
 
2524
+ /**
2525
+ * Timer object tracks how long has passed since it was set
2526
+ * @memberof Engine
2527
+ * @example
2528
+ * let a = new Timer; // creates a timer that is not set
2529
+ * a.set(3); // sets the timer to 3 seconds
2530
+ *
2531
+ * let b = new Timer(1); // creates a timer with 1 second left
2532
+ * b.unset(); // unset the timer
2533
+ */
2534
+ class Timer
2535
+ {
2536
+ /** Create a timer object set time passed in
2537
+ * @param {number} [timeLeft] - How much time left before the timer
2538
+ * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
2539
+ constructor(timeLeft, useRealTime=false)
2540
+ {
2541
+ ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
2542
+ this.useRealTime = useRealTime;
2543
+ const globalTime = this.getGlobalTime();
2544
+ this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
2545
+ this.setTime = timeLeft;
2546
+ }
2547
+
2548
+ /** Set the timer with seconds passed in
2549
+ * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
2550
+ set(timeLeft=0)
2551
+ {
2552
+ ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2553
+ const globalTime = this.getGlobalTime();
2554
+ this.time = globalTime + timeLeft;
2555
+ this.setTime = timeLeft;
2556
+ }
2557
+
2558
+ /** Set if the timer should keep running even when the game is paused
2559
+ * @param {boolean} [useRealTime] */
2560
+ setUseRealTime(useRealTime=true)
2561
+ {
2562
+ ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2563
+ this.useRealTime = useRealTime;
2564
+ }
2565
+
2566
+ /** Unset the timer */
2567
+ unset() { this.time = undefined; }
2568
+
2569
+ /** Returns true if set
2570
+ * @return {boolean} */
2571
+ isSet() { return this.time !== undefined; }
2572
+
2573
+ /** Returns true if set and has not elapsed
2574
+ * @return {boolean} */
2575
+ active() { return this.getGlobalTime() < this.time; }
2576
+
2577
+ /** Returns true if set and elapsed
2578
+ * @return {boolean} */
2579
+ elapsed() { return this.getGlobalTime() >= this.time; }
2580
+
2581
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2582
+ * @return {number} */
2583
+ get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2584
+
2585
+ /** Get percentage elapsed based on time it was set to, returns 0 if not set
2586
+ * @return {number} */
2587
+ getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2588
+
2589
+ /** Get the time this timer was set to, returns 0 if not set
2590
+ * @return {number} */
2591
+ getSetTime() { return this.isSet() ? this.setTime : 0; }
2592
+
2593
+ /** Get the current global time this timer is based on
2594
+ * @return {number} */
2595
+ getGlobalTime() { return this.useRealTime ? timeReal : time; }
2596
+
2597
+ /** Returns this timer expressed as a string
2598
+ * @return {string} */
2599
+ toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
2600
+
2601
+ /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2602
+ * @return {number} */
2603
+ valueOf() { return this.get(); }
2604
+ }
2605
+
2606
+ ///////////////////////////////////////////////////////////////////////////////
2607
+
2568
2608
  /** Formats seconds to mm:ss style for display purposes
2569
2609
  * @param {number} t - time in seconds
2570
2610
  * @return {string}
@@ -2651,86 +2691,29 @@ function shareURL(title, url, callback)
2651
2691
 
2652
2692
  ///////////////////////////////////////////////////////////////////////////////
2653
2693
 
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
2694
+ /** Read save data from local storage
2695
+ * @param {string} saveName - unique name for the game/save
2696
+ * @param {Object} [defaultSaveData] - default values for save
2697
+ * @return {Object}
2698
+ * @memberof Utilities */
2699
+ function readSaveData(saveName, defaultSaveData)
2665
2700
  {
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'; }
2701
+ ASSERT(isString(saveName), 'loadData requires saveName string');
2702
+
2703
+ // replace undefined values with defaults
2704
+ const data = localStorage[saveName];
2705
+ const loadedData = data ? JSON.parse(data) : {};
2706
+ return { ...defaultSaveData, ...loadedData };
2707
+ }
2730
2708
 
2731
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2732
- * @return {number} */
2733
- valueOf() { return this.get(); }
2709
+ /** Write save data to local storage
2710
+ * @param {string} saveName - unique name for the game/save
2711
+ * @param {Object} saveData - object containing data to be saved
2712
+ * @memberof Utilities */
2713
+ function writeSaveData(saveName, saveData)
2714
+ {
2715
+ ASSERT(isString(saveName), 'saveData requires saveName string');
2716
+ localStorage[saveName] = JSON.stringify(saveData);
2734
2717
  }
2735
2718
  /**
2736
2719
  * LittleJS Engine Settings
@@ -3391,17 +3374,19 @@ class EngineObject
3391
3374
  this.additiveColor = undefined;
3392
3375
  /** @property {boolean} - Should it flip along y axis when rendered */
3393
3376
  this.mirror = false;
3377
+ /** @property {boolean} - Has object been destroyed? */
3378
+ this.destroyed = false;
3394
3379
 
3395
3380
  // physical properties
3396
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
3381
+ /** @property {number} - How heavy the object is, static if 0 */
3397
3382
  this.mass = objectDefaultMass;
3398
- /** @property {number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
3383
+ /** @property {number} - How much to slow down velocity each frame (0-1) */
3399
3384
  this.damping = objectDefaultDamping;
3400
- /** @property {number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
3385
+ /** @property {number} - How much to slow down rotation each frame (0-1) */
3401
3386
  this.angleDamping = objectDefaultAngleDamping;
3402
- /** @property {number} [restitution=objectDefaultRestitution] - How bouncy the object is when colliding (0-1) */
3387
+ /** @property {number} - How bouncy the object is when colliding (0-1) */
3403
3388
  this.restitution = objectDefaultRestitution;
3404
- /** @property {number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
3389
+ /** @property {number} - How much friction to apply when sliding (0-1) */
3405
3390
  this.friction = objectDefaultFriction;
3406
3391
  /** @property {number} - How much to scale gravity by for this object */
3407
3392
  this.gravityScale = 1;
@@ -3610,10 +3595,10 @@ class EngineObject
3610
3595
  if (!tileCollisionTest(oldPos, this.size, this))
3611
3596
  {
3612
3597
  // 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)
3598
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
3599
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3600
+ const restitution = max(this.restitution, hitLayer.restitution);
3601
+ if (isBlockedX)
3617
3602
  {
3618
3603
  // try to move up a tiny bit
3619
3604
  const epsilon = 1e-3;
@@ -3629,16 +3614,12 @@ class EngineObject
3629
3614
  return;
3630
3615
  }
3631
3616
 
3632
- // move to previous position and bounce
3617
+ // move to previous X position and bounce
3633
3618
  this.pos.x = oldPos.x;
3634
- this.velocity.x *= -this.restitution;
3619
+ this.velocity.x *= -restitution;
3635
3620
  }
3636
- if (blockedLayerY || !blockedLayerX)
3621
+ if (isBlockedY || !isBlockedX)
3637
3622
  {
3638
- // bounce velocity
3639
- const restitution = max(this.restitution, hitLayer.restitution);
3640
- this.velocity.y *= -restitution;
3641
-
3642
3623
  if (wasFalling)
3643
3624
  {
3644
3625
  // adjust position to slightly away from nearest tile
@@ -3654,10 +3635,12 @@ class EngineObject
3654
3635
  }
3655
3636
  else
3656
3637
  {
3657
- // move to previous position
3638
+ // move to previous Y position
3658
3639
  this.pos.y = oldPos.y;
3659
3640
  this.groundObject = undefined;
3660
3641
  }
3642
+ // bounce velocity
3643
+ this.velocity.y *= -restitution;
3661
3644
  }
3662
3645
  debugPhysics && debugRect(this.pos, this.size, '#f00');
3663
3646
  }
@@ -3675,19 +3658,19 @@ class EngineObject
3675
3658
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3676
3659
  }
3677
3660
 
3678
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
3679
- destroy()
3661
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal
3662
+ * @param {boolean} [immediate] - should attached effects be allowed to die off? */
3663
+ destroy(immediate=false)
3680
3664
  {
3681
- if (this.destroyed)
3682
- return;
3665
+ if (this.destroyed) return;
3683
3666
 
3684
3667
  // disconnect from parent and destroy children
3685
- this.destroyed = 1;
3668
+ this.destroyed = true;
3686
3669
  this.parent?.removeChild(this);
3687
3670
  for (const child of this.children)
3688
3671
  {
3689
3672
  child.parent = undefined;
3690
- child.destroy();
3673
+ child.destroy(immediate);
3691
3674
  }
3692
3675
  }
3693
3676
 
@@ -3786,7 +3769,7 @@ class EngineObject
3786
3769
  }
3787
3770
 
3788
3771
  /** Check if overlapping another engine object
3789
- * Collisions are resoloved to prevent overlaps
3772
+ * Collisions are resolved to prevent overlaps
3790
3773
  * @param {EngineObject} object
3791
3774
  * @return {boolean} */
3792
3775
  isOverlappingObject(object)
@@ -4038,7 +4021,7 @@ class TileInfo
4038
4021
  }
4039
4022
 
4040
4023
  /**
4041
- * Returns a tile info for an index using this tile as refrence
4024
+ * Returns a tile info for an index using this tile as reference
4042
4025
  * @param {Vector2|number} [index=0]
4043
4026
  * @return {TileInfo}
4044
4027
  */
@@ -4577,6 +4560,31 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
4577
4560
  ///////////////////////////////////////////////////////////////////////////////
4578
4561
  // Drawing utilities
4579
4562
 
4563
+ /** Load a texture at a specific index
4564
+ * @param {number} textureIndex - Index to store the texture at
4565
+ * @param {string} [src] - Image source path
4566
+ * @return {Promise} Promise that resolves when texture is loaded
4567
+ * @memberof Draw */
4568
+ async function loadTexture(textureIndex, src)
4569
+ {
4570
+ ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
4571
+ ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
4572
+ ASSERT(!src || isString(src), 'image src must be a string');
4573
+
4574
+ const image = new Image;
4575
+ if (src)
4576
+ {
4577
+ await new Promise(resolve =>
4578
+ {
4579
+ image.onerror = image.onload = resolve;
4580
+ image.crossOrigin = 'anonymous';
4581
+ image.src = src;
4582
+ });
4583
+ }
4584
+
4585
+ textureInfos[textureIndex] = new TextureInfo(image);
4586
+ }
4587
+
4580
4588
  /** Convert from screen to world space coordinates
4581
4589
  * @param {Vector2} screenPos
4582
4590
  * @return {Vector2}
@@ -4868,7 +4876,7 @@ let engineFontImage;
4868
4876
  class FontImage
4869
4877
  {
4870
4878
  /** Create an image font
4871
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
4879
+ * @param {TileInfo} tileInfo - Tile info of first character in font
4872
4880
  */
4873
4881
  constructor(tileInfo)
4874
4882
  {
@@ -4957,23 +4965,20 @@ class FontImage
4957
4965
  }
4958
4966
 
4959
4967
  // load engine font, called automatically on startup
4960
- function fontImageInit()
4968
+ async function fontImageInit()
4961
4969
  {
4962
- return new Promise(resolve =>
4970
+ const image = new Image;
4971
+ await new Promise(resolve =>
4963
4972
  {
4964
- // create the engine font
4965
- const image = new Image;
4966
- image.onerror = image.onload = ()=>
4967
- {
4968
- const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4969
- const textureInfo = new TextureInfo(image);
4970
- const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4971
- engineFontImage = new FontImage(tileInfo);
4972
- resolve();
4973
- }
4973
+ image.onerror = image.onload = resolve;
4974
4974
  image.crossOrigin = 'anonymous';
4975
4975
  image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg==';
4976
4976
  });
4977
+
4978
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
4979
+ const textureInfo = new TextureInfo(image);
4980
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
4981
+ engineFontImage = new FontImage(tileInfo);
4977
4982
  }
4978
4983
  /**
4979
4984
  * LittleJS Input System
@@ -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));
@@ -5956,7 +5961,7 @@ class Sound
5956
5961
 
5957
5962
  /** Loads a sound from a URL and decodes it into sample data.
5958
5963
  * @param {string} filename
5959
- * @return {Promise<void>} */
5964
+ * @return {Promise} */
5960
5965
  async loadSound(filename)
5961
5966
  {
5962
5967
  const response = await fetch(filename);
@@ -6014,7 +6019,7 @@ class Sound
6014
6019
  * // Control the individual instance
6015
6020
  * instance.setVolume(.5);
6016
6021
  * instance.pause();
6017
- * instance.unpause();
6022
+ * instance.resume();
6018
6023
  * instance.stop();
6019
6024
  */
6020
6025
  class SoundInstance
@@ -6128,7 +6133,7 @@ class SoundInstance
6128
6133
  this.startTime = undefined;
6129
6134
  }
6130
6135
 
6131
- /** Unpauses this sound instance */
6136
+ /** Resume this sound instance */
6132
6137
  resume()
6133
6138
  {
6134
6139
  if (!this.isPaused()) return;
@@ -6142,8 +6147,8 @@ class SoundInstance
6142
6147
  */
6143
6148
  isPlaying() { return !!this.source; }
6144
6149
 
6145
- /** Check if this instance is paused and was not stopped
6146
- * @return {boolean} - True if paused
6150
+ /** Check if this instance is paused or stopped (not currently playing)
6151
+ * @return {boolean} - True if not playing
6147
6152
  */
6148
6153
  isPaused() { return !this.isPlaying(); }
6149
6154
 
@@ -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 function 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 function 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)
7454
7544
  {
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;
7545
+ const s = objectMaxSpeed / length2**.5;
7546
+ this.velocity.x *= s;
7547
+ this.velocity.y *= s;
7461
7548
  }
7549
+
7550
+ // check collision against tiles
7551
+ this.groundObject = undefined;
7552
+ const testCollision = collideCallback ? (pos)=>
7553
+ {
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
+ }
7592
+ }
7593
+ }
7594
+
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);
7462
7604
  }
7463
7605
 
7464
- /** Render the particle, automatically called each frame, sorted by renderOrder */
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 = clamp(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
  */
@@ -10352,12 +10511,11 @@ class UIVideo extends UIObject
10352
10511
 
10353
10512
  /** Play or resume the video
10354
10513
  * @return {Promise} Promise that resolves when playback starts */
10355
- play()
10514
+ async play()
10356
10515
  {
10357
10516
  // try to play the video, catch any errors (autoplay may be blocked)
10358
- const promise = this.video.play();
10359
- promise?.catch(()=>{});
10360
- return promise;
10517
+ try { await this.video.play(); }
10518
+ catch(e) {}
10361
10519
  }
10362
10520
 
10363
10521
  /** Pause the video */
@@ -11060,11 +11218,11 @@ class Box2dStaticObject extends Box2dObject
11060
11218
 
11061
11219
  ///////////////////////////////////////////////////////////////////////////////
11062
11220
  /**
11063
- * Box2D Kiematic Object - Box2d with a kinematic physics body
11221
+ * Box2D Kinematic Object - Box2d with a kinematic physics body
11064
11222
  * @extends Box2dObject
11065
11223
  * @memberof Box2D
11066
11224
  */
11067
- class Box2dKiematicObject extends Box2dObject
11225
+ class Box2dKinematicObject extends Box2dObject
11068
11226
  {
11069
11227
  /** Create a LittleJS object with Box2d physics
11070
11228
  * @param {Vector2} [pos]
@@ -11116,20 +11274,19 @@ class Box2dTileLayer extends Box2dStaticObject
11116
11274
  this.destroyAllFixtures();
11117
11275
 
11118
11276
  // create box2d object for this layer
11119
- const size = this.tileLayer.size;
11120
11277
  this.pos = this.tileLayer.pos.copy();
11121
- this.size = size.copy();
11278
+ this.size = this.tileLayer.size.copy();
11122
11279
 
11123
11280
  // track which tiles have been processed
11124
11281
  const processed = [];
11125
- const getIndex = (x, y)=> x + y * size.x;
11282
+ const getIndex = (x, y)=> x + y * this.size.x;
11126
11283
  const isSolidUnprocessed = (x, y)=>
11127
11284
  !processed[getIndex(x, y)] &&
11128
11285
  this.tileLayer.getCollisionData(vec2(x, y)) > 0;
11129
11286
 
11130
11287
  // combine tiles into larger boxes
11131
- for (let x = 0; x < size.x; ++x)
11132
- for (let y = 0; y < size.y; ++y)
11288
+ for (let x = 0; x < this.size.x; ++x)
11289
+ for (let y = 0; y < this.size.y; ++y)
11133
11290
  {
11134
11291
  if (!isSolidUnprocessed(x, y)) continue;
11135
11292
 
@@ -12785,6 +12942,8 @@ export
12785
12942
  saveCanvas,
12786
12943
  saveDataURL,
12787
12944
  shareURL,
12945
+ readSaveData,
12946
+ writeSaveData,
12788
12947
 
12789
12948
  // Random
12790
12949
  rand,
@@ -12994,7 +13153,8 @@ export
12994
13153
  Box2dPlugin,
12995
13154
  Box2dObject,
12996
13155
  Box2dStaticObject,
12997
- Box2dKiematicObject,
13156
+ Box2dKinematicObject,
13157
+ Box2dTileLayer,
12998
13158
  Box2dRaycastResult,
12999
13159
  Box2dJoint,
13000
13160
  Box2dTargetJoint,