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.
- package/LICENSE +10 -10
- package/dist/littlejs.d.ts +254 -171
- package/dist/littlejs.esm.js +892 -628
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +889 -626
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +885 -622
- package/examples/box2d/gameObjects.js +3 -3
- package/examples/electron/index.html +2 -2
- package/examples/index.html +10 -9
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/module/build.bat +7 -0
- package/examples/module/build.js +121 -0
- package/examples/particles/index.html +1 -1
- package/examples/platformer/game.js +1 -1
- package/examples/platformer/gameEffects.js +5 -10
- package/examples/platformer/gameLevel.js +3 -3
- package/examples/puzzle/game.js +2 -2
- package/examples/shorts/base.html +4 -1
- package/examples/shorts/box2dPool.js +2 -2
- package/examples/shorts/box2dTileLayer.js +48 -0
- package/examples/shorts/colors.js +1 -1
- package/examples/shorts/fps.js +1 -1
- package/examples/shorts/music.js +5 -4
- package/examples/shorts/musicPlayer.js +4 -3
- package/examples/shorts/parallax.js +2 -2
- package/examples/shorts/tiltedView.js +1 -1
- package/examples/starter/index.html +2 -2
- package/package.json +5 -5
- package/plugins/box2d.js +166 -63
- package/plugins/pluginExport.js +1 -1
- package/plugins/postProcess.js +1 -1
- package/plugins/uiSystem.js +33 -20
- package/plugins/zzfxm.js +2 -2
- package/reference.md +9 -8
- package/src/engine.js +111 -134
- package/src/engineAudio.js +44 -58
- package/src/engineDebug.js +4 -4
- package/src/engineDraw.js +7 -4
- package/src/engineExport.js +2 -1
- package/src/engineInput.js +5 -5
- package/src/engineMath.js +9 -9
- package/src/engineObject.js +25 -25
- package/src/engineParticles.js +241 -118
- package/src/engineTileLayer.js +117 -88
- package/src/engineUtilities.js +105 -78
- package/src/engineWebGL.js +2 -2
- package/plugins/Box2D_License.txt +0 -17
package/dist/littlejs.esm.js
CHANGED
|
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
|
|
|
33
33
|
* @type {string}
|
|
34
34
|
* @default
|
|
35
35
|
* @memberof Engine */
|
|
36
|
-
const engineVersion = '1.17.
|
|
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
|
-
//
|
|
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
|
-
* ()
|
|
157
|
-
* ()
|
|
158
|
-
* ()
|
|
159
|
-
* ()
|
|
160
|
-
* ()
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)*.
|
|
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
|
-
|
|
588
|
-
|
|
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.
|
|
592
|
-
|
|
593
|
-
|
|
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.
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
x.
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
const color = (c
|
|
612
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
rect(
|
|
641
|
-
|
|
642
|
-
// cab
|
|
643
|
-
rect(
|
|
644
|
-
|
|
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(
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
rect(
|
|
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;
|
|
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
|
-
|
|
675
|
-
circle(
|
|
676
|
-
circle(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
//
|
|
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
|
-
//
|
|
685
|
-
for (let 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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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}
|
|
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}
|
|
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: ()
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
|
|
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
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
{
|
|
2670
|
-
|
|
2671
|
-
|
|
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
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
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}
|
|
3403
|
+
/** @property {number} - How heavy the object is, static if 0 */
|
|
3396
3404
|
this.mass = objectDefaultMass;
|
|
3397
|
-
/** @property {number}
|
|
3405
|
+
/** @property {number} - How much to slow down velocity each frame (0-1) */
|
|
3398
3406
|
this.damping = objectDefaultDamping;
|
|
3399
|
-
/** @property {number}
|
|
3407
|
+
/** @property {number} - How much to slow down rotation each frame (0-1) */
|
|
3400
3408
|
this.angleDamping = objectDefaultAngleDamping;
|
|
3401
|
-
/** @property {number}
|
|
3409
|
+
/** @property {number} - How bouncy the object is when colliding (0-1) */
|
|
3402
3410
|
this.restitution = objectDefaultRestitution;
|
|
3403
|
-
/** @property {number}
|
|
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
|
|
3613
|
-
const
|
|
3614
|
-
|
|
3615
|
-
if (
|
|
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 *= -
|
|
3641
|
+
this.velocity.x *= -restitution;
|
|
3634
3642
|
}
|
|
3635
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
5383
|
-
document.addEventListener('touchmove', (e)
|
|
5384
|
-
document.addEventListener('touchend', (e)
|
|
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
|
|
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
|
|
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
|
-
* //
|
|
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
|
|
5819
|
+
* // play a sound
|
|
5804
5820
|
* sound_example.play();
|
|
5805
5821
|
*/
|
|
5806
5822
|
class Sound
|
|
5807
5823
|
{
|
|
5808
|
-
/**
|
|
5809
|
-
*
|
|
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(
|
|
5837
|
+
constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
|
|
5814
5838
|
{
|
|
5815
5839
|
if (!soundEnable || headlessMode) return;
|
|
5816
5840
|
|
|
5817
|
-
ASSERT(!
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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}
|
|
6471
|
-
* @param {Vector2}
|
|
6472
|
-
* @param {EngineObject} [
|
|
6473
|
-
* @param {boolean}
|
|
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(),
|
|
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,
|
|
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
|
|
6488
|
-
* @param {Vector2}
|
|
6489
|
-
* @param {Vector2}
|
|
6490
|
-
* @param {EngineObject} [
|
|
6491
|
-
* @param {Vector2}
|
|
6492
|
-
* @param {boolean}
|
|
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,
|
|
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,
|
|
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} [
|
|
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(
|
|
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(
|
|
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}
|
|
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(
|
|
6749
|
+
constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
|
|
6742
6750
|
{
|
|
6743
6751
|
const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
|
|
6744
|
-
|
|
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.
|
|
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
|
|
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
|
|
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}
|
|
6989
|
-
* @param {Vector2} size
|
|
6990
|
-
* @param {TileInfo} [tileInfo]
|
|
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(
|
|
7006
|
+
constructor(pos, size, tileInfo=tile(), renderOrder=0, useWebGL=true)
|
|
6994
7007
|
{
|
|
6995
|
-
super(
|
|
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
|
|
7032
|
-
* @param {Vector2}
|
|
7044
|
+
/** Set tile collision data for a given cell in the layer
|
|
7045
|
+
* @param {Vector2} layerPos
|
|
7033
7046
|
* @param {number} [data] */
|
|
7034
|
-
setCollisionData(
|
|
7047
|
+
setCollisionData(layerPos, data=1)
|
|
7035
7048
|
{
|
|
7036
|
-
ASSERT(isVector2(
|
|
7037
|
-
const i = (
|
|
7038
|
-
|
|
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
|
-
/**
|
|
7042
|
-
* @param {Vector2}
|
|
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(
|
|
7062
|
+
getCollisionData(layerPos)
|
|
7045
7063
|
{
|
|
7046
|
-
ASSERT(isVector2(
|
|
7047
|
-
const i = (
|
|
7048
|
-
return
|
|
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,
|
|
7074
|
+
collisionTest(pos, size=new Vector2, callbackObject)
|
|
7057
7075
|
{
|
|
7058
7076
|
ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
|
|
7059
|
-
ASSERT(!
|
|
7060
|
-
|
|
7061
|
-
//
|
|
7062
|
-
const
|
|
7063
|
-
|
|
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}
|
|
7087
|
-
* @param {Vector2}
|
|
7088
|
-
* @param {EngineObject} [
|
|
7089
|
-
* @param {Vector2}
|
|
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,
|
|
7111
|
+
collisionRaycast(posStart, posEnd, callbackObject, normal)
|
|
7092
7112
|
{
|
|
7093
7113
|
ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
|
|
7094
|
-
ASSERT(!
|
|
7114
|
+
ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
|
|
7095
7115
|
|
|
7096
|
-
|
|
7097
|
-
const collisionTest =
|
|
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
|
-
|
|
7100
|
-
|
|
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
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
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
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
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
|
|
7151
|
+
* @callback ParticleCallback - Function that processes a particle
|
|
7124
7152
|
* @param {Particle} particle
|
|
7125
|
-
* @memberof
|
|
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
|
|
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}
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 {
|
|
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 {
|
|
7269
|
-
this.
|
|
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 (
|
|
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 (
|
|
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 =
|
|
7326
|
-
randInCircle(this.emitSize/2)
|
|
7327
|
-
: vec2(rand(-.5,.5), rand(-.5,.5))
|
|
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
|
|
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
|
|
7353
|
-
|
|
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
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
}
|
|
7362
|
-
particle
|
|
7363
|
-
|
|
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
|
-
|
|
7380
|
-
|
|
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
|
-
* @
|
|
7387
|
-
* @memberof Engine
|
|
7454
|
+
* @memberof Particles
|
|
7388
7455
|
*/
|
|
7389
|
-
class Particle
|
|
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 {
|
|
7395
|
-
* @param {
|
|
7396
|
-
* @param {number}
|
|
7397
|
-
* @param {Color}
|
|
7398
|
-
* @param {Color}
|
|
7399
|
-
* @param {number}
|
|
7400
|
-
* @param {number}
|
|
7401
|
-
* @param {number}
|
|
7402
|
-
* @param {
|
|
7403
|
-
* @param {
|
|
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(
|
|
7409
|
-
)
|
|
7472
|
+
constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
|
|
7410
7473
|
{
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
/** @property {
|
|
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}
|
|
7486
|
+
/** @property {Color} */
|
|
7416
7487
|
this.colorEnd = colorEnd;
|
|
7417
|
-
/** @property {number}
|
|
7488
|
+
/** @property {number} */
|
|
7418
7489
|
this.lifeTime = lifeTime;
|
|
7419
|
-
/** @property {number}
|
|
7490
|
+
/** @property {number} */
|
|
7420
7491
|
this.sizeStart = sizeStart;
|
|
7421
|
-
/** @property {number}
|
|
7492
|
+
/** @property {number} */
|
|
7422
7493
|
this.sizeEnd = sizeEnd;
|
|
7423
|
-
/** @property {
|
|
7424
|
-
this.
|
|
7425
|
-
/** @property {
|
|
7426
|
-
this.
|
|
7427
|
-
/** @property {number}
|
|
7428
|
-
this.
|
|
7429
|
-
/** @property {
|
|
7430
|
-
this.
|
|
7431
|
-
/** @property {
|
|
7432
|
-
this.
|
|
7433
|
-
|
|
7434
|
-
this.
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7443
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
7455
|
-
const
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
7628
|
+
additive && setBlendMode(true);
|
|
7482
7629
|
|
|
7483
7630
|
// update the position and angle for drawing
|
|
7484
|
-
|
|
7485
|
-
|
|
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 =
|
|
7636
|
+
const a = emitter.angle;
|
|
7489
7637
|
const c = cos(a), s = sin(a);
|
|
7490
|
-
pos
|
|
7491
|
-
|
|
7492
|
-
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 (
|
|
7642
|
+
if (trailScale)
|
|
7495
7643
|
{
|
|
7496
7644
|
// trail style particles
|
|
7497
|
-
const
|
|
7498
|
-
this.velocity.rotate(-
|
|
7499
|
-
|
|
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 *
|
|
7651
|
+
const trailLength = speed * trailScale;
|
|
7505
7652
|
size.y = max(size.x, trailLength);
|
|
7506
|
-
angle = atan2(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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}
|
|
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} -
|
|
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
|
|
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
|
|
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
|
|
10280
|
-
const
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
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), '
|
|
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}
|
|
10321
|
-
* @param {Vector2}
|
|
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
|
-
|
|
10674
|
+
/** @property {Color} - Line color used for default box2d drawing */
|
|
10512
10675
|
this.lineColor = BLACK;
|
|
10513
|
-
box2d
|
|
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}
|
|
10556
|
-
* @param {Color}
|
|
10557
|
-
* @param {number}
|
|
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 {
|
|
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}
|
|
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
|
-
|
|
13157
|
+
Box2dKinematicObject,
|
|
12894
13158
|
Box2dRaycastResult,
|
|
12895
13159
|
Box2dJoint,
|
|
12896
13160
|
Box2dTargetJoint,
|