littlejsengine 1.17.5 → 1.17.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +10 -10
  2. package/dist/littlejs.d.ts +189 -97
  3. package/dist/littlejs.esm.js +637 -477
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +633 -476
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +631 -474
  8. package/examples/electron/index.html +2 -2
  9. package/examples/games.jpg +0 -0
  10. package/examples/index.html +2 -2
  11. package/examples/logo.png +0 -0
  12. package/examples/logo2.png +0 -0
  13. package/examples/particles/index.html +1 -1
  14. package/examples/platformer/gameEffects.js +3 -3
  15. package/examples/screenshot.jpg +0 -0
  16. package/examples/shorts/base.html +1 -1
  17. package/examples/shorts/colors.js +1 -1
  18. package/examples/shorts/fontImage.js +1 -1
  19. package/examples/shorts/fps.js +1 -1
  20. package/examples/shorts/music.js +3 -2
  21. package/examples/shorts/musicPlayer.js +2 -1
  22. package/examples/shorts/parallax.js +2 -2
  23. package/examples/shorts/tiltedView.js +1 -1
  24. package/examples/starter/index.html +2 -2
  25. package/package.json +1 -1
  26. package/plugins/box2d.js +6 -7
  27. package/plugins/pluginExport.js +2 -1
  28. package/plugins/postProcess.js +1 -1
  29. package/plugins/uiSystem.js +33 -21
  30. package/reference.md +6 -6
  31. package/src/engine.js +106 -152
  32. package/src/engineAudio.js +5 -5
  33. package/src/engineDebug.js +1 -1
  34. package/src/engineDraw.js +36 -14
  35. package/src/engineExport.js +2 -0
  36. package/src/engineInput.js +1 -1
  37. package/src/engineMath.js +6 -4
  38. package/src/engineObject.js +24 -24
  39. package/src/engineParticles.js +241 -118
  40. package/src/engineTileLayer.js +66 -48
  41. package/src/engineUtilities.js +105 -78
  42. package/plugins/Box2D_License.txt +0 -17
package/src/engine.js CHANGED
@@ -30,7 +30,7 @@ const engineName = 'LittleJS';
30
30
  * @type {string}
31
31
  * @default
32
32
  * @memberof Engine */
33
- const engineVersion = '1.17.5';
33
+ const engineVersion = '1.17.11';
34
34
 
35
35
  /** Frames per second to update
36
36
  * @type {number}
@@ -393,33 +393,11 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
393
393
  workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
394
394
 
395
395
  // create promises for loading images
396
- const promises = imageSources.map((src, textureIndex)=>
397
- new Promise(resolve =>
398
- {
399
- ASSERT(isString(src), 'imageSources must be an array of strings');
400
-
401
- const image = new Image;
402
- image.onerror = image.onload = ()=>
403
- {
404
- const textureInfo = new TextureInfo(image);
405
- textureInfos[textureIndex] = textureInfo;
406
- resolve();
407
- }
408
- image.crossOrigin = 'anonymous';
409
- image.src = src;
410
- })
411
- );
396
+ const promises = imageSources.map((src, i)=> loadTexture(i, src));
412
397
 
398
+ // no images to load
413
399
  if (!imageSources.length)
414
- {
415
- // no images to load
416
- promises.push(new Promise(resolve =>
417
- {
418
- const textureInfo = new TextureInfo(new Image);
419
- textureInfos[0] = textureInfo;
420
- resolve();
421
- }));
422
- }
400
+ promises.push(loadTexture(0));
423
401
 
424
402
  // load engine font image
425
403
  promises.push(fontImageInit());
@@ -461,13 +439,13 @@ function engineObjectsUpdate()
461
439
  engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
462
440
 
463
441
  // recursive object update
464
- function updateObject(o)
442
+ function updateChildObject(o)
465
443
  {
466
444
  if (o.destroyed) return;
467
445
 
468
446
  o.update();
469
447
  for (const child of o.children)
470
- updateObject(child);
448
+ updateChildObject(child);
471
449
  }
472
450
  for (const o of engineObjects)
473
451
  {
@@ -477,7 +455,7 @@ function engineObjectsUpdate()
477
455
  o.update();
478
456
  o.updatePhysics();
479
457
  for (const child of o.children)
480
- updateObject(child);
458
+ updateChildObject(child);
481
459
  o.updateTransforms();
482
460
  }
483
461
 
@@ -486,11 +464,14 @@ function engineObjectsUpdate()
486
464
  }
487
465
 
488
466
  /** Destroy and remove all objects
467
+ * - This can be used to clear out all objects when restarting a level
468
+ * - Objects can override their destroy function to do cleanup or stick around
469
+ * @param {boolean} [immediate] - should attached effects be allowed to die off?
489
470
  * @memberof Engine */
490
- function engineObjectsDestroy()
471
+ function engineObjectsDestroy(immediate=true)
491
472
  {
492
473
  for (const o of engineObjects)
493
- o.parent || o.destroy();
474
+ o.parent || o.destroy(immediate);
494
475
  engineObjects = engineObjects.filter(o=>!o.destroyed);
495
476
  }
496
477
 
@@ -565,164 +546,137 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
565
546
  ///////////////////////////////////////////////////////////////////////////////
566
547
  function drawEngineLogo(t)
567
548
  {
549
+ const blackAndWhite = 0;
550
+ const showName = 1;
551
+
568
552
  // LittleJS Logo and Splash Screen
569
553
  const x = mainContext;
570
554
  const w = mainCanvas.width = innerWidth;
571
555
  const h = mainCanvas.height = innerHeight;
572
-
573
556
  {
574
557
  // background
575
558
  const p3 = percent(t, 1, .8);
576
559
  const p4 = percent(t, 0, .5);
577
- const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
560
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
578
561
  g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
579
562
  g.addColorStop(1,hsl(0,0,0,p3).toString());
580
563
  x.save();
581
564
  x.fillStyle = g;
582
565
  x.fillRect(0,0,w,h);
583
566
  }
584
-
585
- // draw LittleJS logo...
586
- const rect = (X, Y, W, H, C)=>
567
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
568
+ {
569
+ if (C >= 0)
570
+ {
571
+ if (blackAndWhite)
572
+ x.fillStyle = '#fff';
573
+ else
574
+ {
575
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
576
+ g.addColorStop(0,color(C,2));
577
+ g.addColorStop(1,color(C,1));
578
+ }
579
+ }
580
+ else
581
+ x.fillStyle = '#000';
582
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
583
+ }
584
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
587
585
  {
588
586
  x.beginPath();
589
- x.rect(X,Y,W,C?H*p:H);
590
- x.fillStyle = C;
591
- C ? x.fill() : x.stroke();
592
- };
593
- const line = (X, Y, Z, W)=>
587
+ x.arc(X,Y,R,p*A,p*B);
588
+ gradient(X,Y-R,X,Y+R,C,S);
589
+ }
590
+ const rect = (X,Y,W,H,C)=>
594
591
  {
595
592
  x.beginPath();
596
- x.lineTo(X,Y);
597
- x.lineTo(Z,W);
598
- x.stroke();
599
- };
600
- const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
593
+ x.rect(X,Y,W,H*p);
594
+ gradient(X,Y+H,X+W,Y,C);
595
+ }
596
+ const poly = (points,C,Y,H)=>
601
597
  {
602
- const D = (A+B)/2, E = p*(B-A)/2;
603
598
  x.beginPath();
604
- F && x.lineTo(X,Y);
605
- x.arc(X,Y,R,D-E,D+E);
606
- x.fillStyle = C;
607
- C ? x.fill() : x.stroke();
608
- };
609
- const color = (c=0, l=0)=>
610
- hsl([.98,.3,.57,.14][c%4],.9,[0,.3,.5,.8,.9][l]).toString();
599
+ for (const p of points)
600
+ x.lineTo(p.x, p.y);
601
+ x.closePath();
602
+ gradient(0, Y, 0, Y+H,C);
603
+ }
604
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
605
+
606
+ // center and fit tos screen
611
607
  const alpha = wave(1,1,t);
612
608
  const p = percent(alpha, .1, .5);
613
-
614
- // setup
609
+ const size = min(6, min(w,h)/99);
615
610
  x.translate(w/2,h/2);
616
- const size = min(6, min(w,h)/99); // fit to screen
617
611
  x.scale(size,size);
618
612
  x.translate(-40,-35);
613
+ p < 1 && x.setLineDash([99*p,99]);
619
614
  x.lineJoin = x.lineCap = 'round';
620
615
  x.lineWidth = .1 + p*1.9;
616
+ //x.strokeStyle='#fff7';
617
+
618
+ if (showName)
619
+ {
620
+ // engine name text
621
+ const Y = 54;
622
+ const s = 'LittleJS';
623
+ x.font = '900 15.5px arial';
624
+ x.lineWidth = .1+p*3.9;
625
+ x.textAlign = 'center';
626
+ x.textBaseline = 'top';
627
+ rect(11,Y+1,59,8*p,-1);
628
+ x.beginPath();
621
629
 
622
- // drawing effect
623
- const p2 = percent(alpha,.1,1);
624
- x.setLineDash([99*p2,99]);
625
-
626
- // cab top
627
- rect(7,16,18,-8,color(2,2));
628
- rect(7,8,18,4,color(2,3));
629
- rect(25,8,8,8,color(2,1));
630
- rect(25,8,-18,8);
631
- rect(25,8,8,8);
632
-
633
- // cab
634
- rect(25,16,7,24,color());
635
- rect(11,39,14,-23,color(1,1));
636
- rect(11,16,14,18,color(1,2));
637
- rect(11,16,14,8,color(1,3));
638
- rect(25,16,-14,24);
639
-
640
- // cab window
641
- rect(15,29,6,-9,color(2,2));
642
- circle(15,21,5,0,PI/2,color(2,4),1);
643
- rect(21,21,-6,9);
644
-
645
- // little stack
646
- rect(37,14,9,6,color(3,2));
647
- rect(37,14,4.5,6,color(3,3));
648
- rect(37,14,9,6);
630
+ let w2 = 0;
631
+ for (let i=0;i<s.length;++i)
632
+ w2 += x.measureText(s[i]).width;
633
+ for (let j=2;j--;)
634
+ for (let i=0,X=40-w2/2;i<s.length;++i)
635
+ {
636
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
637
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
638
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
639
+ X += w;
640
+ }
641
+
642
+ x.lineWidth = .1 + p*1.9;
643
+ rect(3,Y,73,0); // bottom
644
+ }
649
645
 
646
+ rect(7,15,26,-7,0); // cab top
647
+ rect(25,15,8,25,-1); // cab front
648
+ rect(10,40,15,-25,1); // cab back
649
+ rect(14,21,7,9,2); // cab window
650
+ rect(38,20,6,-6,2); // little stack
651
+
650
652
  // big stack
651
- rect(50,20,10,-8,color(0,1));
652
- rect(50,20,6.5,-8,color(0,2));
653
- rect(50,20,3.5,-8,color(0,3));
654
- rect(50,20,10,-8);
655
- circle(55,2,11.4,.5,PI-.5,color(3,3));
656
- circle(55,2,11.4,.5,PI/2,color(3,2),1);
657
- circle(55,2,11.4,.5,PI-.5);
658
- rect(45,7,20,-7,color(0,2));
659
- rect(45,-1,20,4,color(0,3));
660
- rect(45,-1,20,8);
653
+ rect(49,20,10,-6,0);
654
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
655
+ poly(stackPoints,2,8,6*p);
656
+ rect(44,8,20,-7,0);
661
657
 
662
658
  // engine
663
- for (let i=5; i--;)
664
- {
665
- // stagger radius to fix slight seam
666
- circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
667
- circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
668
- circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
669
- }
659
+ for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
660
+ circle(59,30,4,0,7,2); // light
670
661
 
671
662
  // engine outline
672
- circle(36,30,10,PI/2,PI*3/2);
673
- circle(48,30,10,PI/2,PI*3/2);
674
- circle(60,30,10);
675
- line(36,20,60,20);
676
-
677
- // engine front light
678
- circle(60,30,4,PI,3*PI,color(3,2));
679
- circle(60,30,4,PI,2*PI,color(3,3));
680
- circle(60,30,4,PI,3*PI);
681
-
682
- // front brush
683
- for (let i=6; i--;)
684
- {
685
- x.beginPath();
686
- x.lineTo(53,54);
687
- x.lineTo(53,40);
688
- x.lineTo(53+(1+i*2.9)*p,40);
689
- x.lineTo(53+(4+i*3.5)*p,54);
690
- x.fillStyle = color(0,i%2+2);
691
- x.fill();
692
- i%2 && x.stroke();
693
- }
663
+ rect(35,20,24,0); // top
664
+ circle(59,30,10); // front
665
+ circle(47,30,10,PI/2,PI*3/2); // middle
666
+ circle(35,30,10,PI/2,PI*3/2); // back
667
+ rect(7,40,13,7,-1); // bottom back
668
+ rect(17,40,43,14,-1); // bottom center
694
669
 
695
670
  // wheels
696
- rect(6,40,5,5);
697
- rect(6,40,5,5,color());
698
- rect(15,54,38,-14,color());
699
- for (let i=3; i--;)
700
- for (let j=2; j--;)
701
- {
702
- circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
703
- x.stroke();
704
- circle(15*i+15,47,j?7:1,0,PI,color(i,2));
705
- x.stroke();
706
- }
707
- line(6,40,68,40); // center
708
- line(77,54,4,54); // bottom
709
-
710
- // draw engine name
711
- const s = engineName;
712
- x.font = '900 16px arial';
713
- x.textAlign = 'center';
714
- x.textBaseline = 'top';
715
- x.lineWidth = .1+p*3.9;
716
- let w2 = 0;
717
- for (let i=0; i<s.length; ++i)
718
- w2 += x.measureText(s[i]).width;
719
- for (let j=2; j--;)
720
- for (let i=0, X=41-w2/2; i<s.length; ++i)
671
+ for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
672
+
673
+ // cowcatcher
674
+ for (let i=2;i--;)
721
675
  {
722
- x.fillStyle = color(i,2);
723
- const w = x.measureText(s[i]).width;
724
- x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
725
- X += w;
676
+ let w=6, s=7, o=53+w*p*i
677
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
678
+ poly(points,0,40,14);
726
679
  }
680
+
727
681
  x.restore();
728
682
  }
@@ -204,7 +204,7 @@ class Sound
204
204
 
205
205
  /** Loads a sound from a URL and decodes it into sample data.
206
206
  * @param {string} filename
207
- * @return {Promise<void>} */
207
+ * @return {Promise} */
208
208
  async loadSound(filename)
209
209
  {
210
210
  const response = await fetch(filename);
@@ -262,7 +262,7 @@ class Sound
262
262
  * // Control the individual instance
263
263
  * instance.setVolume(.5);
264
264
  * instance.pause();
265
- * instance.unpause();
265
+ * instance.resume();
266
266
  * instance.stop();
267
267
  */
268
268
  class SoundInstance
@@ -376,7 +376,7 @@ class SoundInstance
376
376
  this.startTime = undefined;
377
377
  }
378
378
 
379
- /** Unpauses this sound instance */
379
+ /** Resume this sound instance */
380
380
  resume()
381
381
  {
382
382
  if (!this.isPaused()) return;
@@ -390,8 +390,8 @@ class SoundInstance
390
390
  */
391
391
  isPlaying() { return !!this.source; }
392
392
 
393
- /** Check if this instance is paused and was not stopped
394
- * @return {boolean} - True if paused
393
+ /** Check if this instance is paused or stopped (not currently playing)
394
+ * @return {boolean} - True if not playing
395
395
  */
396
396
  isPaused() { return !this.isPlaying(); }
397
397
 
@@ -158,7 +158,7 @@ function debugPoint(pos, color, time, angle, screenSpace=false)
158
158
  function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
159
159
  {
160
160
  ASSERT(isVector2(posA), 'posA must be a vec2');
161
- ASSERT(isVector2(posB), 'posB must be sa vec2');
161
+ ASSERT(isVector2(posB), 'posB must be a vec2');
162
162
  ASSERT(isNumber(width), 'width must be a number');
163
163
 
164
164
  const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
package/src/engineDraw.js CHANGED
@@ -190,7 +190,7 @@ class TileInfo
190
190
  }
191
191
 
192
192
  /**
193
- * Returns a tile info for an index using this tile as refrence
193
+ * Returns a tile info for an index using this tile as reference
194
194
  * @param {Vector2|number} [index=0]
195
195
  * @return {TileInfo}
196
196
  */
@@ -729,6 +729,31 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
729
729
  ///////////////////////////////////////////////////////////////////////////////
730
730
  // Drawing utilities
731
731
 
732
+ /** Load a texture at a specific index
733
+ * @param {number} textureIndex - Index to store the texture at
734
+ * @param {string} [src] - Image source path
735
+ * @return {Promise} Promise that resolves when texture is loaded
736
+ * @memberof Draw */
737
+ async function loadTexture(textureIndex, src)
738
+ {
739
+ ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
740
+ ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
741
+ ASSERT(!src || isString(src), 'image src must be a string');
742
+
743
+ const image = new Image;
744
+ if (src)
745
+ {
746
+ await new Promise(resolve =>
747
+ {
748
+ image.onerror = image.onload = resolve;
749
+ image.crossOrigin = 'anonymous';
750
+ image.src = src;
751
+ });
752
+ }
753
+
754
+ textureInfos[textureIndex] = new TextureInfo(image);
755
+ }
756
+
732
757
  /** Convert from screen to world space coordinates
733
758
  * @param {Vector2} screenPos
734
759
  * @return {Vector2}
@@ -1020,7 +1045,7 @@ let engineFontImage;
1020
1045
  class FontImage
1021
1046
  {
1022
1047
  /** Create an image font
1023
- * @param {TileInfo} tileInfo - Tile info of first characeter in font
1048
+ * @param {TileInfo} tileInfo - Tile info of first character in font
1024
1049
  */
1025
1050
  constructor(tileInfo)
1026
1051
  {
@@ -1109,21 +1134,18 @@ class FontImage
1109
1134
  }
1110
1135
 
1111
1136
  // load engine font, called automatically on startup
1112
- function fontImageInit()
1137
+ async function fontImageInit()
1113
1138
  {
1114
- return new Promise(resolve =>
1139
+ const image = new Image;
1140
+ await new Promise(resolve =>
1115
1141
  {
1116
- // create the engine font
1117
- const image = new Image;
1118
- image.onerror = image.onload = ()=>
1119
- {
1120
- const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
1121
- const textureInfo = new TextureInfo(image);
1122
- const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
1123
- engineFontImage = new FontImage(tileInfo);
1124
- resolve();
1125
- }
1142
+ image.onerror = image.onload = resolve;
1126
1143
  image.crossOrigin = 'anonymous';
1127
1144
  image.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAeAQMAAABnrVXaAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAjpJREFUOMu9kzFu2zAUhn+CAROgqrk+B2l0BWYxMjlXeYaAtFtbdA1sGgHqRQfI0CNkSG5AwYB0BQ8d5Bsomwah6CPVeGg6tEPzAxLwyI+P78cP4u9lNO9OoMKnLMOobG5020/yaj/MrRcCGh1gBbyiLTPJEYaIiom5KM9Jq7KgynMGtb6L4GL4MF2H4LQKCXTvDVw2I4MsgZT7QLExdiutH+D08VOP3INXRrWX1/mmpbkNgAPYRVANb4xpcegYvhiNbIXauQICEjBuYLfMakaakWQeXxiZ0VDtuJCKs3ztMV59QtsHJNcRxDzfdL21ty3PrfIcXTN+E+GFAv6T5nbT9jd50/WFxb5ksdAv49qS6ouymG66ji08UMT6moykYLAo+V0j23GN4m829ZySAD5K7QsBfQTvOG8eE+gTeGYRAmnNAubN3hf5Zv9tJWDHp/VTuaSm7SN4fyINQqaNO3RMVxvpSPXnOChnRNvFcGY0gnwiPswYwTKVPE0zVtX3mTEIOoFzaqLrGuJaV+Uqumb71fVk/VoOH3cdLNQP/FHi8hV0CQNoqBZsUPlLPMsdCJro9QAaQQ0woDy9BJm0eTxCFnO9srcYlhNVlfR2EyTrph1uUtbUtAJifwRgrKuYdXVHeb0YI3QpawohQHkloI3J5FuVwI5ORxC9k2Tuz9Ir1IjgeIPGMHYkAZe2RuYkmWFmt3gGbTPOmBUWVTmRmHtGrfpzG/yuQNOKa6gBB/WA9khitPgl6/GP+gl2Af6tCbvaygAAAABJRU5ErkJggg==';
1128
1145
  });
1146
+
1147
+ const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
1148
+ const textureInfo = new TextureInfo(image);
1149
+ const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
1150
+ engineFontImage = new FontImage(tileInfo);
1129
1151
  }
@@ -177,6 +177,8 @@ export
177
177
  saveCanvas,
178
178
  saveDataURL,
179
179
  shareURL,
180
+ readSaveData,
181
+ writeSaveData,
180
182
 
181
183
  // Random
182
184
  rand,
@@ -769,7 +769,7 @@ function inputRender()
769
769
  }
770
770
  }
771
771
 
772
- // center position for right tocuh pad face buttons
772
+ // center position for right touch pad face buttons
773
773
  function touchGamepadButtonCenter()
774
774
  {
775
775
  const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
package/src/engineMath.js CHANGED
@@ -267,7 +267,9 @@ function wave(frequency=1, amplitude=1, t=time, offset=0)
267
267
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
268
268
 
269
269
  /**
270
- * Check if object is a valid string or can be converted to one
270
+ * Check if object can be converted to a string (has a toString method)
271
+ * - Returns true for strings, numbers, and most objects
272
+ * - Returns false for null and undefined
271
273
  * @param {any} s
272
274
  * @return {boolean}
273
275
  * @memberof Math */
@@ -344,7 +346,7 @@ function lineTest(posStart, posEnd, testFunction, normal)
344
346
  if (stepX < 0)
345
347
  hitPos.x -= e;
346
348
  }
347
- if (stepY < 0)
349
+ else if (stepY < 0)
348
350
  hitPos.y -= e;
349
351
 
350
352
  // set normal
@@ -1051,7 +1053,7 @@ class Color
1051
1053
  * @memberof Math */
1052
1054
  const WHITE = debugProtectConstant(rgb());
1053
1055
 
1054
- /** Color - Clear White #757474ff with 0 alpha
1056
+ /** Color - Clear White #ffffff00 with 0 alpha
1055
1057
  * @type {Color}
1056
1058
  * @memberof Math */
1057
1059
  const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
@@ -1061,7 +1063,7 @@ const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
1061
1063
  * @memberof Math */
1062
1064
  const BLACK = debugProtectConstant(rgb(0,0,0));
1063
1065
 
1064
- /** Color - Clear Black #000000 with 0 alpha
1066
+ /** Color - Clear Black #00000000 with 0 alpha
1065
1067
  * @type {Color}
1066
1068
  * @memberof Math */
1067
1069
  const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
@@ -66,17 +66,19 @@ class EngineObject
66
66
  this.additiveColor = undefined;
67
67
  /** @property {boolean} - Should it flip along y axis when rendered */
68
68
  this.mirror = false;
69
+ /** @property {boolean} - Has object been destroyed? */
70
+ this.destroyed = false;
69
71
 
70
72
  // physical properties
71
- /** @property {number} [mass=objectDefaultMass] - How heavy the object is, static if 0 */
73
+ /** @property {number} - How heavy the object is, static if 0 */
72
74
  this.mass = objectDefaultMass;
73
- /** @property {number} [damping=objectDefaultDamping] - How much to slow down velocity each frame (0-1) */
75
+ /** @property {number} - How much to slow down velocity each frame (0-1) */
74
76
  this.damping = objectDefaultDamping;
75
- /** @property {number} [angleDamping=objectDefaultAngleDamping] - How much to slow down rotation each frame (0-1) */
77
+ /** @property {number} - How much to slow down rotation each frame (0-1) */
76
78
  this.angleDamping = objectDefaultAngleDamping;
77
- /** @property {number} [restitution=objectDefaultRestitution] - How bouncy the object is when colliding (0-1) */
79
+ /** @property {number} - How bouncy the object is when colliding (0-1) */
78
80
  this.restitution = objectDefaultRestitution;
79
- /** @property {number} [friction=objectDefaultFriction] - How much friction to apply when sliding (0-1) */
81
+ /** @property {number} - How much friction to apply when sliding (0-1) */
80
82
  this.friction = objectDefaultFriction;
81
83
  /** @property {number} - How much to scale gravity by for this object */
82
84
  this.gravityScale = 1;
@@ -285,10 +287,10 @@ class EngineObject
285
287
  if (!tileCollisionTest(oldPos, this.size, this))
286
288
  {
287
289
  // test which side we bounced off (or both if a corner)
288
- const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
289
- const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
290
-
291
- if (blockedLayerX)
290
+ const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
291
+ const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
292
+ const restitution = max(this.restitution, hitLayer.restitution);
293
+ if (isBlockedX)
292
294
  {
293
295
  // try to move up a tiny bit
294
296
  const epsilon = 1e-3;
@@ -304,16 +306,12 @@ class EngineObject
304
306
  return;
305
307
  }
306
308
 
307
- // move to previous position and bounce
309
+ // move to previous X position and bounce
308
310
  this.pos.x = oldPos.x;
309
- this.velocity.x *= -this.restitution;
311
+ this.velocity.x *= -restitution;
310
312
  }
311
- if (blockedLayerY || !blockedLayerX)
313
+ if (isBlockedY || !isBlockedX)
312
314
  {
313
- // bounce velocity
314
- const restitution = max(this.restitution, hitLayer.restitution);
315
- this.velocity.y *= -restitution;
316
-
317
315
  if (wasFalling)
318
316
  {
319
317
  // adjust position to slightly away from nearest tile
@@ -329,10 +327,12 @@ class EngineObject
329
327
  }
330
328
  else
331
329
  {
332
- // move to previous position
330
+ // move to previous Y position
333
331
  this.pos.y = oldPos.y;
334
332
  this.groundObject = undefined;
335
333
  }
334
+ // bounce velocity
335
+ this.velocity.y *= -restitution;
336
336
  }
337
337
  debugPhysics && debugRect(this.pos, this.size, '#f00');
338
338
  }
@@ -350,19 +350,19 @@ class EngineObject
350
350
  drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
351
351
  }
352
352
 
353
- /** Destroy this object, destroy its children, detach its parent, and mark it for removal */
354
- destroy()
353
+ /** Destroy this object, destroy its children, detach its parent, and mark it for removal
354
+ * @param {boolean} [immediate] - should attached effects be allowed to die off? */
355
+ destroy(immediate=false)
355
356
  {
356
- if (this.destroyed)
357
- return;
357
+ if (this.destroyed) return;
358
358
 
359
359
  // disconnect from parent and destroy children
360
- this.destroyed = 1;
360
+ this.destroyed = true;
361
361
  this.parent?.removeChild(this);
362
362
  for (const child of this.children)
363
363
  {
364
364
  child.parent = undefined;
365
- child.destroy();
365
+ child.destroy(immediate);
366
366
  }
367
367
  }
368
368
 
@@ -461,7 +461,7 @@ class EngineObject
461
461
  }
462
462
 
463
463
  /** Check if overlapping another engine object
464
- * Collisions are resoloved to prevent overlaps
464
+ * Collisions are resolved to prevent overlaps
465
465
  * @param {EngineObject} object
466
466
  * @return {boolean} */
467
467
  isOverlappingObject(object)