littlejsengine 1.18.12 → 1.18.15
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/FAQ.md +197 -15
- package/README.md +28 -7
- package/dist/littlejs.d.ts +105 -77
- package/dist/littlejs.esm.js +414 -277
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +406 -272
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +403 -265
- package/package.json +4 -1
- package/plugins/box2d.js +17 -6
- package/plugins/newgrounds.js +15 -3
- package/plugins/pathFinder.js +5 -3
- package/plugins/pluginExport.js +4 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +25 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +14 -3
- package/src/engineAudio.js +27 -13
- package/src/engineBuild.mjs +1 -1
- package/src/engineDebug.js +3 -7
- package/src/engineDraw.js +71 -40
- package/src/engineExport.js +4 -2
- package/src/engineInput.js +17 -12
- package/src/engineLogo.js +1 -1
- package/src/engineMath.js +21 -7
- package/src/engineObject.js +19 -11
- package/src/engineParticles.js +26 -27
- package/src/engineSettings.js +3 -2
- package/src/engineTileLayer.js +62 -43
- package/src/engineUtilities.js +24 -11
- package/src/engineWebGL.js +23 -55
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.15';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -164,12 +164,20 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
|
164
164
|
* ['tiles.png', 'tilesLevel.png'] // images to load
|
|
165
165
|
* );
|
|
166
166
|
* @memberof Engine */
|
|
167
|
-
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement
|
|
167
|
+
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
168
168
|
{
|
|
169
169
|
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
170
170
|
ASSERT(!mainContext, 'engine already initialized');
|
|
171
|
+
// runtime guard so release builds (where the assert is stripped) don't
|
|
172
|
+
// double-register listeners / double-add canvases on a second call
|
|
173
|
+
if (mainContext) return;
|
|
171
174
|
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
172
175
|
|
|
176
|
+
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
177
|
+
if (!document.body)
|
|
178
|
+
document.documentElement.appendChild(document.createElement('body'));
|
|
179
|
+
rootElement ||= document.body;
|
|
180
|
+
|
|
173
181
|
// allow passing in empty functions
|
|
174
182
|
gameInit ||= ()=>{};
|
|
175
183
|
gameUpdate ||= ()=>{};
|
|
@@ -195,6 +203,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
195
203
|
{
|
|
196
204
|
// update time keeping
|
|
197
205
|
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
206
|
+
// skip delta on the very first frame so timeReal doesn't jump
|
|
207
|
+
// by ~page-load-time when RAF starts handing real timestamps
|
|
208
|
+
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
198
209
|
frameTimeLastMS = frameTimeMS;
|
|
199
210
|
if (debug || debugWatermark)
|
|
200
211
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
@@ -425,7 +436,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
425
436
|
promises.push(loadTexture(0));
|
|
426
437
|
|
|
427
438
|
// load engine font image
|
|
428
|
-
promises.push(
|
|
439
|
+
promises.push(imageFontInit());
|
|
429
440
|
|
|
430
441
|
if (showSplashScreen)
|
|
431
442
|
{
|
|
@@ -650,12 +661,8 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
|
|
|
650
661
|
ASSERT(isNumber(time), 'time must be a number');
|
|
651
662
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
652
663
|
|
|
653
|
-
if (typeof size === 'number')
|
|
654
|
-
size = vec2(size); // allow passing in floats
|
|
655
664
|
if (isColor(color))
|
|
656
665
|
color = color.toString();
|
|
657
|
-
pos = pos.copy();
|
|
658
|
-
size = size.copy();
|
|
659
666
|
const timer = new Timer(time);
|
|
660
667
|
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
|
|
661
668
|
}
|
|
@@ -725,7 +732,7 @@ function debugPoint(pos, color, time, angle, screenSpace=false)
|
|
|
725
732
|
* @param {number} [time]
|
|
726
733
|
* @param {boolean} [screenSpace]
|
|
727
734
|
* @memberof Debug */
|
|
728
|
-
function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
|
|
735
|
+
function debugLine(posA, posB, color, width=.1, time=0, screenSpace=false)
|
|
729
736
|
{
|
|
730
737
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
731
738
|
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
@@ -763,7 +770,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
763
770
|
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
|
|
764
771
|
}
|
|
765
772
|
|
|
766
|
-
/** Draw
|
|
773
|
+
/** Draw debug text in world space
|
|
767
774
|
* @param {string|number} text
|
|
768
775
|
* @param {Vector2} pos
|
|
769
776
|
* @param {number} [size]
|
|
@@ -1094,7 +1101,7 @@ function debugRender()
|
|
|
1094
1101
|
continue;
|
|
1095
1102
|
if (parseInt(i) < 3)
|
|
1096
1103
|
mousePressed += i + ' ' ;
|
|
1097
|
-
else
|
|
1104
|
+
else
|
|
1098
1105
|
keysPressed += i + ' ' ;
|
|
1099
1106
|
}
|
|
1100
1107
|
mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
|
|
@@ -1353,19 +1360,19 @@ const max = Math.max;
|
|
|
1353
1360
|
* @param {number} x
|
|
1354
1361
|
* @return {number}
|
|
1355
1362
|
* @memberof Math */
|
|
1356
|
-
const sign = Math.sign;
|
|
1363
|
+
const sign = (x) => Math.sign(x);
|
|
1357
1364
|
|
|
1358
1365
|
/** Returns hypotenuse of values passed in
|
|
1359
1366
|
* @param {...number} values
|
|
1360
1367
|
* @return {number}
|
|
1361
1368
|
* @memberof Math */
|
|
1362
|
-
const hypot = Math.hypot;
|
|
1369
|
+
const hypot = (...values) => Math.hypot(...values);
|
|
1363
1370
|
|
|
1364
1371
|
/** Returns log2 of value passed in
|
|
1365
1372
|
* @param {number} x
|
|
1366
1373
|
* @return {number}
|
|
1367
1374
|
* @memberof Math */
|
|
1368
|
-
const log2 = Math.log2;
|
|
1375
|
+
const log2 = (x) => Math.log2(x);
|
|
1369
1376
|
|
|
1370
1377
|
/** Returns sin of value passed in
|
|
1371
1378
|
* @param {number} x
|
|
@@ -1507,7 +1514,8 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
|
1507
1514
|
const dy = (posA.y - posB.y)*2;
|
|
1508
1515
|
const sx = sizeA.x + sizeB.x;
|
|
1509
1516
|
const sy = sizeA.y + sizeB.y;
|
|
1510
|
-
|
|
1517
|
+
// symmetric so isOverlapping(A,B) === isOverlapping(B,A) at touching edges
|
|
1518
|
+
return abs(dx) < sx && abs(dy) < sy;
|
|
1511
1519
|
}
|
|
1512
1520
|
|
|
1513
1521
|
/** Returns true if a line segment is intersecting an axis aligned box
|
|
@@ -1596,7 +1604,7 @@ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'
|
|
|
1596
1604
|
/**
|
|
1597
1605
|
* Check if object is an array
|
|
1598
1606
|
* @param {any} a
|
|
1599
|
-
* @return {
|
|
1607
|
+
* @return {a is Array<any>}
|
|
1600
1608
|
* @memberof Math */
|
|
1601
1609
|
function isArray(a) { return Array.isArray(a); }
|
|
1602
1610
|
|
|
@@ -1734,7 +1742,13 @@ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length);
|
|
|
1734
1742
|
* @return {Vector2}
|
|
1735
1743
|
* @memberof Random */
|
|
1736
1744
|
function randInCircle(radius=1, minRadius=0)
|
|
1737
|
-
{
|
|
1745
|
+
{
|
|
1746
|
+
// r is uniform in area ⇒ r² uniform in [minRadius², radius²]
|
|
1747
|
+
// (the squared inner bound is what makes minRadius the actual exclusion edge)
|
|
1748
|
+
if (radius <= 0) return new Vector2;
|
|
1749
|
+
const ratio = clamp(minRadius / radius);
|
|
1750
|
+
return randVec2(radius * rand(ratio*ratio, 1)**.5);
|
|
1751
|
+
}
|
|
1738
1752
|
|
|
1739
1753
|
/** Returns a random color between the two passed in colors, combine components if linear
|
|
1740
1754
|
* @param {Color} [colorA=WHITE]
|
|
@@ -1804,7 +1818,14 @@ class RandomGenerator
|
|
|
1804
1818
|
* @param {number} [valueA]
|
|
1805
1819
|
* @param {number} [valueB]
|
|
1806
1820
|
* @return {number} */
|
|
1807
|
-
floatSign(valueA=1, valueB=0)
|
|
1821
|
+
floatSign(valueA=1, valueB=0)
|
|
1822
|
+
{
|
|
1823
|
+
const lo = min(valueA, valueB);
|
|
1824
|
+
const hi = max(valueA, valueB);
|
|
1825
|
+
const d = hi - lo;
|
|
1826
|
+
const e = this.float(d*2);
|
|
1827
|
+
return e < d ? lo + e : d - lo - e;
|
|
1828
|
+
}
|
|
1808
1829
|
|
|
1809
1830
|
/** Returns a random angle between -PI and PI
|
|
1810
1831
|
* @return {number} */
|
|
@@ -2517,9 +2538,15 @@ class Timer
|
|
|
2517
2538
|
* @return {number} */
|
|
2518
2539
|
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2519
2540
|
|
|
2520
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2541
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
2542
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
2521
2543
|
* @return {number} */
|
|
2522
|
-
getPercent()
|
|
2544
|
+
getPercent()
|
|
2545
|
+
{
|
|
2546
|
+
if (!this.isSet()) return 0;
|
|
2547
|
+
if (!this.setTime) return 1;
|
|
2548
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
2549
|
+
}
|
|
2523
2550
|
|
|
2524
2551
|
/** Get the time this timer was set to, returns 0 if not set
|
|
2525
2552
|
* @return {number} */
|
|
@@ -2546,9 +2573,9 @@ class Timer
|
|
|
2546
2573
|
* @memberof Utilities */
|
|
2547
2574
|
function formatTime(t)
|
|
2548
2575
|
{
|
|
2549
|
-
const
|
|
2576
|
+
const signStr = t < 0 ? '-' : '';
|
|
2550
2577
|
t = abs(t)|0;
|
|
2551
|
-
return
|
|
2578
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
2552
2579
|
}
|
|
2553
2580
|
|
|
2554
2581
|
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
@@ -2634,15 +2661,20 @@ function shareURL(title, url, callback)
|
|
|
2634
2661
|
function readSaveData(saveName, defaultSaveData)
|
|
2635
2662
|
{
|
|
2636
2663
|
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
2637
|
-
|
|
2638
|
-
//
|
|
2639
|
-
|
|
2664
|
+
|
|
2665
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
2666
|
+
// iframes) and corrupt JSON in stored data
|
|
2640
2667
|
let loadedData = {};
|
|
2641
|
-
|
|
2668
|
+
try
|
|
2642
2669
|
{
|
|
2643
|
-
|
|
2644
|
-
|
|
2670
|
+
const data = localStorage[saveName];
|
|
2671
|
+
if (data)
|
|
2672
|
+
{
|
|
2673
|
+
try { loadedData = JSON.parse(data); }
|
|
2674
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
2675
|
+
}
|
|
2645
2676
|
}
|
|
2677
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
2646
2678
|
return { ...defaultSaveData, ...loadedData };
|
|
2647
2679
|
}
|
|
2648
2680
|
|
|
@@ -2653,7 +2685,9 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
2653
2685
|
function writeSaveData(saveName, saveData)
|
|
2654
2686
|
{
|
|
2655
2687
|
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
2656
|
-
localStorage
|
|
2688
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
2689
|
+
try { localStorage[saveName] = JSON.stringify(saveData); }
|
|
2690
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
2657
2691
|
}
|
|
2658
2692
|
|
|
2659
2693
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -2748,7 +2782,7 @@ let canvasColorTiles = true;
|
|
|
2748
2782
|
|
|
2749
2783
|
/** Color to clear the canvas to before render, does not clear if alpha is 0
|
|
2750
2784
|
* @type {Color}
|
|
2751
|
-
* @memberof
|
|
2785
|
+
* @memberof Settings */
|
|
2752
2786
|
let canvasClearColor = CLEAR_BLACK;
|
|
2753
2787
|
|
|
2754
2788
|
/** The max size of the canvas, centered if window is larger
|
|
@@ -2948,7 +2982,8 @@ let touchInputEnable = true;
|
|
|
2948
2982
|
let touchGamepadEnable = false;
|
|
2949
2983
|
|
|
2950
2984
|
/** True if touch gamepad should have start button in the center
|
|
2951
|
-
* - Prevents activating
|
|
2985
|
+
* - Prevents activating within 2*touchGamepadSize of the virtual stick or face buttons
|
|
2986
|
+
* (one radius for the visible control + one radius of buffer beyond its edge)
|
|
2952
2987
|
* - When the game is paused, any touch will press the button
|
|
2953
2988
|
* - Set size to enable the center button
|
|
2954
2989
|
* @type {number}
|
|
@@ -3360,7 +3395,7 @@ class EngineObject
|
|
|
3360
3395
|
this.color = color.copy();
|
|
3361
3396
|
/** @property {Color} - Additive color to apply when rendered */
|
|
3362
3397
|
this.additiveColor = undefined;
|
|
3363
|
-
/** @property {boolean} - Should
|
|
3398
|
+
/** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
|
|
3364
3399
|
this.mirror = false;
|
|
3365
3400
|
/** @property {boolean} - Has object been destroyed? */
|
|
3366
3401
|
this.destroyed = false;
|
|
@@ -3446,6 +3481,9 @@ class EngineObject
|
|
|
3446
3481
|
// child objects do not have physics
|
|
3447
3482
|
ASSERT(!this.parent);
|
|
3448
3483
|
|
|
3484
|
+
// bail if a collision callback destroyed us mid-frame
|
|
3485
|
+
if (this.destroyed) return;
|
|
3486
|
+
|
|
3449
3487
|
if (this.clampSpeed)
|
|
3450
3488
|
{
|
|
3451
3489
|
// limit max speed to prevent missing collisions
|
|
@@ -3501,6 +3539,8 @@ class EngineObject
|
|
|
3501
3539
|
|
|
3502
3540
|
// notify objects of collision and check if should be resolved
|
|
3503
3541
|
const collide1 = this.collideWithObject(o);
|
|
3542
|
+
// callback may have destroyed us; stop resolving against more objects
|
|
3543
|
+
if (this.destroyed) return;
|
|
3504
3544
|
const collide2 = o.collideWithObject(this);
|
|
3505
3545
|
if (!collide1 || !collide2) continue;
|
|
3506
3546
|
|
|
@@ -3596,13 +3636,17 @@ class EngineObject
|
|
|
3596
3636
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3597
3637
|
if (isBlockedX)
|
|
3598
3638
|
{
|
|
3599
|
-
// try to
|
|
3639
|
+
// try to step over a 1-tile bump (direction follows gravity sign
|
|
3640
|
+
// so inverted gravity steps down off a ceiling bump instead of up;
|
|
3641
|
+
// zero gravity defaults to the normal-gravity step-up direction)
|
|
3600
3642
|
const epsilon = 1e-3;
|
|
3601
|
-
const
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3643
|
+
const maxMove = .1;
|
|
3644
|
+
const gravitySign = gravity.y > 0 ? -1 : 1;
|
|
3645
|
+
const y = gravitySign > 0 ?
|
|
3646
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
|
|
3647
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
|
|
3648
|
+
const delta = abs(y - this.pos.y);
|
|
3649
|
+
if (delta < maxMove)
|
|
3606
3650
|
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
3607
3651
|
{
|
|
3608
3652
|
this.pos.y = y;
|
|
@@ -3742,6 +3786,8 @@ class EngineObject
|
|
|
3742
3786
|
* @return {EngineObject} The child object added */
|
|
3743
3787
|
addChild(child, localPos=vec2(), localAngle=0)
|
|
3744
3788
|
{
|
|
3789
|
+
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
3790
|
+
if (this.destroyed) return child;
|
|
3745
3791
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
3746
3792
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3747
3793
|
ASSERT(child !== this, 'cannot add self as child');
|
|
@@ -3758,10 +3804,7 @@ class EngineObject
|
|
|
3758
3804
|
removeChild(child)
|
|
3759
3805
|
{
|
|
3760
3806
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
3761
|
-
|
|
3762
|
-
const index = this.children.indexOf(child);
|
|
3763
|
-
ASSERT(index >= 0, 'child not found in children array');
|
|
3764
|
-
index >= 0 && this.children.splice(index, 1);
|
|
3807
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
3765
3808
|
child.parent = undefined;
|
|
3766
3809
|
}
|
|
3767
3810
|
|
|
@@ -3836,7 +3879,7 @@ class EngineObject
|
|
|
3836
3879
|
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
3837
3880
|
* - Primitive drawing for polygons, ellipses, and lines
|
|
3838
3881
|
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
3839
|
-
* - Text rendering with custom fonts and
|
|
3882
|
+
* - Text rendering with custom fonts and ImageFont support
|
|
3840
3883
|
* - Color and additive color blending for effects
|
|
3841
3884
|
* - Rotation, mirroring, and scaling transformations
|
|
3842
3885
|
* - Camera system with position, scale, and rotation
|
|
@@ -3932,7 +3975,7 @@ let primitiveCount;
|
|
|
3932
3975
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
3933
3976
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
3934
3977
|
* @memberof Draw */
|
|
3935
|
-
function tile(index=
|
|
3978
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3936
3979
|
{
|
|
3937
3980
|
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
3938
3981
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
@@ -3983,8 +4026,8 @@ class TileInfo
|
|
|
3983
4026
|
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
3984
4027
|
* @param {Vector2} [size] - Size of tile in pixels
|
|
3985
4028
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
3986
|
-
* @param {number} [padding] - How many pixels padding around
|
|
3987
|
-
* @param {number} [bleed] - How many pixels smaller to
|
|
4029
|
+
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
4030
|
+
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
3988
4031
|
*/
|
|
3989
4032
|
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3990
4033
|
{
|
|
@@ -4016,7 +4059,7 @@ class TileInfo
|
|
|
4016
4059
|
ASSERT(typeof frame === 'number');
|
|
4017
4060
|
const w = this.size.x + this.padding*2;
|
|
4018
4061
|
const x = frame*w;
|
|
4019
|
-
ASSERT(x
|
|
4062
|
+
ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
4020
4063
|
return this.offset(new Vector2(x));
|
|
4021
4064
|
}
|
|
4022
4065
|
|
|
@@ -4105,7 +4148,7 @@ class TextureInfo
|
|
|
4105
4148
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
4106
4149
|
* @memberof Draw */
|
|
4107
4150
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
4108
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
4151
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
4109
4152
|
{
|
|
4110
4153
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4111
4154
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -4148,10 +4191,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
4148
4191
|
}
|
|
4149
4192
|
else
|
|
4150
4193
|
{
|
|
4151
|
-
// untextured:
|
|
4152
|
-
//
|
|
4153
|
-
// uvs/rgba zeroed). Color+additive are folded together to match
|
|
4154
|
-
// the Canvas2D path's color.add(additiveColor) on line ~337.
|
|
4194
|
+
// untextured: fold color+additive to match the Canvas2D path's
|
|
4195
|
+
// color.add(additiveColor) on line ~337.
|
|
4155
4196
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
4156
4197
|
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
4157
4198
|
}
|
|
@@ -4364,7 +4405,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
4364
4405
|
* @param {boolean} [screenSpace]
|
|
4365
4406
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4366
4407
|
* @memberof Draw */
|
|
4367
|
-
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
4408
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4368
4409
|
{
|
|
4369
4410
|
ASSERT(isArray(points), 'points must be an array');
|
|
4370
4411
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -4413,7 +4454,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
4413
4454
|
* @param {boolean} [screenSpace]
|
|
4414
4455
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4415
4456
|
* @memberof Draw */
|
|
4416
|
-
function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
|
|
4457
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4417
4458
|
{
|
|
4418
4459
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
4419
4460
|
const size = vec2(width, halfDelta.length()*2);
|
|
@@ -4429,9 +4470,9 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
4429
4470
|
* @param {Vector2} [size=vec2(1)]
|
|
4430
4471
|
* @param {number} [sides]
|
|
4431
4472
|
* @param {Color} [color=WHITE]
|
|
4432
|
-
* @param {number} [angle]
|
|
4433
4473
|
* @param {number} [lineWidth]
|
|
4434
4474
|
* @param {Color} [lineColor=BLACK]
|
|
4475
|
+
* @param {number} [angle]
|
|
4435
4476
|
* @param {boolean} [useWebGL=glEnable]
|
|
4436
4477
|
* @param {boolean} [screenSpace]
|
|
4437
4478
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4524,7 +4565,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
4524
4565
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4525
4566
|
|
|
4526
4567
|
// clamp line width to prevent artifacts
|
|
4527
|
-
lineWidth = clamp(lineWidth, 0,
|
|
4568
|
+
lineWidth = clamp(lineWidth, 0, min(size.x, size.y));
|
|
4528
4569
|
|
|
4529
4570
|
if (useWebGL && glEnable)
|
|
4530
4571
|
{
|
|
@@ -4566,24 +4607,26 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
4566
4607
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
4567
4608
|
}
|
|
4568
4609
|
|
|
4569
|
-
/** Draw
|
|
4610
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
4570
4611
|
* - Best when batched with other untextured polys
|
|
4571
4612
|
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
4572
4613
|
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
4573
4614
|
* @param {Vector2} pos
|
|
4574
|
-
* @param {
|
|
4615
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
4575
4616
|
* @param {Color} [colorInner=WHITE]
|
|
4576
4617
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
4618
|
+
* @param {number} [angle]
|
|
4577
4619
|
* @param {boolean} [useWebGL=glEnable]
|
|
4578
4620
|
* @param {boolean} [screenSpace]
|
|
4579
4621
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4580
4622
|
* @memberof Draw */
|
|
4581
|
-
let
|
|
4582
|
-
function
|
|
4623
|
+
let drawEllipseGradientOffset = 0;
|
|
4624
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4583
4625
|
{
|
|
4584
4626
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4585
|
-
ASSERT(
|
|
4627
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
4586
4628
|
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
4629
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4587
4630
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4588
4631
|
|
|
4589
4632
|
if (headlessMode) return;
|
|
@@ -4595,26 +4638,33 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
4595
4638
|
{
|
|
4596
4639
|
// convert to world space
|
|
4597
4640
|
pos = screenToWorld(pos);
|
|
4598
|
-
size
|
|
4641
|
+
size = size.scale(1/cameraScale);
|
|
4642
|
+
angle += cameraAngle;
|
|
4599
4643
|
}
|
|
4600
4644
|
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
4601
4645
|
// so back-to-back gradients at the same position have their hole
|
|
4602
4646
|
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
4603
4647
|
// verts and don't visibly stack
|
|
4604
4648
|
const sides = glCircleSides;
|
|
4605
|
-
const
|
|
4649
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
4606
4650
|
const innerInt = colorInner.rgbaInt();
|
|
4607
4651
|
const outerInt = colorOuter.rgbaInt();
|
|
4608
|
-
const offset =
|
|
4652
|
+
const offset = drawEllipseGradientOffset++;
|
|
4653
|
+
const c = cos(-angle), s = sin(-angle);
|
|
4654
|
+
const rim = (a) =>
|
|
4655
|
+
{
|
|
4656
|
+
const lx = sin(a)*radiusX, ly = cos(a)*radiusY;
|
|
4657
|
+
return vec2(pos.x + lx*c - ly*s, pos.y + lx*s + ly*c);
|
|
4658
|
+
};
|
|
4609
4659
|
const startA = (offset%sides)/sides*PI*2;
|
|
4610
|
-
const points = [
|
|
4660
|
+
const points = [rim(startA)];
|
|
4611
4661
|
const colors = [outerInt];
|
|
4612
4662
|
for (let i=sides; i--;)
|
|
4613
4663
|
{
|
|
4614
4664
|
const a = ((i+offset)%sides)/sides*PI*2;
|
|
4615
4665
|
points.push(pos);
|
|
4616
4666
|
colors.push(innerInt);
|
|
4617
|
-
points.push(
|
|
4667
|
+
points.push(rim(a));
|
|
4618
4668
|
colors.push(outerInt);
|
|
4619
4669
|
}
|
|
4620
4670
|
glDrawColoredPoints(points, colors);
|
|
@@ -4624,7 +4674,7 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
4624
4674
|
// normal canvas 2D rendering method (slower)
|
|
4625
4675
|
++drawCount;
|
|
4626
4676
|
++primitiveCount;
|
|
4627
|
-
drawCanvas2D(pos,
|
|
4677
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
4628
4678
|
{
|
|
4629
4679
|
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
4630
4680
|
gradient.addColorStop(0, colorInner.toString());
|
|
@@ -4637,6 +4687,24 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
4637
4687
|
}
|
|
4638
4688
|
}
|
|
4639
4689
|
|
|
4690
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
4691
|
+
* - Best when batched with other untextured polys
|
|
4692
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
4693
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
4694
|
+
* @param {Vector2} pos
|
|
4695
|
+
* @param {number} [size=1] - Diameter
|
|
4696
|
+
* @param {Color} [colorInner=WHITE]
|
|
4697
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
4698
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
4699
|
+
* @param {boolean} [screenSpace]
|
|
4700
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4701
|
+
* @memberof Draw */
|
|
4702
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
4703
|
+
{
|
|
4704
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
4705
|
+
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4640
4708
|
/**
|
|
4641
4709
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
4642
4710
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -4691,7 +4759,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4691
4759
|
* @param {number} [angle]
|
|
4692
4760
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
4693
4761
|
* @memberof Draw */
|
|
4694
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
4762
|
+
function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
4695
4763
|
{
|
|
4696
4764
|
// convert to screen space
|
|
4697
4765
|
pos = worldToScreen(pos);
|
|
@@ -4731,16 +4799,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
4731
4799
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
4732
4800
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4733
4801
|
|
|
4802
|
+
const lines = (text+'').split('\n');
|
|
4803
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4804
|
+
// save before style mutations so caller's context state is preserved
|
|
4805
|
+
context.save();
|
|
4734
4806
|
context.fillStyle = color.toString();
|
|
4735
4807
|
context.strokeStyle = lineColor.toString();
|
|
4736
4808
|
context.lineWidth = lineWidth;
|
|
4737
4809
|
context.textAlign = textAlign;
|
|
4738
4810
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
4739
4811
|
context.textBaseline = 'middle';
|
|
4740
|
-
|
|
4741
|
-
const lines = (text+'').split('\n');
|
|
4742
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4743
|
-
context.save();
|
|
4744
4812
|
context.translate(pos.x, posY);
|
|
4745
4813
|
context.rotate(-angle);
|
|
4746
4814
|
let yOffset = 0;
|
|
@@ -4901,6 +4969,9 @@ function isOnScreen(pos, size=0)
|
|
|
4901
4969
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4902
4970
|
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
4903
4971
|
|
|
4972
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
4973
|
+
if (!cameraScale) return false;
|
|
4974
|
+
|
|
4904
4975
|
// optimized circle on screen test
|
|
4905
4976
|
// pos = worldToScreen(pos);
|
|
4906
4977
|
let x = pos.x - cameraPos.x;
|
|
@@ -4942,7 +5013,10 @@ function combineCanvases()
|
|
|
4942
5013
|
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
4943
5014
|
workCanvas.width = w;
|
|
4944
5015
|
workCanvas.height = h;
|
|
4945
|
-
|
|
5016
|
+
// remove background alpha — explicit fillStyle so a previous caller
|
|
5017
|
+
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
5018
|
+
workContext.fillStyle = '#000';
|
|
5019
|
+
workContext.fillRect(0,0,w,h);
|
|
4946
5020
|
glCopyToContext(workContext);
|
|
4947
5021
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
4948
5022
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -5085,33 +5159,33 @@ function setCursor(cursorStyle = 'auto')
|
|
|
5085
5159
|
///////////////////////////////////////////////////////////////////////////////
|
|
5086
5160
|
|
|
5087
5161
|
/** Engine font image, 8x8 font provided by the engine
|
|
5088
|
-
* @type {
|
|
5162
|
+
* @type {ImageFont}
|
|
5089
5163
|
* @memberof Draw */
|
|
5090
|
-
let
|
|
5164
|
+
let engineImageFont;
|
|
5091
5165
|
|
|
5092
5166
|
/**
|
|
5093
|
-
* Font
|
|
5167
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
5094
5168
|
* - 96 characters (from space to tilde) are stored in an image
|
|
5095
5169
|
* - A 8x8 default engine font is supplied for general use
|
|
5096
5170
|
* - This system is WebGL enabled for fast text rendering
|
|
5097
5171
|
* - Fonts can also be colored and scaled along each axis
|
|
5098
|
-
*
|
|
5172
|
+
*
|
|
5099
5173
|
* @memberof Draw
|
|
5100
5174
|
* @example
|
|
5101
5175
|
* // use built in font
|
|
5102
|
-
* const font =
|
|
5176
|
+
* const font = engineImageFont;
|
|
5103
5177
|
*
|
|
5104
5178
|
* // draw text
|
|
5105
5179
|
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
5106
5180
|
*/
|
|
5107
|
-
class
|
|
5181
|
+
class ImageFont
|
|
5108
5182
|
{
|
|
5109
5183
|
/** Create an image font
|
|
5110
5184
|
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
5111
5185
|
*/
|
|
5112
5186
|
constructor(tileInfo)
|
|
5113
5187
|
{
|
|
5114
|
-
ASSERT(!!tileInfo, 'tileInfo is required for
|
|
5188
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
5115
5189
|
|
|
5116
5190
|
/** @property {TileInfo} - Tile info for the font */
|
|
5117
5191
|
this.tileInfo = tileInfo.frame(0);
|
|
@@ -5196,7 +5270,7 @@ class FontImage
|
|
|
5196
5270
|
}
|
|
5197
5271
|
|
|
5198
5272
|
// load engine font, called automatically on startup
|
|
5199
|
-
async function
|
|
5273
|
+
async function imageFontInit()
|
|
5200
5274
|
{
|
|
5201
5275
|
const image = new Image;
|
|
5202
5276
|
await new Promise(resolve =>
|
|
@@ -5209,7 +5283,7 @@ async function fontImageInit()
|
|
|
5209
5283
|
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
5210
5284
|
const textureInfo = new TextureInfo(image);
|
|
5211
5285
|
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
5212
|
-
|
|
5286
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
5213
5287
|
}
|
|
5214
5288
|
/**
|
|
5215
5289
|
* LittleJS Input System
|
|
@@ -5658,9 +5732,11 @@ function inputInit()
|
|
|
5658
5732
|
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
5659
5733
|
}
|
|
5660
5734
|
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
5661
|
-
function onMouseWheel(e)
|
|
5662
|
-
{
|
|
5663
|
-
|
|
5735
|
+
function onMouseWheel(e)
|
|
5736
|
+
{
|
|
5737
|
+
// accumulate so multiple wheel events in one frame are not lost
|
|
5738
|
+
if (!e.ctrlKey)
|
|
5739
|
+
mouseWheel += sign(e.deltaY);
|
|
5664
5740
|
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
5665
5741
|
e.preventDefault(); // prevent page scrolling
|
|
5666
5742
|
}
|
|
@@ -5785,9 +5861,14 @@ function inputInit()
|
|
|
5785
5861
|
if (button < touchGamepadButtonCount)
|
|
5786
5862
|
touchGamepadButtons[button] = 1;
|
|
5787
5863
|
}
|
|
5788
|
-
else if (startCenter.distance(touchPos)
|
|
5864
|
+
else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize &&
|
|
5865
|
+
stickCenter.distance(touchPos) >= 2 * touchGamepadSize &&
|
|
5866
|
+
buttonCenter.distance(touchPos) >= 2 * touchGamepadSize)
|
|
5789
5867
|
{
|
|
5790
5868
|
// virtual start button in center
|
|
5869
|
+
// require a fat-finger buffer of touchGamepadSize beyond the
|
|
5870
|
+
// edge of the stick/buttons so drift off those controls can't
|
|
5871
|
+
// accidentally fire start
|
|
5791
5872
|
touchGamepadButtons[9] = 1;
|
|
5792
5873
|
}
|
|
5793
5874
|
}
|
|
@@ -5829,7 +5910,7 @@ function inputUpdate()
|
|
|
5829
5910
|
v > min ? percent(v, min, max) :
|
|
5830
5911
|
v < -min ? -percent(-v, min, max) : 0;
|
|
5831
5912
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5832
|
-
}
|
|
5913
|
+
};
|
|
5833
5914
|
|
|
5834
5915
|
// update touch gamepad if enabled
|
|
5835
5916
|
if (touchGamepadEnable && isTouchDevice)
|
|
@@ -5843,7 +5924,12 @@ function inputUpdate()
|
|
|
5843
5924
|
debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5844
5925
|
debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5845
5926
|
if (touchGamepadCenterButtonSize)
|
|
5927
|
+
{
|
|
5846
5928
|
debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
|
|
5929
|
+
// exclusion bubbles around controls (where start is blocked)
|
|
5930
|
+
debugCircle(stickCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5931
|
+
debugCircle(buttonCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5932
|
+
}
|
|
5847
5933
|
}
|
|
5848
5934
|
|
|
5849
5935
|
if (!touchGamepadTimer.isSet()) return;
|
|
@@ -5950,13 +6036,6 @@ function inputUpdate()
|
|
|
5950
6036
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5951
6037
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5952
6038
|
}
|
|
5953
|
-
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
5954
|
-
{
|
|
5955
|
-
// digital style dpad from axes
|
|
5956
|
-
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
5957
|
-
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
5958
|
-
dpad.set(x, -y);
|
|
5959
|
-
}
|
|
5960
6039
|
|
|
5961
6040
|
// copy dpad to left analog stick when pressed
|
|
5962
6041
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
@@ -6162,7 +6241,7 @@ class Sound
|
|
|
6162
6241
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
6163
6242
|
this.onloadCallback = onloadCallback;
|
|
6164
6243
|
|
|
6165
|
-
if (
|
|
6244
|
+
if (isArray(asset))
|
|
6166
6245
|
{
|
|
6167
6246
|
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
6168
6247
|
const zzfxSound = asset.slice();
|
|
@@ -6254,7 +6333,7 @@ class Sound
|
|
|
6254
6333
|
}
|
|
6255
6334
|
|
|
6256
6335
|
/** Get how long this sound is in seconds
|
|
6257
|
-
* @return {number} - How long the sound is in seconds (
|
|
6336
|
+
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
6258
6337
|
*/
|
|
6259
6338
|
getDuration()
|
|
6260
6339
|
{ return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
|
|
@@ -6411,10 +6490,14 @@ class SoundInstance
|
|
|
6411
6490
|
{
|
|
6412
6491
|
if (fadeTime)
|
|
6413
6492
|
{
|
|
6414
|
-
// ramp off gain
|
|
6493
|
+
// ramp off gain from current volume (not 1, or low-volume
|
|
6494
|
+
// instances would jump back up before fading);
|
|
6495
|
+
// cancel any prior scheduling so stacked stop calls don't
|
|
6496
|
+
// re-anchor partway through a previous fade
|
|
6415
6497
|
const startFade = audioContext.currentTime;
|
|
6416
6498
|
const endFade = startFade + fadeTime;
|
|
6417
|
-
this.gainNode.gain.
|
|
6499
|
+
this.gainNode.gain.cancelScheduledValues(startFade);
|
|
6500
|
+
this.gainNode.gain.setValueAtTime(this.volume, startFade);
|
|
6418
6501
|
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
6419
6502
|
this.source.stop(endFade);
|
|
6420
6503
|
}
|
|
@@ -6462,13 +6545,14 @@ class SoundInstance
|
|
|
6462
6545
|
*/
|
|
6463
6546
|
getCurrentTime()
|
|
6464
6547
|
{
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6548
|
+
if (!this.isPlaying()) return this.pausedTime;
|
|
6549
|
+
const duration = this.getDuration();
|
|
6550
|
+
// guard mod against 0 duration (rate=0 or sound not loaded)
|
|
6551
|
+
return duration ? mod(audioContext.currentTime - this.startTime, duration) : 0;
|
|
6468
6552
|
}
|
|
6469
6553
|
|
|
6470
6554
|
/** Get the total duration of this sound
|
|
6471
|
-
* @return {number} - Total duration in seconds
|
|
6555
|
+
* @return {number} - Total duration in seconds (0 if loading)
|
|
6472
6556
|
*/
|
|
6473
6557
|
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
6474
6558
|
|
|
@@ -6492,7 +6576,7 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
6492
6576
|
{
|
|
6493
6577
|
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
6494
6578
|
if (!soundEnable || headlessMode) return;
|
|
6495
|
-
if (
|
|
6579
|
+
if (typeof speechSynthesis === 'undefined') return;
|
|
6496
6580
|
|
|
6497
6581
|
// common languages (not supported by all browsers)
|
|
6498
6582
|
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
@@ -6510,7 +6594,11 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
6510
6594
|
|
|
6511
6595
|
/** Stop all queued speech
|
|
6512
6596
|
* @memberof Audio */
|
|
6513
|
-
function speakStop()
|
|
6597
|
+
function speakStop()
|
|
6598
|
+
{
|
|
6599
|
+
if (typeof speechSynthesis !== 'undefined')
|
|
6600
|
+
speechSynthesis.cancel();
|
|
6601
|
+
}
|
|
6514
6602
|
|
|
6515
6603
|
/** Get frequency of a note on a musical scale
|
|
6516
6604
|
* @param {number} semitoneOffset - How many semitones away from the root note
|
|
@@ -6572,9 +6660,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
6572
6660
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
6573
6661
|
source.connect(pannerNode).connect(gainNode);
|
|
6574
6662
|
|
|
6575
|
-
//
|
|
6576
|
-
|
|
6577
|
-
|
|
6663
|
+
// disconnect nodes when the sound ends so the audio graph doesn't grow
|
|
6664
|
+
// unbounded across many play() calls (source.stop() also fires 'ended')
|
|
6665
|
+
source.addEventListener('ended', ()=>
|
|
6666
|
+
{
|
|
6667
|
+
gainNode.disconnect();
|
|
6668
|
+
pannerNode.disconnect();
|
|
6669
|
+
if (onended) onended(source);
|
|
6670
|
+
});
|
|
6578
6671
|
|
|
6579
6672
|
// play and return sound
|
|
6580
6673
|
const startOffset = offset * rate;
|
|
@@ -6811,14 +6904,29 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
|
6811
6904
|
* @memberof TileLayers */
|
|
6812
6905
|
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
6813
6906
|
{
|
|
6907
|
+
// check every layer and keep the closest hit so a far hit in an
|
|
6908
|
+
// earlier-registered layer doesn't shadow a closer hit in a later one
|
|
6909
|
+
let closestHit, closestDistSq, closestNormal;
|
|
6910
|
+
const scratchNormal = normal && vec2();
|
|
6814
6911
|
for (const layer of tileCollisionLayers)
|
|
6815
6912
|
{
|
|
6816
6913
|
if (!solidOnly || layer.isSolid)
|
|
6817
6914
|
{
|
|
6818
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject,
|
|
6819
|
-
if (hitPos)
|
|
6915
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, scratchNormal);
|
|
6916
|
+
if (hitPos)
|
|
6917
|
+
{
|
|
6918
|
+
const d = posStart.distanceSquared(hitPos);
|
|
6919
|
+
if (closestHit === undefined || d < closestDistSq)
|
|
6920
|
+
{
|
|
6921
|
+
closestHit = hitPos;
|
|
6922
|
+
closestDistSq = d;
|
|
6923
|
+
if (normal) closestNormal = scratchNormal.copy();
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6820
6926
|
}
|
|
6821
6927
|
}
|
|
6928
|
+
if (closestHit && normal) normal.setFrom(closestNormal);
|
|
6929
|
+
return closestHit;
|
|
6822
6930
|
}
|
|
6823
6931
|
|
|
6824
6932
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6992,38 +7100,6 @@ class CanvasLayer extends EngineObject
|
|
|
6992
7100
|
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
6993
7101
|
}
|
|
6994
7102
|
|
|
6995
|
-
/** Draw a tile onto the layer canvas in world space
|
|
6996
|
-
* @param {Vector2} pos
|
|
6997
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6998
|
-
* @param {TileInfo} [tileInfo]
|
|
6999
|
-
* @param {Color} [color=WHITE]
|
|
7000
|
-
* @param {number} [angle]
|
|
7001
|
-
* @param {boolean} [mirror] */
|
|
7002
|
-
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
7003
|
-
{
|
|
7004
|
-
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
7005
|
-
size = size.multiply(this.tileInfo.size);
|
|
7006
|
-
pos.y = this.canvas.height - pos.y;
|
|
7007
|
-
|
|
7008
|
-
// draw the tile onto the layer canvas
|
|
7009
|
-
const oldMainCanvasSize = mainCanvasSize;
|
|
7010
|
-
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
7011
|
-
const useWebGL = this.hasWebGL();
|
|
7012
|
-
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
7013
|
-
const drawContext = useWebGL ? undefined : this.context;
|
|
7014
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
7015
|
-
useWebGL && glSetRenderTarget();
|
|
7016
|
-
mainCanvasSize = oldMainCanvasSize;
|
|
7017
|
-
}
|
|
7018
|
-
|
|
7019
|
-
/** Draw a rectangle onto the layer canvas in world space
|
|
7020
|
-
* @param {Vector2} pos
|
|
7021
|
-
* @param {Vector2} [size=vec2(1)]
|
|
7022
|
-
* @param {Color} [color=WHITE]
|
|
7023
|
-
* @param {number} [angle] */
|
|
7024
|
-
drawRect(pos, size, color, angle)
|
|
7025
|
-
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
7026
|
-
|
|
7027
7103
|
/** Create WebGL texture if necessary and copy layer canvas to it */
|
|
7028
7104
|
updateWebGL()
|
|
7029
7105
|
{ this.textureInfo.createWebGLTexture(); }
|
|
@@ -7079,6 +7155,8 @@ class TileLayer extends CanvasLayer
|
|
|
7079
7155
|
this.redrawTileData = ()=> {};
|
|
7080
7156
|
this.drawLayerTile = ()=> {};
|
|
7081
7157
|
this.drawLayerRect = ()=> {};
|
|
7158
|
+
this.drawTile = ()=> {};
|
|
7159
|
+
this.drawRect = ()=> {};
|
|
7082
7160
|
this.clearLayerRect = ()=> {};
|
|
7083
7161
|
return;
|
|
7084
7162
|
}
|
|
@@ -7106,7 +7184,7 @@ class TileLayer extends CanvasLayer
|
|
|
7106
7184
|
ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
|
|
7107
7185
|
|
|
7108
7186
|
if (!layerPos.arrayCheck(this.size)) return;
|
|
7109
|
-
this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
|
|
7187
|
+
this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] = data;
|
|
7110
7188
|
|
|
7111
7189
|
if (!redraw) return;
|
|
7112
7190
|
const isRedraw = drawContext === this.context;
|
|
@@ -7121,11 +7199,11 @@ class TileLayer extends CanvasLayer
|
|
|
7121
7199
|
|
|
7122
7200
|
/** Get data at a given position in the array
|
|
7123
7201
|
* @param {Vector2} layerPos - Local position in array
|
|
7124
|
-
* @return {TileLayerData} */
|
|
7202
|
+
* @return {TileLayerData|undefined} */
|
|
7125
7203
|
getData(layerPos)
|
|
7126
|
-
{
|
|
7204
|
+
{
|
|
7127
7205
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
7128
|
-
return layerPos.arrayCheck(this.size)
|
|
7206
|
+
return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
|
|
7129
7207
|
}
|
|
7130
7208
|
|
|
7131
7209
|
// Update the tile layer, refresh texture if needed
|
|
@@ -7233,7 +7311,7 @@ class TileLayer extends CanvasLayer
|
|
|
7233
7311
|
|
|
7234
7312
|
// draw the tile if it has layer data
|
|
7235
7313
|
const d = this.getData(layerPos);
|
|
7236
|
-
if (!d.tile) return;
|
|
7314
|
+
if (!d || !d.tile) return;
|
|
7237
7315
|
|
|
7238
7316
|
const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
|
|
7239
7317
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -7279,6 +7357,38 @@ class TileLayer extends CanvasLayer
|
|
|
7279
7357
|
drawLayerRect(pos, size, color, angle=0)
|
|
7280
7358
|
{ this.drawLayerTile(pos, size, undefined, color, angle); }
|
|
7281
7359
|
|
|
7360
|
+
/** Draw a tile onto the layer canvas in world space
|
|
7361
|
+
* @param {Vector2} pos
|
|
7362
|
+
* @param {Vector2} [size=vec2(1)]
|
|
7363
|
+
* @param {TileInfo} [tileInfo]
|
|
7364
|
+
* @param {Color} [color=WHITE]
|
|
7365
|
+
* @param {number} [angle]
|
|
7366
|
+
* @param {boolean} [mirror] */
|
|
7367
|
+
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
7368
|
+
{
|
|
7369
|
+
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
7370
|
+
size = size.multiply(this.tileInfo.size);
|
|
7371
|
+
pos.y = this.canvas.height - pos.y;
|
|
7372
|
+
|
|
7373
|
+
// draw the tile onto the layer canvas
|
|
7374
|
+
const oldMainCanvasSize = mainCanvasSize;
|
|
7375
|
+
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
7376
|
+
const useWebGL = this.hasWebGL();
|
|
7377
|
+
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
7378
|
+
const drawContext = useWebGL ? undefined : this.context;
|
|
7379
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
7380
|
+
useWebGL && glSetRenderTarget();
|
|
7381
|
+
mainCanvasSize = oldMainCanvasSize;
|
|
7382
|
+
}
|
|
7383
|
+
|
|
7384
|
+
/** Draw a rectangle onto the layer canvas in world space
|
|
7385
|
+
* @param {Vector2} pos
|
|
7386
|
+
* @param {Vector2} [size=vec2(1)]
|
|
7387
|
+
* @param {Color} [color=WHITE]
|
|
7388
|
+
* @param {number} [angle] */
|
|
7389
|
+
drawRect(pos, size, color, angle)
|
|
7390
|
+
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
7391
|
+
|
|
7282
7392
|
/** Clear a rectangle in layer space
|
|
7283
7393
|
* @param {Vector2} pos - position in pixel coordinates
|
|
7284
7394
|
* @param {Vector2} size
|
|
@@ -7357,7 +7467,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
7357
7467
|
setCollisionData(layerPos, data=1)
|
|
7358
7468
|
{
|
|
7359
7469
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
7360
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
7470
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
7361
7471
|
layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
|
|
7362
7472
|
}
|
|
7363
7473
|
|
|
@@ -7372,7 +7482,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
7372
7482
|
getCollisionData(layerPos)
|
|
7373
7483
|
{
|
|
7374
7484
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
7375
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
7485
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
7376
7486
|
return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
|
|
7377
7487
|
}
|
|
7378
7488
|
|
|
@@ -7397,8 +7507,10 @@ class TileCollisionLayer extends TileLayer
|
|
|
7397
7507
|
const posY = pos.y - this.pos.y;
|
|
7398
7508
|
const minX = max(posX - size.x/2|0, 0);
|
|
7399
7509
|
const minY = max(posY - size.y/2|0, 0);
|
|
7400
|
-
|
|
7401
|
-
|
|
7510
|
+
// ensure at least one cell is visited even when size is 0 and pos
|
|
7511
|
+
// lands exactly on an integer boundary (documented point-test mode)
|
|
7512
|
+
const maxX = min(max(posX + size.x/2, minX + 1), this.size.x);
|
|
7513
|
+
const maxY = min(max(posY + size.y/2, minY + 1), this.size.y);
|
|
7402
7514
|
const hitPos = new Vector2;
|
|
7403
7515
|
for (let y = minY; y < maxY; ++y)
|
|
7404
7516
|
for (let x = minX; x < maxX; ++x)
|
|
@@ -7517,7 +7629,7 @@ class ParticleEmitter extends EngineObject
|
|
|
7517
7629
|
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
7518
7630
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
7519
7631
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
7520
|
-
* @param {number} [fadeRate] -
|
|
7632
|
+
* @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
|
|
7521
7633
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
7522
7634
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
7523
7635
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
@@ -7601,7 +7713,7 @@ class ParticleEmitter extends EngineObject
|
|
|
7601
7713
|
this.gravityScale = gravityScale;
|
|
7602
7714
|
/** @property {number} - Cone for start particle angle */
|
|
7603
7715
|
this.particleConeAngle = particleConeAngle;
|
|
7604
|
-
/** @property {number} -
|
|
7716
|
+
/** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
|
|
7605
7717
|
this.fadeRate = fadeRate;
|
|
7606
7718
|
/** @property {number} - Apply extra randomness percent */
|
|
7607
7719
|
this.randomness = randomness;
|
|
@@ -7879,33 +7991,32 @@ class Particle
|
|
|
7879
7991
|
const hitLayer = tileCollisionTest(this.pos);
|
|
7880
7992
|
if (!testCollision(oldPos))
|
|
7881
7993
|
{
|
|
7882
|
-
|
|
7994
|
+
// testCollision already invoked collideCallback with the
|
|
7995
|
+
// correct (this, data, pos) args; no need to re-check here.
|
|
7996
|
+
// test which side we bounced off (or both if a corner)
|
|
7997
|
+
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
7998
|
+
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
7999
|
+
const hitRestitution = max(restitution, hitLayer.restitution);
|
|
8000
|
+
const hitFriction = max(friction, hitLayer.friction);
|
|
8001
|
+
if (isBlockedX)
|
|
7883
8002
|
{
|
|
7884
|
-
//
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
const hitFriction = max(friction, hitLayer.friction);
|
|
7889
|
-
if (isBlockedX)
|
|
7890
|
-
{
|
|
7891
|
-
// move to previous X position and bounce
|
|
7892
|
-
this.pos.x = oldPos.x;
|
|
7893
|
-
this.velocity.x *= -hitRestitution;
|
|
7894
|
-
this.velocity.y *= hitFriction;
|
|
7895
|
-
}
|
|
7896
|
-
if (isBlockedY || !isBlockedX)
|
|
7897
|
-
{
|
|
7898
|
-
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
7899
|
-
if (wasFalling)
|
|
7900
|
-
this.groundObject = hitLayer;
|
|
7901
|
-
|
|
7902
|
-
// move to previous Y position and bounce
|
|
7903
|
-
this.pos.y = oldPos.y;
|
|
7904
|
-
this.velocity.y *= -hitRestitution;
|
|
7905
|
-
this.velocity.x *= hitFriction;
|
|
7906
|
-
}
|
|
7907
|
-
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
8003
|
+
// move to previous X position and bounce
|
|
8004
|
+
this.pos.x = oldPos.x;
|
|
8005
|
+
this.velocity.x *= -hitRestitution;
|
|
8006
|
+
this.velocity.y *= hitFriction;
|
|
7908
8007
|
}
|
|
8008
|
+
if (isBlockedY || !isBlockedX)
|
|
8009
|
+
{
|
|
8010
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
8011
|
+
if (wasFalling)
|
|
8012
|
+
this.groundObject = hitLayer;
|
|
8013
|
+
|
|
8014
|
+
// move to previous Y position and bounce
|
|
8015
|
+
this.pos.y = oldPos.y;
|
|
8016
|
+
this.velocity.y *= -hitRestitution;
|
|
8017
|
+
this.velocity.x *= hitFriction;
|
|
8018
|
+
}
|
|
8019
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
7909
8020
|
}
|
|
7910
8021
|
}
|
|
7911
8022
|
}
|
|
@@ -8066,6 +8177,10 @@ function glInit(rootElement)
|
|
|
8066
8177
|
for (const info of glTextureInfos)
|
|
8067
8178
|
info.glTexture = undefined;
|
|
8068
8179
|
glActiveTexture = undefined;
|
|
8180
|
+
// drop any partially-filled batch so the next glFlush doesn't
|
|
8181
|
+
// upload stale glBatchCount against fresh empty buffers on restore
|
|
8182
|
+
glBatchCount = 0;
|
|
8183
|
+
glPolyMode = false;
|
|
8069
8184
|
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
8070
8185
|
});
|
|
8071
8186
|
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
@@ -8424,6 +8539,10 @@ function glSetTextureData(texture, image)
|
|
|
8424
8539
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
8425
8540
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
8426
8541
|
|
|
8542
|
+
// keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
|
|
8543
|
+
if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
|
|
8544
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
8545
|
+
|
|
8427
8546
|
// rebind active texture
|
|
8428
8547
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
8429
8548
|
}
|
|
@@ -8469,7 +8588,7 @@ function glFlush()
|
|
|
8469
8588
|
{
|
|
8470
8589
|
if (glEnable && glContext && glBatchCount)
|
|
8471
8590
|
{
|
|
8472
|
-
// set
|
|
8591
|
+
// set blend mode
|
|
8473
8592
|
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
8474
8593
|
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
8475
8594
|
glContext.enable(glContext.BLEND);
|
|
@@ -8546,9 +8665,9 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
8546
8665
|
}
|
|
8547
8666
|
|
|
8548
8667
|
/** Add an untextured rect to the gl draw list
|
|
8549
|
-
*
|
|
8550
|
-
*
|
|
8551
|
-
*
|
|
8668
|
+
* Zeroes the uvs and rgba so the texture contribution multiplies to 0,
|
|
8669
|
+
* then carries the real color in the additive slot. Works regardless of
|
|
8670
|
+
* which texture is currently bound.
|
|
8552
8671
|
* @param {number} x
|
|
8553
8672
|
* @param {number} y
|
|
8554
8673
|
* @param {number} sizeX
|
|
@@ -8558,52 +8677,7 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
8558
8677
|
* @memberof WebGL */
|
|
8559
8678
|
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
8560
8679
|
{
|
|
8561
|
-
|
|
8562
|
-
{
|
|
8563
|
-
// batch with surrounding polys as a 4-vertex tristrip rect
|
|
8564
|
-
const vertCount = 6; // 4 corners + 2 degenerate verts
|
|
8565
|
-
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8566
|
-
glFlush();
|
|
8567
|
-
|
|
8568
|
-
// compute rotated corners in world space (matches glDrawPointsTransform rotation)
|
|
8569
|
-
const hx = sizeX*.5, hy = sizeY*.5;
|
|
8570
|
-
const c = cos(angle), s = sin(angle);
|
|
8571
|
-
const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
|
|
8572
|
-
const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
|
|
8573
|
-
const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
|
|
8574
|
-
const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
|
|
8575
|
-
const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
|
|
8576
|
-
|
|
8577
|
-
// write tristrip with leading/trailing degenerate verts
|
|
8578
|
-
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
8579
|
-
glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
|
|
8580
|
-
glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
|
|
8581
|
-
glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
|
|
8582
|
-
glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
|
|
8583
|
-
glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
|
|
8584
|
-
glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
|
|
8585
|
-
glBatchCount += vertCount;
|
|
8586
|
-
return;
|
|
8587
|
-
}
|
|
8588
|
-
|
|
8589
|
-
// instanced path: zero uvs and rgba so the texture contribution is killed,
|
|
8590
|
-
// then carry the real color in the additive slot
|
|
8591
|
-
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
|
|
8592
|
-
glFlush();
|
|
8593
|
-
glSetInstancedMode();
|
|
8594
|
-
|
|
8595
|
-
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
8596
|
-
glPositionData[offset++] = x;
|
|
8597
|
-
glPositionData[offset++] = y;
|
|
8598
|
-
glPositionData[offset++] = sizeX;
|
|
8599
|
-
glPositionData[offset++] = sizeY;
|
|
8600
|
-
glPositionData[offset++] = 0;
|
|
8601
|
-
glPositionData[offset++] = 0;
|
|
8602
|
-
glPositionData[offset++] = 0;
|
|
8603
|
-
glPositionData[offset++] = 0;
|
|
8604
|
-
glColorData[offset++] = 0;
|
|
8605
|
-
glColorData[offset++] = rgba;
|
|
8606
|
-
glPositionData[offset++] = angle;
|
|
8680
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
8607
8681
|
}
|
|
8608
8682
|
|
|
8609
8683
|
/** Transform and add a polygon to the gl draw list
|
|
@@ -8619,13 +8693,13 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
|
8619
8693
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
8620
8694
|
{
|
|
8621
8695
|
const pointsOut = [];
|
|
8696
|
+
const sa = sin(-angle);
|
|
8697
|
+
const ca = cos(-angle);
|
|
8622
8698
|
for (const p of points)
|
|
8623
8699
|
{
|
|
8624
8700
|
// transform the point
|
|
8625
8701
|
const px = p.x*sx;
|
|
8626
8702
|
const py = p.y*sy;
|
|
8627
|
-
const sa = sin(-angle);
|
|
8628
|
-
const ca = cos(-angle);
|
|
8629
8703
|
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
8630
8704
|
}
|
|
8631
8705
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
@@ -8657,11 +8731,13 @@ function glDrawPoints(points, rgba)
|
|
|
8657
8731
|
{
|
|
8658
8732
|
if (!glEnable || points.length < 3)
|
|
8659
8733
|
return; // needs at least 3 points to have area
|
|
8660
|
-
|
|
8734
|
+
|
|
8661
8735
|
// flush if there is not enough room or if different blend mode
|
|
8662
8736
|
const vertCount = points.length + 2;
|
|
8663
8737
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8664
8738
|
glFlush();
|
|
8739
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
8740
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
8665
8741
|
glSetPolyMode();
|
|
8666
8742
|
|
|
8667
8743
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -8685,11 +8761,13 @@ function glDrawColoredPoints(points, pointColors)
|
|
|
8685
8761
|
{
|
|
8686
8762
|
if (!glEnable || points.length < 3)
|
|
8687
8763
|
return; // needs at least 3 points to have area
|
|
8688
|
-
|
|
8764
|
+
|
|
8689
8765
|
// flush if there is not enough room or if different blend mode
|
|
8690
8766
|
const vertCount = points.length + 2;
|
|
8691
8767
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8692
8768
|
glFlush();
|
|
8769
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
8770
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
8693
8771
|
glSetPolyMode();
|
|
8694
8772
|
|
|
8695
8773
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -8759,7 +8837,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
8759
8837
|
const strip = [];
|
|
8760
8838
|
const n = points.length;
|
|
8761
8839
|
const e = 1e-6;
|
|
8762
|
-
|
|
8840
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
8841
|
+
const miterLimit = 10;
|
|
8763
8842
|
for (let i = 0; i < n; i++)
|
|
8764
8843
|
{
|
|
8765
8844
|
// for each vertex, calculate normal based on adjacent edges
|
|
@@ -9015,7 +9094,7 @@ function drawEngineLogo(t)
|
|
|
9015
9094
|
x.closePath();
|
|
9016
9095
|
gradient(0, Y, 0, Y+H,C);
|
|
9017
9096
|
}
|
|
9018
|
-
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}
|
|
9097
|
+
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%)`:'#000';
|
|
9019
9098
|
|
|
9020
9099
|
// center and fit to screen
|
|
9021
9100
|
const alpha = oscillate(1,1,t);
|
|
@@ -9425,8 +9504,20 @@ class NewgroundsPlugin
|
|
|
9425
9504
|
|
|
9426
9505
|
// get medals
|
|
9427
9506
|
const medalsResult = this.call('Medal.getList');
|
|
9507
|
+
|
|
9508
|
+
// bail early if the first call failed (offline / bad session /
|
|
9509
|
+
// server error) so we don't block the main thread on more sync
|
|
9510
|
+
// XHRs that are guaranteed to also fail
|
|
9511
|
+
if (!medalsResult || !medalsResult.result || medalsResult.result.error)
|
|
9512
|
+
{
|
|
9513
|
+
debugMedals && LOG('Newgrounds session unavailable; skipping plugin init');
|
|
9514
|
+
this.medals = [];
|
|
9515
|
+
this.scoreboards = [];
|
|
9516
|
+
return;
|
|
9517
|
+
}
|
|
9518
|
+
|
|
9428
9519
|
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
9429
|
-
this.medals = medalsResult
|
|
9520
|
+
this.medals = medalsResult.result.data?.['medals'] || [];
|
|
9430
9521
|
debugMedals && LOG(this.medals);
|
|
9431
9522
|
for (const newgroundsMedal of this.medals)
|
|
9432
9523
|
{
|
|
@@ -9446,11 +9537,11 @@ class NewgroundsPlugin
|
|
|
9446
9537
|
medal.description = medal.description + ` (${ medal.value })`;
|
|
9447
9538
|
}
|
|
9448
9539
|
}
|
|
9449
|
-
|
|
9540
|
+
|
|
9450
9541
|
// get scoreboards
|
|
9451
9542
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
9452
9543
|
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
9453
|
-
this.scoreboards = scoreboardResult
|
|
9544
|
+
this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
|
|
9454
9545
|
debugMedals && LOG(this.scoreboards);
|
|
9455
9546
|
|
|
9456
9547
|
// keep the session alive with a ping every minute
|
|
@@ -9634,10 +9725,14 @@ class PostProcessPlugin
|
|
|
9634
9725
|
function postProcessRender()
|
|
9635
9726
|
{
|
|
9636
9727
|
if (headlessMode || !glEnable) return;
|
|
9637
|
-
|
|
9728
|
+
|
|
9638
9729
|
// clear out the buffer
|
|
9639
9730
|
glFlush();
|
|
9640
9731
|
|
|
9732
|
+
// ensure we render to the default framebuffer (in case any earlier
|
|
9733
|
+
// caller this frame left a render target bound)
|
|
9734
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
9735
|
+
|
|
9641
9736
|
// setup shader program to draw a quad
|
|
9642
9737
|
glContext.useProgram(postProcess.shader);
|
|
9643
9738
|
glContext.bindVertexArray(postProcess.vao);
|
|
@@ -9678,6 +9773,9 @@ class PostProcessPlugin
|
|
|
9678
9773
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
|
|
9679
9774
|
}
|
|
9680
9775
|
|
|
9776
|
+
// restore default so subsequent dynamic texture uploads aren't flipped
|
|
9777
|
+
glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
|
|
9778
|
+
|
|
9681
9779
|
// force it to set instanced mode
|
|
9682
9780
|
glSetInstancedMode(true);
|
|
9683
9781
|
}
|
|
@@ -10304,11 +10402,17 @@ class UISystemPlugin
|
|
|
10304
10402
|
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
10305
10403
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
10306
10404
|
{
|
|
10307
|
-
|
|
10405
|
+
// remove any prior listeners so repeated setup calls don't stack
|
|
10406
|
+
if (this._dragListeners)
|
|
10407
|
+
for (const [type, listener] of this._dragListeners)
|
|
10408
|
+
document.removeEventListener(type, listener);
|
|
10409
|
+
this._dragListeners = [];
|
|
10410
|
+
const setCallback = (callback, listenerType)=>
|
|
10308
10411
|
{
|
|
10309
|
-
|
|
10412
|
+
const listener = (e)=> { e.preventDefault(); callback && callback(e); };
|
|
10310
10413
|
document.addEventListener(listenerType, listener);
|
|
10311
|
-
|
|
10414
|
+
this._dragListeners.push([listenerType, listener]);
|
|
10415
|
+
};
|
|
10312
10416
|
setCallback(onDrop, 'drop');
|
|
10313
10417
|
setCallback(onDragEnter, 'dragenter');
|
|
10314
10418
|
setCallback(onDragLeave, 'dragleave');
|
|
@@ -10632,6 +10736,15 @@ class UIObject
|
|
|
10632
10736
|
if (this.destroyed)
|
|
10633
10737
|
return;
|
|
10634
10738
|
|
|
10739
|
+
// clear ui-system references that point at this object so events
|
|
10740
|
+
// don't keep firing against a destroyed target (especially the
|
|
10741
|
+
// keydown listener attached for keyInputObject)
|
|
10742
|
+
if (uiSystem.activeObject === this) uiSystem.activeObject = undefined;
|
|
10743
|
+
if (uiSystem.hoverObject === this) uiSystem.hoverObject = undefined;
|
|
10744
|
+
if (uiSystem.lastHoverObject === this) uiSystem.lastHoverObject = undefined;
|
|
10745
|
+
if (uiSystem.navigationObject === this) uiSystem.navigationObject = undefined;
|
|
10746
|
+
if (uiSystem.keyInputObject === this) uiSystem.keyInputObject = undefined;
|
|
10747
|
+
|
|
10635
10748
|
// disconnect from parent and destroy children
|
|
10636
10749
|
this.destroyed = 1;
|
|
10637
10750
|
this.parent?.removeChild(this);
|
|
@@ -10640,6 +10753,8 @@ class UIObject
|
|
|
10640
10753
|
child.parent = undefined;
|
|
10641
10754
|
child.destroy();
|
|
10642
10755
|
}
|
|
10756
|
+
// clear references so destroyed children can be GC'd
|
|
10757
|
+
this.children.length = 0;
|
|
10643
10758
|
}
|
|
10644
10759
|
|
|
10645
10760
|
/** Check if the mouse is overlapping this ui object
|
|
@@ -11229,6 +11344,7 @@ class UISlider extends UIObject
|
|
|
11229
11344
|
{
|
|
11230
11345
|
// toggle value between 0 and 1
|
|
11231
11346
|
this.value = this.value ? 0 : 1;
|
|
11347
|
+
this.onChange();
|
|
11232
11348
|
this.onRelease();
|
|
11233
11349
|
super.navigatePressed();
|
|
11234
11350
|
}
|
|
@@ -11588,6 +11704,11 @@ class Box2dObject extends EngineObject
|
|
|
11588
11704
|
// destroy physics body, fixtures, and joints
|
|
11589
11705
|
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
11590
11706
|
box2d.world.DestroyBody(this.body);
|
|
11707
|
+
|
|
11708
|
+
// remove from tracked list so paused / headless sessions don't leak
|
|
11709
|
+
const i = box2d.objects.indexOf(this);
|
|
11710
|
+
if (i >= 0)
|
|
11711
|
+
box2d.objects.splice(i, 1);
|
|
11591
11712
|
super.destroy();
|
|
11592
11713
|
}
|
|
11593
11714
|
|
|
@@ -11676,7 +11797,9 @@ class Box2dObject extends EngineObject
|
|
|
11676
11797
|
/** Add a box shape to the body
|
|
11677
11798
|
* @param {Vector2} [size]
|
|
11678
11799
|
* @param {Vector2} [offset]
|
|
11679
|
-
* @param {number} [angle]
|
|
11800
|
+
* @param {number} [angle] - LittleJS convention (clockwise positive).
|
|
11801
|
+
* Negated internally to match Box2D's CCW-positive convention so the
|
|
11802
|
+
* fixture aligns with the same angle passed to drawRect/drawTile.
|
|
11680
11803
|
* @param {number} [density]
|
|
11681
11804
|
* @param {number} [friction]
|
|
11682
11805
|
* @param {number} [restitution]
|
|
@@ -11689,7 +11812,7 @@ class Box2dObject extends EngineObject
|
|
|
11689
11812
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
11690
11813
|
|
|
11691
11814
|
const shape = new box2d.instance.b2PolygonShape();
|
|
11692
|
-
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
11815
|
+
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), -angle);
|
|
11693
11816
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
11694
11817
|
}
|
|
11695
11818
|
|
|
@@ -11991,9 +12114,10 @@ class Box2dObject extends EngineObject
|
|
|
11991
12114
|
{
|
|
11992
12115
|
const data = new box2d.instance.b2MassData();
|
|
11993
12116
|
this.body.GetMassData(data);
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
12117
|
+
// use !== undefined so setMass(0) (static-equivalent) isn't silently ignored
|
|
12118
|
+
if (localCenter !== undefined) data.set_center(box2d.vec2dTo(localCenter));
|
|
12119
|
+
if (mass !== undefined) data.set_mass(mass);
|
|
12120
|
+
if (momentOfInertia !== undefined) data.set_I(momentOfInertia);
|
|
11997
12121
|
this.body.SetMassData(data);
|
|
11998
12122
|
}
|
|
11999
12123
|
|
|
@@ -13165,6 +13289,8 @@ class Box2dPlugin
|
|
|
13165
13289
|
const fixtureB = contact.GetFixtureB();
|
|
13166
13290
|
const objectA = fixtureA.GetBody().object;
|
|
13167
13291
|
const objectB = fixtureB.GetBody().object;
|
|
13292
|
+
// raw user-created b2Bodies may have no .object — skip those
|
|
13293
|
+
if (!objectA || !objectB) return;
|
|
13168
13294
|
objectA.beginContact(objectB);
|
|
13169
13295
|
objectB.beginContact(objectA);
|
|
13170
13296
|
}
|
|
@@ -13175,6 +13301,7 @@ class Box2dPlugin
|
|
|
13175
13301
|
const fixtureB = contact.GetFixtureB();
|
|
13176
13302
|
const objectA = fixtureA.GetBody().object;
|
|
13177
13303
|
const objectB = fixtureB.GetBody().object;
|
|
13304
|
+
if (!objectA || !objectB) return;
|
|
13178
13305
|
objectA.endContact(objectB);
|
|
13179
13306
|
objectB.endContact(objectA);
|
|
13180
13307
|
};
|
|
@@ -13553,7 +13680,7 @@ async function box2dInit()
|
|
|
13553
13680
|
debugDraw.DrawTransform = function(transform)
|
|
13554
13681
|
{
|
|
13555
13682
|
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
|
|
13556
|
-
const pos =
|
|
13683
|
+
const pos = box2d.vec2From(transform.get_p());
|
|
13557
13684
|
const angle = -transform.get_q().GetAngle();
|
|
13558
13685
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
13559
13686
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
|
@@ -13957,7 +14084,7 @@ const Ease =
|
|
|
13957
14084
|
* @param {number} x
|
|
13958
14085
|
* @returns {number}
|
|
13959
14086
|
* @memberof TweenSystem */
|
|
13960
|
-
EXPO: (x) => 2 ** (10 * x - 10),
|
|
14087
|
+
EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
|
|
13961
14088
|
|
|
13962
14089
|
/** Back ease-in: overshoots backward at the start before snapping forward.
|
|
13963
14090
|
* @param {number} x
|
|
@@ -13970,6 +14097,8 @@ const Ease =
|
|
|
13970
14097
|
* @returns {number}
|
|
13971
14098
|
* @memberof TweenSystem */
|
|
13972
14099
|
ELASTIC: (x) =>
|
|
14100
|
+
x === 0 ? 0 :
|
|
14101
|
+
x === 1 ? 1 :
|
|
13973
14102
|
-(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
|
|
13974
14103
|
|
|
13975
14104
|
/** Spring-like ease-out: oscillates outward after passing the target.
|
|
@@ -14130,29 +14259,32 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
14130
14259
|
}
|
|
14131
14260
|
|
|
14132
14261
|
// Continuation that schedules the next loop iteration when one finishes.
|
|
14133
|
-
//
|
|
14134
|
-
//
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14143
|
-
|
|
14144
|
-
|
|
14145
|
-
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
|
|
14155
|
-
|
|
14262
|
+
// Reuses the same Tween object across iterations so the user's handle
|
|
14263
|
+
// from `.loop()` keeps working — calling `.stop()` mid-loop now cancels
|
|
14264
|
+
// the entire chain instead of just the current iteration.
|
|
14265
|
+
function loopContinuation(tween)
|
|
14266
|
+
{
|
|
14267
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
14268
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
14269
|
+
tween.life = tween.duration;
|
|
14270
|
+
tween.thenCallback = () => loopContinuation(tween);
|
|
14271
|
+
tweenActive.push(tween);
|
|
14272
|
+
// snap to start for the new iteration (matches Tween constructor behavior)
|
|
14273
|
+
tween.callback(tween.interp(tween.duration));
|
|
14274
|
+
}
|
|
14275
|
+
|
|
14276
|
+
// Continuation for pingPong: swaps start and end on the same tween each iteration.
|
|
14277
|
+
function pingPongContinuation(tween)
|
|
14278
|
+
{
|
|
14279
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
14280
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
14281
|
+
const tmp = tween.start;
|
|
14282
|
+
tween.start = tween.end;
|
|
14283
|
+
tween.end = tmp;
|
|
14284
|
+
tween.life = tween.duration;
|
|
14285
|
+
tween.thenCallback = () => pingPongContinuation(tween);
|
|
14286
|
+
tweenActive.push(tween);
|
|
14287
|
+
tween.callback(tween.interp(tween.duration));
|
|
14156
14288
|
}
|
|
14157
14289
|
|
|
14158
14290
|
/** Engine plugin hook: advance every active tween by the appropriate delta.
|
|
@@ -14486,11 +14618,13 @@ class PathFinder
|
|
|
14486
14618
|
if (dx !== 0 && dy !== 0)
|
|
14487
14619
|
{
|
|
14488
14620
|
// Diagonal step: refuse if either cardinal neighbor is
|
|
14489
|
-
// blocked
|
|
14621
|
+
// blocked. Prevents cutting through walls at corners.
|
|
14622
|
+
// (Costed-but-walkable cardinals do not block — diagonal
|
|
14623
|
+
// movement around expensive terrain is standard A*.)
|
|
14490
14624
|
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
14491
|
-
if (!card1 ||
|
|
14625
|
+
if (!card1 || !card1.walkable) continue;
|
|
14492
14626
|
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
14493
|
-
if (!card2 ||
|
|
14627
|
+
if (!card2 || !card2.walkable) continue;
|
|
14494
14628
|
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
14495
14629
|
}
|
|
14496
14630
|
|
|
@@ -15142,6 +15276,7 @@ export
|
|
|
15142
15276
|
setTouchGamepadAnalog,
|
|
15143
15277
|
setTouchGamepadSize,
|
|
15144
15278
|
setTouchGamepadAlpha,
|
|
15279
|
+
setTouchGamepadDisplayTime,
|
|
15145
15280
|
setVibrateEnable,
|
|
15146
15281
|
setSoundEnable,
|
|
15147
15282
|
setSoundVolume,
|
|
@@ -15262,14 +15397,15 @@ export
|
|
|
15262
15397
|
drawRegularPoly,
|
|
15263
15398
|
drawEllipse,
|
|
15264
15399
|
drawCircle,
|
|
15400
|
+
drawEllipseGradient,
|
|
15265
15401
|
drawCircleGradient,
|
|
15266
15402
|
drawCanvas2D,
|
|
15267
15403
|
drawText,
|
|
15268
15404
|
drawTextScreen,
|
|
15269
15405
|
setBlendMode,
|
|
15270
15406
|
combineCanvases,
|
|
15271
|
-
|
|
15272
|
-
|
|
15407
|
+
engineImageFont,
|
|
15408
|
+
ImageFont,
|
|
15273
15409
|
isFullscreen,
|
|
15274
15410
|
toggleFullscreen,
|
|
15275
15411
|
setCursor,
|
|
@@ -15378,16 +15514,16 @@ export
|
|
|
15378
15514
|
// Medals
|
|
15379
15515
|
medals,
|
|
15380
15516
|
medalsPreventUnlock,
|
|
15381
|
-
medalsInit,
|
|
15382
|
-
medalsForEach,
|
|
15383
|
-
Medal,
|
|
15384
15517
|
medalDisplayTime,
|
|
15385
15518
|
medalDisplaySlideTime,
|
|
15386
15519
|
medalDisplaySize,
|
|
15520
|
+
medalsInit,
|
|
15521
|
+
medalsForEach,
|
|
15387
15522
|
setMedalDisplayTime,
|
|
15388
15523
|
setMedalDisplaySlideTime,
|
|
15389
15524
|
setMedalDisplaySize,
|
|
15390
15525
|
setMedalsPreventUnlock,
|
|
15526
|
+
Medal,
|
|
15391
15527
|
|
|
15392
15528
|
// Newgrounds
|
|
15393
15529
|
newgrounds,
|
|
@@ -15400,6 +15536,7 @@ export
|
|
|
15400
15536
|
|
|
15401
15537
|
// ZzFXMusic
|
|
15402
15538
|
ZzFXMusic,
|
|
15539
|
+
zzfxM,
|
|
15403
15540
|
|
|
15404
15541
|
// UI System
|
|
15405
15542
|
uiSystem,
|