littlejsengine 1.17.1 → 1.17.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +10 -10
  2. package/dist/littlejs.d.ts +254 -171
  3. package/dist/littlejs.esm.js +892 -628
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +889 -626
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +885 -622
  8. package/examples/box2d/gameObjects.js +3 -3
  9. package/examples/electron/index.html +2 -2
  10. package/examples/index.html +10 -9
  11. package/examples/logo.png +0 -0
  12. package/examples/logo2.png +0 -0
  13. package/examples/module/build.bat +7 -0
  14. package/examples/module/build.js +121 -0
  15. package/examples/particles/index.html +1 -1
  16. package/examples/platformer/game.js +1 -1
  17. package/examples/platformer/gameEffects.js +5 -10
  18. package/examples/platformer/gameLevel.js +3 -3
  19. package/examples/puzzle/game.js +2 -2
  20. package/examples/shorts/base.html +4 -1
  21. package/examples/shorts/box2dPool.js +2 -2
  22. package/examples/shorts/box2dTileLayer.js +48 -0
  23. package/examples/shorts/colors.js +1 -1
  24. package/examples/shorts/fps.js +1 -1
  25. package/examples/shorts/music.js +5 -4
  26. package/examples/shorts/musicPlayer.js +4 -3
  27. package/examples/shorts/parallax.js +2 -2
  28. package/examples/shorts/tiltedView.js +1 -1
  29. package/examples/starter/index.html +2 -2
  30. package/package.json +5 -5
  31. package/plugins/box2d.js +166 -63
  32. package/plugins/pluginExport.js +1 -1
  33. package/plugins/postProcess.js +1 -1
  34. package/plugins/uiSystem.js +33 -20
  35. package/plugins/zzfxm.js +2 -2
  36. package/reference.md +9 -8
  37. package/src/engine.js +111 -134
  38. package/src/engineAudio.js +44 -58
  39. package/src/engineDebug.js +4 -4
  40. package/src/engineDraw.js +7 -4
  41. package/src/engineExport.js +2 -1
  42. package/src/engineInput.js +5 -5
  43. package/src/engineMath.js +9 -9
  44. package/src/engineObject.js +25 -25
  45. package/src/engineParticles.js +241 -118
  46. package/src/engineTileLayer.js +117 -88
  47. package/src/engineUtilities.js +105 -78
  48. package/src/engineWebGL.js +2 -2
  49. package/plugins/Box2D_License.txt +0 -17
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
33
33
  * @type {string}
34
34
  * @default
35
35
  * @memberof Engine */
36
- const engineVersion = '1.17.1';
36
+ const engineVersion = '1.17.10';
37
37
 
38
38
  /** Frames per second to update
39
39
  * @type {number}
@@ -88,8 +88,9 @@ function getPaused() { return paused; }
88
88
  * @memberof Engine */
89
89
  function setPaused(isPaused=true) { paused = isPaused; }
90
90
 
91
- // Frame time tracking
91
+ // Engine internal variables
92
92
  let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
93
+ let showEngineVersion = true;
93
94
 
94
95
  ///////////////////////////////////////////////////////////////////////////////
95
96
  // plugin hooks
@@ -153,16 +154,17 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
153
154
  * @example
154
155
  * // Basic engine startup
155
156
  * engineInit(
156
- * () => { LOG('Game initialized!'); }, // gameInit
157
- * () => { updateGameLogic(); }, // gameUpdate
158
- * () => { updateUI(); }, // gameUpdatePost
159
- * () => { drawBackground(); }, // gameRender
160
- * () => { drawHUD(); }, // gameRenderPost
157
+ * ()=> { LOG('Game initialized!'); }, // gameInit
158
+ * ()=> { updateGameLogic(); }, // gameUpdate
159
+ * ()=> { updateUI(); }, // gameUpdatePost
160
+ * ()=> { drawBackground(); }, // gameRender
161
+ * ()=> { drawHUD(); }, // gameRenderPost
161
162
  * ['tiles.png', 'tilesLevel.png'] // images to load
162
163
  * );
163
164
  * @memberof Engine */
164
165
  async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
165
166
  {
167
+ showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
166
168
  ASSERT(!mainContext, 'engine already initialized');
167
169
  ASSERT(isArray(imageSources), 'pass in images as array');
168
170
 
@@ -431,7 +433,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
431
433
  promises.push(new Promise(resolve =>
432
434
  {
433
435
  let t = 0;
434
- console.log(`${engineName} Engine v${engineVersion}`);
435
436
  updateSplash();
436
437
  function updateSplash()
437
438
  {
@@ -463,13 +464,13 @@ function engineObjectsUpdate()
463
464
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
464
465
 
465
466
  // recursive object update
466
- function updateObject(o)
467
+ function updateChildObject(o)
467
468
  {
468
469
  if (o.destroyed) return;
469
470
 
470
471
  o.update();
471
472
  for (const child of o.children)
472
- updateObject(child);
473
+ updateChildObject(child);
473
474
  }
474
475
  for (const o of engineObjects)
475
476
  {
@@ -479,7 +480,7 @@ function engineObjectsUpdate()
479
480
  o.update();
480
481
  o.updatePhysics();
481
482
  for (const child of o.children)
482
- updateObject(child);
483
+ updateChildObject(child);
483
484
  o.updateTransforms();
484
485
  }
485
486
 
@@ -488,11 +489,14 @@ function engineObjectsUpdate()
488
489
  }
489
490
 
490
491
  /** Destroy and remove all objects
492
+ * - This can be used to clear out all objects when restarting a level
493
+ * - Objects can override their destroy function to do cleanup or stick around
494
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
491
495
  * @memberof Engine */
492
- function engineObjectsDestroy()
496
+ function engineObjectsDestroy(immediate=true)
493
497
  {
494
498
  for (const o of engineObjects)
495
- o.parent || o.destroy();
499
+ o.parent || o.destroy(immediate);
496
500
  engineObjects = engineObjects.filter(o=>!o.destroyed);
497
501
  }
498
502
 
@@ -567,165 +571,140 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
567
571
  ///////////////////////////////////////////////////////////////////////////////
568
572
  function drawEngineLogo(t)
569
573
  {
574
+ const blackAndWhite = 0;
575
+ const showName = 1;
576
+
570
577
  // LittleJS Logo and Splash Screen
571
578
  const x = mainContext;
572
579
  const w = mainCanvas.width = innerWidth;
573
580
  const h = mainCanvas.height = innerHeight;
574
-
575
581
  {
576
582
  // background
577
583
  const p3 = percent(t, 1, .8);
578
584
  const p4 = percent(t, 0, .5);
579
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
585
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
580
586
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
581
587
  g.addColorStop(1,hsl(0,0,0,p3).toString());
582
588
  x.save();
583
589
  x.fillStyle = g;
584
590
  x.fillRect(0,0,w,h);
585
591
  }
586
-
587
- // draw LittleJS logo...
588
- const rect = (X, Y, W, H, C)=>
592
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
593
+ {
594
+ if (C >= 0)
595
+ {
596
+ if (blackAndWhite)
597
+ x.fillStyle = '#fff';
598
+ else
599
+ {
600
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
601
+ g.addColorStop(0,color(C,2));
602
+ g.addColorStop(1,color(C,1));
603
+ }
604
+ }
605
+ else
606
+ x.fillStyle = '#000';
607
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
608
+ }
609
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
589
610
  {
590
611
  x.beginPath();
591
- x.rect(X,Y,W,C?H*p:H);
592
- x.fillStyle = C;
593
- C ? x.fill() : x.stroke();
594
- };
595
- const line = (X, Y, Z, W)=>
612
+ x.arc(X,Y,R,p*A,p*B);
613
+ gradient(X,Y-R,X,Y+R,C,S);
614
+ }
615
+ const rect = (X,Y,W,H,C)=>
596
616
  {
597
617
  x.beginPath();
598
- x.lineTo(X,Y);
599
- x.lineTo(Z,W);
600
- x.stroke();
601
- };
602
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
618
+ x.rect(X,Y,W,H*p);
619
+ gradient(X,Y+H,X+W,Y,C);
620
+ }
621
+ const poly = (points,C,Y,H)=>
603
622
  {
604
- const D = (A+B)/2, E = p*(B-A)/2;
605
623
  x.beginPath();
606
- F && x.lineTo(X,Y);
607
- x.arc(X,Y,R,D-E,D+E);
608
- x.fillStyle = C;
609
- C ? x.fill() : x.stroke();
610
- };
611
- const color = (c=0, l=0) =>
612
- hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
624
+ for (const p of points)
625
+ x.lineTo(p.x, p.y);
626
+ x.closePath();
627
+ gradient(0, Y, 0, Y+H,C);
628
+ }
629
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
630
+
631
+ // center and fit tos screen
613
632
  const alpha = wave(1,1,t);
614
633
  const p = percent(alpha, .1, .5);
615
-
616
- // setup
634
+ const size = min(6, min(w,h)/99);
617
635
  x.translate(w/2,h/2);
618
- const size = min(6, min(w,h)/99); // fit to screen
619
636
  x.scale(size,size);
620
637
  x.translate(-40,-35);
638
+ p < 1 && x.setLineDash([99*p,99]);
621
639
  x.lineJoin = x.lineCap = 'round';
622
640
  x.lineWidth = .1 + p*1.9;
641
+ //x.strokeStyle='#fff7';
642
+
643
+ if (showName)
644
+ {
645
+ // engine name text
646
+ const Y = 54;
647
+ const s = 'LittleJS';
648
+ x.font = '900 15.5px arial';
649
+ x.lineWidth = .1+p*3.9;
650
+ x.textAlign = 'center';
651
+ x.textBaseline = 'top';
652
+ rect(11,Y+2,59,8*p,-1);
653
+ x.beginPath();
623
654
 
624
- // drawing effect
625
- const p2 = percent(alpha,.1,1);
626
- x.setLineDash([99*p2,99]);
627
-
628
- // cab top
629
- rect(7,16,18,-8,color(2,2));
630
- rect(7,8,18,4,color(2,3));
631
- rect(25,8,8,8,color(2,1));
632
- rect(25,8,-18,8);
633
- rect(25,8,8,8);
634
-
635
- // cab
636
- rect(25,16,7,24,color());
637
- rect(11,39,14,-23,color(1,1));
638
- rect(11,16,14,18,color(1,2));
639
- rect(11,16,14,8,color(1,3));
640
- rect(25,16,-14,24);
641
-
642
- // cab window
643
- rect(15,29,6,-9,color(2,2));
644
- circle(15,21,5,0,PI/2,color(2,4),1);
645
- rect(21,21,-6,9);
646
-
647
- // little stack
648
- rect(37,14,9,6,color(3,2));
649
- rect(37,14,4.5,6,color(3,3));
650
- rect(37,14,9,6);
655
+ let w2 = 0;
656
+ for (let i=0;i<s.length;++i)
657
+ w2 += x.measureText(s[i]).width;
658
+ for (let j=2;j--;)
659
+ for (let i=0,X=40-w2/2;i<s.length;++i)
660
+ {
661
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
662
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
663
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
664
+ X += w;
665
+ }
666
+
667
+ x.lineWidth = .1 + p*1.9;
668
+ rect(3,Y,73,0); // bottom
669
+ }
670
+
671
+ rect(7,15,26,-7,0); // cab top
672
+ rect(25,15,8,25,-1); // cab front
673
+ rect(10,40,15,-25,1); // cab back
674
+ rect(14,21,7,9,2); // cab window
675
+ rect(38,15,6,6,2); // little stack
651
676
 
652
677
  // big stack
653
- rect(50,20,10,-8,color(0,1));
654
- rect(50,20,6.5,-8,color(0,2));
655
- rect(50,20,3.5,-8,color(0,3));
656
- rect(50,20,10,-8);
657
- circle(55,2,11.4,.5,PI-.5,color(3,3));
658
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
659
- circle(55,2,11.4,.5,PI-.5);
660
- rect(45,7,20,-7,color(0,2));
661
- rect(45,-1,20,4,color(0,3));
662
- rect(45,-1,20,8);
678
+ rect(49,20,10,-6,0);
679
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
680
+ poly(stackPoints,2,8,6*p);
681
+ rect(44,8,20,-7,0);
663
682
 
664
683
  // engine
665
- for (let i=5; i--;)
666
- {
667
- // stagger radius to fix slight seam
668
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
669
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
670
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
671
- }
684
+ for (let i=5;i--;) circle(59-i*6,30,10,0,9,1,0);
685
+ circle(59,30,4,0,9,2); // light
672
686
 
673
687
  // engine outline
674
- circle(36,30,10,PI/2,PI*3/2);
675
- circle(48,30,10,PI/2,PI*3/2);
676
- circle(60,30,10);
677
- line(36,20,60,20);
678
-
679
- // engine front light
680
- circle(60,30,4,PI,3*PI,color(3,2));
681
- circle(60,30,4,PI,2*PI,color(3,3));
682
- circle(60,30,4,PI,3*PI);
688
+ rect(35,20,24,0); // top
689
+ circle(59,30,10); // front
690
+ circle(47,30,10,PI/2,PI*3/2); // middle
691
+ circle(35,30,10,PI/2,PI*3/2); // back
692
+ rect(7,40,13,7,-1); // bottom back
693
+ rect(17,40,43,14,-1); // bottom center
683
694
 
684
- // front brush
685
- for (let i=6; i--;)
695
+ // wheels
696
+ for (let i=3;i--;)
697
+ for (let j=2;j--;)
698
+ circle(15*i+17,47,j?7:1,PI,3*PI,2);
699
+
700
+ // cowcatcher
701
+ for (let i=2;i--;)
686
702
  {
687
- x.beginPath();
688
- x.lineTo(53,54);
689
- x.lineTo(53,40);
690
- x.lineTo(53+(1+i*2.9)*p,40);
691
- x.lineTo(53+(4+i*3.5)*p,54);
692
- x.fillStyle = color(0,i%2+2);
693
- x.fill();
694
- i%2 && x.stroke();
703
+ let w=6, s=7, o=53+w*p*i
704
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
705
+ poly(points,0,40,14);
695
706
  }
696
707
 
697
- // wheels
698
- rect(6,40,5,5);
699
- rect(6,40,5,5,color());
700
- rect(15,54,38,-14,color());
701
- for (let i=3; i--;)
702
- for (let j=2; j--;)
703
- {
704
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
705
- x.stroke();
706
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
707
- x.stroke();
708
- }
709
- line(6,40,68,40); // center
710
- line(77,54,4,54); // bottom
711
-
712
- // draw engine name
713
- const s = engineName;
714
- x.font = '900 16px arial';
715
- x.textAlign = 'center';
716
- x.textBaseline = 'top';
717
- x.lineWidth = .1+p*3.9;
718
- let w2 = 0;
719
- for (let i=0; i<s.length; ++i)
720
- w2 += x.measureText(s[i]).width;
721
- for (let j=2; j--;)
722
- for (let i=0, X=41-w2/2; i<s.length; ++i)
723
- {
724
- x.fillStyle = color(i,2);
725
- const w = x.measureText(s[i]).width;
726
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
727
- X += w;
728
- }
729
708
  x.restore();
730
709
  }
731
710
  /**
@@ -777,7 +756,7 @@ let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParti
777
756
  /** Asserts if the expression is false, does nothing in release builds
778
757
  * Halts execution if the assert fails and throws an error
779
758
  * @param {boolean} assert
780
- * @param {...Object} [output] - error message output
759
+ * @param {...Object} output - error message output
781
760
  * @memberof Debug */
782
761
  function ASSERT(assert, ...output)
783
762
  {
@@ -787,7 +766,7 @@ function ASSERT(assert, ...output)
787
766
  }
788
767
 
789
768
  /** Log to console if debug is enabled, does nothing in release builds
790
- * @param {...Object} [output] - message output
769
+ * @param {...Object} output - message output
791
770
  * @memberof Debug */
792
771
  function LOG(...output) { console.log(...output); }
793
772
 
@@ -1434,8 +1413,8 @@ function debugProtectConstant(obj)
1434
1413
  props.forEach(prop =>
1435
1414
  {
1436
1415
  Object.defineProperty(obj, prop, {
1437
- get: () => values[prop],
1438
- set: (value) =>
1416
+ get: ()=> values[prop],
1417
+ set: (value)=>
1439
1418
  {
1440
1419
  ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
1441
1420
  },
@@ -1463,25 +1442,25 @@ function debugProtectConstant(obj)
1463
1442
  const PI = Math.PI;
1464
1443
 
1465
1444
  /** Returns absolute value of value passed in
1466
- * @param {number} value
1445
+ * @param {number} x
1467
1446
  * @return {number}
1468
1447
  * @memberof Math */
1469
1448
  const abs = Math.abs;
1470
1449
 
1471
1450
  /** Returns floored value of value passed in
1472
- * @param {number} value
1451
+ * @param {number} x
1473
1452
  * @return {number}
1474
1453
  * @memberof Math */
1475
1454
  const floor = Math.floor;
1476
1455
 
1477
1456
  /** Returns ceiled value of value passed in
1478
- * @param {number} value
1457
+ * @param {number} x
1479
1458
  * @return {number}
1480
1459
  * @memberof Math */
1481
1460
  const ceil = Math.ceil;
1482
1461
 
1483
1462
  /** Returns rounded value passed in
1484
- * @param {number} value
1463
+ * @param {number} x
1485
1464
  * @return {number}
1486
1465
  * @memberof Math */
1487
1466
  const round = Math.round;
@@ -1499,7 +1478,7 @@ const min = Math.min;
1499
1478
  const max = Math.max;
1500
1479
 
1501
1480
  /** Returns the sign of value passed in
1502
- * @param {number} value
1481
+ * @param {number} x
1503
1482
  * @return {number}
1504
1483
  * @memberof Math */
1505
1484
  const sign = Math.sign;
@@ -1511,25 +1490,25 @@ const sign = Math.sign;
1511
1490
  const hypot = Math.hypot;
1512
1491
 
1513
1492
  /** Returns log2 of value passed in
1514
- * @param {number} value
1493
+ * @param {number} x
1515
1494
  * @return {number}
1516
1495
  * @memberof Math */
1517
1496
  const log2 = Math.log2;
1518
1497
 
1519
1498
  /** Returns sin of value passed in
1520
- * @param {number} value
1499
+ * @param {number} x
1521
1500
  * @return {number}
1522
1501
  * @memberof Math */
1523
1502
  const sin = Math.sin;
1524
1503
 
1525
1504
  /** Returns cos of value passed in
1526
- * @param {number} value
1505
+ * @param {number} x
1527
1506
  * @return {number}
1528
1507
  * @memberof Math */
1529
1508
  const cos = Math.cos;
1530
1509
 
1531
1510
  /** Returns tan of value passed in
1532
- * @param {number} value
1511
+ * @param {number} x
1533
1512
  * @return {number}
1534
1513
  * @memberof Math */
1535
1514
  const tan = Math.tan;
@@ -2564,6 +2543,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
2564
2543
  * @namespace Utilities
2565
2544
  */
2566
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
+
2567
2630
  /** Formats seconds to mm:ss style for display purposes
2568
2631
  * @param {number} t - time in seconds
2569
2632
  * @return {string}
@@ -2650,86 +2713,29 @@ function shareURL(title, url, callback)
2650
2713
 
2651
2714
  ///////////////////////////////////////////////////////////////////////////////
2652
2715
 
2653
- /**
2654
- * Timer object tracks how long has passed since it was set
2655
- * @memberof Engine
2656
- * @example
2657
- * let a = new Timer; // creates a timer that is not set
2658
- * a.set(3); // sets the timer to 3 seconds
2659
- *
2660
- * let b = new Timer(1); // creates a timer with 1 second left
2661
- * b.unset(); // unset the timer
2662
- */
2663
- 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)
2664
2722
  {
2665
- /** Create a timer object set time passed in
2666
- * @param {number} [timeLeft] - How much time left before the timer
2667
- * @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
2668
- constructor(timeLeft, useRealTime=false)
2669
- {
2670
- ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
2671
- this.useRealTime = useRealTime;
2672
- const globalTime = this.getGlobalTime();
2673
- this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
2674
- this.setTime = timeLeft;
2675
- }
2676
-
2677
- /** Set the timer with seconds passed in
2678
- * @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
2679
- set(timeLeft=0)
2680
- {
2681
- ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
2682
- const globalTime = this.getGlobalTime();
2683
- this.time = globalTime + timeLeft;
2684
- this.setTime = timeLeft;
2685
- }
2686
-
2687
- /** Set if the timer should keep running even when the game is paused
2688
- * @param {boolean} [useRealTime] */
2689
- setUseRealTime(useRealTime=true)
2690
- {
2691
- ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
2692
- this.useRealTime = useRealTime;
2693
- }
2694
-
2695
- /** Unset the timer */
2696
- unset() { this.time = undefined; }
2697
-
2698
- /** Returns true if set
2699
- * @return {boolean} */
2700
- isSet() { return this.time !== undefined; }
2701
-
2702
- /** Returns true if set and has not elapsed
2703
- * @return {boolean} */
2704
- active() { return this.getGlobalTime() < this.time; }
2705
-
2706
- /** Returns true if set and elapsed
2707
- * @return {boolean} */
2708
- elapsed() { return this.getGlobalTime() >= this.time; }
2709
-
2710
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2711
- * @return {number} */
2712
- get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
2713
-
2714
- /** Get percentage elapsed based on time it was set to, returns 0 if not set
2715
- * @return {number} */
2716
- getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
2717
-
2718
- /** Get the time this timer was set to, returns 0 if not set
2719
- * @return {number} */
2720
- getSetTime() { return this.isSet() ? this.setTime : 0; }
2721
-
2722
- /** Get the current global time this timer is based on
2723
- * @return {number} */
2724
- getGlobalTime() { return this.useRealTime ? timeReal : time; }
2725
-
2726
- /** Returns this timer expressed as a string
2727
- * @return {string} */
2728
- 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
+ }
2729
2730
 
2730
- /** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
2731
- * @return {number} */
2732
- 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);
2733
2739
  }
2734
2740
  /**
2735
2741
  * LittleJS Engine Settings
@@ -3390,17 +3396,19 @@ class EngineObject
3390
3396
  this.additiveColor = undefined;
3391
3397
  /** @property {boolean} - Should it flip along y axis when rendered */
3392
3398
  this.mirror = false;
3399
+ /** @property {boolean} - Has object been destroyed? */
3400
+ this.destroyed = false;
3393
3401
 
3394
3402
  // physical properties
3395
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
3403
+ /** @property {number} - How heavy the object is, static if 0 */
3396
3404
  this.mass = objectDefaultMass;
3397
- /** @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) */
3398
3406
  this.damping = objectDefaultDamping;
3399
- /** @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) */
3400
3408
  this.angleDamping = objectDefaultAngleDamping;
3401
- /** @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) */
3402
3410
  this.restitution = objectDefaultRestitution;
3403
- /** @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) */
3404
3412
  this.friction = objectDefaultFriction;
3405
3413
  /** @property {number} - How much to scale gravity by for this object */
3406
3414
  this.gravityScale = 1;
@@ -3601,7 +3609,7 @@ class EngineObject
3601
3609
  if (this.collideTiles)
3602
3610
  {
3603
3611
  // check collision against tiles
3604
- const hitLayer = tileCollisionTest(this.pos, this.size, this)
3612
+ const hitLayer = tileCollisionTest(this.pos, this.size, this);
3605
3613
  if (hitLayer)
3606
3614
  {
3607
3615
  // if already was stuck in collision, don't do anything
@@ -3609,10 +3617,10 @@ class EngineObject
3609
3617
  if (!tileCollisionTest(oldPos, this.size, this))
3610
3618
  {
3611
3619
  // test which side we bounced off (or both if a corner)
3612
- const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
3613
- const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
3614
-
3615
- 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)
3616
3624
  {
3617
3625
  // try to move up a tiny bit
3618
3626
  const epsilon = 1e-3;
@@ -3628,16 +3636,12 @@ class EngineObject
3628
3636
  return;
3629
3637
  }
3630
3638
 
3631
- // move to previous position and bounce
3639
+ // move to previous X position and bounce
3632
3640
  this.pos.x = oldPos.x;
3633
- this.velocity.x *= -this.restitution;
3641
+ this.velocity.x *= -restitution;
3634
3642
  }
3635
- if (blockedLayerY || !blockedLayerX)
3643
+ if (isBlockedY || !isBlockedX)
3636
3644
  {
3637
- // bounce velocity
3638
- const restitution = max(this.restitution, hitLayer.restitution);
3639
- this.velocity.y *= -restitution;
3640
-
3641
3645
  if (wasFalling)
3642
3646
  {
3643
3647
  // adjust position to slightly away from nearest tile
@@ -3653,10 +3657,12 @@ class EngineObject
3653
3657
  }
3654
3658
  else
3655
3659
  {
3656
- // move to previous position
3660
+ // move to previous Y position
3657
3661
  this.pos.y = oldPos.y;
3658
3662
  this.groundObject = undefined;
3659
3663
  }
3664
+ // bounce velocity
3665
+ this.velocity.y *= -restitution;
3660
3666
  }
3661
3667
  debugPhysics && debugRect(this.pos, this.size, '#f00');
3662
3668
  }
@@ -3674,19 +3680,19 @@ class EngineObject
3674
3680
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
3675
3681
  }
3676
3682
 
3677
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
3678
- 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)
3679
3686
  {
3680
- if (this.destroyed)
3681
- return;
3687
+ if (this.destroyed) return;
3682
3688
 
3683
3689
  // disconnect from parent and destroy children
3684
- this.destroyed = 1;
3690
+ this.destroyed = true;
3685
3691
  this.parent?.removeChild(this);
3686
3692
  for (const child of this.children)
3687
3693
  {
3688
3694
  child.parent = undefined;
3689
- child.destroy();
3695
+ child.destroy(immediate);
3690
3696
  }
3691
3697
  }
3692
3698
 
@@ -3785,7 +3791,7 @@ class EngineObject
3785
3791
  }
3786
3792
 
3787
3793
  /** Check if overlapping another engine object
3788
- * Collisions are resoloved to prevent overlaps
3794
+ * Collisions are resolved to prevent overlaps
3789
3795
  * @param {EngineObject} object
3790
3796
  * @return {boolean} */
3791
3797
  isOverlappingObject(object)
@@ -4037,7 +4043,7 @@ class TileInfo
4037
4043
  }
4038
4044
 
4039
4045
  /**
4040
- * 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
4041
4047
  * @param {Vector2|number} [index=0]
4042
4048
  * @return {TileInfo}
4043
4049
  */
@@ -4105,7 +4111,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
4105
4111
  ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
4106
4112
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4107
4113
 
4108
- const textureInfo = tileInfo && tileInfo.textureInfo;
4114
+ const textureInfo = tileInfo?.textureInfo;
4109
4115
  const bleed = tileInfo?.bleed ?? 0;
4110
4116
  if (useWebGL && glEnable)
4111
4117
  {
@@ -4411,9 +4417,12 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
4411
4417
  ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
4412
4418
  ASSERT(isNumber(angle), 'angle must be a number');
4413
4419
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
4414
- ASSERT(lineWidth >= 0 && lineWidth < size.x && lineWidth < size.y, 'invalid lineWidth');
4420
+ ASSERT(lineWidth >= 0, 'lineWidth must be a positive value or 0');
4415
4421
  ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
4416
4422
 
4423
+ // clamp line width to prevent artifacts
4424
+ lineWidth = clamp(lineWidth, 0, Math.min(size.x, size.y));
4425
+
4417
4426
  if (useWebGL && glEnable)
4418
4427
  {
4419
4428
  // draw as a regular polygon
@@ -4864,7 +4873,7 @@ let engineFontImage;
4864
4873
  class FontImage
4865
4874
  {
4866
4875
  /** Create an image font
4867
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
4876
+ * @param {TileInfo} tileInfo - Tile info of first character in font
4868
4877
  */
4869
4878
  constructor(tileInfo)
4870
4879
  {
@@ -5379,9 +5388,9 @@ function inputInit()
5379
5388
  function touchInputInit()
5380
5389
  {
5381
5390
  // add non passive touch event listeners
5382
- document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
5383
- document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
5384
- document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
5391
+ document.addEventListener('touchstart', (e)=> handleTouch(e), { passive: false });
5392
+ document.addEventListener('touchmove', (e)=> handleTouch(e), { passive: false });
5393
+ document.addEventListener('touchend', (e)=> handleTouch(e), { passive: false });
5385
5394
 
5386
5395
  // handle all touch events the same way
5387
5396
  let wasTouching;
@@ -5587,7 +5596,7 @@ function inputUpdate()
5587
5596
  // poll gamepads
5588
5597
  const maxGamepads = 8;
5589
5598
  const gamepads = navigator.getGamepads();
5590
- const gamepadCount = min(maxGamepads, gamepads.length)
5599
+ const gamepadCount = min(maxGamepads, gamepads.length);
5591
5600
  for (let i=0; i<gamepadCount; ++i)
5592
5601
  {
5593
5602
  // get or create gamepad data
@@ -5740,7 +5749,7 @@ function inputRender()
5740
5749
  }
5741
5750
  }
5742
5751
 
5743
- // center position for right tocuh pad face buttons
5752
+ // center position for right touch pad face buttons
5744
5753
  function touchGamepadButtonCenter()
5745
5754
  {
5746
5755
  const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
@@ -5792,29 +5801,46 @@ function audioInit()
5792
5801
  ///////////////////////////////////////////////////////////////////////////////
5793
5802
 
5794
5803
  /**
5795
- * Sound Object - Stores a sound for later use and can be played positionally
5804
+ * Sound Object - Stores a sound for later
5805
+ * - this can be used to load and play wave, mp3, and ogg files
5806
+ * - it can also create sounds using the ZzFX sound generator
5807
+ * - can attenuate and apply stereo panning to sounds
5808
+ * - sound instance control with pause/resume capability
5796
5809
  *
5797
5810
  * <a href=https://killedbyapixel.github.io/ZzFX/>Create sounds using the ZzFX Sound Designer.</a>
5798
5811
  * @memberof Audio
5799
5812
  * @example
5800
- * // create a sound
5813
+ * // load an audio asset file
5814
+ * const sound_example = new Sound('sound.mp3');
5815
+ *
5816
+ * // create a zzfx sound
5801
5817
  * const sound_example = new Sound([.5,.5]);
5802
5818
  *
5803
- * // play the sound
5819
+ * // play a sound
5804
5820
  * sound_example.play();
5805
5821
  */
5806
5822
  class Sound
5807
5823
  {
5808
- /** Create a sound object and cache the zzfx samples for later use
5809
- * @param {Array} zzfxSound - Array of zzfx parameters, ex. [.5,.5]
5824
+ /**
5825
+ * @callback SoundLoadCallback - Function called when sound is loaded
5826
+ * @param {Sound} sound
5827
+ * @memberof Audio
5828
+ */
5829
+
5830
+ /** Create a sound object and cache the audio for later use
5831
+ * @param {string|Array} [asset] - Filename of audio file or zzfx array
5832
+ * @param {number} [randomness] - How much to randomize frequency each time sound plays, for zzfx sounds the zzfx default is used if undefined
5810
5833
  * @param {number} [range=soundDefaultRange] - World space max range of sound
5811
5834
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5835
+ * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5812
5836
  */
5813
- constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
5837
+ constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
5814
5838
  {
5815
5839
  if (!soundEnable || headlessMode) return;
5816
5840
 
5817
- ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
5841
+ ASSERT(!asset || isArray(asset) || isString(asset), 'asset must be a file name or zzfx array');
5842
+ ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
5843
+ ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
5818
5844
  ASSERT(isNumber(range), 'range must be a number');
5819
5845
  ASSERT(isNumber(taper), 'taper must be a number');
5820
5846
 
@@ -5823,23 +5849,35 @@ class Sound
5823
5849
  /** @property {number} - At what percentage of range should it start tapering */
5824
5850
  this.taper = taper;
5825
5851
  /** @property {number} - How much to randomize frequency each time sound plays */
5826
- this.randomness = 0;
5852
+ this.randomness = randomness ?? 0;
5827
5853
  /** @property {number} - Sample rate for this sound */
5828
5854
  this.sampleRate = audioDefaultSampleRate;
5829
5855
  /** @property {number} - Percentage of this sound currently loaded */
5830
5856
  this.loadedPercent = 0;
5857
+ /** @property {SoundLoadCallback} - function to call when sound is loaded */
5858
+ this.onloadCallback = onloadCallback;
5831
5859
 
5832
- // generate zzfx sound now for fast playback
5833
- if (zzfxSound)
5860
+ if (Array.isArray(asset))
5834
5861
  {
5862
+ // generate zzfx sound
5863
+ const zzfxSound = asset;
5864
+
5835
5865
  // remove randomness so it can be applied on playback
5836
- const randomnessIndex = 1, defaultRandomness = .05;
5866
+ const defaultRandomness = randomness ?? .05;
5867
+ const randomnessIndex = 1;
5837
5868
  this.randomness = zzfxSound[randomnessIndex] ?? defaultRandomness;
5838
5869
  zzfxSound[randomnessIndex] = 0;
5839
5870
 
5840
5871
  // generate the zzfx samples
5841
5872
  this.sampleChannels = [zzfxG(...zzfxSound)];
5842
5873
  this.loadedPercent = 1;
5874
+ onloadCallback?.(this);
5875
+ }
5876
+ else if (typeof asset === 'string')
5877
+ {
5878
+ // load the audio file
5879
+ const filename = asset;
5880
+ this.loadSound(filename);
5843
5881
  }
5844
5882
  }
5845
5883
 
@@ -5891,7 +5929,7 @@ class Sound
5891
5929
  * @param {number} [volume] - Volume to play the music at
5892
5930
  * @param {boolean} [loop] - Should the music loop?
5893
5931
  * @param {boolean} [paused] - Should the music start paused
5894
- * @return {SoundInstance} - The audio source node
5932
+ * @return {SoundInstance} - The sound instance
5895
5933
  */
5896
5934
  playMusic(volume=1, loop=true, paused=false)
5897
5935
  { return this.play(undefined, volume, 1, 0, loop, paused); }
@@ -5901,70 +5939,27 @@ class Sound
5901
5939
  * @param {number} [semitoneOffset=0] - How many semitones to offset pitch
5902
5940
  * @param {Vector2} [pos] - World space position to play the sound if any
5903
5941
  * @param {number} [volume=1] - How much to scale volume by
5904
- * @return {SoundInstance} - The audio source node
5942
+ * @return {SoundInstance} - The sound instance
5905
5943
  */
5906
5944
  playNote(semitoneOffset=0, pos, volume)
5907
5945
  {
5908
- ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5909
- const pitch = getNoteFrequency(semitoneOffset, 1);
5910
- return this.play(pos, volume, pitch, 0);
5911
- }
5912
-
5913
- /** Get how long this sound is in seconds
5914
- * @return {number} - How long the sound is in seconds (undefined if loading)
5915
- */
5916
- getDuration()
5917
- { return this.sampleChannels?.[0].length / this.sampleRate || 0; }
5918
-
5919
- /** Check if sound is loaded, for sounds fetched from a url
5920
- * @return {boolean} - True if sound is loaded and ready to play
5921
- */
5922
- isLoaded() { return this.loadedPercent === 1; }
5923
- }
5924
-
5925
- ///////////////////////////////////////////////////////////////////////////////
5926
-
5927
- /**
5928
- * Sound Wave Object - Loads and stores an audio file for later use
5929
- * - this can be used to load and play wave, mp3, and ogg files
5930
- * @extends Sound
5931
- * @memberof Audio
5932
- * @example
5933
- * // load an audio asset file
5934
- * const sound_example = new SoundWave('sound.mp3');
5935
- *
5936
- * // play the sound
5937
- * sound_example.play();
5938
- */
5939
- class SoundWave extends Sound
5940
- {
5941
- /**
5942
- * @callback SoundLoadCallback - Function called when sound is loaded
5943
- * @param {SoundWave} sound
5944
- * @memberof Audio
5945
- */
5946
-
5947
- /** Create a sound object and cache the wave file for later use
5948
- * @param {string} filename - Filename of audio file to load
5949
- * @param {number} [randomness] - How much to randomize frequency each time sound plays
5950
- * @param {number} [range=soundDefaultRange] - World space max range of sound
5951
- * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
5952
- * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
5953
- */
5954
- constructor(filename, randomness=0, range, taper, onloadCallback)
5955
- {
5956
- super(undefined, range, taper);
5957
- if (!soundEnable || headlessMode) return;
5958
- ASSERT(!filename || isString(filename), 'filename must be a string');
5959
- ASSERT(isNumber(randomness), 'randomness must be a number');
5960
-
5961
- /** @property {SoundLoadCallback} - callback function to call when sound is loaded */
5962
- this.onloadCallback = onloadCallback;
5963
- this.randomness = randomness;
5964
- filename && this.loadSound(filename);
5946
+ ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
5947
+ const pitch = getNoteFrequency(semitoneOffset, 1);
5948
+ return this.play(pos, volume, pitch, 0);
5965
5949
  }
5966
5950
 
5967
- /** Loads a sound from a URL and decodes it into sample data. Must be used with await!
5951
+ /** Get how long this sound is in seconds
5952
+ * @return {number} - How long the sound is in seconds (undefined if loading)
5953
+ */
5954
+ getDuration()
5955
+ { return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
5956
+
5957
+ /** Check if sound is loaded, for sounds fetched from a url
5958
+ * @return {boolean} - True if sound is loaded and ready to play
5959
+ */
5960
+ isLoaded() { return this.loadedPercent === 1; }
5961
+
5962
+ /** Loads a sound from a URL and decodes it into sample data.
5968
5963
  * @param {string} filename
5969
5964
  * @return {Promise<void>} */
5970
5965
  async loadSound(filename)
@@ -6455,50 +6450,62 @@ const tileCollisionLayers = [];
6455
6450
 
6456
6451
  /** Get tile collision data for a given cell in the grid
6457
6452
  * @param {Vector2} pos
6453
+ * @param {boolean} [solidOnly] - Only check solid layers?
6458
6454
  * @return {number}
6459
6455
  * @memberof TileLayers */
6460
- function tileCollisionGetData(pos)
6456
+ function tileCollisionGetData(pos, solidOnly=true)
6461
6457
  {
6462
6458
  // check all tile collision layers
6463
6459
  for (const layer of tileCollisionLayers)
6460
+ if (!solidOnly || layer.isSolid)
6464
6461
  if (pos.arrayCheck(layer.size))
6465
- return layer.getCollisionData(pos);
6462
+ {
6463
+ const data = layer.getCollisionData(pos);
6464
+ if (data) return data;
6465
+ }
6466
6466
  return 0;
6467
6467
  }
6468
6468
 
6469
6469
  /** Check if a tile layer collides with another object
6470
- * @param {Vector2} pos
6471
- * @param {Vector2} [size=vec2()]
6472
- * @param {EngineObject} [object] - An object or undefined for generic test
6473
- * @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?
6474
6474
  * @return {TileCollisionLayer}
6475
6475
  * @memberof TileLayers */
6476
- function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
6476
+ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
6477
6477
  {
6478
6478
  for (const layer of tileCollisionLayers)
6479
6479
  {
6480
6480
  if (!solidOnly || layer.isSolid)
6481
- if (layer.collisionTest(pos, size, object))
6481
+ if (layer.collisionTest(pos, size, callbackObject))
6482
6482
  return layer;
6483
6483
  }
6484
6484
  }
6485
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
+
6486
6493
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
6487
- * The point will be inside the colliding tile if it hits (may have a tiny shift)
6488
- * @param {Vector2} posStart
6489
- * @param {Vector2} posEnd
6490
- * @param {EngineObject} [object] - An object or undefined for generic test
6491
- * @param {Vector2} [normal] - Optional normal of the surface hit
6492
- * @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?
6493
6500
  * @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
6494
6501
  * @memberof TileLayers */
6495
- function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
6502
+ function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
6496
6503
  {
6497
6504
  for (const layer of tileCollisionLayers)
6498
6505
  {
6499
6506
  if (!solidOnly || layer.isSolid)
6500
6507
  {
6501
- const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
6508
+ const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, normal)
6502
6509
  if (hitPos) return hitPos;
6503
6510
  }
6504
6511
  }
@@ -6619,17 +6626,17 @@ class TileLayerData
6619
6626
  class CanvasLayer extends EngineObject
6620
6627
  {
6621
6628
  /** Create a canvas layer object
6622
- * @param {Vector2} [position] - World space position of the layer
6629
+ * @param {Vector2} [pos] - World space position of the layer
6623
6630
  * @param {Vector2} [size] - World space size of the layer
6624
6631
  * @param {number} [angle] - Angle the layer is rotated by
6625
6632
  * @param {number} [renderOrder] - Objects sorted by renderOrder
6626
6633
  * @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
6627
6634
  * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6628
6635
  */
6629
- constructor(position, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=glEnable)
6636
+ constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
6630
6637
  {
6631
6638
  ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
6632
- super(position, size, undefined, angle, WHITE, renderOrder);
6639
+ super(pos, size, undefined, angle, WHITE, renderOrder);
6633
6640
 
6634
6641
  /** @property {HTMLCanvasElement} - The canvas used by this layer */
6635
6642
  this.canvas = headlessMode ? undefined : new OffscreenCanvas(canvasSize.x, canvasSize.y);
@@ -6733,16 +6740,16 @@ class CanvasLayer extends EngineObject
6733
6740
  class TileLayer extends CanvasLayer
6734
6741
  {
6735
6742
  /** Create a tile layer object
6736
- * @param {Vector2} position - World space position
6743
+ * @param {Vector2} pos - World space position
6737
6744
  * @param {Vector2} size - World space size
6738
6745
  * @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture)
6739
6746
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
6747
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6740
6748
  */
6741
- constructor(position, size, tileInfo=tile(), renderOrder=0)
6749
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6742
6750
  {
6743
6751
  const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
6744
- const useWebGL = true;
6745
- super(position, size, 0, renderOrder, canvasSize, useWebGL);
6752
+ super(pos, size, 0, renderOrder, canvasSize, useWebGL);
6746
6753
 
6747
6754
  /** @property {TileInfo} - Default tile info for layer */
6748
6755
  this.tileInfo = undefined;
@@ -6754,15 +6761,15 @@ class TileLayer extends CanvasLayer
6754
6761
  if (headlessMode)
6755
6762
  {
6756
6763
  // disable rendering in headless mode
6757
- this.render = () => {};
6758
- this.redraw = () => {};
6759
- this.redrawStart = () => {};
6760
- this.redrawEnd = () => {};
6761
- this.drawTileData = () => {};
6762
- this.redrawTileData = () => {};
6763
- this.drawLayerTile = () => {};
6764
- this.drawLayerRect = () => {};
6765
- this.clearLayerRect = () => {};
6764
+ this.render = ()=> {};
6765
+ this.redraw = ()=> {};
6766
+ this.redrawStart = ()=> {};
6767
+ this.redrawEnd = ()=> {};
6768
+ this.drawTileData = ()=> {};
6769
+ this.redrawTileData = ()=> {};
6770
+ this.drawLayerTile = ()=> {};
6771
+ this.drawLayerRect = ()=> {};
6772
+ this.clearLayerRect = ()=> {};
6766
6773
  return;
6767
6774
  }
6768
6775
 
@@ -6796,6 +6803,12 @@ class TileLayer extends CanvasLayer
6796
6803
  isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
6797
6804
  }
6798
6805
 
6806
+ /** Clear data at a given position in the array
6807
+ * @param {Vector2} layerPos - Local position in array
6808
+ * @param {boolean} [redraw] - Force the tile to redraw if true */
6809
+ clearData(layerPos, redraw=false)
6810
+ { this.setData(layerPos, new TileLayerData, redraw) }
6811
+
6799
6812
  /** Get data at a given position in the array
6800
6813
  * @param {Vector2} layerPos - Local position in array
6801
6814
  * @return {TileLayerData} */
@@ -6837,7 +6850,7 @@ class TileLayer extends CanvasLayer
6837
6850
  for (let x = this.size.x; x--;)
6838
6851
  for (let y = this.size.y; y--;)
6839
6852
  this.drawTileData(vec2(x,y), false);
6840
- this.hasWebGL() && glFlush();
6853
+ this.isUsingWebGL && glFlush();
6841
6854
  this.onRedraw();
6842
6855
  this.redrawEnd();
6843
6856
  }
@@ -6886,14 +6899,13 @@ class TileLayer extends CanvasLayer
6886
6899
  if (!this.context) return;
6887
6900
  ASSERT(drawContext === this.context);
6888
6901
 
6889
- if (glEnable && this.textureInfo.glTexture)
6890
- glSetRenderTarget();
6891
-
6892
6902
  // set stuff back to normal
6903
+ if (this.isUsingWebGL)
6904
+ glSetRenderTarget();
6893
6905
  [drawContext, mainCanvasSize, cameraPos, cameraScale, canvasClearColor] = this.savedRenderSettings;
6894
6906
  }
6895
6907
 
6896
- /** Draw the tile at a given position in the tile grid
6908
+ /** Draw the tile at a given position in the tile layer
6897
6909
  * This can be used to clear out tiles when they are destroyed
6898
6910
  * Tiles can also be redrawn if inside a redrawStart/End block
6899
6911
  * @param {Vector2} layerPos
@@ -6917,7 +6929,7 @@ class TileLayer extends CanvasLayer
6917
6929
  this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
6918
6930
  }
6919
6931
 
6920
- /** Draw the tile at a given position in the tile grid
6932
+ /** Draw the tile at a given position in the tile layer
6921
6933
  * This can be used to clear tiles when they are destroyed
6922
6934
  * For better performance use drawTileData inside a redrawStart/End block
6923
6935
  * @param {Vector2} layerPos
@@ -6945,7 +6957,7 @@ class TileLayer extends CanvasLayer
6945
6957
  angle=0, mirror, additiveColor)
6946
6958
  {
6947
6959
  const drawPos = pos.add(size.scale(.5));
6948
- drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor);
6960
+ drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL);
6949
6961
  }
6950
6962
 
6951
6963
  /** Clear a rectangle in layer space
@@ -6985,14 +6997,15 @@ class TileLayer extends CanvasLayer
6985
6997
  class TileCollisionLayer extends TileLayer
6986
6998
  {
6987
6999
  /** Create a tile layer object
6988
- * @param {Vector2} position - World space position
6989
- * @param {Vector2} size - World space size
6990
- * @param {TileInfo} [tileInfo] - Tile info for layer
7000
+ * @param {Vector2} pos - World space position
7001
+ * @param {Vector2} size - World space size
7002
+ * @param {TileInfo} [tileInfo] - Tile info for layer
6991
7003
  * @param {number} [renderOrder] - Objects are sorted by renderOrder
7004
+ * @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
6992
7005
  */
6993
- constructor(position, size, tileInfo=tile(), renderOrder=0)
7006
+ constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
6994
7007
  {
6995
- super(position, size.floor(), tileInfo, renderOrder);
7008
+ super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
6996
7009
 
6997
7010
  /** @property {Array<number>} - The tile collision grid */
6998
7011
  this.collisionData = [];
@@ -7028,41 +7041,50 @@ class TileCollisionLayer extends TileLayer
7028
7041
  this.collisionData.fill(0);
7029
7042
  }
7030
7043
 
7031
- /** Set tile collision data for a given cell in the grid
7032
- * @param {Vector2} gridPos
7044
+ /** Set tile collision data for a given cell in the layer
7045
+ * @param {Vector2} layerPos
7033
7046
  * @param {number} [data] */
7034
- setCollisionData(gridPos, data=1)
7047
+ setCollisionData(layerPos, data=1)
7035
7048
  {
7036
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
7037
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
7038
- gridPos.arrayCheck(this.size) && (this.collisionData[i] = data);
7049
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
7050
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
7051
+ layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
7039
7052
  }
7040
7053
 
7041
- /** Get tile collision data for a given cell in the grid
7042
- * @param {Vector2} gridPos
7054
+ /** Clear tile collision data for a given cell in the layer
7055
+ * @param {Vector2} layerPos */
7056
+ clearCollisionData(layerPos)
7057
+ { this.setCollisionData(layerPos, 0); }
7058
+
7059
+ /** Get tile collision data for a given cell in the layer
7060
+ * @param {Vector2} layerPos
7043
7061
  * @return {number} */
7044
- getCollisionData(gridPos)
7062
+ getCollisionData(layerPos)
7045
7063
  {
7046
- ASSERT(isVector2(gridPos), 'gridPos must be a Vector2');
7047
- const i = (gridPos.y|0)*this.size.x + gridPos.x|0;
7048
- return gridPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
7064
+ ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
7065
+ const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
7066
+ return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
7049
7067
  }
7050
7068
 
7051
7069
  /** Check if collision with another object should occur
7052
7070
  * @param {Vector2} pos
7053
7071
  * @param {Vector2} [size=vec2()]
7054
- * @param {EngineObject} [object]
7072
+ * @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
7055
7073
  * @return {boolean} */
7056
- collisionTest(pos, size=new Vector2, object)
7074
+ collisionTest(pos, size=new Vector2, callbackObject)
7057
7075
  {
7058
7076
  ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
7059
- ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
7060
-
7061
- // transform to local layer space
7062
- const posX = pos.x - this.pos.x;
7063
- 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;
7064
7084
 
7065
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;
7066
7088
  const minX = max(posX - size.x/2|0, 0);
7067
7089
  const minY = max(posY - size.y/2|0, 0);
7068
7090
  const maxX = min(posX + size.x/2, this.size.x);
@@ -7073,9 +7095,7 @@ class TileCollisionLayer extends TileLayer
7073
7095
  {
7074
7096
  // check if the object should collide with this tile
7075
7097
  const tileData = this.collisionData[y*this.size.x+x];
7076
- if (tileData)
7077
- if (!object || object.collideWithTile(tileData,
7078
- 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)))
7079
7099
  return true;
7080
7100
  }
7081
7101
  return false;
@@ -7083,52 +7103,68 @@ class TileCollisionLayer extends TileLayer
7083
7103
 
7084
7104
  /** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
7085
7105
  * The point will be inside the colliding tile if it hits (may have a tiny shift)
7086
- * @param {Vector2} posStart
7087
- * @param {Vector2} posEnd
7088
- * @param {EngineObject} [object] - An object or undefined for generic test
7089
- * @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
7090
7110
  * @return {Vector2|undefined} */
7091
- collisionRaycast(posStart, posEnd, object, normal)
7111
+ collisionRaycast(posStart, posEnd, callbackObject, normal)
7092
7112
  {
7093
7113
  ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
7094
- 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');
7095
7115
 
7096
- const localPos = new Vector2;
7097
- 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)=>
7098
7122
  {
7099
- // check for tile collision
7100
- localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
7101
- const tileData = this.getCollisionData(localPos);
7102
- 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);
7103
7125
  }
7104
- debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
7105
- const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
7106
- 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)
7107
7131
  {
7108
7132
  const tilePos = hitPos.floor().add(vec2(.5));
7109
- debugRaycast && debugRect(tilePos, vec2(1), '#f008');
7110
- debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
7111
- debugRaycast && debugPoint(hitPos, '#0f0');
7112
- debugRaycast && normal &&
7113
- debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
7114
- 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);
7115
7138
  }
7139
+ return hitPos;
7116
7140
  }
7117
7141
  }
7118
7142
  /**
7119
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
7120
7148
  */
7121
7149
 
7122
7150
  /**
7123
- * @callback ParticleCallbackFunction - Function that processes a particle
7151
+ * @callback ParticleCallback - Function that processes a particle
7124
7152
  * @param {Particle} particle
7125
- * @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
7126
7162
  */
7127
7163
 
7128
7164
  /**
7129
7165
  * Particle Emitter - Spawns particles with the given settings
7130
7166
  * @extends EngineObject
7131
- * @memberof Engine
7167
+ * @memberof Particles
7132
7168
  * @example
7133
7169
  * // create a particle emitter
7134
7170
  * let pos = vec2(2,3);
@@ -7146,7 +7182,7 @@ class TileCollisionLayer extends TileLayer
7146
7182
  class ParticleEmitter extends EngineObject
7147
7183
  {
7148
7184
  /** Create a particle system with the given settings
7149
- * @param {Vector2} position - World space position of the emitter
7185
+ * @param {Vector2} pos - World space position of the emitter
7150
7186
  * @param {number} [angle] - Angle to emit the particles
7151
7187
  * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
7152
7188
  * @param {number} [emitTime] - How long to stay alive (0 is forever)
@@ -7176,7 +7212,7 @@ class ParticleEmitter extends EngineObject
7176
7212
  */
7177
7213
  constructor
7178
7214
  (
7179
- position,
7215
+ pos,
7180
7216
  angle,
7181
7217
  emitSize = 0,
7182
7218
  emitTime = 0,
@@ -7205,12 +7241,13 @@ class ParticleEmitter extends EngineObject
7205
7241
  localSpace = false
7206
7242
  )
7207
7243
  {
7208
- super(position, vec2(), tileInfo, angle, undefined, renderOrder);
7244
+ super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
7209
7245
 
7210
7246
  // emitter settings
7247
+ /** @property {boolean} - Should particles be emitted in a circle */
7248
+ this.emitCircle = typeof emitSize === 'number';
7211
7249
  /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
7212
- this.emitSize = emitSize instanceof Vector2 ?
7213
- emitSize.copy() : emitSize;
7250
+ this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
7214
7251
  /** @property {number} - How long to stay alive (0 is forever) */
7215
7252
  this.emitTime = emitTime;
7216
7253
  /** @property {number} - How many particles per second to spawn, does not emit if 0 */
@@ -7261,26 +7298,31 @@ class ParticleEmitter extends EngineObject
7261
7298
  this.localSpace = localSpace;
7262
7299
  /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
7263
7300
  this.trailScale = 0;
7264
- /** @property {ParticleCallbackFunction} - Callback when particle is destroyed */
7265
- this.particleDestroyCallback = undefined;
7266
- /** @property {ParticleCallbackFunction} - Callback when particle is created */
7301
+ /** @property {ParticleCallback} - Callback when particle is created */
7267
7302
  this.particleCreateCallback = undefined;
7268
- /** @property {number} - Track particle emit time */
7269
- 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;
7270
7307
  /** @property {number} - Percentage of velocity to pass to particles (0-1) */
7271
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 = [];
7272
7313
 
7273
7314
  // track previous position and angle
7274
7315
  this.previousAngle = this.angle;
7275
7316
  this.previousPos = this.pos.copy();
7276
7317
  }
7277
7318
 
7278
- /** Emitters do not have physics */
7279
- updatePhysics() {}
7280
-
7281
7319
  /** Update the emitter to spawn particles, called automatically by engine once each frame */
7282
7320
  update()
7283
7321
  {
7322
+ // physics sanity checks
7323
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
7324
+ ASSERT(this.damping >= 0 && this.damping <= 1);
7325
+
7284
7326
  if (this.velocityInheritance)
7285
7327
  {
7286
7328
  // pass emitter velocity to particles
@@ -7294,7 +7336,7 @@ class ParticleEmitter extends EngineObject
7294
7336
  }
7295
7337
 
7296
7338
  // update emitter
7297
- if (!this.emitTime || this.getAliveTime() <= this.emitTime)
7339
+ if (this.isActive())
7298
7340
  {
7299
7341
  // emit particles
7300
7342
  if (this.emitRate && particleEmitRateScale)
@@ -7304,14 +7346,21 @@ class ParticleEmitter extends EngineObject
7304
7346
  this.emitParticle();
7305
7347
  }
7306
7348
  }
7307
- else
7308
- 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
+ });
7309
7358
 
7310
7359
  if (debugParticles)
7311
7360
  {
7312
7361
  // show emitter bounds
7313
- if (typeof this.emitSize === 'number')
7314
- debugCircle(this.pos, this.emitSize/2, '#0f0');
7362
+ if (this.emitCircle)
7363
+ debugCircle(this.pos, this.emitSize.x/2, '#0f0');
7315
7364
  else
7316
7365
  debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
7317
7366
  }
@@ -7322,14 +7371,15 @@ class ParticleEmitter extends EngineObject
7322
7371
  emitParticle()
7323
7372
  {
7324
7373
  // spawn a particle
7325
- let pos = typeof this.emitSize === 'number' ? // check if number was used
7326
- randInCircle(this.emitSize/2) // circle emitter
7327
- : 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
7328
7377
  .multiply(this.emitSize).rotate(this.angle)
7329
7378
  let angle = rand(this.particleConeAngle, -this.particleConeAngle);
7330
7379
  if (!this.localSpace)
7331
7380
  {
7332
- pos = this.pos.add(pos);
7381
+ pos.x += this.pos.x;
7382
+ pos.y += this.pos.y;
7333
7383
  angle += this.angle;
7334
7384
  }
7335
7385
 
@@ -7349,25 +7399,17 @@ class ParticleEmitter extends EngineObject
7349
7399
  const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
7350
7400
 
7351
7401
  // build particle
7352
- const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
7353
- particle.velocity = vec2().setAngle(velocityAngle, speed);
7354
- particle.angleVelocity = angleSpeed;
7402
+ const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
7403
+ let angleVelocity = angleSpeed;
7355
7404
  if (!this.localSpace && this.velocityInheritance > 0)
7356
7405
  {
7357
7406
  // apply emitter velocity to particle
7358
- particle.velocity.x += this.velocity.x;
7359
- particle.velocity.y += this.velocity.y;
7360
- particle.angleVelocity += this.angleVelocity;
7361
- }
7362
- particle.fadeRate = this.fadeRate;
7363
- particle.damping = this.damping;
7364
- particle.angleDamping = this.angleDamping;
7365
- particle.restitution = this.restitution;
7366
- particle.friction = this.friction;
7367
- particle.gravityScale = this.gravityScale;
7368
- particle.collideTiles = this.collideTiles;
7369
- particle.renderOrder = this.renderOrder;
7370
- 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);
7371
7413
 
7372
7414
  // call particle create callback
7373
7415
  this.particleCreateCallback?.(particle);
@@ -7376,140 +7418,245 @@ class ParticleEmitter extends EngineObject
7376
7418
  return particle;
7377
7419
  }
7378
7420
 
7379
- // Particle emitters are not rendered, only the particles are
7380
- 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
+ }
7381
7449
  }
7382
7450
 
7383
7451
  ///////////////////////////////////////////////////////////////////////////////
7384
7452
  /**
7385
7453
  * Particle Object - Created automatically by Particle Emitters
7386
- * @extends EngineObject
7387
- * @memberof Engine
7454
+ * @memberof Particles
7388
7455
  */
7389
- class Particle extends EngineObject
7456
+ class Particle
7390
7457
  {
7391
7458
  /**
7392
7459
  * Create a particle with the passed in settings
7393
7460
  * Typically this is created automatically by a ParticleEmitter
7394
- * @param {Vector2} position - World space position of the particle
7395
- * @param {TileInfo} tileInfo - Tile info to render particles
7396
- * @param {number} angle - Angle to rotate the particle
7397
- * @param {Color} colorStart - Color at start of life
7398
- * @param {Color} colorEnd - Color at end of life
7399
- * @param {number} lifeTime - How long to live for
7400
- * @param {number} sizeStart - Size at start of life
7401
- * @param {number} sizeEnd - Size at end of life
7402
- * @param {number} fadeRate - How quick to fade in/out
7403
- * @param {boolean} additive - Does it use additive blend mode
7404
- * @param {number} trailScale - If a trail, how long to make it
7405
- * @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
7406
- * @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
7407
7471
  */
7408
- constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
7409
- )
7472
+ constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
7410
7473
  {
7411
- super(position, vec2(), tileInfo, angle);
7412
-
7413
- /** @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} */
7414
7485
  this.colorStart = colorStart;
7415
- /** @property {Color} - Color at end of life */
7486
+ /** @property {Color} */
7416
7487
  this.colorEnd = colorEnd;
7417
- /** @property {number} - How long to live for */
7488
+ /** @property {number} */
7418
7489
  this.lifeTime = lifeTime;
7419
- /** @property {number} - Size at start of life */
7490
+ /** @property {number} */
7420
7491
  this.sizeStart = sizeStart;
7421
- /** @property {number} - Size at end of life */
7492
+ /** @property {number} */
7422
7493
  this.sizeEnd = sizeEnd;
7423
- /** @property {number} - How quick to fade in/out */
7424
- this.fadeRate = fadeRate;
7425
- /** @property {boolean} - Is it additive */
7426
- this.additive = additive;
7427
- /** @property {number} - If a trail, how long to make it */
7428
- this.trailScale = trailScale;
7429
- /** @property {ParticleEmitter} - Parent emitter if local space */
7430
- this.localSpaceEmitter = localSpaceEmitter;
7431
- /** @property {ParticleCallbackFunction} - Called when particle dies */
7432
- this.destroyCallback = destroyCallback;
7433
- // particles do not clamp speed by default
7434
- this.clampSpeed = false;
7435
- }
7436
-
7437
- /** 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 */
7438
7511
  update()
7439
7512
  {
7440
- 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)
7441
7525
  {
7442
- // only apply max circular speed if particle can collide
7443
- const length2 = this.velocity.lengthSquared();
7444
- if (length2 > objectMaxSpeed*objectMaxSpeed)
7445
- {
7446
- const s = objectMaxSpeed / length2**.5;
7447
- this.velocity.x *= s;
7448
- this.velocity.y *= s;
7449
- }
7526
+ this.destroy();
7527
+ return;
7450
7528
  }
7451
7529
 
7452
- 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)=>
7553
+ {
7554
+ const data = tileCollisionGetData(pos);
7555
+ return data && collideCallback(this, data, pos);
7556
+ } : (pos)=> tileCollisionGetData(pos) > 0;
7557
+
7558
+ if (testCollision(this.pos))
7453
7559
  {
7454
- // destroy particle when its time runs out
7455
- const c = this.colorEnd;
7456
- this.color.set(c.r, c.g, c.b, c.a);
7457
- this.size.set(this.sizeEnd, this.sizeEnd);
7458
- this.destroyCallback?.(this);
7459
- this.destroyed = 1;
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
+ }
7460
7592
  }
7461
7593
  }
7462
7594
 
7463
- /** 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 */
7464
7607
  render()
7465
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
+
7466
7616
  // lerp color and size
7467
7617
  const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
7468
7618
  const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
7469
7619
  const size = vec2(radius);
7620
+ const alphaFade = p1 < fadeRate ? p1/fadeRate :
7621
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
7470
7622
  this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
7471
7623
  this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
7472
7624
  this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
7473
- this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
7474
-
7475
- // fade alpha
7476
- const fadeRate = this.fadeRate/2;
7477
- this.color.a *= p1 < fadeRate ? p1/fadeRate :
7478
- p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
7625
+ this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
7479
7626
 
7480
7627
  // draw the particle
7481
- this.additive && setBlendMode(true);
7628
+ additive && setBlendMode(true);
7482
7629
 
7483
7630
  // update the position and angle for drawing
7484
- let pos = this.pos, angle = this.angle;
7485
- if (this.localSpaceEmitter)
7631
+ const pos = this.pos.copy();
7632
+ let angle = this.angle;
7633
+ if (localSpace)
7486
7634
  {
7487
7635
  // in local space of emitter
7488
- const a = this.localSpaceEmitter.angle;
7636
+ const a = emitter.angle;
7489
7637
  const c = cos(a), s = sin(a);
7490
- pos = this.localSpaceEmitter.pos.add(
7491
- new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
7492
- 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;
7493
7641
  }
7494
- if (this.trailScale)
7642
+ if (trailScale)
7495
7643
  {
7496
7644
  // trail style particles
7497
- const direction = this.localSpaceEmitter ?
7498
- this.velocity.rotate(-this.localSpaceEmitter.angle) :
7499
- this.velocity;
7500
- const speed = direction.length();
7645
+ const velocity = localSpace ?
7646
+ this.velocity.rotate(-emitter.angle) : this.velocity;
7647
+ const speed = velocity.length();
7501
7648
  if (speed)
7502
7649
  {
7503
7650
  // stretch in direction of motion
7504
- const trailLength = speed * this.trailScale;
7651
+ const trailLength = speed * trailScale;
7505
7652
  size.y = max(size.x, trailLength);
7506
- angle = atan2(direction.x, direction.y);
7653
+ angle = atan2(velocity.x, velocity.y);
7507
7654
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
7508
7655
  }
7509
7656
  }
7510
7657
  else
7511
7658
  drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
7512
- this.additive && setBlendMode();
7659
+ additive && setBlendMode();
7513
7660
  debugParticles && debugRect(pos, size, '#f005', 0, angle);
7514
7661
  }
7515
7662
  }
@@ -7963,7 +8110,7 @@ function glPreRender(clear=true)
7963
8110
  p.x, p.y, 0, 1];
7964
8111
 
7965
8112
  // set the same transform matrix for both shaders
7966
- const initUniform = (program, uniform, value) =>
8113
+ const initUniform = (program, uniform, value)=>
7967
8114
  {
7968
8115
  glContext.useProgram(program);
7969
8116
  const location = glContext.getUniformLocation(program, uniform);
@@ -8357,7 +8504,7 @@ function glSetRenderTarget(texture, clear=false)
8357
8504
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, glFramebuffer);
8358
8505
  glContext.framebufferTexture2D(glContext.FRAMEBUFFER,
8359
8506
  glContext.COLOR_ATTACHMENT0, glContext.TEXTURE_2D, texture, 0);
8360
- glPreRender(clear);
8507
+ glPreRender(clear);
8361
8508
  }
8362
8509
  else
8363
8510
  {
@@ -8789,7 +8936,7 @@ class PostProcessPlugin
8789
8936
  {
8790
8937
  /** Create global post processing shader
8791
8938
  * @param {string} shaderCode
8792
- * @param {boolean} [includeMainCanvas] - combine mainCanvs onto glCanvas
8939
+ * @param {boolean} [includeMainCanvas] - combine mainCanvas onto glCanvas
8793
8940
  * @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
8794
8941
  * @example
8795
8942
  * // create the post process plugin object
@@ -8976,7 +9123,7 @@ class ZzFXMusic extends Sound
8976
9123
  /** Play the music that loops by default
8977
9124
  * @param {number} [volume] - Volume to play the music at
8978
9125
  * @param {boolean} [loop] - Should the music loop?
8979
- * @return {AudioBufferSourceNode} - The audio source node
9126
+ * @return {SoundInstance} - The sound instance
8980
9127
  */
8981
9128
  playMusic(volume=1, loop=true)
8982
9129
  { return super.play(undefined, volume, 1, 0, loop); }
@@ -9023,7 +9170,7 @@ function zzfxM(instruments, patterns, sequence, BPM = 125)
9023
9170
  sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
9024
9171
 
9025
9172
  // for each pattern in sequence
9026
- sequence.forEach((patternIndex, sequenceIndex) => {
9173
+ sequence.forEach((patternIndex, sequenceIndex)=> {
9027
9174
  // get pattern for current channel, use empty 1 note pattern if none found
9028
9175
  patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
9029
9176
 
@@ -9116,7 +9263,7 @@ let uiDebug = 0;
9116
9263
 
9117
9264
  /** Enable UI system debug drawing
9118
9265
  * 0=off, 1=normal, 2=show invisible
9119
- * @param {number|boolean} enable
9266
+ * @param {number|boolean} debugMode
9120
9267
  * @memberof UISystem */
9121
9268
  function uiSetDebug(debugMode)
9122
9269
  { uiDebug = typeof debugMode === 'boolean' ? (debugMode ? 1 : 0) : debugMode; }
@@ -9182,7 +9329,7 @@ class UISystemPlugin
9182
9329
  // navigation properties
9183
9330
  /** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
9184
9331
  this.navigationObject = undefined;
9185
- /** @property {Timer} - Cooldown timer for navigation inputs */
9332
+ /** @property {Timer} - Cool down timer for navigation inputs */
9186
9333
  this.navigationTimer = new Timer(undefined, true);
9187
9334
  /** @property {number} - Time between navigation inputs in seconds */
9188
9335
  this.navigationDelay = .2;
@@ -9648,7 +9795,7 @@ class UISystemPlugin
9648
9795
 
9649
9796
  const savedNavigationDirection = uiSystem.navigationDirection;
9650
9797
 
9651
- // allow both axies for navigation
9798
+ // allow both axes for navigation
9652
9799
  uiSystem.navigationDirection = 2;
9653
9800
 
9654
9801
  // confirm menu
@@ -9743,7 +9890,7 @@ class UIObject
9743
9890
  this.lineWidth = uiSystem.defaultLineWidth;
9744
9891
  /** @property {number} - Corner radius for rounded rects */
9745
9892
  this.cornerRadius = uiSystem.defaultCornerRadius;
9746
- /** @property {string} - Font for this objecct */
9893
+ /** @property {string} - Font for this object */
9747
9894
  this.font = uiSystem.defaultFont;
9748
9895
  /** @property {string} - Font style for this object or undefined */
9749
9896
  this.fontStyle = undefined;
@@ -10231,6 +10378,8 @@ class UIScrollbar extends UIObject
10231
10378
  this.value = value;
10232
10379
  /** @property {Color} - Color for the handle part of the scrollbar */
10233
10380
  this.handleColor = handleColor.copy();
10381
+ /** @property {boolean} - Should it fill up like a progress bar? */
10382
+ this.fillMode = false;
10234
10383
 
10235
10384
  // set properties
10236
10385
  this.text = text;
@@ -10276,19 +10425,30 @@ class UIScrollbar extends UIObject
10276
10425
 
10277
10426
  // handle horizontal or vertical scrollbar
10278
10427
  const isHorizontal = this.size.x > this.size.y;
10279
- const handleSize = isHorizontal ? this.size.y : this.size.x;
10280
- const barSize = isHorizontal ? this.size.x : this.size.y;
10281
- const centerPos = isHorizontal ? this.pos.x : this.pos.y;
10282
-
10283
- // draw the scrollbar handle
10284
- const handleWidth = barSize - handleSize;
10285
- const p1 = centerPos - handleWidth/2;
10286
- const p2 = centerPos + handleWidth/2;
10287
- const handlePos = isHorizontal ?
10288
- vec2(lerp(p1, p2, this.value), this.pos.y) :
10289
- vec2(this.pos.x, lerp(p2, p1, this.value))
10290
- const handleColor = this.disabled ? this.disabledColor : this.handleColor;
10291
- 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
+ }
10292
10452
 
10293
10453
  // draw the text scaled to fit on the scrollbar
10294
10454
  const textSize = this.getTextSize();
@@ -10310,15 +10470,15 @@ class UIScrollbar extends UIObject
10310
10470
  * @extends UIObject
10311
10471
  * @example
10312
10472
  * // Create a video player UI object
10313
- * 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);
10314
10474
  * video.play();
10315
10475
  * @memberof UISystem
10316
10476
  */
10317
10477
  class UIVideo extends UIObject
10318
10478
  {
10319
10479
  /** Create a video player UI object
10320
- * @param {Vector2} [pos]
10321
- * @param {Vector2} [size]
10480
+ * @param {Vector2} pos
10481
+ * @param {Vector2} size
10322
10482
  * @param {string} src - Video file path or URL
10323
10483
  * @param {boolean} [autoplay=false] - Start playing immediately?
10324
10484
  * @param {boolean} [loop=false] - Loop the video?
@@ -10458,6 +10618,7 @@ class UIVideo extends UIObject
10458
10618
  * - Contact begin and end callbacks
10459
10619
  * - Wraps b2Vec2 type to/from Vector2
10460
10620
  * - Raycasting and querying
10621
+ * - Box2dTileLayer for grid based collision
10461
10622
  * - Every type of joint
10462
10623
  * - Debug physics drawing
10463
10624
  * @namespace Box2D
@@ -10507,21 +10668,24 @@ class Box2dObject extends EngineObject
10507
10668
  bodyDef.set_type(bodyType);
10508
10669
  bodyDef.set_position(box2d.vec2dTo(pos));
10509
10670
  bodyDef.set_angle(-angle);
10671
+
10672
+ /** @property {Object} - The Box2d body */
10510
10673
  this.body = box2d.world.CreateBody(bodyDef);
10511
- this.body.object = this;
10674
+ /** @property {Color} - Line color used for default box2d drawing */
10512
10675
  this.lineColor = BLACK;
10513
- box2d.objects.push(this);
10514
-
10515
- // edge lists and loops for drawing
10676
+ /** @property {Array<Object>} - List of all edges for default box2d drawing */
10516
10677
  this.edgeLists = [];
10678
+ /** @property {Array<Object>} - List of all edge loops for default box2d drawing */
10517
10679
  this.edgeLoops = [];
10680
+
10681
+ this.body.object = this; // link body to this object
10682
+ box2d.objects.push(this); // keep track of all box2d objects
10518
10683
  }
10519
10684
 
10520
10685
  /** Destroy this object and its physics body */
10521
10686
  destroy()
10522
10687
  {
10523
- if (this.destroyed)
10524
- return;
10688
+ if (this.destroyed) return;
10525
10689
 
10526
10690
  // destroy physics body, fixtures, and joints
10527
10691
  ASSERT(this.body, 'Box2dObject has no body to destroy');
@@ -10552,11 +10716,12 @@ class Box2dObject extends EngineObject
10552
10716
  }
10553
10717
 
10554
10718
  /** Draws all this object's fixtures
10555
- * @param {Color} [color]
10556
- * @param {Color} [lineColor]
10557
- * @param {number} [lineWidth]
10719
+ * @param {Color} [color]
10720
+ * @param {Color} [lineColor]
10721
+ * @param {number} [lineWidth]
10722
+ * @param {boolean} [useWebGL=glEnable]
10558
10723
  * @param {CanvasRenderingContext2D} [context] */
10559
- drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
10724
+ drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, useWebGL, context)
10560
10725
  {
10561
10726
  // draw non-edge fixtures
10562
10727
  this.getFixtureList().forEach((fixture)=>
@@ -10564,7 +10729,7 @@ class Box2dObject extends EngineObject
10564
10729
  const shape = box2d.castObjectType(fixture.GetShape());
10565
10730
  if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
10566
10731
  {
10567
- box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
10732
+ box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, useWebGL, context);
10568
10733
  }
10569
10734
  });
10570
10735
 
@@ -10790,6 +10955,14 @@ class Box2dObject extends EngineObject
10790
10955
  return fixtures;
10791
10956
  }
10792
10957
 
10958
+ /** Destroy a fixture from the body
10959
+ * @param {Object} [fixture] */
10960
+ destroyFixture(fixture) { this.body.DestroyFixture(fixture); }
10961
+
10962
+ /** Destroy all fixture from the body */
10963
+ destroyAllFixtures()
10964
+ { this.getFixtureList().forEach(fixture=>this.destroyFixture(fixture)); }
10965
+
10793
10966
  ///////////////////////////////////////////////////////////////////////////////
10794
10967
  // physics get functions
10795
10968
 
@@ -11022,6 +11195,135 @@ class Box2dObject extends EngineObject
11022
11195
  }
11023
11196
  }
11024
11197
 
11198
+ ///////////////////////////////////////////////////////////////////////////////
11199
+ /**
11200
+ * Box2D Static Object - Box2d with a static physics body
11201
+ * @extends Box2dObject
11202
+ * @memberof Box2D
11203
+ */
11204
+ class Box2dStaticObject extends Box2dObject
11205
+ {
11206
+ /** Create a LittleJS object with Box2d physics
11207
+ * @param {Vector2} [pos]
11208
+ * @param {Vector2} [size]
11209
+ * @param {TileInfo} [tileInfo]
11210
+ * @param {number} [angle]
11211
+ * @param {Color} [color]
11212
+ * @param {number} [renderOrder] */
11213
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11214
+ {
11215
+ const bodyType = box2d.bodyTypeStatic;
11216
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11217
+ }
11218
+ }
11219
+
11220
+ ///////////////////////////////////////////////////////////////////////////////
11221
+ /**
11222
+ * Box2D Kinematic Object - Box2d with a kinematic physics body
11223
+ * @extends Box2dObject
11224
+ * @memberof Box2D
11225
+ */
11226
+ class Box2dKinematicObject extends Box2dObject
11227
+ {
11228
+ /** Create a LittleJS object with Box2d physics
11229
+ * @param {Vector2} [pos]
11230
+ * @param {Vector2} [size]
11231
+ * @param {TileInfo} [tileInfo]
11232
+ * @param {number} [angle]
11233
+ * @param {Color} [color]
11234
+ * @param {number} [renderOrder] */
11235
+ constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11236
+ {
11237
+ const bodyType = box2d.bodyTypeKinematic;
11238
+ super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11239
+ }
11240
+ }
11241
+
11242
+ ///////////////////////////////////////////////////////////////////////////////
11243
+ /**
11244
+ * Box2d Tile Layer
11245
+ * - adds Box2d support to tile layers
11246
+ * - creates static box2d fixtures for solid tiles
11247
+ * @extends Box2dObject
11248
+ * @memberof Box2D
11249
+ */
11250
+ class Box2dTileLayer extends Box2dStaticObject
11251
+ {
11252
+ /** Create a Box2d tile layer object
11253
+ * @param {TileCollisionLayer} tileLayer - Tile layer for this object */
11254
+ constructor(tileLayer)
11255
+ {
11256
+ ASSERT(tileLayer instanceof TileCollisionLayer, 'tileLayer must be a TileCollisionLayer');
11257
+ super(tileLayer.pos, tileLayer.size);
11258
+
11259
+ /** @property {TileLayer} - The tile layer */
11260
+ this.tileLayer = tileLayer;
11261
+ this.addChild(tileLayer);
11262
+ }
11263
+
11264
+ render()
11265
+ {
11266
+ // do not render fixtures, tile layer handles rendering
11267
+ }
11268
+
11269
+ /** Create box2d collision fixtures for solid tiles
11270
+ * @param {number} [friction]
11271
+ * @param {number} [restitution] */
11272
+ buildCollision(friction=.2, restitution=0)
11273
+ {
11274
+ // destroy all fixtures and create new ones
11275
+ this.destroyAllFixtures();
11276
+
11277
+ // create box2d object for this layer
11278
+ this.pos = this.tileLayer.pos.copy();
11279
+ this.size = this.tileLayer.size.copy();
11280
+
11281
+ // track which tiles have been processed
11282
+ const processed = [];
11283
+ const getIndex = (x, y)=> x + y * this.size.x;
11284
+ const isSolidUnprocessed = (x, y)=>
11285
+ !processed[getIndex(x, y)] &&
11286
+ this.tileLayer.getCollisionData(vec2(x, y)) > 0;
11287
+
11288
+ // combine tiles into larger boxes
11289
+ for (let x = 0; x < this.size.x; ++x)
11290
+ for (let y = 0; y < this.size.y; ++y)
11291
+ {
11292
+ if (!isSolidUnprocessed(x, y)) continue;
11293
+
11294
+ // find max width by scanning right
11295
+ let width = 1, height = 1, canExpand = true;
11296
+ while (isSolidUnprocessed(x + width, y))
11297
+ ++width;
11298
+
11299
+ // find max height by scanning up, ensuring all rows have the same width
11300
+ while (canExpand)
11301
+ {
11302
+ for (let checkX = 0; checkX < width; ++checkX)
11303
+ {
11304
+ if (!isSolidUnprocessed(x + checkX, y + height))
11305
+ {
11306
+ canExpand = false;
11307
+ break;
11308
+ }
11309
+ }
11310
+ if (canExpand)
11311
+ ++height;
11312
+ }
11313
+
11314
+ // mark all tiles in this rectangle as processed
11315
+ for (let rectX = width; rectX--;)
11316
+ for (let rectY = height; rectY--;)
11317
+ processed[getIndex(x + rectX, y + rectY)] = true;
11318
+
11319
+ // create a single fixture for the entire rectangle
11320
+ const shapeSize = vec2(width, height);
11321
+ const offset = vec2(x + width/2, y + height/2);
11322
+ this.addBox(shapeSize, offset, 0, 0, friction, restitution);
11323
+ }
11324
+ }
11325
+ }
11326
+
11025
11327
  ///////////////////////////////////////////////////////////////////////////////
11026
11328
  /**
11027
11329
  * Box2D Raycast Result
@@ -11063,6 +11365,7 @@ class Box2dJoint
11063
11365
  * @param {Object} jointDef */
11064
11366
  constructor(jointDef)
11065
11367
  {
11368
+ /** @property {Object} - The Box2d joint */
11066
11369
  this.box2dJoint = box2d.castObjectType(box2d.world.CreateJoint(jointDef));
11067
11370
  }
11068
11371
 
@@ -11409,7 +11712,7 @@ class Box2dGearJoint extends Box2dJoint
11409
11712
  * @param {Box2dObject} objectB
11410
11713
  * @param {Box2dJoint} joint1
11411
11714
  * @param {Box2dJoint} joint2
11412
- * @param {ratio} [ratio] */
11715
+ * @param {number} [ratio] */
11413
11716
  constructor(objectA, objectB, joint1, joint2, ratio=1)
11414
11717
  {
11415
11718
  const jointDef = new box2d.instance.b2GearJointDef();
@@ -11551,50 +11854,6 @@ class Box2dPrismaticJoint extends Box2dJoint
11551
11854
  getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
11552
11855
  }
11553
11856
 
11554
- ///////////////////////////////////////////////////////////////////////////////
11555
- /**
11556
- * Box2D Static Object - Box2d with a static physics body
11557
- * @extends Box2dObject
11558
- * @memberof Box2D
11559
- */
11560
- class Box2dStaticObject extends Box2dObject
11561
- {
11562
- /** Create a LittleJS object with Box2d physics
11563
- * @param {Vector2} [pos]
11564
- * @param {Vector2} [size]
11565
- * @param {TileInfo} [tileInfo]
11566
- * @param {number} [angle]
11567
- * @param {Color} [color]
11568
- * @param {number} [renderOrder] */
11569
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11570
- {
11571
- const bodyType = box2d.bodyTypeStatic;
11572
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11573
- }
11574
- }
11575
-
11576
- ///////////////////////////////////////////////////////////////////////////////
11577
- /**
11578
- * Box2D Kiematic Object - Box2d with a kinematic physics body
11579
- * @extends Box2dObject
11580
- * @memberof Box2D
11581
- */
11582
- class Box2dKiematicObject extends Box2dObject
11583
- {
11584
- /** Create a LittleJS object with Box2d physics
11585
- * @param {Vector2} [pos]
11586
- * @param {Vector2} [size]
11587
- * @param {TileInfo} [tileInfo]
11588
- * @param {number} [angle]
11589
- * @param {Color} [color]
11590
- * @param {number} [renderOrder] */
11591
- constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
11592
- {
11593
- const bodyType = box2d.bodyTypeKinematic;
11594
- super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
11595
- }
11596
- }
11597
-
11598
11857
  ///////////////////////////////////////////////////////////////////////////////
11599
11858
  /**
11600
11859
  * Box2D Wheel Joint
@@ -11955,10 +12214,13 @@ class Box2dPlugin
11955
12214
  {
11956
12215
  ASSERT(!box2d, 'Box2D already initialized');
11957
12216
  box2d = this;
12217
+
12218
+ /** @property {Object} - The Box2d instance */
11958
12219
  this.instance = instance;
12220
+ /** @property {Object} - The Box2d world */
11959
12221
  this.world = new box2d.instance.b2World();
12222
+ /** @property {Array<Box2dObject>} - List of all Box2d objects */
11960
12223
  this.objects = [];
11961
-
11962
12224
  /** @property {number} - Velocity iterations per update*/
11963
12225
  this.velocityIterations = 8;
11964
12226
  /** @property {number} - Position iterations per update*/
@@ -12157,8 +12419,9 @@ class Box2dPlugin
12157
12419
  * @param {Color} [color]
12158
12420
  * @param {Color} [lineColor]
12159
12421
  * @param {number} [lineWidth]
12422
+ * @param {boolean} [useWebGL=glEnable]
12160
12423
  * @param {CanvasRenderingContext2D} [context] */
12161
- drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, context)
12424
+ drawFixture(fixture, pos, angle, color=WHITE, lineColor=BLACK, lineWidth=.1, useWebgl, context)
12162
12425
  {
12163
12426
  const shape = box2d.castObjectType(fixture.GetShape());
12164
12427
  switch (shape.GetType())
@@ -12168,20 +12431,20 @@ class Box2dPlugin
12168
12431
  let points = [];
12169
12432
  for (let i=shape.GetVertexCount(); i--;)
12170
12433
  points.push(box2d.vec2From(shape.GetVertex(i)));
12171
- drawPoly(points, color, lineWidth, lineColor, pos, angle);
12434
+ drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebgl, false, context);
12172
12435
  break;
12173
12436
  }
12174
12437
  case box2d.instance.b2Shape.e_circle:
12175
12438
  {
12176
12439
  const radius = shape.get_m_radius();
12177
- drawCircle(pos, radius*2, color, lineWidth, lineColor);
12440
+ drawCircle(pos, radius*2, color, lineWidth, lineColor, useWebgl, false, context);
12178
12441
  break;
12179
12442
  }
12180
12443
  case box2d.instance.b2Shape.e_edge:
12181
12444
  {
12182
12445
  const v1 = box2d.vec2From(shape.get_m_vertex1());
12183
12446
  const v2 = box2d.vec2From(shape.get_m_vertex2());
12184
- drawLine(v1, v2, lineWidth, lineColor, pos, angle);
12447
+ drawLine(v1, v2, lineWidth, lineColor, pos, angle, useWebgl, false, context);
12185
12448
  break;
12186
12449
  }
12187
12450
  }
@@ -12199,7 +12462,7 @@ class Box2dPlugin
12199
12462
  }
12200
12463
 
12201
12464
  /** converts a box2d vec2 pointer to a Vector2
12202
- * @param {Object} v */
12465
+ * @param {Object} vp */
12203
12466
  vec2FromPointer(vp)
12204
12467
  {
12205
12468
  const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
@@ -12311,7 +12574,7 @@ async function box2dInit()
12311
12574
  const box2dColorPointer = (c)=>
12312
12575
  box2dColor(box2d.instance.wrapPointer(c, box2d.instance.b2Color));
12313
12576
  const getDebugColor = (color)=>box2dColorPointer(color).scale(1,.8);
12314
- const getPointsList = (vertices, vertexCount) =>
12577
+ const getPointsList = (vertices, vertexCount)=>
12315
12578
  {
12316
12579
  const points = [];
12317
12580
  for (let i=vertexCount; i--;)
@@ -12680,6 +12943,8 @@ export
12680
12943
  saveCanvas,
12681
12944
  saveDataURL,
12682
12945
  shareURL,
12946
+ readSaveData,
12947
+ writeSaveData,
12683
12948
 
12684
12949
  // Random
12685
12950
  rand,
@@ -12818,7 +13083,6 @@ export
12818
13083
  audioMasterGain,
12819
13084
  audioDefaultSampleRate,
12820
13085
  Sound,
12821
- SoundWave,
12822
13086
  SoundInstance,
12823
13087
  speak,
12824
13088
  speakStop,
@@ -12890,7 +13154,7 @@ export
12890
13154
  Box2dPlugin,
12891
13155
  Box2dObject,
12892
13156
  Box2dStaticObject,
12893
- Box2dKiematicObject,
13157
+ Box2dKinematicObject,
12894
13158
  Box2dRaycastResult,
12895
13159
  Box2dJoint,
12896
13160
  Box2dTargetJoint,