littlejsengine 1.18.2 → 1.18.7
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/COPYRIGHT.txt +38 -0
- package/FAQ.md +633 -0
- package/LICENSE +5 -27
- package/README.md +19 -1
- package/dist/littlejs.d.ts +550 -57
- package/dist/littlejs.esm.js +2208 -492
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +2165 -480
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +2166 -481
- package/package.json +8 -1
- package/plugins/box2d.js +36 -9
- package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
- package/plugins/pathFinder.js +758 -0
- package/plugins/pluginExport.js +26 -0
- package/plugins/tween.js +509 -0
- package/plugins/tweenSystem.js +509 -0
- package/plugins/uiSystem.js +151 -12
- package/src/engine.js +9 -145
- package/src/engineAudio.js +8 -5
- package/src/engineBuild.mjs +4 -1
- package/src/engineDebug.js +1 -1
- package/src/engineDraw.js +154 -5
- package/src/engineExport.js +17 -12
- package/src/engineInput.js +27 -1
- package/src/engineLogo.js +146 -0
- package/src/engineMath.js +10 -3
- package/src/engineObject.js +11 -5
- package/src/engineParticles.js +6 -3
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +17 -47
- package/src/engineUtilities.js +7 -2
- package/src/engineWebGL.js +31 -8
- package/src/jsconfig.json +3 -1
- package/.claude/settings.local.json +0 -7
- package/.github/workflows/test.yml +0 -17
- package/AI.md +0 -172
- package/CLAUDE.md +0 -1
- package/examples/box2d/game.js +0 -192
- package/examples/box2d/gameObjects.js +0 -564
- package/examples/box2d/index.html +0 -9
- package/examples/box2d/scenes.js +0 -194
- package/examples/box2d/tiles.png +0 -0
- package/examples/breakout/game.js +0 -175
- package/examples/breakout/gameObjects.js +0 -147
- package/examples/breakout/index.html +0 -8
- package/examples/breakout/tiles.png +0 -0
- package/examples/breakoutTutorial/README.md +0 -521
- package/examples/breakoutTutorial/game.js +0 -191
- package/examples/breakoutTutorial/images/1.png +0 -0
- package/examples/breakoutTutorial/images/10.png +0 -0
- package/examples/breakoutTutorial/images/11.png +0 -0
- package/examples/breakoutTutorial/images/2.png +0 -0
- package/examples/breakoutTutorial/images/3.png +0 -0
- package/examples/breakoutTutorial/images/4.png +0 -0
- package/examples/breakoutTutorial/images/5.png +0 -0
- package/examples/breakoutTutorial/images/6.png +0 -0
- package/examples/breakoutTutorial/images/7.png +0 -0
- package/examples/breakoutTutorial/images/8.png +0 -0
- package/examples/breakoutTutorial/images/9.png +0 -0
- package/examples/breakoutTutorial/index.html +0 -8
- package/examples/electron/build.mjs +0 -127
- package/examples/electron/electron.js +0 -35
- package/examples/electron/game.js +0 -54
- package/examples/electron/index.html +0 -13
- package/examples/electron/package.json +0 -5
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +0 -51
- package/examples/empty/index.html +0 -5
- package/examples/empty/tiles.png +0 -0
- package/examples/favicon.png +0 -0
- package/examples/games.jpg +0 -0
- package/examples/htmlMenu/game.js +0 -82
- package/examples/htmlMenu/index.html +0 -45
- package/examples/htmlMenu/tiles.png +0 -0
- package/examples/index.html +0 -800
- package/examples/logo.png +0 -0
- package/examples/logo2.png +0 -0
- package/examples/module/build.mjs +0 -124
- package/examples/module/game.js +0 -132
- package/examples/module/index.html +0 -10
- package/examples/module/tiles.png +0 -0
- package/examples/particles/index.html +0 -426
- package/examples/particles/tiles.png +0 -0
- package/examples/platformer/data/gameLevelData.tmx +0 -143
- package/examples/platformer/data/gameLevelData.tsx +0 -4
- package/examples/platformer/game.js +0 -147
- package/examples/platformer/gameCharacter.js +0 -309
- package/examples/platformer/gameEffects.js +0 -278
- package/examples/platformer/gameLevel.js +0 -205
- package/examples/platformer/gameLevelData.json +0 -171
- package/examples/platformer/gameObjects.js +0 -378
- package/examples/platformer/gamePlayer.js +0 -35
- package/examples/platformer/index.html +0 -9
- package/examples/platformer/tiles.png +0 -0
- package/examples/platformer/tilesLevel.png +0 -0
- package/examples/puzzle/game.js +0 -331
- package/examples/puzzle/index.html +0 -8
- package/examples/puzzle/tiles.png +0 -0
- package/examples/screenshot.jpg +0 -0
- package/examples/shorts/animation.js +0 -18
- package/examples/shorts/base.html +0 -54
- package/examples/shorts/blending.js +0 -14
- package/examples/shorts/box2d.js +0 -51
- package/examples/shorts/box2dCar.js +0 -59
- package/examples/shorts/box2dPool.js +0 -107
- package/examples/shorts/box2dTileLayer.js +0 -48
- package/examples/shorts/cameraDrag.js +0 -22
- package/examples/shorts/clock.js +0 -21
- package/examples/shorts/colors.js +0 -25
- package/examples/shorts/debugDraw.js +0 -37
- package/examples/shorts/empty.js +0 -30
- package/examples/shorts/flappyGame.js +0 -55
- package/examples/shorts/fontImage.js +0 -17
- package/examples/shorts/fps.js +0 -90
- package/examples/shorts/helloWorld.js +0 -11
- package/examples/shorts/hillGlideGame.js +0 -63
- package/examples/shorts/input.js +0 -64
- package/examples/shorts/landerGame.js +0 -57
- package/examples/shorts/maze.js +0 -48
- package/examples/shorts/medals.js +0 -51
- package/examples/shorts/music.js +0 -78
- package/examples/shorts/musicPlayer.js +0 -137
- package/examples/shorts/nineSlice.js +0 -40
- package/examples/shorts/parallax.js +0 -72
- package/examples/shorts/particles.js +0 -28
- package/examples/shorts/piano.js +0 -44
- package/examples/shorts/platformer.js +0 -45
- package/examples/shorts/pongGame.js +0 -41
- package/examples/shorts/postProcess.js +0 -62
- package/examples/shorts/sequencer.js +0 -124
- package/examples/shorts/shader.js +0 -29
- package/examples/shorts/shapes.js +0 -21
- package/examples/shorts/slidingPuzzle.js +0 -52
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/sound.js +0 -36
- package/examples/shorts/spaceGame.js +0 -58
- package/examples/shorts/spriteAtlas.js +0 -31
- package/examples/shorts/starfield.js +0 -15
- package/examples/shorts/texture.js +0 -16
- package/examples/shorts/tileLayer.js +0 -48
- package/examples/shorts/tileRaycast.js +0 -39
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/tiltedView.js +0 -63
- package/examples/shorts/timers.js +0 -53
- package/examples/shorts/topDown.js +0 -38
- package/examples/shorts/uiSystem.js +0 -61
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +0 -34
- package/examples/starter/build.bat +0 -7
- package/examples/starter/build.mjs +0 -126
- package/examples/starter/game.js +0 -137
- package/examples/starter/index.html +0 -35
- package/examples/starter/tiles.png +0 -0
- package/examples/stress/index.html +0 -173
- package/examples/style.css +0 -150
- package/examples/typescript/build.mjs +0 -60
- package/examples/typescript/game.js +0 -100
- package/examples/typescript/game.ts +0 -132
- package/examples/typescript/index.html +0 -10
- package/examples/typescript/tiles.png +0 -0
- package/examples/typescript/tsconfig.json +0 -17
- package/examples/uiSystem/game.js +0 -139
- package/examples/uiSystem/index.html +0 -10
- package/examples/uiSystem/tiles.png +0 -0
- package/jsconfig.json +0 -12
- package/plugins/desktop.ini +0 -2
- package/reference.md +0 -447
- package/test/math.test.mjs +0 -774
- package/test/setup.mjs +0 -22
- package/test/smoke.test.mjs +0 -258
- package/test/util.test.mjs +0 -80
package/dist/littlejs.esm.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
38
|
+
const engineVersion = '1.18.7';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -69,7 +69,7 @@ let frame = 0;
|
|
|
69
69
|
* @memberof Engine */
|
|
70
70
|
let time = 0;
|
|
71
71
|
|
|
72
|
-
/** Actual clock time since start in seconds (not affected by pause or frame rate clamping)
|
|
72
|
+
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
73
73
|
* @type {number}
|
|
74
74
|
* @memberof Engine */
|
|
75
75
|
let timeReal = 0;
|
|
@@ -200,11 +200,14 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
200
200
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
201
201
|
const debugSpeedUp = debug && keyIsDown('Equal'); // +
|
|
202
202
|
const debugSpeedDown = debug && keyIsDown('Minus'); // -
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
204
|
+
|
|
205
|
+
// apply time deltas
|
|
206
|
+
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
207
|
+
const combinedScale = timeScale * debugScale;
|
|
208
|
+
frameTimeDeltaMS *= combinedScale;
|
|
206
209
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
207
|
-
if (
|
|
210
|
+
if (debugScale <= 1)
|
|
208
211
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
209
212
|
|
|
210
213
|
let wasUpdated = false;
|
|
@@ -562,145 +565,6 @@ function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
|
562
565
|
|
|
563
566
|
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
564
567
|
return hitObjects;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
568
|
-
function drawEngineLogo(t)
|
|
569
|
-
{
|
|
570
|
-
const blackAndWhite = 0;
|
|
571
|
-
const showName = 1;
|
|
572
|
-
|
|
573
|
-
// LittleJS Logo and Splash Screen
|
|
574
|
-
const x = mainContext;
|
|
575
|
-
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
576
|
-
const w = mainCanvas.width = innerWidth * dpr;
|
|
577
|
-
const h = mainCanvas.height = innerHeight * dpr;
|
|
578
|
-
{
|
|
579
|
-
// background
|
|
580
|
-
const p3 = percent(t, 1, .8);
|
|
581
|
-
const p4 = percent(t, 0, .5);
|
|
582
|
-
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
|
|
583
|
-
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
584
|
-
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
585
|
-
x.save();
|
|
586
|
-
x.fillStyle = g;
|
|
587
|
-
x.fillRect(0,0,w,h);
|
|
588
|
-
}
|
|
589
|
-
const gradient = (X1,Y1,X2,Y2,C,S=1)=>
|
|
590
|
-
{
|
|
591
|
-
if (C >= 0)
|
|
592
|
-
{
|
|
593
|
-
if (blackAndWhite)
|
|
594
|
-
x.fillStyle = '#fff';
|
|
595
|
-
else
|
|
596
|
-
{
|
|
597
|
-
const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
|
|
598
|
-
g.addColorStop(0,color(C,2));
|
|
599
|
-
g.addColorStop(1,color(C,1));
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
else
|
|
603
|
-
x.fillStyle = '#000';
|
|
604
|
-
C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
|
|
605
|
-
}
|
|
606
|
-
const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
|
|
607
|
-
{
|
|
608
|
-
x.beginPath();
|
|
609
|
-
x.arc(X,Y,R,p*A,p*B);
|
|
610
|
-
gradient(X,Y-R,X,Y+R,C,S);
|
|
611
|
-
}
|
|
612
|
-
const rect = (X,Y,W,H,C)=>
|
|
613
|
-
{
|
|
614
|
-
x.beginPath();
|
|
615
|
-
x.rect(X,Y,W,H*p);
|
|
616
|
-
gradient(X,Y+H,X+W,Y,C);
|
|
617
|
-
}
|
|
618
|
-
const poly = (points,C,Y,H)=>
|
|
619
|
-
{
|
|
620
|
-
x.beginPath();
|
|
621
|
-
for (const p of points)
|
|
622
|
-
x.lineTo(p.x, p.y);
|
|
623
|
-
x.closePath();
|
|
624
|
-
gradient(0, Y, 0, Y+H,C);
|
|
625
|
-
}
|
|
626
|
-
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
|
|
627
|
-
|
|
628
|
-
// center and fit tos screen
|
|
629
|
-
const alpha = oscillate(1,1,t);
|
|
630
|
-
const p = percent(alpha, .1, .5);
|
|
631
|
-
const size = min(6, min(w,h)/99);
|
|
632
|
-
x.translate(w/2,h/2);
|
|
633
|
-
x.scale(size,size);
|
|
634
|
-
x.translate(-40,-35);
|
|
635
|
-
p < 1 && x.setLineDash([99*p,99]);
|
|
636
|
-
x.lineJoin = x.lineCap = 'round';
|
|
637
|
-
x.lineWidth = .1 + p*1.9;
|
|
638
|
-
//x.strokeStyle='#fff7';
|
|
639
|
-
|
|
640
|
-
if (showName)
|
|
641
|
-
{
|
|
642
|
-
// engine name text
|
|
643
|
-
const Y = 54;
|
|
644
|
-
const s = 'LittleJS';
|
|
645
|
-
x.font = '900 15.5px arial';
|
|
646
|
-
x.lineWidth = .1+p*3.9;
|
|
647
|
-
x.textAlign = 'center';
|
|
648
|
-
x.textBaseline = 'top';
|
|
649
|
-
rect(11,Y+1,59,8*p,-1);
|
|
650
|
-
x.beginPath();
|
|
651
|
-
|
|
652
|
-
let w2 = 0;
|
|
653
|
-
for (let i=0;i<s.length;++i)
|
|
654
|
-
w2 += x.measureText(s[i]).width;
|
|
655
|
-
for (let j=2;j--;)
|
|
656
|
-
for (let i=0,X=40-w2/2;i<s.length;++i)
|
|
657
|
-
{
|
|
658
|
-
const w = x.measureText(s[i]).width, X2 = X+w/2;
|
|
659
|
-
gradient(X2,Y,X2+2,Y+13,i>5?1:0);
|
|
660
|
-
x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
|
|
661
|
-
X += w;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
x.lineWidth = .1 + p*1.9;
|
|
665
|
-
rect(3,Y,73,0); // bottom
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
rect(7,15,26,-7,0); // cab top
|
|
669
|
-
rect(25,15,8,25,-1); // cab front
|
|
670
|
-
rect(10,40,15,-25,1); // cab back
|
|
671
|
-
rect(14,21,7,9,2); // cab window
|
|
672
|
-
rect(38,20,6,-6,2); // little stack
|
|
673
|
-
|
|
674
|
-
// big stack
|
|
675
|
-
rect(49,20,10,-6,0);
|
|
676
|
-
const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
|
|
677
|
-
poly(stackPoints,2,8,6*p);
|
|
678
|
-
rect(44,8,20,-7,0);
|
|
679
|
-
|
|
680
|
-
// engine
|
|
681
|
-
for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
|
|
682
|
-
circle(59,30,4,0,7,2); // light
|
|
683
|
-
|
|
684
|
-
// engine outline
|
|
685
|
-
rect(35,20,24,0); // top
|
|
686
|
-
circle(59,30,10); // front
|
|
687
|
-
circle(47,30,10,PI/2,PI*3/2); // middle
|
|
688
|
-
circle(35,30,10,PI/2,PI*3/2); // back
|
|
689
|
-
rect(7,40,13,7,-1); // bottom back
|
|
690
|
-
rect(17,40,43,14,-1); // bottom center
|
|
691
|
-
|
|
692
|
-
// wheels
|
|
693
|
-
for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
|
|
694
|
-
|
|
695
|
-
// cowcatcher
|
|
696
|
-
for (let i=2;i--;)
|
|
697
|
-
{
|
|
698
|
-
let w=6, s=7, o=53+w*p*i
|
|
699
|
-
const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
|
|
700
|
-
poly(points,0,40,14);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
x.restore();
|
|
704
568
|
}
|
|
705
569
|
/**
|
|
706
570
|
* LittleJS Debug System
|
|
@@ -746,7 +610,7 @@ let debugKey = 'Escape';
|
|
|
746
610
|
let debugOverlay = false;
|
|
747
611
|
|
|
748
612
|
// Engine internal variables not exposed to documentation
|
|
749
|
-
let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false,
|
|
613
|
+
let debugPrimitives = [], debugPhysics = false, debugRaycast = false, debugParticles = false, debugGamepads = false, debugTakeScreenshot;
|
|
750
614
|
|
|
751
615
|
///////////////////////////////////////////////////////////////////////////////
|
|
752
616
|
// Debug helper functions
|
|
@@ -1577,7 +1441,11 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
|
1577
1441
|
* @return {number}
|
|
1578
1442
|
* @memberof Math */
|
|
1579
1443
|
function distanceWrap(valueA, valueB, wrapSize=1)
|
|
1580
|
-
{
|
|
1444
|
+
{
|
|
1445
|
+
ASSERT(wrapSize > 0, 'distanceWrap wrapSize must be > 0');
|
|
1446
|
+
const d = (valueA - valueB) % wrapSize;
|
|
1447
|
+
return d*2 % wrapSize - d;
|
|
1448
|
+
}
|
|
1581
1449
|
|
|
1582
1450
|
/** Linearly interpolates between values passed in with wrapping
|
|
1583
1451
|
* @param {number} valueA
|
|
@@ -1894,6 +1762,7 @@ class RandomGenerator
|
|
|
1894
1762
|
* @param {number} [seed] - Starting seed or engine default seed */
|
|
1895
1763
|
constructor(seed = 123456789)
|
|
1896
1764
|
{
|
|
1765
|
+
ASSERT(seed !== 0, 'RandomGenerator seed must be non-zero (xorshift is fixed at 0)');
|
|
1897
1766
|
/** @property {number} - random seed */
|
|
1898
1767
|
this.seed = seed;
|
|
1899
1768
|
}
|
|
@@ -2187,8 +2056,10 @@ class Vector2
|
|
|
2187
2056
|
* @return {Vector2} */
|
|
2188
2057
|
floor() { return new Vector2(floor(this.x), floor(this.y)); }
|
|
2189
2058
|
|
|
2190
|
-
/** Returns a copy of this vector snapped to a grid
|
|
2191
|
-
*
|
|
2059
|
+
/** Returns a copy of this vector snapped to a grid. Note that `grid` is
|
|
2060
|
+
* the number of snap steps per unit (so `grid=2` snaps to halves and
|
|
2061
|
+
* `grid=0.5` snaps to twos), not the cell size.
|
|
2062
|
+
* @param {number} grid - snap steps per unit
|
|
2192
2063
|
* @return {Vector2} */
|
|
2193
2064
|
snap(grid)
|
|
2194
2065
|
{
|
|
@@ -2758,9 +2629,14 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
2758
2629
|
{
|
|
2759
2630
|
ASSERT(isString(saveName), 'loadData requires saveName string');
|
|
2760
2631
|
|
|
2761
|
-
// replace undefined values with defaults
|
|
2632
|
+
// replace undefined values with defaults; tolerate corrupt JSON
|
|
2762
2633
|
const data = localStorage[saveName];
|
|
2763
|
-
|
|
2634
|
+
let loadedData = {};
|
|
2635
|
+
if (data)
|
|
2636
|
+
{
|
|
2637
|
+
try { loadedData = JSON.parse(data); }
|
|
2638
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
2639
|
+
}
|
|
2764
2640
|
return { ...defaultSaveData, ...loadedData };
|
|
2765
2641
|
}
|
|
2766
2642
|
|
|
@@ -2800,6 +2676,18 @@ let cameraAngle = 0;
|
|
|
2800
2676
|
* @memberof Settings */
|
|
2801
2677
|
let cameraScale = 32;
|
|
2802
2678
|
|
|
2679
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2680
|
+
// Time settings
|
|
2681
|
+
|
|
2682
|
+
/** Scale applied to engine time, can be used for slow motion or fast forward
|
|
2683
|
+
* - 1 is normal speed, 2 is double speed, 0.5 is half speed
|
|
2684
|
+
* - 0 freezes the simulation without setting the paused flag
|
|
2685
|
+
* - Should be >= 0; stacks multiplicatively with the debug +/- shortcut
|
|
2686
|
+
* @type {number}
|
|
2687
|
+
* @default
|
|
2688
|
+
* @memberof Settings */
|
|
2689
|
+
let timeScale = 1;
|
|
2690
|
+
|
|
2803
2691
|
///////////////////////////////////////////////////////////////////////////////
|
|
2804
2692
|
// Display settings
|
|
2805
2693
|
|
|
@@ -3083,33 +2971,6 @@ let soundDefaultRange = 40;
|
|
|
3083
2971
|
* @memberof Settings */
|
|
3084
2972
|
let soundDefaultTaper = .7;
|
|
3085
2973
|
|
|
3086
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3087
|
-
// Medals settings
|
|
3088
|
-
|
|
3089
|
-
/** How long to show medals for in seconds
|
|
3090
|
-
* @type {number}
|
|
3091
|
-
* @default
|
|
3092
|
-
* @memberof Settings */
|
|
3093
|
-
let medalDisplayTime = 5;
|
|
3094
|
-
|
|
3095
|
-
/** How quickly to slide on/off medals in seconds
|
|
3096
|
-
* @type {number}
|
|
3097
|
-
* @default
|
|
3098
|
-
* @memberof Settings */
|
|
3099
|
-
let medalDisplaySlideTime = .5;
|
|
3100
|
-
|
|
3101
|
-
/** Size of medal display
|
|
3102
|
-
* @type {Vector2}
|
|
3103
|
-
* @default Vector2(640,80)
|
|
3104
|
-
* @memberof Settings */
|
|
3105
|
-
let medalDisplaySize = vec2(640, 80);
|
|
3106
|
-
|
|
3107
|
-
/** Set to stop medals from being unlockable (like if cheats are enabled)
|
|
3108
|
-
* @type {boolean}
|
|
3109
|
-
* @default
|
|
3110
|
-
* @memberof Settings */
|
|
3111
|
-
let medalsPreventUnlock = false;
|
|
3112
|
-
|
|
3113
2974
|
///////////////////////////////////////////////////////////////////////////////
|
|
3114
2975
|
// Setters for global variables
|
|
3115
2976
|
|
|
@@ -3128,6 +2989,11 @@ function setCameraAngle(angle) { cameraAngle = angle; }
|
|
|
3128
2989
|
* @memberof Settings */
|
|
3129
2990
|
function setCameraScale(scale) { cameraScale = scale; }
|
|
3130
2991
|
|
|
2992
|
+
/** Set scale applied to engine time
|
|
2993
|
+
* @param {number} scale
|
|
2994
|
+
* @memberof Settings */
|
|
2995
|
+
function setTimeScale(scale) { timeScale = scale; }
|
|
2996
|
+
|
|
3131
2997
|
/** Set if tiles should be colorized when using canvas2d
|
|
3132
2998
|
* This can be slower but results should look nearly identical to WebGL rendering
|
|
3133
2999
|
* It can be enabled/disabled at any time
|
|
@@ -3366,26 +3232,6 @@ function setSoundDefaultRange(range) { soundDefaultRange = range; }
|
|
|
3366
3232
|
* @memberof Settings */
|
|
3367
3233
|
function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
|
|
3368
3234
|
|
|
3369
|
-
/** Set how long to show medals for in seconds
|
|
3370
|
-
* @param {number} time
|
|
3371
|
-
* @memberof Settings */
|
|
3372
|
-
function setMedalDisplayTime(time) { medalDisplayTime = time; }
|
|
3373
|
-
|
|
3374
|
-
/** Set how quickly to slide on/off medals in seconds
|
|
3375
|
-
* @param {number} time
|
|
3376
|
-
* @memberof Settings */
|
|
3377
|
-
function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
|
|
3378
|
-
|
|
3379
|
-
/** Set size of medal display
|
|
3380
|
-
* @param {Vector2} size
|
|
3381
|
-
* @memberof Settings */
|
|
3382
|
-
function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
|
|
3383
|
-
|
|
3384
|
-
/** Set to stop medals from being unlockable
|
|
3385
|
-
* @param {boolean} preventUnlock
|
|
3386
|
-
* @memberof Settings */
|
|
3387
|
-
function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
|
|
3388
|
-
|
|
3389
3235
|
/** Set if watermark with FPS should be shown
|
|
3390
3236
|
* @param {boolean} show
|
|
3391
3237
|
* @memberof Debug */
|
|
@@ -3527,10 +3373,18 @@ class EngineObject
|
|
|
3527
3373
|
const parent = this.parent;
|
|
3528
3374
|
if (parent)
|
|
3529
3375
|
{
|
|
3530
|
-
//
|
|
3376
|
+
// compose with parent transform inline to avoid intermediate vector allocs
|
|
3531
3377
|
const mirror = parent.getMirrorSign();
|
|
3532
|
-
|
|
3533
|
-
|
|
3378
|
+
const lp = this.localPos, pp = parent.pos;
|
|
3379
|
+
const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
|
|
3380
|
+
if (pa)
|
|
3381
|
+
{
|
|
3382
|
+
const c = cos(-pa), s = sin(-pa);
|
|
3383
|
+
this.pos = new Vector2(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
|
|
3384
|
+
}
|
|
3385
|
+
else
|
|
3386
|
+
this.pos = new Vector2(lx + pp.x, ly + pp.y);
|
|
3387
|
+
this.angle = mirror*this.localAngle + pa;
|
|
3534
3388
|
}
|
|
3535
3389
|
|
|
3536
3390
|
// update children
|
|
@@ -3896,8 +3750,6 @@ class EngineObject
|
|
|
3896
3750
|
* @return {string} */
|
|
3897
3751
|
toString()
|
|
3898
3752
|
{
|
|
3899
|
-
if (!debug) return;
|
|
3900
|
-
|
|
3901
3753
|
let text = 'type = ' + this.constructor.name;
|
|
3902
3754
|
if (this.pos.x || this.pos.y)
|
|
3903
3755
|
text += '\npos = ' + this.pos;
|
|
@@ -4002,6 +3854,12 @@ let textureInfos = [];
|
|
|
4002
3854
|
* @memberof Draw */
|
|
4003
3855
|
let drawCount;
|
|
4004
3856
|
|
|
3857
|
+
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
3858
|
+
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
3859
|
+
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
3860
|
+
/** @param {Color} c */ function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
|
|
3861
|
+
/** @param {Color} c */ function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
3862
|
+
|
|
4005
3863
|
///////////////////////////////////////////////////////////////////////////////
|
|
4006
3864
|
|
|
4007
3865
|
/**
|
|
@@ -4141,8 +3999,9 @@ class TextureInfo
|
|
|
4141
3999
|
* Create a TextureInfo, called automatically by the engine
|
|
4142
4000
|
* @param {HTMLImageElement|OffscreenCanvas} image
|
|
4143
4001
|
* @param {boolean} [useWebGL] - Should use WebGL if available?
|
|
4002
|
+
* @param {boolean} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)?
|
|
4144
4003
|
*/
|
|
4145
|
-
constructor(image, useWebGL=true)
|
|
4004
|
+
constructor(image, useWebGL=true, wrap=false)
|
|
4146
4005
|
{
|
|
4147
4006
|
/** @property {HTMLImageElement|OffscreenCanvas} - image source */
|
|
4148
4007
|
this.image = image;
|
|
@@ -4152,6 +4011,8 @@ class TextureInfo
|
|
|
4152
4011
|
this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
|
|
4153
4012
|
/** @property {WebGLTexture} - WebGL texture */
|
|
4154
4013
|
this.glTexture = undefined;
|
|
4014
|
+
/** @property {boolean} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE */
|
|
4015
|
+
this.wrap = wrap;
|
|
4155
4016
|
useWebGL && this.createWebGLTexture();
|
|
4156
4017
|
}
|
|
4157
4018
|
|
|
@@ -4164,6 +4025,14 @@ class TextureInfo
|
|
|
4164
4025
|
/** Check if the texture is webgl enabled
|
|
4165
4026
|
* @return {boolean} */
|
|
4166
4027
|
hasWebGL() { return !!this.glTexture; }
|
|
4028
|
+
|
|
4029
|
+
/** Set the wrap mode for this texture
|
|
4030
|
+
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE */
|
|
4031
|
+
setWrap(wrap=true)
|
|
4032
|
+
{
|
|
4033
|
+
this.wrap = wrap;
|
|
4034
|
+
glSetTextureWrap(this.glTexture, wrap);
|
|
4035
|
+
}
|
|
4167
4036
|
}
|
|
4168
4037
|
|
|
4169
4038
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4225,8 +4094,13 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
4225
4094
|
}
|
|
4226
4095
|
else
|
|
4227
4096
|
{
|
|
4228
|
-
// if no tile info, force untextured
|
|
4229
|
-
|
|
4097
|
+
// if no tile info, force untextured by zeroing rgba (so whatever
|
|
4098
|
+
// texture is bound doesn't leak in) and folding color+additive
|
|
4099
|
+
// into the additive slot — matches the Canvas2D path's
|
|
4100
|
+
// color.add(additiveColor) on line ~337.
|
|
4101
|
+
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
4102
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
|
|
4103
|
+
0, combined.rgbaInt());
|
|
4230
4104
|
}
|
|
4231
4105
|
}
|
|
4232
4106
|
else
|
|
@@ -4331,6 +4205,98 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4331
4205
|
}
|
|
4332
4206
|
}
|
|
4333
4207
|
|
|
4208
|
+
/** Draw a texture tiled (wrapped) across a rectangle in world space.
|
|
4209
|
+
* Useful for backgrounds, repeating patterns, and seamless fills.
|
|
4210
|
+
* The whole texture is tiled — sub-region (TileInfo) wrapping is not supported.
|
|
4211
|
+
* @param {Vector2} pos - Center of the rect in world space
|
|
4212
|
+
* @param {Vector2} size - Size of the rect in world space
|
|
4213
|
+
* @param {Vector2} wrapCount - How many times the texture repeats (x, y)
|
|
4214
|
+
* @param {TextureInfo|number} [texture=0] - TextureInfo or texture index into textureInfos
|
|
4215
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
4216
|
+
* @param {number} [angle=0] - Angle to rotate by
|
|
4217
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
4218
|
+
* @param {boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering?
|
|
4219
|
+
* @param {boolean} [screenSpace=false] - Are pos and size in screen space?
|
|
4220
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
4221
|
+
* @memberof Draw */
|
|
4222
|
+
function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
4223
|
+
angle=0, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
4224
|
+
{
|
|
4225
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4226
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
4227
|
+
ASSERT(isVector2(wrapCount), 'wrapCount must be a vec2');
|
|
4228
|
+
ASSERT(isColor(color), 'color is invalid');
|
|
4229
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4230
|
+
ASSERT(!additiveColor || isColor(additiveColor), 'additiveColor must be a color');
|
|
4231
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4232
|
+
ASSERT(!(texture instanceof TileInfo),
|
|
4233
|
+
'pass a TextureInfo or texture index, not a TileInfo — use tileInfo.textureInfo');
|
|
4234
|
+
|
|
4235
|
+
// short-circuit before texture lookup — textureInfos[0] is undefined in headless mode
|
|
4236
|
+
if (headlessMode) return;
|
|
4237
|
+
|
|
4238
|
+
// resolve texture argument: TextureInfo or index
|
|
4239
|
+
const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
|
|
4240
|
+
ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
|
|
4241
|
+
ASSERT(textureInfo.size.x > 0, 'texture not loaded');
|
|
4242
|
+
ASSERT(textureInfo.wrap,
|
|
4243
|
+
'drawTextureWrapped requires a wrap-enabled texture; call textureInfo.setWrap(true) first');
|
|
4244
|
+
|
|
4245
|
+
if (useWebGL && glEnable)
|
|
4246
|
+
{
|
|
4247
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4248
|
+
if (screenSpace)
|
|
4249
|
+
[pos, size, angle] = screenToWorldTransform(pos, size, angle);
|
|
4250
|
+
glSetTexture(textureInfo.glTexture);
|
|
4251
|
+
glDraw(pos.x, pos.y, size.x, size.y, angle,
|
|
4252
|
+
0, 0, wrapCount.x, wrapCount.y,
|
|
4253
|
+
color.rgbaInt(), additiveColor && additiveColor.rgbaInt());
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
// Canvas2D path — increment drawCount here (WebGL batch counts via glBatchCount)
|
|
4258
|
+
++drawCount;
|
|
4259
|
+
|
|
4260
|
+
if (!screenSpace)
|
|
4261
|
+
{
|
|
4262
|
+
pos = worldToScreen(pos);
|
|
4263
|
+
size = size.scale(cameraScale);
|
|
4264
|
+
angle -= cameraAngle;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
// pick image source: raw, or tinted bake. Match drawImageColor's
|
|
4268
|
+
// "no tint needed" predicate so behavior stays consistent.
|
|
4269
|
+
const noTint = !canvasColorTiles ||
|
|
4270
|
+
(additiveColor
|
|
4271
|
+
? isWhite(color.add(additiveColor)) && additiveColor.a <= 0
|
|
4272
|
+
: isWhite(color));
|
|
4273
|
+
// alpha is baked into pixels by bakeTintedImage's additive branch;
|
|
4274
|
+
// in that case globalAlpha must NOT also apply color.a
|
|
4275
|
+
const alphaBaked = !noTint && additiveColor && !isBlack(additiveColor);
|
|
4276
|
+
const source = noTint
|
|
4277
|
+
? textureInfo.image
|
|
4278
|
+
: bakeTintedImage(textureInfo.image, color, additiveColor);
|
|
4279
|
+
|
|
4280
|
+
context = context || drawContext;
|
|
4281
|
+
context.save();
|
|
4282
|
+
context.translate(pos.x + .5, pos.y + .5);
|
|
4283
|
+
context.rotate(angle);
|
|
4284
|
+
context.globalAlpha = alphaBaked ? 1 : color.a;
|
|
4285
|
+
|
|
4286
|
+
const pattern = context.createPattern(source, 'repeat');
|
|
4287
|
+
// map pattern-source pixels into user space so the rect contains
|
|
4288
|
+
// wrapCount.x × wrapCount.y repeats
|
|
4289
|
+
const m = new DOMMatrix()
|
|
4290
|
+
.translate(-size.x/2, -size.y/2)
|
|
4291
|
+
.scale(size.x / (wrapCount.x * source.width),
|
|
4292
|
+
size.y / (wrapCount.y * source.height));
|
|
4293
|
+
pattern.setTransform(m);
|
|
4294
|
+
context.fillStyle = pattern;
|
|
4295
|
+
context.fillRect(-size.x/2, -size.y/2, size.x, size.y);
|
|
4296
|
+
context.globalAlpha = 1;
|
|
4297
|
+
context.restore();
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4334
4300
|
/** Draw connected lines between a series of points
|
|
4335
4301
|
* @param {Array<Vector2>} points
|
|
4336
4302
|
* @param {number} [width]
|
|
@@ -4854,6 +4820,43 @@ function combineCanvases()
|
|
|
4854
4820
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
4855
4821
|
}
|
|
4856
4822
|
|
|
4823
|
+
// Internal: bake a color/additive-color tint into workReadCanvas at the
|
|
4824
|
+
// image's native resolution. Returns the work canvas, suitable for
|
|
4825
|
+
// passing to context.createPattern. Used by drawTextureWrapped's
|
|
4826
|
+
// Canvas2D path. Caller is responsible for short-circuiting when no
|
|
4827
|
+
// tint is needed (i.e. color is white and additiveColor is black/none).
|
|
4828
|
+
function bakeTintedImage(image, color, additiveColor)
|
|
4829
|
+
{
|
|
4830
|
+
const w = image.width|0, h = image.height|0;
|
|
4831
|
+
workReadCanvas.width = w;
|
|
4832
|
+
workReadCanvas.height = h;
|
|
4833
|
+
workReadContext.drawImage(image, 0, 0);
|
|
4834
|
+
|
|
4835
|
+
const imageData = workReadContext.getImageData(0, 0, w, h);
|
|
4836
|
+
const data = imageData.data;
|
|
4837
|
+
if (additiveColor && !isBlack(additiveColor))
|
|
4838
|
+
{
|
|
4839
|
+
// multiply + additive (slower)
|
|
4840
|
+
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
4841
|
+
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
4842
|
+
additiveColor.b * 255, additiveColor.a * 255];
|
|
4843
|
+
for (let i = 0; i < data.length; ++i)
|
|
4844
|
+
data[i] = data[i] * colorMultiply[i&3] + colorAdd[i&3] |0;
|
|
4845
|
+
}
|
|
4846
|
+
else
|
|
4847
|
+
{
|
|
4848
|
+
// RGB only, faster — alpha left intact for the caller
|
|
4849
|
+
for (let i = 0; i < data.length; i+=4)
|
|
4850
|
+
{
|
|
4851
|
+
data[i ] *= color.r;
|
|
4852
|
+
data[i+1] *= color.g;
|
|
4853
|
+
data[i+2] *= color.b;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
workReadContext.putImageData(imageData, 0, 0);
|
|
4857
|
+
return workReadCanvas;
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4857
4860
|
/** Helper function to draw an image with color and additive color applied
|
|
4858
4861
|
* This is slower then normal drawImage when color is applied
|
|
4859
4862
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -4872,8 +4875,6 @@ function combineCanvases()
|
|
|
4872
4875
|
* @memberof Draw */
|
|
4873
4876
|
function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
|
|
4874
4877
|
{
|
|
4875
|
-
function isWhite(c) { return c.r >= 1 && c.g >= 1 && c.b >= 1; }
|
|
4876
|
-
function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
4877
4878
|
const sx2 = bleed;
|
|
4878
4879
|
const sy2 = bleed;
|
|
4879
4880
|
sWidth = max(1,sWidth|0);
|
|
@@ -5148,7 +5149,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
|
5148
5149
|
* This is useful to disable for html menus so the browser can handle input normally
|
|
5149
5150
|
* @param {boolean} preventDefault
|
|
5150
5151
|
* @memberof Input */
|
|
5151
|
-
function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
|
|
5152
|
+
function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
|
|
5152
5153
|
|
|
5153
5154
|
/** Clears an input key state
|
|
5154
5155
|
* @param {string|number} key
|
|
@@ -5342,6 +5343,32 @@ function gamepadStickCount(gamepad=gamepadPrimary)
|
|
|
5342
5343
|
return gamepadStickData[gamepad]?.length ?? 0;
|
|
5343
5344
|
}
|
|
5344
5345
|
|
|
5346
|
+
/** Pulse a gamepad's vibration hardware using the dual-rumble effect if it exists
|
|
5347
|
+
* Strong magnitude is usually the left side motor, weak magnitude is usually the right side motor
|
|
5348
|
+
* @param {number} [gamepad] - gamepad index
|
|
5349
|
+
* @param {number} [duration] - effect duration in ms
|
|
5350
|
+
* @param {number} [strongMagnitude] - strong (left) motor intensity, 0 to 1
|
|
5351
|
+
* @param {number} [weakMagnitude] - weak (right) motor intensity, 0 to 1
|
|
5352
|
+
* @param {number} [startDelay] - delay in ms before the effect starts
|
|
5353
|
+
* @memberof Input */
|
|
5354
|
+
function gamepadVibrate(gamepad=gamepadPrimary, duration=200, strongMagnitude=1, weakMagnitude=1, startDelay=0)
|
|
5355
|
+
{
|
|
5356
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5357
|
+
if (!vibrateEnable || headlessMode) return;
|
|
5358
|
+
const pad = navigator?.getGamepads?.()[gamepad];
|
|
5359
|
+
pad?.vibrationActuator?.playEffect?.('dual-rumble', {duration, strongMagnitude, weakMagnitude, startDelay});
|
|
5360
|
+
}
|
|
5361
|
+
|
|
5362
|
+
/** Stop vibration on a gamepad
|
|
5363
|
+
* @memberof Input */
|
|
5364
|
+
function gamepadVibrateStop(gamepad=gamepadPrimary)
|
|
5365
|
+
{
|
|
5366
|
+
ASSERT(isNumber(gamepad), 'gamepad must be a number');
|
|
5367
|
+
if (!vibrateEnable || headlessMode) return;
|
|
5368
|
+
const pad = navigator?.getGamepads?.()[gamepad];
|
|
5369
|
+
pad?.vibrationActuator?.reset?.();
|
|
5370
|
+
}
|
|
5371
|
+
|
|
5345
5372
|
///////////////////////////////////////////////////////////////////////////////
|
|
5346
5373
|
|
|
5347
5374
|
/** Pulse the vibration hardware if it exists
|
|
@@ -5918,10 +5945,12 @@ function touchGamepadButtonCenter()
|
|
|
5918
5945
|
* @namespace Audio
|
|
5919
5946
|
*/
|
|
5920
5947
|
|
|
5921
|
-
/** Audio context used by the engine
|
|
5948
|
+
/** Audio context used by the engine. Created lazily in audioInit() to avoid
|
|
5949
|
+
* browser autoplay warnings about constructing an AudioContext before any
|
|
5950
|
+
* user gesture.
|
|
5922
5951
|
* @type {AudioContext}
|
|
5923
5952
|
* @memberof Audio */
|
|
5924
|
-
let audioContext
|
|
5953
|
+
let audioContext;
|
|
5925
5954
|
|
|
5926
5955
|
/** Master gain node for all audio to pass through
|
|
5927
5956
|
* @type {GainNode}
|
|
@@ -5937,12 +5966,13 @@ const audioDefaultSampleRate = 44100;
|
|
|
5937
5966
|
* @return {boolean} - True if the audio context is running
|
|
5938
5967
|
* @memberof Audio */
|
|
5939
5968
|
function audioIsRunning()
|
|
5940
|
-
{ return audioContext
|
|
5969
|
+
{ return audioContext?.state === 'running'; }
|
|
5941
5970
|
|
|
5942
5971
|
function audioInit()
|
|
5943
5972
|
{
|
|
5944
5973
|
if (!soundEnable || headlessMode) return;
|
|
5945
5974
|
|
|
5975
|
+
audioContext = new AudioContext;
|
|
5946
5976
|
audioMasterGain = audioContext.createGain();
|
|
5947
5977
|
audioMasterGain.connect(audioContext.destination);
|
|
5948
5978
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -6009,8 +6039,8 @@ class Sound
|
|
|
6009
6039
|
|
|
6010
6040
|
if (Array.isArray(asset))
|
|
6011
6041
|
{
|
|
6012
|
-
// generate zzfx sound
|
|
6013
|
-
const zzfxSound = asset;
|
|
6042
|
+
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
6043
|
+
const zzfxSound = asset.slice();
|
|
6014
6044
|
|
|
6015
6045
|
// remove randomness so it can be applied on playback
|
|
6016
6046
|
const defaultRandomness = randomness ?? .05;
|
|
@@ -7608,11 +7638,14 @@ class ParticleEmitter extends EngineObject
|
|
|
7608
7638
|
}
|
|
7609
7639
|
|
|
7610
7640
|
///////////////////////////////////////////////////////////////////////////////
|
|
7641
|
+
// scratch vector reused by Particle.render to avoid per-frame allocations
|
|
7642
|
+
const particleDrawPos = new Vector2;
|
|
7643
|
+
|
|
7611
7644
|
/**
|
|
7612
7645
|
* Particle Object - Created automatically by Particle Emitters
|
|
7613
7646
|
* @memberof Particles
|
|
7614
7647
|
*/
|
|
7615
|
-
class Particle
|
|
7648
|
+
class Particle
|
|
7616
7649
|
{
|
|
7617
7650
|
/**
|
|
7618
7651
|
* Create a particle with the passed in settings
|
|
@@ -7787,14 +7820,14 @@ class Particle
|
|
|
7787
7820
|
additive && setBlendMode(true);
|
|
7788
7821
|
|
|
7789
7822
|
// update the position and angle for drawing
|
|
7790
|
-
const pos = this.pos.
|
|
7823
|
+
const pos = particleDrawPos.set(this.pos.x, this.pos.y);
|
|
7791
7824
|
let angle = this.angle;
|
|
7792
7825
|
if (localSpace)
|
|
7793
7826
|
{
|
|
7794
7827
|
// in local space of emitter
|
|
7795
7828
|
const a = emitter.angle;
|
|
7796
7829
|
const c = cos(a), s = sin(a);
|
|
7797
|
-
pos.set(emitter.pos.x + pos.x*c - pos.y*s,
|
|
7830
|
+
pos.set(emitter.pos.x + pos.x*c - pos.y*s,
|
|
7798
7831
|
emitter.pos.y + pos.x*s + pos.y*c);
|
|
7799
7832
|
angle += a;
|
|
7800
7833
|
}
|
|
@@ -7819,200 +7852,6 @@ class Particle
|
|
|
7819
7852
|
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
7820
7853
|
}
|
|
7821
7854
|
}
|
|
7822
|
-
/**
|
|
7823
|
-
* LittleJS Medal System
|
|
7824
|
-
* - Achievement/trophy system for games
|
|
7825
|
-
* - Medal class with name, description, icon, and unlock tracking
|
|
7826
|
-
* - Automatic saving to local storage
|
|
7827
|
-
* - Visual display queue with slide-in notifications
|
|
7828
|
-
* - Newgrounds API integration for online achievements
|
|
7829
|
-
* - Debug mode to unlock/reset medals during development
|
|
7830
|
-
* @namespace Medals
|
|
7831
|
-
*/
|
|
7832
|
-
|
|
7833
|
-
/** List of all medals
|
|
7834
|
-
* @type {Object}
|
|
7835
|
-
* @memberof Medals */
|
|
7836
|
-
const medals = {};
|
|
7837
|
-
|
|
7838
|
-
// Engine internal variables not exposed to documentation
|
|
7839
|
-
let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
|
|
7840
|
-
|
|
7841
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
7842
|
-
|
|
7843
|
-
/** Initialize medals with a save name used for storage
|
|
7844
|
-
* - Call this after creating all medals
|
|
7845
|
-
* - Checks if medals are unlocked
|
|
7846
|
-
* @param {string} saveName
|
|
7847
|
-
* @memberof Medals */
|
|
7848
|
-
function medalsInit(saveName)
|
|
7849
|
-
{
|
|
7850
|
-
// check if medals are unlocked
|
|
7851
|
-
medalsSaveName = saveName;
|
|
7852
|
-
if (!debugMedals)
|
|
7853
|
-
medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
|
|
7854
|
-
|
|
7855
|
-
// engine automatically renders medals
|
|
7856
|
-
engineAddPlugin(undefined, medalsRender);
|
|
7857
|
-
|
|
7858
|
-
// plugin functions
|
|
7859
|
-
function medalsRender()
|
|
7860
|
-
{
|
|
7861
|
-
if (!medalsDisplayQueue.length) return;
|
|
7862
|
-
|
|
7863
|
-
// update first medal in queue
|
|
7864
|
-
const medal = medalsDisplayQueue[0];
|
|
7865
|
-
const time = timeReal - medalsDisplayTimeLast;
|
|
7866
|
-
if (!medalsDisplayTimeLast)
|
|
7867
|
-
medalsDisplayTimeLast = timeReal;
|
|
7868
|
-
else if (time > medalDisplayTime)
|
|
7869
|
-
{
|
|
7870
|
-
medalsDisplayTimeLast = 0;
|
|
7871
|
-
medalsDisplayQueue.shift();
|
|
7872
|
-
}
|
|
7873
|
-
else
|
|
7874
|
-
{
|
|
7875
|
-
// slide on/off medals
|
|
7876
|
-
const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
|
|
7877
|
-
const hidePercent =
|
|
7878
|
-
time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
|
|
7879
|
-
time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
|
|
7880
|
-
medal.render(hidePercent);
|
|
7881
|
-
}
|
|
7882
|
-
}
|
|
7883
|
-
}
|
|
7884
|
-
|
|
7885
|
-
/**
|
|
7886
|
-
* @callback MedalCallbackFunction - Function that processes a medal
|
|
7887
|
-
* @param {Medal} medal
|
|
7888
|
-
* @memberof Medals
|
|
7889
|
-
*/
|
|
7890
|
-
|
|
7891
|
-
/** Calls a function for each medal
|
|
7892
|
-
* @param {MedalCallbackFunction} callback
|
|
7893
|
-
* @memberof Medals */
|
|
7894
|
-
function medalsForEach(callback)
|
|
7895
|
-
{ Object.values(medals).forEach(medal=>callback(medal)); }
|
|
7896
|
-
|
|
7897
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
7898
|
-
|
|
7899
|
-
/**
|
|
7900
|
-
* Medal - Tracks an unlockable medal
|
|
7901
|
-
* @memberof Medals
|
|
7902
|
-
* @example
|
|
7903
|
-
* // create a medal
|
|
7904
|
-
* const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
|
|
7905
|
-
*
|
|
7906
|
-
* // initialize medals
|
|
7907
|
-
* medalsInit('Example Game');
|
|
7908
|
-
*
|
|
7909
|
-
* // unlock the medal
|
|
7910
|
-
* medal_example.unlock();
|
|
7911
|
-
*/
|
|
7912
|
-
class Medal
|
|
7913
|
-
{
|
|
7914
|
-
/** Create a medal object and adds it to the list of medals
|
|
7915
|
-
* @param {number} id - The unique identifier of the medal
|
|
7916
|
-
* @param {string} name - Name of the medal
|
|
7917
|
-
* @param {string} [description] - Description of the medal
|
|
7918
|
-
* @param {string} [icon] - Icon for the medal
|
|
7919
|
-
* @param {string} [src] - Image location for the medal
|
|
7920
|
-
*/
|
|
7921
|
-
constructor(id, name, description='', icon='🏆', src)
|
|
7922
|
-
{
|
|
7923
|
-
ASSERT(id >= 0 && !medals[id]);
|
|
7924
|
-
|
|
7925
|
-
/** @property {number} - The unique identifier of the medal */
|
|
7926
|
-
this.id = id;
|
|
7927
|
-
|
|
7928
|
-
/** @property {string} - Name of the medal */
|
|
7929
|
-
this.name = name;
|
|
7930
|
-
|
|
7931
|
-
/** @property {string} - Description of the medal */
|
|
7932
|
-
this.description = description;
|
|
7933
|
-
|
|
7934
|
-
/** @property {string} - Icon for the medal */
|
|
7935
|
-
this.icon = icon;
|
|
7936
|
-
|
|
7937
|
-
/** @property {boolean} - Is the medal unlocked? */
|
|
7938
|
-
this.unlocked = false;
|
|
7939
|
-
|
|
7940
|
-
// load the source image if provided
|
|
7941
|
-
if (src)
|
|
7942
|
-
(this.image = new Image).src = src;
|
|
7943
|
-
|
|
7944
|
-
// add this to list of medals
|
|
7945
|
-
medals[id] = this;
|
|
7946
|
-
}
|
|
7947
|
-
|
|
7948
|
-
/** Unlocks a medal if not already unlocked */
|
|
7949
|
-
unlock()
|
|
7950
|
-
{
|
|
7951
|
-
if (medalsPreventUnlock || this.unlocked) return;
|
|
7952
|
-
|
|
7953
|
-
// save the medal
|
|
7954
|
-
ASSERT(medalsSaveName, 'save name must be set');
|
|
7955
|
-
localStorage[this.storageKey()] = this.unlocked = true;
|
|
7956
|
-
medalsDisplayQueue.push(this);
|
|
7957
|
-
}
|
|
7958
|
-
|
|
7959
|
-
/** Render a medal
|
|
7960
|
-
* @param {number} [hidePercent] - How much to slide the medal off screen
|
|
7961
|
-
*/
|
|
7962
|
-
render(hidePercent=0)
|
|
7963
|
-
{
|
|
7964
|
-
const context = mainContext;
|
|
7965
|
-
const width = min(medalDisplaySize.x, mainCanvas.width);
|
|
7966
|
-
const height = medalDisplaySize.y;
|
|
7967
|
-
const x = mainCanvas.width - width;
|
|
7968
|
-
const y = -height*hidePercent;
|
|
7969
|
-
const backgroundColor = hsl(0,0,.9);
|
|
7970
|
-
|
|
7971
|
-
// draw containing rect and clip to that region
|
|
7972
|
-
context.save();
|
|
7973
|
-
context.beginPath();
|
|
7974
|
-
context.fillStyle = backgroundColor.toString();
|
|
7975
|
-
context.strokeStyle = BLACK.toString();
|
|
7976
|
-
context.lineWidth = 3;
|
|
7977
|
-
context.rect(x, y, width, height);
|
|
7978
|
-
context.fill();
|
|
7979
|
-
context.stroke();
|
|
7980
|
-
context.clip();
|
|
7981
|
-
|
|
7982
|
-
// draw the icon
|
|
7983
|
-
const gap = vec2(.1, .05).scale(height);
|
|
7984
|
-
const medalDisplayIconSize = height - 2*gap.x;
|
|
7985
|
-
this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
|
|
7986
|
-
|
|
7987
|
-
// draw the name
|
|
7988
|
-
const nameSize = height*.5;
|
|
7989
|
-
const descriptionSize = height*.3;
|
|
7990
|
-
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
7991
|
-
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
7992
|
-
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
7993
|
-
|
|
7994
|
-
// draw the description
|
|
7995
|
-
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
7996
|
-
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
7997
|
-
context.restore();
|
|
7998
|
-
}
|
|
7999
|
-
|
|
8000
|
-
/** Render the icon for a medal
|
|
8001
|
-
* @param {Vector2} pos - Screen space position
|
|
8002
|
-
* @param {number} size - Screen space size
|
|
8003
|
-
*/
|
|
8004
|
-
renderIcon(pos, size)
|
|
8005
|
-
{
|
|
8006
|
-
// draw the image or icon
|
|
8007
|
-
if (this.image)
|
|
8008
|
-
mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
|
|
8009
|
-
else
|
|
8010
|
-
drawTextScreen(this.icon, pos, size*.7, BLACK);
|
|
8011
|
-
}
|
|
8012
|
-
|
|
8013
|
-
// Get local storage key used by the medal
|
|
8014
|
-
storageKey() { return medalsSaveName + '_' + this.id; }
|
|
8015
|
-
}
|
|
8016
7855
|
/**
|
|
8017
7856
|
* LittleJS WebGL Interface
|
|
8018
7857
|
* - WebGL2 rendering engine for high-performance graphics
|
|
@@ -8112,7 +7951,7 @@ function glInit(rootElement)
|
|
|
8112
7951
|
// reinit WebGL and restore textures
|
|
8113
7952
|
initWebGL();
|
|
8114
7953
|
for (const info of glTextureInfos)
|
|
8115
|
-
info.glTexture = glCreateTexture(info.image);
|
|
7954
|
+
info.glTexture = glCreateTexture(info.image, info.wrap);
|
|
8116
7955
|
pluginList.forEach(plugin=>plugin.glContextRestored?.());
|
|
8117
7956
|
});
|
|
8118
7957
|
|
|
@@ -8317,9 +8156,8 @@ function glClearCanvas()
|
|
|
8317
8156
|
/** Set the WebGL texture, called automatically if using multiple textures
|
|
8318
8157
|
* - This may also flush the gl buffer resulting in more draw calls and worse performance
|
|
8319
8158
|
* @param {WebGLTexture} texture
|
|
8320
|
-
* @param {boolean} [wrap] - Should the texture wrap or clamp
|
|
8321
8159
|
* @memberof WebGL */
|
|
8322
|
-
function glSetTexture(texture
|
|
8160
|
+
function glSetTexture(texture)
|
|
8323
8161
|
{
|
|
8324
8162
|
// must flush cache with the old texture to set a new one
|
|
8325
8163
|
if (!glContext || texture === glActiveTexture) return;
|
|
@@ -8327,11 +8165,30 @@ function glSetTexture(texture, wrap=false)
|
|
|
8327
8165
|
glFlush();
|
|
8328
8166
|
glActiveTexture = texture;
|
|
8329
8167
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
8168
|
+
}
|
|
8169
|
+
|
|
8170
|
+
/** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
|
|
8171
|
+
* Flushes the current batch only if the texture is the active one
|
|
8172
|
+
* @param {WebGLTexture} texture
|
|
8173
|
+
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
|
|
8174
|
+
* @memberof WebGL */
|
|
8175
|
+
function glSetTextureWrap(texture, wrap=true)
|
|
8176
|
+
{
|
|
8177
|
+
if (!glContext || !texture) return;
|
|
8178
|
+
|
|
8179
|
+
// flush only if changing wrap on the currently bound texture
|
|
8180
|
+
const isCurrent = texture === glActiveTexture;
|
|
8181
|
+
if (isCurrent)
|
|
8182
|
+
glFlush();
|
|
8183
|
+
else
|
|
8184
|
+
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
8330
8185
|
|
|
8331
|
-
// set wrap mode
|
|
8332
8186
|
const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
|
|
8333
8187
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
|
|
8334
8188
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
|
|
8189
|
+
|
|
8190
|
+
if (!isCurrent && glActiveTexture)
|
|
8191
|
+
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
8335
8192
|
}
|
|
8336
8193
|
|
|
8337
8194
|
/** Compile WebGL shader of the given type, will throw errors if in debug mode
|
|
@@ -8378,9 +8235,10 @@ function glCreateProgram(vsSource, fsSource)
|
|
|
8378
8235
|
/** Create WebGL texture from an image and init the texture settings
|
|
8379
8236
|
* Restores the active texture when done
|
|
8380
8237
|
* @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
|
|
8238
|
+
* @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
|
|
8381
8239
|
* @return {WebGLTexture}
|
|
8382
8240
|
* @memberof WebGL */
|
|
8383
|
-
function glCreateTexture(image)
|
|
8241
|
+
function glCreateTexture(image, wrap=false)
|
|
8384
8242
|
{
|
|
8385
8243
|
if (!glContext) return;
|
|
8386
8244
|
|
|
@@ -8406,6 +8264,9 @@ function glCreateTexture(image)
|
|
|
8406
8264
|
const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
|
|
8407
8265
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
|
|
8408
8266
|
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
|
|
8267
|
+
const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
|
|
8268
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
|
|
8269
|
+
glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
|
|
8409
8270
|
if (mipMap)
|
|
8410
8271
|
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
8411
8272
|
|
|
@@ -8457,7 +8318,7 @@ function glRegisterTextureInfo(textureInfo)
|
|
|
8457
8318
|
if (textureInfo.glTexture)
|
|
8458
8319
|
glSetTextureData(textureInfo.glTexture, textureInfo.image);
|
|
8459
8320
|
else
|
|
8460
|
-
textureInfo.glTexture = glCreateTexture(textureInfo.image);
|
|
8321
|
+
textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
|
|
8461
8322
|
}
|
|
8462
8323
|
|
|
8463
8324
|
/** Tells WebGL to destroy the glTexture and stop tracking it
|
|
@@ -8676,6 +8537,7 @@ function glSetRenderTarget(texture, clear=false)
|
|
|
8676
8537
|
glFlush();
|
|
8677
8538
|
glRenderTarget = undefined;
|
|
8678
8539
|
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
8540
|
+
glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
8679
8541
|
}
|
|
8680
8542
|
}
|
|
8681
8543
|
|
|
@@ -8798,9 +8660,9 @@ function glPolyStrip(points)
|
|
|
8798
8660
|
return area;
|
|
8799
8661
|
}
|
|
8800
8662
|
|
|
8801
|
-
// ensure counter-clockwise winding
|
|
8663
|
+
// ensure counter-clockwise winding (slice first so we don't mutate caller's array)
|
|
8802
8664
|
if (signedArea(points) < 0)
|
|
8803
|
-
points = points.reverse();
|
|
8665
|
+
points = points.slice().reverse();
|
|
8804
8666
|
|
|
8805
8667
|
// check if point is inside triangle
|
|
8806
8668
|
const e = 1e-9;
|
|
@@ -8900,46 +8762,438 @@ function glPolyStrip(points)
|
|
|
8900
8762
|
}
|
|
8901
8763
|
return strip;
|
|
8902
8764
|
}
|
|
8903
|
-
/**
|
|
8904
|
-
* LittleJS
|
|
8905
|
-
* -
|
|
8906
|
-
* -
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
}
|
|
8765
|
+
/**
|
|
8766
|
+
* LittleJS Engine Logo
|
|
8767
|
+
* - Draws the LittleJS splash screen logo
|
|
8768
|
+
* - Used internally during engine startup
|
|
8769
|
+
*/
|
|
8770
|
+
|
|
8771
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
8772
|
+
function drawEngineLogo(t)
|
|
8773
|
+
{
|
|
8774
|
+
const blackAndWhite = 0;
|
|
8775
|
+
const showName = 1;
|
|
8776
|
+
|
|
8777
|
+
// LittleJS Logo and Splash Screen
|
|
8778
|
+
const x = mainContext;
|
|
8779
|
+
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
8780
|
+
const w = mainCanvas.width = innerWidth * dpr;
|
|
8781
|
+
const h = mainCanvas.height = innerHeight * dpr;
|
|
8782
|
+
{
|
|
8783
|
+
// background
|
|
8784
|
+
const p3 = percent(t, 1, .8);
|
|
8785
|
+
const p4 = percent(t, 0, .5);
|
|
8786
|
+
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
|
|
8787
|
+
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
8788
|
+
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
8789
|
+
x.save();
|
|
8790
|
+
x.fillStyle = g;
|
|
8791
|
+
x.fillRect(0,0,w,h);
|
|
8792
|
+
}
|
|
8793
|
+
const gradient = (X1,Y1,X2,Y2,C,S=1)=>
|
|
8794
|
+
{
|
|
8795
|
+
if (C >= 0)
|
|
8796
|
+
{
|
|
8797
|
+
if (blackAndWhite)
|
|
8798
|
+
x.fillStyle = '#fff';
|
|
8799
|
+
else
|
|
8800
|
+
{
|
|
8801
|
+
const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
|
|
8802
|
+
g.addColorStop(0,color(C,2));
|
|
8803
|
+
g.addColorStop(1,color(C,1));
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
else
|
|
8807
|
+
x.fillStyle = '#000';
|
|
8808
|
+
C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
|
|
8809
|
+
}
|
|
8810
|
+
const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
|
|
8811
|
+
{
|
|
8812
|
+
x.beginPath();
|
|
8813
|
+
x.arc(X,Y,R,p*A,p*B);
|
|
8814
|
+
gradient(X,Y-R,X,Y+R,C,S);
|
|
8815
|
+
}
|
|
8816
|
+
const rect = (X,Y,W,H,C)=>
|
|
8817
|
+
{
|
|
8818
|
+
x.beginPath();
|
|
8819
|
+
x.rect(X,Y,W,H*p);
|
|
8820
|
+
gradient(X,Y+H,X+W,Y,C);
|
|
8821
|
+
}
|
|
8822
|
+
const poly = (points,C,Y,H)=>
|
|
8823
|
+
{
|
|
8824
|
+
x.beginPath();
|
|
8825
|
+
for (const p of points)
|
|
8826
|
+
x.lineTo(p.x, p.y);
|
|
8827
|
+
x.closePath();
|
|
8828
|
+
gradient(0, Y, 0, Y+H,C);
|
|
8829
|
+
}
|
|
8830
|
+
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
|
|
8831
|
+
|
|
8832
|
+
// center and fit to screen
|
|
8833
|
+
const alpha = oscillate(1,1,t);
|
|
8834
|
+
const p = percent(alpha, .1, .5);
|
|
8835
|
+
const size = min(6, min(w,h)/99);
|
|
8836
|
+
x.translate(w/2,h/2);
|
|
8837
|
+
x.scale(size,size);
|
|
8838
|
+
x.translate(-40,-35);
|
|
8839
|
+
p < 1 && x.setLineDash([99*p,99]);
|
|
8840
|
+
x.lineJoin = x.lineCap = 'round';
|
|
8841
|
+
x.lineWidth = .1 + p*1.9;
|
|
8842
|
+
//x.strokeStyle='#fff7';
|
|
8843
|
+
|
|
8844
|
+
if (showName)
|
|
8845
|
+
{
|
|
8846
|
+
// engine name text
|
|
8847
|
+
const Y = 54;
|
|
8848
|
+
const s = 'LittleJS';
|
|
8849
|
+
x.font = '900 15.5px arial';
|
|
8850
|
+
x.lineWidth = .1+p*3.9;
|
|
8851
|
+
x.textAlign = 'center';
|
|
8852
|
+
x.textBaseline = 'top';
|
|
8853
|
+
rect(11,Y+1,59,8*p,-1);
|
|
8854
|
+
x.beginPath();
|
|
8855
|
+
|
|
8856
|
+
let w2 = 0;
|
|
8857
|
+
for (let i=0;i<s.length;++i)
|
|
8858
|
+
w2 += x.measureText(s[i]).width;
|
|
8859
|
+
for (let j=2;j--;)
|
|
8860
|
+
for (let i=0,X=40-w2/2;i<s.length;++i)
|
|
8861
|
+
{
|
|
8862
|
+
const w = x.measureText(s[i]).width, X2 = X+w/2;
|
|
8863
|
+
gradient(X2,Y,X2+2,Y+13,i>5?1:0);
|
|
8864
|
+
x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
|
|
8865
|
+
X += w;
|
|
8866
|
+
}
|
|
8867
|
+
|
|
8868
|
+
x.lineWidth = .1 + p*1.9;
|
|
8869
|
+
rect(3,Y,73,0); // bottom
|
|
8870
|
+
}
|
|
8871
|
+
|
|
8872
|
+
rect(7,15,26,-7,0); // cab top
|
|
8873
|
+
rect(25,15,8,25,-1); // cab front
|
|
8874
|
+
rect(10,40,15,-25,1); // cab back
|
|
8875
|
+
rect(14,21,7,9,2); // cab window
|
|
8876
|
+
rect(38,20,6,-6,2); // little stack
|
|
8877
|
+
|
|
8878
|
+
// big stack
|
|
8879
|
+
rect(49,20,10,-6,0);
|
|
8880
|
+
const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
|
|
8881
|
+
poly(stackPoints,2,8,6*p);
|
|
8882
|
+
rect(44,8,20,-7,0);
|
|
8883
|
+
|
|
8884
|
+
// engine
|
|
8885
|
+
for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
|
|
8886
|
+
circle(59,30,4,0,7,2); // light
|
|
8887
|
+
|
|
8888
|
+
// engine outline
|
|
8889
|
+
rect(35,20,24,0); // top
|
|
8890
|
+
circle(59,30,10); // front
|
|
8891
|
+
circle(47,30,10,PI/2,PI*3/2); // middle
|
|
8892
|
+
circle(35,30,10,PI/2,PI*3/2); // back
|
|
8893
|
+
rect(7,40,13,7,-1); // bottom back
|
|
8894
|
+
rect(17,40,43,14,-1); // bottom center
|
|
8895
|
+
|
|
8896
|
+
// wheels
|
|
8897
|
+
for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
|
|
8898
|
+
|
|
8899
|
+
// cowcatcher
|
|
8900
|
+
for (let i=2;i--;)
|
|
8901
|
+
{
|
|
8902
|
+
let w=6, s=7, o=53+w*p*i
|
|
8903
|
+
const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
|
|
8904
|
+
poly(points,0,40,14);
|
|
8905
|
+
}
|
|
8906
|
+
|
|
8907
|
+
x.restore();
|
|
8908
|
+
}
|
|
8909
|
+
|
|
8910
|
+
/**
|
|
8911
|
+
* LittleJS Medal System
|
|
8912
|
+
* - Achievement/trophy system for games
|
|
8913
|
+
* - Medal class with name, description, icon, and unlock tracking
|
|
8914
|
+
* - Automatic saving to local storage
|
|
8915
|
+
* - Visual display queue with slide-in notifications
|
|
8916
|
+
* - Newgrounds API integration for online achievements
|
|
8917
|
+
* - Debug mode to unlock/reset medals during development
|
|
8918
|
+
* @namespace Medals
|
|
8919
|
+
*/
|
|
8920
|
+
|
|
8921
|
+
let debugMedals = false;
|
|
8922
|
+
|
|
8923
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
8924
|
+
// Medals settings
|
|
8925
|
+
|
|
8926
|
+
/** How long to show medals for in seconds
|
|
8927
|
+
* @type {number}
|
|
8928
|
+
* @default
|
|
8929
|
+
* @memberof Settings */
|
|
8930
|
+
let medalDisplayTime = 5;
|
|
8931
|
+
|
|
8932
|
+
/** How quickly to slide on/off medals in seconds
|
|
8933
|
+
* @type {number}
|
|
8934
|
+
* @default
|
|
8935
|
+
* @memberof Settings */
|
|
8936
|
+
let medalDisplaySlideTime = .5;
|
|
8937
|
+
|
|
8938
|
+
/** Size of medal display
|
|
8939
|
+
* @type {Vector2}
|
|
8940
|
+
* @default Vector2(640,80)
|
|
8941
|
+
* @memberof Settings */
|
|
8942
|
+
let medalDisplaySize = vec2(640, 80);
|
|
8943
|
+
|
|
8944
|
+
/** Set to stop medals from being unlockable (like if cheats are enabled)
|
|
8945
|
+
* @type {boolean}
|
|
8946
|
+
* @default
|
|
8947
|
+
* @memberof Settings */
|
|
8948
|
+
let medalsPreventUnlock = false;
|
|
8949
|
+
|
|
8950
|
+
/** List of all medals
|
|
8951
|
+
* @type {Object}
|
|
8952
|
+
* @memberof Medals */
|
|
8953
|
+
const medals = {};
|
|
8954
|
+
|
|
8955
|
+
// Engine internal variables not exposed to documentation
|
|
8956
|
+
let medalsDisplayQueue = [], medalsSaveName, medalsDisplayTimeLast;
|
|
8957
|
+
|
|
8958
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
8959
|
+
|
|
8960
|
+
/** Initialize medals with a save name used for storage
|
|
8961
|
+
* - Call this after creating all medals
|
|
8962
|
+
* - Checks if medals are unlocked
|
|
8963
|
+
* @param {string} saveName
|
|
8964
|
+
* @memberof Medals */
|
|
8965
|
+
function medalsInit(saveName)
|
|
8966
|
+
{
|
|
8967
|
+
// check if medals are unlocked
|
|
8968
|
+
medalsSaveName = saveName;
|
|
8969
|
+
if (!debugMedals)
|
|
8970
|
+
medalsForEach(medal=> medal.unlocked = !!localStorage[medal.storageKey()]);
|
|
8971
|
+
|
|
8972
|
+
// engine automatically renders medals
|
|
8973
|
+
engineAddPlugin(undefined, medalsRender);
|
|
8974
|
+
|
|
8975
|
+
// plugin functions
|
|
8976
|
+
function medalsRender()
|
|
8977
|
+
{
|
|
8978
|
+
if (!medalsDisplayQueue.length) return;
|
|
8979
|
+
|
|
8980
|
+
// update first medal in queue
|
|
8981
|
+
const medal = medalsDisplayQueue[0];
|
|
8982
|
+
const time = timeReal - medalsDisplayTimeLast;
|
|
8983
|
+
if (!medalsDisplayTimeLast)
|
|
8984
|
+
medalsDisplayTimeLast = timeReal;
|
|
8985
|
+
else if (time > medalDisplayTime)
|
|
8986
|
+
{
|
|
8987
|
+
medalsDisplayTimeLast = 0;
|
|
8988
|
+
medalsDisplayQueue.shift();
|
|
8989
|
+
}
|
|
8990
|
+
else
|
|
8991
|
+
{
|
|
8992
|
+
// slide on/off medals
|
|
8993
|
+
const slideOffTime = medalDisplayTime - medalDisplaySlideTime;
|
|
8994
|
+
const hidePercent =
|
|
8995
|
+
time < medalDisplaySlideTime ? 1 - time / medalDisplaySlideTime :
|
|
8996
|
+
time > slideOffTime ? (time - slideOffTime) / medalDisplaySlideTime : 0;
|
|
8997
|
+
medal.render(hidePercent);
|
|
8998
|
+
}
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
9001
|
+
|
|
9002
|
+
/**
|
|
9003
|
+
* @callback MedalCallbackFunction - Function that processes a medal
|
|
9004
|
+
* @param {Medal} medal
|
|
9005
|
+
* @memberof Medals
|
|
9006
|
+
*/
|
|
9007
|
+
|
|
9008
|
+
/** Calls a function for each medal
|
|
9009
|
+
* @param {MedalCallbackFunction} callback
|
|
9010
|
+
* @memberof Medals */
|
|
9011
|
+
function medalsForEach(callback)
|
|
9012
|
+
{ Object.values(medals).forEach(medal=>callback(medal)); }
|
|
9013
|
+
|
|
9014
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9015
|
+
|
|
9016
|
+
/**
|
|
9017
|
+
* Medal - Tracks an unlockable medal
|
|
9018
|
+
* @memberof Medals
|
|
9019
|
+
* @example
|
|
9020
|
+
* // create a medal
|
|
9021
|
+
* const medal_example = new Medal(0, 'Example Medal', 'More info about the medal goes here.', '🎖️');
|
|
9022
|
+
*
|
|
9023
|
+
* // initialize medals
|
|
9024
|
+
* medalsInit('Example Game');
|
|
9025
|
+
*
|
|
9026
|
+
* // unlock the medal
|
|
9027
|
+
* medal_example.unlock();
|
|
9028
|
+
*/
|
|
9029
|
+
class Medal
|
|
9030
|
+
{
|
|
9031
|
+
/** Create a medal object and adds it to the list of medals
|
|
9032
|
+
* @param {number} id - The unique identifier of the medal
|
|
9033
|
+
* @param {string} name - Name of the medal
|
|
9034
|
+
* @param {string} [description] - Description of the medal
|
|
9035
|
+
* @param {string} [icon] - Icon for the medal
|
|
9036
|
+
* @param {string} [src] - Image location for the medal
|
|
9037
|
+
*/
|
|
9038
|
+
constructor(id, name, description='', icon='🏆', src)
|
|
9039
|
+
{
|
|
9040
|
+
ASSERT(id >= 0 && !medals[id]);
|
|
9041
|
+
|
|
9042
|
+
/** @property {number} - The unique identifier of the medal */
|
|
9043
|
+
this.id = id;
|
|
9044
|
+
|
|
9045
|
+
/** @property {string} - Name of the medal */
|
|
9046
|
+
this.name = name;
|
|
9047
|
+
|
|
9048
|
+
/** @property {string} - Description of the medal */
|
|
9049
|
+
this.description = description;
|
|
9050
|
+
|
|
9051
|
+
/** @property {string} - Icon for the medal */
|
|
9052
|
+
this.icon = icon;
|
|
9053
|
+
|
|
9054
|
+
/** @property {boolean} - Is the medal unlocked? */
|
|
9055
|
+
this.unlocked = false;
|
|
9056
|
+
|
|
9057
|
+
// load the source image if provided
|
|
9058
|
+
if (src)
|
|
9059
|
+
(this.image = new Image).src = src;
|
|
9060
|
+
|
|
9061
|
+
// add this to list of medals
|
|
9062
|
+
medals[id] = this;
|
|
9063
|
+
}
|
|
9064
|
+
|
|
9065
|
+
/** Unlocks a medal if not already unlocked */
|
|
9066
|
+
unlock()
|
|
9067
|
+
{
|
|
9068
|
+
if (medalsPreventUnlock || this.unlocked) return;
|
|
9069
|
+
|
|
9070
|
+
// save the medal
|
|
9071
|
+
ASSERT(medalsSaveName, 'save name must be set');
|
|
9072
|
+
localStorage[this.storageKey()] = this.unlocked = true;
|
|
9073
|
+
medalsDisplayQueue.push(this);
|
|
9074
|
+
}
|
|
9075
|
+
|
|
9076
|
+
/** Render a medal
|
|
9077
|
+
* @param {number} [hidePercent] - How much to slide the medal off screen
|
|
9078
|
+
*/
|
|
9079
|
+
render(hidePercent=0)
|
|
9080
|
+
{
|
|
9081
|
+
const context = mainContext;
|
|
9082
|
+
const width = min(medalDisplaySize.x, mainCanvas.width);
|
|
9083
|
+
const height = medalDisplaySize.y;
|
|
9084
|
+
const x = mainCanvas.width - width;
|
|
9085
|
+
const y = -height*hidePercent;
|
|
9086
|
+
const backgroundColor = hsl(0,0,.9);
|
|
9087
|
+
|
|
9088
|
+
// draw containing rect and clip to that region
|
|
9089
|
+
context.save();
|
|
9090
|
+
context.beginPath();
|
|
9091
|
+
context.fillStyle = backgroundColor.toString();
|
|
9092
|
+
context.strokeStyle = BLACK.toString();
|
|
9093
|
+
context.lineWidth = 3;
|
|
9094
|
+
context.rect(x, y, width, height);
|
|
9095
|
+
context.fill();
|
|
9096
|
+
context.stroke();
|
|
9097
|
+
context.clip();
|
|
9098
|
+
|
|
9099
|
+
// draw the icon
|
|
9100
|
+
const gap = vec2(.1, .05).scale(height);
|
|
9101
|
+
const medalDisplayIconSize = height - 2*gap.x;
|
|
9102
|
+
this.renderIcon(vec2(x + gap.x + medalDisplayIconSize/2, y + height/2), medalDisplayIconSize);
|
|
9103
|
+
|
|
9104
|
+
// draw the name
|
|
9105
|
+
const nameSize = height*.5;
|
|
9106
|
+
const descriptionSize = height*.3;
|
|
9107
|
+
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
9108
|
+
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
9109
|
+
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
9110
|
+
|
|
9111
|
+
// draw the description
|
|
9112
|
+
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
9113
|
+
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
9114
|
+
context.restore();
|
|
9115
|
+
}
|
|
9116
|
+
|
|
9117
|
+
/** Render the icon for a medal
|
|
9118
|
+
* @param {Vector2} pos - Screen space position
|
|
9119
|
+
* @param {number} size - Screen space size
|
|
9120
|
+
*/
|
|
9121
|
+
renderIcon(pos, size)
|
|
9122
|
+
{
|
|
9123
|
+
// draw the image or icon
|
|
9124
|
+
if (this.image)
|
|
9125
|
+
mainContext.drawImage(this.image, pos.x-size/2, pos.y-size/2, size, size);
|
|
9126
|
+
else
|
|
9127
|
+
drawTextScreen(this.icon, pos, size*.7, BLACK);
|
|
9128
|
+
}
|
|
9129
|
+
|
|
9130
|
+
// Get local storage key used by the medal
|
|
9131
|
+
storageKey() { return medalsSaveName + '_' + this.id; }
|
|
9132
|
+
}
|
|
9133
|
+
|
|
9134
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9135
|
+
// Medals setting setters
|
|
9136
|
+
|
|
9137
|
+
/** Set how long to show medals for in seconds
|
|
9138
|
+
* @param {number} time
|
|
9139
|
+
* @memberof Settings */
|
|
9140
|
+
function setMedalDisplayTime(time) { medalDisplayTime = time; }
|
|
9141
|
+
|
|
9142
|
+
/** Set how quickly to slide on/off medals in seconds
|
|
9143
|
+
* @param {number} time
|
|
9144
|
+
* @memberof Settings */
|
|
9145
|
+
function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
|
|
9146
|
+
|
|
9147
|
+
/** Set size of medal display
|
|
9148
|
+
* @param {Vector2} size
|
|
9149
|
+
* @memberof Settings */
|
|
9150
|
+
function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
|
|
9151
|
+
|
|
9152
|
+
/** Set to stop medals from being unlockable
|
|
9153
|
+
* @param {boolean} preventUnlock
|
|
9154
|
+
* @memberof Settings */
|
|
9155
|
+
function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
|
|
9156
|
+
|
|
9157
|
+
/**
|
|
9158
|
+
* LittleJS Newgrounds Plugin
|
|
9159
|
+
* - NewgroundsMedal extends Medal with Newgrounds API functionality
|
|
9160
|
+
* - Call new NewgroundsPlugin(app_id) to setup Newgrounds
|
|
9161
|
+
* - Uses CryptoJS for encryption if optional cipher is provided
|
|
9162
|
+
* - provides functions to interact with medals scoreboards
|
|
9163
|
+
* - Keeps connection alive and logs views
|
|
9164
|
+
* @namespace Newgrounds
|
|
9165
|
+
*/
|
|
9166
|
+
|
|
9167
|
+
/** Global Newgrounds object
|
|
9168
|
+
* @type {NewgroundsPlugin}
|
|
9169
|
+
* @memberof Newgrounds */
|
|
9170
|
+
let newgrounds;
|
|
9171
|
+
|
|
9172
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9173
|
+
/**
|
|
9174
|
+
* Newgrounds medal auto unlocks in newgrounds API
|
|
9175
|
+
* @extends Medal
|
|
9176
|
+
* @memberof Newgrounds
|
|
9177
|
+
*/
|
|
9178
|
+
class NewgroundsMedal extends Medal
|
|
9179
|
+
{
|
|
9180
|
+
/** Create a newgrounds medal object and adds it to the list of medals
|
|
9181
|
+
* @param {number} id - The unique identifier of the medal
|
|
9182
|
+
* @param {string} name - Name of the medal
|
|
9183
|
+
* @param {string} [description] - Description of the medal
|
|
9184
|
+
* @param {string} [icon] - Icon for the medal
|
|
9185
|
+
* @param {string} [src] - Image location for the medal
|
|
9186
|
+
*/
|
|
9187
|
+
constructor(id, name, description, icon, src)
|
|
9188
|
+
{ super(id, name, description, icon, src); }
|
|
9189
|
+
|
|
9190
|
+
/** Unlocks a medal if not already unlocked */
|
|
9191
|
+
unlock()
|
|
9192
|
+
{
|
|
9193
|
+
super.unlock();
|
|
9194
|
+
newgrounds && newgrounds.unlockMedal(this.id);
|
|
9195
|
+
}
|
|
9196
|
+
}
|
|
8943
9197
|
|
|
8944
9198
|
///////////////////////////////////////////////////////////////////////////////
|
|
8945
9199
|
/**
|
|
@@ -9516,16 +9770,13 @@ class UISystemPlugin
|
|
|
9516
9770
|
this.lastHoverObject = undefined;
|
|
9517
9771
|
/** @property {UIObject} - Current confirm menu being shown */
|
|
9518
9772
|
this.confirmDialog = undefined;
|
|
9519
|
-
/** @
|
|
9520
|
-
this.
|
|
9773
|
+
/** @private */
|
|
9774
|
+
this._keyInputObject = undefined;
|
|
9775
|
+
/** @private */
|
|
9776
|
+
this._onKeyDown = (e) => this._keyInputObject?.onKeyDown(e);
|
|
9521
9777
|
|
|
9522
9778
|
engineAddPlugin(uiUpdate, uiRender);
|
|
9523
9779
|
|
|
9524
|
-
// key down handler
|
|
9525
|
-
function onKeyDown(e)
|
|
9526
|
-
{ uiSystem.keyInputObject?.onKeyDown(e); }
|
|
9527
|
-
document.addEventListener('keydown', onKeyDown);
|
|
9528
|
-
|
|
9529
9780
|
// set object position in parent space
|
|
9530
9781
|
function updateTransforms(o)
|
|
9531
9782
|
{
|
|
@@ -9867,8 +10118,23 @@ class UISystemPlugin
|
|
|
9867
10118
|
return p;
|
|
9868
10119
|
}
|
|
9869
10120
|
|
|
10121
|
+
/** Object to send keyboard input to (typically a UITextInput).
|
|
10122
|
+
* The document keydown listener is only attached while this is set,
|
|
10123
|
+
* so games that never use text input pay no event-handling cost.
|
|
10124
|
+
* @type {UIObject} */
|
|
10125
|
+
get keyInputObject() { return this._keyInputObject; }
|
|
10126
|
+
set keyInputObject(obj)
|
|
10127
|
+
{
|
|
10128
|
+
const had = !!this._keyInputObject;
|
|
10129
|
+
this._keyInputObject = obj;
|
|
10130
|
+
if (!had && obj)
|
|
10131
|
+
document.addEventListener('keydown', this._onKeyDown);
|
|
10132
|
+
else if (had && !obj)
|
|
10133
|
+
document.removeEventListener('keydown', this._onKeyDown);
|
|
10134
|
+
}
|
|
10135
|
+
|
|
9870
10136
|
/** Destroy and remove all objects
|
|
9871
|
-
* @memberof
|
|
10137
|
+
* @memberof UISystem */
|
|
9872
10138
|
destroyObjects()
|
|
9873
10139
|
{
|
|
9874
10140
|
for (const o of this.uiObjects)
|
|
@@ -10291,15 +10557,13 @@ class UIObject
|
|
|
10291
10557
|
* @return {string} */
|
|
10292
10558
|
toString()
|
|
10293
10559
|
{
|
|
10294
|
-
if (!debug) return;
|
|
10295
|
-
|
|
10296
10560
|
let text = 'type = ' + this.constructor.name;
|
|
10297
10561
|
if (this.text)
|
|
10298
10562
|
text += '\ntext = ' + this.text;
|
|
10299
10563
|
if (this.pos.x || this.pos.y)
|
|
10300
10564
|
text += '\npos = ' + this.pos;
|
|
10301
10565
|
if (this.localPos.x || this.localPos.y)
|
|
10302
|
-
text += '\
|
|
10566
|
+
text += '\nlocalPos = ' + this.localPos;
|
|
10303
10567
|
if (this.size.x || this.size.y)
|
|
10304
10568
|
text += '\nsize = ' + this.size;
|
|
10305
10569
|
if (this.color)
|
|
@@ -10322,7 +10586,7 @@ class UIObject
|
|
|
10322
10586
|
|
|
10323
10587
|
/** Internal function called when object is clicked
|
|
10324
10588
|
* @param {boolean} [playSound] */
|
|
10325
|
-
click(playSound)
|
|
10589
|
+
click(playSound=true)
|
|
10326
10590
|
{
|
|
10327
10591
|
this.onClick();
|
|
10328
10592
|
if (playSound && this.soundClick)
|
|
@@ -10893,6 +11157,135 @@ class UIVideo extends UIObject
|
|
|
10893
11157
|
this.video.remove();
|
|
10894
11158
|
super.destroy();
|
|
10895
11159
|
}
|
|
11160
|
+
}
|
|
11161
|
+
|
|
11162
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11163
|
+
/**
|
|
11164
|
+
* UILayout - A container that auto-arranges children in a vertical list, horizontal list, or grid
|
|
11165
|
+
* - Set columns to 1 for a vertical list (default)
|
|
11166
|
+
* - Set columns to children.length for a horizontal list
|
|
11167
|
+
* - Set columns to N (1 < N < children.length) for a grid with N columns
|
|
11168
|
+
* - Per-child sizing: each row's height = max child.size.y in that row, each column's width = max child.size.x in that column
|
|
11169
|
+
* - Children are positioned centered in their cell
|
|
11170
|
+
* - Container auto-sizes to fit children plus padding
|
|
11171
|
+
* @extends UIObject
|
|
11172
|
+
* @memberof UISystem
|
|
11173
|
+
*/
|
|
11174
|
+
class UILayout extends UIObject
|
|
11175
|
+
{
|
|
11176
|
+
/** Create a UILayout container that auto-arranges children
|
|
11177
|
+
* @param {Vector2} [pos]
|
|
11178
|
+
* @param {number} [columns=1] - Number of columns (1 = vertical list)
|
|
11179
|
+
* @param {number} [gap=10] - Space between children
|
|
11180
|
+
* @param {number} [padding=10] - Space between container border and children
|
|
11181
|
+
* @param {boolean} [transparent=false] - If true, draws no background, outline, or shadow
|
|
11182
|
+
*/
|
|
11183
|
+
constructor(pos, columns=1, gap=10, padding=10, transparent=false)
|
|
11184
|
+
{
|
|
11185
|
+
super(pos);
|
|
11186
|
+
|
|
11187
|
+
ASSERT(isNumber(columns) && columns >= 1, 'ui layout columns must be a number >= 1');
|
|
11188
|
+
ASSERT(isNumber(gap), 'ui layout gap must be a number');
|
|
11189
|
+
ASSERT(isNumber(padding), 'ui layout padding must be a number');
|
|
11190
|
+
|
|
11191
|
+
/** @property {number} - Number of columns in the layout */
|
|
11192
|
+
this.columns = columns;
|
|
11193
|
+
/** @property {number} - Space between children */
|
|
11194
|
+
this.gap = gap;
|
|
11195
|
+
/** @property {number} - Space between container border and children */
|
|
11196
|
+
this.padding = padding;
|
|
11197
|
+
|
|
11198
|
+
if (transparent)
|
|
11199
|
+
{
|
|
11200
|
+
// pure positioning helper - skip background, outline, and shadow
|
|
11201
|
+
this.color = CLEAR_BLACK;
|
|
11202
|
+
this.gradientColor = undefined;
|
|
11203
|
+
this.lineWidth = 0;
|
|
11204
|
+
this.shadowColor = CLEAR_BLACK;
|
|
11205
|
+
}
|
|
11206
|
+
this.relayout();
|
|
11207
|
+
}
|
|
11208
|
+
|
|
11209
|
+
/** Add a child UIObject and re-layout
|
|
11210
|
+
* @param {UIObject} child
|
|
11211
|
+
* @return {UIObject} The child object added */
|
|
11212
|
+
addChild(child)
|
|
11213
|
+
{
|
|
11214
|
+
super.addChild(child);
|
|
11215
|
+
this.relayout();
|
|
11216
|
+
return child;
|
|
11217
|
+
}
|
|
11218
|
+
|
|
11219
|
+
/** Remove a child UIObject and re-layout
|
|
11220
|
+
* @param {UIObject} child */
|
|
11221
|
+
removeChild(child)
|
|
11222
|
+
{
|
|
11223
|
+
super.removeChild(child);
|
|
11224
|
+
this.relayout();
|
|
11225
|
+
}
|
|
11226
|
+
|
|
11227
|
+
/** Recompute child positions and container size based on per-child sizes.
|
|
11228
|
+
* Called automatically by addChild and removeChild. Call manually if you
|
|
11229
|
+
* mutate a child's size or change columns, gap, or padding. */
|
|
11230
|
+
relayout()
|
|
11231
|
+
{
|
|
11232
|
+
const n = this.children.length;
|
|
11233
|
+
if (!n)
|
|
11234
|
+
{
|
|
11235
|
+
this.size = vec2(this.padding * 2);
|
|
11236
|
+
return;
|
|
11237
|
+
}
|
|
11238
|
+
|
|
11239
|
+
const cols = this.columns;
|
|
11240
|
+
const rows = ceil(n / cols);
|
|
11241
|
+
const colWidths = new Array(cols).fill(0);
|
|
11242
|
+
const rowHeights = new Array(rows).fill(0);
|
|
11243
|
+
|
|
11244
|
+
// first pass: compute column widths and row heights from child sizes
|
|
11245
|
+
for (let i = 0; i < n; ++i)
|
|
11246
|
+
{
|
|
11247
|
+
const col = i % cols;
|
|
11248
|
+
const row = floor(i / cols);
|
|
11249
|
+
const child = this.children[i];
|
|
11250
|
+
colWidths[col] = max(colWidths[col], child.size.x);
|
|
11251
|
+
rowHeights[row] = max(rowHeights[row], child.size.y);
|
|
11252
|
+
}
|
|
11253
|
+
|
|
11254
|
+
// total content size (sum of column widths/row heights plus gaps between them)
|
|
11255
|
+
let contentWidth = this.gap * (cols - 1);
|
|
11256
|
+
for (const w of colWidths) contentWidth += w;
|
|
11257
|
+
let contentHeight = this.gap * (rows - 1);
|
|
11258
|
+
for (const h of rowHeights) contentHeight += h;
|
|
11259
|
+
|
|
11260
|
+
// cumulative column/row offsets so positioning is O(n) not O(n^2)
|
|
11261
|
+
const colOffsets = new Array(cols);
|
|
11262
|
+
let xAcc = 0;
|
|
11263
|
+
for (let c = 0; c < cols; ++c)
|
|
11264
|
+
{
|
|
11265
|
+
colOffsets[c] = xAcc;
|
|
11266
|
+
xAcc += colWidths[c];
|
|
11267
|
+
}
|
|
11268
|
+
const rowOffsets = new Array(rows);
|
|
11269
|
+
let yAcc = 0;
|
|
11270
|
+
for (let r = 0; r < rows; ++r)
|
|
11271
|
+
{
|
|
11272
|
+
rowOffsets[r] = yAcc;
|
|
11273
|
+
yAcc += rowHeights[r];
|
|
11274
|
+
}
|
|
11275
|
+
|
|
11276
|
+
// second pass: position each child centered in its cell
|
|
11277
|
+
for (let i = 0; i < n; ++i)
|
|
11278
|
+
{
|
|
11279
|
+
const col = i % cols;
|
|
11280
|
+
const row = floor(i / cols);
|
|
11281
|
+
const x = -contentWidth/2 + colOffsets[col] + this.gap * col + colWidths[col] / 2;
|
|
11282
|
+
const y = -contentHeight/2 + rowOffsets[row] + this.gap * row + rowHeights[row] / 2;
|
|
11283
|
+
this.children[i].localPos = vec2(x, y);
|
|
11284
|
+
}
|
|
11285
|
+
|
|
11286
|
+
// container size = content + padding on all sides
|
|
11287
|
+
this.size = vec2(contentWidth + this.padding * 2, contentHeight + this.padding * 2);
|
|
11288
|
+
}
|
|
10896
11289
|
}
|
|
10897
11290
|
/**
|
|
10898
11291
|
* LittleJS Box2D Physics Plugin
|
|
@@ -11295,7 +11688,8 @@ class Box2dObject extends EngineObject
|
|
|
11295
11688
|
{
|
|
11296
11689
|
this.pos = pos;
|
|
11297
11690
|
this.angle = angle;
|
|
11298
|
-
|
|
11691
|
+
// box2d uses reverse angle
|
|
11692
|
+
this.body.SetTransform(box2d.vec2dTo(pos), -angle);
|
|
11299
11693
|
}
|
|
11300
11694
|
|
|
11301
11695
|
/** Sets the position
|
|
@@ -11306,7 +11700,7 @@ class Box2dObject extends EngineObject
|
|
|
11306
11700
|
/** Sets the angle
|
|
11307
11701
|
* @param {number} angle */
|
|
11308
11702
|
setAngle(angle)
|
|
11309
|
-
{ this.setTransform(box2d.vec2From(this.body.GetPosition()),
|
|
11703
|
+
{ this.setTransform(box2d.vec2From(this.body.GetPosition()), angle); }
|
|
11310
11704
|
|
|
11311
11705
|
/** Sets the linear velocity
|
|
11312
11706
|
* @param {Vector2} velocity */
|
|
@@ -11418,14 +11812,29 @@ class Box2dObject extends EngineObject
|
|
|
11418
11812
|
this.body.ApplyForce(box2d.vec2dTo(force), box2d.vec2dTo(pos));
|
|
11419
11813
|
}
|
|
11420
11814
|
|
|
11421
|
-
/** Apply acceleration to this object
|
|
11815
|
+
/** Apply acceleration to this object (changes velocity by acceleration,
|
|
11816
|
+
* mass-independent — matches EngineObject.applyAcceleration semantics).
|
|
11817
|
+
* Use applyImpulse if you want the mass-dependent velocity change
|
|
11818
|
+
* Δv = impulse / mass, or applyForce for a Newton-style sustained force.
|
|
11422
11819
|
* @param {Vector2} acceleration
|
|
11423
11820
|
* @param {Vector2} [pos] */
|
|
11424
11821
|
applyAcceleration(acceleration, pos)
|
|
11425
|
-
{
|
|
11822
|
+
{
|
|
11426
11823
|
pos ||= this.getCenterOfMass();
|
|
11427
11824
|
this.setAwake();
|
|
11428
|
-
|
|
11825
|
+
const impulse = acceleration.scale(this.getMass());
|
|
11826
|
+
this.body.ApplyLinearImpulse(box2d.vec2dTo(impulse), box2d.vec2dTo(pos));
|
|
11827
|
+
}
|
|
11828
|
+
|
|
11829
|
+
/** Apply an instantaneous linear impulse. Changes velocity immediately by
|
|
11830
|
+
* impulse / mass (so heavier bodies move less for the same impulse).
|
|
11831
|
+
* @param {Vector2} impulse
|
|
11832
|
+
* @param {Vector2} [pos] */
|
|
11833
|
+
applyImpulse(impulse, pos)
|
|
11834
|
+
{
|
|
11835
|
+
pos ||= this.getCenterOfMass();
|
|
11836
|
+
this.setAwake();
|
|
11837
|
+
this.body.ApplyLinearImpulse(box2d.vec2dTo(impulse), box2d.vec2dTo(pos));
|
|
11429
11838
|
}
|
|
11430
11839
|
|
|
11431
11840
|
/** Apply torque to this object
|
|
@@ -11435,13 +11844,23 @@ class Box2dObject extends EngineObject
|
|
|
11435
11844
|
this.setAwake();
|
|
11436
11845
|
this.body.ApplyTorque(torque);
|
|
11437
11846
|
}
|
|
11438
|
-
|
|
11439
|
-
/** Apply angular acceleration to this object
|
|
11847
|
+
|
|
11848
|
+
/** Apply angular acceleration to this object (changes angular velocity by
|
|
11849
|
+
* acceleration, mass-independent — matches EngineObject semantics).
|
|
11440
11850
|
* @param {number} acceleration */
|
|
11441
11851
|
applyAngularAcceleration(acceleration)
|
|
11442
11852
|
{
|
|
11443
11853
|
this.setAwake();
|
|
11444
|
-
this.body.ApplyAngularImpulse(acceleration);
|
|
11854
|
+
this.body.ApplyAngularImpulse(acceleration * this.getInertia());
|
|
11855
|
+
}
|
|
11856
|
+
|
|
11857
|
+
/** Apply an instantaneous angular impulse. Changes angular velocity by
|
|
11858
|
+
* impulse / inertia immediately.
|
|
11859
|
+
* @param {number} impulse */
|
|
11860
|
+
applyAngularImpulse(impulse)
|
|
11861
|
+
{
|
|
11862
|
+
this.setAwake();
|
|
11863
|
+
this.body.ApplyAngularImpulse(impulse);
|
|
11445
11864
|
}
|
|
11446
11865
|
|
|
11447
11866
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -11687,7 +12106,7 @@ class Box2dJoint
|
|
|
11687
12106
|
|
|
11688
12107
|
/** Check if the connected bodies should collide
|
|
11689
12108
|
* @return {boolean} */
|
|
11690
|
-
getCollideConnected() { return this.box2dJoint.
|
|
12109
|
+
getCollideConnected() { return this.box2dJoint.GetCollideConnected();}
|
|
11691
12110
|
|
|
11692
12111
|
/** Check if either connected body is active
|
|
11693
12112
|
* @return {boolean} */
|
|
@@ -12851,6 +13270,7 @@ async function box2dInit()
|
|
|
12851
13270
|
{
|
|
12852
13271
|
if (o.body)
|
|
12853
13272
|
{
|
|
13273
|
+
// box2d uses reverse angle
|
|
12854
13274
|
o.pos = box2d.vec2From(o.body.GetPosition());
|
|
12855
13275
|
o.angle = -o.body.GetAngle();
|
|
12856
13276
|
}
|
|
@@ -13061,6 +13481,1271 @@ function drawThreeSlice(pos, size, startTile, color, borderSize=1, additiveColor
|
|
|
13061
13481
|
drawTile(pos.add(cornerPos.rotate(rotateAngle)), cornerSize, cornerTile, color, a, false, additiveColor, useWebGL, screenSpace, context);
|
|
13062
13482
|
}
|
|
13063
13483
|
}
|
|
13484
|
+
/**
|
|
13485
|
+
* LittleJS Tween System Plugin
|
|
13486
|
+
* - Lightweight tweens for numbers, Vector2, Color, or any .lerp-able type
|
|
13487
|
+
* - Chainable easing, looping, and ping-pong
|
|
13488
|
+
* - Property-path helper for the common case of animating an object field
|
|
13489
|
+
* - Auto-updates via engineAddPlugin; pauses with the game by default
|
|
13490
|
+
* @namespace TweenSystem
|
|
13491
|
+
*/
|
|
13492
|
+
|
|
13493
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
13494
|
+
|
|
13495
|
+
// Module-private list of tweens currently running.
|
|
13496
|
+
const tweenActive = [];
|
|
13497
|
+
|
|
13498
|
+
// Time tracking for delta computation between engine plugin calls.
|
|
13499
|
+
let lastTime = 0;
|
|
13500
|
+
let lastTimeReal = 0;
|
|
13501
|
+
|
|
13502
|
+
// True if the value is an instance of a class that exposes a numeric-percent
|
|
13503
|
+
// `lerp(other, percent)` method (Vector2, Color, or any future class).
|
|
13504
|
+
function isLerpable(v) { return v && typeof v.lerp === 'function'; }
|
|
13505
|
+
|
|
13506
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
13507
|
+
|
|
13508
|
+
/** A numeric tween: drives a callback with a value interpolated between
|
|
13509
|
+
* `start` and `end` over `duration` seconds. Pauses with the game by default.
|
|
13510
|
+
* @memberof TweenSystem
|
|
13511
|
+
* @example
|
|
13512
|
+
* // Animate a fade-out over 2 seconds with an ease-out sine curve.
|
|
13513
|
+
* new Tween((v) => obj.alpha = v, 1, 0, 2, { ease: Ease.OUT(Ease.SINE) });
|
|
13514
|
+
*/
|
|
13515
|
+
class Tween
|
|
13516
|
+
{
|
|
13517
|
+
/** Create a new tween. The callback fires immediately with `start` so the
|
|
13518
|
+
* target snaps to the start value on the same frame the tween is created.
|
|
13519
|
+
*
|
|
13520
|
+
* `start` and `end` may be numbers, Vector2 instances, Color instances, or
|
|
13521
|
+
* any object exposing a `lerp(other, percent) => sameType` method. The
|
|
13522
|
+
* callback receives the interpolated value (a number, or a fresh instance
|
|
13523
|
+
* for lerp-able types). Both endpoints must be the same type.
|
|
13524
|
+
* @param {function(number|Vector2|Color):void} callback - Called with the interpolated value each frame
|
|
13525
|
+
* @param {number|Vector2|Color} [start=0] - Starting value
|
|
13526
|
+
* @param {number|Vector2|Color} [end=1] - Ending value
|
|
13527
|
+
* @param {number} [duration=1] - Duration in seconds
|
|
13528
|
+
* @param {Object} [options]
|
|
13529
|
+
* @param {function(number):number} [options.ease] - Easing function (defaults to LINEAR)
|
|
13530
|
+
* @param {boolean} [options.useRealTime=false] - Advance even when the game is paused (matches Timer's useRealTime)
|
|
13531
|
+
* @param {boolean} [options.paused=false] - Start in paused state */
|
|
13532
|
+
constructor(callback, start = 0, end = 1, duration = 1, options = {})
|
|
13533
|
+
{
|
|
13534
|
+
ASSERT(typeof callback === 'function', 'Tween callback must be a function');
|
|
13535
|
+
if (isLerpable(start))
|
|
13536
|
+
{
|
|
13537
|
+
ASSERT(start.constructor === end.constructor,
|
|
13538
|
+
'Tween start and end must be the same type');
|
|
13539
|
+
}
|
|
13540
|
+
else
|
|
13541
|
+
{
|
|
13542
|
+
ASSERT(isNumber(start), 'Tween start must be a number or have a .lerp method');
|
|
13543
|
+
ASSERT(isNumber(end), 'Tween end must be a number when start is a number');
|
|
13544
|
+
}
|
|
13545
|
+
ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
|
|
13546
|
+
|
|
13547
|
+
this.callback = callback;
|
|
13548
|
+
this.start = start;
|
|
13549
|
+
this.end = end;
|
|
13550
|
+
this.duration = duration;
|
|
13551
|
+
this.life = duration;
|
|
13552
|
+
this.ease = options.ease || Ease.LINEAR;
|
|
13553
|
+
this.useRealTime = !!options.useRealTime;
|
|
13554
|
+
this.paused = !!options.paused;
|
|
13555
|
+
|
|
13556
|
+
/** @private completion callback set by then(), loop(), pingPong(). */
|
|
13557
|
+
this.thenCallback = undefined;
|
|
13558
|
+
/** @private remaining iterations including the current run (loop/pingPong only). */
|
|
13559
|
+
this.loopRemaining = 0;
|
|
13560
|
+
|
|
13561
|
+
tweenActive.push(this);
|
|
13562
|
+
// Snap target to start immediately.
|
|
13563
|
+
callback(this.interp(duration));
|
|
13564
|
+
}
|
|
13565
|
+
|
|
13566
|
+
/** Set the easing curve and return this for chaining.
|
|
13567
|
+
* @param {function(number):number} easeFn
|
|
13568
|
+
* @returns {Tween}
|
|
13569
|
+
* @memberof TweenSystem */
|
|
13570
|
+
setEase(easeFn)
|
|
13571
|
+
{
|
|
13572
|
+
this.ease = easeFn;
|
|
13573
|
+
return this;
|
|
13574
|
+
}
|
|
13575
|
+
|
|
13576
|
+
/** Set a single completion callback. Calling `then` again replaces the
|
|
13577
|
+
* previous callback. Returns this for chaining.
|
|
13578
|
+
*
|
|
13579
|
+
* Calling `then` after `loop` or `pingPong` overrides the loop chain
|
|
13580
|
+
* (last call wins).
|
|
13581
|
+
* @param {function():void} callback
|
|
13582
|
+
* @returns {Tween}
|
|
13583
|
+
* @memberof TweenSystem */
|
|
13584
|
+
then(callback)
|
|
13585
|
+
{
|
|
13586
|
+
this.thenCallback = callback;
|
|
13587
|
+
this.loopRemaining = 0;
|
|
13588
|
+
return this;
|
|
13589
|
+
}
|
|
13590
|
+
|
|
13591
|
+
/** Repeat this tween `n` total times. After each iteration finishes, a
|
|
13592
|
+
* fresh tween with the same parameters takes over via the `then` slot.
|
|
13593
|
+
* `loop()` with no argument loops forever.
|
|
13594
|
+
*
|
|
13595
|
+
* Mutually exclusive with `pingPong`; calling either replaces the other,
|
|
13596
|
+
* and calling `then` after either clears the loop (last call wins).
|
|
13597
|
+
* @param {number} [count=Infinity]
|
|
13598
|
+
* @returns {Tween}
|
|
13599
|
+
* @memberof TweenSystem */
|
|
13600
|
+
loop(count = Infinity)
|
|
13601
|
+
{
|
|
13602
|
+
this.loopRemaining = count;
|
|
13603
|
+
this.thenCallback = () => loopContinuation(this);
|
|
13604
|
+
return this;
|
|
13605
|
+
}
|
|
13606
|
+
|
|
13607
|
+
/** Like `loop`, but swap `start` and `end` between iterations so the value
|
|
13608
|
+
* bounces back and forth. `pingPong()` with no argument bounces forever.
|
|
13609
|
+
*
|
|
13610
|
+
* Mutually exclusive with `loop`; calling either replaces the other, and
|
|
13611
|
+
* calling `then` after either clears the loop (last call wins).
|
|
13612
|
+
* @param {number} [count=Infinity]
|
|
13613
|
+
* @returns {Tween}
|
|
13614
|
+
* @memberof TweenSystem */
|
|
13615
|
+
pingPong(count = Infinity)
|
|
13616
|
+
{
|
|
13617
|
+
this.loopRemaining = count;
|
|
13618
|
+
this.thenCallback = () => pingPongContinuation(this);
|
|
13619
|
+
return this;
|
|
13620
|
+
}
|
|
13621
|
+
|
|
13622
|
+
/** Pause this tween. While paused, tweenUpdate skips it.
|
|
13623
|
+
* @memberof TweenSystem */
|
|
13624
|
+
pause() { this.paused = true; }
|
|
13625
|
+
|
|
13626
|
+
/** Resume a paused tween.
|
|
13627
|
+
* @memberof TweenSystem */
|
|
13628
|
+
resume() { this.paused = false; }
|
|
13629
|
+
|
|
13630
|
+
/** Reset this tween to the start: life back to duration, pause cleared,
|
|
13631
|
+
* re-added to the active list if previously stopped, and the callback
|
|
13632
|
+
* re-fired with the start value.
|
|
13633
|
+
* @memberof TweenSystem */
|
|
13634
|
+
restart()
|
|
13635
|
+
{
|
|
13636
|
+
this.life = this.duration;
|
|
13637
|
+
this.paused = false;
|
|
13638
|
+
if (tweenActive.indexOf(this) < 0) tweenActive.push(this);
|
|
13639
|
+
this.callback(this.interp(this.duration));
|
|
13640
|
+
}
|
|
13641
|
+
|
|
13642
|
+
/** True if this tween is in the active list and not paused.
|
|
13643
|
+
* @returns {boolean}
|
|
13644
|
+
* @memberof TweenSystem */
|
|
13645
|
+
isActive()
|
|
13646
|
+
{
|
|
13647
|
+
return !this.paused && tweenActive.indexOf(this) >= 0;
|
|
13648
|
+
}
|
|
13649
|
+
|
|
13650
|
+
/** Get how far this tween has progressed, from 0 (just started) to 1
|
|
13651
|
+
* (completed). Clamped — overshoot past completion still reads 1.
|
|
13652
|
+
* @returns {number}
|
|
13653
|
+
* @memberof TweenSystem */
|
|
13654
|
+
getPercent()
|
|
13655
|
+
{
|
|
13656
|
+
return percent(this.duration - this.life, 0, this.duration);
|
|
13657
|
+
}
|
|
13658
|
+
|
|
13659
|
+
/** Get the current interpolated value (the value most recently passed to
|
|
13660
|
+
* the callback). Returns a number, Vector2, or Color depending on the
|
|
13661
|
+
* tween's start/end types.
|
|
13662
|
+
* @returns {number|Vector2|Color}
|
|
13663
|
+
* @memberof TweenSystem */
|
|
13664
|
+
getValue()
|
|
13665
|
+
{
|
|
13666
|
+
return this.interp(this.life);
|
|
13667
|
+
}
|
|
13668
|
+
|
|
13669
|
+
/** Compute the interpolated value at the given remaining `life`.
|
|
13670
|
+
* At life === duration the result is `start`; at life === 0 it is `end`.
|
|
13671
|
+
* @param {number} life
|
|
13672
|
+
* @returns {number}
|
|
13673
|
+
* @memberof TweenSystem */
|
|
13674
|
+
interp(life)
|
|
13675
|
+
{
|
|
13676
|
+
const x = this.ease((this.duration - life) / this.duration);
|
|
13677
|
+
if (isLerpable(this.start))
|
|
13678
|
+
return this.start.lerp(this.end, x);
|
|
13679
|
+
return this.start + (this.end - this.start) * x;
|
|
13680
|
+
}
|
|
13681
|
+
|
|
13682
|
+
/** Remove this tween from the active list and prevent any pending then-callback.
|
|
13683
|
+
* @memberof TweenSystem */
|
|
13684
|
+
stop()
|
|
13685
|
+
{
|
|
13686
|
+
const i = tweenActive.indexOf(this);
|
|
13687
|
+
if (i >= 0) tweenActive.splice(i, 1);
|
|
13688
|
+
this.thenCallback = undefined;
|
|
13689
|
+
}
|
|
13690
|
+
}
|
|
13691
|
+
|
|
13692
|
+
/** Library of named easing curves and direction modifiers.
|
|
13693
|
+
* All curves accept `x` in [0,1] and return [0,1] (with possible overshoot
|
|
13694
|
+
* for ELASTIC/BACK/SPRING/BOUNCE). Curves are values you pass to `setEase`
|
|
13695
|
+
* or compose via the IN/OUT/IN_OUT/PIECEWISE/BEZIER modifiers.
|
|
13696
|
+
* @memberof TweenSystem
|
|
13697
|
+
* @example
|
|
13698
|
+
* // Use a basic curve
|
|
13699
|
+
* new Tween(callback, 0, 10, 1).setEase(Ease.SINE);
|
|
13700
|
+
* // Use a modifier on a curve
|
|
13701
|
+
* new Tween(callback, 0, 10, 1).setEase(Ease.OUT(Ease.BACK));
|
|
13702
|
+
*/
|
|
13703
|
+
const Ease =
|
|
13704
|
+
{
|
|
13705
|
+
/** Linear (identity) curve.
|
|
13706
|
+
* @param {number} x
|
|
13707
|
+
* @returns {number}
|
|
13708
|
+
* @memberof TweenSystem */
|
|
13709
|
+
LINEAR: (x) => x,
|
|
13710
|
+
|
|
13711
|
+
/** Power curve factory: `Ease.POWER(n)` returns `x => x**n`.
|
|
13712
|
+
* Use n=2 for quadratic, n=3 for cubic, etc.
|
|
13713
|
+
* @param {number} n
|
|
13714
|
+
* @returns {function(number):number}
|
|
13715
|
+
* @memberof TweenSystem */
|
|
13716
|
+
POWER: (n) => (x) => x ** n,
|
|
13717
|
+
|
|
13718
|
+
/** Sine ease-in curve: starts slow, ends fast.
|
|
13719
|
+
* @param {number} x
|
|
13720
|
+
* @returns {number}
|
|
13721
|
+
* @memberof TweenSystem */
|
|
13722
|
+
SINE: (x) => 1 - cos(x * (PI / 2)),
|
|
13723
|
+
|
|
13724
|
+
/** Circular ease-in curve.
|
|
13725
|
+
* @param {number} x
|
|
13726
|
+
* @returns {number}
|
|
13727
|
+
* @memberof TweenSystem */
|
|
13728
|
+
CIRC: (x) => 1 - (1 - x * x)**.5,
|
|
13729
|
+
|
|
13730
|
+
/** Exponential ease-in curve (`2^(10x-10)`).
|
|
13731
|
+
* @param {number} x
|
|
13732
|
+
* @returns {number}
|
|
13733
|
+
* @memberof TweenSystem */
|
|
13734
|
+
EXPO: (x) => 2 ** (10 * x - 10),
|
|
13735
|
+
|
|
13736
|
+
/** Back ease-in: overshoots backward at the start before snapping forward.
|
|
13737
|
+
* @param {number} x
|
|
13738
|
+
* @returns {number}
|
|
13739
|
+
* @memberof TweenSystem */
|
|
13740
|
+
BACK: (x) => x * x * (2.70158 * x - 1.70158),
|
|
13741
|
+
|
|
13742
|
+
/** Elastic ease-in: oscillates with decreasing amplitude.
|
|
13743
|
+
* @param {number} x
|
|
13744
|
+
* @returns {number}
|
|
13745
|
+
* @memberof TweenSystem */
|
|
13746
|
+
ELASTIC: (x) =>
|
|
13747
|
+
-(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
|
|
13748
|
+
|
|
13749
|
+
/** Spring-like ease-out: oscillates outward after passing the target.
|
|
13750
|
+
* @param {number} x
|
|
13751
|
+
* @returns {number}
|
|
13752
|
+
* @memberof TweenSystem */
|
|
13753
|
+
SPRING: (x) =>
|
|
13754
|
+
1 -
|
|
13755
|
+
(sin(PI * (1 - x) * (0.2 + 2.5 * (1 - x) ** 3)) *
|
|
13756
|
+
x ** 2.2 +
|
|
13757
|
+
(1 - x)) *
|
|
13758
|
+
(1.0 + 1.2 * x),
|
|
13759
|
+
|
|
13760
|
+
/** Bouncing ease-in: slow ramp with bouncing impacts near the end.
|
|
13761
|
+
* Symmetric with the other base curves, which are all ease-in. To get the
|
|
13762
|
+
* classic "object falls and hits the ground" shape (bounces near x=1),
|
|
13763
|
+
* wrap with `Ease.OUT`: `Ease.OUT(Ease.BOUNCE)`.
|
|
13764
|
+
* @param {number} x
|
|
13765
|
+
* @returns {number}
|
|
13766
|
+
* @memberof TweenSystem
|
|
13767
|
+
* @example
|
|
13768
|
+
* Ease.BOUNCE // ease-in bounce (slow, then bouncy at end)
|
|
13769
|
+
* Ease.OUT(Ease.BOUNCE) // ease-out bounce (object hits ground)
|
|
13770
|
+
* Ease.IN_OUT(Ease.BOUNCE) // bounces at both ends
|
|
13771
|
+
*/
|
|
13772
|
+
BOUNCE: (x) =>
|
|
13773
|
+
{
|
|
13774
|
+
// Inverted form of the standard easeOutBounce: 1 - bounceOut(1 - x).
|
|
13775
|
+
let t = 1 - x, f;
|
|
13776
|
+
if (t < 4 / 11) f = 7.5625 * t * t;
|
|
13777
|
+
else if (t < 8 / 11) f = 7.5625 * (t -= 6 / 11) * t + 0.75;
|
|
13778
|
+
else if (t < 10 / 11) f = 7.5625 * (t -= 9 / 11) * t + 0.9375;
|
|
13779
|
+
else f = 7.5625 * (t -= 10.5 / 11) * t + 0.984375;
|
|
13780
|
+
return 1 - f;
|
|
13781
|
+
},
|
|
13782
|
+
|
|
13783
|
+
/** Ease-in direction modifier: returns the curve unchanged. Symmetric
|
|
13784
|
+
* with `OUT` and `IN_OUT`. Base curves are already ease-in by
|
|
13785
|
+
* convention, so wrapping a curve in `IN` is a no-op — useful when
|
|
13786
|
+
* picking the direction programmatically.
|
|
13787
|
+
* @param {function(number):number} f - Curve to use as ease-in (returned unchanged)
|
|
13788
|
+
* @returns {function(number):number}
|
|
13789
|
+
* @memberof TweenSystem
|
|
13790
|
+
* @example
|
|
13791
|
+
* // Pick direction at runtime
|
|
13792
|
+
* const dir = bouncyMode ? Ease.OUT : Ease.IN;
|
|
13793
|
+
* new Tween(cb, 0, 10, 1).setEase(dir(Ease.BACK));
|
|
13794
|
+
*/
|
|
13795
|
+
IN: (f) => f,
|
|
13796
|
+
|
|
13797
|
+
/** Reverse a curve so it eases out instead of in: `x => 1 - f(1 - x)`.
|
|
13798
|
+
* @param {function(number):number} f
|
|
13799
|
+
* @returns {function(number):number}
|
|
13800
|
+
* @memberof TweenSystem
|
|
13801
|
+
* @example
|
|
13802
|
+
* Ease.OUT(Ease.POWER(2)) // ease-out quadratic
|
|
13803
|
+
*/
|
|
13804
|
+
OUT: (f) => (x) => 1 - f(1 - x),
|
|
13805
|
+
|
|
13806
|
+
/** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve.
|
|
13807
|
+
* Bug-fix vs the original library: the original referenced an undefined
|
|
13808
|
+
* global `Piecewise`; this implementation routes through `Ease.PIECEWISE`.
|
|
13809
|
+
* @param {function(number):number} f
|
|
13810
|
+
* @returns {function(number):number}
|
|
13811
|
+
* @memberof TweenSystem */
|
|
13812
|
+
IN_OUT: (f) => Ease.PIECEWISE(f, Ease.OUT(f)),
|
|
13813
|
+
|
|
13814
|
+
/** Split [0,1] into N equal sections and run a different curve in each.
|
|
13815
|
+
* Each curve is mapped to its section: section i runs over [i/n, (i+1)/n]
|
|
13816
|
+
* and its output is mapped to [i/n, (i+1)/n] of the overall range.
|
|
13817
|
+
* @param {...function(number):number} fns
|
|
13818
|
+
* @returns {function(number):number}
|
|
13819
|
+
* @memberof TweenSystem */
|
|
13820
|
+
PIECEWISE: (...fns) =>
|
|
13821
|
+
{
|
|
13822
|
+
const n = fns.length;
|
|
13823
|
+
return (x) =>
|
|
13824
|
+
{
|
|
13825
|
+
const i = (x * n - 1e-9) >> 0;
|
|
13826
|
+
return (fns[i]((x - i / n) * n) + i) / n;
|
|
13827
|
+
};
|
|
13828
|
+
},
|
|
13829
|
+
|
|
13830
|
+
/** Cubic Bezier curve solver in the style of CSS `cubic-bezier`.
|
|
13831
|
+
* Control points (0,0), (x1,y1), (x2,y2), (1,1).
|
|
13832
|
+
* @param {number} x1
|
|
13833
|
+
* @param {number} y1
|
|
13834
|
+
* @param {number} x2
|
|
13835
|
+
* @param {number} y2
|
|
13836
|
+
* @returns {function(number):number}
|
|
13837
|
+
* @memberof TweenSystem
|
|
13838
|
+
* @example
|
|
13839
|
+
* Ease.BEZIER(0.25, 0.1, 0.25, 1) // CSS "ease"
|
|
13840
|
+
*/
|
|
13841
|
+
BEZIER: (x1, y1, x2, y2) =>
|
|
13842
|
+
{
|
|
13843
|
+
// Parametric cubic Bezier with implicit (0,0) and (1,1) endpoints.
|
|
13844
|
+
const curve = (t) =>
|
|
13845
|
+
{
|
|
13846
|
+
const u = 1 - t;
|
|
13847
|
+
const c1 = 3 * u * u * t;
|
|
13848
|
+
const c2 = 3 * u * t * t;
|
|
13849
|
+
const t3 = t ** 3;
|
|
13850
|
+
return [c1 * x1 + c2 * x2 + t3, c1 * y1 + c2 * y2 + t3];
|
|
13851
|
+
};
|
|
13852
|
+
return (x) =>
|
|
13853
|
+
{
|
|
13854
|
+
// Binary search for t such that curve(t).x ≈ x, then return curve(t).y.
|
|
13855
|
+
let t0 = 0, t1 = 1;
|
|
13856
|
+
for (let i = 0; i < 128; i++)
|
|
13857
|
+
{
|
|
13858
|
+
const tMid = (t0 + t1) / 2;
|
|
13859
|
+
const [bx, by] = curve(tMid);
|
|
13860
|
+
if (abs(bx - x) < 1e-5) return by;
|
|
13861
|
+
if (bx < x) t0 = tMid; else t1 = tMid;
|
|
13862
|
+
}
|
|
13863
|
+
return curve((t0 + t1) / 2)[1];
|
|
13864
|
+
};
|
|
13865
|
+
},
|
|
13866
|
+
};
|
|
13867
|
+
|
|
13868
|
+
/** Tween a property on an object by dot-path. Returns the underlying Tween
|
|
13869
|
+
* so all chaining methods (`setEase`, `then`, `loop`, `pingPong`, etc.)
|
|
13870
|
+
* remain available.
|
|
13871
|
+
*
|
|
13872
|
+
* `start` and `end` may be numbers, Vector2 instances, Color instances, or
|
|
13873
|
+
* any object with a `lerp(other, percent) => sameType` method.
|
|
13874
|
+
* @param {Object} target - The object whose property is being animated
|
|
13875
|
+
* @param {string} propertyPath - Dot-separated path, e.g. `'pos.x'` or `'color'`
|
|
13876
|
+
* @param {number|Vector2|Color} start - Starting value
|
|
13877
|
+
* @param {number|Vector2|Color} end - Ending value
|
|
13878
|
+
* @param {number} [duration=1] - Duration in seconds
|
|
13879
|
+
* @param {Object} [options] - Same options as the Tween constructor
|
|
13880
|
+
* @returns {Tween}
|
|
13881
|
+
* @memberof TweenSystem
|
|
13882
|
+
* @example
|
|
13883
|
+
* // Numeric: slide an object's x with an ease-out sine curve
|
|
13884
|
+
* tweenProperty(player, 'pos.x', 0, 10, 2).setEase(Ease.OUT(Ease.SINE));
|
|
13885
|
+
* // Vector2: animate a position diagonally
|
|
13886
|
+
* tweenProperty(player, 'pos', vec2(-5, 0), vec2(5, 3), 2);
|
|
13887
|
+
* // Color: pulse between two colors
|
|
13888
|
+
* tweenProperty(sprite, 'color', RED, BLUE, 1).pingPong();
|
|
13889
|
+
*/
|
|
13890
|
+
function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
|
|
13891
|
+
{
|
|
13892
|
+
ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
|
|
13893
|
+
ASSERT(isString(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
|
|
13894
|
+
|
|
13895
|
+
const parts = propertyPath.split('.');
|
|
13896
|
+
const lastKey = parts.pop();
|
|
13897
|
+
const callback = (value) =>
|
|
13898
|
+
{
|
|
13899
|
+
let obj = target;
|
|
13900
|
+
for (const k of parts) obj = obj[k];
|
|
13901
|
+
obj[lastKey] = value;
|
|
13902
|
+
};
|
|
13903
|
+
return new Tween(callback, start, end, duration, options);
|
|
13904
|
+
}
|
|
13905
|
+
|
|
13906
|
+
// Continuation that schedules the next loop iteration when one finishes.
|
|
13907
|
+
// Called from the completed tween's `then` slot. Decrements the counter and
|
|
13908
|
+
// only spawns a new tween if more iterations remain.
|
|
13909
|
+
function loopContinuation(prev)
|
|
13910
|
+
{
|
|
13911
|
+
if (prev.loopRemaining !== Infinity && prev.loopRemaining <= 1) return;
|
|
13912
|
+
const next = new Tween(prev.callback, prev.start, prev.end, prev.duration,
|
|
13913
|
+
{ ease: prev.ease, useRealTime: prev.useRealTime });
|
|
13914
|
+
next.loopRemaining = prev.loopRemaining === Infinity
|
|
13915
|
+
? Infinity
|
|
13916
|
+
: prev.loopRemaining - 1;
|
|
13917
|
+
next.thenCallback = () => loopContinuation(next);
|
|
13918
|
+
}
|
|
13919
|
+
|
|
13920
|
+
// Continuation for pingPong: spawns a new tween with start and end swapped.
|
|
13921
|
+
function pingPongContinuation(prev)
|
|
13922
|
+
{
|
|
13923
|
+
if (prev.loopRemaining !== Infinity && prev.loopRemaining <= 1) return;
|
|
13924
|
+
const next = new Tween(prev.callback, prev.end, prev.start, prev.duration,
|
|
13925
|
+
{ ease: prev.ease, useRealTime: prev.useRealTime });
|
|
13926
|
+
next.loopRemaining = prev.loopRemaining === Infinity
|
|
13927
|
+
? Infinity
|
|
13928
|
+
: prev.loopRemaining - 1;
|
|
13929
|
+
next.thenCallback = () => pingPongContinuation(next);
|
|
13930
|
+
}
|
|
13931
|
+
|
|
13932
|
+
/** Engine plugin hook: advance every active tween by the appropriate delta.
|
|
13933
|
+
* Called once per render frame by the engine (no arguments). May also be
|
|
13934
|
+
* called explicitly with `(gameDelta, realDelta)` to drive tweens manually
|
|
13935
|
+
* — useful for headless tests or custom replay/scrubbing systems.
|
|
13936
|
+
* @param {number} [gameDelta] - Game-time delta in seconds; default: time - lastTime
|
|
13937
|
+
* @param {number} [realDelta] - Real-time delta in seconds; default: timeReal - lastTimeReal
|
|
13938
|
+
* @memberof TweenSystem */
|
|
13939
|
+
function tweenUpdate(gameDelta, realDelta)
|
|
13940
|
+
{
|
|
13941
|
+
if (gameDelta === undefined)
|
|
13942
|
+
{
|
|
13943
|
+
// Engine path: compute deltas from engine time globals.
|
|
13944
|
+
gameDelta = time - lastTime;
|
|
13945
|
+
realDelta = timeReal - lastTimeReal;
|
|
13946
|
+
lastTime = time;
|
|
13947
|
+
lastTimeReal = timeReal;
|
|
13948
|
+
}
|
|
13949
|
+
else if (realDelta === undefined)
|
|
13950
|
+
{
|
|
13951
|
+
// Manual path with one arg: real and game advance together.
|
|
13952
|
+
realDelta = gameDelta;
|
|
13953
|
+
}
|
|
13954
|
+
|
|
13955
|
+
// Iterate in reverse so removals don't disturb iteration.
|
|
13956
|
+
for (let i = tweenActive.length; i--;)
|
|
13957
|
+
{
|
|
13958
|
+
const t = tweenActive[i];
|
|
13959
|
+
if (t.paused) continue;
|
|
13960
|
+
const dt = t.useRealTime ? realDelta : gameDelta;
|
|
13961
|
+
if (dt <= 0) continue;
|
|
13962
|
+
|
|
13963
|
+
t.life -= dt;
|
|
13964
|
+
if (t.life > 0)
|
|
13965
|
+
{
|
|
13966
|
+
t.callback(t.interp(t.life));
|
|
13967
|
+
}
|
|
13968
|
+
else
|
|
13969
|
+
{
|
|
13970
|
+
// Completion: fire end value, remove from active, fire then-callback.
|
|
13971
|
+
t.callback(t.interp(0));
|
|
13972
|
+
tweenActive.splice(i, 1);
|
|
13973
|
+
const cb = t.thenCallback;
|
|
13974
|
+
t.thenCallback = undefined;
|
|
13975
|
+
if (cb) cb();
|
|
13976
|
+
}
|
|
13977
|
+
}
|
|
13978
|
+
}
|
|
13979
|
+
|
|
13980
|
+
/** Stop every active tween and clear their then-callbacks. Useful for resets
|
|
13981
|
+
* on level transitions or when changing scenes.
|
|
13982
|
+
* @memberof TweenSystem */
|
|
13983
|
+
function tweenStopAll()
|
|
13984
|
+
{
|
|
13985
|
+
for (const t of tweenActive) t.thenCallback = undefined;
|
|
13986
|
+
tweenActive.length = 0;
|
|
13987
|
+
}
|
|
13988
|
+
|
|
13989
|
+
// Register with the engine so tweens auto-advance.
|
|
13990
|
+
engineAddPlugin(tweenUpdate);
|
|
13991
|
+
|
|
13992
|
+
/**
|
|
13993
|
+
* LittleJS PathFinder Plugin
|
|
13994
|
+
* - Grid-based A* pathfinder with two-pass smoothing for natural-looking paths
|
|
13995
|
+
* - Works directly on a TileCollisionLayer, or override isWalkable/getCost for any grid
|
|
13996
|
+
* - Debug visualization via engine debug primitives (stripped in release builds)
|
|
13997
|
+
* - Port of frankforce.com pathFindingBase.cpp (2018)
|
|
13998
|
+
* @namespace PathFinding
|
|
13999
|
+
*/
|
|
14000
|
+
|
|
14001
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14002
|
+
|
|
14003
|
+
// Diagonal step cost — pre-computed for the A* expansion inner loop.
|
|
14004
|
+
const PATHFINDER_DIAGONAL_COST = Math.SQRT2;
|
|
14005
|
+
|
|
14006
|
+
// Shared 1x1 size vector for per-tile debugRect calls. debugRect copies the
|
|
14007
|
+
// argument internally, so reusing one instance is safe.
|
|
14008
|
+
const PATHFINDER_TILE_VEC = vec2(1);
|
|
14009
|
+
|
|
14010
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14011
|
+
|
|
14012
|
+
/** A single grid cell tracked by the pathfinder. Allocated once per cell at
|
|
14013
|
+
* PathFinder construction; reset (not reallocated) at the start of every
|
|
14014
|
+
* findPath call.
|
|
14015
|
+
* @memberof PathFinding */
|
|
14016
|
+
class PathFinderNode
|
|
14017
|
+
{
|
|
14018
|
+
/** @param {number} x - Tile x
|
|
14019
|
+
* @param {number} y - Tile y */
|
|
14020
|
+
constructor(x, y)
|
|
14021
|
+
{
|
|
14022
|
+
/** @property {Vector2} - Tile coords (integer) */
|
|
14023
|
+
this.pos = vec2(x, y);
|
|
14024
|
+
/** @property {Vector2} - World-space center of this tile (set by buildNodeData) */
|
|
14025
|
+
this.posWorld = vec2();
|
|
14026
|
+
/** @property {boolean} - True if this cell is passable (cleared each findPath call) */
|
|
14027
|
+
this.walkable = false;
|
|
14028
|
+
/** @property {number} - Extra cost added to A* G-score for stepping on this cell */
|
|
14029
|
+
this.cost = 0;
|
|
14030
|
+
/** @property {number} - A* G-score: actual cost from start to this node */
|
|
14031
|
+
this.g = 0;
|
|
14032
|
+
/** @property {number} - A* F-score: G + heuristic */
|
|
14033
|
+
this.f = 0;
|
|
14034
|
+
/** @property {PathFinderNode|null} - Parent for path reconstruction */
|
|
14035
|
+
this.parent = null;
|
|
14036
|
+
/** @property {boolean} - In the A* open list */
|
|
14037
|
+
this.isOpen = false;
|
|
14038
|
+
/** @property {boolean} - In the A* closed list */
|
|
14039
|
+
this.isClosed = false;
|
|
14040
|
+
}
|
|
14041
|
+
|
|
14042
|
+
/** Reset per-search state (called at the start of buildNodeData). */
|
|
14043
|
+
reset()
|
|
14044
|
+
{
|
|
14045
|
+
this.walkable = false;
|
|
14046
|
+
this.cost = 0;
|
|
14047
|
+
this.g = 0;
|
|
14048
|
+
this.f = 0;
|
|
14049
|
+
this.parent = null;
|
|
14050
|
+
this.isOpen = false;
|
|
14051
|
+
this.isClosed = false;
|
|
14052
|
+
}
|
|
14053
|
+
|
|
14054
|
+
/** True if walkable and not blocked by cost. */
|
|
14055
|
+
isClear()
|
|
14056
|
+
{
|
|
14057
|
+
return this.walkable && this.cost === 0;
|
|
14058
|
+
}
|
|
14059
|
+
}
|
|
14060
|
+
|
|
14061
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14062
|
+
|
|
14063
|
+
/** Grid pathfinder using A* with two optional smoothing passes.
|
|
14064
|
+
* @memberof PathFinding
|
|
14065
|
+
* @example
|
|
14066
|
+
* // Tile-layer driven (most common):
|
|
14067
|
+
* const pf = new PathFinder(myTileCollisionLayer);
|
|
14068
|
+
* const path = pf.findPath(player.pos, mousePos);
|
|
14069
|
+
*
|
|
14070
|
+
* // Bare grid with custom walkability:
|
|
14071
|
+
* const pf = new PathFinder(vec2(50, 50));
|
|
14072
|
+
* pf.isWalkable = (x, y) => myGrid[y*50 + x] === 0;
|
|
14073
|
+
*/
|
|
14074
|
+
class PathFinder
|
|
14075
|
+
{
|
|
14076
|
+
/** @param {TileCollisionLayer|Vector2} source - Either a TileCollisionLayer
|
|
14077
|
+
* (size and walkability auto-derived) or a Vector2 grid size (user
|
|
14078
|
+
* overrides isWalkable). */
|
|
14079
|
+
constructor(source)
|
|
14080
|
+
{
|
|
14081
|
+
// Accept either a Vector2 size or a TileCollisionLayer (which has a .size).
|
|
14082
|
+
// We don't import TileCollisionLayer to avoid coupling; we duck-type on
|
|
14083
|
+
// .size + .getCollisionData.
|
|
14084
|
+
if (isVector2(source))
|
|
14085
|
+
{
|
|
14086
|
+
this.size = source.floor();
|
|
14087
|
+
this.tileLayer = undefined;
|
|
14088
|
+
}
|
|
14089
|
+
else
|
|
14090
|
+
{
|
|
14091
|
+
ASSERT(source && isVector2(source.size) && typeof source.getCollisionData === 'function',
|
|
14092
|
+
'PathFinder requires a Vector2 size or a TileCollisionLayer');
|
|
14093
|
+
this.size = source.size;
|
|
14094
|
+
this.tileLayer = source;
|
|
14095
|
+
}
|
|
14096
|
+
|
|
14097
|
+
// Tunables (public, freely re-assignable).
|
|
14098
|
+
this.heuristicWeight = 1;
|
|
14099
|
+
this.maxLoop = 500;
|
|
14100
|
+
this.smoothPath = true;
|
|
14101
|
+
this.debug = false;
|
|
14102
|
+
this.debugTime = 2;
|
|
14103
|
+
|
|
14104
|
+
// Pre-allocate the node array — one node per tile, reused across calls.
|
|
14105
|
+
this.nodes = new Array(this.size.x * this.size.y);
|
|
14106
|
+
for (let y = 0; y < this.size.y; ++y)
|
|
14107
|
+
for (let x = 0; x < this.size.x; ++x)
|
|
14108
|
+
this.nodes[x + y * this.size.x] = new PathFinderNode(x, y);
|
|
14109
|
+
|
|
14110
|
+
// Scratch Vector2 reused to avoid allocations in the isWalkable hot path.
|
|
14111
|
+
this.collisionScratch = vec2();
|
|
14112
|
+
}
|
|
14113
|
+
|
|
14114
|
+
/** Default walkability: if a tile layer was provided, returns true when the
|
|
14115
|
+
* cell has no solid collision data; otherwise returns true. Override on
|
|
14116
|
+
* the instance or via a subclass.
|
|
14117
|
+
* @param {number} x - Tile x
|
|
14118
|
+
* @param {number} y - Tile y
|
|
14119
|
+
* @returns {boolean} */
|
|
14120
|
+
isWalkable(x, y)
|
|
14121
|
+
{
|
|
14122
|
+
if (!this.tileLayer) return true;
|
|
14123
|
+
return !this.tileLayer.getCollisionData(this.collisionScratch.set(x, y));
|
|
14124
|
+
}
|
|
14125
|
+
|
|
14126
|
+
/** Default extra cost for stepping on a cell. Returns 0 (free) by default.
|
|
14127
|
+
* Override to add cost-weighted terrain (mud, swamp, etc).
|
|
14128
|
+
* @param {number} x - Tile x
|
|
14129
|
+
* @param {number} y - Tile y
|
|
14130
|
+
* @returns {number} */
|
|
14131
|
+
getCost(x, y)
|
|
14132
|
+
{
|
|
14133
|
+
return 0;
|
|
14134
|
+
}
|
|
14135
|
+
|
|
14136
|
+
/** Get the node at tile coords, or null if out of bounds.
|
|
14137
|
+
* @param {number} x
|
|
14138
|
+
* @param {number} y
|
|
14139
|
+
* @returns {PathFinderNode|null} */
|
|
14140
|
+
getNode(x, y)
|
|
14141
|
+
{
|
|
14142
|
+
if (x < 0 || y < 0 || x >= this.size.x || y >= this.size.y) return null;
|
|
14143
|
+
return this.nodes[x + y * this.size.x];
|
|
14144
|
+
}
|
|
14145
|
+
|
|
14146
|
+
/** Convert a world-space position to integer tile coords (no clamping).
|
|
14147
|
+
* @param {Vector2} worldPos
|
|
14148
|
+
* @returns {Vector2}
|
|
14149
|
+
* @memberof PathFinding */
|
|
14150
|
+
worldToTile(worldPos)
|
|
14151
|
+
{
|
|
14152
|
+
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
14153
|
+
const oy = this.tileLayer ? this.tileLayer.pos.y : 0;
|
|
14154
|
+
return vec2(floor(worldPos.x - ox), floor(worldPos.y - oy));
|
|
14155
|
+
}
|
|
14156
|
+
|
|
14157
|
+
/** Convert integer tile coords to the world-space center of that tile.
|
|
14158
|
+
* @param {number} x
|
|
14159
|
+
* @param {number} y
|
|
14160
|
+
* @returns {Vector2}
|
|
14161
|
+
* @memberof PathFinding */
|
|
14162
|
+
tileToWorld(x, y)
|
|
14163
|
+
{
|
|
14164
|
+
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
14165
|
+
const oy = this.tileLayer ? this.tileLayer.pos.y : 0;
|
|
14166
|
+
return vec2(x + 0.5 + ox, y + 0.5 + oy);
|
|
14167
|
+
}
|
|
14168
|
+
|
|
14169
|
+
/** Reset all nodes and re-populate walkable / cost / posWorld from the
|
|
14170
|
+
* current isWalkable / getCost overrides. Called at the start of
|
|
14171
|
+
* findPath; exposed so tests and tooling can drive it directly.
|
|
14172
|
+
* @private */
|
|
14173
|
+
buildNodeData()
|
|
14174
|
+
{
|
|
14175
|
+
const w = this.size.x;
|
|
14176
|
+
const h = this.size.y;
|
|
14177
|
+
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
14178
|
+
const oy = this.tileLayer ? this.tileLayer.pos.y : 0;
|
|
14179
|
+
for (let y = 0; y < h; ++y)
|
|
14180
|
+
for (let x = 0; x < w; ++x)
|
|
14181
|
+
{
|
|
14182
|
+
const node = this.nodes[x + y * w];
|
|
14183
|
+
node.reset();
|
|
14184
|
+
const walkable = !!this.isWalkable(x, y);
|
|
14185
|
+
const cost = walkable ? max(0, this.getCost(x, y)) : 0;
|
|
14186
|
+
node.walkable = walkable;
|
|
14187
|
+
node.cost = cost;
|
|
14188
|
+
node.posWorld.set(x + 0.5 + ox, y + 0.5 + oy);
|
|
14189
|
+
|
|
14190
|
+
if (this.debug && this.debugTime > 0)
|
|
14191
|
+
{
|
|
14192
|
+
if (!walkable)
|
|
14193
|
+
debugRect(node.posWorld, PATHFINDER_TILE_VEC, rgb(1, 0, 0, 0.25), this.debugTime);
|
|
14194
|
+
else if (cost > 0)
|
|
14195
|
+
debugRect(node.posWorld, PATHFINDER_TILE_VEC, rgb(1, 0, 0, min(0.2, cost * 0.05)), this.debugTime);
|
|
14196
|
+
}
|
|
14197
|
+
}
|
|
14198
|
+
}
|
|
14199
|
+
|
|
14200
|
+
/** Core A* search loop. Expects buildNodeData() to have been called first.
|
|
14201
|
+
* Marks node.parent for path reconstruction. Returns true if endNode was
|
|
14202
|
+
* reached; false on disconnected goal or maxLoop exhaustion.
|
|
14203
|
+
* @param {PathFinderNode} startNode
|
|
14204
|
+
* @param {PathFinderNode} endNode
|
|
14205
|
+
* @returns {boolean}
|
|
14206
|
+
* @private */
|
|
14207
|
+
aStarSearch(startNode, endNode)
|
|
14208
|
+
{
|
|
14209
|
+
ASSERT(startNode && endNode, 'aStarSearch needs both endpoints');
|
|
14210
|
+
ASSERT(startNode !== endNode, 'aStarSearch: start and end must differ — caller should handle trivial case');
|
|
14211
|
+
ASSERT(startNode.walkable && endNode.walkable, 'aStarSearch: endpoints must be walkable');
|
|
14212
|
+
|
|
14213
|
+
const openList = [startNode];
|
|
14214
|
+
startNode.isOpen = true;
|
|
14215
|
+
let loopCount = 0;
|
|
14216
|
+
|
|
14217
|
+
while (openList.length > 0)
|
|
14218
|
+
{
|
|
14219
|
+
// Find the open node with the smallest f score (linear scan).
|
|
14220
|
+
// Same as the C++ — fine up to a few thousand nodes.
|
|
14221
|
+
let bestIndex = 0;
|
|
14222
|
+
let bestF = openList[0].f;
|
|
14223
|
+
for (let i = 1; i < openList.length; ++i)
|
|
14224
|
+
{
|
|
14225
|
+
if (openList[i].f < bestF)
|
|
14226
|
+
{
|
|
14227
|
+
bestF = openList[i].f;
|
|
14228
|
+
bestIndex = i;
|
|
14229
|
+
}
|
|
14230
|
+
}
|
|
14231
|
+
const current = openList[bestIndex];
|
|
14232
|
+
|
|
14233
|
+
if (current === endNode) break;
|
|
14234
|
+
if (++loopCount > this.maxLoop) break;
|
|
14235
|
+
|
|
14236
|
+
// Move current from open to closed.
|
|
14237
|
+
current.isOpen = false;
|
|
14238
|
+
openList.splice(bestIndex, 1);
|
|
14239
|
+
current.isClosed = true;
|
|
14240
|
+
|
|
14241
|
+
if (this.debug && this.debugTime > 0)
|
|
14242
|
+
debugRect(current.posWorld, PATHFINDER_TILE_VEC, rgb(1, 1, 1, 0.05), this.debugTime);
|
|
14243
|
+
|
|
14244
|
+
// Expand all 8 neighbors.
|
|
14245
|
+
for (let dy = -1; dy <= 1; ++dy)
|
|
14246
|
+
for (let dx = -1; dx <= 1; ++dx)
|
|
14247
|
+
{
|
|
14248
|
+
if (dx === 0 && dy === 0) continue;
|
|
14249
|
+
const neighbor = this.getNode(current.pos.x + dx, current.pos.y + dy);
|
|
14250
|
+
if (!neighbor || !neighbor.walkable || neighbor.isClosed) continue;
|
|
14251
|
+
|
|
14252
|
+
let stepCost = 1;
|
|
14253
|
+
if (dx !== 0 && dy !== 0)
|
|
14254
|
+
{
|
|
14255
|
+
// Diagonal step: refuse if either cardinal neighbor is
|
|
14256
|
+
// blocked or has cost. Prevents cutting through corners.
|
|
14257
|
+
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
14258
|
+
if (!card1 || card1.cost > 0 || !card1.walkable) continue;
|
|
14259
|
+
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
14260
|
+
if (!card2 || card2.cost > 0 || !card2.walkable) continue;
|
|
14261
|
+
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
14262
|
+
}
|
|
14263
|
+
|
|
14264
|
+
const tentativeG = current.g + stepCost + neighbor.cost;
|
|
14265
|
+
if (!neighbor.isOpen)
|
|
14266
|
+
{
|
|
14267
|
+
neighbor.isOpen = true;
|
|
14268
|
+
openList.push(neighbor);
|
|
14269
|
+
}
|
|
14270
|
+
else if (tentativeG >= neighbor.g)
|
|
14271
|
+
{
|
|
14272
|
+
continue;
|
|
14273
|
+
}
|
|
14274
|
+
|
|
14275
|
+
// Best path so far through neighbor — record it.
|
|
14276
|
+
neighbor.parent = current;
|
|
14277
|
+
neighbor.g = tentativeG;
|
|
14278
|
+
const gdx = endNode.pos.x - neighbor.pos.x;
|
|
14279
|
+
const gdy = endNode.pos.y - neighbor.pos.y;
|
|
14280
|
+
neighbor.f = neighbor.g + (gdx * gdx + gdy * gdy) * this.heuristicWeight;
|
|
14281
|
+
}
|
|
14282
|
+
}
|
|
14283
|
+
|
|
14284
|
+
return endNode.parent !== null;
|
|
14285
|
+
}
|
|
14286
|
+
|
|
14287
|
+
/** Find the clear (walkable, zero-cost) node closest to the given world
|
|
14288
|
+
* position. Spirals outward in expanding boxes until a clear node is
|
|
14289
|
+
* found or the search range is exhausted. Useful for snapping a click
|
|
14290
|
+
* or NPC spawn position to the nearest open tile.
|
|
14291
|
+
*
|
|
14292
|
+
* By default, calls `buildNodeData()` first so it works correctly on a
|
|
14293
|
+
* fresh PathFinder. If you're calling it many times in a row with
|
|
14294
|
+
* unchanged walkability, pass `rebuild=false` and call `buildNodeData()`
|
|
14295
|
+
* once externally to avoid redundant work.
|
|
14296
|
+
* @param {Vector2} worldPos
|
|
14297
|
+
* @param {number} [searchRange=10] - Max box-radius in tiles
|
|
14298
|
+
* @param {boolean} [rebuild=true] - Whether to call buildNodeData first
|
|
14299
|
+
* @returns {PathFinderNode|null}
|
|
14300
|
+
* @memberof PathFinding */
|
|
14301
|
+
getNearestClearNode(worldPos, searchRange = 10, rebuild = true)
|
|
14302
|
+
{
|
|
14303
|
+
ASSERT(isVector2(worldPos), 'worldPos must be a Vector2');
|
|
14304
|
+
if (rebuild) this.buildNodeData();
|
|
14305
|
+
|
|
14306
|
+
// Inline worldToTile to avoid a Vector2 allocation per call.
|
|
14307
|
+
const ox = this.tileLayer ? this.tileLayer.pos.x : 0;
|
|
14308
|
+
const oy = this.tileLayer ? this.tileLayer.pos.y : 0;
|
|
14309
|
+
const centerX = floor(worldPos.x - ox);
|
|
14310
|
+
const centerY = floor(worldPos.y - oy);
|
|
14311
|
+
|
|
14312
|
+
for (let offset = 0; offset <= searchRange; ++offset)
|
|
14313
|
+
{
|
|
14314
|
+
let nearest = null;
|
|
14315
|
+
let nearestDistSq = 0;
|
|
14316
|
+
|
|
14317
|
+
for (let dy = -offset; dy <= offset; ++dy)
|
|
14318
|
+
for (let dx = -offset; dx <= offset; ++dx)
|
|
14319
|
+
{
|
|
14320
|
+
// Only scan the perimeter of the current ring (skip the
|
|
14321
|
+
// interior we've already searched in earlier iterations).
|
|
14322
|
+
if (offset > 0 && abs(dx) !== offset && abs(dy) !== offset)
|
|
14323
|
+
continue;
|
|
14324
|
+
|
|
14325
|
+
const node = this.getNode(centerX + dx, centerY + dy);
|
|
14326
|
+
if (!node || !node.isClear()) continue;
|
|
14327
|
+
|
|
14328
|
+
const ddx = node.posWorld.x - worldPos.x;
|
|
14329
|
+
const ddy = node.posWorld.y - worldPos.y;
|
|
14330
|
+
const distSq = ddx * ddx + ddy * ddy;
|
|
14331
|
+
if (!nearest || distSq < nearestDistSq)
|
|
14332
|
+
{
|
|
14333
|
+
nearest = node;
|
|
14334
|
+
nearestDistSq = distSq;
|
|
14335
|
+
}
|
|
14336
|
+
}
|
|
14337
|
+
if (nearest) return nearest;
|
|
14338
|
+
}
|
|
14339
|
+
return null;
|
|
14340
|
+
}
|
|
14341
|
+
|
|
14342
|
+
/** Smooth a node path by removing redundant turns and tightening corners
|
|
14343
|
+
* where a grid-aligned diagonal is clear. Modifies the path in place.
|
|
14344
|
+
* Stays on the grid — does not introduce off-tile-center points.
|
|
14345
|
+
* Port of ShortenPath() in pathFinding.cpp.
|
|
14346
|
+
* @param {PathFinderNode[]} path
|
|
14347
|
+
* @private */
|
|
14348
|
+
smoothPathCorners(path)
|
|
14349
|
+
{
|
|
14350
|
+
if (path.length <= 2) return;
|
|
14351
|
+
|
|
14352
|
+
let i = 1;
|
|
14353
|
+
while (i < path.length - 1)
|
|
14354
|
+
{
|
|
14355
|
+
const prev = path[i - 1];
|
|
14356
|
+
const node = path[i];
|
|
14357
|
+
const next = path[i + 1];
|
|
14358
|
+
|
|
14359
|
+
const dx = next.pos.x - prev.pos.x;
|
|
14360
|
+
const dy = next.pos.y - prev.pos.y;
|
|
14361
|
+
const lenSq = dx * dx + dy * dy;
|
|
14362
|
+
|
|
14363
|
+
// dx,dy is the prev-to-current step direction; needed for the
|
|
14364
|
+
// 135° "mostly vertical/horizontal" disambiguation.
|
|
14365
|
+
const stepDx = node.pos.x - prev.pos.x;
|
|
14366
|
+
const stepDy = node.pos.y - prev.pos.y;
|
|
14367
|
+
const stepDxNext = next.pos.x - node.pos.x;
|
|
14368
|
+
const stepDyNext = next.pos.y - node.pos.y;
|
|
14369
|
+
|
|
14370
|
+
if (lenSq === 1)
|
|
14371
|
+
{
|
|
14372
|
+
// 45° angle — middle node is off the straight line. Drop it.
|
|
14373
|
+
if (this.debug && this.debugTime > 0)
|
|
14374
|
+
debugCircle(node.posWorld, 0.3, rgb(0.5, 0, 0.5, 0.5), this.debugTime);
|
|
14375
|
+
path.splice(i, 1);
|
|
14376
|
+
i = max(1, i - 1);
|
|
14377
|
+
continue;
|
|
14378
|
+
}
|
|
14379
|
+
else if (lenSq === 2)
|
|
14380
|
+
{
|
|
14381
|
+
// 90° corner. Check the alternative-diagonal cell.
|
|
14382
|
+
if (this.debug && this.debugTime > 0)
|
|
14383
|
+
debugCircle(node.posWorld, 0.3, rgb(1, 0, 0, 0.5), this.debugTime);
|
|
14384
|
+
|
|
14385
|
+
let sx, sy;
|
|
14386
|
+
if (prev.pos.y === node.pos.y && next.pos.x === node.pos.x)
|
|
14387
|
+
{ sx = prev.pos.x; sy = next.pos.y; }
|
|
14388
|
+
else
|
|
14389
|
+
{ sx = next.pos.x; sy = prev.pos.y; }
|
|
14390
|
+
|
|
14391
|
+
const shortcut = this.getNode(sx, sy);
|
|
14392
|
+
if (shortcut && shortcut.isClear())
|
|
14393
|
+
{
|
|
14394
|
+
path.splice(i, 1);
|
|
14395
|
+
i = max(1, i - 1);
|
|
14396
|
+
continue;
|
|
14397
|
+
}
|
|
14398
|
+
}
|
|
14399
|
+
else if (lenSq === 5)
|
|
14400
|
+
{
|
|
14401
|
+
// 135° angle (a knight's-move offset). Try to relocate the
|
|
14402
|
+
// middle node to whichever of two candidate cells is closer
|
|
14403
|
+
// to prev-of-prev, and only if the corner cut is also clear.
|
|
14404
|
+
if (this.debug && this.debugTime > 0)
|
|
14405
|
+
debugCircle(node.posWorld, 0.3, rgb(1, 1, 0, 0.5), this.debugTime);
|
|
14406
|
+
|
|
14407
|
+
const prevPrev = i >= 2 ? path[i - 2] : prev;
|
|
14408
|
+
let s1x, s1y, s2x, s2y;
|
|
14409
|
+
if (stepDx === 0 || stepDxNext === 0)
|
|
14410
|
+
{
|
|
14411
|
+
// mostly vertical
|
|
14412
|
+
s1x = next.pos.x; s1y = node.pos.y;
|
|
14413
|
+
s2x = prev.pos.x; s2y = node.pos.y;
|
|
14414
|
+
}
|
|
14415
|
+
else
|
|
14416
|
+
{
|
|
14417
|
+
// mostly horizontal
|
|
14418
|
+
s1x = node.pos.x; s1y = next.pos.y;
|
|
14419
|
+
s2x = node.pos.x; s2y = prev.pos.y;
|
|
14420
|
+
}
|
|
14421
|
+
const dd1x = s1x - prevPrev.pos.x;
|
|
14422
|
+
const dd1y = s1y - prevPrev.pos.y;
|
|
14423
|
+
const dd2x = s2x - prevPrev.pos.x;
|
|
14424
|
+
const dd2y = s2y - prevPrev.pos.y;
|
|
14425
|
+
const dist1Sq = dd1x * dd1x + dd1y * dd1y;
|
|
14426
|
+
const dist2Sq = dd2x * dd2x + dd2y * dd2y;
|
|
14427
|
+
const sx = dist1Sq < dist2Sq ? s1x : s1x === s2x && s1y === s2y ? s1x : s2x;
|
|
14428
|
+
const sy = dist1Sq < dist2Sq ? s1y : s1x === s2x && s1y === s2y ? s1y : s2y;
|
|
14429
|
+
|
|
14430
|
+
const shortcut = this.getNode(sx, sy);
|
|
14431
|
+
if (shortcut && shortcut !== node && shortcut.isClear())
|
|
14432
|
+
{
|
|
14433
|
+
// Also check the cut-corner cell is clear.
|
|
14434
|
+
const ccx = next.pos.x + s2x - s1x;
|
|
14435
|
+
const ccy = next.pos.y + s2y - s1y;
|
|
14436
|
+
const cutCorner = this.getNode(ccx, ccy);
|
|
14437
|
+
if (cutCorner && cutCorner.isClear())
|
|
14438
|
+
{
|
|
14439
|
+
path[i] = shortcut;
|
|
14440
|
+
i = max(1, i - 1);
|
|
14441
|
+
continue;
|
|
14442
|
+
}
|
|
14443
|
+
}
|
|
14444
|
+
}
|
|
14445
|
+
else if (lenSq === 4 || lenSq === 8)
|
|
14446
|
+
{
|
|
14447
|
+
// Straight line or a 1-cell bump.
|
|
14448
|
+
if (this.debug && this.debugTime > 0)
|
|
14449
|
+
debugCircle(node.posWorld, 0.3, rgb(0, 1, 0, 0.5), this.debugTime);
|
|
14450
|
+
|
|
14451
|
+
if (stepDx === stepDxNext && stepDy === stepDyNext)
|
|
14452
|
+
{
|
|
14453
|
+
// Truly straight — nothing to do, advance.
|
|
14454
|
+
++i;
|
|
14455
|
+
continue;
|
|
14456
|
+
}
|
|
14457
|
+
else
|
|
14458
|
+
{
|
|
14459
|
+
// Bump — try to flatten via the in-line cell.
|
|
14460
|
+
let sx, sy;
|
|
14461
|
+
if (prev.pos.y === next.pos.y)
|
|
14462
|
+
{ sx = node.pos.x; sy = prev.pos.y; }
|
|
14463
|
+
else
|
|
14464
|
+
{ sx = prev.pos.x; sy = node.pos.y; }
|
|
14465
|
+
const shortcut = this.getNode(sx, sy);
|
|
14466
|
+
if (shortcut && shortcut.isClear())
|
|
14467
|
+
{
|
|
14468
|
+
path[i] = shortcut;
|
|
14469
|
+
i = max(1, i - 1);
|
|
14470
|
+
continue;
|
|
14471
|
+
}
|
|
14472
|
+
}
|
|
14473
|
+
}
|
|
14474
|
+
|
|
14475
|
+
++i;
|
|
14476
|
+
}
|
|
14477
|
+
}
|
|
14478
|
+
|
|
14479
|
+
/** Smooth a node path via line-of-sight ("string pulling"). Walks the
|
|
14480
|
+
* input path collapsing runs of nodes into straight segments whenever
|
|
14481
|
+
* isLineClear permits, so the result can leave grid centers and cut
|
|
14482
|
+
* cleanly across open spaces.
|
|
14483
|
+
*
|
|
14484
|
+
* Bails (leaves the path unchanged) if any node has nonzero cost — a
|
|
14485
|
+
* straight geometric shortcut can't be trusted to be the lowest-cost
|
|
14486
|
+
* route when cost-weighted terrain is in play.
|
|
14487
|
+
*
|
|
14488
|
+
* Port of ShortenPath2() in pathFinding.cpp.
|
|
14489
|
+
* @param {PathFinderNode[]} path
|
|
14490
|
+
* @private */
|
|
14491
|
+
smoothPathStringPull(path)
|
|
14492
|
+
{
|
|
14493
|
+
if (path.length <= 2) return;
|
|
14494
|
+
for (const n of path)
|
|
14495
|
+
{
|
|
14496
|
+
if (!n.isClear()) return;
|
|
14497
|
+
}
|
|
14498
|
+
|
|
14499
|
+
const original = path.slice();
|
|
14500
|
+
path.length = 0;
|
|
14501
|
+
path.push(original[0]);
|
|
14502
|
+
let searchIndex = 0;
|
|
14503
|
+
|
|
14504
|
+
for (let i = 1; i < original.length; ++i)
|
|
14505
|
+
{
|
|
14506
|
+
const node = original[i];
|
|
14507
|
+
|
|
14508
|
+
// Skip if node is collinear with the search-window start and the
|
|
14509
|
+
// previous node — it adds no information. Note: a == b is the
|
|
14510
|
+
// degenerate i=1, searchIndex=0 case; skip the test then.
|
|
14511
|
+
{
|
|
14512
|
+
const a = original[searchIndex];
|
|
14513
|
+
const b = original[i - 1];
|
|
14514
|
+
if (a !== b)
|
|
14515
|
+
{
|
|
14516
|
+
const cross =
|
|
14517
|
+
(b.pos.x - a.pos.x) * (node.pos.y - a.pos.y) -
|
|
14518
|
+
(b.pos.y - a.pos.y) * (node.pos.x - a.pos.x);
|
|
14519
|
+
if (cross === 0) continue;
|
|
14520
|
+
}
|
|
14521
|
+
}
|
|
14522
|
+
|
|
14523
|
+
if (!this.isLineClear(node.pos, path[path.length - 1].pos))
|
|
14524
|
+
{
|
|
14525
|
+
// Look ahead — if any later node has a clear shot to the
|
|
14526
|
+
// back of our new path, skip this node and try later.
|
|
14527
|
+
let foundClearAfter = false;
|
|
14528
|
+
for (let j = i + 1; j < original.length; ++j)
|
|
14529
|
+
{
|
|
14530
|
+
if (this.isLineClear(original[j].pos, path[path.length - 1].pos))
|
|
14531
|
+
{
|
|
14532
|
+
foundClearAfter = true;
|
|
14533
|
+
break;
|
|
14534
|
+
}
|
|
14535
|
+
}
|
|
14536
|
+
if (foundClearAfter)
|
|
14537
|
+
{
|
|
14538
|
+
if (this.debug && this.debugTime > 0)
|
|
14539
|
+
debugLine(node.posWorld, path[path.length - 1].posWorld, rgb(0, 0, 1, 0.3), 0.02, this.debugTime);
|
|
14540
|
+
continue;
|
|
14541
|
+
}
|
|
14542
|
+
|
|
14543
|
+
// No clear line ahead — fall back to the last waypoint we did
|
|
14544
|
+
// have a clear line to. searchIndex tracks our scan position.
|
|
14545
|
+
for (; searchIndex < original.length; ++searchIndex)
|
|
14546
|
+
{
|
|
14547
|
+
const cand = original[searchIndex];
|
|
14548
|
+
if (this.isLineClear(node.pos, cand.pos))
|
|
14549
|
+
{
|
|
14550
|
+
path.push(cand);
|
|
14551
|
+
i = searchIndex;
|
|
14552
|
+
break;
|
|
14553
|
+
}
|
|
14554
|
+
}
|
|
14555
|
+
ASSERT(searchIndex < original.length, 'smoothPathStringPull: ran out of candidates');
|
|
14556
|
+
}
|
|
14557
|
+
}
|
|
14558
|
+
|
|
14559
|
+
path.push(original[original.length - 1]);
|
|
14560
|
+
}
|
|
14561
|
+
|
|
14562
|
+
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
14563
|
+
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
14564
|
+
* @param {number} x
|
|
14565
|
+
* @param {number} y
|
|
14566
|
+
* @returns {boolean}
|
|
14567
|
+
* @private */
|
|
14568
|
+
isNodeClear(x, y)
|
|
14569
|
+
{
|
|
14570
|
+
const n = this.getNode(x, y);
|
|
14571
|
+
return n !== null && n.isClear();
|
|
14572
|
+
}
|
|
14573
|
+
|
|
14574
|
+
/** Check that the line between two tile-coord endpoints stays entirely
|
|
14575
|
+
* inside walkable, zero-cost cells. Stricter than just sampling along
|
|
14576
|
+
* the line — it also checks the diagonal-corner-adjacent cells so the
|
|
14577
|
+
* line can never "scrape past" a wall corner.
|
|
14578
|
+
*
|
|
14579
|
+
* Both endpoints must themselves be clear (asserted in debug). Port of
|
|
14580
|
+
* CheckLine() in pathFinding.cpp.
|
|
14581
|
+
* @param {Vector2} startPos - Tile coords
|
|
14582
|
+
* @param {Vector2} endPos - Tile coords
|
|
14583
|
+
* @returns {boolean}
|
|
14584
|
+
* @private */
|
|
14585
|
+
isLineClear(startPos, endPos)
|
|
14586
|
+
{
|
|
14587
|
+
ASSERT(isVector2(startPos) && isVector2(endPos), 'isLineClear needs Vector2 endpoints');
|
|
14588
|
+
ASSERT(this.isNodeClear(startPos.x, startPos.y) && this.isNodeClear(endPos.x, endPos.y),
|
|
14589
|
+
'isLineClear endpoints must be in-bounds and clear');
|
|
14590
|
+
|
|
14591
|
+
const dx = endPos.x - startPos.x;
|
|
14592
|
+
const dy = endPos.y - startPos.y;
|
|
14593
|
+
const adx = abs(dx);
|
|
14594
|
+
const ady = abs(dy);
|
|
14595
|
+
const sx = sign(dx);
|
|
14596
|
+
const sy = sign(dy);
|
|
14597
|
+
let x = startPos.x;
|
|
14598
|
+
let y = startPos.y;
|
|
14599
|
+
|
|
14600
|
+
if (ady === adx)
|
|
14601
|
+
{
|
|
14602
|
+
// Pure diagonal.
|
|
14603
|
+
while (x !== endPos.x)
|
|
14604
|
+
{
|
|
14605
|
+
if (x !== startPos.x)
|
|
14606
|
+
{
|
|
14607
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14608
|
+
if (!this.isNodeClear(x, y - sy)) return false;
|
|
14609
|
+
}
|
|
14610
|
+
if (!this.isNodeClear(x, y + sy)) return false;
|
|
14611
|
+
x += sx;
|
|
14612
|
+
y += sy;
|
|
14613
|
+
}
|
|
14614
|
+
if (!this.isNodeClear(endPos.x, endPos.y - sy)) return false;
|
|
14615
|
+
}
|
|
14616
|
+
else if (ady < adx)
|
|
14617
|
+
{
|
|
14618
|
+
// Mostly horizontal.
|
|
14619
|
+
if (dy === 0)
|
|
14620
|
+
{
|
|
14621
|
+
// Purely horizontal.
|
|
14622
|
+
x += sx;
|
|
14623
|
+
while (x !== endPos.x)
|
|
14624
|
+
{
|
|
14625
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14626
|
+
x += sx;
|
|
14627
|
+
}
|
|
14628
|
+
}
|
|
14629
|
+
else
|
|
14630
|
+
{
|
|
14631
|
+
let lastY = startPos.y;
|
|
14632
|
+
while (x !== endPos.x)
|
|
14633
|
+
{
|
|
14634
|
+
y = startPos.y + Math.trunc((dy * (x - startPos.x)) / dx);
|
|
14635
|
+
if (lastY !== y)
|
|
14636
|
+
{
|
|
14637
|
+
if (!this.isNodeClear(x - sx, y + sy)) return false;
|
|
14638
|
+
if (!this.isNodeClear(x, y - sy)) return false;
|
|
14639
|
+
}
|
|
14640
|
+
lastY = y;
|
|
14641
|
+
if (x !== startPos.x)
|
|
14642
|
+
{
|
|
14643
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14644
|
+
}
|
|
14645
|
+
y += sy;
|
|
14646
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14647
|
+
x += sx;
|
|
14648
|
+
}
|
|
14649
|
+
const finalY = endPos.y - sy;
|
|
14650
|
+
if (!this.isNodeClear(endPos.x, finalY)) return false;
|
|
14651
|
+
}
|
|
14652
|
+
}
|
|
14653
|
+
else
|
|
14654
|
+
{
|
|
14655
|
+
// Mostly vertical.
|
|
14656
|
+
if (dx === 0)
|
|
14657
|
+
{
|
|
14658
|
+
y += sy;
|
|
14659
|
+
while (y !== endPos.y)
|
|
14660
|
+
{
|
|
14661
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14662
|
+
y += sy;
|
|
14663
|
+
}
|
|
14664
|
+
}
|
|
14665
|
+
else
|
|
14666
|
+
{
|
|
14667
|
+
let lastX = startPos.x;
|
|
14668
|
+
while (y !== endPos.y)
|
|
14669
|
+
{
|
|
14670
|
+
x = startPos.x + Math.trunc((dx * (y - startPos.y)) / dy);
|
|
14671
|
+
if (lastX !== x)
|
|
14672
|
+
{
|
|
14673
|
+
if (!this.isNodeClear(x + sx, y - sy)) return false;
|
|
14674
|
+
if (!this.isNodeClear(x - sx, y)) return false;
|
|
14675
|
+
}
|
|
14676
|
+
lastX = x;
|
|
14677
|
+
if (y !== startPos.y)
|
|
14678
|
+
{
|
|
14679
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14680
|
+
}
|
|
14681
|
+
x += sx;
|
|
14682
|
+
if (!this.isNodeClear(x, y)) return false;
|
|
14683
|
+
y += sy;
|
|
14684
|
+
}
|
|
14685
|
+
const finalX = endPos.x - sx;
|
|
14686
|
+
if (!this.isNodeClear(finalX, endPos.y)) return false;
|
|
14687
|
+
}
|
|
14688
|
+
}
|
|
14689
|
+
return true;
|
|
14690
|
+
}
|
|
14691
|
+
|
|
14692
|
+
/** Find a path from startPos to endPos in world space. Returns an array
|
|
14693
|
+
* of world-space Vector2 points; empty array if no path exists.
|
|
14694
|
+
*
|
|
14695
|
+
* Start and end are snapped to the nearest walkable tile via
|
|
14696
|
+
* getNearestClearNode. Intermediate points are tile centers unless the
|
|
14697
|
+
* string-pulling smoothing pass moves them off-grid.
|
|
14698
|
+
* @param {Vector2} startPos - World-space start
|
|
14699
|
+
* @param {Vector2} endPos - World-space end
|
|
14700
|
+
* @returns {Vector2[]}
|
|
14701
|
+
* @memberof PathFinding */
|
|
14702
|
+
findPath(startPos, endPos)
|
|
14703
|
+
{
|
|
14704
|
+
ASSERT(isVector2(startPos) && isVector2(endPos), 'findPath needs Vector2 endpoints');
|
|
14705
|
+
|
|
14706
|
+
this.buildNodeData();
|
|
14707
|
+
|
|
14708
|
+
// rebuild=false because we just built — avoid redundant work per snap.
|
|
14709
|
+
const startNode = this.getNearestClearNode(startPos, 10, false);
|
|
14710
|
+
const endNode = this.getNearestClearNode(endPos, 10, false);
|
|
14711
|
+
if (!startNode || !endNode) return [];
|
|
14712
|
+
|
|
14713
|
+
// Trivial case: start and end snapped to the same tile.
|
|
14714
|
+
if (startNode === endNode) return [startNode.posWorld.copy()];
|
|
14715
|
+
|
|
14716
|
+
if (!this.aStarSearch(startNode, endNode)) return [];
|
|
14717
|
+
|
|
14718
|
+
// Walk back from endNode via parent pointers, then reverse — cheaper
|
|
14719
|
+
// than unshifting on every step.
|
|
14720
|
+
const nodePath = [];
|
|
14721
|
+
for (let n = endNode; n; n = n.parent)
|
|
14722
|
+
nodePath.push(n);
|
|
14723
|
+
nodePath.reverse();
|
|
14724
|
+
|
|
14725
|
+
if (this.smoothPath)
|
|
14726
|
+
{
|
|
14727
|
+
this.smoothPathCorners(nodePath);
|
|
14728
|
+
this.smoothPathStringPull(nodePath);
|
|
14729
|
+
}
|
|
14730
|
+
|
|
14731
|
+
// Convert to world-space Vector2 path. Return copies, not live node
|
|
14732
|
+
// references — callers shouldn't be able to mutate the grid.
|
|
14733
|
+
const result = nodePath.map(n => n.posWorld.copy());
|
|
14734
|
+
|
|
14735
|
+
if (this.debug && this.debugTime > 0 && result.length > 0)
|
|
14736
|
+
{
|
|
14737
|
+
for (let i = 1; i < result.length; ++i)
|
|
14738
|
+
debugLine(result[i - 1], result[i], RED, 0.1, this.debugTime);
|
|
14739
|
+
for (const p of result)
|
|
14740
|
+
debugCircle(p, 0.5, rgb(1, 0, 0, 0.3), this.debugTime);
|
|
14741
|
+
debugCircle(result[0], 0.5, rgb(0, 1, 0, 0.5), this.debugTime);
|
|
14742
|
+
debugCircle(result[result.length - 1], 0.5, rgb(0, 1, 0, 0.5), this.debugTime);
|
|
14743
|
+
}
|
|
14744
|
+
|
|
14745
|
+
return result;
|
|
14746
|
+
}
|
|
14747
|
+
}
|
|
14748
|
+
|
|
13064
14749
|
|
|
13065
14750
|
/**
|
|
13066
14751
|
* LittleJS Module Export
|
|
@@ -13075,6 +14760,7 @@ export
|
|
|
13075
14760
|
frameRate,
|
|
13076
14761
|
timeDelta,
|
|
13077
14762
|
engineObjects,
|
|
14763
|
+
engineObjectsCollide,
|
|
13078
14764
|
frame,
|
|
13079
14765
|
time,
|
|
13080
14766
|
timeReal,
|
|
@@ -13097,6 +14783,7 @@ export
|
|
|
13097
14783
|
// Debug
|
|
13098
14784
|
ASSERT,
|
|
13099
14785
|
LOG,
|
|
14786
|
+
debugPointSize,
|
|
13100
14787
|
debugRect,
|
|
13101
14788
|
debugPoly,
|
|
13102
14789
|
debugCircle,
|
|
@@ -13115,6 +14802,7 @@ export
|
|
|
13115
14802
|
cameraPos,
|
|
13116
14803
|
cameraAngle,
|
|
13117
14804
|
cameraScale,
|
|
14805
|
+
timeScale,
|
|
13118
14806
|
canvasColorTiles,
|
|
13119
14807
|
canvasClearColor,
|
|
13120
14808
|
canvasMaxSize,
|
|
@@ -13140,11 +14828,14 @@ export
|
|
|
13140
14828
|
gravity,
|
|
13141
14829
|
particleEmitRateScale,
|
|
13142
14830
|
glEnable,
|
|
14831
|
+
glCircleSides,
|
|
13143
14832
|
gamepadsEnable,
|
|
13144
14833
|
gamepadDirectionEmulateStick,
|
|
13145
14834
|
inputWASDEmulateDirection,
|
|
14835
|
+
touchInputEnable,
|
|
13146
14836
|
touchGamepadEnable,
|
|
13147
14837
|
touchGamepadCenterButtonSize,
|
|
14838
|
+
touchGamepadButtonCount,
|
|
13148
14839
|
touchGamepadAnalog,
|
|
13149
14840
|
touchGamepadSize,
|
|
13150
14841
|
touchGamepadAlpha,
|
|
@@ -13154,14 +14845,12 @@ export
|
|
|
13154
14845
|
soundVolume,
|
|
13155
14846
|
soundDefaultRange,
|
|
13156
14847
|
soundDefaultTaper,
|
|
13157
|
-
medalDisplayTime,
|
|
13158
|
-
medalDisplaySlideTime,
|
|
13159
|
-
medalDisplaySize,
|
|
13160
14848
|
|
|
13161
14849
|
// Setters for globals
|
|
13162
14850
|
setCameraPos,
|
|
13163
14851
|
setCameraAngle,
|
|
13164
14852
|
setCameraScale,
|
|
14853
|
+
setTimeScale,
|
|
13165
14854
|
setCanvasColorTiles,
|
|
13166
14855
|
setCanvasClearColor,
|
|
13167
14856
|
setCanvasMaxSize,
|
|
@@ -13187,6 +14876,7 @@ export
|
|
|
13187
14876
|
setObjectMaxSpeed,
|
|
13188
14877
|
setGravity,
|
|
13189
14878
|
setParticleEmitRateScale,
|
|
14879
|
+
setGLCircleSides,
|
|
13190
14880
|
setTouchInputEnable,
|
|
13191
14881
|
setGamepadsEnable,
|
|
13192
14882
|
setGamepadDirectionEmulateStick,
|
|
@@ -13202,10 +14892,6 @@ export
|
|
|
13202
14892
|
setSoundVolume,
|
|
13203
14893
|
setSoundDefaultRange,
|
|
13204
14894
|
setSoundDefaultTaper,
|
|
13205
|
-
setMedalDisplayTime,
|
|
13206
|
-
setMedalDisplaySlideTime,
|
|
13207
|
-
setMedalDisplaySize,
|
|
13208
|
-
setMedalsPreventUnlock,
|
|
13209
14895
|
setDebugWatermark,
|
|
13210
14896
|
setDebugKey,
|
|
13211
14897
|
|
|
@@ -13235,6 +14921,7 @@ export
|
|
|
13235
14921
|
percentLerp,
|
|
13236
14922
|
smoothStep,
|
|
13237
14923
|
nearestPowerOfTwo,
|
|
14924
|
+
isPowerOfTwo,
|
|
13238
14925
|
isOverlapping,
|
|
13239
14926
|
isIntersecting,
|
|
13240
14927
|
lineTest,
|
|
@@ -13310,9 +14997,11 @@ export
|
|
|
13310
14997
|
drawTile,
|
|
13311
14998
|
drawRect,
|
|
13312
14999
|
drawRectGradient,
|
|
15000
|
+
drawTextureWrapped,
|
|
13313
15001
|
drawLineList,
|
|
13314
15002
|
drawLine,
|
|
13315
15003
|
drawPoly,
|
|
15004
|
+
drawRegularPoly,
|
|
13316
15005
|
drawEllipse,
|
|
13317
15006
|
drawCircle,
|
|
13318
15007
|
drawCanvas2D,
|
|
@@ -13326,6 +15015,7 @@ export
|
|
|
13326
15015
|
toggleFullscreen,
|
|
13327
15016
|
setCursor,
|
|
13328
15017
|
getCameraSize,
|
|
15018
|
+
isOnScreen,
|
|
13329
15019
|
|
|
13330
15020
|
// WebGL
|
|
13331
15021
|
glCanvas,
|
|
@@ -13333,6 +15023,7 @@ export
|
|
|
13333
15023
|
glAntialias,
|
|
13334
15024
|
glClearCanvas,
|
|
13335
15025
|
glSetTexture,
|
|
15026
|
+
glSetTextureWrap,
|
|
13336
15027
|
glCompileShader,
|
|
13337
15028
|
glCreateProgram,
|
|
13338
15029
|
glCreateTexture,
|
|
@@ -13374,8 +15065,11 @@ export
|
|
|
13374
15065
|
gamepadWasPressed,
|
|
13375
15066
|
gamepadWasReleased,
|
|
13376
15067
|
gamepadStick,
|
|
15068
|
+
gamepadStickCount,
|
|
13377
15069
|
gamepadDpad,
|
|
13378
15070
|
gamepadConnected,
|
|
15071
|
+
gamepadVibrate,
|
|
15072
|
+
gamepadVibrateStop,
|
|
13379
15073
|
vibrate,
|
|
13380
15074
|
vibrateStop,
|
|
13381
15075
|
pointerLockRequest,
|
|
@@ -13386,6 +15080,7 @@ export
|
|
|
13386
15080
|
audioContext,
|
|
13387
15081
|
audioMasterGain,
|
|
13388
15082
|
audioDefaultSampleRate,
|
|
15083
|
+
audioIsRunning,
|
|
13389
15084
|
Sound,
|
|
13390
15085
|
SoundInstance,
|
|
13391
15086
|
speak,
|
|
@@ -13413,11 +15108,6 @@ export
|
|
|
13413
15108
|
ParticleEmitter,
|
|
13414
15109
|
Particle,
|
|
13415
15110
|
|
|
13416
|
-
// Medals
|
|
13417
|
-
medals,
|
|
13418
|
-
medalsPreventUnlock,
|
|
13419
|
-
medalsInit,
|
|
13420
|
-
Medal,
|
|
13421
15111
|
}
|
|
13422
15112
|
/**
|
|
13423
15113
|
* LittleJS Module Plugins Export
|
|
@@ -13425,6 +15115,20 @@ export
|
|
|
13425
15115
|
|
|
13426
15116
|
export
|
|
13427
15117
|
{
|
|
15118
|
+
// Medals
|
|
15119
|
+
medals,
|
|
15120
|
+
medalsPreventUnlock,
|
|
15121
|
+
medalsInit,
|
|
15122
|
+
medalsForEach,
|
|
15123
|
+
Medal,
|
|
15124
|
+
medalDisplayTime,
|
|
15125
|
+
medalDisplaySlideTime,
|
|
15126
|
+
medalDisplaySize,
|
|
15127
|
+
setMedalDisplayTime,
|
|
15128
|
+
setMedalDisplaySlideTime,
|
|
15129
|
+
setMedalDisplaySize,
|
|
15130
|
+
setMedalsPreventUnlock,
|
|
15131
|
+
|
|
13428
15132
|
// Newgrounds
|
|
13429
15133
|
newgrounds,
|
|
13430
15134
|
NewgroundsPlugin,
|
|
@@ -13450,6 +15154,7 @@ export
|
|
|
13450
15154
|
UICheckbox,
|
|
13451
15155
|
UISlider,
|
|
13452
15156
|
UIVideo,
|
|
15157
|
+
UILayout,
|
|
13453
15158
|
|
|
13454
15159
|
// Box2D Physics
|
|
13455
15160
|
box2d,
|
|
@@ -13481,4 +15186,15 @@ export
|
|
|
13481
15186
|
drawNineSliceScreen,
|
|
13482
15187
|
drawThreeSlice,
|
|
13483
15188
|
drawThreeSliceScreen,
|
|
15189
|
+
|
|
15190
|
+
// Tween System
|
|
15191
|
+
Tween,
|
|
15192
|
+
tweenProperty,
|
|
15193
|
+
tweenStopAll,
|
|
15194
|
+
tweenUpdate,
|
|
15195
|
+
Ease,
|
|
15196
|
+
|
|
15197
|
+
// Path Finding
|
|
15198
|
+
PathFinder,
|
|
15199
|
+
PathFinderNode,
|
|
13484
15200
|
}
|