littlejsengine 1.17.5 → 1.17.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -10
- package/dist/littlejs.d.ts +160 -88
- package/dist/littlejs.esm.js +586 -426
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +583 -425
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +582 -424
- package/examples/electron/index.html +2 -2
- package/examples/index.html +2 -2
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/particles/index.html +1 -1
- package/examples/platformer/gameEffects.js +3 -3
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/colors.js +1 -1
- package/examples/shorts/fps.js +1 -1
- package/examples/shorts/music.js +3 -2
- package/examples/shorts/musicPlayer.js +2 -1
- package/examples/shorts/parallax.js +2 -2
- package/examples/shorts/tiltedView.js +1 -1
- package/examples/starter/index.html +2 -2
- package/package.json +1 -1
- package/plugins/box2d.js +6 -7
- package/plugins/pluginExport.js +1 -1
- package/plugins/postProcess.js +1 -1
- package/plugins/uiSystem.js +30 -17
- package/reference.md +6 -6
- package/src/engine.js +103 -127
- package/src/engineDraw.js +2 -2
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +1 -1
- package/src/engineObject.js +24 -24
- package/src/engineParticles.js +241 -118
- package/src/engineTileLayer.js +66 -48
- package/src/engineUtilities.js +105 -78
- 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}
|
|
@@ -464,13 +464,13 @@ function engineObjectsUpdate()
|
|
|
464
464
|
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
465
465
|
|
|
466
466
|
// recursive object update
|
|
467
|
-
function
|
|
467
|
+
function updateChildObject(o)
|
|
468
468
|
{
|
|
469
469
|
if (o.destroyed) return;
|
|
470
470
|
|
|
471
471
|
o.update();
|
|
472
472
|
for (const child of o.children)
|
|
473
|
-
|
|
473
|
+
updateChildObject(child);
|
|
474
474
|
}
|
|
475
475
|
for (const o of engineObjects)
|
|
476
476
|
{
|
|
@@ -480,7 +480,7 @@ function engineObjectsUpdate()
|
|
|
480
480
|
o.update();
|
|
481
481
|
o.updatePhysics();
|
|
482
482
|
for (const child of o.children)
|
|
483
|
-
|
|
483
|
+
updateChildObject(child);
|
|
484
484
|
o.updateTransforms();
|
|
485
485
|
}
|
|
486
486
|
|
|
@@ -489,11 +489,14 @@ function engineObjectsUpdate()
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
/** Destroy and remove all objects
|
|
492
|
+
* - This can be used to clear out all objects when restarting a level
|
|
493
|
+
* - Objects can override their destroy function to do cleanup or stick around
|
|
494
|
+
* @param {boolean} [immediate] - should attached effects be allowed to die off?
|
|
492
495
|
* @memberof Engine */
|
|
493
|
-
function engineObjectsDestroy()
|
|
496
|
+
function engineObjectsDestroy(immediate=true)
|
|
494
497
|
{
|
|
495
498
|
for (const o of engineObjects)
|
|
496
|
-
o.parent || o.destroy();
|
|
499
|
+
o.parent || o.destroy(immediate);
|
|
497
500
|
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
498
501
|
}
|
|
499
502
|
|
|
@@ -568,165 +571,140 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
568
571
|
///////////////////////////////////////////////////////////////////////////////
|
|
569
572
|
function drawEngineLogo(t)
|
|
570
573
|
{
|
|
574
|
+
const blackAndWhite = 0;
|
|
575
|
+
const showName = 1;
|
|
576
|
+
|
|
571
577
|
// LittleJS Logo and Splash Screen
|
|
572
578
|
const x = mainContext;
|
|
573
579
|
const w = mainCanvas.width = innerWidth;
|
|
574
580
|
const h = mainCanvas.height = innerHeight;
|
|
575
|
-
|
|
576
581
|
{
|
|
577
582
|
// background
|
|
578
583
|
const p3 = percent(t, 1, .8);
|
|
579
584
|
const p4 = percent(t, 0, .5);
|
|
580
|
-
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.
|
|
585
|
+
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
|
|
581
586
|
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
582
587
|
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
583
588
|
x.save();
|
|
584
589
|
x.fillStyle = g;
|
|
585
590
|
x.fillRect(0,0,w,h);
|
|
586
591
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
592
|
+
const gradient = (X1,Y1,X2,Y2,C,S=1)=>
|
|
593
|
+
{
|
|
594
|
+
if (C >= 0)
|
|
595
|
+
{
|
|
596
|
+
if (blackAndWhite)
|
|
597
|
+
x.fillStyle = '#fff';
|
|
598
|
+
else
|
|
599
|
+
{
|
|
600
|
+
const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
|
|
601
|
+
g.addColorStop(0,color(C,2));
|
|
602
|
+
g.addColorStop(1,color(C,1));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else
|
|
606
|
+
x.fillStyle = '#000';
|
|
607
|
+
C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
|
|
608
|
+
}
|
|
609
|
+
const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
|
|
590
610
|
{
|
|
591
611
|
x.beginPath();
|
|
592
|
-
x.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const line = (X, Y, Z, W)=>
|
|
612
|
+
x.arc(X,Y,R,p*A,p*B);
|
|
613
|
+
gradient(X,Y-R,X,Y+R,C,S);
|
|
614
|
+
}
|
|
615
|
+
const rect = (X,Y,W,H,C)=>
|
|
597
616
|
{
|
|
598
617
|
x.beginPath();
|
|
599
|
-
x.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const circle = (X, Y, R, A=0, B=2*PI, C, F)=>
|
|
618
|
+
x.rect(X,Y,W,H*p);
|
|
619
|
+
gradient(X,Y+H,X+W,Y,C);
|
|
620
|
+
}
|
|
621
|
+
const poly = (points,C,Y,H)=>
|
|
604
622
|
{
|
|
605
|
-
const D = (A+B)/2, E = p*(B-A)/2;
|
|
606
623
|
x.beginPath();
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
x.
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
const color = (c
|
|
613
|
-
|
|
624
|
+
for (const p of points)
|
|
625
|
+
x.lineTo(p.x, p.y);
|
|
626
|
+
x.closePath();
|
|
627
|
+
gradient(0, Y, 0, Y+H,C);
|
|
628
|
+
}
|
|
629
|
+
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
|
|
630
|
+
|
|
631
|
+
// center and fit tos screen
|
|
614
632
|
const alpha = wave(1,1,t);
|
|
615
633
|
const p = percent(alpha, .1, .5);
|
|
616
|
-
|
|
617
|
-
// setup
|
|
634
|
+
const size = min(6, min(w,h)/99);
|
|
618
635
|
x.translate(w/2,h/2);
|
|
619
|
-
const size = min(6, min(w,h)/99); // fit to screen
|
|
620
636
|
x.scale(size,size);
|
|
621
637
|
x.translate(-40,-35);
|
|
638
|
+
p < 1 && x.setLineDash([99*p,99]);
|
|
622
639
|
x.lineJoin = x.lineCap = 'round';
|
|
623
640
|
x.lineWidth = .1 + p*1.9;
|
|
641
|
+
//x.strokeStyle='#fff7';
|
|
642
|
+
|
|
643
|
+
if (showName)
|
|
644
|
+
{
|
|
645
|
+
// engine name text
|
|
646
|
+
const Y = 54;
|
|
647
|
+
const s = 'LittleJS';
|
|
648
|
+
x.font = '900 15.5px arial';
|
|
649
|
+
x.lineWidth = .1+p*3.9;
|
|
650
|
+
x.textAlign = 'center';
|
|
651
|
+
x.textBaseline = 'top';
|
|
652
|
+
rect(11,Y+2,59,8*p,-1);
|
|
653
|
+
x.beginPath();
|
|
624
654
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
rect(
|
|
642
|
-
|
|
643
|
-
// cab
|
|
644
|
-
rect(
|
|
645
|
-
|
|
646
|
-
rect(21,21,-6,9);
|
|
647
|
-
|
|
648
|
-
// little stack
|
|
649
|
-
rect(37,14,9,6,color(3,2));
|
|
650
|
-
rect(37,14,4.5,6,color(3,3));
|
|
651
|
-
rect(37,14,9,6);
|
|
655
|
+
let w2 = 0;
|
|
656
|
+
for (let i=0;i<s.length;++i)
|
|
657
|
+
w2 += x.measureText(s[i]).width;
|
|
658
|
+
for (let j=2;j--;)
|
|
659
|
+
for (let i=0,X=40-w2/2;i<s.length;++i)
|
|
660
|
+
{
|
|
661
|
+
const w = x.measureText(s[i]).width, X2 = X+w/2;
|
|
662
|
+
gradient(X2,Y,X2+2,Y+13,i>5?1:0);
|
|
663
|
+
x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
|
|
664
|
+
X += w;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
x.lineWidth = .1 + p*1.9;
|
|
668
|
+
rect(3,Y,73,0); // bottom
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
rect(7,15,26,-7,0); // cab top
|
|
672
|
+
rect(25,15,8,25,-1); // cab front
|
|
673
|
+
rect(10,40,15,-25,1); // cab back
|
|
674
|
+
rect(14,21,7,9,2); // cab window
|
|
675
|
+
rect(38,15,6,6,2); // little stack
|
|
652
676
|
|
|
653
677
|
// big stack
|
|
654
|
-
rect(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
rect(
|
|
658
|
-
circle(55,2,11.4,.5,PI-.5,color(3,3));
|
|
659
|
-
circle(55,2,11.4,.5,PI/2,color(3,2),1);
|
|
660
|
-
circle(55,2,11.4,.5,PI-.5);
|
|
661
|
-
rect(45,7,20,-7,color(0,2));
|
|
662
|
-
rect(45,-1,20,4,color(0,3));
|
|
663
|
-
rect(45,-1,20,8);
|
|
678
|
+
rect(49,20,10,-6,0);
|
|
679
|
+
const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
|
|
680
|
+
poly(stackPoints,2,8,6*p);
|
|
681
|
+
rect(44,8,20,-7,0);
|
|
664
682
|
|
|
665
683
|
// engine
|
|
666
|
-
for (let i=5;
|
|
667
|
-
|
|
668
|
-
// stagger radius to fix slight seam
|
|
669
|
-
circle(60-i*6,30, 9.9,0,2*PI,color(i+2,3));
|
|
670
|
-
circle(60-i*6,30,10.0,-.5,PI+.5,color(i+2,2));
|
|
671
|
-
circle(60-i*6,30,10.1,.5,PI-.5,color(i+2,1));
|
|
672
|
-
}
|
|
684
|
+
for (let i=5;i--;) circle(59-i*6,30,10,0,9,1,0);
|
|
685
|
+
circle(59,30,4,0,9,2); // light
|
|
673
686
|
|
|
674
687
|
// engine outline
|
|
675
|
-
|
|
676
|
-
circle(
|
|
677
|
-
circle(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
//
|
|
681
|
-
circle(60,30,4,PI,3*PI,color(3,2));
|
|
682
|
-
circle(60,30,4,PI,2*PI,color(3,3));
|
|
683
|
-
circle(60,30,4,PI,3*PI);
|
|
688
|
+
rect(35,20,24,0); // top
|
|
689
|
+
circle(59,30,10); // front
|
|
690
|
+
circle(47,30,10,PI/2,PI*3/2); // middle
|
|
691
|
+
circle(35,30,10,PI/2,PI*3/2); // back
|
|
692
|
+
rect(7,40,13,7,-1); // bottom back
|
|
693
|
+
rect(17,40,43,14,-1); // bottom center
|
|
684
694
|
|
|
685
|
-
//
|
|
686
|
-
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--;)
|
|
687
702
|
{
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
x.lineTo(53+(1+i*2.9)*p,40);
|
|
692
|
-
x.lineTo(53+(4+i*3.5)*p,54);
|
|
693
|
-
x.fillStyle = color(0,i%2+2);
|
|
694
|
-
x.fill();
|
|
695
|
-
i%2 && x.stroke();
|
|
703
|
+
let w=6, s=7, o=53+w*p*i
|
|
704
|
+
const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
|
|
705
|
+
poly(points,0,40,14);
|
|
696
706
|
}
|
|
697
707
|
|
|
698
|
-
// wheels
|
|
699
|
-
rect(6,40,5,5);
|
|
700
|
-
rect(6,40,5,5,color());
|
|
701
|
-
rect(15,54,38,-14,color());
|
|
702
|
-
for (let i=3; i--;)
|
|
703
|
-
for (let j=2; j--;)
|
|
704
|
-
{
|
|
705
|
-
circle(15*i+15,47,j?7:1,PI,3*PI,color(i,3));
|
|
706
|
-
x.stroke();
|
|
707
|
-
circle(15*i+15,47,j?7:1,0,PI,color(i,2));
|
|
708
|
-
x.stroke();
|
|
709
|
-
}
|
|
710
|
-
line(6,40,68,40); // center
|
|
711
|
-
line(77,54,4,54); // bottom
|
|
712
|
-
|
|
713
|
-
// draw engine name
|
|
714
|
-
const s = engineName;
|
|
715
|
-
x.font = '900 16px arial';
|
|
716
|
-
x.textAlign = 'center';
|
|
717
|
-
x.textBaseline = 'top';
|
|
718
|
-
x.lineWidth = .1+p*3.9;
|
|
719
|
-
let w2 = 0;
|
|
720
|
-
for (let i=0; i<s.length; ++i)
|
|
721
|
-
w2 += x.measureText(s[i]).width;
|
|
722
|
-
for (let j=2; j--;)
|
|
723
|
-
for (let i=0, X=41-w2/2; i<s.length; ++i)
|
|
724
|
-
{
|
|
725
|
-
x.fillStyle = color(i,2);
|
|
726
|
-
const w = x.measureText(s[i]).width;
|
|
727
|
-
x[j?'strokeText':'fillText'](s[i],X+w/2,55.5,17*p);
|
|
728
|
-
X += w;
|
|
729
|
-
}
|
|
730
708
|
x.restore();
|
|
731
709
|
}
|
|
732
710
|
/**
|
|
@@ -2565,6 +2543,90 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
2565
2543
|
* @namespace Utilities
|
|
2566
2544
|
*/
|
|
2567
2545
|
|
|
2546
|
+
/**
|
|
2547
|
+
* Timer object tracks how long has passed since it was set
|
|
2548
|
+
* @memberof Engine
|
|
2549
|
+
* @example
|
|
2550
|
+
* let a = new Timer; // creates a timer that is not set
|
|
2551
|
+
* a.set(3); // sets the timer to 3 seconds
|
|
2552
|
+
*
|
|
2553
|
+
* let b = new Timer(1); // creates a timer with 1 second left
|
|
2554
|
+
* b.unset(); // unset the timer
|
|
2555
|
+
*/
|
|
2556
|
+
class Timer
|
|
2557
|
+
{
|
|
2558
|
+
/** Create a timer object set time passed in
|
|
2559
|
+
* @param {number} [timeLeft] - How much time left before the timer
|
|
2560
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
2561
|
+
constructor(timeLeft, useRealTime=false)
|
|
2562
|
+
{
|
|
2563
|
+
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
2564
|
+
this.useRealTime = useRealTime;
|
|
2565
|
+
const globalTime = this.getGlobalTime();
|
|
2566
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
2567
|
+
this.setTime = timeLeft;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/** Set the timer with seconds passed in
|
|
2571
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
2572
|
+
set(timeLeft=0)
|
|
2573
|
+
{
|
|
2574
|
+
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
2575
|
+
const globalTime = this.getGlobalTime();
|
|
2576
|
+
this.time = globalTime + timeLeft;
|
|
2577
|
+
this.setTime = timeLeft;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
/** Set if the timer should keep running even when the game is paused
|
|
2581
|
+
* @param {boolean} [useRealTime] */
|
|
2582
|
+
setUseRealTime(useRealTime=true)
|
|
2583
|
+
{
|
|
2584
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
2585
|
+
this.useRealTime = useRealTime;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
/** Unset the timer */
|
|
2589
|
+
unset() { this.time = undefined; }
|
|
2590
|
+
|
|
2591
|
+
/** Returns true if set
|
|
2592
|
+
* @return {boolean} */
|
|
2593
|
+
isSet() { return this.time !== undefined; }
|
|
2594
|
+
|
|
2595
|
+
/** Returns true if set and has not elapsed
|
|
2596
|
+
* @return {boolean} */
|
|
2597
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
2598
|
+
|
|
2599
|
+
/** Returns true if set and elapsed
|
|
2600
|
+
* @return {boolean} */
|
|
2601
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
2602
|
+
|
|
2603
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2604
|
+
* @return {number} */
|
|
2605
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2606
|
+
|
|
2607
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2608
|
+
* @return {number} */
|
|
2609
|
+
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
2610
|
+
|
|
2611
|
+
/** Get the time this timer was set to, returns 0 if not set
|
|
2612
|
+
* @return {number} */
|
|
2613
|
+
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
2614
|
+
|
|
2615
|
+
/** Get the current global time this timer is based on
|
|
2616
|
+
* @return {number} */
|
|
2617
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
2618
|
+
|
|
2619
|
+
/** Returns this timer expressed as a string
|
|
2620
|
+
* @return {string} */
|
|
2621
|
+
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
2622
|
+
|
|
2623
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2624
|
+
* @return {number} */
|
|
2625
|
+
valueOf() { return this.get(); }
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2629
|
+
|
|
2568
2630
|
/** Formats seconds to mm:ss style for display purposes
|
|
2569
2631
|
* @param {number} t - time in seconds
|
|
2570
2632
|
* @return {string}
|
|
@@ -2651,86 +2713,29 @@ function shareURL(title, url, callback)
|
|
|
2651
2713
|
|
|
2652
2714
|
///////////////////////////////////////////////////////////////////////////////
|
|
2653
2715
|
|
|
2654
|
-
/**
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2657
|
-
*
|
|
2658
|
-
*
|
|
2659
|
-
|
|
2660
|
-
*
|
|
2661
|
-
* let b = new Timer(1); // creates a timer with 1 second left
|
|
2662
|
-
* b.unset(); // unset the timer
|
|
2663
|
-
*/
|
|
2664
|
-
class Timer
|
|
2716
|
+
/** Read save data from local storage
|
|
2717
|
+
* @param {string} saveName - unique name for the game/save
|
|
2718
|
+
* @param {Object} [defaultSaveData] - default values for save
|
|
2719
|
+
* @return {Object}
|
|
2720
|
+
* @memberof Utilities */
|
|
2721
|
+
function readSaveData(saveName, defaultSaveData)
|
|
2665
2722
|
{
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
{
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
const globalTime = this.getGlobalTime();
|
|
2674
|
-
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
2675
|
-
this.setTime = timeLeft;
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
/** Set the timer with seconds passed in
|
|
2679
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
2680
|
-
set(timeLeft=0)
|
|
2681
|
-
{
|
|
2682
|
-
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
2683
|
-
const globalTime = this.getGlobalTime();
|
|
2684
|
-
this.time = globalTime + timeLeft;
|
|
2685
|
-
this.setTime = timeLeft;
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
/** Set if the timer should keep running even when the game is paused
|
|
2689
|
-
* @param {boolean} [useRealTime] */
|
|
2690
|
-
setUseRealTime(useRealTime=true)
|
|
2691
|
-
{
|
|
2692
|
-
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
2693
|
-
this.useRealTime = useRealTime;
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
|
-
/** Unset the timer */
|
|
2697
|
-
unset() { this.time = undefined; }
|
|
2698
|
-
|
|
2699
|
-
/** Returns true if set
|
|
2700
|
-
* @return {boolean} */
|
|
2701
|
-
isSet() { return this.time !== undefined; }
|
|
2702
|
-
|
|
2703
|
-
/** Returns true if set and has not elapsed
|
|
2704
|
-
* @return {boolean} */
|
|
2705
|
-
active() { return this.getGlobalTime() < this.time; }
|
|
2706
|
-
|
|
2707
|
-
/** Returns true if set and elapsed
|
|
2708
|
-
* @return {boolean} */
|
|
2709
|
-
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
2710
|
-
|
|
2711
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2712
|
-
* @return {number} */
|
|
2713
|
-
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2714
|
-
|
|
2715
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2716
|
-
* @return {number} */
|
|
2717
|
-
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
2718
|
-
|
|
2719
|
-
/** Get the time this timer was set to, returns 0 if not set
|
|
2720
|
-
* @return {number} */
|
|
2721
|
-
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
2722
|
-
|
|
2723
|
-
/** Get the current global time this timer is based on
|
|
2724
|
-
* @return {number} */
|
|
2725
|
-
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
2726
|
-
|
|
2727
|
-
/** Returns this timer expressed as a string
|
|
2728
|
-
* @return {string} */
|
|
2729
|
-
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
2723
|
+
ASSERT(isString(saveName), 'loadData requires saveName string');
|
|
2724
|
+
|
|
2725
|
+
// replace undefined values with defaults
|
|
2726
|
+
const data = localStorage[saveName];
|
|
2727
|
+
const loadedData = data ? JSON.parse(data) : {};
|
|
2728
|
+
return { ...defaultSaveData, ...loadedData };
|
|
2729
|
+
}
|
|
2730
2730
|
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2731
|
+
/** Write save data to local storage
|
|
2732
|
+
* @param {string} saveName - unique name for the game/save
|
|
2733
|
+
* @param {Object} saveData - object containing data to be saved
|
|
2734
|
+
* @memberof Utilities */
|
|
2735
|
+
function writeSaveData(saveName, saveData)
|
|
2736
|
+
{
|
|
2737
|
+
ASSERT(isString(saveName), 'saveData requires saveName string');
|
|
2738
|
+
localStorage[saveName] = JSON.stringify(saveData);
|
|
2734
2739
|
}
|
|
2735
2740
|
/**
|
|
2736
2741
|
* LittleJS Engine Settings
|
|
@@ -3391,17 +3396,19 @@ class EngineObject
|
|
|
3391
3396
|
this.additiveColor = undefined;
|
|
3392
3397
|
/** @property {boolean} - Should it flip along y axis when rendered */
|
|
3393
3398
|
this.mirror = false;
|
|
3399
|
+
/** @property {boolean} - Has object been destroyed? */
|
|
3400
|
+
this.destroyed = false;
|
|
3394
3401
|
|
|
3395
3402
|
// physical properties
|
|
3396
|
-
/** @property {number}
|
|
3403
|
+
/** @property {number} - How heavy the object is, static if 0 */
|
|
3397
3404
|
this.mass = objectDefaultMass;
|
|
3398
|
-
/** @property {number}
|
|
3405
|
+
/** @property {number} - How much to slow down velocity each frame (0-1) */
|
|
3399
3406
|
this.damping = objectDefaultDamping;
|
|
3400
|
-
/** @property {number}
|
|
3407
|
+
/** @property {number} - How much to slow down rotation each frame (0-1) */
|
|
3401
3408
|
this.angleDamping = objectDefaultAngleDamping;
|
|
3402
|
-
/** @property {number}
|
|
3409
|
+
/** @property {number} - How bouncy the object is when colliding (0-1) */
|
|
3403
3410
|
this.restitution = objectDefaultRestitution;
|
|
3404
|
-
/** @property {number}
|
|
3411
|
+
/** @property {number} - How much friction to apply when sliding (0-1) */
|
|
3405
3412
|
this.friction = objectDefaultFriction;
|
|
3406
3413
|
/** @property {number} - How much to scale gravity by for this object */
|
|
3407
3414
|
this.gravityScale = 1;
|
|
@@ -3610,10 +3617,10 @@ class EngineObject
|
|
|
3610
3617
|
if (!tileCollisionTest(oldPos, this.size, this))
|
|
3611
3618
|
{
|
|
3612
3619
|
// test which side we bounced off (or both if a corner)
|
|
3613
|
-
const
|
|
3614
|
-
const
|
|
3615
|
-
|
|
3616
|
-
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)
|
|
3617
3624
|
{
|
|
3618
3625
|
// try to move up a tiny bit
|
|
3619
3626
|
const epsilon = 1e-3;
|
|
@@ -3629,16 +3636,12 @@ class EngineObject
|
|
|
3629
3636
|
return;
|
|
3630
3637
|
}
|
|
3631
3638
|
|
|
3632
|
-
// move to previous position and bounce
|
|
3639
|
+
// move to previous X position and bounce
|
|
3633
3640
|
this.pos.x = oldPos.x;
|
|
3634
|
-
this.velocity.x *= -
|
|
3641
|
+
this.velocity.x *= -restitution;
|
|
3635
3642
|
}
|
|
3636
|
-
if (
|
|
3643
|
+
if (isBlockedY || !isBlockedX)
|
|
3637
3644
|
{
|
|
3638
|
-
// bounce velocity
|
|
3639
|
-
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3640
|
-
this.velocity.y *= -restitution;
|
|
3641
|
-
|
|
3642
3645
|
if (wasFalling)
|
|
3643
3646
|
{
|
|
3644
3647
|
// adjust position to slightly away from nearest tile
|
|
@@ -3654,10 +3657,12 @@ class EngineObject
|
|
|
3654
3657
|
}
|
|
3655
3658
|
else
|
|
3656
3659
|
{
|
|
3657
|
-
// move to previous position
|
|
3660
|
+
// move to previous Y position
|
|
3658
3661
|
this.pos.y = oldPos.y;
|
|
3659
3662
|
this.groundObject = undefined;
|
|
3660
3663
|
}
|
|
3664
|
+
// bounce velocity
|
|
3665
|
+
this.velocity.y *= -restitution;
|
|
3661
3666
|
}
|
|
3662
3667
|
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
3663
3668
|
}
|
|
@@ -3675,19 +3680,19 @@ class EngineObject
|
|
|
3675
3680
|
drawTile(this.pos, this.drawSize || this.size, this.tileInfo, this.color, this.angle, this.mirror, this.additiveColor);
|
|
3676
3681
|
}
|
|
3677
3682
|
|
|
3678
|
-
/** Destroy this object, destroy its children, detach its parent, and mark it for removal
|
|
3679
|
-
|
|
3683
|
+
/** Destroy this object, destroy its children, detach its parent, and mark it for removal
|
|
3684
|
+
* @param {boolean} [immediate] - should attached effects be allowed to die off? */
|
|
3685
|
+
destroy(immediate=false)
|
|
3680
3686
|
{
|
|
3681
|
-
if (this.destroyed)
|
|
3682
|
-
return;
|
|
3687
|
+
if (this.destroyed) return;
|
|
3683
3688
|
|
|
3684
3689
|
// disconnect from parent and destroy children
|
|
3685
|
-
this.destroyed =
|
|
3690
|
+
this.destroyed = true;
|
|
3686
3691
|
this.parent?.removeChild(this);
|
|
3687
3692
|
for (const child of this.children)
|
|
3688
3693
|
{
|
|
3689
3694
|
child.parent = undefined;
|
|
3690
|
-
child.destroy();
|
|
3695
|
+
child.destroy(immediate);
|
|
3691
3696
|
}
|
|
3692
3697
|
}
|
|
3693
3698
|
|
|
@@ -3786,7 +3791,7 @@ class EngineObject
|
|
|
3786
3791
|
}
|
|
3787
3792
|
|
|
3788
3793
|
/** Check if overlapping another engine object
|
|
3789
|
-
* Collisions are
|
|
3794
|
+
* Collisions are resolved to prevent overlaps
|
|
3790
3795
|
* @param {EngineObject} object
|
|
3791
3796
|
* @return {boolean} */
|
|
3792
3797
|
isOverlappingObject(object)
|
|
@@ -4038,7 +4043,7 @@ class TileInfo
|
|
|
4038
4043
|
}
|
|
4039
4044
|
|
|
4040
4045
|
/**
|
|
4041
|
-
* Returns a tile info for an index using this tile as
|
|
4046
|
+
* Returns a tile info for an index using this tile as reference
|
|
4042
4047
|
* @param {Vector2|number} [index=0]
|
|
4043
4048
|
* @return {TileInfo}
|
|
4044
4049
|
*/
|
|
@@ -4868,7 +4873,7 @@ let engineFontImage;
|
|
|
4868
4873
|
class FontImage
|
|
4869
4874
|
{
|
|
4870
4875
|
/** Create an image font
|
|
4871
|
-
* @param {TileInfo} tileInfo - Tile info of first
|
|
4876
|
+
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
4872
4877
|
*/
|
|
4873
4878
|
constructor(tileInfo)
|
|
4874
4879
|
{
|
|
@@ -5744,7 +5749,7 @@ function inputRender()
|
|
|
5744
5749
|
}
|
|
5745
5750
|
}
|
|
5746
5751
|
|
|
5747
|
-
// center position for right
|
|
5752
|
+
// center position for right touch pad face buttons
|
|
5748
5753
|
function touchGamepadButtonCenter()
|
|
5749
5754
|
{
|
|
5750
5755
|
const center = mainCanvasSize.subtract(vec2(touchGamepadSize));
|
|
@@ -6445,50 +6450,62 @@ const tileCollisionLayers = [];
|
|
|
6445
6450
|
|
|
6446
6451
|
/** Get tile collision data for a given cell in the grid
|
|
6447
6452
|
* @param {Vector2} pos
|
|
6453
|
+
* @param {boolean} [solidOnly] - Only check solid layers?
|
|
6448
6454
|
* @return {number}
|
|
6449
6455
|
* @memberof TileLayers */
|
|
6450
|
-
function tileCollisionGetData(pos)
|
|
6456
|
+
function tileCollisionGetData(pos, solidOnly=true)
|
|
6451
6457
|
{
|
|
6452
6458
|
// check all tile collision layers
|
|
6453
6459
|
for (const layer of tileCollisionLayers)
|
|
6460
|
+
if (!solidOnly || layer.isSolid)
|
|
6454
6461
|
if (pos.arrayCheck(layer.size))
|
|
6455
|
-
|
|
6462
|
+
{
|
|
6463
|
+
const data = layer.getCollisionData(pos);
|
|
6464
|
+
if (data) return data;
|
|
6465
|
+
}
|
|
6456
6466
|
return 0;
|
|
6457
6467
|
}
|
|
6458
6468
|
|
|
6459
6469
|
/** Check if a tile layer collides with another object
|
|
6460
|
-
* @param {Vector2}
|
|
6461
|
-
* @param {Vector2}
|
|
6462
|
-
* @param {EngineObject} [
|
|
6463
|
-
* @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?
|
|
6464
6474
|
* @return {TileCollisionLayer}
|
|
6465
6475
|
* @memberof TileLayers */
|
|
6466
|
-
function tileCollisionTest(pos, size=vec2(),
|
|
6476
|
+
function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
6467
6477
|
{
|
|
6468
6478
|
for (const layer of tileCollisionLayers)
|
|
6469
6479
|
{
|
|
6470
6480
|
if (!solidOnly || layer.isSolid)
|
|
6471
|
-
if (layer.collisionTest(pos, size,
|
|
6481
|
+
if (layer.collisionTest(pos, size, callbackObject))
|
|
6472
6482
|
return layer;
|
|
6473
6483
|
}
|
|
6474
6484
|
}
|
|
6475
6485
|
|
|
6486
|
+
/**
|
|
6487
|
+
* @callback TileCollisionCallback - Function to handle a tile collision test
|
|
6488
|
+
* @param {number} tileData - the value of the tile at the position
|
|
6489
|
+
* @param {Vector2} pos - world space position of tile where the collision occurred
|
|
6490
|
+
* @memberof TileLayers
|
|
6491
|
+
*/
|
|
6492
|
+
|
|
6476
6493
|
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
6477
|
-
* The point will be inside the colliding tile if it hits
|
|
6478
|
-
* @param {Vector2}
|
|
6479
|
-
* @param {Vector2}
|
|
6480
|
-
* @param {EngineObject} [
|
|
6481
|
-
* @param {Vector2}
|
|
6482
|
-
* @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?
|
|
6483
6500
|
* @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
|
|
6484
6501
|
* @memberof TileLayers */
|
|
6485
|
-
function tileCollisionRaycast(posStart, posEnd,
|
|
6502
|
+
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
6486
6503
|
{
|
|
6487
6504
|
for (const layer of tileCollisionLayers)
|
|
6488
6505
|
{
|
|
6489
6506
|
if (!solidOnly || layer.isSolid)
|
|
6490
6507
|
{
|
|
6491
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd,
|
|
6508
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, normal)
|
|
6492
6509
|
if (hitPos) return hitPos;
|
|
6493
6510
|
}
|
|
6494
6511
|
}
|
|
@@ -7052,18 +7069,22 @@ class TileCollisionLayer extends TileLayer
|
|
|
7052
7069
|
/** Check if collision with another object should occur
|
|
7053
7070
|
* @param {Vector2} pos
|
|
7054
7071
|
* @param {Vector2} [size=vec2()]
|
|
7055
|
-
* @param {EngineObject} [object
|
|
7072
|
+
* @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
|
|
7056
7073
|
* @return {boolean} */
|
|
7057
|
-
collisionTest(pos, size=new Vector2,
|
|
7074
|
+
collisionTest(pos, size=new Vector2, callbackObject)
|
|
7058
7075
|
{
|
|
7059
7076
|
ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
|
|
7060
|
-
ASSERT(!
|
|
7061
|
-
|
|
7062
|
-
//
|
|
7063
|
-
const
|
|
7064
|
-
|
|
7077
|
+
ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
|
|
7078
|
+
|
|
7079
|
+
// make function to check for collision
|
|
7080
|
+
const collisionTest = callbackObject ? typeof callbackObject === 'function' ?
|
|
7081
|
+
(tileData, pos)=> callbackObject(tileData, pos) :
|
|
7082
|
+
(tileData, pos)=> callbackObject.collideWithTile(tileData, pos) :
|
|
7083
|
+
()=> true;
|
|
7065
7084
|
|
|
7066
7085
|
// check any tiles in the area for collision
|
|
7086
|
+
const posX = pos.x - this.pos.x;
|
|
7087
|
+
const posY = pos.y - this.pos.y;
|
|
7067
7088
|
const minX = max(posX - size.x/2|0, 0);
|
|
7068
7089
|
const minY = max(posY - size.y/2|0, 0);
|
|
7069
7090
|
const maxX = min(posX + size.x/2, this.size.x);
|
|
@@ -7074,9 +7095,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
7074
7095
|
{
|
|
7075
7096
|
// check if the object should collide with this tile
|
|
7076
7097
|
const tileData = this.collisionData[y*this.size.x+x];
|
|
7077
|
-
if (tileData)
|
|
7078
|
-
if (!object || object.collideWithTile(tileData,
|
|
7079
|
-
hitPos.set(x + this.pos.x, y + this.pos.y)))
|
|
7098
|
+
if (tileData && collisionTest(tileData, hitPos.set(x+this.pos.x, y+this.pos.y)))
|
|
7080
7099
|
return true;
|
|
7081
7100
|
}
|
|
7082
7101
|
return false;
|
|
@@ -7084,52 +7103,68 @@ class TileCollisionLayer extends TileLayer
|
|
|
7084
7103
|
|
|
7085
7104
|
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
7086
7105
|
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
7087
|
-
* @param {Vector2}
|
|
7088
|
-
* @param {Vector2}
|
|
7089
|
-
* @param {EngineObject} [
|
|
7090
|
-
* @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
|
|
7091
7110
|
* @return {Vector2|undefined} */
|
|
7092
|
-
collisionRaycast(posStart, posEnd,
|
|
7111
|
+
collisionRaycast(posStart, posEnd, callbackObject, normal)
|
|
7093
7112
|
{
|
|
7094
7113
|
ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
|
|
7095
|
-
ASSERT(!
|
|
7114
|
+
ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a funtion or EngineObject');
|
|
7096
7115
|
|
|
7097
|
-
|
|
7098
|
-
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)=>
|
|
7099
7122
|
{
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
const tileData = this.getCollisionData(localPos);
|
|
7103
|
-
return tileData && (!object || object.collideWithTile(tileData, pos));
|
|
7123
|
+
const tileData = this.getCollisionData(localPos.set(pos.x - this.pos.x, pos.y - this.pos.y));
|
|
7124
|
+
return tileData && collisionTest(tileData, pos);
|
|
7104
7125
|
}
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7126
|
+
|
|
7127
|
+
// use line test against tile collision
|
|
7128
|
+
const localPos = new Vector2;
|
|
7129
|
+
const hitPos = lineTest(posStart, posEnd, testFunction, normal);
|
|
7130
|
+
if (debugRaycast && hitPos)
|
|
7108
7131
|
{
|
|
7109
7132
|
const tilePos = hitPos.floor().add(vec2(.5));
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
return hitPos;
|
|
7133
|
+
debugRect(tilePos, vec2(1), '#f008');
|
|
7134
|
+
debugLine(posStart, posEnd, '#00f', .02);
|
|
7135
|
+
debugLine(posStart, hitPos, '#f00', .02);
|
|
7136
|
+
debugPoint(hitPos, '#0f0');
|
|
7137
|
+
normal && debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
|
|
7116
7138
|
}
|
|
7139
|
+
return hitPos;
|
|
7117
7140
|
}
|
|
7118
7141
|
}
|
|
7119
7142
|
/**
|
|
7120
7143
|
* LittleJS Particle System
|
|
7144
|
+
* - A simple but fast and flexible particle system
|
|
7145
|
+
* - Lightweight Particles are created and managed by ParticleEmitters
|
|
7146
|
+
* - The particle design tool can be used to help create emitters
|
|
7147
|
+
* @namespace Particles
|
|
7121
7148
|
*/
|
|
7122
7149
|
|
|
7123
7150
|
/**
|
|
7124
|
-
* @callback
|
|
7151
|
+
* @callback ParticleCallback - Function that processes a particle
|
|
7125
7152
|
* @param {Particle} particle
|
|
7126
|
-
* @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
|
|
7127
7162
|
*/
|
|
7128
7163
|
|
|
7129
7164
|
/**
|
|
7130
7165
|
* Particle Emitter - Spawns particles with the given settings
|
|
7131
7166
|
* @extends EngineObject
|
|
7132
|
-
* @memberof
|
|
7167
|
+
* @memberof Particles
|
|
7133
7168
|
* @example
|
|
7134
7169
|
* // create a particle emitter
|
|
7135
7170
|
* let pos = vec2(2,3);
|
|
@@ -7147,7 +7182,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
7147
7182
|
class ParticleEmitter extends EngineObject
|
|
7148
7183
|
{
|
|
7149
7184
|
/** Create a particle system with the given settings
|
|
7150
|
-
* @param {Vector2}
|
|
7185
|
+
* @param {Vector2} pos - World space position of the emitter
|
|
7151
7186
|
* @param {number} [angle] - Angle to emit the particles
|
|
7152
7187
|
* @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
|
|
7153
7188
|
* @param {number} [emitTime] - How long to stay alive (0 is forever)
|
|
@@ -7177,7 +7212,7 @@ class ParticleEmitter extends EngineObject
|
|
|
7177
7212
|
*/
|
|
7178
7213
|
constructor
|
|
7179
7214
|
(
|
|
7180
|
-
|
|
7215
|
+
pos,
|
|
7181
7216
|
angle,
|
|
7182
7217
|
emitSize = 0,
|
|
7183
7218
|
emitTime = 0,
|
|
@@ -7206,12 +7241,13 @@ class ParticleEmitter extends EngineObject
|
|
|
7206
7241
|
localSpace = false
|
|
7207
7242
|
)
|
|
7208
7243
|
{
|
|
7209
|
-
super(
|
|
7244
|
+
super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
|
|
7210
7245
|
|
|
7211
7246
|
// emitter settings
|
|
7247
|
+
/** @property {boolean} - Should particles be emitted in a circle */
|
|
7248
|
+
this.emitCircle = typeof emitSize === 'number';
|
|
7212
7249
|
/** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
|
|
7213
|
-
this.emitSize = emitSize
|
|
7214
|
-
emitSize.copy() : emitSize;
|
|
7250
|
+
this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
|
|
7215
7251
|
/** @property {number} - How long to stay alive (0 is forever) */
|
|
7216
7252
|
this.emitTime = emitTime;
|
|
7217
7253
|
/** @property {number} - How many particles per second to spawn, does not emit if 0 */
|
|
@@ -7262,26 +7298,31 @@ class ParticleEmitter extends EngineObject
|
|
|
7262
7298
|
this.localSpace = localSpace;
|
|
7263
7299
|
/** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
|
|
7264
7300
|
this.trailScale = 0;
|
|
7265
|
-
/** @property {
|
|
7266
|
-
this.particleDestroyCallback = undefined;
|
|
7267
|
-
/** @property {ParticleCallbackFunction} - Callback when particle is created */
|
|
7301
|
+
/** @property {ParticleCallback} - Callback when particle is created */
|
|
7268
7302
|
this.particleCreateCallback = undefined;
|
|
7269
|
-
/** @property {
|
|
7270
|
-
this.
|
|
7303
|
+
/** @property {ParticleCallback} - Callback when particle is destroyed */
|
|
7304
|
+
this.particleDestroyCallback = undefined;
|
|
7305
|
+
/** @property {ParticleCollideCallback} - Callback when particle collides */
|
|
7306
|
+
this.particleCollideCallback = undefined;
|
|
7271
7307
|
/** @property {number} - Percentage of velocity to pass to particles (0-1) */
|
|
7272
7308
|
this.velocityInheritance = 0;
|
|
7309
|
+
/** @property {number} - Track particle emit time */
|
|
7310
|
+
this.emitTimeBuffer = 0;
|
|
7311
|
+
/** @property {Array<Particle>} - Array of particles for this emitter */
|
|
7312
|
+
this.particles = [];
|
|
7273
7313
|
|
|
7274
7314
|
// track previous position and angle
|
|
7275
7315
|
this.previousAngle = this.angle;
|
|
7276
7316
|
this.previousPos = this.pos.copy();
|
|
7277
7317
|
}
|
|
7278
7318
|
|
|
7279
|
-
/** Emitters do not have physics */
|
|
7280
|
-
updatePhysics() {}
|
|
7281
|
-
|
|
7282
7319
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
7283
7320
|
update()
|
|
7284
7321
|
{
|
|
7322
|
+
// physics sanity checks
|
|
7323
|
+
ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
|
|
7324
|
+
ASSERT(this.damping >= 0 && this.damping <= 1);
|
|
7325
|
+
|
|
7285
7326
|
if (this.velocityInheritance)
|
|
7286
7327
|
{
|
|
7287
7328
|
// pass emitter velocity to particles
|
|
@@ -7295,7 +7336,7 @@ class ParticleEmitter extends EngineObject
|
|
|
7295
7336
|
}
|
|
7296
7337
|
|
|
7297
7338
|
// update emitter
|
|
7298
|
-
if (
|
|
7339
|
+
if (this.isActive())
|
|
7299
7340
|
{
|
|
7300
7341
|
// emit particles
|
|
7301
7342
|
if (this.emitRate && particleEmitRateScale)
|
|
@@ -7305,14 +7346,21 @@ class ParticleEmitter extends EngineObject
|
|
|
7305
7346
|
this.emitParticle();
|
|
7306
7347
|
}
|
|
7307
7348
|
}
|
|
7308
|
-
else
|
|
7309
|
-
this.destroy();
|
|
7349
|
+
else if (this.particles.length === 0)
|
|
7350
|
+
this.destroy(true);
|
|
7351
|
+
|
|
7352
|
+
// update and remove destroyed particles
|
|
7353
|
+
this.particles = this.particles.filter((p)=>
|
|
7354
|
+
{
|
|
7355
|
+
p.update();
|
|
7356
|
+
return !p.destroyed;
|
|
7357
|
+
});
|
|
7310
7358
|
|
|
7311
7359
|
if (debugParticles)
|
|
7312
7360
|
{
|
|
7313
7361
|
// show emitter bounds
|
|
7314
|
-
if (
|
|
7315
|
-
debugCircle(this.pos, this.emitSize/2, '#0f0');
|
|
7362
|
+
if (this.emitCircle)
|
|
7363
|
+
debugCircle(this.pos, this.emitSize.x/2, '#0f0');
|
|
7316
7364
|
else
|
|
7317
7365
|
debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
|
|
7318
7366
|
}
|
|
@@ -7323,14 +7371,15 @@ class ParticleEmitter extends EngineObject
|
|
|
7323
7371
|
emitParticle()
|
|
7324
7372
|
{
|
|
7325
7373
|
// spawn a particle
|
|
7326
|
-
let pos =
|
|
7327
|
-
randInCircle(this.emitSize/2)
|
|
7328
|
-
: 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
|
|
7329
7377
|
.multiply(this.emitSize).rotate(this.angle)
|
|
7330
7378
|
let angle = rand(this.particleConeAngle, -this.particleConeAngle);
|
|
7331
7379
|
if (!this.localSpace)
|
|
7332
7380
|
{
|
|
7333
|
-
pos
|
|
7381
|
+
pos.x += this.pos.x;
|
|
7382
|
+
pos.y += this.pos.y;
|
|
7334
7383
|
angle += this.angle;
|
|
7335
7384
|
}
|
|
7336
7385
|
|
|
@@ -7350,25 +7399,17 @@ class ParticleEmitter extends EngineObject
|
|
|
7350
7399
|
const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
|
|
7351
7400
|
|
|
7352
7401
|
// build particle
|
|
7353
|
-
const
|
|
7354
|
-
|
|
7355
|
-
particle.angleVelocity = angleSpeed;
|
|
7402
|
+
const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
|
|
7403
|
+
let angleVelocity = angleSpeed;
|
|
7356
7404
|
if (!this.localSpace && this.velocityInheritance > 0)
|
|
7357
7405
|
{
|
|
7358
7406
|
// apply emitter velocity to particle
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
}
|
|
7363
|
-
particle
|
|
7364
|
-
|
|
7365
|
-
particle.angleDamping = this.angleDamping;
|
|
7366
|
-
particle.restitution = this.restitution;
|
|
7367
|
-
particle.friction = this.friction;
|
|
7368
|
-
particle.gravityScale = this.gravityScale;
|
|
7369
|
-
particle.collideTiles = this.collideTiles;
|
|
7370
|
-
particle.renderOrder = this.renderOrder;
|
|
7371
|
-
particle.mirror = randBool();
|
|
7407
|
+
velocity.x += this.velocity.x;
|
|
7408
|
+
velocity.y += this.velocity.y;
|
|
7409
|
+
angleVelocity += this.angleVelocity;
|
|
7410
|
+
}
|
|
7411
|
+
const particle = new Particle(this, pos, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, velocity, angleVelocity);
|
|
7412
|
+
this.particles.push(particle);
|
|
7372
7413
|
|
|
7373
7414
|
// call particle create callback
|
|
7374
7415
|
this.particleCreateCallback?.(particle);
|
|
@@ -7377,140 +7418,245 @@ class ParticleEmitter extends EngineObject
|
|
|
7377
7418
|
return particle;
|
|
7378
7419
|
}
|
|
7379
7420
|
|
|
7380
|
-
|
|
7381
|
-
|
|
7421
|
+
/** Particle emitters do not have physics */
|
|
7422
|
+
updatePhysics() {}
|
|
7423
|
+
|
|
7424
|
+
/** Render all particles for this emitter */
|
|
7425
|
+
render()
|
|
7426
|
+
{
|
|
7427
|
+
// render all particles
|
|
7428
|
+
for (const particle of this.particles)
|
|
7429
|
+
particle.render();
|
|
7430
|
+
}
|
|
7431
|
+
|
|
7432
|
+
/** is emitter actively spawning */
|
|
7433
|
+
isActive() { return !this.emitTime || this.getAliveTime() < this.emitTime; }
|
|
7434
|
+
|
|
7435
|
+
/** Destroy the particle emitter
|
|
7436
|
+
* @param {boolean} [immediate] - should particle emitters and other attached effects be allowed to die off */
|
|
7437
|
+
destroy(immediate=false)
|
|
7438
|
+
{
|
|
7439
|
+
if (this.destroyed) return;
|
|
7440
|
+
|
|
7441
|
+
super.destroy(immediate);
|
|
7442
|
+
if (!immediate && this.particles.length > 0)
|
|
7443
|
+
{
|
|
7444
|
+
// wait for particles to die off
|
|
7445
|
+
this.destroyed = false;
|
|
7446
|
+
this.emitTime = -1;
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7382
7449
|
}
|
|
7383
7450
|
|
|
7384
7451
|
///////////////////////////////////////////////////////////////////////////////
|
|
7385
7452
|
/**
|
|
7386
7453
|
* Particle Object - Created automatically by Particle Emitters
|
|
7387
|
-
* @
|
|
7388
|
-
* @memberof Engine
|
|
7454
|
+
* @memberof Particles
|
|
7389
7455
|
*/
|
|
7390
|
-
class Particle
|
|
7456
|
+
class Particle
|
|
7391
7457
|
{
|
|
7392
7458
|
/**
|
|
7393
7459
|
* Create a particle with the passed in settings
|
|
7394
7460
|
* Typically this is created automatically by a ParticleEmitter
|
|
7395
|
-
* @param {
|
|
7396
|
-
* @param {
|
|
7397
|
-
* @param {number}
|
|
7398
|
-
* @param {Color}
|
|
7399
|
-
* @param {Color}
|
|
7400
|
-
* @param {number}
|
|
7401
|
-
* @param {number}
|
|
7402
|
-
* @param {number}
|
|
7403
|
-
* @param {
|
|
7404
|
-
* @param {
|
|
7405
|
-
* @param {number} trailScale - If a trail, how long to make it
|
|
7406
|
-
* @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
|
|
7407
|
-
* @param {ParticleCallbackFunction} [destroyCallback] - Callback when particle dies
|
|
7461
|
+
* @param {ParticleEmitter} emitter - The emitter that created this particle
|
|
7462
|
+
* @param {Vector2} pos - World or local space position
|
|
7463
|
+
* @param {number} angle - Angle of the particle
|
|
7464
|
+
* @param {Color} colorStart - Color at start of life
|
|
7465
|
+
* @param {Color} colorEnd - Color at end of life
|
|
7466
|
+
* @param {number} lifeTime - How long to live for
|
|
7467
|
+
* @param {number} sizeStart - Size at start of life
|
|
7468
|
+
* @param {number} sizeEnd - Size at end of life
|
|
7469
|
+
* @param {Vector2} [velocity] - Velocity of the particle
|
|
7470
|
+
* @param {number} [angleVelocity] - Angular speed of the particle
|
|
7408
7471
|
*/
|
|
7409
|
-
constructor(
|
|
7410
|
-
)
|
|
7472
|
+
constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
|
|
7411
7473
|
{
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
/** @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} */
|
|
7415
7485
|
this.colorStart = colorStart;
|
|
7416
|
-
/** @property {Color}
|
|
7486
|
+
/** @property {Color} */
|
|
7417
7487
|
this.colorEnd = colorEnd;
|
|
7418
|
-
/** @property {number}
|
|
7488
|
+
/** @property {number} */
|
|
7419
7489
|
this.lifeTime = lifeTime;
|
|
7420
|
-
/** @property {number}
|
|
7490
|
+
/** @property {number} */
|
|
7421
7491
|
this.sizeStart = sizeStart;
|
|
7422
|
-
/** @property {number}
|
|
7492
|
+
/** @property {number} */
|
|
7423
7493
|
this.sizeEnd = sizeEnd;
|
|
7424
|
-
/** @property {
|
|
7425
|
-
this.
|
|
7426
|
-
/** @property {
|
|
7427
|
-
this.
|
|
7428
|
-
/** @property {number}
|
|
7429
|
-
this.
|
|
7430
|
-
/** @property {
|
|
7431
|
-
this.
|
|
7432
|
-
/** @property {
|
|
7433
|
-
this.
|
|
7434
|
-
|
|
7435
|
-
this.
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7494
|
+
/** @property {Vector2} */
|
|
7495
|
+
this.velocity = velocity;
|
|
7496
|
+
/** @property {number} */
|
|
7497
|
+
this.angleVelocity = angleVelocity;
|
|
7498
|
+
/** @property {number} */
|
|
7499
|
+
this.spawnTime = time;
|
|
7500
|
+
/** @property {boolean} */
|
|
7501
|
+
this.mirror = randBool();
|
|
7502
|
+
/** @property {EngineObject} */
|
|
7503
|
+
this.groundObject = undefined;
|
|
7504
|
+
/** @property {boolean} */
|
|
7505
|
+
this.destroyed = false;
|
|
7506
|
+
/** @property {TileInfo} */
|
|
7507
|
+
this.tileInfo = emitter.tileInfo;
|
|
7508
|
+
}
|
|
7509
|
+
|
|
7510
|
+
/** Update the particle */
|
|
7439
7511
|
update()
|
|
7440
7512
|
{
|
|
7441
|
-
|
|
7513
|
+
// emitter properties
|
|
7514
|
+
const emitter = this.emitter;
|
|
7515
|
+
const damping = emitter.damping;
|
|
7516
|
+
const angleDamping = emitter.angleDamping;
|
|
7517
|
+
const restitution = emitter.restitution;
|
|
7518
|
+
const friction = emitter.friction;
|
|
7519
|
+
const gravityScale = emitter.gravityScale;
|
|
7520
|
+
const collideTiles = emitter.collideTiles;
|
|
7521
|
+
const collideCallback = emitter.particleCollideCallback;
|
|
7522
|
+
|
|
7523
|
+
// destroy particle when its time runs out
|
|
7524
|
+
if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
|
|
7442
7525
|
{
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
if (length2 > objectMaxSpeed*objectMaxSpeed)
|
|
7446
|
-
{
|
|
7447
|
-
const s = objectMaxSpeed / length2**.5;
|
|
7448
|
-
this.velocity.x *= s;
|
|
7449
|
-
this.velocity.y *= s;
|
|
7450
|
-
}
|
|
7526
|
+
this.destroy();
|
|
7527
|
+
return;
|
|
7451
7528
|
}
|
|
7452
7529
|
|
|
7453
|
-
|
|
7530
|
+
// apply physics
|
|
7531
|
+
const oldPos = this.pos.copy();
|
|
7532
|
+
this.velocity.x *= damping;
|
|
7533
|
+
this.velocity.y *= damping;
|
|
7534
|
+
this.pos.x += this.velocity.x += gravity.x * gravityScale;
|
|
7535
|
+
this.pos.y += this.velocity.y += gravity.y * gravityScale;
|
|
7536
|
+
this.angle += this.angleVelocity *= angleDamping;
|
|
7537
|
+
|
|
7538
|
+
// don't do collision if solver disabled
|
|
7539
|
+
if (!enablePhysicsSolver || !collideTiles) return;
|
|
7540
|
+
|
|
7541
|
+
// apply max circular speed to prevent going through collision
|
|
7542
|
+
const length2 = this.velocity.lengthSquared();
|
|
7543
|
+
if (length2 > objectMaxSpeed*objectMaxSpeed)
|
|
7544
|
+
{
|
|
7545
|
+
const s = objectMaxSpeed / length2**.5;
|
|
7546
|
+
this.velocity.x *= s;
|
|
7547
|
+
this.velocity.y *= s;
|
|
7548
|
+
}
|
|
7549
|
+
|
|
7550
|
+
// check collision against tiles
|
|
7551
|
+
this.groundObject = undefined;
|
|
7552
|
+
const testCollision = collideCallback ? (pos)=>
|
|
7454
7553
|
{
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7554
|
+
const data = tileCollisionGetData(pos);
|
|
7555
|
+
return data && collideCallback(this, data, pos);
|
|
7556
|
+
} : (pos)=> tileCollisionGetData(pos) > 0;
|
|
7557
|
+
|
|
7558
|
+
if (testCollision(this.pos))
|
|
7559
|
+
{
|
|
7560
|
+
// if already was stuck in collision, don't do anything
|
|
7561
|
+
const hitLayer = tileCollisionTest(this.pos);
|
|
7562
|
+
if (!testCollision(oldPos))
|
|
7563
|
+
{
|
|
7564
|
+
if (!collideCallback || collideCallback?.(this, hitLayer))
|
|
7565
|
+
{
|
|
7566
|
+
// test which side we bounced off (or both if a corner)
|
|
7567
|
+
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
7568
|
+
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
7569
|
+
const hitRestitution = max(restitution, hitLayer.restitution);
|
|
7570
|
+
const hitFriction = max(friction, hitLayer.friction);
|
|
7571
|
+
if (isBlockedX)
|
|
7572
|
+
{
|
|
7573
|
+
// move to previous X position and bounce
|
|
7574
|
+
this.pos.x = oldPos.x;
|
|
7575
|
+
this.velocity.x *= -hitRestitution;
|
|
7576
|
+
this.velocity.y *= hitFriction;
|
|
7577
|
+
}
|
|
7578
|
+
if (isBlockedY || !isBlockedX)
|
|
7579
|
+
{
|
|
7580
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
7581
|
+
if (wasFalling)
|
|
7582
|
+
this.groundObject = hitLayer;
|
|
7583
|
+
|
|
7584
|
+
// move to previous Y position and bounce
|
|
7585
|
+
this.pos.y = oldPos.y;
|
|
7586
|
+
this.velocity.y *= -hitRestitution;
|
|
7587
|
+
this.velocity.x *= hitFriction;
|
|
7588
|
+
}
|
|
7589
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7461
7592
|
}
|
|
7462
7593
|
}
|
|
7463
7594
|
|
|
7464
|
-
/**
|
|
7595
|
+
/** Destroy this particle */
|
|
7596
|
+
destroy()
|
|
7597
|
+
{
|
|
7598
|
+
const destroyCallback = this.emitter.particleDestroyCallback;
|
|
7599
|
+
const c = this.colorEnd;
|
|
7600
|
+
this.color.set(c.r, c.g, c.b, c.a);
|
|
7601
|
+
this.size.set(this.sizeEnd, this.sizeEnd);
|
|
7602
|
+
this.destroyed = true;
|
|
7603
|
+
destroyCallback?.(this);
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
/** Render the particle, automatically called each frame */
|
|
7465
7607
|
render()
|
|
7466
7608
|
{
|
|
7609
|
+
// emitter properties
|
|
7610
|
+
const emitter = this.emitter;
|
|
7611
|
+
const localSpace = emitter.localSpace;
|
|
7612
|
+
const additive = emitter.additive;
|
|
7613
|
+
const trailScale = emitter.trailScale;
|
|
7614
|
+
const fadeRate = emitter.fadeRate / 2;
|
|
7615
|
+
|
|
7467
7616
|
// lerp color and size
|
|
7468
7617
|
const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
|
|
7469
7618
|
const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
|
|
7470
7619
|
const size = vec2(radius);
|
|
7620
|
+
const alphaFade = p1 < fadeRate ? p1/fadeRate :
|
|
7621
|
+
p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
|
|
7471
7622
|
this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
|
|
7472
7623
|
this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
|
|
7473
7624
|
this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
|
|
7474
|
-
this.color.a = p2 * this.colorStart.a + p1 * this.colorEnd.a;
|
|
7475
|
-
|
|
7476
|
-
// fade alpha
|
|
7477
|
-
const fadeRate = this.fadeRate/2;
|
|
7478
|
-
this.color.a *= p1 < fadeRate ? p1/fadeRate :
|
|
7479
|
-
p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
|
|
7625
|
+
this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
|
|
7480
7626
|
|
|
7481
7627
|
// draw the particle
|
|
7482
|
-
|
|
7628
|
+
additive && setBlendMode(true);
|
|
7483
7629
|
|
|
7484
7630
|
// update the position and angle for drawing
|
|
7485
|
-
|
|
7486
|
-
|
|
7631
|
+
const pos = this.pos.copy();
|
|
7632
|
+
let angle = this.angle;
|
|
7633
|
+
if (localSpace)
|
|
7487
7634
|
{
|
|
7488
7635
|
// in local space of emitter
|
|
7489
|
-
const a =
|
|
7636
|
+
const a = emitter.angle;
|
|
7490
7637
|
const c = cos(a), s = sin(a);
|
|
7491
|
-
pos
|
|
7492
|
-
|
|
7493
|
-
angle +=
|
|
7638
|
+
pos.set(emitter.pos.x + pos.x*c - pos.y*s,
|
|
7639
|
+
emitter.pos.y + pos.x*s + pos.y*c);
|
|
7640
|
+
angle += a;
|
|
7494
7641
|
}
|
|
7495
|
-
if (
|
|
7642
|
+
if (trailScale)
|
|
7496
7643
|
{
|
|
7497
7644
|
// trail style particles
|
|
7498
|
-
const
|
|
7499
|
-
this.velocity.rotate(-
|
|
7500
|
-
|
|
7501
|
-
const speed = direction.length();
|
|
7645
|
+
const velocity = localSpace ?
|
|
7646
|
+
this.velocity.rotate(-emitter.angle) : this.velocity;
|
|
7647
|
+
const speed = velocity.length();
|
|
7502
7648
|
if (speed)
|
|
7503
7649
|
{
|
|
7504
7650
|
// stretch in direction of motion
|
|
7505
|
-
const trailLength = speed *
|
|
7651
|
+
const trailLength = speed * trailScale;
|
|
7506
7652
|
size.y = max(size.x, trailLength);
|
|
7507
|
-
angle = atan2(
|
|
7653
|
+
angle = atan2(velocity.x, velocity.y);
|
|
7508
7654
|
drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
|
|
7509
7655
|
}
|
|
7510
7656
|
}
|
|
7511
7657
|
else
|
|
7512
7658
|
drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
|
|
7513
|
-
|
|
7659
|
+
additive && setBlendMode();
|
|
7514
7660
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
7515
7661
|
}
|
|
7516
7662
|
}
|
|
@@ -8790,7 +8936,7 @@ class PostProcessPlugin
|
|
|
8790
8936
|
{
|
|
8791
8937
|
/** Create global post processing shader
|
|
8792
8938
|
* @param {string} shaderCode
|
|
8793
|
-
* @param {boolean} [includeMainCanvas] - combine
|
|
8939
|
+
* @param {boolean} [includeMainCanvas] - combine mainCanvas onto glCanvas
|
|
8794
8940
|
* @param {boolean} [feedbackTexture] - use glCanvas from previous frame as the texture
|
|
8795
8941
|
* @example
|
|
8796
8942
|
* // create the post process plugin object
|
|
@@ -9183,7 +9329,7 @@ class UISystemPlugin
|
|
|
9183
9329
|
// navigation properties
|
|
9184
9330
|
/** @property {UIObject} - Object currently selected by navigation (gamepad or keyboard) */
|
|
9185
9331
|
this.navigationObject = undefined;
|
|
9186
|
-
/** @property {Timer} -
|
|
9332
|
+
/** @property {Timer} - Cool down timer for navigation inputs */
|
|
9187
9333
|
this.navigationTimer = new Timer(undefined, true);
|
|
9188
9334
|
/** @property {number} - Time between navigation inputs in seconds */
|
|
9189
9335
|
this.navigationDelay = .2;
|
|
@@ -9649,7 +9795,7 @@ class UISystemPlugin
|
|
|
9649
9795
|
|
|
9650
9796
|
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
9651
9797
|
|
|
9652
|
-
// allow both
|
|
9798
|
+
// allow both axes for navigation
|
|
9653
9799
|
uiSystem.navigationDirection = 2;
|
|
9654
9800
|
|
|
9655
9801
|
// confirm menu
|
|
@@ -9744,7 +9890,7 @@ class UIObject
|
|
|
9744
9890
|
this.lineWidth = uiSystem.defaultLineWidth;
|
|
9745
9891
|
/** @property {number} - Corner radius for rounded rects */
|
|
9746
9892
|
this.cornerRadius = uiSystem.defaultCornerRadius;
|
|
9747
|
-
/** @property {string} - Font for this
|
|
9893
|
+
/** @property {string} - Font for this object */
|
|
9748
9894
|
this.font = uiSystem.defaultFont;
|
|
9749
9895
|
/** @property {string} - Font style for this object or undefined */
|
|
9750
9896
|
this.fontStyle = undefined;
|
|
@@ -10232,6 +10378,8 @@ class UIScrollbar extends UIObject
|
|
|
10232
10378
|
this.value = value;
|
|
10233
10379
|
/** @property {Color} - Color for the handle part of the scrollbar */
|
|
10234
10380
|
this.handleColor = handleColor.copy();
|
|
10381
|
+
/** @property {boolean} - Should it fill up like a progress bar? */
|
|
10382
|
+
this.fillMode = false;
|
|
10235
10383
|
|
|
10236
10384
|
// set properties
|
|
10237
10385
|
this.text = text;
|
|
@@ -10277,19 +10425,30 @@ class UIScrollbar extends UIObject
|
|
|
10277
10425
|
|
|
10278
10426
|
// handle horizontal or vertical scrollbar
|
|
10279
10427
|
const isHorizontal = this.size.x > this.size.y;
|
|
10280
|
-
const
|
|
10281
|
-
const
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10428
|
+
const barWidth = isHorizontal ? this.size.x : this.size.y;
|
|
10429
|
+
const handleWidth = isHorizontal ? this.size.y : this.size.x;
|
|
10430
|
+
if (this.fillMode)
|
|
10431
|
+
{
|
|
10432
|
+
// draw progress bar
|
|
10433
|
+
const minWidth = min(handleWidth, this.cornerRadius * 2);
|
|
10434
|
+
const progressWidth = lerp(minWidth, barWidth, this.value);
|
|
10435
|
+
const p = (progressWidth - barWidth) * (isHorizontal ? .5 : -.5);
|
|
10436
|
+
const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
|
|
10437
|
+
const color = this.disabled ? this.disabledColor : this.handleColor;
|
|
10438
|
+
const drawSize = isHorizontal ?
|
|
10439
|
+
vec2(progressWidth, this.size.y) : vec2(this.size.x, progressWidth);
|
|
10440
|
+
uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
10441
|
+
}
|
|
10442
|
+
else
|
|
10443
|
+
{
|
|
10444
|
+
// draw the scrollbar handle
|
|
10445
|
+
const value = isHorizontal ? this.value : 1 - this.value;
|
|
10446
|
+
const p = (barWidth - handleWidth) * (value - .5);
|
|
10447
|
+
const pos = this.pos.add(isHorizontal ? vec2(p, 0) : vec2(0, p));
|
|
10448
|
+
const color = this.disabled ? this.disabledColor : this.handleColor;
|
|
10449
|
+
const drawSize = vec2(handleWidth);
|
|
10450
|
+
uiSystem.drawRect(pos, drawSize, color, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
10451
|
+
}
|
|
10293
10452
|
|
|
10294
10453
|
// draw the text scaled to fit on the scrollbar
|
|
10295
10454
|
const textSize = this.getTextSize();
|
|
@@ -10311,7 +10470,7 @@ class UIScrollbar extends UIObject
|
|
|
10311
10470
|
* @extends UIObject
|
|
10312
10471
|
* @example
|
|
10313
10472
|
* // Create a video player UI object
|
|
10314
|
-
* const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), '
|
|
10473
|
+
* const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'video.mp4', true);
|
|
10315
10474
|
* video.play();
|
|
10316
10475
|
* @memberof UISystem
|
|
10317
10476
|
*/
|
|
@@ -11060,11 +11219,11 @@ class Box2dStaticObject extends Box2dObject
|
|
|
11060
11219
|
|
|
11061
11220
|
///////////////////////////////////////////////////////////////////////////////
|
|
11062
11221
|
/**
|
|
11063
|
-
* Box2D
|
|
11222
|
+
* Box2D Kinematic Object - Box2d with a kinematic physics body
|
|
11064
11223
|
* @extends Box2dObject
|
|
11065
11224
|
* @memberof Box2D
|
|
11066
11225
|
*/
|
|
11067
|
-
class
|
|
11226
|
+
class Box2dKinematicObject extends Box2dObject
|
|
11068
11227
|
{
|
|
11069
11228
|
/** Create a LittleJS object with Box2d physics
|
|
11070
11229
|
* @param {Vector2} [pos]
|
|
@@ -11116,20 +11275,19 @@ class Box2dTileLayer extends Box2dStaticObject
|
|
|
11116
11275
|
this.destroyAllFixtures();
|
|
11117
11276
|
|
|
11118
11277
|
// create box2d object for this layer
|
|
11119
|
-
const size = this.tileLayer.size;
|
|
11120
11278
|
this.pos = this.tileLayer.pos.copy();
|
|
11121
|
-
this.size = size.copy();
|
|
11279
|
+
this.size = this.tileLayer.size.copy();
|
|
11122
11280
|
|
|
11123
11281
|
// track which tiles have been processed
|
|
11124
11282
|
const processed = [];
|
|
11125
|
-
const getIndex = (x, y)=> x + y * size.x;
|
|
11283
|
+
const getIndex = (x, y)=> x + y * this.size.x;
|
|
11126
11284
|
const isSolidUnprocessed = (x, y)=>
|
|
11127
11285
|
!processed[getIndex(x, y)] &&
|
|
11128
11286
|
this.tileLayer.getCollisionData(vec2(x, y)) > 0;
|
|
11129
11287
|
|
|
11130
11288
|
// combine tiles into larger boxes
|
|
11131
|
-
for (let x = 0; x < size.x; ++x)
|
|
11132
|
-
for (let y = 0; y < size.y; ++y)
|
|
11289
|
+
for (let x = 0; x < this.size.x; ++x)
|
|
11290
|
+
for (let y = 0; y < this.size.y; ++y)
|
|
11133
11291
|
{
|
|
11134
11292
|
if (!isSolidUnprocessed(x, y)) continue;
|
|
11135
11293
|
|
|
@@ -12785,6 +12943,8 @@ export
|
|
|
12785
12943
|
saveCanvas,
|
|
12786
12944
|
saveDataURL,
|
|
12787
12945
|
shareURL,
|
|
12946
|
+
readSaveData,
|
|
12947
|
+
writeSaveData,
|
|
12788
12948
|
|
|
12789
12949
|
// Random
|
|
12790
12950
|
rand,
|
|
@@ -12994,7 +13154,7 @@ export
|
|
|
12994
13154
|
Box2dPlugin,
|
|
12995
13155
|
Box2dObject,
|
|
12996
13156
|
Box2dStaticObject,
|
|
12997
|
-
|
|
13157
|
+
Box2dKinematicObject,
|
|
12998
13158
|
Box2dRaycastResult,
|
|
12999
13159
|
Box2dJoint,
|
|
13000
13160
|
Box2dTargetJoint,
|