littlejsengine 1.18.7 → 1.18.12
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/README.md +2 -4
- package/dist/littlejs.d.ts +76 -8
- package/dist/littlejs.esm.js +333 -73
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +327 -72
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +317 -64
- package/package.json +1 -1
- package/plugins/medalSystem.js +2 -1
- package/plugins/newgrounds.js +7 -0
- package/plugins/pathFinder.js +35 -6
- package/plugins/tweenSystem.js +9 -1
- package/plugins/uiSystem.js +36 -14
- package/src/engine.js +3 -2
- package/src/engineAudio.js +7 -9
- package/src/engineDebug.js +10 -8
- package/src/engineDraw.js +94 -14
- package/src/engineExport.js +6 -1
- package/src/engineInput.js +7 -7
- package/src/engineMath.js +5 -3
- package/src/engineUtilities.js +49 -6
- package/src/engineWebGL.js +63 -1
- package/plugins/tween.js +0 -509
package/dist/littlejs.release.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.12';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -207,7 +207,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
207
207
|
const combinedScale = timeScale * debugScale;
|
|
208
208
|
frameTimeDeltaMS *= combinedScale;
|
|
209
209
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
210
|
-
if (
|
|
210
|
+
if (combinedScale <= 1)
|
|
211
211
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
212
212
|
|
|
213
213
|
let wasUpdated = false;
|
|
@@ -294,6 +294,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
294
294
|
glFlush();
|
|
295
295
|
debugRenderPost();
|
|
296
296
|
drawCount = 0;
|
|
297
|
+
primitiveCount = 0;
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -900,13 +901,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
|
|
|
900
901
|
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
901
902
|
|
|
902
903
|
/**
|
|
903
|
-
* Check if
|
|
904
|
+
* Check if a value is stringifiable — i.e. it has a toString that returns
|
|
905
|
+
* a string. Use this for ASSERTs and inputs that will be coerced to text;
|
|
906
|
+
* use `typeof x === 'string'` inline if you need strict-string semantics.
|
|
904
907
|
* - Returns true for strings, numbers, and most objects
|
|
905
908
|
* - Returns false for null and undefined
|
|
906
909
|
* @param {any} s
|
|
907
910
|
* @return {boolean}
|
|
908
911
|
* @memberof Math */
|
|
909
|
-
function
|
|
912
|
+
function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
|
|
910
913
|
|
|
911
914
|
/**
|
|
912
915
|
* Check if object is an array
|
|
@@ -1650,7 +1653,7 @@ class Color
|
|
|
1650
1653
|
* @return {Color} */
|
|
1651
1654
|
setHex(hex)
|
|
1652
1655
|
{
|
|
1653
|
-
ASSERT(
|
|
1656
|
+
ASSERT(isStringLike(hex), 'Color hex code must be a string');
|
|
1654
1657
|
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
1655
1658
|
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
1656
1659
|
|
|
@@ -1767,6 +1770,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
1767
1770
|
* - File saving (text, canvas, data URLs)
|
|
1768
1771
|
* - Native share dialog support
|
|
1769
1772
|
* - Local storage save data management
|
|
1773
|
+
* - Gradient noise (1D and 2D)
|
|
1770
1774
|
* @namespace Utilities
|
|
1771
1775
|
*/
|
|
1772
1776
|
|
|
@@ -1914,8 +1918,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
1914
1918
|
* @memberof Utilities */
|
|
1915
1919
|
function saveDataURL(url, filename='download', revokeTime)
|
|
1916
1920
|
{
|
|
1917
|
-
ASSERT(
|
|
1918
|
-
ASSERT(
|
|
1921
|
+
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
1922
|
+
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
1919
1923
|
|
|
1920
1924
|
// create link for saving screenshots
|
|
1921
1925
|
const link = document.createElement('a');
|
|
@@ -1933,8 +1937,8 @@ function saveDataURL(url, filename='download', revokeTime)
|
|
|
1933
1937
|
* @memberof Utilities */
|
|
1934
1938
|
function shareURL(title, url, callback)
|
|
1935
1939
|
{
|
|
1936
|
-
ASSERT(
|
|
1937
|
-
ASSERT(
|
|
1940
|
+
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
1941
|
+
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
1938
1942
|
navigator.share?.({title, url}).then(()=>callback?.());
|
|
1939
1943
|
}
|
|
1940
1944
|
|
|
@@ -1947,7 +1951,7 @@ function shareURL(title, url, callback)
|
|
|
1947
1951
|
* @memberof Utilities */
|
|
1948
1952
|
function readSaveData(saveName, defaultSaveData)
|
|
1949
1953
|
{
|
|
1950
|
-
ASSERT(
|
|
1954
|
+
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
1951
1955
|
|
|
1952
1956
|
// replace undefined values with defaults; tolerate corrupt JSON
|
|
1953
1957
|
const data = localStorage[saveName];
|
|
@@ -1966,8 +1970,50 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
1966
1970
|
* @memberof Utilities */
|
|
1967
1971
|
function writeSaveData(saveName, saveData)
|
|
1968
1972
|
{
|
|
1969
|
-
ASSERT(
|
|
1973
|
+
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
1970
1974
|
localStorage[saveName] = JSON.stringify(saveData);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1978
|
+
|
|
1979
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
1980
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
1981
|
+
function noiseHash(i)
|
|
1982
|
+
{
|
|
1983
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
1984
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
1985
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
1986
|
+
h ^= h >>> 16;
|
|
1987
|
+
return (h >>> 0) / 2**32;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
/** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
|
|
1991
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
1992
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
1993
|
+
* @param {number} x
|
|
1994
|
+
* @return {number}
|
|
1995
|
+
* @memberof Utilities */
|
|
1996
|
+
function noise1D(x)
|
|
1997
|
+
{
|
|
1998
|
+
const i = floor(x);
|
|
1999
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
/** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
2003
|
+
* @param {number} x
|
|
2004
|
+
* @param {number} y
|
|
2005
|
+
* @return {number}
|
|
2006
|
+
* @memberof Utilities */
|
|
2007
|
+
function noise2D(x, y)
|
|
2008
|
+
{
|
|
2009
|
+
const ix = floor(x), iy = floor(y);
|
|
2010
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
2011
|
+
// large prime decorrelates neighboring rows
|
|
2012
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
2013
|
+
return lerp(
|
|
2014
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
2015
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
2016
|
+
fy);
|
|
1971
2017
|
}
|
|
1972
2018
|
/**
|
|
1973
2019
|
* LittleJS Engine Settings
|
|
@@ -3174,6 +3220,12 @@ let textureInfos = [];
|
|
|
3174
3220
|
* @memberof Draw */
|
|
3175
3221
|
let drawCount;
|
|
3176
3222
|
|
|
3223
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
3224
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
3225
|
+
* @type {number}
|
|
3226
|
+
* @memberof Draw */
|
|
3227
|
+
let primitiveCount;
|
|
3228
|
+
|
|
3177
3229
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
3178
3230
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
3179
3231
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
@@ -3414,19 +3466,19 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
3414
3466
|
}
|
|
3415
3467
|
else
|
|
3416
3468
|
{
|
|
3417
|
-
//
|
|
3418
|
-
//
|
|
3419
|
-
//
|
|
3420
|
-
// color.add(additiveColor) on line ~337.
|
|
3469
|
+
// untextured: glDrawUntextured picks the optimal path (poly
|
|
3470
|
+
// tristrip if already in poly mode, otherwise instanced with
|
|
3471
|
+
// uvs/rgba zeroed). Color+additive are folded together to match
|
|
3472
|
+
// the Canvas2D path's color.add(additiveColor) on line ~337.
|
|
3421
3473
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
3422
|
-
|
|
3423
|
-
0, combined.rgbaInt());
|
|
3474
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
3424
3475
|
}
|
|
3425
3476
|
}
|
|
3426
3477
|
else
|
|
3427
3478
|
{
|
|
3428
3479
|
// normal canvas 2D rendering method (slower)
|
|
3429
3480
|
++drawCount;
|
|
3481
|
+
++primitiveCount;
|
|
3430
3482
|
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
3431
3483
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
3432
3484
|
{
|
|
@@ -3466,13 +3518,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
3466
3518
|
* @param {Vector2} pos
|
|
3467
3519
|
* @param {Vector2} [size=vec2(1)]
|
|
3468
3520
|
* @param {Color} [colorTop=WHITE]
|
|
3469
|
-
* @param {Color} [colorBottom=
|
|
3521
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
3470
3522
|
* @param {number} [angle]
|
|
3471
3523
|
* @param {boolean} [useWebGL=glEnable]
|
|
3472
3524
|
* @param {boolean} [screenSpace]
|
|
3473
3525
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3474
3526
|
* @memberof Draw */
|
|
3475
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=
|
|
3527
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3476
3528
|
{
|
|
3477
3529
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3478
3530
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -3512,6 +3564,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3512
3564
|
{
|
|
3513
3565
|
// normal canvas 2D rendering method (slower)
|
|
3514
3566
|
++drawCount;
|
|
3567
|
+
++primitiveCount;
|
|
3515
3568
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
3516
3569
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
3517
3570
|
{
|
|
@@ -3574,8 +3627,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
3574
3627
|
return;
|
|
3575
3628
|
}
|
|
3576
3629
|
|
|
3577
|
-
// Canvas2D path — increment
|
|
3630
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
3578
3631
|
++drawCount;
|
|
3632
|
+
++primitiveCount;
|
|
3579
3633
|
|
|
3580
3634
|
if (!screenSpace)
|
|
3581
3635
|
{
|
|
@@ -3649,6 +3703,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
3649
3703
|
{
|
|
3650
3704
|
// normal canvas 2D rendering method (slower)
|
|
3651
3705
|
++drawCount;
|
|
3706
|
+
++primitiveCount;
|
|
3652
3707
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
3653
3708
|
{
|
|
3654
3709
|
context.strokeStyle = color.toString();
|
|
@@ -3829,6 +3884,77 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
3829
3884
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
3830
3885
|
}
|
|
3831
3886
|
|
|
3887
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
3888
|
+
* - Best when batched with other untextured polys
|
|
3889
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
3890
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
3891
|
+
* @param {Vector2} pos
|
|
3892
|
+
* @param {number} [size=1] - Diameter
|
|
3893
|
+
* @param {Color} [colorInner=WHITE]
|
|
3894
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
3895
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3896
|
+
* @param {boolean} [screenSpace]
|
|
3897
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3898
|
+
* @memberof Draw */
|
|
3899
|
+
let drawCircleGradientOffset = 0;
|
|
3900
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
3901
|
+
{
|
|
3902
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3903
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
3904
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
3905
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3906
|
+
|
|
3907
|
+
if (headlessMode) return;
|
|
3908
|
+
|
|
3909
|
+
if (useWebGL && glEnable)
|
|
3910
|
+
{
|
|
3911
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3912
|
+
if (screenSpace)
|
|
3913
|
+
{
|
|
3914
|
+
// convert to world space
|
|
3915
|
+
pos = screenToWorld(pos);
|
|
3916
|
+
size /= cameraScale;
|
|
3917
|
+
}
|
|
3918
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
3919
|
+
// so back-to-back gradients at the same position have their hole
|
|
3920
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
3921
|
+
// verts and don't visibly stack
|
|
3922
|
+
const sides = glCircleSides;
|
|
3923
|
+
const radius = size/2;
|
|
3924
|
+
const innerInt = colorInner.rgbaInt();
|
|
3925
|
+
const outerInt = colorOuter.rgbaInt();
|
|
3926
|
+
const offset = drawCircleGradientOffset++;
|
|
3927
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
3928
|
+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
|
|
3929
|
+
const colors = [outerInt];
|
|
3930
|
+
for (let i=sides; i--;)
|
|
3931
|
+
{
|
|
3932
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
3933
|
+
points.push(pos);
|
|
3934
|
+
colors.push(innerInt);
|
|
3935
|
+
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
|
|
3936
|
+
colors.push(outerInt);
|
|
3937
|
+
}
|
|
3938
|
+
glDrawColoredPoints(points, colors);
|
|
3939
|
+
}
|
|
3940
|
+
else
|
|
3941
|
+
{
|
|
3942
|
+
// normal canvas 2D rendering method (slower)
|
|
3943
|
+
++drawCount;
|
|
3944
|
+
++primitiveCount;
|
|
3945
|
+
drawCanvas2D(pos, vec2(size), 0, false, (context)=>
|
|
3946
|
+
{
|
|
3947
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
3948
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
3949
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
3950
|
+
context.fillStyle = gradient;
|
|
3951
|
+
context.beginPath();
|
|
3952
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
3953
|
+
context.fill();
|
|
3954
|
+
}, screenSpace, context);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3832
3958
|
/**
|
|
3833
3959
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
3834
3960
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -3912,15 +4038,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
3912
4038
|
* @memberof Draw */
|
|
3913
4039
|
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
3914
4040
|
{
|
|
3915
|
-
ASSERT(
|
|
4041
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
3916
4042
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3917
4043
|
ASSERT(isNumber(size), 'size must be a number');
|
|
3918
4044
|
ASSERT(isColor(color), 'color must be a color');
|
|
3919
4045
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
3920
4046
|
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
3921
4047
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
3922
|
-
ASSERT(
|
|
3923
|
-
ASSERT(
|
|
4048
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
4049
|
+
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
3924
4050
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3925
4051
|
|
|
3926
4052
|
context.fillStyle = color.toString();
|
|
@@ -3957,7 +4083,7 @@ async function loadTexture(textureIndex, src)
|
|
|
3957
4083
|
{
|
|
3958
4084
|
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
3959
4085
|
ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
|
|
3960
|
-
ASSERT(!src ||
|
|
4086
|
+
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
3961
4087
|
|
|
3962
4088
|
const image = new Image;
|
|
3963
4089
|
if (src)
|
|
@@ -4345,7 +4471,7 @@ class FontImage
|
|
|
4345
4471
|
*/
|
|
4346
4472
|
drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
|
|
4347
4473
|
{
|
|
4348
|
-
ASSERT(
|
|
4474
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
4349
4475
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4350
4476
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
4351
4477
|
ASSERT(isColor(color), 'color must be a color');
|
|
@@ -4506,7 +4632,7 @@ function inputClear()
|
|
|
4506
4632
|
* @memberof Input */
|
|
4507
4633
|
function keyIsDown(key, device=0)
|
|
4508
4634
|
{
|
|
4509
|
-
ASSERT(
|
|
4635
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
4510
4636
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4511
4637
|
return !!(inputData[device]?.[key] & 1);
|
|
4512
4638
|
}
|
|
@@ -4518,7 +4644,7 @@ function keyIsDown(key, device=0)
|
|
|
4518
4644
|
* @memberof Input */
|
|
4519
4645
|
function keyWasPressed(key, device=0)
|
|
4520
4646
|
{
|
|
4521
|
-
ASSERT(
|
|
4647
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
4522
4648
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4523
4649
|
return !!(inputData[device]?.[key] & 2);
|
|
4524
4650
|
}
|
|
@@ -4530,7 +4656,7 @@ function keyWasPressed(key, device=0)
|
|
|
4530
4656
|
* @memberof Input */
|
|
4531
4657
|
function keyWasReleased(key, device=0)
|
|
4532
4658
|
{
|
|
4533
|
-
ASSERT(
|
|
4659
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
4534
4660
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
4535
4661
|
return !!(inputData[device]?.[key] & 4);
|
|
4536
4662
|
}
|
|
@@ -4544,10 +4670,10 @@ function keyWasReleased(key, device=0)
|
|
|
4544
4670
|
* @memberof Input */
|
|
4545
4671
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
4546
4672
|
{
|
|
4547
|
-
ASSERT(
|
|
4548
|
-
ASSERT(
|
|
4549
|
-
ASSERT(
|
|
4550
|
-
ASSERT(
|
|
4673
|
+
ASSERT(isStringLike(up), 'up key must be a string');
|
|
4674
|
+
ASSERT(isStringLike(down), 'down key must be a string');
|
|
4675
|
+
ASSERT(isStringLike(left), 'left key must be a string');
|
|
4676
|
+
ASSERT(isStringLike(right), 'right key must be a string');
|
|
4551
4677
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
4552
4678
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
4553
4679
|
}
|
|
@@ -5265,12 +5391,10 @@ function touchGamepadButtonCenter()
|
|
|
5265
5391
|
* @namespace Audio
|
|
5266
5392
|
*/
|
|
5267
5393
|
|
|
5268
|
-
/** Audio context used by the engine
|
|
5269
|
-
* browser autoplay warnings about constructing an AudioContext before any
|
|
5270
|
-
* user gesture.
|
|
5394
|
+
/** Audio context used by the engine
|
|
5271
5395
|
* @type {AudioContext}
|
|
5272
5396
|
* @memberof Audio */
|
|
5273
|
-
let audioContext;
|
|
5397
|
+
let audioContext = new AudioContext;
|
|
5274
5398
|
|
|
5275
5399
|
/** Master gain node for all audio to pass through
|
|
5276
5400
|
* @type {GainNode}
|
|
@@ -5286,13 +5410,12 @@ const audioDefaultSampleRate = 44100;
|
|
|
5286
5410
|
* @return {boolean} - True if the audio context is running
|
|
5287
5411
|
* @memberof Audio */
|
|
5288
5412
|
function audioIsRunning()
|
|
5289
|
-
{ return audioContext
|
|
5413
|
+
{ return audioContext.state === 'running'; }
|
|
5290
5414
|
|
|
5291
5415
|
function audioInit()
|
|
5292
5416
|
{
|
|
5293
5417
|
if (!soundEnable || headlessMode) return;
|
|
5294
5418
|
|
|
5295
|
-
audioContext = new AudioContext;
|
|
5296
5419
|
audioMasterGain = audioContext.createGain();
|
|
5297
5420
|
audioMasterGain.connect(audioContext.destination);
|
|
5298
5421
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -5338,7 +5461,7 @@ class Sound
|
|
|
5338
5461
|
{
|
|
5339
5462
|
if (!soundEnable || headlessMode) return;
|
|
5340
5463
|
|
|
5341
|
-
ASSERT(!asset || isArray(asset) ||
|
|
5464
|
+
ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
|
|
5342
5465
|
ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
|
|
5343
5466
|
ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
|
|
5344
5467
|
ASSERT(isNumber(range), 'range must be a number');
|
|
@@ -5677,14 +5800,15 @@ class SoundInstance
|
|
|
5677
5800
|
|
|
5678
5801
|
/** Speak text with passed in settings
|
|
5679
5802
|
* @param {string} text - The text to speak
|
|
5680
|
-
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
5681
5803
|
* @param {number} [volume] - How much to scale volume by
|
|
5682
5804
|
* @param {number} [rate] - How quickly to speak
|
|
5683
5805
|
* @param {number} [pitch] - How much to change the pitch by
|
|
5806
|
+
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
5684
5807
|
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
5685
5808
|
* @memberof Audio */
|
|
5686
|
-
function speak(text,
|
|
5809
|
+
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
5687
5810
|
{
|
|
5811
|
+
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
5688
5812
|
if (!soundEnable || headlessMode) return;
|
|
5689
5813
|
if (!speechSynthesis) return;
|
|
5690
5814
|
|
|
@@ -7677,7 +7801,8 @@ function glFlush()
|
|
|
7677
7801
|
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
7678
7802
|
else
|
|
7679
7803
|
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
7680
|
-
drawCount
|
|
7804
|
+
++drawCount;
|
|
7805
|
+
primitiveCount += glBatchCount;
|
|
7681
7806
|
glBatchCount = 0;
|
|
7682
7807
|
}
|
|
7683
7808
|
glBatchAdditive = glAdditive;
|
|
@@ -7738,6 +7863,67 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
7738
7863
|
glPositionData[offset++] = angle;
|
|
7739
7864
|
}
|
|
7740
7865
|
|
|
7866
|
+
/** Add an untextured rect to the gl draw list
|
|
7867
|
+
* Picks the optimal path: if already in poly mode, emits a tristrip rect
|
|
7868
|
+
* so it batches with surrounding polys; otherwise uses the instanced path
|
|
7869
|
+
* with uvs and rgba zeroed so the color falls through the additive slot.
|
|
7870
|
+
* @param {number} x
|
|
7871
|
+
* @param {number} y
|
|
7872
|
+
* @param {number} sizeX
|
|
7873
|
+
* @param {number} sizeY
|
|
7874
|
+
* @param {number} angle
|
|
7875
|
+
* @param {number} rgba - color as 32-bit integer
|
|
7876
|
+
* @memberof WebGL */
|
|
7877
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
7878
|
+
{
|
|
7879
|
+
if (glPolyMode)
|
|
7880
|
+
{
|
|
7881
|
+
// batch with surrounding polys as a 4-vertex tristrip rect
|
|
7882
|
+
const vertCount = 6; // 4 corners + 2 degenerate verts
|
|
7883
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
7884
|
+
glFlush();
|
|
7885
|
+
|
|
7886
|
+
// compute rotated corners in world space (matches glDrawPointsTransform rotation)
|
|
7887
|
+
const hx = sizeX*.5, hy = sizeY*.5;
|
|
7888
|
+
const c = cos(angle), s = sin(angle);
|
|
7889
|
+
const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
|
|
7890
|
+
const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
|
|
7891
|
+
const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
|
|
7892
|
+
const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
|
|
7893
|
+
const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
|
|
7894
|
+
|
|
7895
|
+
// write tristrip with leading/trailing degenerate verts
|
|
7896
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
7897
|
+
glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
|
|
7898
|
+
glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
|
|
7899
|
+
glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
|
|
7900
|
+
glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
|
|
7901
|
+
glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
|
|
7902
|
+
glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
|
|
7903
|
+
glBatchCount += vertCount;
|
|
7904
|
+
return;
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
// instanced path: zero uvs and rgba so the texture contribution is killed,
|
|
7908
|
+
// then carry the real color in the additive slot
|
|
7909
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
|
|
7910
|
+
glFlush();
|
|
7911
|
+
glSetInstancedMode();
|
|
7912
|
+
|
|
7913
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
7914
|
+
glPositionData[offset++] = x;
|
|
7915
|
+
glPositionData[offset++] = y;
|
|
7916
|
+
glPositionData[offset++] = sizeX;
|
|
7917
|
+
glPositionData[offset++] = sizeY;
|
|
7918
|
+
glPositionData[offset++] = 0;
|
|
7919
|
+
glPositionData[offset++] = 0;
|
|
7920
|
+
glPositionData[offset++] = 0;
|
|
7921
|
+
glPositionData[offset++] = 0;
|
|
7922
|
+
glColorData[offset++] = 0;
|
|
7923
|
+
glColorData[offset++] = rgba;
|
|
7924
|
+
glPositionData[offset++] = angle;
|
|
7925
|
+
}
|
|
7926
|
+
|
|
7741
7927
|
/** Transform and add a polygon to the gl draw list
|
|
7742
7928
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
7743
7929
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -8374,7 +8560,8 @@ class Medal
|
|
|
8374
8560
|
/** @property {boolean} - Is the medal unlocked? */
|
|
8375
8561
|
this.unlocked = false;
|
|
8376
8562
|
|
|
8377
|
-
|
|
8563
|
+
/** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
|
|
8564
|
+
this.image = undefined;
|
|
8378
8565
|
if (src)
|
|
8379
8566
|
(this.image = new Image).src = src;
|
|
8380
8567
|
|
|
@@ -8537,13 +8724,18 @@ class NewgroundsPlugin
|
|
|
8537
8724
|
ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
|
|
8538
8725
|
|
|
8539
8726
|
newgrounds = this; // set global newgrounds object
|
|
8727
|
+
/** @property {string} - The newgrounds App ID */
|
|
8540
8728
|
this.app_id = app_id;
|
|
8729
|
+
/** @property {string|undefined} - AES-128/Base64 encryption key, if any */
|
|
8541
8730
|
this.cipher = cipher;
|
|
8731
|
+
/** @property {Object|undefined} - CryptoJS instance used when cipher is set */
|
|
8542
8732
|
this.cryptoJS = cryptoJS;
|
|
8733
|
+
/** @property {string} - Hostname used when logging views */
|
|
8543
8734
|
this.host = location ? location.hostname : '';
|
|
8544
8735
|
|
|
8545
8736
|
// get session id from url search params
|
|
8546
8737
|
const url = new URL(location.href);
|
|
8738
|
+
/** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
|
|
8547
8739
|
this.session_id = url.searchParams.get('ngio_session_id');
|
|
8548
8740
|
|
|
8549
8741
|
if (!this.session_id)
|
|
@@ -8551,6 +8743,7 @@ class NewgroundsPlugin
|
|
|
8551
8743
|
|
|
8552
8744
|
// get medals
|
|
8553
8745
|
const medalsResult = this.call('Medal.getList');
|
|
8746
|
+
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
8554
8747
|
this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
|
|
8555
8748
|
debugMedals && LOG(this.medals);
|
|
8556
8749
|
for (const newgroundsMedal of this.medals)
|
|
@@ -8574,6 +8767,7 @@ class NewgroundsPlugin
|
|
|
8574
8767
|
|
|
8575
8768
|
// get scoreboards
|
|
8576
8769
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
8770
|
+
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
8577
8771
|
this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
|
|
8578
8772
|
debugMedals && LOG(this.scoreboards);
|
|
8579
8773
|
|
|
@@ -9097,12 +9291,31 @@ class UISystemPlugin
|
|
|
9097
9291
|
|
|
9098
9292
|
engineAddPlugin(uiUpdate, uiRender);
|
|
9099
9293
|
|
|
9100
|
-
// set object position
|
|
9294
|
+
// set object position based on anchor target (parent box, or canvas for roots),
|
|
9295
|
+
// self-pivot, and localPos offset
|
|
9101
9296
|
function updateTransforms(o)
|
|
9102
9297
|
{
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9298
|
+
let targetPos, targetSize;
|
|
9299
|
+
if (o.parent)
|
|
9300
|
+
{
|
|
9301
|
+
targetPos = o.parent.pos;
|
|
9302
|
+
targetSize = o.parent.size;
|
|
9303
|
+
}
|
|
9304
|
+
else
|
|
9305
|
+
{
|
|
9306
|
+
// anchor to canvas in native coords (handles nativeHeight if set)
|
|
9307
|
+
targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
9308
|
+
targetSize = uiSystem.nativeHeight
|
|
9309
|
+
? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
|
|
9310
|
+
uiSystem.nativeHeight)
|
|
9311
|
+
: mainCanvasSize;
|
|
9312
|
+
}
|
|
9313
|
+
|
|
9314
|
+
const a = o.anchor;
|
|
9315
|
+
o.pos = targetPos
|
|
9316
|
+
.add(targetSize.multiply(a).scale(.5)) // anchor point on target
|
|
9317
|
+
.subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
|
|
9318
|
+
.add(o.localPos); // user offset
|
|
9106
9319
|
}
|
|
9107
9320
|
|
|
9108
9321
|
// setup recursive update and render
|
|
@@ -9569,9 +9782,8 @@ class UISystemPlugin
|
|
|
9569
9782
|
// confirm menu
|
|
9570
9783
|
const confirmMenu = new UIObject(vec2(), size);
|
|
9571
9784
|
uiSystem.confirmDialog = confirmMenu;
|
|
9572
|
-
confirmMenu.onRender = ()=>
|
|
9785
|
+
confirmMenu.onRender = ()=>
|
|
9573
9786
|
{
|
|
9574
|
-
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
9575
9787
|
const backgroundColor = hsl(0,0,0,.7);
|
|
9576
9788
|
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
9577
9789
|
}
|
|
@@ -9704,7 +9916,11 @@ class UIObject
|
|
|
9704
9916
|
this.navigationIndex = undefined;
|
|
9705
9917
|
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
9706
9918
|
this.navigationAutoSelect = false;
|
|
9707
|
-
|
|
9919
|
+
/** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
|
|
9920
|
+
* Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
|
|
9921
|
+
* Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
|
|
9922
|
+
this.anchor = vec2();
|
|
9923
|
+
|
|
9708
9924
|
uiSystem.uiObjects.push(this);
|
|
9709
9925
|
}
|
|
9710
9926
|
|
|
@@ -9957,9 +10173,9 @@ class UIText extends UIObject
|
|
|
9957
10173
|
{
|
|
9958
10174
|
super(pos, size);
|
|
9959
10175
|
|
|
9960
|
-
ASSERT(
|
|
10176
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
9961
10177
|
ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
|
|
9962
|
-
ASSERT(
|
|
10178
|
+
ASSERT(isStringLike(font), 'ui text font must be a string');
|
|
9963
10179
|
|
|
9964
10180
|
// set properties
|
|
9965
10181
|
this.text = text;
|
|
@@ -10007,7 +10223,7 @@ class UITextInput extends UIObject
|
|
|
10007
10223
|
{
|
|
10008
10224
|
super(pos, size);
|
|
10009
10225
|
|
|
10010
|
-
ASSERT(
|
|
10226
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
10011
10227
|
|
|
10012
10228
|
/** @property {number} - Max length of input (0 = no limit) */
|
|
10013
10229
|
this.maxLength = 0;
|
|
@@ -10144,7 +10360,7 @@ class UIButton extends UIObject
|
|
|
10144
10360
|
{
|
|
10145
10361
|
super(pos, size);
|
|
10146
10362
|
|
|
10147
|
-
ASSERT(
|
|
10363
|
+
ASSERT(isStringLike(text), 'ui button must be a string');
|
|
10148
10364
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
10149
10365
|
|
|
10150
10366
|
/** @property {Vector2} - Text offset for the button */
|
|
@@ -10185,7 +10401,7 @@ class UICheckbox extends UIObject
|
|
|
10185
10401
|
{
|
|
10186
10402
|
super(pos, size);
|
|
10187
10403
|
|
|
10188
|
-
ASSERT(
|
|
10404
|
+
ASSERT(isStringLike(text), 'ui checkbox must be a string');
|
|
10189
10405
|
ASSERT(isColor(color), 'ui checkbox color must be a color');
|
|
10190
10406
|
|
|
10191
10407
|
/** @property {boolean} - Current percentage value of this slider 0-1 */
|
|
@@ -10242,7 +10458,7 @@ class UISlider extends UIObject
|
|
|
10242
10458
|
super(pos, size);
|
|
10243
10459
|
|
|
10244
10460
|
ASSERT(isNumber(value), 'ui slider value must be a number');
|
|
10245
|
-
ASSERT(
|
|
10461
|
+
ASSERT(isStringLike(text), 'ui slider must be a string');
|
|
10246
10462
|
ASSERT(isColor(color), 'ui slider color must be a color');
|
|
10247
10463
|
ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
|
|
10248
10464
|
|
|
@@ -10360,7 +10576,7 @@ class UIVideo extends UIObject
|
|
|
10360
10576
|
{
|
|
10361
10577
|
super(pos, size || vec2());
|
|
10362
10578
|
|
|
10363
|
-
ASSERT(
|
|
10579
|
+
ASSERT(isStringLike(src), 'video src must be a string');
|
|
10364
10580
|
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
10365
10581
|
|
|
10366
10582
|
this.color = BLACK; // default to black background
|
|
@@ -12864,13 +13080,21 @@ class Tween
|
|
|
12864
13080
|
}
|
|
12865
13081
|
ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
|
|
12866
13082
|
|
|
13083
|
+
/** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
|
|
12867
13084
|
this.callback = callback;
|
|
13085
|
+
/** @property {number|Vector2|Color} - Starting value */
|
|
12868
13086
|
this.start = start;
|
|
13087
|
+
/** @property {number|Vector2|Color} - Ending value */
|
|
12869
13088
|
this.end = end;
|
|
13089
|
+
/** @property {number} - Total duration in seconds */
|
|
12870
13090
|
this.duration = duration;
|
|
13091
|
+
/** @property {number} - Remaining time in seconds (counts down from duration to 0) */
|
|
12871
13092
|
this.life = duration;
|
|
13093
|
+
/** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
|
|
12872
13094
|
this.ease = options.ease || Ease.LINEAR;
|
|
13095
|
+
/** @property {boolean} - If true, advance even when the game is paused */
|
|
12873
13096
|
this.useRealTime = !!options.useRealTime;
|
|
13097
|
+
/** @property {boolean} - If true, stop advancing until cleared */
|
|
12874
13098
|
this.paused = !!options.paused;
|
|
12875
13099
|
|
|
12876
13100
|
/** @private completion callback set by then(), loop(), pingPong(). */
|
|
@@ -13210,7 +13434,7 @@ const Ease =
|
|
|
13210
13434
|
function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
|
|
13211
13435
|
{
|
|
13212
13436
|
ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
|
|
13213
|
-
ASSERT(
|
|
13437
|
+
ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
|
|
13214
13438
|
|
|
13215
13439
|
const parts = propertyPath.split('.');
|
|
13216
13440
|
const lastKey = parts.pop();
|
|
@@ -13403,7 +13627,9 @@ class PathFinder
|
|
|
13403
13627
|
// .size + .getCollisionData.
|
|
13404
13628
|
if (isVector2(source))
|
|
13405
13629
|
{
|
|
13630
|
+
/** @property {Vector2} - Grid dimensions in tiles */
|
|
13406
13631
|
this.size = source.floor();
|
|
13632
|
+
/** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
|
|
13407
13633
|
this.tileLayer = undefined;
|
|
13408
13634
|
}
|
|
13409
13635
|
else
|
|
@@ -13415,13 +13641,18 @@ class PathFinder
|
|
|
13415
13641
|
}
|
|
13416
13642
|
|
|
13417
13643
|
// Tunables (public, freely re-assignable).
|
|
13644
|
+
/** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
|
|
13418
13645
|
this.heuristicWeight = 1;
|
|
13419
|
-
|
|
13646
|
+
/** @property {number} - Maximum A* expansions before giving up */
|
|
13647
|
+
this.maxLoop = 1e3;
|
|
13648
|
+
/** @property {boolean} - If true, post-process paths with two-pass smoothing */
|
|
13420
13649
|
this.smoothPath = true;
|
|
13650
|
+
/** @property {boolean} - If true, draw debug visualization during findPath */
|
|
13421
13651
|
this.debug = false;
|
|
13422
|
-
|
|
13652
|
+
/** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
|
|
13653
|
+
this.debugTime = 1;
|
|
13423
13654
|
|
|
13424
|
-
|
|
13655
|
+
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
|
|
13425
13656
|
this.nodes = new Array(this.size.x * this.size.y);
|
|
13426
13657
|
for (let y = 0; y < this.size.y; ++y)
|
|
13427
13658
|
for (let x = 0; x < this.size.x; ++x)
|
|
@@ -13595,9 +13826,12 @@ class PathFinder
|
|
|
13595
13826
|
// Best path so far through neighbor — record it.
|
|
13596
13827
|
neighbor.parent = current;
|
|
13597
13828
|
neighbor.g = tentativeG;
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13829
|
+
// Octile heuristic — tightest admissible distance for an
|
|
13830
|
+
// 8-connected grid with cardinal cost 1 and diagonal cost √2.
|
|
13831
|
+
const adx = abs(endNode.pos.x - neighbor.pos.x);
|
|
13832
|
+
const ady = abs(endNode.pos.y - neighbor.pos.y);
|
|
13833
|
+
const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
|
|
13834
|
+
neighbor.f = neighbor.g + h * this.heuristicWeight;
|
|
13601
13835
|
}
|
|
13602
13836
|
}
|
|
13603
13837
|
|
|
@@ -13879,6 +14113,24 @@ class PathFinder
|
|
|
13879
14113
|
path.push(original[original.length - 1]);
|
|
13880
14114
|
}
|
|
13881
14115
|
|
|
14116
|
+
/** Drop any middle node that lies exactly on the line through its two
|
|
14117
|
+
* neighbors. Backstop for the smoothing passes — the corners pass
|
|
14118
|
+
* intentionally keeps truly-straight runs, and the string-pulling pass
|
|
14119
|
+
* checks collinearity against the original path, not the in-progress
|
|
14120
|
+
* result, so it can leave 3+ collinear nodes in some edge cases.
|
|
14121
|
+
* @param {PathFinderNode[]} path
|
|
14122
|
+
* @private */
|
|
14123
|
+
dropCollinearNodes(path)
|
|
14124
|
+
{
|
|
14125
|
+
for (let i = path.length - 2; i >= 1; --i)
|
|
14126
|
+
{
|
|
14127
|
+
const a = path[i - 1], b = path[i], c = path[i + 1];
|
|
14128
|
+
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
|
|
14129
|
+
(b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
|
|
14130
|
+
path.splice(i, 1);
|
|
14131
|
+
}
|
|
14132
|
+
}
|
|
14133
|
+
|
|
13882
14134
|
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
13883
14135
|
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
13884
14136
|
* @param {number} x
|
|
@@ -14046,6 +14298,7 @@ class PathFinder
|
|
|
14046
14298
|
{
|
|
14047
14299
|
this.smoothPathCorners(nodePath);
|
|
14048
14300
|
this.smoothPathStringPull(nodePath);
|
|
14301
|
+
this.dropCollinearNodes(nodePath);
|
|
14049
14302
|
}
|
|
14050
14303
|
|
|
14051
14304
|
// Convert to world-space Vector2 path. Return copies, not live node
|